事务与 Lua 脚本
约 785 字大约 3 分钟
布欧-Lewyon
2026-05-15
Redis 事务
Redis 事务(MULTI/EXEC)将多个命令打包,按顺序原子执行。
# 事务:队列所有命令,EXEC 时一次性执行
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET balance:100 100
QUEUED
127.0.0.1:6379> SET balance:200 200
QUEUED
127.0.0.1:6379> DECRBY balance:100 50
QUEUED
127.0.0.1:6379> INCRBY balance:200 50
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
3) (integer) 50
4) (integer) 250
# 放弃事务
DISCARD事务错误处理
# 语法错误 → 整个事务失败
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key value
QUEUED
127.0.0.1:6379> BADCOMMAND # 语法错误
(error) ERR unknown command...
127.0.0.1:6379> EXEC
(error) EXECABORTED Transaction discarded
# 运行时错误(如 INCR String)→ 其他命令成功
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET a "hello"
QUEUED
127.0.0.1:6379> INCR a # 运行时错误(String 不能 INCR)
QUEUED
127.0.0.1:6379> SET b "world"
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) ERR value is not an integer
3) OK
# a="hello" b="world" ← INCR 失败不影响其他WATCH 乐观锁
WATCH 实现 CAS(Compare And Swap),在 EXEC 之前检测 key 是否被修改。
# 模拟:扣减库存
127.0.0.1:6379> SET stock:item1 10
OK
# 客户端 A
127.0.0.1:6379> WATCH stock:item1
OK
127.0.0.1:6379> GET stock:item1
"10"
# 客户端 B 修改了 stock:item1
127.0.0.1:6379> SET stock:item1 9
OK
# 客户端 A 尝试执行事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECR stock:item1
QUEUED
127.0.0.1:6379> EXEC
(nil) # 事务执行失败!因为 WATCH 的 key 被修改
# 重试模式
while true:
WATCH stock:item1
stock = GET stock:item1
if stock <= 0: break
MULTI
DECR stock:item1
if EXEC: break # 成功则退出Lua 脚本
Lua 脚本原子执行全部逻辑,是复杂业务的推荐方案。
# EVAL 执行脚本
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey "hello"
# 脚本中执行多个命令
EVAL "
redis.call('INCR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[1])
return redis.call('GET', KEYS[1])
" 1 counter:user1 60原子扣库存
-- stock.lua
-- 参数:KEYS[1]=库存 key, ARGV[1]=扣减数量
local stock = redis.call('GET', KEYS[1])
if not stock then
return -1 -- 不存在
end
if tonumber(stock) < tonumber(ARGV[1]) then
return 0 -- 库存不足
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1 -- 扣减成功# 加载脚本,返回 SHA
SCRIPT LOAD "local stock ..."
# "abc123..."
# 通过 SHA 执行(避免每次传输脚本)
EVALSHA abc123 1 stock:item1 1限流器
-- rate_limit.lua
-- KEYS[1]=限流 key, ARGV[1]=限制次数, ARGV[2]=窗口秒数
local current = redis.call('GET', KEYS[1])
if current and tonumber(current) >= tonumber(ARGV[1]) then
return 0 -- 限流
end
redis.call('INCR', KEYS[1])
if not current then
redis.call('EXPIRE', KEYS[1], ARGV[2])
end
return 1 -- 通过# 每分钟最多 10 次
EVAL "rate_limit" 1 rate:user:100 10 60脚本缓存管理
# 查看所有缓存的脚本
SCRIPT EXISTS sha1 sha2 ...
# 清除缓存
SCRIPT FLUSH
# 终止正在执行的脚本(会阻塞其他命令)
SCRIPT KILL小结
| 特性 | 事务(MULTI/EXEC) | Lua 脚本 |
|---|---|---|
| 原子性 | ✅ | ✅ |
| 条件逻辑 | ❌ | ✅ |
| 复杂度 | 简单 | 灵活 |
| 网络开销 | N 次命令 | 1 次传输 |
- 事务保证批量命令原子性,但不支持条件判断。
WATCH实现乐观锁,适用于扣库存等并发场景。- Lua 脚本支持完整逻辑,推荐用于复杂原子操作。
EVALSHA通过 SHA 执行已缓存的脚本,减少传输。
上一节:RDB 与 AOF 持久化 下一节:主从复制与 Sentinel
