Laravel中的cache:tags()批量删除缓存前缀是怎么实现的?

序言

我们知道 Laravel 中可以通过 cache::tag("xxx")->flush() 实现批量删除。但是删除的底层命令是啥(这里以redis为缓存驱动)? 是会造成堵塞的keys命令, 还是scan命令又或者就是普通的del命令?
不研究源码的可以直接到最下面看总结

底层代码分析

一、设置tag缓存底层代码分析

1) 设置带有tag的缓存代码如下:

Cache::tags('foo')->put('bar', 'value');

2) 通过编辑器的全局搜索我们找到tags方法代码,如下:

 /**
 * Begin executing a new tags operation.
 *
 * @param array|mixed $names
 * @return \Illuminate\Cache\RedisTaggedCache
 */
 public function tags($names)
 {
 return new RedisTaggedCache(
 $this, new TagSet($this, is_array($names) ? $names : func_get_args())
 );
 }

我们先看TagSet类, TagSet类构造函数如下, 我们可以看到在TagSet这个类中设置2了个成员变量

 /**
 * Create a new TagSet instance.
 *
 * @param \Illuminate\Contracts\Cache\Store $store
 * @param array $names
 * @return void
 */
 public function __construct(Store $store, array $names = [])
 {
 $this->store = $store;
 $this->names = $names;
 }

我们再回过头来看RedisTaggedCache类的构造函数, RedisTaggedCache类并没有自己的构造函数,但是他继承了TaggedCache类, TaggedCache类构造函数如下, 这里先执行TaggedCache父类构造方法,再来设置了tags变量. 我们再来看看TaggedCache的父类的构造方法, 如下

 /**
 * Create a new tagged cache instance.
 *
 * @param \Illuminate\Contracts\Cache\Store $store
 * @param \Illuminate\Cache\TagSet $tags
 * @return void
 */
 public function __construct(Store $store, TagSet $tags)
 {
 parent::__construct($store);
 $this->tags = $tags;
 }
 /**
 * Create a new cache repository instance.
 *
 * @param \Illuminate\Contracts\Cache\Store $store
 * @return void
 */
 public function __construct(Store $store)
 {
 $this->store = $store;
 }

这里我们可以看到, 都只是设置成员变量, 并没有redis相关操作.

3) 那我们再来看put方法的代码. 因为Cache::tags()返回的是RedisTaggedCache类的对象,我们直接在这个类找, put()方法底层代码如下

 /**
 * Store an item in the cache.
 *
 * @param string $key
 * @param mixed $value
 * @param \DateTimeInterface|\DateInterval|int|null $ttl
 * @return bool
 */
 public function put($key, $value, $ttl = null)
 {
 if ($ttl === null) {
 return $this->forever($key, $value);
 }
 $this->pushStandardKeys($this->tags->getNamespace(), $key);
 return parent::put($key, $value, $ttl);
 }

这里我们就以 $ttl = null 这个条件来分析这个方法, 我们直接看forever的方法,代码如下

 /**
 * Store an item in the cache indefinitely.
 *
 * @param string $key
 * @param mixed $value
 * @return bool
 */
 public function forever($key, $value)
 {
 $this->pushForeverKeys($this->tags->getNamespace(), $key);
 return parent::forever($key, $value);
 }

继续跟踪getNamespace()方法, 代码如下

 /**
 * Get a unique namespace that changes when any of the tags are flushed.
 *
 * @return string
 */
 public function getNamespace()
 {
 return implode('|', $this->tagIds());
 }
 /**
 * Get an array of tag identifiers for all of the tags in the set.
 *
 * @return array
 */
 protected function tagIds()
 {
 return array_map([$this, 'tagId'], $this->names);
 }
 

getNamespace()方法会去调用tagIds()方法, tagIds()方法通过array_map()把names数组中的每一个成员调用tagId方法. 在本文中names就是["foo"]数组.

我们再来看tagId方法,代码如下

/**
 * Get the unique tag identifier for a given tag.
 *
 * @param string $name
 * @return string
 */
 public function tagId($name)
 {
 return $this->store->get($this->tagKey($name)) ?: $this->resetTag($name);
 }
 /**
 * Get the tag identifier key for a given tag.
 *
 * @param string $name
 * @return string
 */
 public function tagKey($name)
 {
 return 'tag:'.$name.':key';
 }

这里首先会去判断这个tag key有没有被设置, 如果被设置了直接返回这个tag key的值. 如果没有设置,再调用resetTag()方法. resetTag()方法代码如下

 /**
 * Reset the tag and return the new tag identifier.
 *
 * @param string $name
 * @return string
 */
 public function resetTag($name)
 {
 $this->store->forever($this->tagKey($name), $id = str_replace('.', '', uniqid('', true)));
 return $id;
 }

这里先通过php自带的uniqid函数生成唯一Id, 再通过forever函数设置缓存,最终在redis显示如下, 最终getNamespace()方法返回的就是是这个uniqid函数生成的值

127.0.0.1:6379[8]> get xxx_cache:tag:foo:key
"s:22:\"61f213827059d213700903\";"

搞清楚getNamespace()方法之后, 我们再回过头进入pushForeverKeys()方法中, 代码如下

 /**
 * Store forever key references into store.
 *
 * @param string $namespace
 * @param string $key
 * @return void
 */
 protected function pushForeverKeys($namespace, $key)
 {
 $this->pushKeys($namespace, $key, self::REFERENCE_KEY_FOREVER);
 }

继续进入到pushKeys方法中,代码如下

 /**
 * Store a reference to the cache key against the reference key.
 *
 * @param string $namespace
 * @param string $key
 * @param string $reference
 * @return void
 */
 protected function pushKeys($namespace, $key, $reference)
 {
 $fullKey = $this->store->getPrefix().sha1($namespace).':'.$key;
 foreach (explode('|', $namespace) as $segment) {
 $this->store->connection()->sadd($this->referenceKey($segment, $reference), $fullKey);
 }
 }

最终我们可以看到, 上面通过uniqid返回的值再拼接一个固定值当做redis set集合中的key, 集合中的成员为: 项目前缀 + sha1值 + key,最终在redis中显示如下

127.0.0.1:6379[8]> smembers cache:61f213827059d213700903:forever_ref
1) "xxx_cache:9af69b7ac2b2a27d0ab2666b3e21204d878885d3:bar"

到这里pushForeverKeys()方法就执行完毕, 最后再执行父类的forever方法设置缓存.

 /**
 * Store an item in the cache indefinitely.
 *
 * @param string $key
 * @param mixed $value
 * @return bool
 */
 public function forever($key, $value)
 {
 $this->pushForeverKeys($this->tags->getNamespace(), $key);
 return parent::forever($key, $value);
 }

到这里Cache::tags('foo')->put('bar', 'value')就执行完毕了

搞清楚cache::tag是如何设置缓存的,那么cache::tag()->flush()批量删除缓存也就不难了.

二、批量删除tag缓存底层代码分析

1) 批量删除tag缓存代码如下

 Cache::tags('foo')->flush();

2) Cache::tags('foo')方法我们在上面已经分析过了, 返回的是RedisTaggedCache类的对象, 直接找到flush方法代码

 /**
 * Remove all items from the cache.
 *
 * @return bool
 */
 public function flush()
 {
 $this->deleteForeverKeys();
 $this->deleteStandardKeys();
 return parent::flush();
 }

这里我们只需要看deleteForeverKeys()即可, 因为这2个调用的方法是一样的,只是传入的参数不一样,代码如下

 /**
 * Delete all of the items that were stored forever.
 *
 * @return void
 */
 protected function deleteForeverKeys()
 {
 $this->deleteKeysByReference(self::REFERENCE_KEY_FOREVER);
 }
 /**
 * Delete all standard items.
 *
 * @return void
 */
 protected function deleteStandardKeys()
 {
 $this->deleteKeysByReference(self::REFERENCE_KEY_STANDARD);
 }

我们继续进入到deleteKeysByReference()方法中

 /**
 * Find and delete all of the items that were stored against a reference.
 *
 * @param string $reference
 * @return void
 */
 protected function deleteKeysByReference($reference)
 {
 foreach (explode('|', $this->tags->getNamespace()) as $segment) {
 $this->deleteValues($segment = $this->referenceKey($segment, $reference));
 $this->store->connection()->del($segment);
 }
 }

继续进入到deleteValues()方法中

 /**
 * Delete item keys that have been stored against a reference.
 *
 * @param string $referenceKey
 * @return void
 */
 protected function deleteValues($referenceKey)
 {
 $values = array_unique($this->store->connection()->smembers($referenceKey));
 if (count($values) > 0) {
 foreach (array_chunk($values, 1000) as $valuesChunk) {
 call_user_func_array([$this->store->connection(), 'del'], $valuesChunk);
 }
 }
 }

从上面的代码我们可以看到, 先是用 $this->deleteValues() 删除这个tag集合中每个成员的缓存, 再用$this->store->connection()->del()删除这个tag集合.

这里我们需要注意,在上面的flush()方法最后一行代码,调用了父类的flush方法, 父类的flush方法代码如下:

 /**
 * Remove all items from the cache.
 *
 * @return bool
 */
 public function flush()
 {
 $this->tags->reset();
 return true;
 }

继续进入到reset()方法中

 /**
 * Reset all tags in the set.
 *
 * @return void
 */
 public function reset()
 {
 array_walk($this->names, [$this, 'resetTag']);
 }
 /**
 * Reset the tag and return the new tag identifier.
 *
 * @param string $name
 * @return string
 */
 public function resetTag($name)
 {
 $this->store->forever($this->tagKey($name), $id = str_replace('.', '', uniqid('', true)));
 return $id;
 }

从上面代码中可以看到, 会重新设置tag的key值,如下

之前:

127.0.0.1:6379[8]> get xxx_cache:tag:foo:key
"s:22:\"61f213827059d213700903\";"

现在:

127.0.0.1:6379[8]> get xxx_cache:tag:foo:key
"s:22:\"62032a0620195884559057\";

三、总结

laravel中带tag设置缓存代码流程如下:

这里以 Cache::tags('foo')->put('bar', 'value')为例

1) 先设置tag缓存, key为自己定义的tag(本文就是foo), 值为uniqid()函数生成的值

127.0.0.1:6379[8]> get xxx_cache:tag:foo:key
"s:22:\"61f213827059d213700903\";"

2) 通过上面的uniqid值, 找到这个uniqid值对应的集合(没有则创建集合),

127.0.0.1:6379[8]> type xxx_cache:61f213827059d213700903:forever_ref
set

3)把要设置的key值(本文就是bar)加入到上面的这个集合(方便后续的批量删除)

127.0.0.1:6379[8]> smembers xxx_cache:61f213827059d213700903:forever_ref
1) "xxx_cache:9af69b7ac2b2a27d0ab2666b3e21204d878885d3:bar"

4) 再设置key缓存

laravel中带tag批量删除缓存代码流程如下:

1) 先删除tag集合中每个成员的缓存

2) 再删除这个tag集合

作者:Rachel原文地址:https://segmentfault.com/a/1190000041377388

%s 个评论

要回复文章请先登录注册