Redis 的事务实现与传统关系型数据库(如 MySQL)有根本性的不同。它不保证严格的原子性(Atomicity),也没有回滚(Rollback)机制。其核心思想是将多个命令打包,然后顺序地执行,并保证在执行期间不会被其他客户端的命令打断

Redis 事务的实现主要依赖于以下几个命令和机制:

核心命令

  • MULTI:标记一个事务块的开始。随后的命令都不会立即执行,而是被服务器放入一个队列中。
  • EXEC:执行事务队列中的所有命令,并返回所有命令的返回值。
  • DISCARD:取消事务,清空事务队列。
  • WATCH:监视一个或多个 key。如果在 EXEC 命令执行前,这些被监视的 key 被其他客户端修改,那么整个事务将被取消(EXEC 返回 nil 表示事务失败)。
  • UNWATCH:取消所有 WATCH 命令对 key 的监视。

事务执行的三阶段

一个完整的 Redis 事务过程可以分为三个阶段:

  1. 事务开始 (MULTI)

    • 客户端执行 MULTI 命令,服务器会将此客户端的状态从非事务状态切换到事务状态。之后,除了 EXEC, DISCARD, WATCH, MULTI 这几个命令外,其他命令都不会立即执行。
  2. 命令入队

    • 当客户端处于事务状态时,它发送的正常命令(如 SET, GET, SADD 等)不会被立即执行,而是被服务器放入一个名为 事务队列(FIFO队列) 的数组中。
  3. 事务执行 (EXEC) 或 丢弃 (DISCARD)

    • EXEC:服务器收到 EXEC 命令后,会遍历事务队列,依次执行其中的所有命令,并将所有结果收集起来,一次性返回给客户端。在此执行期间,Redis 服务器不会处理其他客户端的任何请求,从而保证了这些命令被隔离(Isolation)地、不间断地执行。
    • DISCARD:服务器收到 DISCARD 命令后,会清空事务队列,并将客户端状态切换回非事务状态。

为什么没有回滚?

这是 Redis 事务最显著的特点。Redis 在设计上选择了一种更简单、更高效的方式:

  • 语法错误(入队错误):如果一个命令在入队时就被检测出有语法错误(例如命令名写错、参数个数不对),那么整个事务在 EXEC 时都不会被执行。EXEC 会直接返回错误。
  • 运行错误:如果一个命令在入队时语法正确,但在 EXEC 运行时出错(例如,对字符串类型的 key 执行 SADD 操作),Redis 不会停止事务,也不会回滚之前已执行的命令,它会继续执行队列中的后续命令。
  • 设计哲学:Redis 的作者认为,Redis 事务中的错误通常都是编程错误(比如用错了命令类型),这种错误应该在开发阶段被发现,而不是在生产环境通过回滚来挽救。省略回滚功能使得 Redis 内部实现更简单、速度更快。

实现乐观锁的 WATCH 命令

WATCH 机制是 Redis 实现 乐观锁(Optimistic Locking) 的关键,用于解决事务的并发问题。

  • 原理

    1. 每个 Redis 数据库都维护一个 watched_keys 字典,它记录了哪个客户端监视了哪个 key。
    2. 当客户端 WATCH 一个 key 时,它就会记录当前 key 的版本号(实际上是一个全局递增的计数器)。
    3. 在任何客户端修改了一个 key 之后,这个 key 的版本号就会增加。
    4. 当执行 EXEC 时,服务器会检查所有被 WATCH 的 key 的版本号是否自 WATCH 以来发生过变化。
    5. 只要有一个 key 的版本号变化了,就说明有其他客户端修改过这个数据,当前客户端的事务安全性已被破坏。此时服务器会拒绝执行事务(EXEC 返回 nil)。
  • 流程

    1
    2
    3
    4
    5
    6
    客户端 A> WATCH balance
    客户端 A> MULTI
    客户端 A> DECRBY balance 100
    (此时客户端 B 执行了 `SET balance 1000`,修改了 balance)
    客户端 A> EXEC
    (返回 nil,因为 WATCH 的 key 被修改了,事务执行失败)

    客户端 A 需要做的就是重试这个事务(重新 WATCH 并执行流程)。

与 Lua 脚本的对比

Redis 2.6 之后引入了 Lua 脚本,它提供了另一种更强大的事务性执行方式:

  • 更强的一致性:整个脚本在执行时是以原子方式进行的,中途绝不会被其他命令打断,无需使用 WATCH
  • 更灵活:可以在脚本中实现复杂的逻辑和计算。
  • 因此,对于复杂的多命令操作,Lua 脚本通常是比 MULTI/EXEC 事务更推荐的选择

总结

特性 Redis 事务实现方式
原子性 部分原子性:要么全部执行,要么全部不执行(仅在入队错误时)。运行时错误不会中断和回滚。
隔离性 严格隔离EXEC 执行期间,命令队列是串行化执行的,不会被中断。
回滚 不支持
一致性 通过 WATCH 实现的乐观锁 来保证在并发环境下数据的一致性。
用途 适合用于需要一次性、按顺序执行多个命令,且不希望中途被打断的场景。对于需要回滚或复杂逻辑的场景,应优先选用 Lua 脚本

简单来说,Redis 事务的本质是 命令的批量打包和顺序执行,配合 WATCH 实现了一种乐观锁的并发控制机制。