必须用lua脚本做二级缓存同步,因为redis不支持跨key原子操作,而同步链路涉及多步状态变更;lua脚本在服务端单线程原子执行,可避免网络中断、并发覆盖及部分成功导致的不一致。

为什么必须用 Lua 脚本做二级缓存同步
因为 Redis 本身不支持跨 key 的原子操作,而二级缓存同步常涉及「读本地缓存 → 未命中 → 查 Redis → 未命中 → 查 DB → 写 Redis + 写本地缓存」这一整条链路。如果拆成多个命令发给 Redis,中间任意一步失败(比如写 Redis 成功但本地缓存写失败),就会导致状态不一致。Lua 脚本能保证整个逻辑在服务端原子执行,避免网络中断、并发覆盖、部分成功等典型问题。
EVAL 和 EVALSHA 怎么选
开发阶段直接用 EVAL 更灵活,上线后建议切到 EVALSHA。原因很简单:EVAL 每次都传完整脚本,浪费带宽;EVALSHA 只传 40 字符的 SHA1 值,Redis 会查内部缓存命中后执行。但要注意两点:
- 首次调用
EVALSHA前必须先用EVAL执行一次,否则返回NIL - 脚本内容变更后,SHA1 值失效,需重新
EVAL注册,否则脚本不更新 - OpenResty 中常用
redis:evalsha,传入前需确保脚本已加载,否则要 fallback 到eval
一个典型的二级缓存读取 Lua 脚本长什么样
下面这个脚本用于「先查 Redis,命中则返回;未命中则查 DB(模拟)、写入 Redis 并返回」——注意它只负责 Redis 层逻辑,DB 查询由外部(如 OpenResty)完成后再传参进来:
Redis 8.2.3 是一款安全优先的高性能键值存储系统。该版本紧急修复了可能引发远程代码执行(RCE)的高危漏洞(CVE-2025-62507),并解决了 HyperLogLog 及 Cuckoo Filter 等数据结构在特定场景下的崩溃问题。建议所有用户立即升级,以保障生产环境的系统稳定与数据安全。
local redis_key = KEYS[1]
local ttl = tonumber(ARGV[1])
local data = redis.call('GET', redis_key)
if data ~= false then
return data
end
-- 注意:这里不能直接查 DB,DB 操作必须由调用方完成并传入 ARGV[2]
redis.call('SET', redis_key, ARGV[2])
redis.call('EXPIRE', redis_key, ttl)
return ARGV[2]
关键点:
-
KEYS[1]是缓存 key,必须由调用方传入,不能拼接字符串(防注入) -
ARGV[2]是预查好的数据,不是让 Lua 去连 MySQL —— Lua 没有内置 DB 驱动,OpenResty 的 mysql 模块在 Nginx worker 进程里,不在 Redis 进程里 - 不要在脚本里做耗时操作(如循环、复杂计算),超时默认 5 秒,可配
lua-time-limit - 返回值类型要和客户端预期一致,比如 Java 用
redisTemplate.execute()时,需匹配resultType
本地缓存和 Redis 同步最容易漏掉的细节
很多人以为写了 Lua 就万事大吉,其实真正难的是「本地缓存怎么跟上 Redis 变更」。常见错误包括:
- 只同步 Redis,没通知其他节点刷新本地缓存(比如 GuavaCache 或 Caffeine),导致多实例间看到不同数据
- 用 Pub/Sub 做通知,但没处理连接断开、消息丢失、重复消费(Redis Pub/Sub 无持久化、无 ACK)
- 依赖定时轮询 Redis 的 key 变更,但 Redis 不提供 key 修改事件(
KEYSPACE NOTIFICATIONS开销大且不可靠) - 在 Lua 脚本里调用
redis.call('PUBLISH', ...),看似能广播,但订阅方可能还没连上,或消费延迟高,无法保证实时性
真正可靠的方案是:所有写操作统一走 Lua 脚本更新 Redis,再由业务层主动触发本地缓存 invalidate(比如通过 Spring Cache 的 @CacheEvict),或用分布式锁 + 版本号控制本地缓存 reload。Lua 只管 Redis 层的原子性,别让它背本地缓存的锅。










