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

转自: https://juejin.cn/post/6844903903130042376

文末有源代码,非常棒

摘要:

本篇博文是“Java秒杀系统实战系列文章”的第十篇,本篇博文我们将采用RabbitMQ的死信队列的方式处理“用户秒杀成功生成订单后,却迟迟没有支付”的情况,一起来见识一下RabbitMQ死信队列在实际业务环境下的强大之处!内容:对于消息中间件RabbitMQ,Debug其实在前面的篇章中已经简单分享介绍过了,在这里就不再赘述了!在本文我们将采用RabbitMQ的死信队列实现这样的业务需求:“用户在秒杀成功并成功创建一笔订单记录后,理论上应该是执行去支付的操作,但是却存在着一种情况是用户迟迟不肯去支付~至于原因,不得而知!”对于这种场景,各位小伙伴可以在一些商城平台体验一下,即挑选完商品,加入购物车后,点击去结算,这个时候会有个倒计时,提醒你需要在指定的时间内完成付款,否则订单将失效!对于这种业务逻辑的处理,传统的做法是采用“定时器的方式”,定时轮询获取已经超过指定时间的订单,然后执行一系列的处理措施(比如再争取给用户发送短信,提醒超过多长时间订单就要失效了等等。。。),在这个秒杀系统中,我们将借助RabbitMQ死信队列这一组件,对该订单执行“失效”的措施!“死信队列”,顾明思议,是可以延时、延迟一定的时间再处理消息的一种特殊队列,它相对于“普通的队列”而言,可以实现“进入死信队列的消息不立即处理,而是可以等待一定的时间再进行处理”的功能!而普通的队列则不行,即进入队列后的消息会立即被对应的消费者监听消费,如下图所示为普通队列的基本消息模型:而对于“死信队列”,它的构成以及使用相对而言比较复杂一点,在正常情况,死信队列由三大核心组件组成:死信交换机+死信路由+TTL(消息存活时间~非必需的),而死信队列又可以由“面向生产者的基本交换机+基本路由”绑定而成,故而生产者首先是将消息发送至“基本交换机+基本路由”所绑定而成的消息模型中,即间接性地进入到死信队列中,当过了TTL,消息将“挂掉”,从而进入下一个中转站,即“面下那个消费者的死信交换机+死信路由”所绑定而成的消息模型中。如下图所示:下面,我们以实际的代码来构建死信队列的消息模型,并将此消息模型应用到秒杀系统的上述功能模块中。(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后端控制台应用,找到该死信队列,可以看到该死信队列的详细信息,如下图所示:(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,即可看到消息转移到真正的队列,并被真正的消费者监听消费,如下所示: 好了,关于“RabbitMQ死信队列”的介绍以及应用实战本文就暂且介绍到这里了,此种方式可以很灵活对“超时未支付的订单”,进行很好的处理,而且整个过程是“自动、自然”的,而无需人为去手动点击按钮触发了!当然啦,万事万物都并非十全十美的,死信队列也是如此,在一篇文章中我们将介绍此种方式的瑕疵之处,并采用相应的解决方案进行处理!补充:1、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,

 

完整的源代码数据库地址可以来这里下载: https://gitee.com/steadyjack/SpringBoot-SecondKill

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/330116.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

主流Java数据库连接池比较及前瞻

转载自 主流Java数据库连接池比较及前瞻主流数据库连接池 常用的主流开源数据库连接池有C3P0、DBCP、Tomcat Jdbc Pool、BoneCP、Druid等 C3p0: 开源的JDBC连接池&#xff0c;实现了数据源和JNDI绑定&#xff0c;支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibern…

有序数组中查找第一个比target大的数

思路&#xff1a;二分法 时间复杂度&#xff1a;O(logn) 空间复杂度&#xff1a;O(1) 代码&#xff1a; public class Solution{public int search(int[] nums, int target){int lf0, rtn.length-1;while(lf<rt){int mid lf(rt-lf)/2;if(n[mid]>t){rt mid-1;}else{lf…

2020最新Java线程池入门(超详细)

转 https://blog.csdn.net/weixin_43893397/article/details/104361154 【1】代码示例 /*** 线程池测试-自定义线程池创建方式* since 2021/03/23 */ public class ThreadPoolMain2 {public static void main(String[] args) throws Exception {newMethod();}public static…

HDFS的诞生

转载自 HDFS的诞生 1牛刀小试 张大胖找了个实习的工作&#xff0c; 第一天上班Bill师傅给他分了个活儿&#xff1a;日志分析。张大胖拿到了师傅给的日志文件&#xff0c;大概有几十兆&#xff0c;打开一看&#xff0c; 每一行都长得差不多&#xff0c;类似这样&#xff1a;212.…

项目背景怎么描述_产品经理写简历,如何让「项目经验」更出众?

项目经验怎么写更出众&#xff1f;时间长但效果一般的项目经验要不要写&#xff1f;没有项目经验怎么办&#xff1f;本文凭借作者自己长期招聘产品、阅读大量简历所积累的经验解答了这三个问题&#xff0c;希望对你有所帮助。产品经理写简历时&#xff0c;都会通过项目案例来证…

一致性Hash算法原理

前言 当在需要将数据分发到多个数据库/缓存&#xff0c;或将请求分发给多个服务节点时&#xff0c;不可避免的会遇到以下问题&#xff1a; 如何将数据均匀的分散到各个节点中&#xff0c;并且尽量的在加减节点时能使受影响的数据最少。 选择节点的方法 随机放置 从多个节点…

转: java多线程-ThreadPoolExecutor的拒绝策略RejectedExecutionHandler

转自&#xff1a; https://blog.csdn.net/qq_25806863/article/details/71172823 概述 原文地址 http://blog.csdn.net/qq_25806863/article/details/71172823 在分析ThreadPoolExecutor的构造参数时&#xff0c;有一个RejectedExecutionHandler参数。 RejectedExecutionH…

已知两点坐标如何快速增加其他坐标_「测绘精选」坐标转换概述

引言&#xff1a;这篇“坐标转换概述”献给各位&#xff0c;可以对坐标转换有一个大致地、整体地了解。文中有些名词是为了便于表达而自创的&#xff0c;大家不用考据、较真。一、静态坐标和动态坐标(1)静态坐标传统大地测量没有考虑板块运动对坐标的影响。虽然板块运动客观存在…

什么是G1垃圾回收算法

转载自 什么是G1垃圾回收算法为解决CMS算法产生空间碎片和其它一系列的问题缺陷&#xff0c;HotSpot提供了另外一种垃圾回收策略&#xff0c;G1&#xff08;Garbage First&#xff09;算法&#xff0c;通过参数 -XX:UseG1GC来启用&#xff0c;该算法在JDK 7u4版本被正式推出&am…

一文理清RocketMQ顺序消费、重复消费、消息丢失问题

前言 在使用消息队列时不可避免的会遇到顺序消费、重复消费、消息丢失三个问题。在一次面试字节的时候&#xff0c;面试官问到如何保证顺序消费&#xff0c;当时回答不太准确&#xff0c;特意此文回顾如何解决顺序消费、重复消费、消息丢失三个问题。 重复消费 解决重复消费…

一道丧心病狂的java面试题

转载自 一道丧心病狂的java面试题无意中了解到如下题目&#xff0c;觉得蛮好。 题目如下&#xff1a; public class TestSync2 implements Runnable {int b 100; synchronized void m1() throws InterruptedException {b 1000;Thread.sleep(500); //6System.out.pri…

水晶报表图形位置_看了我用Excel做的年度报表,老板直夸好

2020年前5个月&#xff0c;最火爆的莫过于口罩。口罩的整条产业链都变得炙手可热&#xff0c;口罩、口罩机、炒熔喷布、聚丙烯等等相关企业的业务数据往往都是去年的几倍。那我们现在作为一家“表姐牌”的口罩厂的员工&#xff0c;老板叫我用Excel做一个既酷炫又简洁的年度报表…

Mysql优化(三):优化order by

MySQL中的两种排序方式 .通过有序索引顺序扫描直接返回有序数据 因为索引的结构是B树&#xff0c;索引中的数据是按照一定顺序进行排列的&#xff0c;所以在排序查询中如果能利用索引&#xff0c;就能避免额外的排序操作。EXPLAIN分析查询时&#xff0c;Extra显示为Using inde…

漫画:什么是服务熔断

转载自 漫画&#xff1a;什么是服务熔断什么是服务熔断&#xff1f;熔断这一概念来源于电子工程中的断路器&#xff08;Circuit Breaker&#xff09;。在互联网系统中&#xff0c;当下游服务因访问压力过大而响应变慢或失败&#xff0c;上游服务为了保护系统整体的可用性&#…

rabbitmq手动确认ack

【README】 参考 https://blog.csdn.net/u012943767/article/details/79300673 &#xff1b; 【0】声明交换机&#xff0c;队列 与绑定 /*** 交换机&#xff0c;队列声明与绑定 */ public class AckDeclarer {/** 确认交换机 */public static final String ACK_EXCHANGE2 &q…

python图片保存_python读取和保存图片5种方法对比

python读取和保存图片5种方法对比 python中对象之间的赋值是按引用传递的&#xff0c;如果需要拷贝对象&#xff0c;需要用到标准库中的copy模块 方法一&#xff1a;利用 PIL 中的 Image 函数 这个函数读取出来不是 array 格式&#xff0c;这时候需要用 np.asarray(im) 或者 np…

finally块不被执行的情况总结

finally块的作用 通常用于处理善后工作。当try块里出现异常时&#xff0c;会立即跳出try块&#xff0c;到catch块匹配对应的异常&#xff0c;执行catch块里的语句。此时&#xff0c;可能在try块里存在打开的文件没关闭&#xff0c;连接的网络没断开&#xff0c;这部分资源是GC…

rabbitmq生产者基于事务实现发送确认

【README】 业务场景&#xff1a; 业务处理伴随消息的发送&#xff0c;业务处理失败&#xff08;事务回滚&#xff09;后要求消息不发送。 补充1&#xff1a;ACK与CONFIRM的区别 ACK-消费者消费成功后确认&#xff1b;&#xff08;消费者确认已收到&#xff09; CONFIRM-事…

什么是CAP定理

转载自 什么是CAP定理计算机界有很多高大上又难于理解的术语&#xff0c;CAP就是其中之一&#xff0c; 什么一致性&#xff08;Consistency&#xff09;&#xff0c; 可用性&#xff08;Availability&#xff09;&#xff0c; 分区容错性&#xff08;Partition tolerance&#…

python找不到指定的文件夹里_Python环球网在Unix中的指定文件路径中找不到*.txt

我在Windows环境中写了一些文件&#xff0c;我在转换文件时遇到了麻烦。在Windows中&#xff0c;我通常使用类似以下内容读取目录中的所有.txt文件&#xff1a;pathtotxt "C:\\Text Data\\EJC\\Philosophical Transactions 1665-1678\\*\\*.txt" for file in glob.g…