Redis:缓存、集群、优化与数据结构

1. 缓存更新

1.1 三种更新策略

对比 内存淘汰 超时剔除 主动更新(更优)
说明 不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存。 给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存。 编写业务逻辑,在修改数据库的同时,更新缓存。
一致性 一般
维护成本

业务场景:

  • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。

1.2 三种主动更新

1.2.1 旁路缓存

  • 核心逻辑
    调用者(业务代码)负责同时操作数据库和缓存。
    • 读:先查缓存 → 无则查库 → 写入缓存 → 返回
    • 写:先更新数据库 → 再删除/更新缓存
  • 特点
    • 最常用、最直观
    • ❗ 一致性依赖开发者手动保证(如"先更库后删缓存")
    • ❗存在并发下脏数据风险(需配合锁或延时双删等策略)
  • 适用场景:对一致性要求较高、可接受一定复杂度的业务系统。

1.2.2 读写穿透

  • 核心逻辑
    缓存层与数据库封装为一个统一服务(如 Redis + DB 中间件),调用者只与该服务交互。
    • 读:服务自动判断是否命中缓存,未命中则查库并回填
    • 写:服务同步更新缓存和数据库
  • 特点
    • ✅ 调用者无需关心缓存细节,解耦性好
    • ✅ 一致性由服务内部保障(原子操作或事务)
    • ❗ 需要额外基础设施支持(如自定义缓存代理层)
  • 适用场景:希望简化业务代码、追求高一致性的中大型系统。

1.2.3 异步回写

  • 核心逻辑
    调用者只操作缓存,缓存层异步将变更持久化到数据库。
    • 写:直接写入缓存 → 标记为"待同步" → 后台线程批量刷入DB
    • 读:直接从缓存读取(可能包含未落盘的数据)
  • 特点
    • ✅ 写入性能极高(异步非阻塞)
    • ✅ 最终一致性(适合容忍短暂不一致的场景)
    • ❗ 有数据丢失风险(若缓存宕机且未刷盘)
    • ❗ 实现复杂,需处理失败重试、顺序保证等问题
  • 适用场景:高性能写入需求、允许最终一致性的场景(如计数器、日志、消息队列缓冲)。

1.3 旁路缓存与数据一致性

1.3.1 三个问题

操作缓存和数据库时有三个问题需要考虑:

  1. 删除缓存还是更新缓存?
    • 更新缓存:每次更新数据库都更新缓存,无效写操作较多 ❌
    • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存 ✅
  2. 如何保证缓存与数据库的操作的同时成功或失败?
    • 单体系统,将缓存与数据库操作放在一个事务
    • 分布式系统,利用TCC等分布式事务方案
  3. 先操作缓存还是先操作数据库?
    • 先删除缓存,再操作数据库
    • 先操作数据库,再删除缓存**(更优)**
    • 问题:数据一致性

总结:

查询数据时,先查询缓存,未命中则则查询数据库,然后将结果写入缓存并设置TTL

修改数据时:先更新数据库,在删除缓存(可采用异步延时双删)

1.3.2 数据一致性问题

cpp 复制代码
============================================
1.方案一:先删除缓存,再操作数据库
============================================
初始状态:
  Cache: 10
  DB:    10

线程1(写操作)          线程2(读操作)
   |                        |
   | 1. 删除缓存 Cache=空    |
   |                        | 
   |                        | 2. 查询缓存 → miss
   |                        | 3. 查询DB → 得到旧值 DB=10
   |                        | 4. 写入缓存 → Cache=10
   |                        |
   | 5. 更新DB → DB=20      |
   |                        |
最终状态:
  Cache: 10 ← 脏数据!
  DB:    20

============================================
2.方案二:先操作数据库,再删除缓存(多线程问题)
============================================
初始状态:
  Cache: 无数据
  DB:    10

线程1(写操作)          线程2(读操作)
   |                        |
   |                        |1. 查询缓存,未命中,查询数据库 DB=10
   | 2. 更新DB → DB=20      | 
   |                        | 
   | 3. 删除缓存(等于没删)   |
   |                        | 
   |                        | 4. 写入缓存 Cache = 10
   |                        |
最终状态:
  Cache: 10 ← 脏数据!
  DB:    20
============================================
3.方案二:先操作数据库,再删除缓存(MySQL主从同步问题)
============================================
初始状态:
  Cache: 10
  DB:    10

线程1(写操作)          线程2(读操作)
   |                        |
   | 1. 更新DB → 主库DB=20   |
   | 2. 删除缓存 → Cache=空  | 
   |                        | 
   |                        | 3. 查询从DB → 得到旧值 从库DB=10
   |                        |
   | 4. 从库更新完成         | 
   |    从DB=20             |
   |                        | 5. 写入缓存 → Cache=10
   |                        |
   |                        |
  Cache: 10  ← 脏数据!
  DB:    20

1.3.3 异步延时双删

CAP 定理: 在分布式系统中,一致性、可用性和分区容错性三者最多只能同时满足两项,无法兼顾三者。数据库和缓存必定存在数据不一致性。

场景:先更新DB,再删缓存,但监控发现偶发缓存脏数据。

原因:多线程或MySQL主从同步问题。

解决方案:异步延迟双删

cpp 复制代码
// 伪代码:异步延迟删除(避免阻塞支付线程)
void updateOrderAndInvalidateCache(int orderId) {
    db.update(DB, "20");    // 同步更新DB
    cache.del("Cache:");    // 立即删
    
    // 提交异步任务到线程池(非sleep!)
    thread_pool.enqueue([this]() {
        std::this_thread::sleep_for(500ms); // 等待潜在脏写入完成
        cache.del("Cache:"); // 二次删除
    });
}

2. 缓存问题

| 问题 | 现象 | 原因 | 解决方案 |
| 缓存穿透 | 查询不存在的数据,每次请求都打到 DB | 恶意攻击/bug 导致查询不存在的 key | 布隆过滤器 + 缓存空值 |
| 缓存雪崩 | 大量 key 同时过期/Redis宕机,DB 压力骤增 | 缓存集中失效/Redis 宕机 | 随机TTL + 多级缓存 +降级限流 + Redis集群 |

缓存击穿 热点 key过期瞬间,大量请求打到 DB 热点 key 过期 + 高并发 互斥锁/逻辑过期

2.1 缓存穿透

缓存穿透:查询不存在的数据,每次请求都打到 DB且缓存永远不会生效。

解决方案:

  • 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点:额外的内存消耗;可能造成短期的不一致
  • 布隆过滤
    • 优点:内存占用较少,没有多余 key
    • 缺点:实现复杂;存在误判可能

2.2 缓存雪崩

缓存雪崩:同一时间缓存集中失效/Redis 宕机,导致大量请求到达DB压力骤增

解决方案:

  • 给不同的 Key 的TTL 添加随机值避免一起到期
  • 利用 Redis 集群提高服务的可用性
  • 给缓存业务添加降级限流 策略,比如直接拒绝请求而不是穿透到数据库
  • 给业务添加多级缓存如LRU缓存

2.3 缓存击穿

缓存击穿:一个被高并发访问且缓存重建业务很复杂的热点 key 失效,无数请求瞬间穿透到数据库带来巨大压力。

解决方案

  • 互斥锁: 只有获取锁成功的线程才能查询数据库重建缓存,直到释放锁后其他线程才能命中缓存。
    • 问题:死锁;线程需要一直等待,性能低。
    • 实现:
      • 获取锁:setnx lock 1 EX 10
      • 释放锁:del lock
  • 逻辑过期: TTL永不过期而是用一个字段表示是否过期,开辟新线程进行缓存重建,其他线程直接返回旧缓存。
    • 问题:不保证数据一致性。

3. 唯一ID与锁

3.1 全局唯一ID

基于Redis,要求递增但不能规律 (便于数据库创建索引且不暴露信息),采用Redis INCR自增

ID 的组成部分:

  • 符号位:1bit,永远为 0
  • 时间戳:31bit,以秒为单位,可以使用 69 年
  • 序列号:32bit,秒内的计数器,支持每秒产生 2^32 个不同 ID

3.2 乐观锁与超卖

乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。

  • 如果没有修改则认为是安全的,自己才更新数据。
  • 如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常。
  • 实现解决超卖问题:
    • 版本号法:
    • CAS法(实现中只要判断库存大于0即可,不必必须相同 ):

3.3 悲观锁与一人一单

悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。

  • 使用 std::lock_guard<std::mutex> lock(*user_lock); 锁住该用户对应的唯一的锁,再进行后续的下单等业务。
  • 问题:只能锁住同一进程下的多线程中的同一用户锁不住多进程下的同一用户
  • 解决方案:分布式锁。

3.4 异步优化

上述的操作都是数据库层面的,性能极低,进行下面优化:

① Lua 脚本保证原子性(替代数据库锁)

  • 作用 :将 查库存、查重复、扣库存、记用户 四步操作打包成一个不可分割的整体
  • 优势
    • 利用 Redis 单线程特性,天然无并发冲突,不需要加锁。
    • 减少网络往返次数(4 次请求变 1 次),性能极高。
    • 返回值明确:0 成功,1 库存不足,2 重复购买。

② Set 集合实现"一人一单"

  • 作用:使用 SADD (Set Add) 和 SISMEMBER (Set Is Member)。
  • 优势
    • Set 结构天然去重,判断用户是否买过只需 O(1) 时间复杂度。
    • 比查数据库快几个数量级。

③ 消息队列 (MQ) 实现异步下单

  • 作用:Lua 脚本成功后,不直接写库,而是发送消息到 MQ。
  • 优势
    • 解耦:秒杀逻辑与订单创建逻辑分离。
    • 削峰:消费者按照数据库能承受的速度(如 2000 QPS)慢慢消费,避免打挂 DB。
    • 快速响应:用户端几乎瞬间收到"抢购成功,正在处理"的反馈。

4. 分布式锁

**分布式锁:**满足分布式系统或集群模式在多进城可见并且互斥的锁。

4.1 三种实现

特性 MySQL Redis ZooKeeper
互斥 利用 MySQL 本身的互斥锁机制 利用 SETNX 互斥命令 利用节点的唯一性和有序性实现互斥
高可用
高性能 一般 一般
安全性 断开连接,自动释放锁 利用锁超时时间,到期释放 临时节点,断开连接自动释放
分布式锁实现方式 创建一张锁表,插入唯一键(如 user_id),利用主键/唯一索引冲突实现互斥;或使用 GET_LOCK() / RELEASE_LOCK() 函数 使用 SET key value NX EX timeout 原子命令设置带过期时间的锁;或用 Lua 脚本保证"判断+设值"原子性 创建临时顺序节点(Ephemeral Sequential Node),客户端监听前一个节点删除事件来获取锁

4.2 分布式锁基础

实现分布式锁时需要实现的两个基本方法:

  • 获取锁:
    • 互斥:确保只能有一个线程获取锁
    • 非阻塞:尝试一次,成功返回 true,失败返回 false
    • SET lock thread1 NX EX 10 (键唯一,多个线程或进程就是通过这个键 lock 获取的)
  • 释放锁:
    • 手动释放
    • 超时释放:获取锁时添加一个超时时间
    • DEL key

4.3 唯一标识

上述的基础分布式锁问题:锁过期后当前线程释放了其他线程的锁。

在 Redis 分布式锁的实现中,如果没有为锁设置"持有者标识" ,并且业务执行时间超过了锁的超时时间,就会导致:

  1. 线程1 获取了锁(Key: lock, Value: thread1),并设置了 10 秒过期。
  2. 但线程1因网络延迟、GC、复杂计算等原因,业务阻塞超过 10 秒
  3. Redis 自动超时删除了锁(因为 TTL 到了)。
  4. 线程2 此时成功获取到锁(因为锁已不存在),开始执行业务。
  5. 线程1终于恢复,继续执行,并在完成后手动调用 DEL lock 释放锁 ------ 但它不知道锁已经被线程2拿走了!
  6. 结果:线程1误删了线程2正在使用的锁!
  7. 线程3随后也能轻易获取锁 → 多个线程同时进入临界区 → 失去互斥性 → 数据不一致!

解决方案:分布式锁引入唯一标识。

  • 1. 获取锁时,Value 设为唯一标识(如 UUID + 线程ID)
  • 2. 释放锁时,先判断 Value 是否匹配,再删除(必须用 Lua 脚本保证原子性)

4.4 原子释放

上述的锁加入唯一标识后,解决了锁过期后当前线程误删其他线程的锁的问题 ,但是唯一标识的校验和锁的删除不是原子性的

问题:校验完成后,线程阻塞,锁到期自动删除,此时线程阻塞后删除了其他线程的锁。

解决方案:Lua脚本原子性释放锁。

Lua 复制代码
-- 安全释放锁的 Lua 脚本
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

4.5 可重入锁

上述锁的问题:不可重入,同一个线程无法多次获取同一把锁。

实现:Redis 中的 map 结构,允许同一线程多次获取同一把锁,每次获取计数器 +1;每次释放计数器 -1;只有当计数器归零时才真正释放锁。

4.6 可重试锁与超时续约

**不可重试:**获取锁只尝试一次就返回 false,没有重试机制。

  • 解决方案:自旋 + 休眠重试,在获取锁失败后,不立即放弃,而是循环尝试多次,每次间隔一小段时间(如 50ms),直到成功或达到最大重试次数/总超时时间。

**超时释放:**锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患。

  • 解决方案:看门狗,在持有锁期间,启动一个后台线程,定期检查业务是否仍在运行,如果是,则给锁续命,延长其过期时间。

4.7 主从一致

  • 线程 A 在 Master 节点 成功获取锁(SET lock threadA NX EX 30)。
  • 此时锁数据只存在于 Master 内存中。
  • 步骤 1 :Master 收到写命令,但还没来得及同步给 Slave
  • 步骤 2:Master 突然宕机(断电、网络故障等)。
  • 步骤 3 :Sentinel 或集群机制检测到 Master 挂了,选举一个 Slave 晋升为新的 Master
  • 步骤 4 :因为刚才的锁数据没同步过来,新 Master 里没有这把锁!
  • 步骤 5 :线程 B 向新 Master 请求锁,发现锁不存在,成功获取锁
  • 结果 :线程 A(以为还持有锁)和线程 B(刚拿到锁)同时进入临界区互斥性彻底失效!

解决方案:

1. RedLock:不再依赖单个 Redis 实例,而是向多个独立的 Master 节点申请锁

  1. 客户端尝试向 N 个独立的 Redis Master 节点(通常 N=5)依次获取锁。

  2. 获取锁时设置相同的 Key 和过期时间。

  3. 如果客户端在 N/2 + 1 (即大多数,如 5 个中拿到 3 个)节点上成功获取了锁,且总耗时小于锁有效期,则认为获取锁成功

  4. 释放锁时,向所有节点发送释放命令。

  5. ZooKeeper

  • ZooKeeper 和 Etcd 基于 Paxos/Raft 共识算法
  • 它们保证:一旦写入成功,所有节点立即一致
  • 如果 Leader 宕机,在选出新 Leader 之前,整个集群不可写(牺牲可用性保一致性)。
  • 因此,绝对不会出现"锁丢了"的情况
  1. 业务保障,下沉到数据库

  2. 数据库唯一索引

    • 比如"一人一单",在数据库 (user_id, activity_id) 上加唯一索引
    • 即使两个线程同时拿到锁进入代码,第二个线程在执行 INSERT 时会报 DuplicateKeyException,捕获异常即可。
  3. 版本号/CAS 机制

    • 更新数据时带上版本号:UPDATE table SET stock = stock - 1, version = v + 1 WHERE id = x AND version = v。
    • 如果影响行数为 0,说明数据已被别人修改,重试或报错。

4.8 总结

分布式锁的实现中需要:

  1. 引入TTL防止死锁
  2. 使用唯一标识防止误删
  3. 使用lua确保校验唯一标识和删除操作的原子性
  4. 使用 map 实现可重入锁
  5. 使用自旋+休眠实现可重试锁
  6. 使用看门狗实现超时续约
  7. RedLock/Zookeeper/业务兜底保障主从一致

5. 消息队列

5.1 List

list 模拟消息队列。

优点:

  • 利用 Redis 存储,不受限于 程序 内存上限
  • 基于 Redis 的持久化机制,数据安全性有保证
  • 可以满足消息有序性

缺点:

  • 无法避免消息丢失
  • 只支持单消费者

5.2 PubSub

PubSub(发布订阅) 是 Redis2.0 版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个 channel,生产者向对应 channel 发送消息后,所有订阅者都能收到相关消息。

  • SUBSCRIBE channel channel :订阅一个或多个频道
  • PUBLISH channel msg :向一个频道发送消息
  • PSUBSCRIBE patternpattern :订阅与 pattern 格式匹配的所有频道

优点:

  • 采用发布订阅模型,支持多生产、多消费

缺点:

  • 不支持数据持久化
  • 无法避免消息丢失
  • 消息堆积有上限,超出时数据丢失

5.3 Stream

Stream 是 Redis 5.0 引入的一种新数据类型,可以实现一个功能非常完善的消息队列。

STREAM 类型消息队列的 XREAD 命令特点:

  • 消息可回溯
  • 一个消息可以被多个消费者读取
  • 可以阻塞读取
  • 有消息漏读的风险

基于 Stream 的消息队列 - 消费者组

消费者组(Consumer Group): 将多个消费者划分到一个组中,监听同一个队列。具备下列特点:

  • 1 消息分流 队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度
  • 2 消息标示 消费者组会维护一个标示,记录最后一个被处理的消息,哪怕消费者宕机重启,还会从标示之后读取消息。确保每一个消息都会被消费
  • 3 消息确认 消费者获取消息后,消息处于 pending 状态,并存入一个 pending-list。当处理完成后需要通过 XACK 来确认消息,标记消息为已处理,才会从 pending-list 移除。

STREAM 类型消息队列的 XREADGROUP 命令特点:

  • 消息可回溯
  • 可以多消费者争抢消息,加快消费速度
  • 可以阻塞读取
  • 没有消息漏读的风险
  • 有消息确认机制,保证消息至少被消费一次
特性 List PubSub Stream(推荐)
消息持久化 ✅ 支持(数据存在内存/持久化) ❌ 不支持(消息不存储,即时投递) ✅ 支持(天然持久化,可配置保留策略)
阻塞读取 ✅ 支持(BLPOP/BRPOP ✅ 支持(订阅后等待推送) ✅ 支持(XREAD BLOCK
消息堆积处理 ⚠️ 受限于内存空间 可通过多消费者加速 ⚠️ 受限于消费者缓冲区 无堆积能力 ✅ 受限于队列长度 可用消费者组分流提速
消息确认机制 ❌ 不支持 ❌ 不支持 ✅ 支持(XACK + pending-list)
消息回溯 ❌ 不支持(消费即删除) ❌ 不支持 ✅ 支持(通过 ID 范围重复读取)
消费模型 点对点(单消费者) 发布订阅(广播给所有订阅者) 灵活:支持点对点 + 发布订阅 + 消费者组
可靠性 中(易丢失、无确认) 低(无持久化、无确认、易丢消息) 高(持久化+确认+回溯+分组)
适用场景 简单任务队列、临时缓冲 实时通知、事件广播 高可靠消息队列、订单处理、日志收集等

6. 持久化与优化

6.1 RDB

Snapshotting/RDB :快照是默认的持久化方式。简单来说就是把内存中的所有数据都记录到磁盘中,默认服务停止时执行一次。当 Redis 实例故障重启后,从磁盘读取快照文件,恢复数据。

  • 默认是保存在当前运行目录,默认文件名为dump.rdb。

  • 配置redis在 n 秒内如果超过 m 个key被修改就执行 bgsave,默认:save 900 1 #900秒内如果超过1个key被修改,则发起快照保存。

  • bgsave 开始时会 fork 主进程得到子进程 ,子进程共享主进程内存空间 。子进程读取内存数据并写入新的 RDB 文件 ,用新 RDB 文件替换旧 RDB 文件。fork 采用的是 copy-on-write 技术:

    • 当主进程执行写操作时,则会拷贝一份数据,执行写操作。
    • 当主进程执行读操作时,访问共享内存;
  • RDB 的缺点:

    • RDB 执行间隔时间长,两次 RDB 之间写入数据有丢失的风险
    • fork 子进程、压缩、写出 RDB 文件都比较耗时

6.2 AOF

Append-only file :aof 比快照方式有更好的持久化性每一个写命令都记录在文件中 (默认是 appendonly.aof)。当redis重启时会通过重新执行文件中保存的写命令 来在内存中**重建整个数据库的内容。**当然由于os会在内核中缓存 write做的修改,所以可能不是立即写到磁盘上。这样aof方式的持久化也还是有可能会丢失部分修改。不过可以通过配置文件告诉redis我们想要通过fsync函数强制os写入到磁盘的时机。

方式如下(默认是:每秒fsync一次)

  • appendonly yes //启用aof持久化方式
  • appendfsync always //每次收到写命令就立即强制写入磁盘 ,最慢的,但是保证完全的持久化,不推荐使用
  • appendfsync everysec //每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐
  • appendfsync no //完全依赖os,性能最好,持久化没保证

因为是记录命令,AOF 文件会比 RDB 文件大的多 。而且 AOF 会记录对同一个 key 的多次写操作,但只有最后一次写操作才有意义 。通过执行 bgrewriteaof命令,可以让 AOF 文件执行重写功能,用最少的命令达到相同效果。

特性 RDB(Redis Database Backup) AOF(Append Only File)
持久化方式 定时对整个内存做快照 记录每一次执行的写命令
数据完整性 ❌ 不完整 两次备份之间写入的数据可能丢失 ✅ 相对完整 取决于刷盘策略(always/everysec/no)
文件大小 ✅ 小 经过压缩,二进制格式 ❌ 大 记录原始命令,冗余多(需定期重写)
宕机恢复速度 ✅ 很快 直接加载快照文件 ❌ 慢 需重放所有命令
数据恢复优先级 ⚠️ 低 因数据完整性不如 AOF ✅ 高 因能还原更多操作,更安全
系统资源占用 ❌ 高 fork 子进程 + 压缩 + 写磁盘 → 消耗 CPU/内存 ⚠️ 中~高 日常低(磁盘 IO),但重写时消耗大量 CPU/内存
使用场景 可容忍分钟级数据丢失,追求快速启动和重启 对数据安全性要求高,如金融、订单等核心业务

6.3 优化

建议:

  1. 用来做缓存的Redis实例尽量不要开启持久化安全性要求低的数据)。

  2. 建议关闭RDB 持久化功能,使用AOF持久化:

    1. RDB频率低
    2. RDB fork机制影响性能
  3. RDB适合做数据备份:利用脚本定期在slave节点做 RDB

  4. AOF 设置合理的 rewrite 阈值,避免频繁的 bgrewrite(磁盘IO多)

  5. AOF 配置 no-appendfsync-on-rewrite = yes禁止在 rewrite 重写期间做 aof 刷盘,避免因 AOF 引起的阻塞

    1. 在执行 AOF 重写生成新AOF文件 )时,它仍然需要把新的写命令追加到旧 AOF 文件中(刷盘);

    2. 触发 fsync() 系统调用,是同步阻塞 的,刷盘阻塞了重写

  6. 部署建议

    1. Redis实例的物理机要预留足够内存,应对fork和rewrite

    2. 单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力

    3. 不要与CPU密集型应用部署在一起,fork用CPU多

    4. 不要与高硬盘负载应用一起部署。例如:数据库、消息队列,rewrite和刷盘都要大量磁盘IO

7. Redis集群

7.1 主从数据同步原理

7.1.1 全量同步

重要概念:

  • Replication Id :简称 replid,是数据集标记,id 一致则说明是同一数据集。每一个 master 都有唯一的 replid,slave 会继承 master 的 replid,slave在同步前也有自己唯一的replid。
  • offset:偏移量,随着记录在 repl_baklog 中的数据增多而逐渐增大。slave 完成同步时也会记录当前同步的 offset。如果 slave 的 offset 小于 master 的 offset,说明 slave 数据落后于 master,需要更新。

全量同步:master 将完整内存数据生成 RDB,发送 RDB 到 slave。后续命令则记录在 repl_baklog,逐个发送给 slave。

执行时机:

  • slave 节点第一次连接 master 节点时
  • slave 节点断开时间太久,repl_baklog 中的 offset 已经被覆盖时

流程:

  • slave 节点请求增量同步
  • master 节点判断 replid,发现不一致,拒绝增量同步;
  • master 将完整内存数据生成 RDB,发送 RDB 到 slave;
  • slave 清空本地数据,加载 master 的 RDB;
  • master 将 RDB 期间的命令记录在 repl_baklog,并持续将 log 中的命令发送给 slave;
  • slave 执行接收到的命令,保持与master之间的同步。

7.1.2 增量同步

增量同步:slave 提交自己的 offset 到 master,master 获取 repl_baklog 中从 offset 之后的命令给 slave。

**执行时机:**slave 节点断开又恢复,并且在 repl_baklog 中能找到 offset 时。

repl_baklog 大小有上限 ,写满后会覆盖最早的数据。如果 slave 断开时间过久,导致尚未备份的数据被覆盖,则无法基于 log 做增量同步,只能再次全量同步。

优化:

  • 在 master 中配置 repl-diskless-sync yes 启用无磁盘复制,避免全量同步时的磁盘 IO。
  • Redis 单节点上的内存占用不要太大,减少 RDB 导致的过多磁盘 IO
  • 适当提高 repl_baklog 的大小 ,发现 slave 宕机时尽快实现故障恢复 ,尽可能避免全量同步
  • 限制一个 master 上的 slave 节点数量 ,如果实在是太多 slave,则可以采用主 - 从 - 从链式结构,减少 master 压力

7.2 哨兵

7.2.1 监测机制

Sentinel 基于心跳机制监测服务状态,每隔 1 秒向集群的每个实例发送 ping 命令:

  • 主观下线:如果某 sentinel 节点发现某实例未在规定时间响应,则认为该实例主观下线。
  • 客观下线:若超过指定数量(quorum)的 sentinel 都认为该实例主观下线,则该实例客观下线。quorum 值最好超过 Sentinel 实例数量的一半。

7.2.2 master选择

一旦发现 master 故障,sentinel 需要在 salve 中选择一个作为新的 master,选择依据:

  • 首先会判断 slave 节点与 master 节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该 slave 节点
  • 然后判断 slave 节点的slave-priority 值,越小优先级越高,如果是 0 则永不参与选举
  • 如果 slave-prority 一样,则判断 slave 节点的 offset值,越大说明数据越新,优先级越高
  • 最后是判断 slave 节点的运行 id大小,越小优先级越高。

7.2.3 故障转移

当选中了其中一个 slave 为新的 master 后(例如 slave1),故障的转移的步骤如下:

  • sentinel 给备选的 slave1 节点发送 slaveof no one 命令,让该节点成为 master
  • sentinel 给所有其它 slave 发送 slaveof 192.168.150.101 7002 命令,让这些 slave 成为新 master 的从节点,开始从新的 master 上同步数据。
  • 最后,sentinel 将故障节点标记为 slave,当故障节点恢复后会自动成为新的 master 的 slave 节点

7.3 分片集群

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:

  • 海量数据存储问题
  • 高并发写的问题

使用分片集群可以解决上述问题,分片集群特征:

  • 集群中有多个 master,每个 master 保存不同数据
  • 每个 master 都可以有多个 slave 节点
  • master 之间通过 ping 监测彼此健康状态
  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

7.3.1 散列插槽

有 3 台 Redis 服务器(A、B、C),想让它们一起存数据,怎么分?

  • 如果按 key 名字哈希后直接映射到节点 → 以后加机器或减机器时,所有 key 都要重新分布 → 灾难!
  • 所以 Redis 发明了 16384 个固定插槽,先把插槽分好,key 只和插槽挂钩,插槽再挂在节点上 → 扩容缩容只需移动插槽,不影响其他 key!

Redis 会把每一个 master 节点映射到 0~16383 共 16384 个插槽(hash slot)上,数据 key 不是与节点绑定,而是与插槽绑定。redis 会根据 key 的有效部分计算插槽值,分两种情况:

  • key 中包含"{}",且"{}"中至少包含 1 个字符,"{}"中的部分是有效部分,可以把同一类数据保存在同一个Redis实例。
  • key 中不包含"{}",整个 key 都是有效部分。

例如:key 是 num,那么就根据 num 计算,如果是{aaa}num,则根据 aaa 计算。计算方式是利用 CRC16 算法得到一个 hash 值,然后对 16384 取余,得到的结果就是 slot 值。

7.3.2 故障转移

自动故障转移:master宕机处理

  1. 首先是该实例与其它实例失去连接;
  2. 然后是疑似宕机;
  3. 最后是确定下线,自动提升一个 slave 为新的 master、

手动故障转移:数据转移:

利用 cluster failover 命令可以手动让集群中的某个 master 宕机,切换到执行 cluster failover 命令的这个 slave 节点,实现无感知的数据迁移。其流程如下:

手动的 Failover 支持三种不同模式:

  • 缺省:默认的流程,如图 1~6 步
  • force:省略了对 offset 的一致性校验
  • takeover:直接执行第 5 步,忽略数据一致性、忽略 master 状态和其它 master 的意见

7.4 优化

**完整性问题:默认配置中如果发现任意一个插槽不可用,则整个集群不可用,**可修改:

cluster-require-full-coverage 配置为 false。

集群带宽问题:集群节点之间会不断的互相 Ping 来确定集群中其它节点的状态。每次 Ping 携带的信息至少包括:

  • 插槽信息
  • 集群状态信息

集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高。

解决途径:

  1. 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群。
  2. 避免在单个物理机中运行太多 Redis 实例
  3. 配置合适的 cluster-node-timeout 值

**建议:**单体 Redis(主从 Redis)已经能达到万级别的 QPS,并且也具备很强的高可用特性。如果主从能满足业务需求的情况下,尽量不搭建 Redis 集群。

8. 多级缓存与同步

8.1 三种同步方式

  • 设置有效期 :给缓存设置有效期,到期后自动删除。再次查询时更新
    • 优势:简单、方便
    • 缺点:时效性差,缓存过期之前可能不一致
    • 场景:更新频率较低,时效性要求低的业务
  • 同步双写 :在修改数据库的同时,直接修改缓存
    • 优势:时效性强,缓存与数据库强一致
    • 缺点:有代码侵入,耦合度高;
    • 场景:对一致性、时效性要求较高的缓存数据
  • 异步通知 :修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
    • 优势:低耦合,可以同时通知多个缓存服务
    • 缺点:时效性一般,可能存在中间不一致状态
    • 场景:时效性要求一般,有多个服务需要同步

8.2 异步通知

Canal 就是把自己伪装成 MySQL 的一个 slave 节点,从而监听 master 的 binary log 变化。再把得到的变化信息通知给 Canal 的客户端,进而完成对其它数据库的同步。

9. Redis 优化

9.1 键值设计

9.1.1 key结构

设计规范:

  • 遵循基本格式:业务名称:数据名:id
  • 长度不超过44字节
  • 不包含特殊字符

例如:我们的登录业务,保存用户信息,其key是这样的:login:user:10

优点: ① 可读性强 ② 避免key冲突 ③ 方便管理 ④ 更节省内存:key是string类型,底层编码包含int、embstr和raw三种。embstr在小于44字节使用,采用连续内存空间,内存占用更小。

9.1.2 大Key治理

①BigKey通常以Key的大小和Key中成员的数量来综合判定,例如:

  • Key本身的数据量过大:一个String类型的Key,它的值为5 MB。
  • Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个。
  • Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100 MB。

②推荐值:

  • 单个key的value小于10KB
  • 对于集合类型的key,建议元素数量小于1000

③BigKey的危害:

  • 网络阻塞
    对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢
  • 数据倾斜
    BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
  • Redis阻塞
    对元素较多的hash、list、zset等做运算会耗时较旧,使主线程被阻塞
  • CPU压力
    对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用

④发现BigKey:

  • redis - cli --bigkeys:返回Key的整体统计信息与每个数据的Top1的big key,为了不影响线上业务 ,一定要加上采样间隔参数,如 redis-cli --bigkeys -i 0.1。这个 -i 0.1表示每扫描100个key,就暂停0.1秒。
  • scan扫描:自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度(此处不建议使用MEMORY USAGE)。
  • 第三方工具:利用第三方工具,如Redis - Rdb - Tools分析RDB快照文件,全面分析内存使用情况。
  • 网络监控:自定义工具,监控进出Redis的网络数据,超出预警值时主动告警。

⑤删除BigKey:BigKey内存占用较多,即便时删除这样的key也需要耗费很长时间,导致Redis主线程阻塞。

  • Redis 3.0 及以下版本:如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey
  • Redis 4.0以后:Redis在4.0后提供了异步删除的命令:unlink

⑥ 安全迁移大Key:

  • 使用HSCAN分批从大Key中读取数据
  • 将字段写入新的分片Key
  • 从原大Key中删除已迁移的字段,逐步释放内存

在迁移期间,业务代码需要做兼容。不能立即只读新Key

  • 先尝试从新的分片Key中查找
  • 新Key中没找到,再回退到查旧的大Key(保障迁移期间数据不丢失)

⑦ 写入逻辑中拦截大Key:

  • 在写入前进行校验:在封装 hset, hmset等命令的函数中,加入大小检查。

  • 定义明确的阈值:例如,规定单个Hash的字段数不能超过1000,单个String的值不能超过10KB,整个Hash的预估大小不能超过1MB等。

9.1.3 类型选择

方式一:json字符串

  • user:1 -> {"name": "Jack", "age": 21}
  • 优点:实现简单粗暴
  • 缺点:数据耦合,不够灵活

方式二:字段打散

  • user:1:name -> Jack
  • user:1:age -> 21
  • 优点:可以灵活访问对象任意字段
  • 缺点:占用空间大、没办法做统一控制

方式三:hash

  • user:1 -> name: Jack, jack: 21
  • 优点:底层使用ziplist,空间占用小,可以灵活访问对象的任意字段
  • 缺点:代码相对复杂

例:如果有100万对field和value,field是自增id,则存在问题:

  • ① 使用 hassh_map 的问题:
    • entry 超过500,使用哈希表而不是ziplist,占用内存过多;
    • 可以配置entry上限,但过大会导致BigKey。
  • ② 使用 string 的问题:
    • 底层没内存优化,内存占用过多
    • 批量获取麻烦
  • 拆分为小的hash,将 id / 100 作为key,将id % 100 作为field,这样每100个元素为一个Hash

9.1.4 热Key治理

场景:商品库存Key stock:1001 在秒杀时QPS 10w+,单Redis节点CPU 100%,服务大量超时。

1. 本地缓存+定时同步 :不在每次请求时都访问Redis,而是在C++服务的内存里 放一份库存数据。大部分扣减操作在内存里完成,然后批量、异步地同步到Redis。

  • 超卖风险:多个C++服务实例都有本地缓存,会超卖
  • 解决方案 :设置一个安全阈值。比如本地库存降到100以下时,每次扣减都必须同步检查Redis的真实库存。

2. 分段库存 + 扣减原子性: 不把1000件库存放在一个Key里,而是拆成10个段,每个段100件。热点从1个Key分散到10个Key

  • 原始:stock:1001= 1000

  • 分段后:stock:1001:0= 100, stock:1001:1= 100, ..., stock:1001:9= 100

  • 分段不均衡:可能某个分段先卖完,其他分段还有货

  • 解决方案 :定期重新平衡分段(用Redis的INCRBY/DECRBY调整各分段数量)

  • 原子性:上面的Lua脚本保证了单个分段内扣减的原子性

9.2 批处理优化

9.2.1 pipeline和mset

Redis提供了很多Mxxx这样的命令,可以实现批量插入数据,例如:

  • mset
  • hmset

注意:不要在一次批处理中传输太多命令,否则单次命令占用带宽过多,会导致网络阻塞。

MSET虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用Pipeline功能。

对比维度 MSET / HMSET(批量命令) Pipeline(管道)
适用数据类型 仅支持简单类型: • MSET → String • HMSET → Hash 支持所有 Redis 数据类型和任意命令组合
原子性 ✅ 是:整个命令执行是原子的 ❌ 否:多个命令独立执行,不保证原子性,会被插队
网络开销 ⚠️ 单次请求传输大量数据,可能阻塞网络 ✅ 多次命令打包发送,减少 RTT,提升吞吐量
错误处理 ❌ 整体失败或成功,难以定位具体哪条出错 ✅ 可逐条获取响应,便于错误诊断和处理
典型用途 批量写入同类型键值对(如初始化配置、缓存预热) 复杂业务逻辑批量操作(如事务模拟、多步更新)
性能优势场景 小批量、结构简单的数据插入 大批量、跨类型、需高吞吐的场景
注意事项 避免一次性传入过多键值对,防止带宽占用过大导致阻塞 注意客户端缓冲区大小;避免在事务中误用 Pipeline

9.2.2 集群下的批处理

如 MSET 或 Pipeline 这样的批处理需要在一次请求中携带多条命令,而此时如果 Redis 是一个集群,那批处理命令的多个 key 必须落在一个插槽(slot)中,否则就会导致执行失败。

维度 串行命令 串行 slot 并行 slot hash_tag
实现思路 for 循环遍历,依次执行每个命令 在客户端计算每个 key 的 slot,将 slot 一致分为一组,每组都利用 Pipeline 批处理。串行执行各组命令 在客户端计算每个 key 的 slot,将 slot 一致分为一组,每组都利用 Pipeline 批处理。并行执行各组命令 将所有 key 设置相同的 hash_tag,则所有 key 的 slot 一定相同
耗时 N 次网络耗时 + N 次命令耗时 m 次网络耗时 + N 次命令耗时 (m = key 的 slot 个数) 1 次网络耗时 + N 次命令耗时 1 次网络耗时 + N 次命令耗时
优点 实现简单 耗时较短 耗时非常短 耗时非常短、实现简单
缺点 耗时非常久 实现稍复杂 slot 越多,耗时越久 实现复杂 容易出现数据倾斜

9.3 慢查询

慢查询的阈值可以通过配置指定:

  • slowlog-log-slower-than :慢查询阈值,单位是微秒。默认是 10000,建议 1000
    (即执行时间超过该值的命令会被记录为"慢查询")

慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定:

  • slowlog-max-len :慢查询日志(本质是一个队列)的长度。默认是 128,建议 1000
    (表示最多保留多少条慢查询记录,超出后旧记录会被淘汰)

MySQL 慢查询例: 数据库表结构是 VARCHAR,字符串类型;C++ 代码是 int64_t,整数类型;MySQL 隐式类型转换,得先把表中所有的 user_id 从字符串转换成数字,然后再比较,无法使用索引快速定位,只能全表扫描。

  1. 统一类型,把C++的类型也改成字符串
  2. 在查询时显式转换成字符串
  3. 手写SQL绕过ORM的限制

9.4 内存配置

当 Redis 内存不足时,可能导致 Key 频繁被删除、响应时间变长、QPS 不稳定等问题。当内存使用率达到 90% 以上时就需要我们警惕,并快速定位到内存占用的原因。可以通过info memorymemory xxx查看目前内存分配状态。

内存占用类型 说明
数据内存 是 Redis 最主要的部分,存储 Redis 的键值信息。主要问题是 BigKey 问题、内存碎片问题。
进程内存 Redis 主进程本身运行肯定需要占用内存,如代码、常量池等等;这部分内存大约几兆,在大多数生产环境中与 Redis 数据占用的内存相比可以忽略。
缓冲区内存 一般包括客户端缓冲区、AOF 缓冲区、复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,不当使用 BigKey,可能导致内存溢出。

内存缓冲区常见的有三种:

  • 复制缓冲区 :主从复制的 repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能。通过 repl-backlog-size 来设置,默认 1mb
  • AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行 rewrite 的缓冲区。无法设置容量上限
  • 客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置。输出缓冲区可以设置

10. 数据结构

10.1 基础数据结构

数据结构 内存布局(结构体 核心字段/特性 时间复杂度 关键特点
SDS 动态字符串 lenallocflags\[\]buf... len:已用长度 free:申请长度 flags:SDS头类型 buf:字节数组 获取长度O(1) 追加O(1)均摊 ① 二进制安全 ② 动态扩容 ③ 减少内存重分配
IntSet 整数集合 encodinglengthcontents... encoding:16/32/64位 length:元素个数 contents:有序数组 查找O(logN) 插入O(N) ① 升序排列+二分查找 ② 支持升级(小→大) ③ 不支持降级
Dict 哈希表 ht2{table} table:指针数组,每个元素是一个链表,解决哈希冲突 ht2:1个为空rehash用 查找O(1)平均 最坏O(N) ① 链地址法解决冲突 ② **渐进式rehash,**负载因子>1扩容,<0.1缩容
ZipList 压缩列表 zlbyteszltailzllenentry...zlend prevlen:前节点长度,以此定位当前节点 encoding:数据类型cotent:实际数据 不保存前后指针 查找O(N) 插入O(N) ① 连续内存存储 ② 节省内存 ③ 连锁更新风险,即前节点修改后,后节点prevlen本身占用字节数变化 ④ 适合小数据
QuickList 快速列表 双向链表{每个节点都是ZipList} head/tail:链表头尾 node:链表节点 zl:每个节点的ZipList 两端操作O(1) 中间O(N) ① ZipList+链表结合 ② fill控制节点大小 ③ compress压缩深度 ④ List默认结构
SkipList 跳表 多层有序链表 header:头节点tail:尾节点level:最大层数forward:前进指针 查找O(logN) 插入O(logN) ① 随机层数(1~32) ② 范围查询高效 ③ 实现比红黑树简单 ④ ZSet核心结构
RedisObject 对象系统 typeencodingptrlrurefcount type:5种类型 encoding:底层编码 ptr:指向底层结构 refcount:引用计数 类型检查O(1) ① 统一对象封装 ② 支持多态 ③ 内存回收依据 ④ 类型转换基础

10.2 5种数据类型

Redis类型 编码方式 底层结构 转换条件 内存对比
String raw SDS 字符串>44字节 基础结构
embstr SDS(优化) 字符串≤44字节 一次分配
int 整数 可转为long的整数 最省内存
List quicklist QuickList Redis 3.2+默认 平衡方案
ziplist ZipList 元素<512且值<64字节 已废弃
Hash ziplist ZipList 字段<512且值<64字节 紧凑存储
hashtable Dict 超过上述阈值 标准哈希
Set intset IntSet 全整数且<512个 数组存储
无序且不重复 hashtable Dict 超过上述条件 标准哈希
ZSet ziplist ZipList 元素<128且值<64字节 紧凑存储
有序集合 skiplist SkipList+Dict 超过上述阈值 跳表+哈希

11. 分布式事务

11.1 核心概念

  • 分布式事务 :在分布式系统环境下,保证跨多个微服务/数据库的操作满足 ACID(原子性、一致性、隔离性、持久性)特性。
  • 角色定义
    • 协调者:事务的发起者,负责调度所有参与者(如 Seata-TC, 数据库管理器)。
    • 参与者:执行具体业务逻辑的服务或数据库(如 Order-Service, Inventory-Service)

11.2 三种方案

11.2.1 2PC (两阶段提交)

  • 阶段一 (Prepare准备):协调者问"能提交吗?",参与者执行 SQL 但不 Commit,锁定资源,返回 Yes/No。
  • 阶段二 (Commit/Rollback提交/回滚)
    • 全 Yes →→ 发送 Commit →→ 参与者正式提交,释放锁。
    • 有 No →→ 发送 Rollback →→ 参与者回滚,释放锁。
  • 缺陷:只要有一个参与者没响应,所有人一直阻塞等待;协调者挂了,大家都不知道该提交还是回滚。

11.2.2 3PC (三阶段提交)

  • 第一阶段 :CanCommit (询问)
    • 协调者问参与者:"你能提交吗?"
    • 参与者只回答 Yes/No,不执行任何事务操作,不锁资源。
  • 第二阶段 :PreCommit (预提交)
    • 如果大家都说 Yes,协调者发"预提交"指令。
    • 参与者执行本地事务,写 Undo/Redo Log,但不提交,然后反馈 Ack。
    • 关键: 如果参与者在这个阶段超时没收到指令,它可以自动提交(因为协调者可能挂了,且之前已经确认过 CanCommit 成功)。
  • 第三阶段: DoCommit (正式提交)
    • 协调者收到所有 Ack 后,发"正式提交"指令
    • 参与者提交事务,释放资源。
  • 缺陷:如果网络分区,协调者认为失败发了 Abort,但部分参与者超时自动 Commit 了,导致数据不一致。

11.2.3 TCC (补偿事务)

  • 阶段一 (Try尝试)
    • 业务检查。
    • 预留资源 (例如:冻结账户余额 100 元,而不是直接扣减;锁定库存 1 件)但不真正扣减。
  • 阶段二 (Confirm提交)
    • 所有 Try 成功后执行。
    • 真正扣减(使用 Try 预留的资源)。
    • **要求:**必须幂等(重复调用不影响结果),通常不允许失败(失败需无限重试)。
  • 阶段三 (Cancel取消)
    • 任意 Try 失败后执行。
    • 释放资源(解冻余额,释放库存)。
    • 要求:必须幂等,需处理"空回滚"(Try 没执行直接来 Cancel)和"悬挂"(Cancel 比 Try 先到)。
  • 缺点
    • 开发成本高:每个业务接口都要写 Try/Confirm/Cancel 三个方法。
维度 2PC (两阶段提交) 3PC (三阶段提交) TCC (补偿事务)
核心阶段 1. Prepare (准备/锁定) 2. Commit/Rollback (提交/回滚) 1. CanCommit (询问) 2. PreCommit (预提交) 3. DoCommit (正式提交) 1. Try (预留资源) 2. Confirm (确认执行) 3. Cancel (撤销资源)
一致性 强一致性 (CP) 准强一致性 (仍有不一致风险) 最终一致性 (AP)
阻塞情况 严重阻塞 参与者在整个过程中锁定资源,直到收到最终指令。 减少阻塞 引入超时机制,参与者超时后可自动提交/回滚。 无长期阻塞 资源仅在 Try 阶段短暂锁定,Confirm/Cancel 极快。
单点故障 协调者宕机,参与者永久阻塞。 优化 参与者可通过超时机制自主决策,降低依赖。 依赖事务管理器 但业务可自定义重试策略,容错性强。
数据不一致风险 (极端情况下) 若协调者在第二阶段部分发送指令后宕机。 网络分区时,部分节点可能因超时自动提交,导致数据不一致。 (需业务保证) 需处理幂等、空回滚、悬挂等问题,否则也会不一致。
性能 低 同步等待,资源占用久。 中 交互次数多,网络开销大。 高 无数据库长锁,并发能力强。
代码侵入性 通常由中间件或数据库底层支持。 通常由中间件或数据库底层支持。 极高 需为每个业务写 写Try/ Confirm/Cancel 三个接口。
实现复杂度 简单 复杂 **非常复杂,**需处理各种异常场景(幂等、防悬挂等)。
典型应用场景 传统银行内部核心系统,对一致性要求极高,并发低 极少使用 (理论完美,落地困难) 互联网高并发场景 (电商下单、支付、秒杀)
实现 XA 协议 (MySQL XA), Atomikos 几乎无主流开源实现 Hmily, Tars, Seata-TCC, 自研
相关推荐
正儿八经的少年2 小时前
redis 的大 key 和热 key 详解
数据库·redis·缓存
止语Lab2 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活2 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困2 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn2 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530142 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908402 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金2 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
X-⃢_⃢-X2 小时前
十、Redis之布隆过滤器
数据库·redis·缓存
人间凡尔赛3 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构