高并发外卖霸王餐订单创建:Java后端如何用“本地消息表+定时任务”替代分布式事务保证数据不丢失

高并发外卖霸王餐订单创建:Java后端如何用“本地消息表+定时任务”替代分布式事务保证数据不丢失

在美团外卖“霸王餐”业务场景下,核心流程涉及两个关键动作:用户下单(库存扣减)与发放奖励(优惠券/积分)。在高并发环境下,若强依赖Seata等分布式事务框架,虽然能保证强一致性,但往往带来性能瓶颈和复杂的运维成本。为了追求更高的吞吐量和最终一致性,业界普遍采用“本地消息表”模式来替代传统的分布式事务。本文将深入探讨如何在Java后端利用该模式,结合定时任务补偿,确保数据不丢失。

核心设计思想:本地消息表

“本地消息表”的核心理念是将分布式事务拆解为多个本地事务。关键在于引入一张中间表(如message_transaction),该表与业务表(如order)处于同一个数据库实例中。通过在同一个本地事务中同时操作业务数据和消息状态,利用数据库的ACID特性,保证“业务执行”与“消息记录”的原子性。随后由独立的消费者或定时任务异步处理该消息,通知下游服务(如奖励服务)进行数据同步。

数据库表结构设计

首先,我们需要在订单库中创建一张本地消息表。该表用于记录待处理的奖励发放任务。

CREATE TABLE `local_message` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
  `order_id` VARCHAR(64) NOT NULL COMMENT '关联订单ID',
  `user_id` BIGINT NOT NULL COMMENT '用户ID',
  `campaign_id` VARCHAR(64) NOT NULL COMMENT '活动ID',
  `status` TINYINT NOT NULL DEFAULT '0' COMMENT '状态:0-待处理,1-已发送,2-已完成,3-失败',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `retry_count` INT NOT NULL DEFAULT 0 COMMENT '重试次数',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
订单创建与消息落库

在订单服务的业务逻辑中,我们将“创建订单”和“插入本地消息”放在同一个数据库事务中。只要订单创建成功,本地消息必然生成,从而保证了数据源的可靠性,避免了因网络抖动导致的消息丢失。

package baodanbao.com.cn.service;

import baodanbao.com.cn.dao.LocalMessageMapper;
import baodanbao.com.cn.dao.OrderMapper;
import baodanbao.com.cn.model.LocalMessage;
import baodanbao.com.cn.model.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private LocalMessageMapper messageMapper;

    /**
     * 创建霸王餐订单
     * 该方法在一个本地事务中完成,保证了业务与消息的一致性
     */
    @Transactional(rollbackFor = Exception.class)
    public void createOrderWithMessage(String orderId, Long userId, String campaignId) {
        // 1. 插入订单记录
        Order order = new Order();
        order.setOrderId(orderId);
        order.setUserId(userId);
        order.setCampaignId(campaignId);
        order.setStatus("CREATED");
        orderMapper.insert(order);

        // 2. 插入本地消息表(状态:待处理)
        // 如果这一步失败,整个事务回滚,订单也不会被创建
        LocalMessage message = new LocalMessage();
        message.setOrderId(orderId);
        message.setUserId(userId);
        message.setCampaignId(campaignId);
        message.setStatus(0); // 0: 待处理
        message.setRetryCount(0);
        messageMapper.insert(message);
    }
}

在这里插入图片描述

异步处理与定时补偿

数据落库后,我们需要一个机制来驱动消息的消费。通常采用“直接发送+定时补偿”的双保险策略。

  1. 直接发送:在订单创建后,通过线程池异步尝试发送MQ或调用奖励服务。
  2. 定时补偿:若直接发送失败或超时,由定时任务扫描状态为“待处理”且超过一定时间的消息,进行重试。
package baodanbao.com.cn.task;

import baodanbao.com.cn.dao.LocalMessageMapper;
import baodanbao.com.cn.model.LocalMessage;
import baodanbao.com.cn.service.RewardServiceClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Component
public class MessageCompensationTask {

    private static final Logger logger = LoggerFactory.getLogger(MessageCompensationTask.class);

    @Autowired
    private LocalMessageMapper messageMapper;

    @Autowired
    private RewardServiceClient rewardServiceClient;

    /**
     * 定时扫描未完成的消息并进行补偿
     * 每隔30秒执行一次
     */
    @Scheduled(fixedDelay = 30000)
    @Transactional(rollbackFor = Exception.class)
    public void compensateMessages() {
        // 查询状态为“待处理”且重试次数小于3次的消息
        // 这里可以加时间条件,如 create_time < now() - 1分钟,防止刚插入的数据被误扫
        List<LocalMessage> messages = messageMapper.select待处理Messages(3);

        for (LocalMessage message : messages) {
            try {
                // 调用奖励服务发放奖励
                boolean success = rewardServiceClient.grantReward(message.getUserId(), message.getCampaignId());

                if (success) {
                    // 更新消息状态为“已完成”
                    messageMapper.updateStatus(message.getId(), 2, null);
                    logger.info("补偿成功,订单ID: {}", message.getOrderId());
                } else {
                    // 更新重试次数
                    int newCount = message.getRetryCount() + 1;
                    messageMapper.incrementRetryCount(message.getId(), newCount);
                    logger.warn("补偿失败,重试次数+1,订单ID: {}", message.getOrderId());
                }
            } catch (Exception e) {
                logger.error("补偿任务执行异常,订单ID: {}", message.getOrderId(), e);
                int newCount = message.getRetryCount() + 1;
                messageMapper.incrementRetryCount(message.getId(), newCount);
            }
        }
    }
}
消息消费与幂等性

在奖励服务端接收到消息后,必须保证接口的幂等性,因为定时任务可能会导致同一条消息被多次投递。

package baodanbao.com.cn.service;

import org.springframework.stereotype.Service;

@Service
public class RewardService {

    /**
     * 发放奖励(必须保证幂等)
     */
    public boolean grantReward(Long userId, String campaignId) {
        // 1. 查询该用户在该活动下是否已领取过奖励
        // SELECT * FROM user_reward WHERE user_id = ? AND campaign_id = ?
        // 如果已存在记录,直接返回成功(幂等性保证)
        
        // 2. 如果不存在,则插入奖励记录
        // INSERT INTO user_reward ...
        
        // 3. 返回结果
        return true; 
    }
}

本文著作权归 俱美开放平台 ,转载请注明出处!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值