Skip to content

数据一致性设计

数据一致性是分布式系统的核心问题。它不是一句“保持一致”就能解决,而是要先明确:哪些数据必须立刻一致,哪些数据可以短暂不一致,哪些数据失败后可以补偿,哪些数据必须人工确认。

本页侧重业务状态机、补偿、对账和最终一致闭环。要深入理解多副本的一次读写、Quorum、版本时钟和冲突收敛,请继续阅读:

为什么会不一致

单库事务里,订单和订单明细可以一起提交:

mermaid
flowchart TD
    A["开启本地事务"] --> B["写订单表"]
    B --> C["写订单明细表"]
    C --> D["提交事务"]

分布式场景里,订单、库存、支付可能在不同服务:

mermaid
flowchart TD
    A["订单服务创建订单"] --> B["订单库提交"]
    B --> C["调用库存服务扣库存"]
    C --> D{"库存服务失败或超时"}
    D -- "失败" --> E["订单已创建但库存未扣"]
    D -- "成功" --> F["继续支付流程"]

只要有跨服务、跨数据库、跨消息系统,就可能出现部分成功、部分失败。

一致性不是只有一种

一致性要求含义商业例子
强一致操作完成后所有地方立即一致账户余额、支付记账
最终一致允许短暂不一致,最终修复ES 索引、积分入账、消息通知
读己之写用户自己写完后自己能立刻看到修改头像、修改昵称
单调读用户读到新状态后不再读到旧状态订单已支付后不再显示待支付
因果一致有因果关系的操作按顺序可见先发评论,再显示评论回复

一致性设计的第一步是分类,而不是直接上分布式事务框架。

业务分级

数据推荐一致性说明
余额、账务流水强一致错了就是资金事故
库存最终扣减强一致或强约束不能超卖
订单状态核心状态强约束,外围最终一致支付、关闭、发货要有状态机
积分、优惠券到账最终一致可以延迟,但不能丢
短信、站内信尽力而为失败可重试或人工处理
搜索索引最终一致MySQL 是事实来源,ES 可重建
报表统计最终一致可重跑、可修正

一致性工具箱

mermaid
flowchart TD
    A["一致性问题"] --> B["本地事务"]
    A --> C["唯一索引和状态机"]
    A --> D["分布式事务"]
    A --> E["可靠消息"]
    A --> F["补偿任务"]
    A --> G["对账"]
    D --> D1["XA、TCC、Saga、Seata"]
    E --> E1["本地消息表、事务消息、Outbox"]

实际项目里很少只靠一种方案。比如支付成功改订单,可能同时用到:

  1. 支付流水唯一索引防重复。
  2. 订单状态机防状态倒退。
  3. MQ 通知订单服务。
  4. 消费失败重试。
  5. 定时对账补偿。

状态机是核心

订单状态流转:

mermaid
flowchart TD
    A["WAIT_PAY"] --> B["PAID"]
    A --> C["CLOSED"]
    B --> D["SHIPPED"]
    D --> E["FINISHED"]
    B --> F["REFUNDING"]
    F --> G["REFUNDED"]

状态机要定义:

  1. 哪些状态可以流向哪些状态。
  2. 哪些操作允许重复。
  3. 哪些操作必须拒绝。
  4. 哪些状态是终态。

支付回调更新订单:

sql
update t_order
set status = 'PAID',
    paid_at = now()
where order_no = ?
  and status = 'WAIT_PAY';

如果回调重复,第二次更新行数为 0,不会造成重复支付。

补偿机制

补偿不是简单“失败了再调用一次”。补偿要回答:

  1. 补偿哪条业务数据。
  2. 为什么失败。
  3. 重试几次。
  4. 下次什么时候重试。
  5. 最终失败谁处理。

补偿表设计:

sql
create table t_compensation_task (
    id bigint primary key,
    biz_type varchar(64) not null,
    biz_key varchar(128) not null,
    payload text not null,
    status varchar(32) not null,
    retry_count int not null default 0,
    next_retry_time datetime not null,
    last_error varchar(1024),
    created_at datetime not null,
    updated_at datetime not null,
    unique key uk_biz_type_key (biz_type, biz_key)
);

补偿任务执行逻辑:

java
public void executeCompensationTask(CompensationTask task) {
    try {
        if ("ORDER_PAID_NOTIFY".equals(task.bizType())) {
            orderService.handlePaymentSuccess(task.bizKey());
        }
        task.markSuccess();
    } catch (Exception ex) {
        task.markRetry(ex.getMessage());
    }
}

对账机制

补偿解决已知失败,对账发现未知失败。

支付对账示例:

mermaid
flowchart TD
    A["下载支付渠道账单"] --> B["读取本地支付流水"]
    B --> C["按交易号匹配"]
    C --> D{"金额和状态一致?"}
    D -- "是" --> E["标记对账成功"]
    D -- "否" --> F["写入差异表"]
    F --> G["人工确认或自动修复"]

差异表:

sql
create table t_reconcile_diff (
    id bigint primary key,
    channel varchar(32) not null,
    trade_no varchar(128) not null,
    local_status varchar(32),
    channel_status varchar(32),
    local_amount decimal(18, 2),
    channel_amount decimal(18, 2),
    diff_type varchar(64) not null,
    status varchar(32) not null,
    created_at datetime not null
);

资金相关系统必须有对账。不要以为接口返回成功就永远正确。

最终一致标准流程

mermaid
flowchart TD
    A["本地事务写业务数据"] --> B["写消息表或发送事务消息"]
    B --> C["提交本地事务"]
    C --> D["消息投递到 MQ"]
    D --> E["消费者幂等处理"]
    E --> F{"处理成功?"}
    F -- "成功" --> G["标记完成"]
    F -- "失败" --> H["重试、死信、补偿"]
    H --> I["告警和人工处理"]

这个流程的关键点:

  1. 业务数据和消息不能出现“业务成功但消息丢失”。
  2. 消费者必须幂等。
  3. 失败必须可见。
  4. 不能无限重试,要有最终处理出口。

常见一致性问题

问题原因解决方向
订单支付了还显示待支付支付回调丢失或 MQ 消费失败支付对账、回调重试、消息补偿
商品改价后搜索页还是旧价ES 同步失败MQ 重试、同步记录、定时补偿
重复发券消息重复消费唯一索引、消费日志、幂等表
库存超卖并发扣减无约束条件更新、库存冻结、TCC
缓存旧值覆盖新值并发更新缓存删除缓存、延迟删除、版本号

代码 Demo:消费幂等

java
@Transactional
public void handleOrderPaid(OrderPaidEvent event) {
    if (consumeLogRepository.exists(event.messageId())) {
        return;
    }

    int updated = orderRepository.markPaid(event.orderNo(), event.paidAt());
    if (updated > 0) {
        pointsService.addPayPoints(event.userId(), event.orderNo());
    }

    consumeLogRepository.save(new ConsumeLog(event.messageId(), "ORDER_PAID"));
}

这里有两层幂等:

  1. consumeLog 防止同一消息重复消费。
  2. markPaid 用状态条件防止订单重复从待支付变成已支付。

本章小结

数据一致性设计的核心不是“用不用分布式事务”,而是先判断业务风险,再选择一致性级别和工具。

强一致适合资金、库存最终确认等核心链路;最终一致适合通知、索引、报表、积分等可补偿场景。不管选择哪种方案,都必须配套幂等、重试、补偿、对账和告警。

继续深入:多副本一致性原理 · 独立面试题