谈一谈在分布式系统中,如何保证数据一致性?

一、基于MQ的分布式事务模式

1. 本地消息表(Local Transaction Table)

适用场景: 需要强最终一致性的场景(如订单创建后发送通知)

实现步骤:

  1. 业务与消息原子化写入

    • 业务操作和消息日志(本地消息表)在同一个数据库事务中完成。
    • 消息表字段 : msg_id, status(待发送/已发送), topic, payload, retry_count, create_time
    sql 复制代码
    -- 示例表结构
    CREATE TABLE local_message (
        msg_id VARCHAR(64) PRIMARY KEY,
        status TINYINT NOT NULL COMMENT '0-待发送,1-已发送',
        topic VARCHAR(255) NOT NULL,
        payload TEXT NOT NULL,
        retry_count INT DEFAULT 0,
        create_time DATETIME DEFAULT CURRENT_TIMESTAMP
    );
  2. 后台任务异步发送消息

    • 定时扫描本地消息表中 status=0 的记录,发送到MQ。
    • 发送成功后更新状态为 1,失败则重试(需设置最大重试次数)。
  3. 消费者幂等处理

    • 消费者需保证多次处理同一消息的结果一致(通过唯一ID去重)。

优点 : 强一致性,实现简单
缺点: 需要维护本地消息表,数据库压力较大


2. MQ事务消息(如RocketMQ)

适用场景: 需要简化本地消息表维护的场景

实现步骤:

  1. 生产者发送半消息(Half Message)

    • 发送消息到MQ,但消息对消费者不可见(处于PREPARED状态)。
  2. 执行本地事务

    • 执行业务逻辑(如扣减库存),记录事务结果。
  3. 提交/回滚消息

    • 本地事务成功:向MQ发送commit,消息变为COMMITTED,消费者可见。
    • 本地事务失败:向MQ发送rollback,消息被删除。
  4. 事务状态回查(Checkback)

    • 若生产者未响应commit/rollback,MQ会回调生产者的接口确认事务状态。

RocketMQ事务消息代码示例:

java 复制代码
// 生产者代码
TransactionMQProducer producer = new TransactionMQProducer("group");
producer.setTransactionListener(new TransactionListener() {
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            // 执行本地事务(如更新数据库)
            boolean success = orderService.createOrder(arg);
            return success ? LocalTransactionState.COMMIT_MESSAGE : 
                            LocalTransactionState.ROLLBACK_MESSAGE;
        } catch (Exception e) {
            return LocalTransactionState.UNKNOW;
        }
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        // 回查本地事务状态
        return orderService.isOrderExists(msg.getKeys()) ? 
               LocalTransactionState.COMMIT_MESSAGE : 
               LocalTransactionState.ROLLBACK_MESSAGE;
    }
});
producer.sendMessageInTransaction(msg, null);

优点 : 无侵入,不需要本地消息表
缺点: 依赖MQ的事务消息功能(仅部分MQ支持)


3. 最大努力通知(Best-Effort Delivery)

适用场景: 允许最终一致且对时效性要求不高的场景(如退款结果通知)

实现步骤:

  1. 业务操作与消息解耦

    • 先完成本地事务,再异步发送消息(允许消息丢失)。
  2. 消息重试机制

    • 设置阶梯式重试间隔(如1s, 5s, 10s),达到最大次数后告警人工处理。
  3. 对账补偿

    • 定期扫描业务数据,对未通知成功的记录补发消息。

优点 : 实现简单,系统压力小
缺点: 一致性弱,需配合对账系统


二、关键问题与解决方案

1. 消息丢失问题

  • 生产者端
    • 开启MQ的confirm模式(如RabbitMQ)或事务消息(如RocketMQ)。
  • 消费者端
    • 手动ACK机制,处理成功后再确认消息。

2. 消息重复消费

  • 幂等性设计
    • 业务层通过唯一ID(如订单号)判断是否已处理。
    • 数据库唯一索引或Redis SetNX去重。

3. 消息顺序性

  • 单一队列分区:同一业务ID的消息发送到同一个队列(如Kafka的Key Partitioning)。
  • 消费者单线程处理同一分区的消息。

三、方案对比与选型

方案 一致性强度 实现复杂度 适用场景
本地消息表 强最终一致 需要高可靠性的核心业务
MQ事务消息 强最终一致 RocketMQ用户,希望简化实现
最大努力通知 弱最终一致 非核心业务(如通知类)

四、最佳实践

  1. 消息表设计优化

    • 分库分表:按业务ID哈希分片,避免单表过大。
    • 索引优化:对statuscreate_time建立联合索引。
  2. 监控与告警

    • 监控MQ堆积量、消息延迟。
    • 本地消息表中长期未处理的消息触发告警。
  3. 事务消息超时控制

    • 设置事务超时时间(如RocketMQ的transactionTimeout),避免事务悬挂。

五、总结

实现分布式事务的核心是 将分布式事务拆分为本地事务 + 可靠消息传递。选择方案时需根据业务对一致性的要求权衡复杂度:

  • 强一致性要求高 → 本地消息表或MQ事务消息
  • 允许延迟最终一致 → 最大努力通知 + 对账补偿

结合幂等性设计和完善的重试机制,可构建高可靠的分布式事务系统。

相关推荐
Python私教13 分钟前
Java手写链表全攻略:从单链表到双向链表的底层实现艺术
java·python·链表
吴生439622 分钟前
数据库ALGORITHM = INSTANT 特性研究过程
后端
小麟有点小靈26 分钟前
VSCode写java时常用的快捷键
java·vscode·编辑器
小黑屋的黑小子35 分钟前
【数据结构】反射、枚举以及lambda表达式
数据结构·面试·枚举·lambda表达式·反射机制
程序猿chen37 分钟前
JVM考古现场(十九):量子封神·用鸿蒙编译器重铸天道法则
java·jvm·git·后端·程序人生·java-ee·restful
JiangJiang1 小时前
🚀 Vue人看React useRef:它不只是替代 ref
javascript·react.js·面试
Chandler241 小时前
Go:接口
开发语言·后端·golang
&白帝&1 小时前
java HttpServletRequest 和 HttpServletResponse
java·开发语言
ErizJ1 小时前
Golang|Channel 相关用法理解
开发语言·后端·golang
automan021 小时前
golang 在windows 系统的交叉编译
开发语言·后端·golang