1. 分布式事务的异步处理方案解析
在微服务架构中,事务处理一直是个棘手的问题。传统的同步调用方式在跨服务操作时,要么面临性能瓶颈,要么难以保证数据一致性。最近我在一个电商秒杀项目中,就遇到了这样的挑战:订单服务和用户服务需要协同完成扣减库存、扣减余额、生成订单三个操作,但同步调用的方式在高并发下完全撑不住。
经过多次尝试,最终我们采用了"本地消息表+可靠消息队列"的异步方案。这个方案的核心思路是:将服务间的同步调用改为异步消息交互,通过本地消息表记录事务状态,利用消息队列的可靠性保证消息最终被消费,从而实现跨服务的最终一致性。
2. 方案设计与核心组件
2.1 整体架构设计
这个方案主要由三个核心组件构成:
- 本地消息表:记录所有待处理的事务消息及其状态
- 消息队列(RabbitMQ):作为可靠的消息传输通道
- 定时任务+多线程:负责扫描和发送待处理消息
工作流程可以概括为:
- 主服务(如订单服务)在本地事务中记录业务操作和消息
- 定时任务扫描待发送消息,通过线程池异步发送到MQ
- 从服务(如用户服务)消费消息并处理业务
- 从服务返回处理结果,主服务更新消息状态
2.2 本地消息表设计
本地消息表是这个方案的核心,它需要记录足够的信息来追踪整个事务的生命周期。在我们的实现中,Local_Message表包含以下关键字段:
java复制@Entity
@Table(name="local_message")
@Data
public class Local_Message {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name="message_id", nullable=false, unique=true)
private String messageId; // 消息唯一ID
@Column(name="business_key", nullable=false)
private String businessKey; // 业务键(如订单号)
@Column(name="business_type", nullable=false)
private String businessType; // 业务类型
@Column(name="content", nullable=false)
private String content; // 消息内容(JSON)
@Column(name="status", nullable=false)
private int status; // 状态:0-待发送 1-已发送 2-成功 3-失败 4-发送中
@Column(name="retry_count")
private int retryCount; // 重试次数
@Column(name="next_retry_time")
private LocalDateTime nextRetryTime; // 下次重试时间
// 创建和更新时间...
}
状态机的设计很关键,它决定了消息的生命周期流转。我们定义了5种状态:
- 0(待发送):消息刚创建,等待被发送
- 4(发送中):消息正在被处理发送
- 1(已发送):消息已成功发送到MQ
- 2(处理成功):下游服务处理成功
- 3(处理失败):下游服务处理失败
2.3 RabbitMQ配置
消息队列的配置需要特别注意可靠性和延迟重试机制。我们为订单服务配置了以下交换机和队列:
java复制@Configuration
public class OrderRabbitMQConfig {
// 订单支付交换机
@Bean
public DirectExchange orderExchange() {
return new DirectExchange("order.exchange", true, false);
}
// 延迟交换机(用于处理失败后的重试)
@Bean
public DirectExchange orderDelayExchange() {
return new DirectExchange("order.delay.exchange", true, false);
}
// 支付队列
@Bean
public Queue orderPayQueue() {
return new Queue("order.pay.queue", true);
}
// 延迟队列(使用死信队列实现延迟)
@Bean
public Queue orderDelayQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "order.delay.process.exchange");
args.put("x-dead-letter-routing-key", "order.delay.process");
args.put("x-message-ttl", 1 * 60 * 1000); // 1分钟延迟
return QueueBuilder.durable("order.delay.queue")
.withArguments(args)
.build();
}
// 绑定关系...
}
这里特别使用了死信队列实现延迟消息,用于处理失败后的重试。当消息在order.delay.queue中过期后,会自动转发到order.delay.process.queue进行处理。
3. 核心实现细节
3.1 订单创建流程
订单创建是整个流程的起点,需要在一个本地事务中完成订单记录、库存扣减和消息创建:
java复制@Transactional
public ApiResponse<?> createOrder(long userId, long goodsId) {
// 1. 检查库存(使用SELECT FOR UPDATE加锁)
Optional<Goods> goodsOptional = goodsRepos.findByIdForUpdate(goodsId);
if(goodsOptional.isPresent()) {
Goods goods = goodsOptional.get();
if(goods.getStock()<1) {
return ApiResponse.badRequest("库存不足");
}
// 2. 创建订单
String order_sn = UUID.randomUUID().toString();
Order order = new Order();
order.setOrderSn(order_sn);
order.setStatus('0'); //0-待支付
order.setGoodsId(goodsId);
order.setUserid(userId);
orderRepository.save(order);
// 3. 扣减库存(实际是冻结库存)
goods.setStock(goods.getStock()-1);
goods.setFrozen_stock(goods.getFrozen_stock()+1);
goodsRepos.save(goods);
// 4. 创建本地消息
createLocalMessage(order_sn, userId, goods.getPrice());
return ApiResponse.success("已提交成功,请等待");
}
return ApiResponse.badRequest("商品不存在");
}
这里有几个关键点:
- 使用SELECT FOR UPDATE锁定商品记录,防止超卖
- 库存扣减实际上是"可用库存减1,冻结库存加1"
- 消息创建和业务操作在同一个事务中,保证原子性
3.2 消息发送机制
消息发送采用了"定时扫描+多线程"的方式,既保证了发送的及时性,又避免了阻塞主流程:
java复制@Service
@Slf4j
public class MultiThreadService {
private final LocalMessageRespository localMessageRespository;
private final RabbitTemplate rabbitTemplate;
private final ExecutorService[] threadPools;
// 每1秒扫描一次待发送消息
@Scheduled(fixedRate = 1000)
public void scanAndSendMessage() {
List<Local_Message> messages = localMessageRespository.findPendingMessage();
messages.forEach(message -> {
// 先更新状态为"发送中",防止重复发送
int updateCnt = localMessageRespository.updateToSending(message.getId());
if(updateCnt > 0) {
// 根据ID哈希分配到不同线程处理
int index = (int)(message.getId() % threadPools.length);
threadPools[index].submit(() -> sendPayMessage(message.getId()));
}
});
}
private void sendPayMessage(long messageId) {
Optional<Local_Message> localMessageOptional = localMessageRespository.findById(messageId);
if(localMessageOptional.isPresent()) {
Local_Message localMessage = localMessageOptional.get();
// 发送到MQ
rabbitTemplate.convertAndSend("order.exchange", "order.pay", localMessage.getContent());
// 更新状态为"已发送"
localMessageRespository.updateToSent(messageId);
}
}
}
线程池的配置也很讲究,我们使用了多个单线程的Executor,而不是一个大的线程池:
java复制@Configuration
@EnableAsync
public class ThreadPoolConfig {
private static final int THREAD_COUNT = 8;
@Bean
public ExecutorService[] threadPools() {
ExecutorService[] pools = new ExecutorService[THREAD_COUNT];
for(int i=0; i<THREAD_COUNT; i++) {
pools[i] = Executors.newSingleThreadExecutor(r ->
new Thread(r, "msg-processor-"+i)
);
}
return pools;
}
}
这样设计的原因是:
- 单线程Executor可以保证同一订单的消息顺序性
- 多个Executor可以并行处理不同订单的消息
- 避免了大线程池的上下文切换开销
3.3 消息处理与结果回调
用户服务接收到支付消息后,处理逻辑如下:
java复制@Transactional
public void handlePayMessage(PayMessage payMessage) {
String order_sn = payMessage.getOrderSn();
log.info("处理支付消息,order_sn:{}", order_sn);
// 幂等性检查
if(payRepos.existsByXid(order_sn)) {
log.info("该笔订单已支付,order_sn:{}", order_sn);
return;
}
long userId = payMessage.getUserId();
User user = userRepos.findUserById(userId);
if(user == null) {
sendPayResult(order_sn, 1, "用户不存在");
return;
}
BigDecimal amount = payMessage.getAmount();
if(user.getBalance().compareTo(amount) <= 0) {
sendPayResult(order_sn, 1, "余额不足");
return;
}
// 扣减余额
user.setBalance(user.getBalance().subtract(amount));
userRepos.save(user);
// 记录支付流水
Pay pay = new Pay();
pay.setXid(order_sn);
pay.setAmount(amount);
pay.setStatus('1');
pay.setUserid(userId);
payRepos.save(pay);
// 发送成功响应
sendPayResult(order_sn, 0, "");
}
订单服务接收到处理结果后,更新本地状态:
java复制@Transactional
public void handlePayResultMessage(PayResultMessage payResultMessage) {
String order_sn = payResultMessage.getOrderSn();
int status = payResultMessage.getStatus();
// 更新本地消息状态
Optional<Local_Message> optionalLocalMessage = localMessageRespository.findByBusinessKey(order_sn);
if(optionalLocalMessage.isPresent()) {
Local_Message localMessage = optionalLocalMessage.get();
localMessage.setStatus(status == 0 ? 2 : 3); // 2-成功, 3-失败
localMessageRespository.save(localMessage);
}
// 更新订单状态
Optional<Order> optionalOrder = orderRepository.findByOrderSn(order_sn);
if(optionalOrder.isPresent()) {
Order order = optionalOrder.get();
if(status == 0) {
order.setStatus('1'); // 支付成功
// 解冻库存(实际扣减)
Goods goods = goodsRepos.findById(order.getGoodsId());
goods.setFrozen_stock(goods.getFrozen_stock()-1);
goodsRepos.save(goods);
} else {
order.setStatus('2'); // 支付失败
// 触发延迟检查
sendDelayMessage(order_sn);
}
orderRepository.save(order);
}
}
对于处理失败的情况,会发送延迟消息进行后续处理:
java复制private void sendDelayMessage(String order_sn) {
DelayMessage delayMessage = new DelayMessage();
delayMessage.setOrderSn(order_sn);
delayMessage.setType('0'); //0-PAY-CHECK
rabbitTemplate.convertAndSend(
"order.delay.exchange",
"order.delay",
JSON.toJSONString(delayMessage)
);
}
延迟消息会在1分钟后被处理,检查订单状态并做最终处理:
java复制@Transactional
public void handleOrderDelayMessage(DelayMessage delayMessage) {
String order_sn = delayMessage.getOrderSn();
Optional<Order> optionalOrder = orderRepository.findByOrderSn(order_sn);
if(optionalOrder.isPresent()) {
Order order = optionalOrder.get();
if(order.getStatus() != '1') { // 非终态(未支付成功)
order.setStatus('3'); // 自动关闭(过期)
orderRepository.save(order);
// 释放冻结库存
Goods goods = goodsRepos.findById(order.getGoodsId());
goods.setFrozen_stock(goods.getFrozen_stock()-1);
goods.setStock(goods.getStock()+1);
goodsRepos.save(goods);
}
}
}
4. 关键问题与解决方案
4.1 消息可靠性保证
在实际应用中,我们需要确保消息不丢失、不重复。这个方案通过以下机制保证可靠性:
- 本地消息表:所有消息先持久化到数据库,再异步发送
- 定时任务补偿:定时扫描待处理消息,自动重试失败的消息
- 消费端幂等:通过业务键(如订单号)保证重复消息不会重复处理
- 事务状态追踪:完整记录消息生命周期,便于排查问题
4.2 性能优化实践
在高并发场景下,我们做了以下优化:
- 批量处理:定时任务一次获取多条待处理消息,减少数据库查询次数
- 多线程发送:使用线程池并行发送消息,提高吞吐量
- 消息分区:根据消息ID哈希到不同线程,保证同一订单的顺序性
- 异步回调:支付结果通过MQ异步通知,不阻塞主流程
4.3 常见问题排查
在实际运行中,我们遇到过以下典型问题及解决方案:
-
消息积压:
- 原因:消费者处理速度跟不上生产速度
- 解决:增加消费者实例,优化处理逻辑,设置合理的并发参数
-
状态不一致:
- 原因:网络问题导致回调消息丢失
- 解决:增加定时补偿任务,定期检查超时未处理的消息
-
重复消费:
- 原因:MQ的重试机制导致
- 解决:消费端实现幂等处理,通过业务键去重
-
死信堆积:
- 原因:某些消息一直处理失败
- 解决:设置最大重试次数,超过阈值后人工干预
5. 方案适用场景与局限性
5.1 适用场景
这种方案特别适合以下场景:
- 高并发系统:如秒杀、抢购等需要快速响应的场景
- 最终一致性:可以接受短暂不一致,但最终必须一致的业务
- 跨服务操作:需要协调多个服务的业务场景
- 异步处理:主流程需要快速返回,后台异步处理的场景
5.2 局限性
这种方案也有其局限性:
- 开发复杂度高:需要维护消息状态、处理各种异常情况
- 排查问题困难:问题可能发生在多个服务,需要完整追踪消息流
- 不适合强一致性:对于要求强一致性的业务不适用
- 依赖MQ可靠性:如果MQ出现故障,整个系统会受影响
6. 实践经验与建议
在实际项目中落地这个方案,我有以下几点建议:
-
消息设计:
- 消息体要包含足够的信息,但不宜过大
- 定义清晰的消息类型和状态码
- 使用JSON等通用格式,方便扩展
-
监控报警:
- 监控消息积压情况
- 设置处理超时报警
- 记录关键指标:发送成功率、处理耗时等
-
测试策略:
- 模拟MQ故障,测试系统容错能力
- 测试消息重复消费的场景
- 进行长时间的压力测试
-
文档维护:
- 记录消息流转的完整流程图
- 维护常见问题处理手册
- 记录每个消息类型的处理逻辑
这个方案在我们的秒杀系统中表现良好,峰值时能处理上万TPS,且保证了数据的最终一致性。虽然实现复杂度较高,但对于需要高并发和高可用的场景,这种折中是值得的。