Redis 知识梳理(4)

作者:没有四次元口袋的蓝胖

日期:2026-10-14

标签:Redis, 事务, 发布订阅, WATCH

Redis知识梳理(4)

写在前面

Redis 是单线程模型,但业务中我们经常要执行"多个操作"。怎么保证这几个操作的原子性 和一致性?这就是 Redis 事务要解决的问题。

另外,Redis 还提供了**发布/订阅(Pub/Sub)**机制,让多个客户端之间进行消息广播。虽然不是专业 MQ,但在很多场景下也能派上用场。

本篇把这两块讲透:

  1. Redis 事务(MULTI/EXEC)是什么、怎么用、有什么坑
  2. WATCH 乐观锁机制
  3. 发布/订阅模型

一、Redis 事务

1.1 什么是 Redis 事务

Redis 事务是一种批量执行多个命令的机制:

  • 把多个命令打包
  • 按顺序一次性执行
  • 执行过程中不会被其他客户端打断

但是,Redis 事务 ≠ 传统数据库事务。这是最大的坑,必须分清。

1.2 核心命令

bash 复制代码
# 1. 开启事务
MULTI
# OK

# 2. 在事务中排队命令
SET account:a 100
# QUEUED
SET account:b 200
# QUEUED
INCR account:a
# QUEUED
GET account:a
# QUEUED

# 3. 执行事务
EXEC
# 1) OK
# 2) OK
# 3) (integer) 101
# 4) "101"

# 取消事务(放弃所有排队的命令)
DISCARD

事务执行的三个阶段:

复制代码
1. MULTI    → 开启事务,之后命令进入"排队"状态
2. 多个命令 → 全部排队,不立即执行,返回 QUEUED
3. EXEC     → 一次性执行所有命令(顺序执行,不可打断)

1.3 事务的特性 vs 传统事务

特性 传统数据库事务 Redis 事务
原子性 (Atomicity) 全部成功或全部回滚 ❌ 不支持回滚(见下文)
一致性 (Consistency) 操作前后数据库状态一致 部分一致(出错不回滚)
隔离性 (Isolation) 有锁/MVCC 隔离 ✅ 单线程执行,天然隔离
持久性 (Durability) 写入磁盘持久化 取决于持久化配置

Redis 事务的"原子性"真相:

bash 复制代码
# 假设 account 不存在
MULTI
SET account a b        # 语法错误,这条会被拒绝
INCR account           # 语法正确
EXEC
# 整个事务都会被拒绝,因为命令语法错误
bash 复制代码
# 假设 account 存的是字符串
MULTI
SET account "abc"
INCR account           # 运行时错误(INCR 需要整数)
SET account "def"
EXEC
# 1) OK                ← 这条执行了
# (error) ERR value is not an integer  ← 这条报错
# 3) OK                ← 这条继续执行(不回滚!)

结论:Redis 事务遇到运行时错误不会回滚,只会跳过出错的命令,继续执行后面的。

这是 Redis 事务的"不完全原子性",和 MySQL 的 ACID 完全不同。

🎯 面试高频:Redis 事务能保证 ACID 吗?

答:Redis 事务只有隔离性和一致性 (单线程执行不会被其他客户端打断),但不支持原子性 (失败不回滚)和持久性(依赖持久化策略)。这是面试必考题。

1.4 事务的错误处理

Redis 事务分两种错误:

1. 语法错误(入队时就能检测到的错误):

  • 整个事务被拒绝,EXEC 时直接报错
bash 复制代码
MULTI
SET key value          # 正确
GET key                # 正确
XXXCMD                 # 命令不存在 → 整个事务被拒绝
EXEC
(error) EXECABORT Transaction discarded because of previous errors.

2. 运行时错误(入队时检测不到,执行时才报错):

  • 出错的那条命令返回错误,其他命令继续执行
  • 这就是"事务无法回滚"的根源

1.5 事务与 Lua 脚本

当需要"原子地执行多个操作,并且根据结果决定后续步骤"时,Lua 脚本是更好的选择:

bash 复制代码
EVAL "
    local current = redis.call('GET', KEYS[1])
    if tonumber(current) > tonumber(ARGV[1]) then
        redis.call('INCRBY', KEYS[1], -ARGV[2])
        return 1
    end
    return 0
" 1 account:balance 100 50

Lua 脚本在 Redis 中整体作为一个命令执行,具有真正的原子性。

方式 原子性 条件判断 适用场景
事务 部分原子(不支持回滚) 不支持 简单批量命令
Lua 脚本 完全原子 支持 复杂业务逻辑

二、WATCH 乐观锁

2.1 为什么需要 WATCH?

Redis 事务本身不能解决并发问题。考虑这个场景:

复制代码
场景:转账

账户 A 余额:100
账户 B 余额:0

客户端1(A转50给B):
  1. 读 A 余额 → 100
  2. A 余额 -= 50  → 50
  3. B 余额 += 50  → 50

客户端2(同时,A转30给B):
  1. 读 A 余额 → 100(还没看到客户端1的修改)
  2. A 余额 -= 30  → 70
  3. B 余额 += 30  → 30

结果:A 只剩 70(应该是20),B 有 30(应该是80),钱凭空消失了!

问题根源: Redis 事务没有"读-写"之间的隔离,客户端在读取余额到修改余额之间,其他客户端可能已经改了数据。

2.2 WATCH 的工作机制

WATCH 是一种乐观锁(CAS,Compare And Swap):

bash 复制代码
# 客户端1:
WATCH account:a                # 监视 account:a
balance = GET account:a        # 读取余额(假设=100)
MULTI                          # 开启事务
DECRBY account:a 50            # 队列化修改
INCRBY account:b 50
EXEC                           # 执行事务

# 如果 WATCH 的键在 EXEC 之前被其他客户端修改过
# EXEC 会返回 nil,事务执行失败
# 客户端需要重新尝试

WATCH 的流程:

复制代码
1. WATCH key            → 标记监视该键
2. 读取键值            → 记住当前值
3. MULTI               → 开始事务
4. 队列化修改命令      → 准备修改
5. EXEC                → 提交事务
   ├─ 监视的键未被其他客户端修改 → 事务成功执行
   └─ 监视的键被其他客户端修改 → 事务失败,返回 nil

2.3 完整转账示例

bash 复制代码
# 转账函数(伪代码)
function transfer(from, to, amount):
    while true:
        WATCH from
        balance = GET from
        if balance < amount:
            UNWATCH
            return "余额不足"
        
        MULTI
        DECRBY from amount
        INCRBY to amount
        result = EXEC
        
        if result is not null:
            return "转账成功"
        # else:事务失败(因为 from 被其他客户端修改了),重试

Java 客户端示例(Jedis):

java 复制代码
public boolean transfer(String from, String to, long amount) {
    Jedis jedis = jedisPool.getResource();
    while (true) {
        // 1. 监视转出账户
        jedis.watch(from);
        
        // 2. 读取余额
        long balance = Long.parseLong(jedis.get(from));
        if (balance < amount) {
            jedis.unwatch();
            return false;
        }
        
        // 3. 开启事务
        Transaction tx = jedis.multi();
        tx.decrBy(from, amount);
        tx.incrBy(to, amount);
        
        // 4. 执行事务
        List<Object> result = tx.exec();
        if (result != null) {
            return true;    // 成功
        }
        // 失败:继续重试
    }
}

2.4 WATCH 特性总结

特性 说明
类型 乐观锁(CAS)
监视对象 一个或多个 key
触发时机 EXEC 时检查 key 是否被修改
修改了怎么办 EXEC 返回 nil,事务失败
取消监视 UNWATCH 或 EXEC 执行后自动解除

注意:

  • WATCH 在 MULTI 之前执行
  • 一旦 EXEC 执行,所有 WATCH 自动解除
  • 也可以用 UNWATCH 主动取消所有监视

2.5 WATCH 与分布式锁对比

对比 WATCH 乐观锁 分布式锁
思路 乐观尝试,失败重试 悲观锁定,独占访问
适用场景 并发不激烈 高并发、冲突频繁
性能 冲突少时效率高 开销较大但可控
死锁风险 无 需注意释放

三、发布订阅(Pub/Sub)

3.1 什么是发布订阅

Redis 提供了发布/订阅(Publish/Subscribe)消息模型:

  • 发布者(Publisher):向某个频道发送消息
  • 订阅者(Subscriber):订阅一个或多个频道,接收消息
  • 频道(Channel):消息的传输通道

类比:微信群聊------一个人发消息(发布),群里其他人都能收到(订阅)。

3.2 基本命令

bash 复制代码
# 订阅频道(阻塞等待消息)
SUBSCRIBE channel:news

# 订阅多个频道
SUBSCRIBE channel:news channel:sports channel:tech

# 按模式订阅(通配符)
PSUBSCRIBE channel:*

# 发布消息到频道
PUBLISH channel:news "Redis 7.0 发布了!"
PUBLISH channel:sports "国足赢了!"

# 查看订阅某频道的客户端数量
PUBSUB NUMSUB channel:news

订阅者终端输出:

复制代码
127.0.0.1:6379> SUBSCRIBE channel:news
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel:news"
3) (integer) 1          # 当前订阅的频道数

1) "message"            # 消息类型
2) "channel:news"       # 频道
3) "Redis 7.0 发布了!"  # 消息内容

3.3 Pub/Sub 工作流

复制代码
Publisher(发布者)
     │
     │ PUBLISH channel:news "xxx"
     ▼
┌────────────────────────┐
│    Redis Pub/Sub       │
│    频道:channel:news  │
└────┬────┬────┬────────┘
     │    │    │
     │    │    │ 广播给所有订阅者
     ▼    ▼    ▼
   Sub1 Sub2 Sub3(订阅者)

关键点:

  • 消息不持久化,发完就没了
  • 订阅者离线期间的消息收不到
  • 适合实时性要求高、允许丢消息的场景

3.4 Pub/Sub 适用场景

场景 说明
实时通知 订单状态变更通知所有相关服务
配置广播 主服务广播配置变更,从服务立即应用
聊天系统 频道聊天、群聊消息
缓存同步 多实例缓存失效通知
事件驱动架构 微服务间事件发布/订阅

3.5 Pub/Sub 的局限

致命缺陷:消息不持久化,离线丢失

复制代码
客户端A 订阅了 channel:news
客户端B 在 10:00 发布消息 "xxx"
客户端A 此时离线
客户端A 在 10:01 重新上线
结果:A 收不到 "xxx" 这条消息

因此,Pub/Sub 不适用于:

  • 需要可靠传递的场景(订单、支付)
  • 需要回溯历史消息的场景

3.6 Redis Stream(替代方案)

Redis 5.0 引入了 Stream 类型,专门用于解决 Pub/Sub 的局限:

特性 Pub/Sub Stream
消息持久化 ❌ ✅
离线消息 ❌ ✅
消息回溯 ❌ ✅
消费者组 ❌ ✅
ACK 机制 ❌ ✅

Stream 是 Redis 实现的专业消息队列,功能强大,但篇幅较长,后面单独讲。

3.7 Pub/Sub vs 专业 MQ 对比

对比 Redis Pub/Sub RabbitMQ / Kafka
性能 快(内存) 高
持久化 ❌ ✅
消息回溯 ❌ ✅
消息确认 ❌ ✅
复杂路由 ❌ ✅
适用场景 轻量级实时广播 企业级消息中间件

四、事务、WATCH 与 Pub/Sub 的选择

复制代码
需要多个命令原子执行?
    │
    ├── 不需要条件判断 → MULTI/EXEC 事务
    │
    ├── 需要条件判断(如检查余额)→ Lua 脚本
    │
    └── 需要并发保护 → WATCH 乐观锁 + 重试
                      或 分布式锁

需要多个客户端消息广播?
    │
    ├── 允许消息丢失 → Pub/Sub
    │
    └── 需要可靠传递 → Stream / RabbitMQ / Kafka

五、常见坑点与面试题

5.1 坑点总结

坑 描述 解决方案
事务不回滚 运行时错误不会触发回滚 用 Lua 脚本代替
WATCH 重试风暴 高并发下事务频繁失败 用分布式锁替代
Pub/Sub 丢消息 离线订阅者收不到消息 用 Stream 或 MQ
事务中的阻塞 事务内执行慢命令会阻塞其他客户端 控制事务命令数量

5.2 面试高频题

Q1:Redis 事务支持 ACID 吗?

答:Redis 事务部分支持:

  • ✅ 隔离性:单线程执行,事务期间其他命令排队
  • ✅ 一致性:事务前后数据状态一致
  • ❌ 原子性:运行时错误不会回滚
  • ❌ 持久性:取决于持久化配置

Q2:WATCH 是乐观锁还是悲观锁?

答:乐观锁(CAS)。先不加锁,等到提交时再检查冲突,有冲突就重试。

Q3:Pub/Sub 和 Stream 的区别?

答:Pub/Sub 消息不持久化、不保证送达;Stream 持久化、支持消费者组、消息确认,是专业消息队列。

Q4:如何用 Redis 实现转账?

答:用 WATCH + MULTI/EXEC 乐观锁方案,或者用 Lua 脚本保证原子性。


思维导图速览

复制代码
事务与发布订阅
│
├── Redis 事务(MULTI/EXEC)
│   ├── 命令排队 + 一次性执行
│   ├── ❌ 不支持回滚(运行时错误继续执行)
│   ├── ✅ 隔离性(单线程)
│   ├── 适用:批量无依赖操作
│   └── 不适合:需要条件判断的场景
│
├── WATCH 乐观锁(CAS)
│   ├── 监视键 → 读值 → MULTI → EXEC
│   ├── 键被修改 → 事务失败(返回 nil)
│   ├── 需要客户端重试
│   ├── 适合:并发不激烈的场景
│   └── 与分布式锁互补
│
├── Lua 脚本
│   ├── 整个脚本原子执行
│   ├── 支持条件判断
│   └── 替代复杂事务
│
└── 发布订阅(Pub/Sub)
    ├── SUBSCRIBE/PUBLISH
    ├── 频道订阅 + 实时广播
    ├── ❌ 消息不持久化,离线丢失
    ├── 适用:实时通知、配置广播
    └── 替代方案:Stream / RabbitMQ

写在最后

Redis 事务、WATCH、Pub/Sub 是三个独立的概念,但都围绕"如何正确处理多客户端并发"这个主题。

三个关键认知:

  1. Redis 事务 ≠ MySQL 事务------它不支持回滚,运行时错误会跳过继续
  2. WATCH 是乐观锁------冲突时重试,高并发下可能风暴
  3. Pub/Sub 不可靠------适合实时广播,需要可靠性用 Stream

这三个在面试中出场率都很高,尤其是"Redis 事务支持 ACID 吗"这个问题,几乎每场面试都会问。

相关推荐
l1t1 小时前
一个clickhouse易错的语法问题
数据库·sql·clickhouse
啵啵啵鱼1 小时前
六、联合查询
数据库·oracle
foolishlee1 小时前
Neon compute GetPage
数据库
我不会起名字3221 小时前
Redis 从 0 到 1(十一):缓存穿透/击穿/雪崩三连击,Go 3 套防御代码
redis·缓存·go·布隆过滤器·雪崩
对空六课3 小时前
两个统计平台数据不同:用同一时间窗和事件样本做差异定位
大数据·前端·网络·数据库
wno7048 小时前
postgresSQL压缩版安装与基本命令
数据库·postgresql
聚美智数10 小时前
智能结构化识别-图片结构解析-图像结构化解析-照片结构化解析
android·java·数据库
瀚高PG实验室10 小时前
瀚高数据库常用系统表查询入门指南(附常用SQL)
数据库·sql·postgresql·瀚高数据库
ben9518chen11 小时前
mysql数据库基本操作
数据库·mysql