Redis - 事务机制:能实现 ACID 属性吗

文章目录

数据库的事务都讲 ACID------原子性、一致性、隔离性、持久性。这四个性质是关系型数据库的根基。Redis 也提供了事务机制,但要回答"Redis 能不能做事务",必须把这四个性质拆开来看。结论是:Redis 的事务和传统数据库事务有本质差异,更像是"批量原子执行"。

Redis 事务的命令组合

Redis 的事务由四个命令组成:

  • MULTI:开启事务,后续命令进入队列。
  • EXEC:提交事务,依次执行队列里的所有命令。
  • DISCARD:放弃事务,清空队列。
  • WATCH:监视一个或多个 key,事务期间被改就放弃执行。

典型用法:

java 复制代码
MULTI
SET account:A 100
SET account:B 200
INCRBY account:A -50
INCRBY account:B 50
EXEC

MULTI 之后到 EXEC 之前的所有命令都进入队列,不立即执行。EXEC 一旦发出,队列里的命令依次执行,期间不会被其他客户端打断。

用 ACID 标准重新审视

原子性(A):部分支持

传统事务的原子性是"全部成功或全部回滚"。Redis 的情况复杂一些:

情况 1 :命令入队时就有错(比如命令名拼错)。Redis 5.0+ 会拒绝整个事务,EXEC 返回错误,所有命令都不执行。这种情况下原子性是有的。

情况 2 :入队时语法没错,但执行时出错(比如对 String 用 LPUSH)。Redis 会继续执行后面的命令,前面成功的命令不会回滚。这种情况下原子性是缺失的。

java 复制代码
MULTI
SET key1 "hello"
LPUSH key1 "world"   # 类型错误,但 EXEC 时才暴露
SET key2 "ok"
EXEC
# 结果:key1 = "hello",key2 = "ok",没有回滚

为什么不支持回滚?Antirez 的解释是:Redis 事务里出错的几乎都是"程序员的 bug",回滚也救不了你;而且不支持回滚让 Redis 实现更简单、性能更好。

一致性(C):依赖原子性

一致性指事务执行前后数据库都处于合法状态。在 Redis 里:

  • 如果事务原子执行成功,最终状态自然合法。
  • 如果中间命令失败但后面继续执行,就可能进入"半完成"状态,违反一致性。

所以 Redis 事务的一致性强弱完全取决于上面说的原子性场景。

隔离性(I):完全支持

EXEC 开始执行后,整个队列里的命令是串行运行的,期间不会有其他客户端的命令插入。这是单线程模型给的天然隔离。

但要注意:MULTI 到 EXEC 之间的"入队期",其他客户端是可以正常操作的。如果你需要"开启事务时数据状态不变",必须配合 WATCH。

持久性(D):取决于持久化配置

事务执行结果是否持久化,取决于 Redis 的持久化策略:

  • 关闭持久化:完全不持久。
  • AOF + appendfsync everysec:最多丢 1 秒。
  • AOF + appendfsync always:理论上不丢。

也就是说,Redis 事务本身不提供持久性保证,要靠 AOF 配合。

WATCH:实现乐观锁

WATCH 是 Redis 事务的精髓,让事务支持"基于条件的提交"。

典型场景:账户扣款,需要保证扣款时余额不变。

java 复制代码
WATCH account:A
balance = GET account:A
if balance < 100:
    UNWATCH
    return "余额不足"
MULTI
DECRBY account:A 100
EXEC   # 如果在 WATCH 之后 account:A 被改过,EXEC 返回 nil

机制:WATCH 后,Redis 会跟踪这些 key。EXEC 执行时,如果发现任何被监视的 key 被其他客户端改过,事务就整体放弃,返回 nil。

这是典型的乐观锁:先假设没人会改,真到提交时再检查。客户端发现失败要自行重试。

事务 vs Lua 脚本

Lua 脚本能实现的事情,Redis 事务也能做。但两者各有取舍:

维度 MULTI/EXEC Lua 脚本
中间结果可见 不可见(只能在 EXEC 后看结果) 可见(脚本里可以基于中间结果判断)
条件分支 弱(只能 WATCH) 强(脚本里随便写)
错误处理 不支持回滚 可以提前返回
网络开销 多次往返 一次调用
可读性 命令直观 需要懂 Lua

实际选择:

  • 简单批量操作:用 MULTI/EXEC,可读性好。
  • 复杂逻辑(需要中间判断、循环):用 Lua。
  • 需要严格条件提交:MULTI + WATCH 或者 Lua。

事务的常见误区

误区 1:以为 EXEC 失败会回滚

事务里命令执行失败不会回滚。如果业务依赖回滚,必须自己写补偿逻辑。

误区 2:以为 WATCH 是悲观锁

WATCH 是乐观锁,发现冲突就放弃,不阻塞其他客户端。如果竞争激烈,重试次数会很多。

误区 3:忘了 EXEC 之后还可能 nil

被 WATCH 的 key 被改了,EXEC 返回 nil 而不是错误。客户端要主动检查这个返回值。

误区 4:事务里执行慢命令

EXEC 期间会阻塞整个 Redis。事务里的命令必须都是快速的,避免长时间阻塞。

实践建议

  1. 明白 Redis 事务的真实保证:原子性弱、隔离性强、持久性靠 AOF。
  2. 复杂逻辑优先用 Lua:Lua 能做的事情比事务更多,且只需一次网络往返。
  3. 乐观锁场景用 WATCH:余额检查、库存扣减等"先看再改"的场景。
  4. 事务里不放慢命令:避免 EXEC 期间阻塞 Redis。
  5. 业务上做好幂等设计:因为 Redis 事务不能回滚,业务层要能容忍重试不重复扣款。

Redis 的事务机制提供了最基本的"批量原子执行"能力,但远不能和数据库事务相提并论。理解它的边界------什么能保证、什么不能------才能在合适的场景用对它。需要更复杂的事务语义时,要么换用 Lua 脚本,要么直接用关系型数据库。

相关推荐
十二10 小时前
【洛谷题解】P1083 [NOIP 2012 提高组] 借教室(二分答案 + 差分)
数据结构·c++·算法·蓝桥杯
aiks_yuan11 小时前
Token 放 Redis 还是用 JWT:Java 实测验签和查 Redis 耗时,退出登录、改密码踢下线怎么做(jjwt 0.12 + Redis 7.2)
redis
D e re k ZH12 小时前
C++ 从零入门(一):基础语法与程序结构
数据结构·c++·算法
寂寞的船17 小时前
【Unity知识速递】第6期 URP性能优化完全指南:从内存到GPU的三维调优
unity·性能优化·编辑器
careathers18 小时前
【数据结构】基于比较的排序算法 - 交换排序
数据结构·算法·排序算法
我不会起名字32218 小时前
Redis 从 0 到 1(二十九):CPU 突然 100%——commandstats 定位法与 6 步排查链路
redis·cpu·慢查询·排查·commandstats
H.莓飛18 小时前
【C++】【STL】vector_OJ题
linux·服务器·开发语言·数据结构·c++·visual studio
Flynt18 小时前
内存省16倍的Lightpanda实测:数字是真的,但并发代码要重写
性能优化·开源·agent
我不会起名字32220 小时前
Redis 从 0 到 1(二十八):Redis 抖一下,重试把它打挂了——超时/重试/熔断的 Go 封装
redis·go·超时·熔断·重试
OpenAnolis小助手20 小时前
RISC-V 基础软件的工程化与性能优化实践
性能优化·操作系统·龙蜥社区·risc-v