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 + 重试

经典库存扣减流程(程序里写成重试循环):

  1. WATCH stock:sku1001,GET 读取当前库存;
  2. 库存 <= 0 返回"售罄";否则 MULTI、DECR stock:sku1001、EXEC;
  3. EXEC 返回 nil 说明期间被别的请求改过,回到第 1 步重试;
  4. EXEC 成功则返回扣减结果,结束。

注意:重试次数要设上限;WATCH 应在 MULTI 之前执行;把"读-判-写"整体放进 Lua 脚本(见后续章节)可由服务器原子完成、省去重试,新项目多数优先选 Lua。

事务、管道与 Lua 的区别

手段解决的问题说明
Pipeline减少网络往返一批命令一次发送,不保证原子
MULTI/EXEC原子批量执行打包执行不被插队,不回滚
WATCH并发写保护乐观锁,冲突时放弃并重试
Lua 脚本原子读改写服务端整体执行,可带判断逻辑

小结

Redis 事务 = MULTI 排队 + EXEC 原子执行,配合 WATCH 实现乐观并发控制;它没有回滚,运行期错误不影响其他命令。秒杀扣减这类"读-判-写"场景用 WATCH + 重试或 Lua 脚本,不要拿裸事务硬扛。

笔记加载中…