必须用 lua 脚本而非多次 java 调用,因 redis 单命令原子但跨网络“读-判-写”存在竞态;lua 在服务端一次性执行,由单线程保障原子性,需通过 eval/evalsha 传入 keys 和 argv,并注意脚本耗时、keys 声明及返回类型一致性。

Java 中用 Redis 执行多步缓存操作(比如先查库存、再扣减、再设过期)时,靠多次 `redisTemplate` 调用无法保证原子性——中间可能被其他客户端插队修改数据。真正可靠的做法是把整个逻辑写成 Lua 脚本,在 Redis 服务端一次性执行,由 Redis 单线程模型天然保障原子性。
为什么必须用 Lua 而不是 Java 代码控制
Redis 单个命令(如 INCR、GET)本身是原子的,但 Java 端的“读-判-写”三步属于跨网络的多次往返:
- Java 发送
GET stock→ 网络延迟 + Redis 响应 - Java 判断结果 → 本地 CPU 运算
- Java 再发
DECR stock→ 又一次网络延迟
这期间别的客户端完全可能抢先完成扣减,导致超卖或逻辑错乱。Lua 脚本直接在 Redis 内部运行,整个脚本从开始到结束不被打断,等效于一个“超长命令”。
Java 中调用 Lua 脚本的标准流程
核心是用 Jedis 或 Lettuce 的 eval / evalsha 方法,传入脚本内容和参数:
-
KEYS 数组:只放真正涉及的 Redis key(如
"stock:1001"),用于脚本内通过KEYS[1]引用 -
ARGV 数组:放动态值(如扣减数量
"1"、过期时间"60"),脚本内用ARGV[1]获取 -
脚本里必须用
redis.call()调用 Redis 命令,不能用redis.pcall()(除非需要容错捕获异常)
示例脚本(库存扣减 + 设置过期):
-- stock_decr.lualocal stock = redis.call("GET", KEYS[1])<br>
if not stock or tonumber(stock)
return -1 -- 库存不足<br>
end<br>
redis.call("DECRBY", KEYS[1], ARGV[1])<br>
redis.call("EXPIRE", KEYS[1], ARGV[2])<br>
return tonumber(stock) - tonumber(ARGV[1])
Java 代码调用示例(Jedis)
推荐预加载脚本再用 EVALSHA,避免每次传输大段 Lua 文本:
- 先用
scriptLoad将脚本上传到 Redis,获取 SHA1 值(如"a1b2c3...") - 后续所有请求都用
evalsha("a1b2c3...", keys, args) - 如果 Redis 返回
NOSCRIPT错误,说明脚本丢失,需重新scriptLoad并重试
关键代码片段:
List<string> keys = Arrays.asList("stock:1001");<br>
List<string> args = Arrays.asList("1", "3600");<br>
Object result = jedis.evalsha(sha1, keys, args); // 返回 -1 或剩余库存数</string></string>
生产环境注意事项
Lua 脚本不是万能的,用错反而引发问题:
-
避免耗时操作:不能用
for i=1,10000 do ... end这类纯计算循环,会阻塞 Redis 其他请求 - KEYS 必须显式声明:脚本里访问的所有 key 都得出现在 KEYS 参数中,否则集群模式下会路由失败
-
返回值类型要一致:Java 接收时按
Long、String或List显式转换,避免 ClassCastException - 脚本长度限制:默认最大 512MB(实际远小于此),复杂逻辑建议拆分或改用服务端模块(Redis Modules)
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











