(三十五)Redis 命令详解:事务 Transaction
·
摘要
- 本文基于
redis-7.4.7 - Redis官网:https://redis.io/
Redis Transaction 简介
- 最关键的核心点是:Redis 的事务模型和关系型数据库(如 MySQL)的事务模型有本质区别。
- 关系型数据库事务:强调 ACID(原子性、一致性、隔离性、持久性),特别是在执行过程中,命令会看到一致的数据库状态(通过锁或MVCC),并且可以回滚。
- Redis 事务:主要目的是将多个命令打包,按顺序、连续、排他地执行。它更接近于一个批量执行和乐观锁的机制。它不提供回滚功能(只有命令入队时的语法检查,没有执行时的错误回滚)。
事务 Transaction 命令详解
- Redis 事务命令总览表
| 命令 | 作用 | 所处阶段 | 返回值 | 典型使用场景 |
|---|---|---|---|---|
| MULTI | 开启一个事务,之后的命令进入队列 | 事务开始 | OK |
批量原子执行多个命令 |
| EXEC | 执行事务队列中的所有命令 | 事务提交 | 数组(每个命令的执行结果)或 nil |
提交事务 |
| DISCARD | 放弃事务队列中的所有命令 | 事务取消 | OK |
回滚未执行的事务 |
| WATCH key [key …] | 监控一个或多个 key 是否被修改(乐观锁) | 事务前 | OK |
并发控制、防止覆盖更新 |
| UNWATCH | 取消对所有 key 的监控 | 事务前 / 中 | OK |
主动释放监控 |
MULTI / EXEC —— 基本事务示例
- 可以在 MULTI 和 EXEC 之间使用的命令
几乎所有 “写操作” 和 “只读操作” 命令都可以放入 MULTI...EXEC 块中。例如:
字符串操作:SET, GET, INCR, MSET
哈希操作:HSET, HGET, HINCRBY
列表操作:LPUSH, RPOP, LRANGE
集合操作:SADD, SREM, SMEMBERS
有序集合操作:ZADD, ZRANGE, ZREM
流操作(Redis 5.0+):XADD, XREADGROUP
地理空间、位图等:所有相关操作命令。
管理类命令:DEL, EXPIRE, PERSIST 等。
一个重要的特例:WATCH 命令
WATCH 命令用于实现乐观锁,它必须在 MULTI 命令之前执行,用来监视一个或多个键。
阻塞命令在事务中是被禁止的
如 BLPOP, BRPOP, BRPOPLPUSH, BZPOPMIN, BZPOPMAX, XREAD, XREADGROUP(带阻塞选项),因为事务要求所有命令被一次性入队,而阻塞命令需要等待数据到达,这会阻塞整个服务器,导致死锁。Redis 会直接返回错误。
- 示例:原子递增两个计数器
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> INCR counter:a
QUEUED
127.0.0.1:6379> INCR counter:b
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 1
2) (integer) 1
- 说明
- MULTI 后,所有命令只会进入队列,返回 QUEUED,不会立即执行。
- EXEC 时才真正执行。
- Redis 保证:
- 命令顺序执行
- 执行期间不会被其他客户端插入命令
- 但不支持回滚(某条命令失败,不会自动撤销已执行的命令)。
DISCARD —— 放弃事务
- 示例:取消事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> GET k1
(nil)
- 说明
- DISCARD 会:
- 清空事务队列
- 退出事务状态
- 队列中的命令 不会被执行。
- DISCARD 会:
WATCH / EXEC —— 乐观锁示例
- 乐观锁事务模板
WATCH key
读取并校验数据
MULTI
写操作...
EXEC
如果返回 nil → 重试
- 场景:实现一个安全的余额扣减逻辑,如果余额在事务执行前被别人修改,则放弃本次事务。
- 客户端 A
127.0.0.1:6379> SET balance 100
OK
127.0.0.1:6379> WATCH balance
OK
127.0.0.1:6379> GET balance
"100"
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY balance 30
QUEUED
- 客户端 B(并发修改)
127.0.0.1:6379> INCRBY balance 50
(integer) 150
- 客户端 A 提交事务
127.0.0.1:6379> EXEC
(nil)
- 说明
- 因为 balance 在 WATCH 之后被客户端 B 修改过:
- Redis 判定监控失败
- EXEC 返回 nil
- 事务不会执行
- 这是典型的 乐观锁(CAS)机制。
- 因为 balance 在 WATCH 之后被客户端 B 修改过:
UNWATCH —— 取消监控
- 示例:取消 WATCH 监控
127.0.0.1:6379> WATCH k1 k2
OK
127.0.0.1:6379> UNWATCH
OK
- 说明
- 取消所有被监控的 key。
- 常见用途:
- 逻辑判断失败,不再继续事务
- 主动释放监控,避免误触发事务失败
- 如果执行了 EXEC 或 DISCARD,Redis 会自动 UNWATCH。
关键行为总结(工程视角)
| 维度 | 行为 |
|---|---|
| 原子性 | EXEC 内命令顺序执行,不会被其他客户端插入 |
| 隔离性 | 不是数据库级事务隔离,仅保证执行期串行 |
| 回滚能力 | ❌ 不支持回滚 |
| 并发控制 | 通过 WATCH 实现乐观锁 |
| 失败行为 | WATCH 冲突 → EXEC 返回 nil |
| 性能 | 队列入内存,执行非常快 |
SpringBoot 中使用 Redis 事务
- 实际项目中Redis事务很少使用,因为
WATCH + MULTI的性能不如Lua或INCR等原子指令。 - 同一事务代码必须在同一线程内完成,推荐使用
SessionCallback
String key = "stock:1001";
// 初始化库存
redisTemplate.opsForValue().set(key, 10);
// 执行事务操作
List<Object> result = redisTemplate.execute(new SessionCallback<>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
try {
// 监视key的变化
operations.watch(key);
// 安全获取当前库存值
Object stock = operations.opsForValue().get(key);
// 检查库存是否充足
if (stock == null || (Integer) stock <= 0) {
operations.unwatch(); // 取消监视
System.out.println("库存不足,无法扣减");
return null;
}
// 开始事务
operations.multi();
operations.opsForValue().decrement(key);
// 执行事务并返回结果
List<Object> execResult = operations.exec();
System.out.println("事务执行成功,扣减库存后结果: " + execResult);
return execResult;
} catch (Exception e) {
// 发生异常时取消监视
operations.unwatch();
System.err.println("事务执行异常: " + e.getMessage());
throw new DataAccessException("Redis事务执行失败", e) {
};
}
}
});
if (result != null) {
System.out.println("最终事务结果: " + result);
} else {
System.out.println("事务未执行或执行失败");
}
- 验证事务是否生效,可以在 redis 终端执行
MONITOR命令查询
127.0.0.1:6379> MONITOR
OK
1768054048.497866 [0 127.0.0.1:53634] "HELLO" "3" "AUTH" "(redacted)" "(redacted)"
1768054048.510741 [0 127.0.0.1:53634] "CLIENT" "SETINFO" "lib-name" "Lettuce"
1768054048.510759 [0 127.0.0.1:53634] "CLIENT" "SETINFO" "lib-ver" "6.6.0.RELEASE/643bd47"
1768054048.538458 [0 127.0.0.1:53634] "SET" "stock:1001" "10"
1768054048.557699 [0 127.0.0.1:53635] "HELLO" "3" "AUTH" "(redacted)" "(redacted)"
1768054048.559907 [0 127.0.0.1:53635] "CLIENT" "SETINFO" "lib-name" "Lettuce"
1768054048.559918 [0 127.0.0.1:53635] "CLIENT" "SETINFO" "lib-ver" "6.6.0.RELEASE/643bd47"
1768054048.562642 [0 127.0.0.1:53635] "WATCH" "stock:1001"
1768054048.566201 [0 127.0.0.1:53634] "GET" "stock:1001"
1768054048.583727 [0 127.0.0.1:53635] "MULTI"
1768054048.645412 [0 127.0.0.1:53635] "DECR" "stock:1001"
1768054048.645421 [0 127.0.0.1:53635] "EXEC"
重点看:
WATCH以及MULTI和EXEC之间的输出,要在一个连接中才能生效
更多推荐




所有评论(0)