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

但要注意:MULTIEXEC 之间的"入队期",其他客户端是可以正常操作的。如果你需要"开启事务时数据状态不变",必须配合 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 脚本,要么直接用关系型数据库。

相关推荐
玖玥拾1 小时前
C/C++ 数据结构(七)栈、容器适配器
c语言·数据结构·c++··容器适配器
Qres8213 小时前
算法复键——树状数组
数据结构·算法
大鱼>5 小时前
地平线BPU部署实战:YOLOv8在J5/X3上的算法适配与性能优化
算法·yolo·性能优化
taocarts_bidfans5 小时前
反向海淘跨境缓存架构优化:taocarts Redis分层缓存实战技术
redis·缓存·架构·反向海淘·taocarts
牛油果子哥q5 小时前
并查集(DSU)超精讲,路径压缩、按秩合并、万能模板、连通性判定、最小生成树与刷题实战全解
数据结构·c++·最小生成树·并查集
醉颜凉6 小时前
Elasticsearch高性能优化:Bulk API大规模数据导入性能调优全攻略
elasticsearch·性能优化·jenkins
凌波粒6 小时前
LeetCode--491.递增子序列(回溯算法)
数据结构·算法·leetcode
隔窗听雨眠7 小时前
C语言函数递归从入门到精通(下):性能优化与工程实践
c语言·算法·性能优化