欢迎光临
我们一直在努力

RabbitMQ死信队列处理超时未支付订单:

RabbiMQ消息队列:

RabbitMQ异步执行库存扣减:

What:使用RabbitMQ来取代课程中的队列,将添加订单数据至mysql改成异步的,并且使用令牌桶算法来进行一定程度上的限流。

//令牌桶算法 限流
if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)){
return Result.fail("目前网络正忙,请重试");
}

How:

  • 通过lua脚本去判断库存是否充足以及用户是否多次购买,直接返回报错即可,保证Redis执行的操作的原子性。

  • 若都满足条件,则通过生产者发送VoucherOrder对象到消息队列里面,返回前端订单号即可。

  • 通过消费者去处理消息队列,进行数据库扣减库存、订单生成操作。

  • 基于死信队列处理订单超时未支付:

    What:秒杀订单创建后,用户通常有 10-30 分钟支付时间,超时后需自动恢复库存。一般设计到定时的问题都会用到xxl-job等定时任务组件,但是高并发下不推荐定时任务轮询(压力大、延迟高),优先用「消息中间件延迟队列」,这里我们使用RabbitMQ。

    Who:

    当一个队列中的某条消息满足下列情况之一时,就会成为死信(dead letter):

    消费者使用 basic.reject 或 basic.nack 声明消费失败,并且消息的 requeue 参数设置为 false。

    过期消息(达到了队列或消息本身设置的过期时间),消息超时后无人消费。

    要投递的队列消息堆积满了,最早的消息可能成为死信。

    What:

  • 在发送创建订单消息前面先将订单放入延迟队列。

    rabbitTemplate.convertAndSend("delay.topic", "delay.order", voucherOrder, new MessagePostProcessor() {
    @Override
    public Message postProcessMessage(Message message) throws AmqpException {
    message.getMessageProperties().setExpiration("180000");
    return message;
    }
    });

  • 编写lua脚本,实现库存恢复以及去除已购买用户集合,并记录已进行恢复的Order订单号。(防止重复消费)

    local VoucherId = ARGV[1]
    local UserId = ARGV[2]
    local OrderId = ARGV[3]
    –变量key
    local seckill_key = "seckill:stock:"..VoucherId
    local user_key = "seckill:order:"..VoucherId
    local order_key = "seckill:order:"..OrderId
    if (tonumber(redis.call("sismember",order_key,OrderId))==1) then
    return 1
    end
    –恢复库存
    redis.call('incrby',seckill_key,1)
    –恢复用户购买资格
    redis.call("srem",user_key,UserId)
    –将恢复的订单号保存
    redis.call("xadd",order_key,OrderId)
    return 0

  • 采用乐观锁实现数据库的原子性,以及lua脚本实现Redis的原子性,实现库存恢复。

  • @Transactional
    public void recoverStock(Long orderId, Long voucherId, Long userId) {
    // 1. 数据库层面:更新订单状态为“已取消”+恢复库存(乐观锁保证原子性)
    // 1.1 先更新订单状态(仅未支付订单可取消)
    boolean update = this.lambdaUpdate()
    .set(VoucherOrder::getStatus, 4) // 2-已取消
    .eq(VoucherOrder::getId, orderId)
    .eq(VoucherOrder::getStatus, 1) // 仅未支付订单
    .update();
    if (!update) {
    log.info("订单{}无需恢复(已支付或已取消)", orderId);
    return;
    }
    // 1.2 恢复数据库库存
    boolean recoverDb = seckillVoucherMapper.update(voucherId);
    if (!recoverDb) {
    log.error("订单{}数据库库存恢复失败", orderId);
    return;
    }
    // 2. Redis层面:执行Lua脚本恢复库存
    Long redisResult = stringRedisTemplate.execute(
    SECKILL_RECOVER_SCRIPT,
    Collections.emptyList(),
    orderId.toString(),
    voucherId.toString(),
    userId.toString()
    );
    if (redisResult == 0) {
    log.info("订单{}库存恢复成功(DB+Redis)", orderId);
    } else {
    log.warn("订单{}Redis库存已恢复,无需重复操作", orderId);
    }
    }

  • 消费者处理延迟队列步骤:1.先进行订单和支付状态的判断,若已支付或者已取消,直接执行ACK。2.反之,执行库存恢复业务。3.开启消费者确认机制:Auto,实现MessageRecover接口中的RepublishMessageRecover,重试耗尽后,将消息发送到异常交换机和消息队列,进行人工处理。

  • package com.hmdp.utils;

    import com.hmdp.entity.VoucherOrder;
    import com.hmdp.service.IVoucherOrderService;
    import com.hmdp.service.impl.VoucherOrderServiceImpl;
    import com.rabbitmq.client.Channel;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.core.ExchangeTypes;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.rabbit.annotation.*;
    import org.springframework.stereotype.Component;

    import javax.annotation.Resource;
    import java.io.IOException;
    import java.util.Map;

    @Component
    @Slf4j
    public class MQRecoveryReceiver {
    @Resource
    private IVoucherOrderService voucherOrderService;

    // 处理超时未支付订单(核心优化:添加状态校验)
    @RabbitListener(
    bindings = @QueueBinding(
    value = @Queue(name = "dlx.queue", durable = "true"),
    exchange = @Exchange(name = "dlx.direct", durable = "true"),
    key = "dlx.key"
    )
    )
    public void handleTimeoutOrder(VoucherOrder voucherOrder, com.rabbitmq.client.Channel channel, Message message) throws IOException {
    long deliveryTag = message.getMessageProperties().getDeliveryTag();
    try {
    Long orderId = voucherOrder.getId();
    Long voucherId = voucherOrder.getVoucherId();
    Long userId = voucherOrder.getUserId();

    // 1. 校验订单是否存在
    VoucherOrder order = voucherOrderService.getById(orderId);
    if (order == null) {
    log.info("订单{}不存在,确认消息", orderId);
    channel.basicAck(deliveryTag, false);
    return;
    }

    // 2. 校验订单状态:仅未支付订单执行恢复
    if (order.getStatus() != 1) {
    log.info("订单{}已支付或取消,无需处理超时", orderId);
    channel.basicAck(deliveryTag, false);
    return;
    }
    // 3. 执行库存恢复(事务内执行)
    voucherOrderService.recoverStock(orderId, voucherId, userId);
    // 4. 确认消息
    channel.basicAck(deliveryTag, false);
    } catch (Exception e) {
    log.error("超时订单处理失败, orderId={}", voucherOrder.getId(), e);
    // 修正:临时异常时重回队列(限制重试次数,避免死循环)
    // 生产环境建议:设置重试次数,超过次数后转发到死信队列归档
    channel.basicNack(deliveryTag, false, true);
    }
    }
    }

    死信交换机;

    package com.hmdp.config;
    import org.springframework.amqp.core.*;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import java.util.HashMap;
    import java.util.Map;
    @Configuration
    public class RabbitMQConfig {
    // 1. 延迟交换机(topic类型,和代码中发送的交换机一致)
    @Bean
    public Exchange delayTopicExchange() {
    return ExchangeBuilder.topicExchange("delay.topic").durable(true).build();
    }

    // 2. 延迟队列(设置死信参数,不设置队列级别TTL)
    @Bean
    public Queue delayQueue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-dead-letter-exchange", "dlx.direct"); // 死信交换机
    args.put("x-dead-letter-routing-key", "dlx.key"); // 死信路由键
    // 移除队列级别TTL,仅用消息级别TTL
    return QueueBuilder.durable("delay.queue").withArguments(args).build();
    }

    // 3. 绑定延迟交换机和延迟队列
    @Bean
    public Binding delayBinding() {
    return BindingBuilder.bind(delayQueue()).to(delayTopicExchange()).with("delay.order").noargs();
    }

    // 4. 死信交换机
    @Bean
    public Exchange dlxDirectExchange() {
    return ExchangeBuilder.directExchange("dlx.direct").durable(true).build();
    }

    // 5. 死信队列
    @Bean
    public Queue dlxQueue() {
    return QueueBuilder.durable("dlx.queue").build();
    }

    // 6. 绑定死信交换机和死信队列
    @Bean
    public Binding dlxBinding() {
    return BindingBuilder.bind(dlxQueue()).to(dlxDirectExchange()).with("dlx.key").noargs();
    }
    @Bean
    public Exchange topicExchange() {
    return ExchangeBuilder.directExchange("error.exchange").durable(true).build();
    }
    @Bean
    public Queue errorQueue() {
    return QueueBuilder.durable("error.queue").build();
    }
    @Bean
    public Binding errorBinding() {
    return BindingBuilder.bind(errorQueue()).to(topicExchange()).with("error").noargs();
    }
    }

  • 支付成功接口,修改支付状态。

  • 用户主动取消订单:先进行订单的判断,判断订单是否存在、是否本人操作以及订单状态(已支付无法取消),进行(3)操作。

  • // 3. 核心校验:订单存在+归属正确+状态为“未支付”
    VoucherOrder order = getById(orderId);
    if (order == null) {
    return Result.fail("订单不存在");
    }
    if (!order.getUserId().equals(userId)) {
    return Result.fail("无权限取消此订单");
    }
    if (order.getStatus() != 1) { // 0=未支付,只有未支付订单可取消
    return Result.fail("订单已支付/已取消,无需重复操作");
    }

    赞(0)
    未经允许不得转载:171主机测评 » RabbitMQ死信队列处理超时未支付订单:
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址