Redis 事务与并发控制
Redis 事务用 MULTI / EXEC 把一批命令"打包一次性执行",执行期间不会被其他客户端的命令插队,解决"多条命令要原子完成"的问题(如转账的扣减与入账)。但它与传统数据库事务不同:没有回滚,还需要 WATCH 乐观锁配合才能安全处理"先读后写"的并发场景。
基本用法:MULTI / EXEC
MULTI 开启事务,之后的命令只入队不执行,EXEC 时按顺序一次性执行:
MULTI
# 输出:OK
SET acct:a 100
# 输出:QUEUED
INCR acct:a
# 输出:QUEUED
EXEC
# 输出:1) OK
# 2) (integer) 101
中途不想执行可用 DISCARD 丢弃队列里的全部命令。
事务的两个关键特性
- 原子执行:EXEC 后命令依次执行,单线程下不会被其他请求插队,执行过程对别的客户端不可见。
- 不回滚:入队时就发现的错误(命令不存在、参数个数不对)会让 EXEC 直接放弃;运行期错误(如对字符串做 INCR)只影响出错的那条,其余照常执行:
MULTI
# 输出:OK
SET msg "not-a-number"
# 输出:QUEUED
INCR msg
# 输出:QUEUED
LPUSH list:x 1
# 输出:QUEUED
EXEC
# 输出:1) OK
# 2) (error) ERR value is not an integer or out of range
# 3) (integer) 1
可见 SET 与 LPUSH 都生效了,只有 INCR 失败——Redis 事务不保证"出错全部回滚"。
WATCH:乐观锁
事务内命令是排队后执行,无法"读到上一步结果再决定下一步";若业务是"先读后写",用 WATCH 保护:WATCH 之后、EXEC 之前,只要被监视的键被其他客户端改过,EXEC 就返回 nil 放弃执行,程序拿到 nil 后重试即可:
# 终端 1
WATCH num
# 输出:OK
MULTI
# 输出:OK
INCR num
# 输出:QUEUED
# 终端 2(在终端 1 执行 EXEC 之前)
INCR num
# 输出:(integer) 1
# 终端 1 回到 EXEC
EXEC
# 输出:(nil) # num 已被终端 2 修改,事务放弃
未发生冲突时 EXEC 正常返回结果数组;UNWATCH 可手动解除监视。
秒杀扣减:WATCH + 重试
经典库存扣减流程(程序里写成重试循环):
- WATCH stock:sku1001,GET 读取当前库存;
- 库存 <= 0 返回"售罄";否则 MULTI、DECR stock:sku1001、EXEC;
- EXEC 返回 nil 说明期间被别的请求改过,回到第 1 步重试;
- EXEC 成功则返回扣减结果,结束。
注意:重试次数要设上限;WATCH 应在 MULTI 之前执行;把"读-判-写"整体放进 Lua 脚本(见后续章节)可由服务器原子完成、省去重试,新项目多数优先选 Lua。
事务、管道与 Lua 的区别
| 手段 | 解决的问题 | 说明 |
|---|---|---|
| Pipeline | 减少网络往返 | 一批命令一次发送,不保证原子 |
| MULTI/EXEC | 原子批量执行 | 打包执行不被插队,不回滚 |
| WATCH | 并发写保护 | 乐观锁,冲突时放弃并重试 |
| Lua 脚本 | 原子读改写 | 服务端整体执行,可带判断逻辑 |
小结
Redis 事务 = MULTI 排队 + EXEC 原子执行,配合 WATCH 实现乐观并发控制;它没有回滚,运行期错误不影响其他命令。秒杀扣减这类"读-判-写"场景用 WATCH + 重试或 Lua 脚本,不要拿裸事务硬扛。