① MySQL 主从延迟 — 用户改了密码,刷新后还是旧的?
📅 周二下午2点,客服群里炸了:"我改了密码,退出重新登录居然还是旧密码!你们系统有bug吗?"
后端开发小林查了一下代码——改密码写的明明是主库,登录验证也是查主库啊?不,等等,登录那个接口……走的是从库。
👩
客服小刘
用户张三反馈:改了密码后重新登录失败,提示密码错误。已经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
Exec_Master_Log_Pos: 4589231
Read_Master_Log_Pos: 4599876
mysql> SHOW GLOBAL VARIABLES LIKE 'gtid_executed';
🛠️ 怎么解决?
| 方案 | 原理 | 类比 | 优缺点 |
| 强制走主库 | 对一致性要求高的查询直接查主库 | 想确认最新价格就打电话问总店 | 简单但增加主库压力 |
| 多线程复制 | 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还没推到从库 → 数据丢失。
🧑💻
技术负责人老陈
昨天主库宕机切换,丢失了约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;
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和B认为C挂了 → 把C的Slave提升为新Master → 但C其实还活着 → 两个Master同时负责C的槽!
🛠️ 怎么防止脑裂?
Redis Cluster 的防护机制:超过半数Master不可达 = 集群停止服务
cluster-node-timeout 15000
类比:投票选举——如果超过一半的人联系不上,选举就暂停,不选出两个总统。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
What is the receiving node ID? xxx
Source node #1: all
生产踩坑记录:
- 迁移过程中查询失败 — 客户端收到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 INCR | INCR 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 集成雪花算法
<dependency>
<groupId>com.github.yitter</groupId>
<artifactId>yitter-idgenerator</artifactId>
<version>1.0.6</version>
</dependency>
yitter:
worker-id: 1
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 | 主库挂了从库还能读 → 可用;但可能读到旧数据 → 不一致 | 大部分互联网业务 |
| ZooKeeper | CP | 超过半数节点不可用 → 整个集群不可用;保证数据强一致 | 配置中心、分布式锁 |
| Nacos | AP+CP | 临时实例用AP(服务注册),永久实例用CP(配置管理) | 服务发现+配置中心 |
| Redis Cluster | AP | 网络分区时两边都能写 → 可用;但可能数据不一致 | 缓存、会话 |
| Elasticsearch | AP | 主分片挂了副分片顶上 → 可用;但副分片可能少几条数据 | 搜索引擎 |
| Kafka | AP | Leader挂了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,选对方案,做好降级和兜底,才是生产级架构。