Laravel 实用小技巧——缓存标签的小秘密(下)

背景

在上篇文章《Laravel 实用小技巧——缓存标签的小秘密(上)》中,我们重点看了 Laravel 6 版本下,有关缓存标签在 Redis 方面的表现。本篇文章,我们来看一下 Laravel 11(11.13.0)版本下表现如何。

不知道是编排失误还是有意为之,Laravel 11 的官方网站中,有关缓存标签的部分居然删除了!!!不过代码中依旧可以正常使用,所以这并不会影响我们的测试。

Redis 表现

存储过程

首先,我们触发一条简单的带有缓存标签的缓存存储逻辑:

$md5Key = 'c4ca4238a0b923820dcc509a6f75849b';
Cache::tags(['md5_cache'])->put($md5Key, 'value');

Redis 的 MONITOR 命令监控如下:

1721030328.697946 [0 172.19.0.12:51492] "SELECT" "1"
1721030328.698334 [1 172.19.0.12:51492] "ZADD" "_database_tag:md5_cache:entries" "-1" "290cb8745b1f0ddb9afaaf05ba984c48288ffd1f:c4ca4238a0b923820dcc509a6f75849b"
1721030328.701448 [1 172.19.0.12:51492] "SET" "_database_290cb8745b1f0ddb9afaaf05ba984c48288ffd1f:c4ca4238a0b923820dcc509a6f75849b" "s:5:\"value\";"

从 Redis 操作命令可以看出,这里主要用到了两个 Key ,我们「大胆」猜测一下各自的作用,然后再根据后续的表现加以验证:

KEY 名称 KEY 类型 KEY 值 作用 简称
_database_tag: md5_cache:entries ZSET 290cb8745b1f0ddb9afaaf05ba984c48288ffd1f: c4ca4238a0b923820dcc509a6f75849b 缓存标签数据结构 A
_database_290cb8745b1f0ddb9afaaf05ba984c48288ffd1f: c4ca4238a0b923820dcc509a6f75849b STRING s: 5: “value”; 存储标记了缓存标签的缓存内容 B

推理结论:

  1. 通过对比发现,Laravel 11 和 Laravel 6,在缓存标签的存储结构设计上,有着较大的不同。Laravel 11 仅通过一个 ZSET 结构,就存储了缓存标签的名称和缓存的 KEY;
  2. Laravel 11 使用 ZSET 的分值记录缓存对应的过期时间。同一份缓存数据,在 Laravel 6 版本中,会维护两份缓存标签数据(如果永久性缓存和临时性缓存同时设置的话),而在 Laravel 11 中仅维护一份数据,缓存标签中缓存的过期性通过分值区分。在 Laravel 6 中,需要在两份 SET 数据中获取,而在 Laravel 11 中,仅需要在一个 ZSET 数据中存储一份数据即可。如果需要筛选未设置过期的缓存 KEY,可以通过分值进行筛选;
  3. 与 Laravel 6 相同,Laravel 11 设置了多个缓存标签的同一份缓存数据,也会维护多个缓存 KEY。

我们可以进一步测试下:

$md5Key = 'c4ca4238a0b923820dcc509a6f75849b';
Cache::tags(['md5_cache'])->put($md5Key, 'value', 1000);
Cache::tags(['md5_cache'])->put($md5Key, 'value');

Redis 的 MONITOR 命令监控如下:

1721471422.872381 [1 127.0.0.1:40410] "ZADD" "_database_tag:md5_cache:entries" "1721472422" "290cb8745b1f0ddb9afaaf05ba984c48288ffd1f:c4ca4238a0b923820dcc509a6f75849b"
1721471422.876893 [1 127.0.0.1:40410] "SETEX" "_database_290cb8745b1f0ddb9afaaf05ba984c48288ffd1f:c4ca4238a0b923820dcc509a6f75849b" "1000" "s:5:\"value\";"
1721471422.878603 [1 127.0.0.1:40410] "ZADD" "_database_tag:md5_cache:entries" "-1" "290cb8745b1f0ddb9afaaf05ba984c48288ffd1f:c4ca4238a0b923820dcc509a6f75849b"
1721471422.878882 [1 127.0.0.1:40410] "SET" "_database_290cb8745b1f0ddb9afaaf05ba984c48288ffd1f:c4ca4238a0b923820dcc509a6f75849b" "s:5:\"value\";"

从 Redis 的命令可以看出,当同一个缓存 KEY 分别设置不同的过期时,ZSET 结构和 STRING 结构都是以覆盖的形式进行更新,这与 Laravel 6 的逻辑有所不同。

读取过程

读取带有缓存标签的缓存逻辑如下:

$md5Key = 'c4ca4238a0b923820dcc509a6f75849b';
$value = Cache::tags(['md5_cache'])->get($md5Key);

Redis 的 MONITOR 命令监控如下:

1721466729.401411 [0 127.0.0.1:38464] "SELECT" "1"
1721466729.402736 [1 127.0.0.1:38464] "GET" "_database_290cb8745b1f0ddb9afaaf05ba984c48288ffd1f:c4ca4238a0b923820dcc509a6f75849b"

Laravel 11 的缓存标签读取逻辑,相较于 Laravel 6,也做了调整。

现在少了一步从缓存标签 KEY 中读取缓存 KEY 的逻辑,这里直接可以得到缓存 KEY。缓存 KEY 中缓存标签部分应该是通过 哈希算法(缓存标签名称) 得到的。这比 Laravel 6 简洁了不少。

删除过程

删除带有缓存标签的缓存逻辑如下:

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

Redis 的 MONITOR 命令监控如下:

1721472120.371575 [1 127.0.0.1:33344] "ZSCAN" "_database_tag:md5_cache:entries" "0" "COUNT" "1000" "MATCH" "*"
1721472120.373301 [1 127.0.0.1:33344] "DEL" "_database_290cb8745b1f0ddb9afaaf05ba984c48288ffd1f:c4ca4238a0b923820dcc509a6f75849b"
1721472120.373597 [1 127.0.0.1:33344] "DEL" "_database_tag:md5_cache:entries"

通过监控命令发现,删除缓存标签的逻辑,相较于 Laravel 6 做了很大程度的优化:

  1. 使用 ZSCAN 命令进行迭代,性能比 SMEMBERS 更优,虽说是全量扫描,但是采用游标迭代的方式进行获取数据,不会阻塞 Redis 线程;
  2. 其他删除逻辑与 Laravel 6 并无大的差异;
  3. 经过测试发现,当缓存标签标记的缓存数量超过 1000 个时,在删除缓存标签时,会自动进行分组,每 1000 个缓存 KEY 执行一次 DEL 命令,这个 Laravel 6 的逻辑一样。

总结

我们通过 Laravel 6 和 Laravel 11 分别测试了缓存标签在 Redis 操作方面的表现,基本可以得到以下结论:

  1. Laravel 6 的缓存标签因为使用了 SMEMBERS 命令进行删除缓存标签,所以会存在阻塞 Redis 的隐患;
  2. Laravel 11 缓存标签在 Redis 数据结构设计方面有较大改进,结构更精简,而且删除缓存时,使用的是 ZSCAN 命令,不会阻塞 Redis ,同时,由于结构精简,操作 Redis 的命令也少了很多;
  3. 尽管缓存标签在分类管理缓存方面,设计得比较出色,但不得不说,如果放在之前提到的业务场景中(批量删除八十万无规则 KEY ),还是有些欠妥。为什么呢?因为这可能会导致 ZSET 结构发展成一个 BIGKEY 。

当然,以上结论仅仅是我们通过 Redis 的表现反向推测出来的,如果想了解更多底层细节的话,我们还需要去研究框架的底层代码,这将是我下一步的工作。

温馨提醒:
新技术的使用一定要慎重,要稳中求进,这不是保守,而是对线上业务负责的一种工作态度。另外,发现框架问题时,大家从多个角度去分析下问题,比如看看新版本的实现逻辑是否一致。因为框架本身也处于一个不断迭代升级的过程中,我们需要以发展的眼光来看待这些问题。

解决问题永远比抱怨问题更有意义。

感谢大家的持续关注~

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

来了来了,对的,后来我也直接试了试,发现用scan代替了 smembers ,其实当时就在想为什么不用scan。因为不会堵塞啊。但其实现在很多实际生产环境也不会用到lara11,我们单位一般就是9和10,其实这也已经相当激进了。

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

棒棒的。

1个月前 评论

有點不清楚
get 資料時能直接計算出 key 值,那為什麼還要做 zadd 存 key

1721466729.401411 [0 127.0.0.1:38464] "SELECT" "1"
1721466729.402736 [1 127.0.0.1:38464] "GET" "_database_290cb8745b1f0ddb9afaaf05ba984c48288ffd1f:c4ca4238a0b923820dcc509a6f75849b"
1个月前 评论
快乐的皮拉夫 (楼主) 1个月前
DotO (作者) 1个月前
leo

因为 cache tag 的实现在并发场景下有 bug,而 Taylor 认为现在的 cache tag 实现太复杂以至于无法维护,因此隐藏了 L11 文档中这部分的内容。

参考:


顺便打个招聘广告,我们长期招资深 PHP 开发

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

已看完,求更新!

22小时前 评论

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