Redis Lua脚本原子性源于单线程事件循环,脚本独占主线程、redis.call直调内部函数、其他命令被挂起;但不支持回滚,需显式传KEYS、禁用危险操作、合理使用redis.pcall和redis.log。

Redis 通过 Lua 脚本保证多步操作的原子性,核心在于单线程串行执行 + 命令直入内部队列,不是靠加锁,而是 Redis 自身事件循环模型决定的天然特性。
整个脚本从开始到结束,在主线程中一气呵成,中间不会被其他客户端命令插队。哪怕你写了 10 个 redis.call(),它们也像一条流水线那样依次执行,不中断、不切换上下文。
但要注意:这种“原子性”只保证执行过程不被打断,不等于数据库事务那种“全成功或全回滚”。脚本里一旦 redis.call() 成功执行了前几步,后面出错,前面的操作也不会撤销。
为什么能不被打断?
- Redis 主线程是单事件循环(event loop),
EVAL或EVALSHA调用后,脚本会独占主线程直到执行完毕; - 脚本里的
redis.call()不走网络解析、不进命令排队队列,而是直接调用内部函数,跳过所有中间环节; - 其他客户端发来的任何命令(包括
GET、SET、甚至SCRIPT KILL)都会被挂起,等脚本跑完才处理。
怎么写才能真正发挥原子性?
KEYS 必须显式传入,不能动态拼接
✅ 正确:EVAL "return redis.call('GET', KEYS[1])" 1 user:1001
❌ 错误:EVAL "local k = 'user:'..ARGV[1]; return redis.call('GET', k)" 0 1001(集群下直接报CROSSSLOT)所有 Redis 操作必须用
redis.call()或redis.pcall()redis.call()失败会中止整个脚本;redis.pcall()可捕获错误继续执行,但返回值需手动判断。
Redis Skill - 高性能缓存管理下载Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
避免三类危险操作
- 耗时计算(如万级 for 循环、大 JSON 解析)→ 会阻塞整个 Redis
- 非 Redis I/O(如
os.execute,io.open)→ 直接报错 - 依赖缺失函数(如
print,math.random,os.time)→ 不存在,要用redis.log()替代调试
典型安全用法示例:库存扣减
local stock_key = KEYS[1]
local log_key = KEYS[2]
local need = tonumber(ARGV[1])
local op_id = ARGV[2]
local curr = tonumber(redis.call('GET', stock_key) or '0')
if curr < need then
return -1
end
redis.call('DECRBY', stock_key, need)
redis.call('LPUSH', log_key, 'deduct:'..op_id..':'..need)
return curr - need调用方式:
EVAL "...脚本内容..." 2 stock:item_123 log:item_123 10 abc123
这个脚本把“查库存 → 判断 → 扣减 → 记日志”全部包在一个不可分割的单元里,并发请求进来,谁先拿到执行权,谁就完成整套动作,其他人排队等——彻底规避超卖。
调试和上线前必须做的事
- 本地用
lua5.1或luajit跑通逻辑(禁用print/os.time等) - 在脚本里加
redis.log(redis.LOG_WARNING, "step1: "..tostring(x)),配合loglevel warning查日志 - 显式检查返回类型:
if type(res) == "number" then ...,别把nil当0 - 生产环境优先用
SCRIPT LOAD+EVALSHA,减少网络传输开销
不复杂,但容易忽略细节。关键不是“能不能写”,而是“写得稳不稳、跑得安不安全”。

















