Redis作为高性能内存数据库,常面临多个客户端并发修改同一份数据的场景。如果直接分多条命令发送,网络往返之间可能出现其他客户端插队修改,引发超卖或状态不一致。Redis提供的Lua脚本功能,允许把一段逻辑在服务端一次性执行完毕,期间不会响应其他命令,从而形成了天然的事务边界。

Redis单线程与脚本执行的底层机制
Redis核心处理网络请求和命令执行的逻辑运行在单一线程中,依靠事件循环依次处理文件事件。当客户端发送EVAL命令时,服务端会把Lua脚本载入内置的Lua解释器,并在同一个事件周期内从头到尾跑完。因为事件循环不会在脚本中途切走去处理新来的命令,所以这段脚本相对于其他连接而言是“不可中断”的。
这种原子性的本质并不是加锁,而是执行权独占。脚本运行期间,所有其他客户端的命令都会被排队,直到当前脚本返回结果。Redis源码中通过标记lua_timedout和脚本超时检测来防止无限循环,默认脚本执行超过一定毫秒数会强制中断并返回错误,避免整个服务假死。因此,Lua脚本的原子性来自于单线程调度模型,而非分布式锁那样复杂的协商过程。
与MULTI/EXEC事务相比,Lua脚本能把判断逻辑放进服务端。例如先读余额再扣款,事务只能保证命令顺序,无法在EXEC前做条件分支;而Lua可以在脚本里用redis.call读取后判断,再决定是否写入。这让原子性不仅意味着“不中断”,还意味着“带业务逻辑的不可分割”。
用Lua脚本实现并发安全的库存扣减
电商秒杀是最典型的场景。假设商品库存存放在键stock:1001中,多个客户端同时下单。如果先GET再DECR,两个命令间可能被其他请求插空导致超卖。把逻辑写成Lua脚本后,读取和扣减在同一个原子单元完成。
下面示例展示一个带校验的扣减脚本:先取当前库存,若大于零才减一并返回成功,否则返回失败。由于脚本原子执行,不会出现两个请求都读到“1”然后都扣成负数的情况。
local key = KEYS[1]
local count = tonumber(ARGV[1])
local cur = tonumber(redis.call('GET', key))
if not cur then
return -1
end
if cur < count then
return 0
end
redis.call('DECRBY', key, count)
return 1
在Java客户端中可通过eval方法传入键和参数调用。注意KEYS与ARGV的规范使用:键必须显式声明,便于Redis集群计算槽位。脚本内不要拼接字符串访问未知键,否则在集群模式下可能触发跨槽错误。相比用分布式锁包裹普通命令,Lua方案减少了一次网络往返和锁竞争,吞吐更高。
当然Lua脚本也不是万能。脚本应尽量短小,避免复杂循环和大数据遍历,否则会拉长阻塞时间。若业务需要访问多个独立业务数据且无法放在同一槽,应重新设计键结构或采用其他协调方式,而不能强行依赖脚本原子性。
EVAL与EVALSHA的生产实践差异
每次都发送完整脚本给Redis会消耗带宽,尤其脚本较长时。SCRIPT LOAD可把脚本预注册到服务端,拿到一个SHA1摘要,之后用EVALSHA带上摘要执行即可。两者在原子性上没有任何区别,区别只在传输内容。
生产环境常见做法是启动时把核心脚本SCRIPT LOAD,把返回的摘要缓存到本地。调用时先试EVALSHA,若服务端返回NOSCRIPT错误说明脚本丢失(如实例重启),则回退到EVAL并重新加载。这样既省流量又保证可用。
import redis
r = redis.Redis()
script = "return redis.call('GET', KEYS[1])"
sha = r.script_load(script)
try:
print(r.evalsha(sha, 1, 'foo'))
except redis.exceptions.NoScriptError:
sha = r.script_load(script)
print(r.evalsha(sha, 1, 'foo'))
另外要注意,Redis集群要求脚本内所有键必须落在同一槽。可通过用花括号包裹相同哈希标签来强制同槽,例如{user100}:age与{user100}:name。若忽略这点,EVALSHA也会报跨槽错误,破坏原本预期的原子操作。合理规划键名和摘要管理,才能让Lua脚本在分布式环境中稳定提供原子性保障。
脚本原子性带来的运维与调试注意点
因为脚本执行时其他请求全阻塞,一旦出现慢脚本就会影响整体延迟。Redis提供了redis-cli --eval调试方式,也可以在开发阶段用redis.call写日志到本地文件(需改配置)。更重要的是在CI环节对脚本做复杂度审查,禁止未知长度的KEYS遍历。
当主节点故障切换时,Lua脚本本身不会自动同步到从节点,但SCRIPT LOAD的脚本会随命令传播复制。若用EVALSHA且摘要未加载到新主,就会报错。因此故障演练时要确认脚本预热机制。理解这些边界,才能把“原子性”真正转化为线上稳定性,而不是埋下隐蔽的阻塞隐患。
RedisLua_scriptatomicity修改时间:2026-08-17 16:28:33