Redis 事务、Lua 脚本与原子操作

深入 Redis 事务机制(MULTI/EXEC/DISCARD/WATCH)、Lua 脚本原子执行、EVAL 与 EVALSHA 命令,以及使用 Lua 实现原子扣库存、限流器等实战模式。

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原子性+条件判断

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「redis」更多文章

  1. Redis 性能调优与生产监控
  2. 09. Java 客户端与 Spring Data Redis
  3. Redis 主从复制与 Sentinel 高可用