前言

当我们谈事务时,很容易把 Redis 和 MySQL、PostgreSQL 放在同一个框架里理解:开始事务、执行一批命令、提交或回滚。但这种类比只对了一半。

Redis 的事务更像“把一批指令先排队,再按顺序一次执行”;关系型数据库的事务则是一套围绕数据一致性、并发隔离和故障恢复建立的完整机制。理解两者差异,关键不在 API 长什么样,而在它们各自承诺了什么、又没有承诺什么。

一句话结论

Redis 事务保证的是:事务队列中的命令会连续、按顺序执行,执行期间不会被其他客户端命令插入。

关系型数据库事务通常保证的是 ACID:原子性、一致性、隔离性、持久性,并能够借助日志、锁与 MVCC 等机制处理失败、并发和恢复。

换句话说,Redis 事务重点解决“命令编排与执行不被打断”,而关系型数据库事务重点解决“数据正确性与可靠性”。

Redis 事务:先入队,后执行

Redis 使用 MULTI 开启事务,之后的命令不会立即执行,而是被放入队列;调用 EXEC 后,Redis 会依次执行队列中的命令。

1
2
3
4
MULTI
DECR product:1001:stock
INCR user:42:coupon_count
EXEC

这里的核心价值是:DECRINCR 会作为一个连续的命令序列执行。Redis 单线程执行命令,因此在 EXEC 开始后,其他客户端的普通命令不会穿插进来。

但请注意,“连续执行”并不等于“失败后自动撤销”。

例如:

1
2
3
4
5
MULTI
SET account:1:balance 100
INCR account:1:balance
LPUSH account:1:balance x
EXEC

第三条命令会因为键的类型不匹配而失败,但前两条已经成功执行的命令不会自动回滚。Redis 会返回每条命令各自的执行结果:成功的成功、失败的失败。

这正是 Redis 事务与传统数据库事务最容易被误解的地方。

Redis事务执行流程图
Redis事务执行流程图

关系型数据库事务:不仅提交,还要能回退

在关系型数据库中,事务通常是一个更强的边界:

1
2
3
4
5
6
7
8
9
10
11
BEGIN;

UPDATE accounts
SET balance = balance - 100
WHERE id = 1;

UPDATE accounts
SET balance = balance + 100
WHERE id = 2;

COMMIT;

如果第二条更新失败,应用可以执行:

1
ROLLBACK;

数据库会把第一条更新造成的影响撤销,使数据回到事务开始前的状态。即使数据库在提交过程中发生宕机,恢复机制也会依据事务日志判断哪些修改应保留、哪些修改应撤销。

这背后涉及预写日志(WAL)、Undo/Redo 日志、锁、MVCC、崩溃恢复等机制。不同数据库实现有差异,但总体目标相同:让事务面对错误、并发和故障时依然可预测。

Redis与关系型数据库的失败对比图
Redis与关系型数据库的失败对比图

核心区别一览

对比维度 Redis 事务 关系型数据库事务
核心目标 将多条命令连续、顺序地执行 保证数据修改在并发与故障下保持正确
原子性 命令序列不会被其他命令插入;运行时错误不会整体回滚 通常支持真正的全成全败
回滚能力 DISCARD 只能丢弃尚未执行的队列;EXEC 后不能回滚 ROLLBACK 可撤销未提交事务中的修改
隔离机制 Redis 主线程串行执行,事务执行过程天然不被打断 通过锁、MVCC、隔离级别控制并发可见性
并发冲突处理 常用 WATCH 实现乐观锁 支持锁、死锁检测、隔离级别、冲突控制等
持久性 依赖 RDB/AOF 配置,强度取决于持久化策略 通常通过日志和刷盘策略提供较强持久性
适用场景 缓存计数、库存预扣、会话状态、轻量原子操作 订单、支付、账务、库存最终扣减等核心业务

Redis 为什么不提供传统回滚?

Redis 的设计取舍很明确:追求高性能、简单的数据模型和快速的内存操作。

传统回滚意味着系统必须保存修改前的数据状态,或者记录足以逆向恢复的日志。对于 Redis 而言,这会带来额外的内存占用、实现复杂度和性能成本。更重要的是,Redis 的许多操作本身已经是原子的,例如:

1
2
3
INCR page:view_count
HINCRBY product:1001 stock -1
LPUSH queue:email job-id

对于单个键或单条命令能完成的操作,Redis 本来就不需要事务。真正需要多步骤一致性的场景,Redis 通常建议通过 Lua 脚本、乐观锁或业务补偿来处理,而不是模仿关系型数据库的回滚模型。

WATCH:Redis 的乐观锁工具

Redis 提供 WATCH 来监视一个或多个键。如果客户端从 WATCHEXEC 期间,任意被监视的键被其他客户端修改,事务将被取消执行。

1
2
3
4
5
6
7
8
9
WATCH product:1001:stock

GET product:1001:stock
# 应用判断库存是否充足

MULTI
DECR product:1001:stock
LPUSH order:queue order-9001
EXEC

它适合“先读取、再根据读取结果写入”的场景,例如库存扣减、余额判断或抢购资格校验。

不过WATCH 不是悲观锁:它不会阻止其他客户端修改数据,只是在提交时告诉你“你读到的数据已经过期,请重试”。

一个订单扣库存案例

假设用户下单时需要完成两件事:

  1. 扣减商品库存。
  2. 创建订单记录。

如果全部数据都在 Redis 中,并且业务允许失败后通过补偿或重试处理,可以使用 Lua 脚本确保判断与修改在一次原子执行中完成。

但如果订单、支付、库存扣减都属于核心业务事实,通常应由关系型数据库承载最终状态。原因很简单:你往往需要确保“订单创建成功”和“库存扣减成功”要么同时成立,要么同时不发生。

典型架构就是:

  • Redis:缓存商品信息、承接高并发库存预检、限流、排队、分布式计数。
  • 关系型数据库:保存订单、支付、账务、最终库存等强一致业务数据。
  • 消息队列:解耦后续通知、积分、物流等异步流程。
  • 补偿机制:处理跨系统操作无法由单一数据库事务覆盖的问题。

Redis 可以帮你“快”,关系型数据库帮助你“稳”。它们不是对立关系,我们使用它们通常不是二选一,而是让两者各自处理最擅长的部分。

一些常见误区

误区一:Redis 的 MULTI/EXEC 等同于数据库事务

不等同。它提供连续执行与命令排队,但通常不提供运行时失败后的自动回滚。

误区二:Redis 是单线程,所以不会有并发问题

Redis 的命令执行是串行的,但业务逻辑可能跨越多次网络往返。你读取库存后、真正扣库存前,其他客户端仍可能修改库存。因此,读改写场景仍需使用 WATCH、Lua 脚本或其他并发控制手段。

误区三:用了 Redis 事务就可以不做幂等设计

不可以。网络超时、客户端重试、消费者重复投递等问题仍可能导致重复操作。订单创建、扣款、发券等关键动作应有唯一业务标识和幂等校验。

如何选择

如果你只需要把几个 Redis 命令连续执行,且能接受失败后通过重试或补偿修正,Redis 事务是轻量、直接的选择。

如果业务要求严格的“要么全部成功,要么完全不发生”,并且涉及订单、资金、账务、核心库存等不可轻易出错的数据,应优先使用关系型数据库事务。

如果操作包含“先判断再修改”,可优先考虑 Redis Lua 脚本;若涉及多个服务、多个数据库或外部系统,则需要进一步考虑消息可靠投递、事务消息、Saga 或补偿事务等分布式一致性方案。

总结

Redis 事务与关系型数据库事务最大的区别,并不是命令名称不同,而是承诺边界不同。

Redis 负责把一组命令紧凑、顺序地执行;关系型数据库负责让一组数据修改在错误、并发与故障面前仍然保持一致。把 Redis 当成“更快的数据库事务”容易埋下隐患,把它当成高性能数据操作与协调工具,才能真正发挥它的价值。