副标题:如何确保分布式系统真的达到了一致?🎯
🎬 开场:最终一致性的陷阱
什么是最终一致性?
强一致性(Strong Consistency):
写入后立即可见
Client → 写入数据 → 立即能读到 ✅
最终一致性(Eventual Consistency):
写入后不保证立即可见,但"最终"会一致
Client → 写入数据 → 暂时读不到 → 等一会儿 → 能读到了 ✅
问题:这个"最终"是多久?
- 1秒?
- 10秒?
- 1分钟?
- 永远?❌ (这才是问题所在!)
真实故障案例
某电商平台的恐怖故事:
10:00 用户A下单,支付成功
订单状态:已支付
10:01 订单服务同步状态到数据仓库
预期:最终一致性,几秒后同步完成
10:05 数据分析师查询:订单状态还是"未支付" ❌
12:00 中午开会讨论:为什么今天上午销售额是0?
14:00 技术排查:发现消息队列堆积了10万条消息!
15:00 问题根因:消费者挂了,消息没被处理
结论:
所谓的"最终一致性"变成了"永远不一致"!💥
📚 一致性验证方法
方法1:对账系统 ⭐⭐⭐⭐⭐
最可靠的验证方式!
架构设计
┌─────────────────────────────────────┐
│ 对账系统 │
│ (Reconciliation System) │
│ │
│ ┌────────────────────────────────┐│
│ │ 定时任务(如每小时一次) ││
│ └─────────┬──────────────────────┘│
│ │ │
│ ┌─────────▼──────────┐ │
│ │ 数据采集 │ │
│ │ - 源系统 │ │
│ │ - 目标系统 │ │
│ └─────────┬──────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ 数据对比 │ │
│ │ - 数量对比 │ │
│ │ - 内容对比 │ │
│ │ - 时间对比 │ │
│ └─────────┬──────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ 差异处理 │ │
│ │ - 告警 │ │
│ │ - 修复 │ │
│ │ - 记录 │ │
│ └────────────────────┘ │
└─────────────────────────────────────┘
数据库设计
-- 对账任务表
CREATE TABLE `reconciliation_task` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`task_name` VARCHAR(100) NOT NULL COMMENT '任务名称',
`source_type` VARCHAR(32) NOT NULL COMMENT '源系统类型',
`target_type` VARCHAR(32) NOT NULL COMMENT '目标系统类型',
`schedule_cron` VARCHAR(50) NOT NULL COMMENT '调度表达式',
`status` TINYINT NOT NULL COMMENT '状态',
`create_time` DATETIME NOT NULL,
INDEX `idx_schedule`(`schedule_cron`)
);
-- 对账记录表
CREATE TABLE `reconciliation_record` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`task_id` BIGINT NOT NULL COMMENT '任务ID',
`execute_time` DATETIME NOT NULL COMMENT '执行时间',
`source_count` BIGINT NOT NULL COMMENT '源数据数量',
`target_count` BIGINT NOT NULL COMMENT '目标数据数量',
`diff_count` BIGINT NOT NULL COMMENT '差异数量',
`status` TINYINT NOT NULL COMMENT '状态:1-一致,2-不一致',
`duration_ms` BIGINT NOT NULL COMMENT '耗时(毫秒)',
`create_time` DATETIME NOT NULL,
INDEX `idx_task`(`task_id`, `execute_time`)
);
-- 差异明细表
CREATE TABLE `reconciliation_diff` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`record_id` BIGINT NOT NULL COMMENT '对账记录ID',
`business_key` VARCHAR(128) NOT NULL COMMENT '业务主键',
`diff_type` TINYINT NOT NULL COMMENT '差异类型:1-缺失,2-不一致',
`source_data` TEXT COMMENT '源数据',
`target_data` TEXT COMMENT '目标数据',
`fixed` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已修复',
`create_time` DATETIME NOT NULL,
INDEX `idx_record`(`record_id`),
INDEX `idx_business_key`(`business_key`)
);
代码实现
/**
* 对账服务
*/
@Service
public class ReconciliationService {
@Autowired
private OrderMapper orderMapper; // 源:订单库
@Autowired
private DataWarehouseMapper dataWarehouseMapper; // 目标:数据仓库
@Autowired
private ReconciliationRecordMapper recordMapper;
@Autowired
private ReconciliationDiffMapper diffMapper;
/**
* 执行对账任务
*/
@Scheduled(cron = "0 0 * * * ?") // 每小时执行
public void reconcile() {
long startTime = System.currentTimeMillis();
// 对账时间范围:上一小时
LocalDateTime endTime = LocalDateTime.now().withMinute(0).withSecond(0);
LocalDateTime startTime2 = endTime.minusHours(1);
log.info("开始对账: {} - {}", startTime2, endTime);
// 1. 查询源数据(订单库)
List<Order> sourceOrders = orderMapper.selectByTimeRange(
startTime2, endTime
);
// 2. 查询目标数据(数据仓库)
List<OrderSnapshot> targetOrders = dataWarehouseMapper.selectByTimeRange(
startTime2, endTime
);
log.info("源数据:{}条,目标数据:{}条",
sourceOrders.size(), targetOrders.size());
// 3. 转换为Map,便于对比
Map<String, Order> sourceMap = sourceOrders.stream()
.collect(Collectors.toMap(Order::getOrderNo, o -> o));
Map<String, OrderSnapshot> targetMap = targetOrders.stream()
.collect(Collectors.toMap(OrderSnapshot::getOrderNo, o -> o));
// 4. 对比差异
List<ReconciliationDiff> diffs = new ArrayList<>();
// 4.1 检查源数据在目标中是否存在
for (Order source : sourceOrders) {
String orderNo = source.getOrderNo();
OrderSnapshot target = targetMap.get(orderNo);
if (target == null) {
// 目标缺失
ReconciliationDiff diff = new ReconciliationDiff();
diff.setBusinessKey(orderNo);
diff.setDiffType(DiffType.MISSING);
diff.setSourceData(JSON.toJSONString(source));
diff.setTargetData(null);
diffs.add(diff);
log.warn("数据缺失: {}", orderNo);
} else {
// 检查数据是否一致
if (!isConsistent(source, target)) {
ReconciliationDiff diff = new ReconciliationDiff();
diff.setBusinessKey(orderNo);
diff.setDiffType(DiffType.INCONSISTENT);
diff.setSourceData(JSON.toJSONString(source));
diff.setTargetData(JSON.toJSONString(target));
diffs.add(diff);
log.warn("数据不一致: {}", orderNo);
}
}
}
// 4.2 检查目标中多余的数据
for (OrderSnapshot target : targetOrders) {
if (!sourceMap.containsKey(target.getOrderNo())) {
ReconciliationDiff diff = new ReconciliationDiff();
diff.setBusinessKey(target.getOrderNo());
diff.setDiffType(DiffType.REDUNDANT);
diff.setSourceData(null);
diff.setTargetData(JSON.toJSONString(target));
diffs.add(diff);
log.warn("多余数据: {}", target.getOrderNo());
}
}
// 5. 保存对账记录
ReconciliationRecord record = new ReconciliationRecord();
record.setTaskId(1L);
record.setExecuteTime(LocalDateTime.now());
record.setSourceCount((long) sourceOrders.size());
record.setTargetCount((long) targetOrders.size());
record.setDiffCount((long) diffs.size());
record.setStatus(diffs.isEmpty() ? ReconciliationStatus.CONSISTENT :
ReconciliationStatus.INCONSISTENT);
record.setDurationMs(System.currentTimeMillis() - startTime);
recordMapper.insert(record);
// 6. 保存差异明细
if (!diffs.isEmpty()) {
for (ReconciliationDiff diff : diffs) {
diff.setRecordId(record.getId());
diffMapper.insert(diff);
}
// 7. 发送告警
sendAlert(record, diffs);
}
log.info("对账完成: 差异{}条,耗时{}ms", diffs.size(), record.getDurationMs());
}
/**
* 检查数据是否一致
*/
private boolean isConsistent(Order source, OrderSnapshot target) {
return Objects.equals(source.getOrderNo(), target.getOrderNo()) &&
Objects.equals(source.getUserId(), target.getUserId()) &&
Objects.equals(source.getTotalAmount(), target.getTotalAmount()) &&
Objects.equals(source.getStatus(), target.getStatus());
}
/**
* 发送告警
*/
private void sendAlert(ReconciliationRecord record, List<ReconciliationDiff> diffs) {
String message = String.format(
"对账发现差异!\n" +
"源数据:%d条\n" +
"目标数据:%d条\n" +
"差异:%d条\n" +
"详情请查看系统",
record.getSourceCount(),
record.getTargetCount(),
record.getDiffCount()
);
alertService.send("数据对账告警", message);
log.error("发送对账告警: {}", message);
}
}
差异修复
/**
* 差异修复服务
*/
@Service
public class ReconciliationFixService {
@Autowired
private ReconciliationDiffMapper diffMapper;
@Autowired
private DataWarehouseMapper dataWarehouseMapper;
@Autowired
private OrderMapper orderMapper;
/**
* 自动修复差异
*/
public void autoFix() {
// 查询未修复的差异
List<ReconciliationDiff> diffs = diffMapper.selectUnfixed();
log.info("待修复差异:{}条", diffs.size());
for (ReconciliationDiff diff : diffs) {
try {
fixDiff(diff);
// 标记为已修复
diffMapper.updateFixed(diff.getId(), true);
log.info("差异修复成功: {}", diff.getBusinessKey());
} catch (Exception e) {
log.error("差异修复失败: {}", diff.getBusinessKey(), e);
}
}
}
/**
* 修复单个差异
*/
private void fixDiff(ReconciliationDiff diff) {
String orderNo = diff.getBusinessKey();
switch (diff.getDiffType()) {
case MISSING:
// 目标缺失,从源补数据
Order source = orderMapper.selectByOrderNo(orderNo);
if (source != null) {
OrderSnapshot snapshot = convertToSnapshot(source);
dataWarehouseMapper.insert(snapshot);
log.info("补全数据: {}", orderNo);
}
break;
case INCONSISTENT:
// 数据不一致,以源为准更新目标
Order source2 = orderMapper.selectByOrderNo(orderNo);
if (source2 != null) {
OrderSnapshot snapshot2 = convertToSnapshot(source2);
dataWarehouseMapper.updateByOrderNo(snapshot2);
log.info("更新数据: {}", orderNo);
}
break;
case REDUNDANT:
// 目标多余,删除
dataWarehouseMapper.deleteByOrderNo(orderNo);
log.info("删除多余数据: {}", orderNo);
break;
}
}
}
方法2:版本号对比 ⭐⭐⭐⭐
/**
* 使用版本号验证一致性
*/
@Service
public class VersionCheckService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private OrderMapper orderMapper;
/**
* 更新订单时记录版本号
*/
@Transactional
public void updateOrder(Order order) {
// 1. 更新数据库(版本号+1)
int rows = orderMapper.updateWithVersion(order);
if (rows == 0) {
throw new BusinessException("更新失败,数据已被修改");
}
// 2. 更新缓存的版本号
String versionKey = "order:version:" + order.getOrderNo();
redisTemplate.opsForValue().set(versionKey, order.getVersion() + 1);
// 3. 发送MQ消息(携带版本号)
OrderMessage message = new OrderMessage();
message.setOrderNo(order.getOrderNo());
message.setVersion(order.getVersion() + 1);
message.setData(order);
mqProducer.send(message);
}
/**
* 检查版本一致性
*/
public boolean checkVersionConsistency(String orderNo) {
// 1. 查询数据库版本
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
return false;
}
long dbVersion = order.getVersion();
// 2. 查询缓存版本
String versionKey = "order:version:" + orderNo;
Object cacheVersionObj = redisTemplate.opsForValue().get(versionKey);
if (cacheVersionObj == null) {
return false;
}
long cacheVersion = Long.parseLong(cacheVersionObj.toString());
// 3. 对比版本号
boolean consistent = (dbVersion == cacheVersion);
if (!consistent) {
log.warn("版本不一致: orderNo={}, dbVersion={}, cacheVersion={}",
orderNo, dbVersion, cacheVersion);
}
return consistent;
}
}
方法3:事件溯源(Event Sourcing)⭐⭐⭐
/**
* 事件溯源验证
*/
@Service
public class EventSourcingCheckService {
@Autowired
private EventStoreMapper eventStoreMapper;
@Autowired
private OrderMapper orderMapper;
/**
* 重放事件验证一致性
*/
public boolean verifyConsistency(String orderNo) {
// 1. 查询当前订单状态
Order currentOrder = orderMapper.selectByOrderNo(orderNo);
// 2. 查询所有事件
List<OrderEvent> events = eventStoreMapper.selectByOrderNo(orderNo);
// 3. 重放事件,重建订单状态
Order rebuiltOrder = replayEvents(events);
// 4. 对比状态是否一致
boolean consistent = Objects.equals(currentOrder, rebuiltOrder);
if (!consistent) {
log.error("事件溯源验证不一致: orderNo={}", orderNo);
log.error("当前状态: {}", JSON.toJSONString(currentOrder));
log.error("重建状态: {}", JSON.toJSONString(rebuiltOrder));
}
return consistent;
}
/**
* 重放事件
*/
private Order replayEvents(List<OrderEvent> events) {
Order order = new Order();
for (OrderEvent event : events) {
switch (event.getEventType()) {
case "ORDER_CREATED":
// 创建订单事件
order = JSON.parseObject(event.getEventData(), Order.class);
break;
case "ORDER_PAID":
// 支付事件
order.setStatus(OrderStatus.PAID);
order.setPayTime(event.getEventTime());
break;
case "ORDER_SHIPPED":
// 发货事件
order.setStatus(OrderStatus.SHIPPED);
order.setShipTime(event.getEventTime());
break;
// ... 其他事件
}
}
return order;
}
}
📊 一致性监控
监控指标
/**
* 一致性监控服务
*/
@Service
public class ConsistencyMonitorService {
@Autowired
private MeterRegistry meterRegistry;
@Autowired
private OrderMapper orderMapper;
@Autowired
private DataWarehouseMapper dataWarehouseMapper;
/**
* 实时监控一致性延迟
*/
@Scheduled(fixedDelay = 60000) // 每分钟
public void monitorConsistencyDelay() {
// 1. 查询最新订单
Order latestOrder = orderMapper.selectLatest();
if (latestOrder == null) {
return;
}
String orderNo = latestOrder.getOrderNo();
LocalDateTime createTime = latestOrder.getCreateTime();
// 2. 查询数据仓库是否已同步
OrderSnapshot snapshot = dataWarehouseMapper.selectByOrderNo(orderNo);
if (snapshot != null) {
// 已同步,计算延迟
long delaySeconds = Duration.between(createTime,
snapshot.getSyncTime()).getSeconds();
// 记录延迟指标
Gauge.builder("consistency.delay.seconds", delaySeconds, Number::doubleValue)
.tag("type", "order")
.register(meterRegistry);
log.info("一致性延迟: {}秒", delaySeconds);
// 告警:延迟超过阈值
if (delaySeconds > 300) { // 5分钟
alertService.send("一致性延迟告警",
String.format("数据同步延迟%d秒", delaySeconds));
}
} else {
// 未同步,记录为最大延迟
long delaySeconds = Duration.between(createTime,
LocalDateTime.now()).getSeconds();
log.warn("数据未同步: orderNo={}, delay={}秒", orderNo, delaySeconds);
if (delaySeconds > 600) { // 10分钟
alertService.send("数据同步失败告警",
String.format("订单%s未同步,已延迟%d秒", orderNo, delaySeconds));
}
}
}
/**
* 监控一致性比例
*/
@Scheduled(fixedDelay = 300000) // 每5分钟
public void monitorConsistencyRate() {
// 随机抽样100个订单
List<String> sampleOrderNos = orderMapper.selectRandomSamples(100);
int consistentCount = 0;
for (String orderNo : sampleOrderNos) {
Order source = orderMapper.selectByOrderNo(orderNo);
OrderSnapshot target = dataWarehouseMapper.selectByOrderNo(orderNo);
if (source != null && target != null &&
isConsistent(source, target)) {
consistentCount++;
}
}
double consistencyRate = (double) consistentCount / sampleOrderNos.size() * 100;
// 记录一致性比例
Gauge.builder("consistency.rate", consistencyRate, Number::doubleValue)
.tag("type", "order")
.register(meterRegistry);
log.info("一致性比例: {}%", String.format("%.2f", consistencyRate));
// 告警:一致性低于阈值
if (consistencyRate < 95.0) {
alertService.send("一致性比例告警",
String.format("一致性比例仅%.2f%%", consistencyRate));
}
}
}
Grafana监控大盘
一致性监控大盘:
┌─────────────────────────────────────┐
│ 一致性延迟 │
│ ┌───────────────────────────┐ │
│ │ 5秒 │ │
│ │ /\ /\ │ │
│ │ / \/ \ │ │
│ └───────────────────────────┘ │
│ 告警阈值:300秒 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 一致性比例 │
│ ┌───────────────────────────┐ │
│ │ 99.8% │ │
│ │ ████████████ │ │
│ └───────────────────────────┘ │
│ 告警阈值:95% │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 对账任务执行情况 │
│ ┌───────────────────────────┐ │
│ │ 今日:24次 │ │
│ │ 成功:23次 ✅ │ │
│ │ 失败:1次 ❌ │ │
│ └───────────────────────────┘ │
└─────────────────────────────────────┘
🎉 总结
核心方法
1. 对账系统 ⭐⭐⭐⭐⭐
- 定时对比
- 发现差异
- 自动修复
- 推荐方案
2. 版本号对比 ⭐⭐⭐⭐
- 轻量级
- 实时性好
- 辅助手段
3. 事件溯源 ⭐⭐⭐
- 完整追溯
- 可重放
- 实现复杂
4. 实时监控 ⭐⭐⭐⭐⭐
- 延迟指标
- 一致性比例
- 及时告警
最佳实践
1. 多层次验证
├── 实时监控(分钟级)
├── 定时对账(小时级)
└── 全量对账(天级)
2. 自动化处理
├── 自动发现
├── 自动告警
└── 自动修复
3. 可视化大盘
├── 一致性延迟
├── 一致性比例
└── 差异趋势
4. 应急预案
├── 差异处理流程
├── 人工介入机制
└── 数据修复工具
记忆口诀
最终一致性,
最终要验证!
对账系统是王道,
定时执行要可靠。
源数据和目标数据,
逐条对比找差异。
版本号辅助,
实时性更好。
每次更新记版本,
不一致立即知道。
实时监控不可少,
延迟指标要关注。
一致性比例要统计,
低于阈值要告警。
自动修复能力强,
差异发现自动补。
人工介入作后备,
复杂问题人来解。
可视化大盘好,
一目了然心不慌。
问题趋势早发现,
防患未然最重要!
愿你的系统最终一致,差异无所遁形! 🔍✨