问题

什么是最终一致性?

答案

1. 核心概念

最终一致性(Eventual Consistency)是分布式系统中的一种一致性模型,指系统在一段时间内可能处于不一致状态,但最终会达到一致状态。

通俗理解

  • 强一致性:就像银行转账,你转出100元,对方必须立即收到100元
  • 最终一致性:就像微信朋友圈点赞,你点赞后,朋友可能过几秒才看到,但最终一定能看到

关键特征

  • 系统在一段时间内可能不一致
  • 不需要立即返回最新数据
  • 保证在没有新更新的情况下,最终所有副本都会一致

2. 理论基础

CAP理论

CAP定理:分布式系统最多只能同时满足三个特性中的两个

    Consistency           Availability         Partition Tolerance
    (一致性)              (可用性)              (分区容错性)
        ↓                     ↓                      ↓
   所有节点同时          服务一直可用          网络分区时系统
   看到相同数据          (快速响应)            仍能工作

分布式系统必须容忍分区(P),所以只能在C和A之间权衡:
- CP系统:选择一致性,牺牲可用性(如Zookeeper、HBase)
- AP系统:选择可用性,牺牲强一致性(如Cassandra、DynamoDB)

网络分区示例

正常情况:
节点A ←──网络──→ 节点B
数据一致,互相同步

网络分区:
节点A  X━━━━━X  节点B
网络故障,无法通信

此时面临选择:
1. CP:停止服务(保证一致性,牺牲可用性)
2. AP:继续服务(保证可用性,允许数据不一致)

BASE理论

BASE是对CAP的延伸,是对AP系统的实践总结:

  • BA (Basically Available):基本可用
    • 允许损失部分可用性
    • 例如:响应时间变长、功能降级
  • S (Soft State):软状态
    • 允许系统存在中间状态
    • 中间状态不影响系统整体可用性
  • E (Eventually Consistent):最终一致性
    • 系统中的数据副本经过一段时间后,最终能达到一致

ACID vs BASE

ACID (传统数据库)              BASE (分布式系统)
┌──────────────┐              ┌──────────────┐
│ Atomicity    │              │ Basically    │
│ Consistency  │    ──→       │ Available    │
│ Isolation    │              │ Soft State   │
│ Durability   │              │ Eventually   │
└──────────────┘              └──────────────┘
  强一致性                      最终一致性
  刚性事务                      柔性事务

3. 一致性级别

一致性模型谱系

强一致性 ←──────────────────────────────────→ 弱一致性
    │                                           │
    │                                           │
线性一致性  顺序一致性  因果一致性  最终一致性  弱一致性
(Linearizability) (Sequential) (Causal) (Eventual) (Weak)
    │              │            │          │        │
    最严格        严格         中等        宽松     最宽松
    性能最差                              性能最好

强一致性(Strong Consistency)

定义:任何时刻,所有节点看到的数据都是一致的。

// 示例:强一致性读写
public void strongConsistency() {
    // 写入数据
    database.write("key", "value");  // t1时刻
    
    // 立即读取,必须读到最新值
    String value = database.read("key");  // t2时刻
    // value 一定等于 "value",即使t2-t1只有几毫秒
}

时序图

客户端A          主节点          从节点1         从节点2
  |               |               |               |
  |--写入value--->|               |               |
  |               |--同步-------->|               |
  |               |--同步--------------------->   |
  |               |<--确认--------|               |
  |               |<--确认------------------------| 
  |<--写入成功----|               |               |
  |               |               |               |
  |--读取-------->|               |               |
  |<--返回value---|               |               |
       ↑
   读到最新值(所有节点已同步)

特点

  • ✅ 数据一致性最强
  • ❌ 性能较差(需等待所有节点同步)
  • ❌ 可用性较低(部分节点故障影响服务)

应用场景:金融系统、库存系统

最终一致性(Eventual Consistency)

定义:系统保证在没有新更新的情况下,经过一段时间后,所有副本最终会一致。

// 示例:最终一致性读写
public void eventualConsistency() {
    // 写入数据
    database.write("key", "value");  // t1时刻
    
    // 立即读取,可能读到旧值
    String value = database.read("key");  // t2时刻(t2-t1很短)
    // value 可能是旧值,也可能是新值
    
    // 等待一段时间后读取
    Thread.sleep(1000);
    value = database.read("key");  // t3时刻
    // value 最终会等于 "value"
}

时序图

客户端A          主节点          从节点1         从节点2
  |               |               |               |
  |--写入value--->|               |               |
  |<--写入成功----|               |               |  (立即返回,异步同步)
  |               |               |               |
  |               |--异步同步---→ |               |
  |--读取--------------------->  |               |
  |<--返回oldValue(未同步)-----|               |
  |               |               |               |
  |               |--异步同步-------------------→ |
  |               |               |               |
  |--等待一段时间--|               |               |
  |               |               |               |
  |--读取--------------------->  |               |
  |<--返回value(已同步)--------|               |
       ↑
   最终读到新值

特点

  • ✅ 性能好(异步同步)
  • ✅ 可用性高(不等待所有节点)
  • ❌ 存在数据不一致窗口期

应用场景:社交网络(点赞、评论)、日志系统、缓存系统

4. 最终一致性的实现方案

方案1:异步消息

原理:通过消息队列异步通知其他节点。

@Service
public class OrderService {
    
    @Autowired
    private MessageProducer messageProducer;
    
    /**
     * 创建订单(主库)
     */
    @Transactional
    public void createOrder(Order order) {
        // 1. 写入主库
        orderMapper.insert(order);
        
        // 2. 发送消息到MQ(异步)
        OrderCreatedMessage message = new OrderCreatedMessage(order);
        messageProducer.send("order-topic", message);
        
        // 3. 立即返回(不等待从库同步)
    }
}

// 从库消费者
@Service
public class OrderReplicaConsumer {
    
    /**
     * 消费消息,同步到从库
     */
    @RabbitListener(queues = "order-topic")
    public void onOrderCreated(OrderCreatedMessage message) {
        // 同步到从库(最终一致)
        replicaOrderMapper.insert(message.getOrder());
    }
}

时序

主库写入 → 立即返回 → 异步发送MQ → 从库消费 → 最终一致
  t0         t1         t2          t3        t4
  ↑          ↑                               ↑
主库一致   对外可用                      从库一致
          (不一致窗口期: t1-t4)

方案2:定时对账

原理:通过定时任务对比数据差异,修复不一致。

@Service
public class DataReconciliationService {
    
    /**
     * 定时对账任务(每小时执行)
     */
    @Scheduled(cron = "0 0 * * * ?")
    public void reconcile() {
        // 1. 从主库查询数据
        List<Order> masterOrders = masterOrderMapper.selectAll();
        
        // 2. 从从库查询数据
        List<Order> replicaOrders = replicaOrderMapper.selectAll();
        
        // 3. 对比差异
        Set<String> masterIds = masterOrders.stream()
            .map(Order::getId)
            .collect(Collectors.toSet());
        
        Set<String> replicaIds = replicaOrders.stream()
            .map(Order::getId)
            .collect(Collectors.toSet());
        
        // 4. 主库有但从库没有的数据
        Set<String> missingIds = new HashSet<>(masterIds);
        missingIds.removeAll(replicaIds);
        
        // 5. 补偿同步
        for (String id : missingIds) {
            Order order = masterOrderMapper.selectById(id);
            replicaOrderMapper.insert(order);
            log.info("Reconciliation: synced order {}", id);
        }
    }
}

方案3:版本号机制

原理:通过版本号判断数据新旧,逐步同步到最新版本。

@Data
public class VersionedData {
    private String id;
    private String value;
    private Long version;  // 版本号
    private Long timestamp; // 时间戳
}

@Service
public class VersionedDataService {
    
    /**
     * 读取数据(最终一致性读)
     */
    public VersionedData read(String id) {
        // 从多个副本读取
        VersionedData data1 = replica1.read(id);
        VersionedData data2 = replica2.read(id);
        VersionedData data3 = replica3.read(id);
        
        // 返回版本号最大的数据(最新)
        return Stream.of(data1, data2, data3)
            .max(Comparator.comparing(VersionedData::getVersion))
            .orElse(null);
    }
    
    /**
     * 写入数据
     */
    @Transactional
    public void write(String id, String value) {
        // 递增版本号
        Long newVersion = getCurrentVersion(id) + 1;
        
        VersionedData data = new VersionedData();
        data.setId(id);
        data.setValue(value);
        data.setVersion(newVersion);
        data.setTimestamp(System.currentTimeMillis());
        
        // 异步同步到所有副本
        syncToReplicas(data);
    }
}

方案4:读时修复

原理:读取时发现不一致,触发修复。

@Service
public class ReadRepairService {
    
    public Order readOrder(String orderId) {
        // 从多个副本读取
        Order order1 = replica1.read(orderId);
        Order order2 = replica2.read(orderId);
        Order order3 = replica3.read(orderId);
        
        // 检查是否一致
        if (!isConsistent(order1, order2, order3)) {
            // 不一致,触发修复
            Order latestOrder = getLatest(order1, order2, order3);
            
            // 异步修复其他副本
            asyncRepair(orderId, latestOrder);
        }
        
        // 返回最新数据
        return getLatest(order1, order2, order3);
    }
    
    private void asyncRepair(String orderId, Order latestOrder) {
        executor.execute(() -> {
            replica1.write(orderId, latestOrder);
            replica2.write(orderId, latestOrder);
            replica3.write(orderId, latestOrder);
        });
    }
}

5. 实际应用案例

案例1:Redis主从复制

Master (主节点)
   |
   |-- 客户端写入 key=value
   |-- 立即返回成功 ✓
   |
   |-- 异步同步 --->  Slave1 (从节点1)
   |-- 异步同步 --->  Slave2 (从节点2)
   
读操作:
   客户端读取 Slave1 → 可能读到旧值(最终一致)
   客户端读取 Master → 读到新值(强一致)

配置

# Redis主从复制默认是异步的(最终一致性)
replicaof 127.0.0.1 6379

# 可以配置同步复制(类似强一致性,但性能差)
min-replicas-to-write 1
min-replicas-max-lag 10

案例2:MySQL主从同步

-- 主库写入
INSERT INTO orders (id, user_id, amount) VALUES (1, 100, 50.00);
-- 立即返回

-- 从库异步同步(延迟几毫秒到几秒)
-- 读从库可能暂时读不到新数据

解决方案

@Service
public class OrderService {
    
    @Autowired
    private DataSource masterDataSource;  // 主库
    @Autowired
    private DataSource slaveDataSource;   // 从库
    
    /**
     * 写操作:使用主库
     */
    public void createOrder(Order order) {
        masterDataSource.getConnection()
            .prepareStatement("INSERT INTO orders ...")
            .execute();
    }
    
    /**
     * 读操作:根据场景选择
     */
    public Order getOrder(String orderId, boolean requireLatest) {
        DataSource ds;
        if (requireLatest) {
            // 需要最新数据,读主库(强一致性)
            ds = masterDataSource;
        } else {
            // 可以接受延迟,读从库(最终一致性)
            ds = slaveDataSource;
        }
        
        return ds.getConnection()
            .prepareStatement("SELECT * FROM orders WHERE id = ?")
            .executeQuery();
    }
}

案例3:分布式缓存

@Service
public class ProductService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private ProductMapper productMapper;
    
    /**
     * 更新商品信息
     */
    @Transactional
    public void updateProduct(Product product) {
        // 1. 更新数据库
        productMapper.updateById(product);
        
        // 2. 异步删除缓存(延迟双删)
        String key = "product:" + product.getId();
        redisTemplate.delete(key);
        
        // 3. 延迟再删一次(处理并发问题)
        executor.schedule(() -> {
            redisTemplate.delete(key);
        }, 1, TimeUnit.SECONDS);
        
        // 缓存和数据库最终一致
    }
    
    /**
     * 查询商品
     */
    public Product getProduct(String productId) {
        String key = "product:" + productId;
        
        // 1. 先查缓存
        Product product = (Product) redisTemplate.opsForValue().get(key);
        if (product != null) {
            return product;  // 可能是旧数据(最终一致)
        }
        
        // 2. 缓存未命中,查数据库
        product = productMapper.selectById(productId);
        
        // 3. 写入缓存
        redisTemplate.opsForValue().set(key, product, 10, TimeUnit.MINUTES);
        
        return product;
    }
}

6. 最终一致性的挑战

挑战1:不一致窗口期

问题:在一致性窗口期内,用户可能看到不一致的数据。

场景:用户修改昵称
t0: 修改昵称为"Alice" → 主库已更新
t1: 从库1正在同步中...
t2: 用户刷新页面,读到从库1 → 显示旧昵称"Bob"(用户困惑)
t3: 从库1同步完成
t4: 用户再次刷新 → 显示新昵称"Alice"

解决方案

// 方案1:写后读主库
@Service
public class UserService {
    
    private ThreadLocal<Boolean> forceReadMaster = new ThreadLocal<>();
    
    public void updateNickname(String userId, String nickname) {
        // 更新主库
        masterDB.update(userId, nickname);
        
        // 标记:接下来的读操作强制读主库
        forceReadMaster.set(true);
    }
    
    public User getUser(String userId) {
        if (Boolean.TRUE.equals(forceReadMaster.get())) {
            // 读主库(强一致)
            return masterDB.selectById(userId);
        } else {
            // 读从库(最终一致)
            return slaveDB.selectById(userId);
        }
    }
}

// 方案2:客户端缓存
public void updateNickname(String userId, String nickname) {
    masterDB.update(userId, nickname);
    
    // 返回最新数据给客户端
    return new User(userId, nickname);
    // 客户端短时间内使用这个数据,不再查询
}

挑战2:数据冲突

问题:多个节点同时更新,产生冲突。

场景:两个用户同时修改同一文档

节点A: doc.title = "Title A"  (t1)
节点B: doc.title = "Title B"  (t2)

异步同步后,如何决定最终值?

解决方案

// 方案1:Last Write Wins(最后写入胜出)
public void resolve(VersionedData dataA, VersionedData dataB) {
    if (dataA.getTimestamp() > dataB.getTimestamp()) {
        return dataA;
    } else {
        return dataB;
    }
}

// 方案2:Vector Clock(向量时钟)
public class VectorClock {
    private Map<String, Long> versions;
    
    public boolean happensAfter(VectorClock other) {
        // 判断因果关系
        // ...
    }
}

// 方案3:业务层合并
public Document resolve(Document docA, Document docB) {
    // 业务逻辑合并两个版本
    Document merged = new Document();
    merged.setTitle(docA.getTitle());  // 取A的标题
    merged.setContent(docB.getContent()); // 取B的内容
    return merged;
}

7. 总结

核心要点

  • 最终一致性是分布式系统的权衡选择
  • 系统在一段时间内可能不一致,但最终会一致
  • 基于CAP理论和BASE理论
  • 通过异步消息、定时对账等方式实现

一致性级别选择: | 场景 | 一致性要求 | 推荐方案 | |——|———–|———| | 金融交易 | 强一致性 | 同步复制、2PC | | 库存扣减 | 强一致性 | TCC | | 用户昵称 | 最终一致性 | 异步消息 | | 点赞评论 | 最终一致性 | 异步消息 | | 日志记录 | 最终一致性 | 异步消息 |

面试要点

  • 能清晰描述最终一致性的概念
  • 理解CAP和BASE理论
  • 掌握最终一致性的实现方案
  • 能够对比强一致性和最终一致性的适用场景
  • 了解最终一致性面临的挑战和解决方案