1. Redis 事务(MULTI/EXEC/DISCARD)
1.1 事务模型
Redis 事务与关系型数据库事务不同:没有回滚机制,命令在 EXEC 时批量顺序执行,中间某命令失败不影响后续命令。
# 基本事务流程
MULTI # 开启事务,后续命令入队
SET balance:alice 100
DECRBY balance:alice 30
INCRBY balance:bob 30
EXEC # 执行所有入队命令
# 返回: [OK, 85, 30]
# 取消事务
MULTI
SET key1 value1
DISCARD # 取消事务,清空队列,不回滚已执行命令(还没执行)
1.2 WATCH 乐观锁
# 场景:转账时检查余额是否变化
WATCH balance:alice # 监控 key
GET balance:alice # 当前余额 100
MULTI
DECRBY balance:alice 30
INCRBY balance:bob 30
EXEC
# 如果在 WATCH 和 EXEC 之间,alice 的 balance 被修改 → EXEC 返回 nil(事务失败)
# 应用层应重试
1.3 事务的限制
| 限制 | 说明 |
|---|---|
| 无回滚 | 命令失败后继续执行后续命令 |
| 语法错误 | 入队时检查语法错误,EXEC 拒绝执行 |
| 运行时错误 | 如对 String 执行 LPOP,命令失败但继续执行 |
| 不隔离 | 其他客户端可在 MULTI 和 EXEC 间读取中间状态 |
2. Lua 脚本(推荐)
Lua 脚本在 Redis 服务端原子执行,天然解决事务回滚、原子性和复杂逻辑问题。
2.1 EVAL 命令
# 语法: EVAL "脚本" key数量 [key...] [arg...]
# 示例:原子扣减库存(防超卖)
EVAL "
local stock = tonumber(redis.call('GET', KEYS[1]))
local qty = tonumber(ARGV[1])
if stock == nil then
return -1
end
if stock >= qty then
redis.call('DECRBY', KEYS[1], qty)
return stock - qty
else
return -2
end
" 1 stock:sku:001 5
# 返回: 剩余库存 或 -1(key不存在) 或 -2(库存不足)
2.2 EVALSHA(缓存脚本)
# 第一次:EVAL,Redis 缓存脚本 SHA1
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# 返回: "a5260dd7e3c0a8f6f3a5d9e1b0c2d3e4f5a6b7c8"
# 后续:用 EVALSHA 执行(减少带宽和解析开销)
EVALSHA a5260dd7e3c0a8f6f3a5d9e1b0c2d3e4f5a6b7c8 1 mykey
2.3 Lua 脚本实战模式
模式 1:分布式锁释放
-- KEYS[1]: 锁的 key
-- ARGV[1]: 持有者标识
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
模式 2:滑动窗口限流
-- KEYS[1]: 限流 key(如 rate:user:1001:api:create)
-- ARGV[1]: 窗口大小(ms)
-- ARGV[2]: 最大请求数
-- ARGV[3]: 当前时间戳(ms)
local window = tonumber(ARGV[1])
local max = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local key = KEYS[1]
-- 清理过期记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 统计当前窗口内的请求数
local count = redis.call('ZCARD', key)
if count < max then
redis.call('ZADD', key, now, now .. ':' .. redis.call('INCR', key .. ':seq'))
redis.call('PEXPIRE', key, window)
return 1 -- 允许通过
else
return 0 -- 拒绝
end
模式 3:原子读取并删除
-- 获取并删除 key(用于消息队列消费确认)
local value = redis.call('GET', KEYS[1])
if value ~= nil then
redis.call('DEL', KEYS[1])
end
return value
2.4 Lua 脚本注意事项
| 注意点 | 说明 |
|---|---|
| 执行时间限制 | lua-time-limit(默认 5s),超时后脚本可被 SCRIPT KILL |
| 禁止随机命令 | RANDOMKEY、SRANDMEMBER 等在副本同步时有问题 |
| 禁止全局变量 | 防止并发问题 |
| 脚本原子性 | 执行期间阻塞其他命令,脚本应尽量短小 |
3. 事务 vs Lua 脚本选择
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 批量命令执行 | MULTI/EXEC | 简单、无需脚本 |
| 条件判断+原子操作 | Lua | 事务无回滚和条件判断 |
| 分布式锁 | Lua | 保证"判断+删除"原子性 |
| 复杂聚合逻辑 | Lua | 服务端计算减少网络往返 |
| 限流/计数 | Lua | 原子性+条件判断 |
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。