Java秒杀系统实战系列~RabbitMQ死信队列处理超时未支付的订单

简介: 摘要:本篇博文是“Java秒杀系统实战系列文章”的第十篇,本篇博文我们将采用RabbitMQ的死信队列的方式处理“用户秒杀成功生成订单后,却迟迟没有支付”的情况,一起来见识一下RabbitMQ死信队列在实际业务环境下的强大之处!内容:对于消息中间件RabbitMQ,Debug其实在前面的篇章中已...

摘要:
本篇博文是“Java秒杀系统实战系列文章”的第十篇,本篇博文我们将采用RabbitMQ的死信队列的方式处理“用户秒杀成功生成订单后,却迟迟没有支付”的情况,一起来见识一下RabbitMQ死信队列在实际业务环境下的强大之处!

内容:
对于消息中间件RabbitMQ,Debug其实在前面的篇章中已经简单分享介绍过了,在这里就不再赘述了!在本文我们将采用RabbitMQ的死信队列实现这样的业务需求:“用户在秒杀成功并成功创建一笔订单记录后,理论上应该是执行去支付的操作,但是却存在着一种情况是用户迟迟不肯去支付~至于原因,不得而知!”

对于这种场景,各位小伙伴可以在一些商城平台体验一下,即挑选完商品,加入购物车后,点击去结算,这个时候会有个倒计时,提醒你需要在指定的时间内完成付款,否则订单将失效!

对于这种业务逻辑的处理,传统的做法是采用“定时器的方式”,定时轮询获取已经超过指定时间的订单,然后执行一系列的处理措施(比如再争取给用户发送短信,提醒超过多长时间订单就要失效了等等。。。),在这个秒杀系统中,我们将借助RabbitMQ死信队列这一组件,对该订单执行“失效”的措施!

“死信队列”,顾明思议,是可以延时、延迟一定的时间再处理消息的一种特殊队列,它相对于“普通的队列”而言,可以实现“进入死信队列的消息不立即处理,而是可以等待一定的时间再进行处理”的功能!而普通的队列则不行,即进入队列后的消息会立即被对应的消费者监听消费,如下图所示为普通队列的基本消息模型:

image

而对于“死信队列”,它的构成以及使用相对而言比较复杂一点,在正常情况,死信队列由三大核心组件组成:死信交换机+死信路由+TTL(消息存活时间~非必需的),而死信队列又可以由“面向生产者的基本交换机+基本路由”绑定而成,故而生产者首先是将消息发送至“基本交换机+基本路由”所绑定而成的消息模型中,即间接性地进入到死信队列中,当过了TTL,消息将“挂掉”,从而进入下一个中转站,即“面下那个消费者的死信交换机+死信路由”所绑定而成的消息模型中。如下图所示:

image

下面,我们以实际的代码来构建死信队列的消息模型,并将此消息模型应用到秒杀系统的上述功能模块中。

(1)首先,需要在RabbitmqConfig配置类创建死信队列的消息模型,其完整的源代码如下所示:

//构建秒杀成功之后-订单超时未支付的死信队列消息模型

@Bean
public Queue successKillDeadQueue(){
    Map<String, Object> argsMap= Maps.newHashMap();
    argsMap.put("x-dead-letter-exchange",env.getProperty("mq.kill.item.success.kill.dead.exchange"));
    argsMap.put("x-dead-letter-routing-key",env.getProperty("mq.kill.item.success.kill.dead.routing.key"));
    return new Queue(env.getProperty("mq.kill.item.success.kill.dead.queue"),true,false,false,argsMap);
}

//基本交换机
@Bean
public TopicExchange successKillDeadProdExchange(){
    return new TopicExchange(env.getProperty("mq.kill.item.success.kill.dead.prod.exchange"),true,false);
}
//创建基本交换机+基本路由 -> 死信队列 的绑定
@Bean
public Binding successKillDeadProdBinding(){
    return BindingBuilder.bind(successKillDeadQueue()).to(successKillDeadProdExchange()).with(env.getProperty("mq.kill.item.success.kill.dead.prod.routing.key"));
}
//真正的队列
@Bean
public Queue successKillRealQueue(){
    return new Queue(env.getProperty("mq.kill.item.success.kill.dead.real.queue"),true);
}
//死信交换机
@Bean
public TopicExchange successKillDeadExchange(){
    return new TopicExchange(env.getProperty("mq.kill.item.success.kill.dead.exchange"),true,false);
}
//死信交换机+死信路由->真正队列 的绑定
@Bean
public Binding successKillDeadBinding(){
    return BindingBuilder.bind(successKillRealQueue()).to(successKillDeadExchange()).with(env.getProperty("mq.kill.item.success.kill.dead.routing.key"));
}

其中,环境变量对象实例env读取的变量是配置在application.properties配置文件中的,取值如下所示:

#订单超时未支付自动失效-死信队列消息模型
mq.kill.item.success.kill.dead.queue=${mq.env}.kill.item.success.kill.dead.queue
mq.kill.item.success.kill.dead.exchange=${mq.env}.kill.item.success.kill.dead.exchange
mq.kill.item.success.kill.dead.routing.key=${mq.env}.kill.item.success.kill.dead.routing.key

mq.kill.item.success.kill.dead.real.queue=${mq.env}.kill.item.success.kill.dead.real.queue
mq.kill.item.success.kill.dead.prod.exchange=${mq.env}.kill.item.success.kill.dead.prod.exchange
mq.kill.item.success.kill.dead.prod.routing.key=${mq.env}.kill.item.success.kill.dead.prod.routing.key

#单位为ms
mq.kill.item.success.kill.expire=20000

(2)成功创建了消息模型之后,紧接着,我们需要在通用的RabbitMQ发送消息服务类RabbitSenderService中开发“发送消息入死信队列”的功能,在该功能方法中,我们指定了消息的存活时间TTL,取值为配置的变量:mq.kill.item.success.kill.expire 的值,即20s;其完整的源代码如下所示:

//秒杀成功后生成抢购订单-发送信息入死信队列,等待着一定时间失效超时未支付的订单
public void sendKillSuccessOrderExpireMsg(final String orderCode){
    try {
        if (StringUtils.isNotBlank(orderCode)){
            KillSuccessUserInfo info=itemKillSuccessMapper.selectByCode(orderCode);
            if (info!=null){
                rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
                rabbitTemplate.setExchange(env.getProperty("mq.kill.item.success.kill.dead.prod.exchange"));
                rabbitTemplate.setRoutingKey(env.getProperty("mq.kill.item.success.kill.dead.prod.routing.key"));
                rabbitTemplate.convertAndSend(info, new MessagePostProcessor() {
                    @Override
                    public Message postProcessMessage(Message message) throws AmqpException {
                        MessageProperties mp=message.getMessageProperties();
                        mp.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                        mp.setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME,KillSuccessUserInfo.class);

                        //TODO:动态设置TTL(为了测试方便,暂且设置20s)
                        mp.setExpiration(env.getProperty("mq.kill.item.success.kill.expire"));
                        return message;
                    }
                });
            }
        }
    }catch (Exception e){
        log.error("秒杀成功后生成抢购订单-发送信息入死信队列,等待着一定时间失效超时未支付的订单-发生异常,消息为:{}",orderCode,e.fillInStackTrace());
    }
}

从该“发送消息入死信队列”的代码中,我们可以看到,消息首先是先入到“基本交换机+基本路由”所绑定的死信队列的消息模型中的!当消息到了TTL,自然会从死信队列中出来(即“解脱了”),然后进入下一个中转站,即:“死信交换机+死信路由” 所绑定而成的真正队列的消息模型中,最终真正被消费者监听消费!

此时,可以将整个项目、系统运行在外置的tomcat服务器中,然后打开RabbitMQ后端控制台应用,找到该死信队列,可以看到该死信队列的详细信息,如下图所示:

image

(3)最后,是需要在RabbitMQ通用的消息监听服务类RabbitReceiverService 中监听“真正队列”中的消息并进行处理:在这里我们是对该订单进行失效处理(前提是还没付款的情况下!),其完整的源代码如下所示:

//用户秒杀成功后超时未支付-监听者
@RabbitListener(queues = {"${mq.kill.item.success.kill.dead.real.queue}"},containerFactory = "singleListenerContainer")
public void consumeExpireOrder(KillSuccessUserInfo info){
    try {
        log.info("用户秒杀成功后超时未支付-监听者-接收消息:{}",info);

        if (info!=null){
            ItemKillSuccess entity=itemKillSuccessMapper.selectByPrimaryKey(info.getCode());
            if (entity!=null && entity.getStatus().intValue()==0){
                itemKillSuccessMapper.expireOrder(info.getCode());
            }
        }
    }catch (Exception e){
        log.error("用户秒杀成功后超时未支付-监听者-发生异常:",e.fillInStackTrace());
    }
}

其中,失效更新订单的记录的操作由 itemKillSuccessMapper.expireOrder(info.getCode()); 来实现,其对应的动态Sql的写法如下所示:

<!--失效更新订单信息-->
<update id="expireOrder">
  UPDATE item_kill_success
  SET status = -1
  WHERE code = #{code} AND status = 0
</update>

(4)至此,关于RabbitMQ死信队列消息模型的代码实战已经完毕了!最后我只需要在“用户秒杀成功创建订单的那一刻,发送消息入死信队列”的地方调用即可,其调用代码如下所示:

/**
 * 通用的方法-记录用户秒杀成功后生成的订单-并进行异步邮件消息的通知
 * @param kill
 * @param userId
 * @throws Exception
 */
private void commonRecordKillSuccessInfo(ItemKill kill, Integer userId) throws Exception{
    //TODO:记录抢购成功后生成的秒杀订单记录

    ItemKillSuccess entity=new ItemKillSuccess();
    String orderNo=String.valueOf(snowFlake.nextId());

    //entity.setCode(RandomUtil.generateOrderCode());   //传统时间戳+N位随机数
    entity.setCode(orderNo); //雪花算法
    entity.setItemId(kill.getItemId());
    entity.setKillId(kill.getId());
    entity.setUserId(userId.toString());
    entity.setStatus(SysConstant.OrderStatus.SuccessNotPayed.getCode().byteValue());
    entity.setCreateTime(DateTime.now().toDate());
    //TODO:学以致用,举一反三 -> 仿照单例模式的双重检验锁写法
    if (itemKillSuccessMapper.countByKillUserId(kill.getId(),userId) <= 0){
        int res=itemKillSuccessMapper.insertSelective(entity);

        if (res>0){
            //TODO:进行异步邮件消息的通知=rabbitmq+mail
            rabbitSenderService.sendKillSuccessEmailMsg(orderNo);

            //TODO:入死信队列,用于 “失效” 超过指定的TTL时间时仍然未支付的订单
            rabbitSenderService.sendKillSuccessOrderExpireMsg(orderNo);
        }
    }
}

最后,是进行自测:点击“抢购”按钮,用户秒杀成功后,会发送一条消息入死信队列(这一点可以在RabbitMQ后端控制台中可以看到一条正Ready好的消息),等待20s,即可看到消息转移到真正的队列,并被真正的消费者监听消费,如下所示:

image

好了,关于“RabbitMQ死信队列”的介绍以及应用实战本文就暂且介绍到这里了,此种方式可以很灵活对“超时未支付的订单”,进行很好的处理,而且整个过程是“自动、自然”的,而无需人为去手动点击按钮触发了!当然啦,万事万物都并非十全十美的,死信队列也是如此,在一篇文章中我们将介绍此种方式的瑕疵之处,并采用相应的解决方案进行处理!

补充:

1、由于相应的博客的更新可能并不会很快,故而如果有想要快速入门以及实战整套系统的,可以参考阅读: Java商城秒杀系统的设计与实战视频教程(SpringBoot版)

2、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,完整的源代码数据库地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill

相关实践学习
RocketMQ一站式入门使用
从源码编译、部署broker、部署namesrv,使用java客户端首发消息等一站式入门RocketMQ。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
9天前
|
存储 SQL 安全
Java 安全性编程:基本概念与实战指南
【4月更文挑战第27天】在当今的软件开发领域,安全性编程是一个至关重要的方面。Java,作为广泛使用的编程语言之一,提供了多种机制来保护应用免受常见的安全威胁。本博客将探讨 Java 安全性编程的基本概念,并通过实际示例来展示如何实现这些安全措施。
17 3
|
4天前
|
存储 Java 数据格式
Java实战:轻松掌握文件重命名与路径提取技巧
Java实战:轻松掌握文件重命名与路径提取技巧
12 0
|
6天前
|
设计模式 算法 安全
Java多线程编程实战:从入门到精通
【4月更文挑战第30天】本文介绍了Java多线程编程的基础,包括线程概念、创建线程(继承`Thread`或实现`Runnable`)、线程生命周期。还讨论了线程同步与锁(同步代码块、`ReentrantLock`)、线程间通信(等待/通知、并发集合)以及实战技巧,如使用线程池、线程安全设计模式和避免死锁。性能优化方面,建议减少锁粒度和使用非阻塞算法。理解这些概念和技术对于编写高效、可靠的多线程程序至关重要。
|
7天前
|
XML Java 测试技术
Java异常处理神器:Guava Throwables类概念与实战
【4月更文挑战第29天】在Java开发中,异常处理是保证程序稳定性和可靠性的关键。Google的Guava库提供了一个强大的工具类Throwables,用于简化和增强异常处理。本篇博客将探讨Throwables类的核心功能及其在实战中的应用。
18 2
|
7天前
|
安全 Java 测试技术
利用Java反射机制提高Spring Boot的代码质量:概念与实战
【4月更文挑战第29天】Java反射机制提供了一种强大的方法来在运行时检查或修改类和对象的行为。在Spring Boot应用中,合理利用反射可以提高代码的灵活性和可维护性。本篇博客将探讨Java反射的核心概念,并展示如何通过反射提高Spring Boot项目的代码质量。
23 0
|
7天前
|
存储 Java 大数据
JAVA:编程的艺术与实战解析
JAVA:编程的艺术与实战解析
17 2
|
9天前
|
监控 Java API
Java 模块化设计:概念与实战应用
【4月更文挑战第27天】模块化设计是现代软件开发的关键,它帮助开发者构建可管理、可维护的大型系统。Java 平台的模块化支持始于 Java 9,引入了一种全新的模块系统。
18 3
|
9天前
|
设计模式 算法 Java
Java 设计模式:探索策略模式的概念和实战应用
【4月更文挑战第27天】策略模式是一种行为设计模式,它允许在运行时选择算法的行为。在 Java 中,策略模式通过定义一系列的算法,并将每一个算法封装起来,并使它们可以互换,这样算法的变化不会影响到使用算法的客户。
17 1
|
9天前
|
Java
Java 事件驱动编程:概念、优势与实战示例
【4月更文挑战第27天】事件驱动编程是一种编程范式,其中程序的执行流程由外部事件的发生而触发或驱动。
17 0
|
9天前
|
Java 程序员
Java 异步编程:概念、优势与实战示例
【4月更文挑战第27天】在现代软件开发中,异步编程是一种重要的编程范式,特别适用于处理长时间运行的任务,如网络通信、文件操作等。
18 0