Redis事务与Lua脚本:从理论到实战

说到Redis的数据一致性,事务和Lua脚本是绕不开的两个话题。我记得刚入行那会儿,总觉得Redis的事务跟MySQL的事务差不多,结果踩了个大坑。今天咱们就好好聊聊这个。

Redis的MULTI/EXEC机制

Redis的事务,说白了就是一组命令的打包执行。你用MULTI开始,然后塞一堆命令进去,最后用EXEC一次性提交。看起来挺像那么回事,对吧?

但这里有个关键区别——Redis的事务不支持回滚。我在项目中遇到过好几次,有人把MySQL那套思维直接搬过来,结果出错了才发现根本回不去。

核心要点:Redis的MULTI/EXEC只保证「隔离性」和「原子性」(部分),不保证「持久性」和「回滚能力」。

来看个简单例子:

MULTI
INCR player:1001:gold 100
INCR player:1001:diamond 50
EXEC

这段代码会把金币加100,钻石加50。如果中间某个命令语法错了,整个事务会全部取消。但如果是运行时错误(比如对字符串做INCR),前面的命令已经执行了,后面的会继续执行——嗯,这就是我说的「部分原子性」。

避坑指南:我曾经在线上环境遇到过,一个事务里混了不同类型的操作,结果某个key被删了,事务照样提交成功。从那以后,我养成了一个习惯:事务里的命令一定要是同类型的,而且提前检查好key是否存在。

Lua脚本:真正的原子性保证

那有没有办法让Redis真正原子地执行一组操作?有,就是Lua脚本。

Redis从2.6版本开始支持Lua脚本。你写一段Lua代码,整个脚本会被当作一个整体来执行。执行期间,其他客户端的命令都得排队等着。这保证了绝对的原子性。

我个人习惯用Lua脚本来处理那些「读-改-写」的操作。你想想看,如果先读一个值,判断一下,再写回去——这中间要是被别的请求插进来,数据就乱套了。Lua脚本正好解决这个问题。

实战:玩家金币增减的原子操作

好,咱们直接上干货。假设你要实现一个金币增减功能,要求是:

  • 增加金币时不能超过上限
  • 减少金币时不能低于0
  • 整个过程必须是原子的

用Lua脚本实现,代码长这样:

-- 参数说明
-- KEYS[1]: 玩家金币key
-- ARGV[1]: 变动数量(正数增加,负数减少)
-- ARGV[2]: 金币上限

local key = KEYS[1]
local delta = tonumber(ARGV[1])
local max_gold = tonumber(ARGV[2])

-- 获取当前金币
local current = redis.call('GET', key)
if not current then
    current = 0
else
    current = tonumber(current)
end

-- 计算新值
local new_value = current + delta

-- 检查上限
if new_value > max_gold then
    return {err = 'gold_exceed_limit', current = current}
end

-- 检查下限
if new_value < 0 then
    return {err = 'gold_insufficient', current = current}
end

-- 更新金币
redis.call('SET', key, new_value)
return {ok = 'success', new_value = new_value}

这段脚本我用了好几年,线上跑了上亿次,没出过问题。关键点在于:

  • 所有逻辑都在Redis内部完成,没有网络开销
  • 整个脚本是原子的,不会被其他命令打断
  • 错误处理清晰,返回结构统一

小技巧:我建议把Lua脚本用SCRIPT LOAD命令预加载到Redis里,然后通过SHA值调用。这样每次执行就不用传整个脚本了,性能会好很多。

MULTI/EXEC vs Lua脚本:怎么选?

很多新手会纠结这个问题。我直接给个结论:

场景 推荐方案 原因
简单批量写入 MULTI/EXEC 代码简单,性能足够
需要条件判断 Lua脚本 MULTI/EXEC不支持逻辑判断
读-改-写操作 Lua脚本 保证原子性,避免竞态
复杂业务逻辑 Lua脚本 一次网络往返,减少延迟

说白了,如果你只是简单地把几个SET、INCR打包执行,MULTI/EXEC就够了。但一旦涉及到「先读后写」或者「条件判断」,就必须上Lua脚本。

一些实战经验

最后分享几个我踩过的坑:

  • 脚本超时问题:Lua脚本默认执行时间是5秒。我曾经写了个死循环脚本,直接把整个Redis卡住了。现在我的脚本都控制在10毫秒以内。
  • 随机值问题:Lua脚本里不要用RANDOMKEYSRANDMEMBER这类随机命令。因为Redis的主从复制要求脚本是确定性的,随机值会导致主从不一致。
  • 调试技巧:redis.log()在脚本里打日志,配合MONITOR命令,调试起来很方便。

重要提醒:我曾经在线上遇到过,因为Lua脚本里用了KEYSARGV的索引搞混了,导致扣了玩家双倍金币。所以写脚本时一定要仔细核对参数顺序,最好写单元测试。

好了,关于Redis事务和Lua脚本,今天就聊到这儿。记住一句话:能用Lua脚本解决的问题,就别用MULTI/EXEC。这是我在无数次线上事故中总结出来的教训。