必须在redis侧用lua脚本原子比对:脚本内用redis.call('get', keys[1])读redis版本,与客户端传入的argv[1](本地缓存版本)直接比较并返回布尔结果,避免应用层比对引发的竞态问题。

本地缓存版本号和Redis版本号怎么比才不漏判
直接用 GET 拿 Redis 里的版本号再在应用层比对,会引入竞态窗口:读完 Redis 版本、还没来得及读本地缓存时,另一个写请求已更新 Redis,导致误判“一致”而跳过刷新。真正安全的比对必须原子落在 Redis 侧。
正确做法是把版本比较逻辑塞进 Lua 脚本,用 redis.call('GET', KEYS[1]) 读 Redis 版本,再用 ARGV[1] 传入本地缓存当前版本(由客户端提供),脚本内完成 == 判断并返回布尔结果:
return redis.call('GET', KEYS[1]) == ARGV[1]
这样整个“读远端 + 比对”过程不可分割,不会被其他写操作干扰。
- KEYS[1] 是 Redis 中存储版本号的 key,例如
version:product:123 - ARGV[1] 是客户端本地缓存此刻持有的版本值(比如时间戳或整数序列号)
- 返回
1表示版本一致,可放心用本地缓存;返回0表示已过期,需重建
为什么不能在 Lua 脚本里读本地缓存
Lua 脚本运行在 Redis 服务端,完全隔离于应用进程内存。它无法访问 Java 的 ConcurrentHashMap、Python 的 dict 或任何本地变量——所谓“本地缓存”对脚本而言是黑盒。
所以必须由客户端主动把本地版本号作为参数(ARGV)传进去,而不是让脚本去“查”。常见错误是试图在脚本里调用 redis.call('GET', 'local_cache_version'),这只会去 Redis 里查另一个 key,不是你进程里的真实本地值。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 本地缓存版本号必须由业务代码在读取本地缓存时同步取出,并随请求一起传给 Lua
- 若本地缓存未命中,自然没有版本号可传,此时应直接走全量加载流程,不走校验脚本
- 传错类型(比如传字符串 "123" 但 Redis 存的是整数 123)会导致恒不等,务必统一序列化格式
EVAL 和 EVALSHA 怎么选才不影响性能
高频调用的校验脚本必须用 EVALSHA,否则每次 EVAL 都要传输完整脚本体,网络开销陡增且 Redis 需重复解析。
首次部署时用 SCRIPT LOAD 加载脚本并缓存 SHA1,后续所有调用都走 EVALSHA <sha> 1 <key><local_version></local_version></key></sha>。注意:Redis 重启后 SHA1 失效,客户端需有 fallback 到 EVAL 的逻辑。
- 脚本内容不变时,SHA1 不变;改一行注释都会导致新 SHA1
- 不要把版本比较逻辑和数据获取混在一个脚本里——校验失败后数据拉取是另一轮请求,避免脚本过长阻塞主线程
- 设置
lua-time-limit防止意外死循环,但校验类脚本通常几微秒就结束,无需担心超时
多级缓存失效时,版本号怎么保证全局递增
版本号不是靠脚本生成的,而是由写操作统一推进。所有更新入口(DB 写入、缓存写入)必须同步 bump 版本号,例如:
redis.call('INCR', 'version:product:123')
或者用 SET version:product:123 <timestamp> XX</timestamp> 确保只更新已存在项。关键点在于:版本号变更必须和数据变更在同一事务/脚本中完成,否则会出现“版本已升但数据没更新”的撕裂状态。
- 禁止在应用层生成版本号后分两步写入(先 SET version,再 SET data),中间可能被中断
- 若用时间戳作版本,需确保所有节点时钟同步(NTP),且精度不低于业务容忍的乱序窗口
- 读场景只校验,不修改版本号;写场景必须原子更新版本号 + 数据,哪怕只是删除操作也要 bump 版本










