事务与 Lua 脚本
用途:需要“多命令原子执行”或“读-改-写”并发安全时查阅
MULTI/EXEC 的真实语义
「MULTI/EXEC 的真实语义」小节链接MULTIINCR counterLPUSH list vEXEC特点与限制:
| 特性 | 说明 |
|---|---|
| 入队而非立即执行 | EXEC 前不产生效果 |
| 无回滚 | 某条命令执行失败(运行时错误),其余仍会执行;只有入队阶段的语法错误会让整个事务不执行 |
| 无法基于读结果分支 | 事务内不能“读到值再决定做什么” |
WATCH 提供乐观并发 |
被监视 key 在 EXEC 前被改则事务放弃(返回 nil),需客户端重试 |
结论:MULTI 适合“一批独立命令一起发送”(减少 RTT),不适合需要条件判断与原子读改写的场景。
Lua 脚本
「Lua 脚本」小节链接脚本在服务端原子执行(执行期间不插入其他命令),可读可写、可分支、可返回复合结果。
-- KEYS[1] 计数器,ARGV[1] 阈值,ARGV[2] 过期秒数local cur = tonumber(redis.call('GET', KEYS[1]) or '0')if cur + 1 > tonumber(ARGV[1]) then return -1 endlocal n = redis.call('INCR', KEYS[1])if n == 1 then redis.call('EXPIRE', KEYS[1], ARGV[2]) endreturn n适用场景:限流计数、分布式锁的校验解锁、库存扣减、复杂条件更新、批量操作合并。
脚本的硬约束
「脚本的硬约束」小节链接- 阻塞风险:脚本执行期间实例不处理其他请求,任何循环或全量扫描都会拖垮实例。必须保证 O(小常数) 复杂度。
- Cluster 下所有 KEYS 必须同 slot,用 hash tag 保证。
- 不要依赖随机/时间:脚本需可重放(
redis.call('TIME')、math.random在旧版本会破坏复制语义;新版本对只读脚本有放松,但仍应避免)。 - 写操作必须通过
redis.call显式声明 key,否则在 Cluster/AOF 复制下行为不正确。 - 脚本缓存:
EVALSHA+SCRIPT LOAD减少传输;SCRIPT KILL只能杀未写过的脚本,写过就得SHUTDOWN NOSAVE——所以务必先在测试环境验证。 - 用
EVAL_RO/函数库(Redis Functions)等较新机制时确认版本支持。
与数据库侧原子性的分工
「与数据库侧原子性的分工」小节链接Redis 原子操作适合“以 Redis 为权威源”的计数、限流、锁。业务数据的正确性(余额、库存)应以数据库条件更新为最终权威(UPDATE ... SET x = x - 1 WHERE x >= 1),Redis 只做加速与预检,否则故障恢复时会出现无法解释的偏差。
常见误区
「常见误区」小节链接| 误区 | 修正 |
|---|---|
| 认为 MULTI 有回滚 | 无回滚,需要原子判断与写入就用 Lua |
| 脚本里遍历大集合 | 阻塞实例,改为分批或客户端处理 |
| Cluster 下跨 slot 脚本 | 用 hash tag 或拆脚本 |
| 用 Redis 计数当业务库存权威 | 以 DB 条件更新为权威,Redis 做预检 |
| 忘设过期导致计数永久累积 | 计数 key 必须有 TTL(如按时间窗口分 key) |
延伸阅读
「延伸阅读」小节链接- Redis 官方文档:Transactions、Lua scripts、Patterns(限流示例)
- 《Redis 设计与实现》脚本与事务章节