资讯动态

Redis事务与Lua脚本的实战解析

发布时间:2026/10/26 11:49:20 来源:尧图企业网站定制
1. Redis事务的本质与边界条件Redis事务与传统数据库事务有着本质区别。MULTI/EXEC命令组合提供的是命令打包执行的机制而非ACID特性中的原子性。当我们在Redis客户端输入以下命令序列时MULTI SET user:1000:balance 5000 INCRBY user:1000:balance -3000 EXEC这个事务块中的命令会经历三个阶段入队阶段所有命令被放入队列但不立即执行执行阶段EXEC触发队列中所有命令顺序执行结果返回所有命令的执行结果被打包返回关键区别Redis事务执行期间如果某条命令失败比如对字符串执行INCR操作后续命令仍会继续执行这与关系型数据库的全部成功或全部失败有本质不同。事务执行期间可能遇到的典型边界条件包括类型错误对错误数据类型执行操作如对字符串执行LPUSH语法错误命令本身存在语法问题如缺少参数运行时错误执行时出现的异常如内存不足MULTI SET counter abc INCR counter # 这条命令会报错但事务不会回滚 SET status active EXEC2. MULTI/EXEC的实战陷阱与解决方案2.1 竞态条件的经典场景考虑电商库存扣减场景以下代码存在严重问题def deduct_stock(item_id, quantity): current redis.get(fitem:{item_id}:stock) if int(current) quantity: redis.multi() redis.decrby(fitem:{item_id}:stock, quantity) redis.incrby(fitem:{item_id}:sold, quantity) redis.exec() return True return False问题在于GET和MULTI之间的间隙期其他客户端可能已修改库存。我曾在一个秒杀系统中因此损失了价值数万元的超卖订单。2.2 WATCH命令的正确使用姿势Redis提供了WATCH机制实现乐观锁def safe_deduct_stock(item_id, quantity): with redis.pipeline() as pipe: while True: try: pipe.watch(fitem:{item_id}:stock) current int(pipe.get(fitem:{item_id}:stock)) if current quantity: pipe.unwatch() return False pipe.multi() pipe.decrby(fitem:{item_id}:stock, quantity) pipe.incrby(fitem:{item_id}:sold, quantity) pipe.execute() return True except WatchError: continue性能提示WATCH会显著增加Redis服务端内存消耗监控大量key时可能导致OOM。建议单个事务监控的key不超过20个。3. Lua脚本的原子性魔法3.1 脚本执行模型剖析Redis执行Lua脚本时具有以下特性脚本作为一个整体执行期间不会穿插其他命令脚本内所有写操作要么全部执行要么全部不执行脚本执行是单线程的天然避免竞态条件-- 库存扣减脚本 local stock_key KEYS[1] local sold_key KEYS[2] local quantity tonumber(ARGV[1]) local current tonumber(redis.call(GET, stock_key)) if current quantity then return 0 end redis.call(DECRBY, stock_key, quantity) redis.call(INCRBY, sold_key, quantity) return 13.2 脚本调试技巧开发复杂脚本时可以使用redis-cli的--ldb选项启动调试模式redis-cli --ldb --eval /path/to/script.lua key1 key2 , arg1 arg2调试命令示例step单步执行continue继续执行到结束list显示当前执行的代码段print variable打印变量值4. 性能优化与生产实践4.1 事务与脚本的性能对比在相同操作下不同方式的性能表现基于Redis 6.2基准测试操作方式QPS (万次/秒)平均延迟(ms)内存消耗(MB)普通命令12.40.8120MULTI事务9.71.0135Lua脚本11.20.91254.2 脚本缓存的最佳实践Redis通过SHA1校验和缓存脚本生产环境中应该预先加载常用脚本script_sha redis.script_load(lua_code)后续通过evalsha调用redis.evalsha(script_sha, keys, args)实现自动重载机制try: return redis.evalsha(sha, keys, args) except NoScriptError: sha redis.script_load(script) return redis.evalsha(sha, keys, args)5. 高级应用场景5.1 分布式锁的原子性释放常见的错误实现-- 错误示范 if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) end return 0正确实现应该包含过期时间检查和删除操作的原子性if redis.call(GET, KEYS[1]) ARGV[1] then redis.call(DEL, KEYS[1]) return 1 elseif redis.call(EXISTS, KEYS[1]) 0 then return 2 -- 锁已自动过期 end return 05.2 限流算法的实现令牌桶算法的Lua实现local key KEYS[1] local now tonumber(ARGV[1]) local window tonumber(ARGV[2]) local limit tonumber(ARGV[3]) local last redis.call(GET, key) if last false then redis.call(SET, key, window - 1, EX, window) return 1 end local remaining tonumber(last) if remaining 0 then return 0 end redis.call(DECR, key) return 16. 生产环境踩坑实录6.1 脚本执行超时问题Redis默认配置的脚本执行超时为5秒超过会报错。解决方案拆分复杂脚本为多个小脚本使用SCRIPT KILL终止长时间运行的脚本调整lua-time-limit配置不推荐6.2 内存泄漏陷阱Lua脚本中如果误用全局变量会导致内存泄漏-- 错误示范 counter 0 -- 这个全局变量会一直存在 redis.call(SET, key, value)应该总是使用local声明变量local counter 0 redis.call(SET, key, value)6.3 集群环境下的特殊处理Redis Cluster中使用Lua脚本时所有操作的key必须在同一个slot可以使用hash tag确保key路由到同一节点-- {user}保证两个key在同一slot redis.call(SET, user:{1000}:name, Alice) redis.call(SET, user:{1000}:age, 30)7. 监控与诊断7.1 关键指标监控通过INFO命令获取的重要指标redis_calls命令调用统计redis_commands各命令执行次数lua_scripts脚本缓存数量script_latency脚本执行延迟7.2 慢查询日志配置在redis.conf中设置slowlog-log-slower-than 10000 # 记录超过10ms的查询 slowlog-max-len 128 # 保留128条慢查询记录查询慢日志SLOWLOG GET 10 # 获取最近10条慢查询8. 客户端实现差异8.1 Java(Jedis)中的事务处理try (Jedis jedis pool.getResource()) { jedis.watch(key1, key2); Transaction t jedis.multi(); t.set(key1, value1); t.set(key2, value2); t.exec(); } catch (Exception e) { // 处理WatchError }8.2 Python(redis-py)的Pipeline优化with redis.pipeline(transactionFalse) as pipe: for i in range(1000): pipe.set(fkey:{i}, i) pipe.execute() # 单次网络往返完成批量操作8.3 Go(go-redis)的Lua脚本调用script : redis.NewScript( local key KEYS[1] local value ARGV[1] return redis.call(SET, key, value) ) result, err : script.Run(ctx, rdb, []string{key}, value).Result()9. 版本兼容性考量不同Redis版本的重要变化3.2支持Lua脚本复制到从节点5.0脚本支持effects replication6.0支持脚本的ACL权限控制7.0新增FUNCTION命令管理脚本生产环境升级时特别需要注意测试所有关键脚本在新版本的执行结果检查事务中使用的命令语法是否有变更验证WATCH机制的行为一致性10. 安全最佳实践永远不要从不受信任的来源接收Lua脚本使用rename-command禁用危险命令rename-command EVAL rename-command SCRIPT 为生产环境的脚本设置SHA1白名单使用ACL限制脚本执行权限ACL SETUSER script_runner on password ~* scripting

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑