Skip to content
第 29 章 架构 ⏱ 11 分钟阅读

第 29 章:数据一致性 ​

学习目标 ​

  • 理解 CAP/BASE 与一致性级别
  • 掌握分布式协议:Paxos、Raft
  • 学会最终一致性的实践
  • 避免脑裂、数据丢失的坑

一、一致性级别 ​

text
强一致性: 任何读都返回最新写
├── 线性一致性: 实时同步
└── 顺序一致性: 按顺序

弱一致性: 不保证读到最新
├── 因果一致性: 因果相关操作有序
└── 最终一致性: 经过一段时间后一致
java
// 单库:强一致(ACID)
// 多库:最终一致(消息队列)

二、CAP 定理 ​

text
C - Consistency
A - Availability
P - Partition tolerance

分布式系统 P 必然存在,只能选 C 或 A
java
// 选 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 竞选新 Leader
java
// 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共识(简单)
ZABZK
关键点建议
集群3 或 5 节点
QuorumW+R>N
对账每日一次
幂等必做

动手练习 ​

  1. Raft 集群:用 etcd 部署 3 节点,看 leader 选举
  2. 最终一致:下单 + 消息,观察 1 秒后库存扣减
  3. 对账脚本:写一个脚本比对订单和库存
  4. 冲突检测:用 Redis Set 模拟分布式锁,验证读写一致

下一章:第 30 章:安全基础 →

本站基于 VitePress 构建 · 由 StackHub 团队维护