Laravel 实用小技巧——缓存标签的小秘密(上)
背景
在《Redis 实用小技巧——线上不让用 KEYS 命令,怎么搞?》一文中,有小伙伴留言,建议使用《Laravel 缓存标签》功能来批量删除缓存。
《Laravel 缓存标签》功能之前有了解过,但是在最近的项目中没有实际使用过,于是我又看了一遍文档,复习了一遍它的使用方法。
一遍操作下来,发现确实可以使用《Laravel 缓存标签》替代我之前提到的应用场景。
缓存标签实现方案
缓存标签用起来比较简单,你可以理解为将缓存通过标签的方式进行归类。它主要包含了以下几个方法:
Cache::tags(['people', 'artists'])->put('John', $john, $seconds); //为缓存添加多个缓存标签
$john = Cache::tags(['people', 'artists'])->get('John'); //访问标记了缓存标签的缓存
Cache::tags(['people', 'authors'])->flush(); //按多个缓存标签删除缓存数据
Cache::tags('authors')->flush(); //按单个缓存标签删除缓存数据
之前的场景,我们可以使用如下的方式进行替代:
// 存储缓存
$md5Key1 = 'c4ca4238a0b923820dcc509a6f75849b';
$md5Key2 = 'c81e728d9d4c2f636f067f89cc14862c';
Cache::tags(['md5_cache'])->put($md5Key1, 'value');
Cache::tags(['md5_cache'])->put($md5Key2, 'value');
// 删除缓存
Cache::tags('md5_cache')->flush();
Done。就是这么简单。
完美方案?
这里使用缓存标签,看上去确实解决了我们的问题:通过缓存标签对某种类型的缓存进行集中管理,当需要对该类缓存进行集中删除时,只需要针对缓存标签进行统一清理即可。
不过,这种方式真的就一劳永逸了么?
对于新技术的使用,我一向比较谨慎。在我看来,优雅的代码仅仅是让开发效率得以提升。而优秀的代码,在性能方面,必须能够经得住考验。
于是,我打算从两个方面,进一步看看缓存标签这样用有没有什么问题。
一方面,因为这是由 Redis 缓存清理引发的问题,所以,我需要看一下,使用缓存标签以后,Redis 的表现如何。
另一方面,我需要进一步研究下,缓存标签的底层实现逻辑是否可靠。
关于缓存标签底层逻辑的实现,我打算后续单独拿出来分析。本篇文章,我们先来个简单点的,单单从 Redis 命令的执行过程,逆向分析下缓存标签的使用原理,以及可能存在的问题。
提示:
需要提前说明,我们仅仅是从 Redis 命令的表现,反向推理缓存标签可能的实现逻辑。具体的实现,需要通过研究其底层实现代码加以验证。
推理过程也很简单,通过 Redis 的 MONITOR
命令监控即可。
OK,一切准备就绪,发车~
Redis 表现
说明:
我们先使用低版本的 Laravel(6.20.44)进行测试,因为这是我们项目中用的比较多的版本。另外,为了进行对比,我还会使用最新版本的 Laravel(11.13.0)进行测试。
存储过程
首先,我们触发一条简单的带有缓存标签的缓存存储逻辑:
$md5Key1 = 'c4ca4238a0b923820dcc509a6f75849b';
Cache::tags(['md5_cache'])->put($md5Key1, 'value');
使用 Redis 的 MONITOR
命令监控如下:
1721012500.246310 [0 127.0.0.1:41168] "SELECT" "1"
1721012500.246592 [1 127.0.0.1:41168] "GET" "laravel_database_laravel_cache:tag:md5_cache:key"
1721012500.246933 [1 127.0.0.1:41168] "SET" "laravel_database_laravel_cache:tag:md5_cache:key" "s:22:\"669491143c3b9110447653\";"
1721012500.247926 [1 127.0.0.1:41168] "SADD" "laravel_database_laravel_cache:669491143c3b9110447653:forever_ref" "laravel_cache:e3bcc880c7090c3d28cec29cfa43354dc03a9a0b:c4ca4238a0b923820dcc509a6f75849b"
1721012500.248299 [1 127.0.0.1:41168] "GET" "laravel_database_laravel_cache:tag:md5_cache:key"
1721012500.248577 [1 127.0.0.1:41168] "SET" "laravel_database_laravel_cache:e3bcc880c7090c3d28cec29cfa43354dc03a9a0b:c4ca4238a0b923820dcc509a6f75849b" "s:5:\"value\";"
从 Redis 操作命令可以看出,这里主要用到了三个 Key ,我们「大胆」猜测一下各自的作用,然后再根据后续的表现加以验证:
KEY 名称 | KEY 类型 | KEY 值 | 作用 | 简称 |
---|---|---|---|---|
laravel_database_laravel_cache: tag: md5_cache: key | STRING | s: 22: “669491143c3b9110447653”; | 存储缓存标签序列化标识 | A |
laravel_database_laravel_cache: 669491143c3b9110447653: forever_ref | SET | laravel_cache: e3bcc880c7090c3d28cec29cfa43354dc03a9a0b: c4ca4238a0b923820dcc509a6f75849b | 存储标记了缓存标签的缓存 KEY 名称 | B |
laravel_database_laravel_cache: e3bcc880c7090c3d28cec29cfa43354dc03a9a0b: c4ca4238a0b923820dcc509a6f75849b | STRING | s: 5: “value”; | 存储具体的缓存内容 | C |
推理结论:
- 使用三类 KEY 实现缓存标签数据的存储:一个 KEY 存储缓存标签的序列化值(简称 A),一个 KEY 存储被标记该标签的缓存 KEY 名称(简称 B),一个 KEY 存储具体的缓存内容(简称 C),下面提到这三种 KEY 时均以简称描述;
- B 使用 SET 存储,可以存储多个 KEY ,而且 SET 本身支持去重;
- B 的 KEY 名称中,有
forever_ref
关键字,应该和设置缓存时,没有设置缓存时间(永久性缓存)有关,有待验证; - C 的 KEY 名称中,除了包含缓存名称,还有另一个哈希值,可能与缓存标签有关,有待验证。
我们可以通过一个小测试,验证下 推论3
和 推论4
的推测是否正确:
// 同时添加两个缓存标签
Cache::tags(['md5_cache', 'md5_cache1'])->put($md5Key1, 'value', 1000);
与此同时,我们在存储缓存的时候添加了一个缓存时间参数,通过 MONITOR
监控发现:
- B 的 KEY 名称为:
laravel_database_laravel_cache: 6694947599af5874179309: standard_ref
,说明推论3
应该正确; - 本次生成了两个 B 的 KEY,每个 B 的 KEY 对应一个 C 的 KEY。由此可以推断,B 和 C 的关系是一对多的关系,说明
推论4
应该正确。
提示:
换言之,通过缓存标签设置的缓存,也必须通过缓存标签来获取。因为缓存中包含缓存标签的哈希值,所以,必须通过缓存标签来获取缓存的内容。
读取过程
读取带有缓存标签的缓存逻辑如下:
$md5Key1 = 'c4ca4238a0b923820dcc509a6f75849b';
$value = Cache::tags(['md5_cache'])->get($md5Key1);
使用 Redis 的 MONITOR
命令监控如下:
1721013156.419214 [0 127.0.0.1:38906] "SELECT" "1"
1721013156.419966 [1 127.0.0.1:38906] "GET" "laravel_database_laravel_cache:tag:md5_cache:key"
1721013156.420250 [1 127.0.0.1:38906] "GET" "laravel_database_laravel_cache:e3bcc880c7090c3d28cec29cfa43354dc03a9a0b:c4ca4238a0b923820dcc509a6f75849b"
读取逻辑比较容易理解,先获取缓存标签对应的序列号,然后通过缓存 KEY 直接获取缓存内容。
前面提到,缓存 KEY 与缓存标签和缓存名称有关,由此可以推断,这里的缓存标签的哈希应该是由缓存标签的序列号,经过某个哈希算法生成的。至于为什么不直接使用序列号作为缓存标签的唯一标识,后续可以通过研究底层代码进一步确认。
删除过程
删除带有缓存标签的缓存逻辑如下:
Cache::tags('md5_cache')->flush();
使用 Redis 的 MONITOR
命令监控如下:
1721013365.626877 [0 127.0.0.1:57336] "SELECT" "1"
1721013365.627070 [1 127.0.0.1:57336] "GET" "laravel_database_laravel_cache:tag:md5_cache:key"
1721013365.627259 [1 127.0.0.1:57336] "SMEMBERS" "laravel_database_laravel_cache:669491143c3b9110447653:forever_ref"
1721013365.628368 [1 127.0.0.1:57336] "DEL" "laravel_database_laravel_cache:e3bcc880c7090c3d28cec29cfa43354dc03a9a0b:c4ca4238a0b923820dcc509a6f75849b"
1721013365.628600 [1 127.0.0.1:57336] "DEL" "laravel_database_laravel_cache:669491143c3b9110447653:forever_ref"
1721013365.628878 [1 127.0.0.1:57336] "GET" "laravel_database_laravel_cache:tag:md5_cache:key"
1721013365.629115 [1 127.0.0.1:57336] "SMEMBERS" "laravel_database_laravel_cache:669491143c3b9110447653:standard_ref"
1721013365.629387 [1 127.0.0.1:57336] "DEL" "laravel_database_laravel_cache:669491143c3b9110447653:standard_ref"
1721013365.629630 [1 127.0.0.1:57336] "SET" "laravel_database_laravel_cache:tag:md5_cache:key" "s:22:\"6694947599af5874179309\";"
删除操作看着执行的命令挺多,实际上仔细分析一下的话,并不复杂:
- 获取缓存标签 A 的值;
- 根据缓存标签的值,使用
SMEMBERS
命令分别获取 B 中标记了该缓存标签的永久性(forever_ref
)和临时性(standard_ref
)缓存 KEY ; - 使用
DEL
命令删除SMEMBERS
命令获取到的缓存 KEY ; - 使用
DEL
命令删除 B ; - 重置 A 的值(目的暂不明确)。
初步结论
到这里,实际上已经可以看出一部分问题来了:
SMEMBERS
命令的使用。SMEMBERS
命令的时间复杂度为 O(N), N 为集合的基数。 这是个危险的命令,和之前文章提到的KEYS
命令一样,是个危险的家伙,它可能会阻塞进程;- 使用
DEL
命令删除 KEY ,如果遇到 BIGKEY 的话,会造成阻塞。
问题2
在有 BIGKEY 的情况下需要重点考虑,这里我们重点关注问题1
。
按照之前我们提到的场景,md5
类型的无规则 KEY 有八十万之多,这种情况下,使用 SMEMBERS
一次性返回八十多万的 KEY ,肯定是一个致命的操作。
那事实是不是真的和我们预期的一致呢?
我们分别以 10,000
的基数和 100,000
的基数进行测试,看 Redis 表现是否和预期一致。
经过测试,得到的结论如下:
- 基数为
10,000
和100,000
时,均使用了SMEMBERS
命令; - 基数为
10,000
时,SMEMBERS
耗时为 8.4 毫秒。基数为100,000
时,SMEMBERS
耗时为 652 毫秒; - 当缓存标签下有多个缓存时,使用
DEL
命令删除缓存时做了优化。并非一次性全部删除,而是分批次进行删除,每次删除的 KEY 的数量为 1000 个。
通过测试能够发现,当使用缓存标签标记的缓存数量过大时,确实会产生性能问题。
不过,这就是最终结论了吗?难道 Laravel 还有这么大的漏洞???
别着急,我们刚刚使用的比较老的 Laravel 版本(6.20.44)进行的测试,让我们再用最新的版本试试表现如何。
限于篇幅问题,我们将在 《Laravel 实用小技巧——缓存标签的小秘密(下)》一文中,接着讨论这个问题。
感谢您的持续关注~
本作品采用《CC 协议》,转载必须注明作者和本文链接
:+1:
很好,很棒。
:+1: :+1:
:heart:等不及了,我决定自己先去试。