本文 的 原文 地址
原始的内容,请参考 本文 的 原文 地址
45岁老架构 尼恩说在前面
在45岁老架构师 尼恩的读者交流群(100+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格,遇到很多很重要的相关面试题:
- 10Wqps+高并发,如何实现分布式事务架构?
- 你们项目的分布式事务,是如何架构的?
**最近有小伙伴面试美团、阿里,都问到了这个面试题。**小伙伴没有系统的去梳理和总结,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V175版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取
CP (强一致)和AP(高并发)的 根本冲突
从上面的指标数据可以知道, Seata AT/TCC是 强一致,并发能力弱。
CP (强一致)和AP(高并发)是一对 根本矛盾,存在根本冲突。
10Wqps 的高并发事务,并不是CP,而是属于AP 高并发。Seata 如果不做特殊改造, 很难满足。
具体请参见尼恩 cap 文章:
字节面试:聊聊 CAP 定理?哪些中间件是AP? 哪些是CP? 说说 为什么?
CAP 定理
CAP 该定理指出一个 分布式系统 最多只能同时满足一致性(Consistency)、**可用性(Availability)和分区容错性(Partition tolerance)**这三项中的两项。

CAP定理的三个要素可以用来描述分布式系统的一致性和可用性。
具体请参见尼恩 cap 文章:
字节面试:聊聊 CAP 定理?哪些中间件是AP? 哪些是CP? 说说 为什么?
如果事务要追求高并发,根据cap定理,需要放弃强一致性,只需要保证数据的最终一致性。
所以,在实践可以使用本地消息表的方案来解决分布式事务问题。
经典ebay 本地消息表方案
本地消息表方案最初是ebay提出的,其实也是BASE理论的应用,属于可靠消息最终一致性的范畴。
消息生产方/ 消息消费方,需要额外建一个消息表,并记录消息发送状态。
一个简单的本地消息表, 设计如下
| 字段 | 类型 | 注释 |
|---|---|---|
| id | long | id |
| msg_type | varchar | 消息类型 |
| biz_id | varchar | 业务唯一标志 |
| content | text | 消息体 |
| state | varchar | 状态(待发送,已消费) |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。
然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方 需要处理这个消息,并完成自己的业务逻辑。
此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。
如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
经典ebay 本地消息表步骤
生产方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。
发送消息方:
- 需要有一个消息表,记录着消息状态相关信息。
- 业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数据和事务消息直接写入数据库。
- 在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。使用专门的投递工作线程进行事务消息投递到MQ,根据投递ACK去删除事务消息表记录
- 消息会发到消息消费方,如果发送失败,即进行重试。
消息消费方:
- 处理消息队列中的消息,完成自己的业务逻辑。
- 如果本地事务处理成功,则表明已经处理成功了。
- 如果本地事务处理失败,那么就会重试执行。
- 如果是业务层面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。

经典ebay本地消息表方案中,还设计了靠谱的自动对账补账逻辑,确保数据的最终一致性。
经典ebay本地消息表 的注意事项
使用本地消息表实现分布式事务可以确保消息在分布式环境中的可靠传递和一致性。
然而,需要注意以下几点:
- 消息的幂等性: 消费者一定需要保证接口的幂等性,消息的幂等性非常重要,以防止消息重复处理导致的数据不一致。
- 本地消息表的设计: 本地消息表的设计需要考虑到消息状态、重试次数、创建时间等字段,以便实现消息的跟踪和管理。
- 定时任务和重试机制: 需要实现定时任务或者重试机制来确保消息的可靠发送和处理。
经典ebay本地消息表 访问的 优点和缺点:
优点:
- 本地消息表建设成本比较低,实现了可靠消息的传递确保了分布式事务的最终一致性。
- 无需提供回查方法,进一步减少的业务的侵入。
- 在某些场景下,还可以进一步利用注解等形式进行解耦,有可能实现无业务代码侵入式的实现。
缺点:
-
本地消息表与业务耦合在一起,难于做成通用性,不可独立伸缩。
-
本地消息表是基于数据库来做的,而数据库是要读写磁盘IO的,因此在高并发下是有性能瓶颈的
-
数据大时,消息积压问题,扫表效率慢
-
数据大时,事务表数据爆炸,定时扫表存在延迟问题
使用 定时任务(如 XXL-Job )实现分布式事务最终一致性方案
通过 XXL-Job 定时任务替代延迟消息,定期查询 “待对账” 业务数据,对比 Service A 与 Service B 的业务状态,通过 “主动核查 + 差异修复” 确保最终一致性。
核心是 “本地事务保初始一致 + 定时任务查状态差异 + 人工 / 自动补单修偏差”。
1. Service A(发起方):本地消息表设计与事务保障
Service A 在执行核心业务(如创建订单)时,需在本地数据库事务中同时完成两件事:
-
执行核心业务逻辑(如插入订单表,状态标记为 “已创建”);
-
插入 “本地消息对账表”(字段含:对账 ID、业务 ID(如订单 ID)、业务类型(如 “订单扣库存”)、Service A 状态(如 “订单已创建”)、Service B 状态(初始为 “未确认”)、对账状态(初始为 “待对账”)、创建时间、最后对账时间),确保 “业务成功则对账记录必存在”,避免初始数据缺失。
2. Service B(依赖方):业务状态可查与结果反馈
Service B 执行依赖业务(如扣减库存)时,需:
- 执行核心业务逻辑(如扣减库存表,标记 “已扣减”,并关联 Service A 的业务 ID(订单 ID));
- 提供状态查询接口(如 “根据订单 ID 查询库存扣减状态”),返回 “已成功”“已失败”“处理中” 三种明确状态,方便定时任务核查;
- 若 Service B 执行成功 / 失败,可主动调用 Service A 的 “状态回调接口” 更新本地消息对账表的 “Service B 状态”(非强制,定时任务会兜底核查)。
3、XXL-Job 定时任务设计:对账与修复
定时任务执行时,按以下步骤完成对账:
1、筛选待对账数据:查询 Service A 本地消息对账表中 “对账状态 = 待对账” 且 “创建时间超过 5 分钟”(避免业务未执行完就对账)的记录,按分片范围批量获取(如每次查 1000 条,避免一次性查太多导致 OOM);
2、 双端状态查询:对每条待对账记录,分别调用 Service A 的 “业务状态接口”(确认订单是否真的已创建)、Service B 的 “状态查询接口”(确认库存是否已扣减);
3、 状态一致性判断与处理:
- 若 “Service A 成功 + Service B 成功”:更新本地消息对账表 “对账状态 = 已一致”“Service B 状态 = 已成功”,完成对账;
- 若 “Service A 失败 + Service B 失败”:更新 “对账状态 = 已一致”“Service B 状态 = 已失败”,无需额外处理;
- 若 “Service A 成功 + Service B 失败 / 处理中”:触发自动重试(调用 Service B 的 “重试执行接口”,如重新扣减库存),重试 3 次仍失败则标记 “对账状态 = 不一致”,生成业务工单;
- 若 “Service A 失败 + Service B 成功”:属于异常数据(Service A 业务失败但 Service B 执行成功),直接标记 “对账状态 = 不一致”,生成业务工单;
4、 异常兜底:若调用 Service A/Service B 接口超时,标记该记录 “对账状态 = 待重试”,下次定时任务重新核查,避免因临时网络问题误判不一致。
RocketMQ 事务消息 + 本地消息表 + XXL-Job 对账 分布式式事务方案实操
本方案将规整为标准 Markdown 格式,同时补充 XXL-Job 定时任务事务对账机制,通过 “事务消息保初始一致性 + 定时对账兜底差异” 的双层保障,确保电商下单(生成订单→扣减库存→发送通知)场景的分布式事务最终一致。
1. 系统架构
以电商下单场景为核心,涉及 3 个服务与 1 个中间件,职责分工明确:
- 订单服务(发起方):核心服务,负责生成订单、记录本地消息表、发送 RocketMQ 事务消息,同时提供订单状态查询接口。
- 库存服务(依赖方 1):订阅订单消息,执行库存扣减,提供库存扣减状态查询接口,支持重试执行。
- 通知服务(依赖方 2):订阅订单消息,发送短信 / APP 通知,提供通知发送状态查询接口。
- RocketMQ:作为事务协调中间件,接收订单服务的事务消息,确认本地事务成功后投递消息至下游服务。
- XXL-Job:定时任务调度中心,部署对账任务,定期核查订单服务与下游服务的业务状态差异,兜底修复不一致数据。
2. 数据库表设计
订单服务的 本地消息表(message_log)包括对账相关字段(如对账状态、重试次数、下次对账时间),支撑定时对账逻辑;
message_log 同时 包含 核心业务字段,确保消息与订单的关联可追溯。
-- 订单服务:本地消息表(含对账字段)
CREATE TABLE message_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL COMMENT '订单ID(关联t_order表,唯一)',
rocketmq_msg_id VARCHAR(64) DEFAULT NULL COMMENT 'RocketMQ消息唯一ID(关联消息中间件)',
message_content TEXT NOT NULL COMMENT '消息内容(JSON格式,含orderId、skuId、quantity等)',
business_type VARCHAR(32) NOT NULL COMMENT '业务类型:ORDER_CREATE(创建订单)、INVENTORY_DEDUCT(扣库存)、NOTICE_SEND(发通知)',
msg_status ENUM('INIT','SENT','CONSUMED','FAIL') DEFAULT 'INIT' COMMENT '消息状态:INIT=初始,SENT=已投递,CONSUMED=已消费,FAIL=失败',
reconcile_status ENUM('PENDING','SUCCESS','FAIL','RETRY') DEFAULT 'PENDING' COMMENT '对账状态:PENDING=待对账,SUCCESS=对账一致,FAIL=对账不一致,RETRY=待重试',
retry_count TINYINT DEFAULT 0 COMMENT '对账重试次数(最大5次)',
next_reconcile_time DATETIME NOT NULL COMMENT '下次对账时间(定时任务筛选依据)',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '消息创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '状态更新时间',
UNIQUE KEY uk_order_id_business_type (order_id, business_type) COMMENT '避免同一订单同一业务类型重复发消息'
);
-- 订单服务:订单表(简化,仅保留核心字段)
CREATE TABLE t_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id VARCHAR(64) NOT NULL COMMENT '订单唯一编号',
user_id BIGINT NOT NULL COMMENT '用户ID',
sku_id BIGINT NOT NULL COMMENT '商品SKU ID',
quantity INT NOT NULL COMMENT '购买数量',
order_status ENUM('CREATED','PAID','SHIPPED','FINISHED','CANCELED') DEFAULT 'CREATED' COMMENT '订单状态',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '订单创建时间',
UNIQUE KEY uk_order_id (order_id)
);
-- 库存服务:库存扣减记录表(支撑状态查询与幂等)
CREATE TABLE inventory_deduct_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id VARCHAR(64) NOT NULL COMMENT '订单ID(关联订单服务)',
sku_id BIGINT NOT NULL COMMENT '商品SKU ID',
deduct_quantity INT NOT NULL COMMENT '扣减数量',
deduct_status ENUM('SUCCESS','FAIL','PROCESSING') DEFAULT 'PROCESSING' COMMENT '扣减状态',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_order_id (order_id) COMMENT '按订单ID幂等,避免重复扣减'
);
3. 代码实现
3.1 Producer 端(订单服务):事务消息发送
通过 TransactionMQProducer 发送事务消息,确保 “生成订单” 与 “记录本地消息” 在同一本地事务中,保证初始数据一致性;
同时初始化消息的对账状态(PENDING)与下次对账时间(默认 5 分钟后,避免业务未执行完就对账)。
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Service
public class OrderServiceImpl implements OrderService {
// 注入RocketMQ事务生产者(单例,由Spring容器初始化)
@Resource
private TransactionMQProducer transactionMQProducer;
@Resource
private OrderMapper orderMapper;
@Resource
private MessageLogMapper messageLogMapper;
/**
* 创建订单 + 发送事务消息
*/
@Override
public void createOrder(OrderCreateDTO dto) throws Exception {
// 1. 构造订单数据(生成唯一订单号)
String orderId = generateOrderId();
TOrder order = TOrder.builder()
.orderId(orderId)
.userId(dto.getUserId())
.skuId(dto.getSkuId())
.quantity(dto.getQuantity())
.orderStatus("CREATED")
.build();
// 2. 构造RocketMQ事务消息(主题:OrderTopic,标签:INVENTORY_DEDUCT+NOTICE_SEND,支持多下游订阅)
String msgContent = JSON.toJSONString(dto);
Message message = new Message(
"OrderTopic", // 主题:下游服务订阅此主题
"INVENTORY_DEDUCT||NOTICE_SEND", // 标签:区分业务类型,下游可按标签过滤
orderId.getBytes(StandardCharsets.UTF_8), // 消息Key:订单ID,便于定位
msgContent.getBytes(StandardCharsets.UTF_8)
);
// 3. 发送事务消息(将订单数据作为参数透传给事务监听器)
transactionMQProducer.sendMessageInTransaction(message, order);
}
/**
* 事务监听器:执行本地事务 + 事务回查
*/
@Resource
private TransactionListener orderTransactionListener;
// 初始化事务生产者时绑定监听器(Spring Bean初始化方法)
@PostConstruct
public void initProducer() {
transactionMQProducer.setTransactionListener(orderTransactionListener);
}
/**
* 本地事务逻辑(由监听器调用,确保订单与消息表同成功/同失败)
*/
@Transactional(rollbackFor = Exception.class)
public LocalTransactionState executeLocalTransaction(TOrder order, Message message) {
try {
// 步骤1:保存订单到订单表
orderMapper.insert(order);
// 步骤2:记录本地消息表(对账状态初始为PENDING,下次对账时间5分钟后)
Date nextReconcileTime = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5));
MessageLog log = MessageLog.builder()
.orderId(order.getOrderId())
.rocketmqMsgId(message.getMsgId())
.messageContent(new String(message.getBody()))
.businessType("ORDER_CREATE")
.msgStatus("INIT")
.reconcileStatus("PENDING")
.retryCount(0)
.nextReconcileTime(nextReconcileTime)
.build();
messageLogMapper.insert(log);
// 步骤3:提交本地事务,返回COMMIT(通知RocketMQ投递消息)
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
// 本地事务失败,回滚,返回ROLLBACK(通知RocketMQ丢弃消息)
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
/**
* 事务回查逻辑(Broker未收到Commit/Rollback时触发)
*/
public LocalTransactionState checkLocalTransaction(String orderId) {
// 查本地消息表,按订单ID判断本地事务状态
MessageLog log = messageLogMapper.selectByOrderId(orderId);
if (log == null) {
return LocalTransactionState.ROLLBACK_MESSAGE; // 本地无记录,回滚
}
// 本地消息已记录,说明本地事务成功,返回COMMIT
if ("INIT".equals(log.getMsgStatus()) || "PENDING".equals(log.getReconcileStatus())) {
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.ROLLBACK_MESSAGE;
}
// 工具方法:生成唯一订单号
private String generateOrderId() {
return "ORDER_" + System.currentTimeMillis() + RandomUtils.nextInt(1000, 9999);
}
}
3.2 Consumer 端(库存服务):消息消费与幂等控制
下游服务消费消息时,需通过 “订单 ID” 实现幂等(避免重复扣库存),同时记录消费状态,为后续对账提供查询依据;消费失败时返回RECONSUME_LATER,触发 RocketMQ 重试,重试耗尽后进入死信队列。
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Component
// 订阅订单主题,仅消费“扣库存”标签的消息
@RocketMQMessageListener(topic = "OrderTopic", selectorExpression = "INVENTORY_DEDUCT", consumerGroup = "inventory_consumer_group")
public class InventoryConsumer implements RocketMQListener<MessageExt> {
@Resource
private InventoryMapper inventoryMapper;
@Resource
private InventoryDeductLogMapper deductLogMapper;
@Resource
private MessageLogFeignClient messageLogFeignClient; // 调用订单服务的消息表接口
@Override
@Transactional(rollbackFor = Exception.class)
public void onMessage(MessageExt messageExt) {
// 1. 解析消息(获取订单ID、商品ID、扣减数量)
String msgContent = new String(messageExt.getBody());
OrderCreateDTO dto = JSON.parseObject(msgContent, OrderCreateDTO.class);
String orderId = dto.getOrderId();
Long skuId = dto.getSkuId();
Integer quantity = dto.getQuantity();
// 2. 幂等控制:查询是否已扣减(按订单ID)
InventoryDeductLog existLog = deductLogMapper.selectByOrderId(orderId);
if (existLog != null && "SUCCESS".equals(existLog.getDeductStatus())) {
// 已成功扣减,直接返回成功
messageLogFeignClient.updateMsgStatus(orderId, "CONSUMED"); // 通知订单服务更新消息状态
return;
}
try {
// 3. 执行库存扣减(先查库存是否充足)
Inventory inventory = inventoryMapper.selectBySkuId(skuId);
if (inventory == null || inventory.getStock() < quantity) {
// 库存不足,记录失败状态,返回失败(不重试,避免无效循环)
deductLogMapper.insert(InventoryDeductLog.builder()
.orderId(orderId)
.skuId(skuId)
.deductQuantity(quantity)
.deductStatus("FAIL")
.build());
messageLogFeignClient.updateMsgStatus(orderId, "FAIL"); // 通知订单服务更新消息状态
throw new RuntimeException("库存不足,扣减失败");
}
// 4. 扣减库存并记录日志
inventory.setStock(inventory.getStock() - quantity);
inventoryMapper.updateById(inventory);
deductLogMapper.insert(InventoryDeductLog.builder()
.orderId(orderId)
.skuId(skuId)
.deductQuantity(quantity)
.deductStatus("SUCCESS")
.build());
// 5. 通知订单服务更新消息状态为“已消费”
messageLogFeignClient.updateMsgStatus(orderId, "CONSUMED");
// 返回消费成功
} catch (Exception e) {
// 消费失败,记录“处理中”状态,返回重试
deductLogMapper.insertOrUpdate(InventoryDeductLog.builder()
.orderId(orderId)
.skuId(skuId)
.deductQuantity(quantity)
.deductStatus("PROCESSING")
.build());
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
/**
* 对外提供库存扣减状态查询接口(供XXL-Job对账调用)
*/
public String queryDeductStatus(String orderId) {
InventoryDeductLog log = deductLogMapper.selectByOrderId(orderId);
if (log == null) {
return "NOT_PROCESSED"; // 未处理
}
return log.getDeductStatus(); // SUCCESS/FAIL/PROCESSING
}
/**
* 对外提供库存扣减重试接口(供XXL-Job对账修复调用)
*/
public boolean retryDeduct(String orderId) {
// 逻辑同onMessage,仅针对“PROCESSING/FAIL”状态的记录重试,此处省略
return true;
}
}
4. 基础流程说明
4.1 正常流程
1、 用户发起下单请求,订单服务调用 createOrder 方法,发送事务消息。
2、 RocketMQ 收到 “半消息” 后,触发订单服务的本地事务(executeLocalTransaction):
- 本地事务成功:保存订单、记录本地消息表(
msg_status=INIT,reconcile_status=PENDING),返回COMMIT_MESSAGE。 - RocketMQ 确认后,将消息投递至库存服务、通知服务。
3、 库存服务 / 通知服务消费消息,执行业务逻辑(扣库存 / 发通知),消费成功后通知订单服务更新消息状态为 CONSUMED。
4、 5 分钟后,XXL-Job 对账任务触发,核查订单服务与下游服务状态一致,更新对账状态为 SUCCESS,流程闭环。
4.2 基础异常流程(无对账时)
- 本地事务失败:订单 / 消息表插入失败,返回
ROLLBACK_MESSAGE,RocketMQ 不投递消息,无下游影响。 - Broker 超时未收状态:RocketMQ 触发事务回查(
checkLocalTransaction),按本地消息表状态返回COMMIT_MESSAGE,重新投递消息。 - Consumer 消费失败:返回
RECONSUME_LATER,RocketMQ 按重试策略重试(默认 16 次),重试耗尽后进入死信队列。
5. XXL-Job 定时事务对账机制
…由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址
344

被折叠的 条评论
为什么被折叠?



