问题
什么是最终一致性?
答案
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理论
- 掌握最终一致性的实现方案
- 能够对比强一致性和最终一致性的适用场景
- 了解最终一致性面临的挑战和解决方案