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

推理结论:

  1. 使用三类 KEY 实现缓存标签数据的存储:一个 KEY 存储缓存标签的序列化值(简称 A),一个 KEY 存储被标记该标签的缓存 KEY 名称(简称 B),一个 KEY 存储具体的缓存内容(简称 C),下面提到这三种 KEY 时均以简称描述;
  2. B 使用 SET 存储,可以存储多个 KEY ,而且 SET 本身支持去重;
  3. B 的 KEY 名称中,有 forever_ref 关键字,应该和设置缓存时,没有设置缓存时间(永久性缓存)有关,有待验证;
  4. C 的 KEY 名称中,除了包含缓存名称,还有另一个哈希值,可能与缓存标签有关,有待验证。

我们可以通过一个小测试,验证下 推论3推论4 的推测是否正确:

// 同时添加两个缓存标签
Cache::tags(['md5_cache', 'md5_cache1'])->put($md5Key1, 'value', 1000);

与此同时,我们在存储缓存的时候添加了一个缓存时间参数,通过 MONITOR 监控发现:

  1. B 的 KEY 名称为:laravel_database_laravel_cache: 6694947599af5874179309: standard_ref,说明 推论3 应该正确;
  2. 本次生成了两个 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\";"

删除操作看着执行的命令挺多,实际上仔细分析一下的话,并不复杂:

  1. 获取缓存标签 A 的值;
  2. 根据缓存标签的值,使用 SMEMBERS 命令分别获取 B 中标记了该缓存标签的永久性(forever_ref)和临时性(standard_ref)缓存 KEY ;
  3. 使用 DEL 命令删除 SMEMBERS 命令获取到的缓存 KEY ;
  4. 使用 DEL 命令删除 B ;
  5. 重置 A 的值(目的暂不明确)。

初步结论

到这里,实际上已经可以看出一部分问题来了:

  1. SMEMBERS 命令的使用。SMEMBERS 命令的时间复杂度为 O(N), N 为集合的基数。 这是个危险的命令,和之前文章提到的 KEYS 命令一样,是个危险的家伙,它可能会阻塞进程;
  2. 使用 DEL 命令删除 KEY ,如果遇到 BIGKEY 的话,会造成阻塞。

问题2 在有 BIGKEY 的情况下需要重点考虑,这里我们重点关注 问题1

按照之前我们提到的场景,md5 类型的无规则 KEY 有八十万之多,这种情况下,使用 SMEMBERS一次性返回八十多万的 KEY ,肯定是一个致命的操作。

那事实是不是真的和我们预期的一致呢?

我们分别以 10,000 的基数和 100,000 的基数进行测试,看 Redis 表现是否和预期一致。

经过测试,得到的结论如下:

  1. 基数为10,000100,000 时,均使用了 SMEMBERS 命令;
  2. 基数为 10,000 时,SMEMBERS 耗时为 8.4 毫秒。基数为 100,000 时,SMEMBERS 耗时为 652 毫秒;
  3. 当缓存标签下有多个缓存时,使用 DEL 命令删除缓存时做了优化。并非一次性全部删除,而是分批次进行删除,每次删除的 KEY 的数量为 1000 个。

通过测试能够发现,当使用缓存标签标记的缓存数量过大时,确实会产生性能问题。

不过,这就是最终结论了吗?难道 Laravel 还有这么大的漏洞???

别着急,我们刚刚使用的比较老的 Laravel 版本(6.20.44)进行的测试,让我们再用最新的版本试试表现如何。

限于篇幅问题,我们将在 《Laravel 实用小技巧——缓存标签的小秘密(下)》一文中,接着讨论这个问题。

感谢您的持续关注~

本作品采用《CC 协议》,转载必须注明作者和本文链接
你应该了解真相,真相会让你自由。
本帖由系统于 1个月前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 6

很好,很棒。

1个月前 评论

:+1: :+1:

1个月前 评论

:heart:等不及了,我决定自己先去试。

1个月前 评论
快乐的皮拉夫 (楼主) 1个月前
快乐的皮拉夫 (楼主) 1个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
文章
40
粉丝
117
喜欢
698
收藏
751
排名:255
访问:3.8 万
私信
所有博文
社区赞助商