第 29 章:数据一致性
学习目标
- 理解 CAP/BASE 与一致性级别
- 掌握分布式协议:Paxos、Raft
- 学会最终一致性的实践
- 避免脑裂、数据丢失的坑
一、一致性级别
text
强一致性: 任何读都返回最新写
├── 线性一致性: 实时同步
└── 顺序一致性: 按顺序
弱一致性: 不保证读到最新
├── 因果一致性: 因果相关操作有序
└── 最终一致性: 经过一段时间后一致java
// 单库:强一致(ACID)
// 多库:最终一致(消息队列)二、CAP 定理
text
C - Consistency
A - Availability
P - Partition tolerance
分布式系统 P 必然存在,只能选 C 或 Ajava
// 选 C:CP 系统(Etcd、Zookeeper)
// 选 A:AP 系统(Eureka、Cassandra)⚠️ 坑 1:"我全都要" → 选 CP 又要 7x24 不停 = 矛盾。业务决定优先级。
三、BASE 理论
text
BA - Basically Available(基本可用)
S - Soft State(软状态)
E - Eventually Consistent(最终一致)java
// 实战:下单 + 扣库存
// 1. 订单库 INSERT(本地事务)
// 2. 发消息 topic=order.placed
// 3. 库存服务消费,扣库存
// 4. 1-2 秒后数据一致四、分布式协议
Paxos
text
Paxos 难点
├── Proposer: 提议
├── Acceptor: 投票
└── Learner: 学习
多数派共识,> 50% 同意 = 提案通过Raft
text
Raft 三角色
├── Leader: 接收写入,广播
├── Follower: 同步日志
└── Candidate: 竞选 Leader
Leader 挂了,FOLLOWER 竞选新 Leaderjava
// Etcd 用 Raft
// Zookeeper 用 ZAB(类似 Paxos)
// Consul 用 Raft⚠️ 坑 2:Raft 集群建议 3 或 5 节点,偶数节点 4 个不行(无法形成多数派),实际 3 节点容忍 1 故障。
五、Quorum 机制
java
// 写:必须 N 个副本中 W 个返回成功
// 读:必须从 N 个副本中读 R 个
// W + R > N 保证强一致
// 例:N=3, W=2, R=2
// 写至少 2 个副本成功
// 读至少读 2 个副本,返回最新java
// 实战:对象存储
public class S3Service {
private final int N = 3; // 副本数
private final int W = 2; // 写成功数
private final int R = 2; // 读副本数
public void put(String key, byte[] data) {
List<Future<Boolean>> futures = new ArrayList<>();
for (Node node : nodes) {
futures.add(executor.submit(() -> node.write(key, data)));
}
// 至少 W 个成功
int successes = 0;
for (Future<Boolean> f : futures) {
if (f.get()) successes++;
}
if (successes < W) throw new RuntimeException("Write failed");
}
}六、最终一致性实践
java
// 1. 写前写后对比
public void writeToCache(String key, Object value) {
long timestamp = redis.opsForValue().getOperations().getExpire(key);
// 写时检查
redis.opsForValue().set(key, value);
// 异步校验
executor.submit(() -> {
Object dbValue = readFromDB(key);
if (!value.equals(dbValue)) {
// 不一致,重写
redis.opsForValue().set(key, dbValue);
}
});
}java
// 2. 版本号兜底
public class ProductCache {
private Product product;
private long version;
}
public Product getProduct(Long id) {
ProductCache cache = redis.get(...);
if (cache.version < dbVersion(id)) {
cache = rebuild(id);
}
return cache.product;
}七、对账系统
java
// 每日对账
@Scheduled(cron = "0 0 3 * * ?")
public void reconcile() {
log.info("开始对账");
// 1. 订单总额 vs 实际收款
BigDecimal orderTotal = orderDao.sumPaidAmount(yesterday);
BigDecimal paymentTotal = paymentDao.sumReceived(yesterday);
if (orderTotal.compareTo(paymentTotal) != 0) {
alert.send("订单/支付不一致:order=" + orderTotal + ", payment=" + paymentTotal);
}
// 2. 订单数 vs 库存扣减数
int orderCount = orderDao.countPaid(yesterday);
int deductCount = inventoryDao.countDeducted(yesterday);
if (orderCount != deductCount) {
alert.send("订单/库存扣减不一致:order=" + orderCount + ", deduct=" + deductCount);
}
}java
// 失败的补偿
public void compensate(Order order) {
// 1. 检查订单状态
if (order.status == PAID) {
// 2. 检查库存是否扣减
if (!inventoryDao.isDeducted(order.id)) {
inventoryService.deduct(order);
}
}
}⚠️ 坑 3:对账任务遍历百万订单,内存爆。分批 + 多线程,每次处理 1000 条。
八、分布式锁与一致性
java
// 用分布式锁保证关键操作不重复
public void processOrder(Long orderId) {
RLock lock = redisson.getLock("order:" + orderId);
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
try {
Order order = orderRepository.findById(orderId);
if (order.status != PROCESSED) {
doProcess(order);
order.setStatus(PROCESSED);
}
} finally {
lock.unlock();
}
}
}九、向量时钟
java
// 用于检测并发更新
public class VectorClock {
Map<String, Long> clocks = new HashMap<>();
public void increment(String node) {
clocks.merge(node, 1L, Long::sum);
}
// v1 happens-before v2:所有时钟值 v1 <= v2
public boolean isBefore(VectorClock other) {
return clocks.entrySet().stream()
.allMatch(e -> e.getValue() <= other.clocks.getOrDefault(e.getKey(), 0L));
}
}十、实战:分布式系统 5 个常见不一致
java
// 1. 缓存与 DB
// 解决:先 DB 后删缓存,延迟双删
// 2. 主从延迟
// 解决:关键业务读主库,异步校对
// 3. 分布式事务
// 解决:TCC/Saga/可靠消息
// 4. 消息重复
// 解决:幂等消费
// 5. 跨地域同步
// 解决:异步同步 + 冲突解决本章小结
| 级别 | 适用 | 方案 |
|---|---|---|
| 强一致 | 金融 | 2PC、XA |
| 最终一致 | 通用 | 消息、补偿 |
| 弱一致 | 通知 | 推送 |
| 协议 | 用途 |
|---|---|
| Paxos | 共识 |
| Raft | 共识(简单) |
| ZAB | ZK |
| 关键点 | 建议 |
|---|---|
| 集群 | 3 或 5 节点 |
| Quorum | W+R>N |
| 对账 | 每日一次 |
| 幂等 | 必做 |
动手练习
- Raft 集群:用 etcd 部署 3 节点,看 leader 选举
- 最终一致:下单 + 消息,观察 1 秒后库存扣减
- 对账脚本:写一个脚本比对订单和库存
- 冲突检测:用 Redis Set 模拟分布式锁,验证读写一致
下一章:第 30 章:安全基础 →