Redis 事务那些事儿:实用技巧和避坑指南!
一、为什么我们关心Redis事务?
在Java开发的日常工作中,Redis几乎无处不在。你可能用它做缓存、排行榜、分布式锁,甚至用它做轻量级的数据存储。
但随着业务复杂度提升,很多人都会遇到这样的问题:
- 多个Redis操作需要保证原子性,怎么做?
- Redis的事务和MySQL事务一样靠谱吗?
- WATCH、MULTI、EXEC这些命令到底怎么用?能不能防止并发下的数据不一致?
这些问题看似简单,实则暗藏不少坑。今天这篇文章,我想用最实在的语言,把Redis事务的本质、用法和注意事项讲清楚,帮你在实际开发中少踩坑。
二、Redis事务机制全解析
1. Redis到底支不支持事务?
结论先行:Redis支持事务,但和MySQL事务完全不是一回事。
MySQL事务强调ACID(原子性、一致性、隔离性、持久性),而Redis的事务机制更像是“命令打包、顺序执行”,没有复杂的隔离和回滚机制。
2. Redis事务的基本命令和用法
Redis事务的核心命令有四个:MULTI、EXEC、DISCARD、WATCH。
(1) MULTI/EXEC:事务的开始与提交
- MULTI:开启事务,后续命令进入队列
- EXEC:提交事务,队列中的命令依次执行
举个例子:
# 1. 初始化库存
127.0.0.1:6379> set a:stock 100
OK
127.0.0.1:6379> set b:stock 200
OK
# 2. 开启事务
127.0.0.1:6379> multi
OK
# 3. 将a:stock减1
127.0.0.1:6379> decr a:stock
QUEUED
# 4. 将b:stock减1
127.0.0.1:6379> decr b:stock
QUEUED
# 5. 实际执行事务
127.0.0.1:6379> exec
1) (integer) 99
2) (integer) 199
127.0.0.1:6379>
(2) DISCARD:放弃事务
如果在MULTI之后,发现有问题,可以用DISCARD放弃事务,清空命令队列。
(3) WATCH:乐观锁的实现
在并发场景下,单靠MULTI/EXEC还不够。比如转账操作,两个客户端同时读取余额,都判断可以转账,结果都扣了钱,余额就出错了。
这时候可以用WATCH命令,类似乐观锁。WATCH会监控指定的key,如果在事务提交前这些key被其他客户端修改,EXEC会失败,事务不会执行。
示例:
127.0.0.1:6379> get a:stock
"99"
127.0.0.1:6379> watch a:stock
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decr a:stock
QUEUED
127.0.0.1:6379> decr b:stock
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379>
如上所示,a:stock在EXEC前被其他客户端修改,EXEC会返回null,表示事务失败。
三、Redis事务的常见“坑”和注意事项
1. 没有回滚机制
只要EXEC执行,前面的命令就算后面有错,也不会回滚。比如:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name tom
QUEUED
127.0.0.1:6379> incr name
QUEUED
127.0.0.1:6379> set age 18
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379>
- set name tom执行成功。
- incr name 执行时报错(因为 name 是字符串,不能自增)。
- set age 18依然会被执行。
注意:Redis事务中,某条命令出错不会影响其他命令的执行,也不会回滚。
那如果我们想实现回滚的效果怎么办呢?
2. 如何用DISCARD修复?
如果你在MULTI之后发现命令写错了,可以在EXEC之前执行DISCARD,这样所有已入队的命令都不会被执行,数据不会被修改。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set addr bj
QUEUED
127.0.0.1:6379> incr addr
QUEUED
127.0.0.1:6379> set code 110
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get addr
(nil)
127.0.0.1:6379>
执行结果:
- set addr bj、set code 110都不会被执行。
- 事务被彻底放弃,Redis状态不会有任何变化。
注意点:
- 一旦执行了EXEC,就无法再用DISCARD撤销事务,已经执行的命令不会回滚。
- DISCARD只能在事务提交前使用,相当于“撤销”本次事务。
3. 没有隔离性
Redis事务期间,其他客户端依然可以操作相关key。WATCH只能监控key本身的变化,不能保证更复杂的业务一致性。
比如你WATCH了a:stock,但b:stock被其他客户端修改了,你的事务依然会执行。
四、实用干货:Redis事务的正确打开方式
(1) 能用原子命令就用原子命令
Redis本身很多命令就是原子的,比如INCR、DECR、SETNX等,优先用这些。
(2) 事务只保证命令的“批量、顺序、一次性”执行
不保证命令之间的隔离和回滚。
(3) WATCH适合乐观锁场景
比如扣库存、转账等,先WATCH关键key,判断条件后再MULTI/EXEC。
(4) 复杂业务建议用Lua脚本
Lua脚本在Redis中是原子执行的,可以实现更复杂的业务逻辑和回滚。
(5) 不要把Redis事务当成数据库事务用
Redis事务和MySQL事务完全不是一回事,不能指望它帮你兜底所有一致性问题。
五、Redis事务和ACID的对比
很多同学会问:Redis事务到底支持ACID的哪几项?
(1) 原子性(Atomicity)
Redis事务只保证“命令队列”整体的原子性,不保证单条命令的原子性。EXEC时,要么所有命令都执行,要么都不执行(WATCH监控失败时)。
(2) 一致性(Consistency)
Redis事务本身不保证数据的一致性,需要开发者自己保证。
(3) 隔离性(Isolation)
Redis事务没有严格的隔离性,事务执行期间,其他客户端可以修改相关key。
(4) 持久性(Durability)
取决于Redis的持久化配置(RDB、AOF),和事务机制本身无关。
一句话总结:Redis事务只保证“命令批量执行的原子性”,不保证隔离和回滚。
六、面试高频问答
(1) Redis事务和MySQL事务的区别?
- MySQL事务支持ACID,Redis事务只保证命令批量执行的原子性。
- MySQL事务有回滚机制,Redis事务没有。
- MySQL事务有隔离级别,Redis事务没有。
(2) Redis事务失败会回滚吗?
不会。只要EXEC执行,前面的命令就算后面有错,也不会回滚。
(3) WATCH命令的作用是什么?
实现乐观锁,监控指定key,防止并发下的数据不一致。
(4) Redis事务适合哪些场景?
适合批量命令、简单乐观锁场景。不适合强一致性、复杂回滚的业务。
本文地址:https://www.yitenyun.com/294.html