🌐 分布式集群生产问题纪实

从MySQL主从到Redis集群 — 10个真实线上事故 · 第三人称视角 · 原理到解决

MySQL主从延迟读写分离主从切换 Redis ClusterRedis脑裂缓存一致性 分布式事务分布式锁分布式ID CAP理论
① MySQL 主从延迟 — 用户改了密码,刷新后还是旧的?
📅 周二下午2点,客服群里炸了:"我改了密码,退出重新登录居然还是旧密码!你们系统有bug吗?"

后端开发小林查了一下代码——改密码写的明明是主库,登录验证也是查主库啊?不,等等,登录那个接口……走的是从库。
🛎️ 客服群 — 14:02
👩
客服小刘
用户张三反馈:改了密码后重新登录失败,提示密码错误。已经3个用户反馈了
👨‍💻
后端小林
我看了下,改密码是写主库的,登录验证查的是从库。应该是主从延迟了
🧑‍💻
技术负责人老陈
看下从库延迟多少?
👨‍💻
后端小林
Seconds_Behind_Master: 8秒…刚才那个批量更新用户标签的任务跑起来了
🔍 到底什么是主从延迟?
主从延迟 = 主库写完了,但从库还没来得及抄完。中间这段时间,从库的数据是"旧的"。

类比:你在总部改了Excel表格里的某个价格,分公司的电脑还在下载旧版本。分公司的人打开看,还是旧价格。
📋 为什么会延迟?底层原理
MySQL 主从复制的数据流(3个线程)
✍️ Master 主库
① 应用写入 → 写入binlog
Binlog Dump线程 ② 推送binlog给从库
📖 Slave 从库
③ IO线程接收 → 写relay log   ④ SQL线程执行relay log
⚠️ 延迟发生在第④步:SQL线程是单线程执行(MySQL 5.6之前),主库并发写入100条SQL,从库只能一条条回放 → 越积越多

延迟的5个常见原因:

  • ① 主库并发写入,从库单线程回放 — 最大的元凶。主库4个线程同时写,从库1个线程慢慢执行
  • ② 大事务 — 一个事务里UPDATE了50万行,binlog超大,从库执行也要很久
  • ③ 从库机器性能差 — 主库是SSD,从库是机械硬盘;或者从库CPU弱
  • ④ 网络抖动 — 主从之间网络不稳定,binlog传输慢
  • ⑤ 从库上跑了重查询 — 有人从从库跑报表SQL,占满了CPU,SQL线程得不到执行时间
📊 怎么看延迟多少?
# 在从库上执行 mysql> SHOW SLAVE STATUS\G Slave_IO_Running: Yes Slave_SQL_Running: Yes Seconds_Behind_Master: 8 # ← 延迟8秒! Exec_Master_Log_Pos: 4589231 # SQL线程执行到这 Read_Master_Log_Pos: 4599876 # IO线程读到这(差距=积压量) # 更精确的方法:对比主从的GTID mysql> SHOW GLOBAL VARIABLES LIKE 'gtid_executed'; # 在主库和从库分别执行,看最后的GTID差多少
🛠️ 怎么解决?
方案原理类比优缺点
强制走主库对一致性要求高的查询直接查主库想确认最新价格就打电话问总店简单但增加主库压力
多线程复制MySQL 5.7+ 支持按库并行回放relay log从库多雇几个抄写员,每人负责不同本子5.7默认按库并行,8.0支持按行并行
半同步复制至少1个从库确认收到binlog才返回成功总店改完价格,等至少1个分店说"收到了"才告诉客户降低丢数据风险,但不消除延迟
延迟检测代码里判断延迟超过阈值就切主库发现分店还在抄旧版,就暂时不让客户看分店的需要业务层配合
💻 代码里的解决方案
ForceMasterAspect.java — 强制走主库注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ForceMaster {
    // 标记这个方法必须走主库
}

// AOP 切面
@Aspect
@Component
public class ForceMasterAspect {

    @Before("@annotation(forceMaster)")
    public void before(ForceMaster forceMaster) {
        DataSourceContextHolder.set("master"); // 切到主库
    }

    @After("@annotation(forceMaster)")
    public void after(ForceMaster forceMaster) {
        DataSourceContextHolder.clear(); // 恢复默认
    }
}

// 使用示例
@ForceMaster // ← 这个查询必须走主库!
public User getUserAfterUpdate(Long userId) {
    return userMapper.selectById(userId);
}
🔑 关键知识点:MySQL 5.7 多线程复制
为什么5.6之前从库只能单线程?
因为从库的SQL线程执行relay log时,必须保证和主库一样的执行顺序。如果主库是串行执行的事务A→B→C,从库也必须串行执行A→B→C,不能乱序。

5.7怎么做到并行的?
核心思想:同一个库里的操作必须保序,不同库的操作可以并行。就像Excel——同一个Sheet里的操作必须按顺序来(否则行号会乱),但不同Sheet的操作可以同时进行。

配置:slave_parallel_type=LOGICAL_CLOCK(按组提交并行),slave_parallel_workers=4(4个Worker线程)。
② 读写分离后读到旧数据 — "我刚下的单,列表里怎么看不到?"
📅 电商场景:用户下单成功了(写主库),立刻查看"我的订单"列表(走从库),但列表里没有这条新订单。
用户以为下单失败,又下了一单 → 重复下单!客服退单量飙升。
🔍 问题的本质
写后读不一致 — 时间线
T1 14:00:00 用户点击"下单" → 写入主库 ✅
T2 14:00:00.1 主库返回成功 → binlog 还没推到从库
T3 14:00:00.2 用户点"我的订单" → 查从库 → 从库还没有!❌
T4 14:00:01 binlog终于到从库 → 从库数据同步了(但用户已经走了)
类比:你刚在总店的电脑上改了Excel里A3单元格的数据,立刻跑到分店去看同一个Excel,分店的电脑还没同步到最新版,所以你看到的A3还是旧值。不是bug,是"同步还在路上"。
🛠️ 4种解决方案对比
方案原理类比缺点
写后强制走主库写操作后的N秒内,读也走主库改完东西立刻问总店,不等分店主库压力大
中间件路由ShardingSphere识别写后读,自动路由主库调度员知道你刚改过,自动帮你问总店有延迟窗口
半同步+等待写操作等至少1个从库确认后再返回等分店确认收到再告诉客户写延迟增加
业务层缓存写完后把结果放Redis,读时先查Redis改完东西在门口公告栏也写一份缓存要管理失效
最推荐的做法:写操作后,5秒内同一用户的相关查询都走主库。这是业界最常见的方案,Netflix、阿里都是这么做的。
Spring Boot + ShardingSphere 可以用 hintManager.addMasterRouteOnly() 实现。
💻 ShardingSphere 强制主库示例
OrderService.java
public OrderResult createOrder(OrderRequest req) {
    // 1. 写入主库
    orderMapper.insert(order);

    // 2. 标记:这个用户5秒内的查询走主库
    masterRouteCache.put(req.getUserId(), 5, TimeUnit.SECONDS);

    return OrderResult.success(order.getId());
}

public List<Order> listOrders(Long userId) {
    // 检查是否在"写后窗口期"
    if (masterRouteCache.containsKey(userId)) {
        HintManager hintManager = HintManager.getInstance();
        hintManager.addMasterRouteOnly("ds"); // ← 强制走主库
        try {
            return orderMapper.selectByUserId(userId);
        } finally {
            hintManager.close();
        }
    }
    return orderMapper.selectByUserId(userId); // 走从库
}
③ 主从切换失败 — 凌晨3点主库宕机,切换后数据丢了?
📅 凌晨3:17,监控报警:MySQL主库10.0.0.1无响应。MHA自动发起切换,把Slave1提升为新Master。

第二天早上8点,运营发现昨天23:59到3:17之间的部分订单"消失"了。
事故复盘:主库用的是异步复制,主库挂了但binlog还没推到从库 → 数据丢失。
🔧 事故复盘会 — 10:00
🧑‍💻
技术负责人老陈
昨天主库宕机切换,丢失了约120条订单数据。先定位原因
🗄️
DBA老王
主库3:17宕机,但最后一条成功推送到从库的binlog是3:14的。中间3分钟的数据在主库磁盘上,但主库起不来了
👨‍💻
后端小林
我们用的异步复制…主库写入成功就返回了,不等从库确认
🧑‍💻
技术负责人老陈
改成半同步。至少要1个从库确认收到binlog,写入才算成功
📋 异步 vs 半同步 vs 全同步 — 原理深度对比
1️⃣ 异步复制(当前)
Master写入binlog
立即返回客户端 ✅
↓ 后台
Slave慢慢同步
❌ 主库挂了 → binlog可能还没推 → 丢数据
2️⃣ 半同步复制(推荐)
Master写入binlog
↓ 推送
等1个Slave确认收到
返回客户端 ✅
⚠️ 至少1个从库有数据 → 切换不丢
3️⃣ 全同步复制
Master写入binlog
等所有Slave执行完
返回客户端 ✅
❌ 太慢了,任何1个从库卡住都影响写入
💻 开启半同步复制
# ===== 主库上安装半同步插件 ===== mysql> INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so'; mysql> SET GLOBAL rpl_semi_sync_master_enabled = 1; mysql> SET GLOBAL rpl_semi_sync_master_timeout = 1000; # 等1秒,超时降级为异步 # ===== 从库上安装半同步插件 ===== mysql> INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so'; mysql> SET GLOBAL rpl_semi_sync_slave_enabled = 1; mysql> STOP SLAVE IO_THREAD; START SLAVE IO_THREAD; # ===== 验证 ===== mysql> SHOW STATUS LIKE 'Rpl_semi_sync_master_status'; | Rpl_semi_sync_master_status | ON | # ✅ 半同步已开启
半同步复制的"超时降级"机制:
如果主库等了1秒(rpl_semi_sync_master_timeout),从库还没确认收到binlog → 主库自动降级为异步复制,不再等待。
这是为了保可用性——如果从库挂了,主库不至于被卡死。但降级期间如果主库也挂了,还是会丢数据。

类比:总店发出传真后等分店回电确认。如果1分钟内分店没回电,总店就不再等了,继续做生意(降级)。但这时如果总店着火了,这份传真就丢了。
🔑 MHA 切换流程 + 脑裂防护
正常切换流程:
1. MHA Manager检测到Master不可达(连续3次ping失败)
2. 验证其他Slave是否也认为Master挂了(防网络分区误判)
3. 选择数据最新的Slave提升为新Master
4. 其他Slave改为指向新Master
5. VIP漂移到新Master(应用无感切换)

脑裂防护:如果网络抖动导致MHA误判主库挂了,发起切换 → 旧主库还活着 → 两个Master同时写入 → 数据冲突!
解决:切换前先确保旧Master已停掉(SSH上去kill mysqld进程),这是MHA的默认行为。
④ Redis Cluster 脑裂 — 两个Master同时接受写入?
📅 Redis Cluster 3主3从。某天机房网络抖动,Node B和C之间断了,但A和B还能通。
结果:集群分裂成两个"小团体",各自选自己为主 → 两个Master同时接受写入 → 数据冲突。
🔍 Redis Cluster 的分片原理
📐 核心原理:16384个Hash Slot
Redis Cluster 把所有数据分成 16384个槽(hash slot),每个Master节点负责一部分槽。

写入流程:
1. 客户端发 SET user:101 "张三"
2. 计算 slot = CRC16("user:101") % 16384 = 比如得到 8190
3. 8190号槽属于Node B → 客户端把请求发给Node B
4. Node B写入成功

类比:一个图书馆把所有书按首字母分成3个阅览室。A-M在1号,N-S在2号,T-Z在3号。找书先算首字母 → 知道去哪个阅览室。如果1号阅览室关了 → 它的备览室自动开放。
📋 脑裂是怎么发生的?
😱 Redis Cluster 脑裂过程
正常状态
A (M) B (M) C (M)
3个Master互相通信(Ping/Pong) ✅
😱 网络分区
A (M)
B (M)
A和B互通
✖️
C (M)
C被孤立了
A和B认为C挂了 → 把C的Slave提升为新Master → 但C其实还活着 → 两个Master同时负责C的槽!
🛠️ 怎么防止脑裂?

Redis Cluster 的防护机制:超过半数Master不可达 = 集群停止服务

# Redis的cluster-node-timeout配置(默认15秒) cluster-node-timeout 15000 # 如果集群有3个Master: # - 1个Master不可达 → 剩余2个(>半数) → 正常工作,故障转移 # - 2个Master不可达 → 剩余1个(<半数) → 整个集群不可写! # 这就是为什么Redis Cluster推荐至少5个Master → 允许挂2个

类比:投票选举——如果超过一半的人联系不上,选举就暂停,不选出两个总统。Redis Cluster用的是"少数服从多数"原则。

生产教训:Redis Cluster的Master节点数必须是奇数(3、5、7),因为偶数的话挂一半刚好是"少数派",容易脑裂。3个Master只允许挂1个,5个Master允许挂2个。
⑤ Redis Cluster 扩缩容 — 加节点后数据迁移出问题了
📅 业务增长,缓存数据越来越多,3个Master快存不下了。运维决定加2个新Master节点。
扩容过程中,部分key迁移到新节点,但迁移到一半时客户端访问这些key → 报 MOVED 错误。
📋 扩容原理:槽迁移
Redis Cluster 扩容 — 槽迁移过程
扩容前:3个Master
A: 槽0~5460
B: 槽5461~10922
C: 槽10923~16383
扩容后:5个Master
A: 槽0~3276
B: 槽3277~6553
C: 槽6554~9830
D: 槽9831~13106
E: 槽13107~16383
⚠️ 迁移过程中,一个槽可能"一半在旧节点,一半在新节点" → 客户端访问需要特殊处理
# 添加新节点到集群 [root@redis ~]# redis-cli --cluster add-node 10.0.0.4:7001 10.0.0.1:7001 [OK] New node added # 重新分配槽(从现有节点匀一部分给新节点) [root@redis ~]# redis-cli --cluster reshard 10.0.0.1:7001 How many slots do you want to move? 3276 # 移3276个槽 What is the receiving node ID? xxx # 新节点的ID Source node #1: all # 从所有节点匀 # 迁移过程中客户端可能收到 MOVED 或 ASK # MOVED: 槽已经永久迁移到新节点 → 客户端更新路由表 # ASK: 槽正在迁移中,下次查询试试新节点 → 临时重定向

生产踩坑记录:

  • 迁移过程中查询失败 — 客户端收到MOVED但不更新路由表 → 反复重定向。解决:用支持Cluster的Jedis/Lettuce客户端
  • 迁移太慢 — 100GB数据迁移可能要数小时。解决:redis-cli --cluster reshard--cluster-threshold 参数控制迁移速度
  • 迁移时主节点挂了 — 正在迁移的槽数据不完整。解决:先迁移完再处理故障
最佳实践:在业务低峰期(凌晨2-5点)做扩缩容。迁移速度设为每秒1000个key,避免影响正常业务。扩容前确保每个新节点都有对应的Slave。
⑥ 缓存与数据库一致性 — Redis里的价格是18,数据库里是15?
📅 运营改了某个商品的价格(15→18元),写入了MySQL。但用户看到的还是15元——因为Redis缓存里还是旧的。
小林以为加个 DELETE 删缓存就行,结果更乱了:删缓存和写数据库之间,另一个请求又把旧值写回了缓存。
🔍 经典问题:先删缓存还是先写数据库?
❌ 方案A:先删缓存,再写DB
① 线程1:删缓存 (price=null)
② 线程2:读缓存miss → 查DB(旧值15) → 写回缓存(15)
③ 线程1:写DB(price=18)
④ 结果:缓存=15,DB=18 → 不一致!
类比:你先把白板擦了,准备写新值,但另一个人趁你写字的空档看了旧Excel又写回白板了
⚠️ 方案B:先写DB,再删缓存
① 线程1:写DB(price=18)
② 线程1:删缓存(price=null)
③ 线程2:读缓存miss → 查DB(新值18) → 写回缓存(18)
④ 结果:缓存=18,DB=18 → ✅ 一致
极端情况也有不一致窗口:删缓存失败 → 缓存一直是旧值
🛠️ 最终方案:延迟双删
延迟双删策略
public void updatePrice(Long productId, BigDecimal newPrice) {
    // 1. 先删缓存
    redis.del("product:" + productId);

    // 2. 写数据库
    productMapper.updatePrice(productId, newPrice);

    // 3. 延迟再删一次(等1-2秒)
    // 防止在步骤1和2之间,其他线程把旧值写回了缓存
    CompletableFuture.runAsync(() -> {
        try { Thread.sleep(1500); } catch (Exception e) {}
        redis.del("product:" + productId);
    });
}

为什么需要"第二次删"?

时间线:线程1删缓存 → 线程2查DB旧值 → 线程1写DB新值 → 线程2把旧值写回缓存

第二次删(延迟1.5秒后)就是在"线程2写回旧值"之后再把缓存清掉。这样下次读就会查DB拿到新值。

延迟时间设多少? — 略大于一次读操作的耗时即可,一般1-2秒。

更可靠的方案:Canal 监听 binlog
用Canal伪装成MySQL的从库,监听binlog变更 → 发消息到MQ → 消费者删Redis缓存。
这样不管是谁改了数据,都能自动清缓存。而且不走业务代码,不会影响写操作性能。

类比:不是你自己去擦白板,而是雇了个"助手"盯着Excel,一有人改了就自动帮你擦白板。
⑦ 分布式事务 — 订单创建了但库存没扣?
📅 微服务架构下,订单服务和库存服务是两个独立的应用,各有自己的数据库。
用户下单:订单服务创建订单 ✅ → 调库存服务扣库存 → 库存服务超时失败 ❌ → 订单创建了但库存没扣!超卖!
🔍 为什么会有这个问题?
📐 CAP 定理 — 分布式系统的不可能三角
C (Consistency) 一致性:所有节点看到的数据一样
A (Availability) 可用性:每个请求都能得到响应
P (Partition tolerance) 分区容错:网络断开时系统还能工作

不可能同时满足3个,只能选2个。在网络必定会断的现实下,P是必选的,所以只能在C和A之间二选一:
- CP系统:网络断开时宁可不可用也不返回不一致的数据(ZooKeeper、etcd)
- AP系统:网络断开时继续服务,但可能返回旧数据(Eureka、Cassandra)

类比:CP = 两个分店必须看到同样的价格才能营业(宁可关门也不让顾客看到不同价格);AP = 分店各自营业,可能有短暂的价格不一致(宁可让顾客看到不同价格也不关门)
📋 分布式事务的3种方案
方案原理类比优缺点
2PC (两阶段提交)协调者问所有人"准备好了吗",都回答Yes才执行班长相:问大家"作业都写好了吗",都点头了才统一交强一致但慢,有单点故障
TCC (Try-Confirm-Cancel)业务层面分3步:预留→确认→取消网购下单:先冻结库存(Try)→支付后扣库存(Confirm)→超时取消(Cancel)性能好但代码复杂
最终一致性允许短暂不一致,通过消息队列异步保证最终一致转账:先扣你的钱,发个通知给对方银行,对方银行慢慢加最实用,但有延迟窗口
💻 最终一致性方案(最常用)
OrderService.java — 基于MQ的最终一致性
@Transactional
public Order createOrder(OrderRequest req) {
    // 1. 创建订单(本地事务)
    Order order = new Order(req);
    order.setStatus("CREATED");
    orderMapper.insert(order);

    // 2. 发消息到MQ(半消息+本地事务)
    // RocketMQ事务消息机制:
    // - 先发半消息(对消费者不可见)
    // - 本地事务提交成功 → 半消息变成正常消息
    // - 本地事务回滚 → 半消息被丢弃
    rocketMQTemplate.sendMessageInTransaction(
        "order-topic",
        new OrderMessage(order.getId(), req.getProductId()),
        order // 传本地事务对象
    );

    return order;
}

// 库存消费者(另一个服务)
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "stock-group")
public class StockConsumer implements RocketMQListener<OrderMessage> {
    public void onMessage(OrderMessage msg) {
        // 扣库存(库存服务的本地事务)
        stockService.deduct(msg.getProductId(), 1);
        // 如果扣库存失败 → MQ会自动重试
        // 如果重试多次还是失败 → 进入死信队列,人工处理
    }
}
关键概念:可靠消息最终一致性
核心思路:本地事务 + 消息队列
1. 本地事务和发消息必须原子性——要么都成功,要么都失败(RocketMQ事务消息保证)
2. 消费者必须幂等——同一条消息消费多次结果一样(防止MQ重重复投递)
3. 消费者失败要重试——MQ自动重试,重试多次失败进入死信队列
⑧ 分布式锁 — 两个人同时抢最后一件商品?
📅 秒杀场景:库存只剩1件,两个用户同时点击"购买"。两个请求同时读到库存=1 → 都扣减 → 库存变成-1 → 超卖!
需要一把锁:谁先抢到谁就能扣库存,另一个等。
🔍 为什么单机锁不行?

单机锁(synchronized/ReentrantLock)只在一个JVM进程内有效。微服务架构下,2台应用服务器各自有1把锁 → 锁不住对方!

类比:两个教室各有自己的门锁。你锁了1号教室的门,但2号教室还有另一扇门可以进入同一个仓库 → 锁了个寂寞。

🛠️ Redis 分布式锁的演进
版本实现问题
V1 简单版SETNX key value❌ 持有锁的进程挂了 → 死锁!锁永远不释放
V2 加过期SETNX key value EX 30⚠️ 设置过期和加锁不是原子操作
V3 原子加锁SET key value EX 30 NX⚠️ 业务没执行完锁就过期了 → 别人拿到锁 → 并发执行
V4 防误删value设为唯一ID,删除时先检查ID是否匹配⚠️ 检查和删除不是原子操作
V5 Lua脚本用Lua脚本保证"检查+删除"原子性⚠️ 锁续期问题(看门狗)
V6 Redisson自动续期 + Lua原子操作 + 可重入✅ 生产推荐!但Master挂了锁可能丢失
💻 Redisson 分布式锁
SeckillService.java
@Service
public class SeckillService {

    @Autowired
    private RedissonClient redisson;

    public SeckillResult seckill(Long userId, Long productId) {
        RLock lock = redisson.getLock("seckill:" + productId);
        try {
            // 尝试加锁:等5秒,锁30秒后自动过期
            // Redisson的看门狗会自动续期(每10秒续30秒)
            boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
            if (!locked) {
                return SeckillResult.fail("手慢了,再试试");
            }

            // 扣库存
            int stock = stockMapper.getStock(productId);
            if (stock <= 0) {
                return SeckillResult.fail("已售罄");
            }
            stockMapper.deduct(productId);
            orderService.createOrder(userId, productId);
            return SeckillResult.success();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock(); // 释放锁(Lua脚本原子删除)
            }
        }
    }
}
🔑 Redisson 看门狗(Watchdog)原理
问题:锁设了30秒过期,但业务执行了50秒 → 第31秒时锁自动过期 → 别人拿到了锁 → 两个线程同时执行!

看门狗解决:Redisson加锁时启动一个后台线程(看门狗),每隔 lockWatchdogTimeout/3(默认10秒)检查一次:如果锁还被当前线程持有 → 自动续期到30秒。
业务执行完释放锁 → 看门狗停止续期。

类比:你租了个储物柜(30分钟),但你要用更久。你每隔10分钟就去前台续一下租期。等你用完了,不用续了,柜子30分钟后自动开锁。
极端场景:Redlock 算法
如果Redis Master加了锁但还没同步到Slave就挂了 → Slave提升为新Master → 新Master没有这把锁 → 另一个人也能加锁 → 两把锁!
Redlock:向5个独立的Redis实例加锁,超过半数(3个)成功才算加锁成功。
类比:去5个银行保管箱都存一份,只要3个银行确认了就算存成功。一个银行着火不怕。
⑨ 分布式ID — 分库分表后自增ID冲突了?
📅 订单表做了水平分库,分成了 order_0 和 order_1。两个库各自自增 → 都从1开始 → ID冲突!
老陈说:"用雪花算法生成全局唯一ID。"
🔍 为什么自增ID不行了?

单库自增ID:MySQL的AUTO_INCREMENT在单库内保证唯一。但分库后,每个库都从1开始自增 → 两个库都可能出现 id=1 的记录 → 查询混乱。

类比:两个分店都从"第1号订单"开始编号 → 汇总时发现有2个"1号订单",不知道谁是谁。

📋 4种分布式ID方案
方案原理类比优缺点
UUID随机生成32位16进制字符串每个人自己编一个没人用过的编号无序→索引效率差,太长
数据库号段用一个表分配号段:A拿1-1000,B拿1001-2000总店给分店1发001-100号牌,给分店2发101-200号牌简单但依赖DB,号段用完要再取
Redis INCRINCR order_id_seq前台有个计数器,每来一个号+1快但Redis挂了就废了
Snowflake雪花时间戳+机器ID+序列号 → 64位long编号里包含日期+分店号+当天序号✅ 推荐!不依赖外部服务
📐 雪花算法结构
Snowflake ID 结构(64位 = 8字节 long)
0
1位
符号位
时间戳
41位
约69年
机器ID
10位
1024台
序列号
12位
4096/毫秒
每毫秒能生成 4096 个ID,每秒约 400万 个。足够了!
时间戳41位:从2020年起算,可用约69年(到2089年)
机器ID10位:5位数据中心ID + 5位机器ID = 32×32 = 1024台机器

示例:ID = 1867382947382937600 可以反推出:
- 生成时间:2026-06-13 09:15:32
- 机器ID:5号数据中心/3号机器
- 序列号:352
💻 Spring Boot 集成雪花算法
# pom.xml <dependency> <groupId>com.github.yitter</groupId> <artifactId>yitter-idgenerator</artifactId> <version>1.0.6</version> </dependency> # application.yml yitter: worker-id: 1 # 机器ID(0-1023),每台机器不同! # Java代码 long id = IdGeneratorHelper.nextId(); // 一行搞定 → 1867382947382937600
时钟回拨问题:
雪花算法依赖系统时钟。如果NTP同步导致时钟回拨(比如从10:00:05跳回10:00:03)→ 生成的ID可能和3秒前重复!
解决:检测到回拨 → 抛异常 / 等待时钟追上 / 使用历史最大时间戳。Yitter等开源实现都已处理。
⑩ CAP理论与技术选型 — 为什么没有完美的方案?
📅 新项目启动,技术选型会上争论不休:
"用ZooKeeper还是Nacos做服务注册?" "用Kafka还是RabbitMQ?" "用强一致还是最终一致?"
老陈说:"先搞清楚CAP,再选型。没有银弹,只有取舍。"
📐 CAP 深度理解
CAP 三角 — 只能选两个(P是必选)
C
一致性
A
可用性
P
分区容错
网络必断
→ P是必选
→ 在C和A之间选
📋 常见技术的CAP定位
技术CAP选择为什么这么选典型场景
MySQL主从AP主库挂了从库还能读 → 可用;但可能读到旧数据 → 不一致大部分互联网业务
ZooKeeperCP超过半数节点不可用 → 整个集群不可用;保证数据强一致配置中心、分布式锁
NacosAP+CP临时实例用AP(服务注册),永久实例用CP(配置管理)服务发现+配置中心
Redis ClusterAP网络分区时两边都能写 → 可用;但可能数据不一致缓存、会话
ElasticsearchAP主分片挂了副分片顶上 → 可用;但副分片可能少几条数据搜索引擎
KafkaAPLeader挂了Follower顶上 → 可用;但可能丢几条消息消息队列、日志
📐 BASE 理论 — CAP的实用版
📐 BASE = Basically Available + Soft State + Eventually Consistent
Basically Available(基本可用):系统出故障时,允许响应时间变长或功能降级,但不停服
Soft State(软状态):允许系统中的数据存在中间状态(比如订单"处理中"),不要求每时每刻都一致
Eventually Consistent(最终一致性):系统保证在一段时间后,数据最终会达到一致

类比:你给朋友转账100块。银行不需要你和朋友的余额同一毫秒都变(强一致),只需要保证你的钱扣了,朋友的钱最终会多100块(最终一致)。中间可能有几秒的不一致窗口,但最终会一致。
📊 生产环境选型决策树
你的业务能接受短暂不一致吗?
能接受 → AP
用户头像、商品描述、评论列表
→ 用异步复制、缓存、MQ
→ 追求高性能、高可用
不能接受 → CP
账户余额、库存数量、支付流水
→ 用强一致(事务/分布式锁)
→ 追求数据准确
⚠️ 实际生产中,大部分系统是混合的:
核心链路(下单、支付)用CP,非核心链路(推荐、评论)用AP
这就是"最终一致性+局部强一致"的架构模式
📝 全文总结 — 10个问题的核心要点
#问题核心原理生产解法
主从延迟从库SQL线程单线程回放跟不上主库并发写入多线程复制 + 强制走主库 + 半同步
读写不一致写主库后立刻读从库,从库还没同步写后5秒走主库 + ShardingSphere路由
主从切换丢数据异步复制下主库binlog可能还没推到从库半同步复制 + MHA + 确保旧Master停掉
Redis Cluster脑裂网络分区导致两个Master同时接受写入奇数Master + 超过半数不可达则停服
Redis扩缩容槽迁移过程中key跨节点,客户端需处理MOVED低峰期操作 + 支持Cluster的客户端
缓存DB不一致并发下先删缓存再写DB会被回写旧值先写DB再删缓存 + 延迟双删 + Canal监听binlog
分布式事务多个服务各管各的DB,本地事务不够用MQ最终一致性 + RocketMQ事务消息
分布式锁单机锁锁不住跨进程,Redis锁要防死锁/误删Redisson + 看门狗自动续期 + Lua原子操作
分布式ID分库后自增ID冲突雪花算法(时间戳+机器ID+序列号)
CAP选型网络必断→P必选→在C和A之间权衡核心链路CP,非核心AP → BASE最终一致
一句话总结:分布式系统没有银弹。每个方案都是在一致性、可用性、性能之间做取舍。理解CAP,选对方案,做好降级和兜底,才是生产级架构。