🌈个人主页 :一条泥憨鱼 (欢迎各位大佬莅临)

前言:
在日常开发中,很多开发者要么不知道 Redis 有事务,要么以为它和 MySQL 的事务是一回事。两者差挺远的,搞混了线上容易出问题。这篇文章把核心机制、坑点、乐观锁和面试常问的几个问题串一遍。
Redis 事务到底是什么
把多条命令塞进一个队列,到点了按顺序一口气执行完。执行过程中其他客户端插不进来。就这四个命令:
**- MULTI --- 开始攒命令
- EXEC --- 队列里的命令全部执行
- DISCARD --- 不玩了,清空队列
- WATCH --- 盯住一个或多个 key,EXEC 之前如果有人动过,事务自动取消**
上手:转账的例子
A 账户扣 100,B 加 100。先看看正常流程:
bash
127.0.0.1:6379> SET account:A 500
OK
127.0.0.1:6379> SET account:B 200
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY account:A 100
QUEUED
127.0.0.1:6379> INCRBY account:B 100
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 400
2) (integer) 300
MULTI 之后每条命令都返回 QUEUED,说明在排队。EXEC 一执行,结果按入队顺序返回。
想反悔的话:
bash
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> DISCARD
OK
队列清空,什么都没发生。
最容易踩的坑:错误处理
Redis 事务里的错误分两种,处理逻辑完全不同,搞混了线上真的会出事。
第一种:语法错误(入队时就发现了)
命令拼错了、参数不对,Redis 当场报错,EXEC 的时候整个事务直接取消:
bash
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> NOTACMD
(error) ERR unknown command 'NOTACMD'
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
k1 和 k2 都没写进去。这种还好,至少是"全有或全无"。
第二种:运行时错误(执行了才发现不对)
语法没问题,执行时栽了------比如对字符串做 INCR。Redis 会跳过那条出错的命令,其余的照常执行,已经执行的不会回滚:
bash
127.0.0.1:6379> SET k1 "hello"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 "world"
QUEUED
127.0.0.1:6379> INCR k1
QUEUED
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
k1 变成了 world,k2 也写进去了。只有 INCR 那条挂了。这就是 Redis 事务和 MySQL 事务最要命的区别------Redis 事务不支持回滚。
不支持回滚是故意的
很多人第一次知道这个会觉得很坑。说实话我第一次也这么觉得。
但 Redis 官方的逻辑是:运行时错误是程序员的 bug------你对 String 类型的 key 做 INCR,这应该在测试阶段就发现。为此让 Redis 支持回滚,意味着每次操作前要保存旧数据、出错时逐条恢复,这对一个追求极简和高性能的东西来说太重了。
所以用 Redis 事务,你得自己保证命令是对的。别把它当 MySQL 用。
WATCH:乐观锁
MULTI/EXEC 有个盲区:如果事务里的命令依赖某个 key 当前的值,而这个值在 MULTI 之后、EXEC 之前被别的客户端改了,事务照样正常执行,结果就出错了。
比如库存剩 1,两个客户端同时读到 1,都觉得自己能下单,两个都执行了 DECR,库存直接变 -1。
WATCH 解决的就是这个。
怎么用
bash
127.0.0.1:6379> SET stock 1
OK
127.0.0.1:6379> WATCH stock
OK
127.0.0.1:6379> GET stock
"1"
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECR stock
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 0
被别人抢先改了
另一个客户端在 WATCH 之后、EXEC 之前动了 stock,你的 EXEC 返回 nil,一条命令都不会执行:
bash
127.0.0.1:6379> EXEC
(nil)
收到 nil 就说明事务没执行。重读数据、重新判断、再试一次,这就是乐观锁的标准套路。
几个细节:
**- EXEC 执行后(不管成败),WATCH 自动解除
- DISCARD 也会解除所有 WATCH
- 想主动解除可以执行 UNWATCH**
ACID 四个维度怎么看
你可以这样说:
- 原子性 --- 有条件的。队列命令按顺序执行、不被插入,这部分是原子的。但运行出错不回头,不是严格意义的原子性。
-一致性 --- 语法错误全取消,不会脏写。
-
隔离性 --- EXEC 前队列里的命令对其他客户端不可见,执行不被打断。
-
持久性--- 看你开了什么。AOF + appendfsync always 就有持久性;RDB 或 AOF everysec 就有可能丢数据。
Redis 事务 vs 关系型数据库
|---------|-------------|---------------|
| 对比项 | Redis事务 | 关系型数据库事务 |
| 回滚支持 | 不支持 | 支持 |
| 运行时错误处理 | 跳过出错命令,继续执行 | 回滚整个事务 |
| 隔离级别 | 执行期间不被打断 | 多级隔离级别可选 |
| 持久性 | 取决于持久化配置 | 默认持久化 |
| 使用场景 | 简单批量操作 | 复杂业务逻辑,强一致性要求 |
简单说:Redis 事务轻、快、弱一致。别拿它处理复杂业务逻辑。
实际开发中的建议
别把 Redis 事务当 MySQL 事务用。 它不回滚。如果你真的需要"要么全做要么不做",光靠 Redis 事务做不到。
复杂逻辑用 Lua 脚本。 Redis 执行 Lua 是原子的,脚本里还能加判断逻辑,比事务灵活。生产环境里,需要复杂原子操作的基本都用 Lua,很少裸写事务。
WATCH 重试要设上限。 并发高的时候可能反复失败,不设上限就是死循环。
事务里的命令尽量短。 Redis 单线程处理,你的队列越长,所有其他命令都得等着。
疑点解惑
Redis 事务是原子的吗?
有条件的原子。命令顺序执行不被打断,但出错不回头。回答时分两种情况:语法错全取消,运行错只跳当前命令。
为什么不支持回滚?
官方觉得运行错误是 bug,测试阶段就该抓到。加回滚等于给 Redis 塞一套恢复机制,又复杂又慢,跟 Redis 的设计方向冲突。
WATCH 是悲观锁还是乐观锁?
乐观锁。不加锁,不阻塞,EXEC 时检查 key 有没有被改过。被改了返回 nil,客户端自己决定要不要重试。
Redis 事务和 Lua 脚本的区别?
都能保证原子执行。Lua 脚本能做条件判断,有逻辑控制,事务就是纯命令队列。生产上用复杂原子操作,优先 Lua。
最后
Redis 事务就是命令排队 + 顺序执行,有隔离性但不回滚。WATCH 补上了乐观锁的能力。技术本身不复杂,复杂的是搞清楚什么场景该用什么工具。别把 Redis 事务想成 MySQL 事务的平替------它不是。