什么是限时订单?在各种电商网站下订单后会保留一个时间段,时间段内未支付则自动将订单状态设置为已过期,这种订单称之为限时订单。

代码地址:https://gitee.com/hankin_chj/rocketmq-platform.git (rocket-delay-order)

一、如何实现限时订单

1、限时订单的流程

电商平台都会包含以下 5 种状态。

待付款:代表买家下单了但是还没有付款。

待发货:代表买家付款了卖家还没有发货。

已发货:代表卖家已经发货并寄出商品了。

已完成:代表买家已经确认收到货了。

已关闭:代表订单过期了买家也没付款、或者卖家关闭了订单。

2、限时订单实现的关键

我们可以看到,订单中的很多状态都是可以用户触发的,唯独订单过期了买家也没付款我们需要自动的把订单给关闭,这个操作是没有用户或者是人工干预的,所以限时订单的关键就是如何检查订单状态,如果订单过期了则把该订单设置为关闭状态。

3、轮询数据库?

轮询数据库在实现限时订单上是可行的,而且实现起来很简单,写个定时器去每隔一段时间扫描数据库,检查到订单过期了,做适当的业务处理。

但是轮询会带来什么问题?

1)轮询大部分时间其实是在做无用功,我们假设一张订单是45分钟过期,每1分钟我们扫描一次,对这张订单来说,要扫描45次以后,才会检查到这张订单过期,这就意味着数据库的资源(连接,IO)被白白浪费了;

2)处理上的不及时,一个待支付的电影票订单我们假设是12:00:35过期,但是上次扫描的时间是 12:00:30,那么这个订单实际的过期时间是12:01:30,和我本来的过期时间差了55秒钟。放在业务上,会带来什么问题?这张电影票,假设是最后一张,有个人12:00:55来买票,买得到吗?当然买不到了。那么这张电影票很有可能就浪费了。如果缩短扫描的时间间隔,第一只能改善不能解决,第二,又会对数据库造成更大的压力。 那么我们能否有种机制,不用定时扫描,当订单到期了,自然通知我们的应用去处理这些到期的订单呢?

4、Java本身的提供的解决方案

java其实已经为我们提供了解决问题的方法。我们想要处理限时支付的问题,肯定是要有个地方保存这些限时订单的信息的,意味着我们需要一个容器,于是我们在Java容器中去寻找Map? List? Queue?

看看java为我们提供的容器,我们是个多线程下的应用,会有多个用户同时下订单,所以所有并发不安全的容器首先被排除,并发安全的容器有哪些?java在阻塞队列里为我们提供了一种叫延迟队列delayQueue的容器,刚好可以为我们解决问题。

DelayQueue:阻塞队列(先进先出)

1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。

2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。延迟期满时才能从中提取元素(光队列里有元素还不行)。

Delayed接口使对象成为延迟对象,它使存放在DelayQueue类中的对象具有了激活日期,该接口强制实现下列两个方法:

• CompareTo(Delayed o):Delayed接口继承了Comparable接口,因此有了这个方法,让元素按激活日期排队。

• getDelay(TimeUnit unit):这个方法返回到激活日期的剩余时间,时间单位由单位参数指定。 阻塞队列更多详情,参考《并发编程》。

5、架构师应该多考虑一点

架构师在设计和实现系统时需要考虑些什么?

功能:这个没什么好说,实现一个应用,连基本的功能都没实现,要这个应用有何用?

高性能:能不能尽快的为用户提供服务和能为多少用户同时提供服务,性能这个东西是个很综合性的东西,从前端到后端,从架构(缓存机制、异步机制)到 web 容器、数据库本身再到虚拟机到算法、java 代码、sql语句的编写,全部都对性能有影响。如何提升性能,要建立在充分的性能测试的基础上,然后一个个的去解决性能瓶颈。对上面提到的应用来讲,我们不想去轮询数据库,其实跟性能有非常大的关系。

高可用:应用正确处理业务,服务用户的时间,这个时间当然是越长越好,希望可以7*24小时。而且哪怕服务器出现了升级,宕机等等情况下,能够以最短的时间恢复,为用户继续服务,但是实际过程中没有哪个网站可以说做到100%,不管是Google、FaceBook、阿里、腾讯,一般来说可以做到99.99%的可用性,已经是相当厉害了,这个水平大概就是一个服务在一年可以做到只有50分钟不可用。这个需要技术、资金、技术人员的水平和责任心,还要运气。

高伸缩:伸缩性是指通过不断向集群中加入服务器的手段来缓解不断上升的用户并发访问压力和不断增长的数据存储需求。就像弹簧一样挂东西一样,用户多,伸一点,用户少,缩一点。衡量架构是否高伸缩性的主要标准就是是否可用多台服务器构建集群,是否容易向集群中添加新的服务器。加入新的服务器后是否可以提供和原来服务器无差别的服务。集群中可容纳的总的服务器数量是否有限制。

高扩展:的主要标准就是在网站增加新的业务产品时,是否可以实现对现有产品透明无影响,不需要任何改动或者很少改动既有业务功能就可以上线新产品。比如购买电影票的应用,用户购买电影票,现在我们要增加一个功能,用户买了票后,随机抽取用户送限量周边。怎么做到不改动用户下订单功能的基础上增加这个功能。熟悉设计模式的同学,应该很眼熟,这是设计模式中的开闭原则(对扩展开放,对修改关闭)在架构层面的一个原则。

6、从系统可用性角度考虑

应用重启带来的问题

保存在Queue中的订单会丢失,这些丢失的订单会在什么时候过期,因为队列里已经没有这个订单了,无法检查了,这些订单就得不到处理了。

已过期的订单不会被处理,在应用的重启阶段,可能会有一部分订单过期,这部分过期未支付的订单同样也得不到处理,会一直放在数据库里,过期未支付订单所对应的资源比如电影票所对应的座位,就不能被释放出来,让别的用户来购买。

解决之道 :在系统启动时另行处理

7、从系统伸缩性角度考虑

集群化了会带来什么问题?应用之间会相互抢夺订单,特别是在应用重启的时候,重新启动的那个应用会把不属于自己的订单,也全部加载到自己的队列里去,一是造成内存的浪费,二来会造成订单的重复处理,而且加大了数据库的压力。

解决方案:让应用分区处理

1)给每台服务器编号,然后在订单表里登记每条订单的服务器编号;

2)更简单的,在订单表里登记每台服务器的IP地址,修改相应的sql语句即可。

几个问题:如果有一台服务器挂了怎么办?如果是某台服务器下线或者宕机,起不来怎么搞?这个还是还是稍微有点麻烦,需要人工干预一下,手动把库里的每条订单数据的服务器编号改为目前正常的服务器的编号,不过也就是一条sql语句的事,然后想办法让正常的服务器进行处理(重启正常的服务器)。

二、RocketMQ实现限时订单

引入RocketMQ使用延时消息,一举解决我们限时订单的伸缩性和扩展性问题。

1、延时消息

概念介绍

延时消息:Producer将消息发送到消息队列RocketMQ服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到Consumer进行消费,该消息即延时消息。

适用场景

消息生产和消费有时间窗口要求:比如在电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条延时消息。这条消息将会在30分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付;如支付未完成,则关闭订单,如已完成支付则忽略。

2、核心的代码

整个代码见delayOrder包,Git地址:https://gitee.com/hankin_chj/rocketmq-platform.git

2.1、配置部分

<!-- rocketMq生产者配置 -->
<bean id="rocketMQProducer" class="com.chj.service.mq.RocketMQProducer"
     init-method="init" destroy-method="destroy">
   <property name="producerGroup" value="DelayOrderProducer" />
   <property name="namesrvAddr" value="127.0.0.1:9876" />
</bean>
<!-- 消费者监听 -->
<bean id="messageListeners" class="com.chj.service.mq.MessageListenerImpl"></bean>
<!-- 消费者配置 -->
<bean id="rocketmqConsumer" class="org.apache.rocketmq.client.consumer.DefaultMQPushConsumer"
     init-method="start" destroy-method="shutdown">
   <property name="consumerGroup" value="TimeOrderGroup" />
   <property name="namesrvAddr" value="127.0.0.1:9876" />
   <property name="messageModel" value="CLUSTERING" />
   <property name="consumeFromWhere" value="CONSUME_FROM_LAST_OFFSET" />
   <property name="messageListener" ref="messageListeners" />
   <property name="subscription">
      <map>
         <entry key="TimeOrder" value="*" />
      </map>
   </property>
</bean>

订单处理的控制器代码实现:

@Controller
public class OrderController {private static final String SUCCESS = "suc";private static final String FAILUER = "failure";@Autowiredprivate SaveOrder saveOrder;@RequestMapping("/index")public String userOrder(){return "order";}//保存订单(界面生成几个订单)@RequestMapping("/submitOrder")@ResponseBodypublic String saveOrder(@RequestParam("orderNumber")int orderNumber){saveOrder.insertOrders(orderNumber);return SUCCESS;}
}

2.2、核心代码实现

1)保存订单SaveOrder.java的时候,作为生产者往消息队列里推入订单,核心RocketMQProducer,这个类当然是要继承IDelayOrder,同时也是RocketMQ的生产者。

订单相关的服务SaveOrder.java代码实现:

@Service
public class SaveOrder {private Logger logger = LoggerFactory.getLogger(SaveOrder.class);public final static short UNPAY = 0;public final static short PAYED = 1;public final static short EXPIRED = -1;@Autowiredprivate OrderExpDao orderExpDao;@Autowired@Qualifier("rocketmq")private IDelayOrder delayOrder;/*** 接收前端页面参数,生成订单* @param orderNumber 订单个数*/public void insertOrders(int orderNumber){Random r = new Random();OrderExp orderExp ;for(int i=0;i<orderNumber;i++) {//这个是设置延时消息的属性//"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"  18个等级long expire_duration =30;long expireTime =4;orderExp = new OrderExp();String orderNo = "DD00_30S";orderExp.setOrderNo(orderNo);orderExp.setOrderNote("享学订单——"+orderNo);orderExp.setOrderStatus(UNPAY);orderExpDao.insertDelayOrder(orderExp,expire_duration);logger.info("保存订单到DB:"+orderNo);//TODO 这里需要把订单信息存入RocketMQdelayOrder.orderDelay(orderExp, expireTime);}}@PostConstructpublic void initDelayOrder() {logger.info("系统启动,扫描表中过期未支付的订单并处理.........");int counts = orderExpDao.updateExpireOrders();logger.info("系统启动,处理了表中["+counts+"]个过期未支付的订单!");List<OrderExp> orderList = orderExpDao.selectUnPayOrders();logger.info("系统启动,发现了表中还有["+orderList.size()+"]个未到期未支付的订单!推入检查队列准备到期检查....");for(OrderExp order:orderList) {long expireTime = order.getExpireTime().getTime()-(new Date().getTime());delayOrder.orderDelay(order, expireTime);}}
}

消息队列的实现RocketMQProducer:

@Service
@Qualifier("rocketmq")
public class RocketMQProducer implements IDelayOrder {@Autowiredprivate DlyOrderProcessor processDelayOrder;private Thread takeOrder;private static final Logger logger = LoggerFactory.getLogger(RocketMQProducer.class);private DefaultMQProducer defaultMQProducer;private String producerGroup;private String namesrvAddr;@PostConstructpublic void init() throws MQClientException {this.defaultMQProducer = new DefaultMQProducer(this.producerGroup);defaultMQProducer.setNamesrvAddr(this.namesrvAddr);defaultMQProducer.start();logger.info("rocketMQ初始化生产者完成[producerGroup:" + producerGroup + "]");}@PreDestroypublic void destroy() {defaultMQProducer.shutdown();logger.info("rocketMQ生产者[producerGroup: " + producerGroup + "]已停止");}public DefaultMQProducer getDefaultMQProducer() {return defaultMQProducer;}public void setProducerGroup(String producerGroup) {this.producerGroup = producerGroup;}public void setNamesrvAddr(String namesrvAddr) {this.namesrvAddr = namesrvAddr;}public void orderDelay(OrderExp order, long timeLevel) {try {//TODO 使用Gson序列化Gson gson = new Gson();String txtMsg = gson.toJson(order);//TODO 发送延时消息Message msg = new Message("TimeOrder", null, txtMsg.getBytes());//这个是设置延时消息的属性//"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"  18个等级msg.setDelayTimeLevel((int)timeLevel);SendResult result = defaultMQProducer.send(msg);if(result.getSendStatus() !=null && result.getSendStatus()== SendStatus.SEND_OK){System.out.println("订单被推入延迟队列,订单详情:"+order);logger.info("订单被推入延迟队列,订单详情:"+order);}else{logger.error("订单推入RocketMq失败,订单详情:"+order+"SendStatus:"+result.getSendStatus());}} catch (Exception e) {logger.error("单推入RocketMq失败,失败详情:"+e.toString());}}
}

2)消息队列会把延时的订单发给消费者MessageListenerImpl,它是一个RocketMQ的消费者监听,它来负责检查订单是否过期,有消息过来,证明消息订单过期了,则把订单状态修改为过期订单。RocketMQ本身又如何保证可用性和伸缩性?这个就需要RocketMQ的主从同步(HA机制)。

处理消息队列返回的延时订单MessageListenerImpl:

@Service
public class MessageListenerImpl implements MessageListenerConcurrently {private Logger logger = LoggerFactory.getLogger(MessageListenerImpl.class);@Autowiredprivate DlyOrderProcessor processDlyOrder;public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {for (MessageExt msg : msgs) {try {//TODO 使用GSON反序列化String txtMsg = new String(msg.getBody());Gson gson = new Gson();System.out.println("接收到RocketMQ的消息:"+txtMsg);OrderExp order = (OrderExp)gson.fromJson(txtMsg, OrderExp.class);//TODO 修改订单状态为过期if(order.getId()!=null){processDlyOrder.checkDelayOrder(order);}} catch (Exception e) {e.printStackTrace();return ConsumeConcurrentlyStatus.RECONSUME_LATER;}}// 如果没有异常会认为都成功消费return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;}
}

处理过期订单的服务:

@Service
public class DlyOrderProcessor {private Logger logger = LoggerFactory.getLogger(DlyOrderProcessor.class);@Autowiredprivate OrderExpDao orderExpDao;/*** 检查数据库中指定id的订单的状态,如果为未支付,则修改为已过期* */public void checkDelayOrder(OrderExp record) {OrderExp dbOrder = orderExpDao.selectByPrimaryKey(record.getId());if(dbOrder.getOrderStatus()==SaveOrder.UNPAY) {logger.info("订单【"+record+"】未支付已过期,需要更改为过期订单!");orderExpDao.updateExpireOrder(record.getId());}else {logger.info("已支付订单【"+record+"】,无需修改!");}}
}

第五章 限时订单实战笔记相关推荐

  1. 详细讲解:RocketMQ的限时订单实战与RocketMQ的源码分析!

    目录 一.限时订单实战 1.1.什么是限时订单 1.2.如何实现限时订单 1.2.1.限时订单的流程 1.2.2.限时订单实现的关键 1.2.3.轮询数据库? 1.2.4.Java 本身的提供的解决方 ...

  2. 计算机科学导论第五章计算机组成 学习笔记+习题答案

    第五章 计算机组成 通过本章的学习,学生应该能够∶ 列出计算机的三个子系统; 描述计算机中央处理单元(CPU)的作用; 描述典型计算机中指令周期的取指令-译码-执行阶段; 描述主存和它的地址空间; 区 ...

  3. 《数据科学家养成手册》第五章---矛盾的世界笔记

    (1)世界中存在继续多相互矛盾的事物.矛盾通常是指事物相互影响和对立的关系.比如太阳的东升的同时没有办法西落. (2)数学中的虚数是与现实世界的实数相对的.虚数和实数的结合形成复数,通过复数构建的矢量 ...

  4. 【hadoop权威指南第四版】第五章MR应用【笔记+代码】

    5.1 API的配置 配置文件 <?xml version="1.0"?> <configuration> <property> <nam ...

  5. 学习笔记----周志华《机器学习》第五章(神经网络)(一)

    周志华的<机器学习>算作一本入门的宝书.本文是对周志华的机器学习第五章神经网络的学习笔记.在第五章主要涉及的内容:神经网络.常见激活函数.感知机.多层前馈神经网络 .反向传播算法(BP算法 ...

  6. 第五章 Global and Detailed Placement [VLSL Physical Design 学习笔记 ]

    文章目录 5.1 简介 5.2 术语和定义 5.3 优化目标 全定制布局的布线 标准网格的布线 门阵列设计(Gate-array designs) 5.4 布线区域的表示(Representation ...

  7. 《管理学》第五章 组织

    <管理学>第五章 组织 前言 今天上课讲的内容是第二章的内容,由于我之前做过第二章的笔记,所以上课听起来思路清晰,学有余力,于是看了看组织部分的内容,发现不错,于是先把书本笔记记录 下来: ...

  8. 第三章:zigbee学习笔记之物理层和mac层帧格式分析

    本文原地址:https://blog.csdn.net/tainjau/article/details/81634681 IEEE802.15.4工作组致力于无线个人区域网络(wireless per ...

  9. R语言实战笔记--第十五章 处理缺失数据

    R语言实战笔记–第十五章 处理缺失数据 标签(空格分隔): R语言 处理缺失数据 VIM mice 缺失值(NA),是导致我们计算错误的一大来源,处理缺失数据在实际的应用中有着较为重要的作用. 基本方 ...

最新文章

  1. MonkeyRunner实例及使用说明
  2. Java中String相关类型的区别
  3. PyTorch 1.0 中文文档:数据类型信息
  4. Python之网络爬虫(爬虫基本认知、网络爬虫之路)
  5. scratch素材准备
  6. 真实的网络赚钱经历:另类推广引流操作CPA!
  7. 命令改计算机用户名和密码,利用NET命令添加、修改用户账户信息
  8. 谭浩强《C程序设计》(第四版)错误不完全汇集
  9. dpdk LRO功能总结
  10. mysql redo查看_mysql redo
  11. 国产DeFi,困在币价里
  12. 基于java springboot android安卓点餐外卖系统源码(毕设)
  13. Quartus II下载器件库(新)
  14. 软件著作权 -- 注册+实名认证
  15. 结课作业:机械设计制造及其自动化文献综述
  16. 微分和导数的关系是什么?两者的几何意义有什么不同?为什么要定义微分 ?...
  17. 腾讯云轻量服务器8核16G14M带宽性能评测CPU内存带宽系统盘
  18. win10开机从10秒变成3分钟怎么办
  19. 戴尔服务器带液晶显示,戴尔机架R730外观设计_戴尔服务器_服务器评测与技术-中关村在线...
  20. 蜜蜂剪辑会员特权_会员有其特权

热门文章

  1. 二进制转十进制(c语言)
  2. android 多语言设置没法切到印尼语
  3. 画图实现考试成绩管理系统
  4. Golang依赖下载安装失败解决方法
  5. datagridview删除第一列空白
  6. 新手小白怎么学UI设计 推荐学习路线是什么
  7. c语言16进制直接文本输出,c语言问题求助:将16进制文本转10进制并输出文本
  8. 一定用好自己的应届毕业生身份
  9. 10个挖掘edusrc常用工具、脚本
  10. 17天搞定GRE词汇--留留学论坛