
你在做一个电商系统的'秒杀扣库存'功能。 你的 Java 代码逻辑:
int stock = redis.get("sku:1001:stock");(获取库存)if (stock > 0) { redis.decr("sku:1001:stock"); }(库存大于 0 则扣减)
灾难降临:
这两步操作之间有网络延迟,并且不是原子的。
当库存只剩 1 个时,100 个并发线程同时执行到了第 1 步,都发现 stock = 1 > 0,于是全部执行了第 2 步。
结果: 库存变成了 -99,超卖了 99 件商品,老板连夜找你谈话。
破局之道:
使用 Redis Lua 脚本。将'查库存 + 判断 + 扣减'打包成一段脚本发给 Redis。因为 Redis 执行 Lua 脚本是单线程且排他的,这 100 个请求只能乖乖排队一个个执行,完美解决超卖问题。
1. 核心原理:为什么是 Lua?
Redis 官方内置了 Lua 解析器。它能带来两大无可替代的优势:
- 绝对的原子性 (Atomicity): Redis 会将整个 Lua 脚本作为一个整体执行。在脚本运行期间,Redis 不会执行其他任何客户端的命令。不用加任何分布式锁,天然防止并发冲突。
- 减少网络开销 (Reduce Network Overhead): 原本需要 3 次请求(请求头 + 网络传输 + 解析)的操作,现在合并成 1 次请求发送给 Redis,极大地降低了网络延迟。
2. 从'菜鸟'到'老鸟':EVAL vs SCRIPT LOAD
执行 Lua 脚本有两个命令,代表了两种不同的段位。
菜鸟写法:每次都传整个脚本 (EVAL)
# 语法:EVAL script numkeys key [key ...] arg [arg ...]
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 mylock myrandomvalue
- 痛点: 你的业务逻辑脚本可能长达上百行(几 KB)。如果秒杀时 QPS 是 10 万,你每秒钟都要在网络上传输
10 万次 * 几 KB的一段重复字符串,这会瞬间打满服务器的网卡带宽。
老鸟写法:脚本预加载 (SCRIPT LOAD + EVALSHA)
为了解决带宽浪费,Redis 提供了'缓存脚本'的机制。
第一步:预加载脚本 (SCRIPT LOAD) 在应用启动时(或者首次执行时),把长达百行的 Lua 脚本发送给 Redis。
SCRIPT LOAD "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
Redis 编译这段代码,把它缓存在内存中,并返回一个 ,例如:。


