Redis 命令详解:事务 Transaction

摘要

事务 Transaction 命令

  • Redis 事务命令总览表

命令 作用 所处阶段 返回值 典型使用场景
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)
  • 说明

    • DISCARD 会:
      • 清空事务队列
      • 退出事务状态
    • 队列中的命令 不会被执行。

WATCH / EXEC —— 乐观锁示例

  • 乐观锁事务模板

1
2
3
4
5
6
WATCH key
读取并校验数据
MULTI
写操作...
EXEC
如果返回 nil → 重试
  • 场景:实现一个安全的余额扣减逻辑,如果余额在事务执行前被别人修改,则放弃本次事务。

  • 客户端 A

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
  • 客户端 B(并发修改)

1
2
127.0.0.1:6379> INCRBY balance 50
(integer) 150
  • 客户端 A 提交事务

1
2
127.0.0.1:6379> EXEC
(nil)
  • 说明

    • 因为 balance 在 WATCH 之后被客户端 B 修改过:
      • Redis 判定监控失败
      • EXEC 返回 nil
      • 事务不会执行
    • 这是典型的 乐观锁(CAS)机制。

UNWATCH —— 取消监控

  • 示例:取消 WATCH 监控

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 事务

  • 实际项目中Redis事务很少使用,因为WATCH + MULTI 的性能不如 LuaINCR 等原子指令。

  • 同一事务代码必须在同一线程内完成,推荐使用 SessionCallback

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 {
// 监视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 命令查询

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 以及 MULTIEXEC之间的输出,要在一个连接中才能生效