摘要
事务 Transaction 命令
| 命令 |
作用 |
所处阶段 |
返回值 |
典型使用场景 |
| MULTI |
开启一个事务,之后的命令进入队列 |
事务开始 |
OK |
批量原子执行多个命令 |
| EXEC |
执行事务队列中的所有命令 |
事务提交 |
数组(每个命令的执行结果)或 nil |
提交事务 |
| DISCARD |
放弃事务队列中的所有命令 |
事务取消 |
OK |
回滚未执行的事务 |
| WATCH key [key …] |
监控一个或多个 key 是否被修改(乐观锁) |
事务前 |
OK |
并发控制、防止覆盖更新 |
| UNWATCH |
取消对所有 key 的监控 |
事务前 / 中 |
OK |
主动释放监控 |
MULTI / EXEC —— 基本事务示例
1 2 3 4 5 6 7 8 9 10 11 12
| 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 —— 放弃事务
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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)
|
WATCH / EXEC —— 乐观锁示例
1 2 3 4 5 6
| WATCH key 读取并校验数据 MULTI 写操作... EXEC 如果返回 nil → 重试
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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
|
1 2
| 127.0.0.1:6379> INCRBY balance 50 (integer) 150
|
1 2
| 127.0.0.1:6379> EXEC (nil)
|
-
说明
- 因为 balance 在 WATCH 之后被客户端 B 修改过:
- Redis 判定监控失败
- EXEC 返回 nil
- 事务不会执行
- 这是典型的 乐观锁(CAS)机制。
UNWATCH —— 取消监控
1 2 3 4 5
| 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 事务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| 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 { 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("事务未执行或执行失败"); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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之间的输出,要在一个连接中才能生效