前面几篇我们从限流角度,缓存角度来优化了用户下单的速度,减少了服务器和数据库的压力。这些处理对于一个秒杀系统都是非常重要的,并且效果立竿见影,那还有什么操作也能有立竿见影的效果呢?答案是下单的异步处理。

前期概述

在秒杀系统用户进行抢购的过程中,由于在同一时间会有大量请求涌入服务器,如果每个请求都立即访问数据库进行扣减库存和写入订单的操作,对数据库的压力是巨大的。我们可以将每一条秒杀的请求存入消息队列(例如RabbitMQ)中,放入消息队列后,给用户返回类似“抢购请求发送成功”的结果。而在消息队列中,我们将收到的下订单请求一个个的写入数据库中,比起多线程同步修改数据库的操作,大大缓解了数据库的连接压力,最主要的好处就表现在数据库连接的减少。这种实现可以理解为是一中流量削峰,让数据库按照他的处理能力,从消息队列中拿取消息进行处理。接下来我们用代码来具体来实现一下订单的异步处理。

订单异步处理

代码编写

在原来代码的基础上,OrderController中增加接口createUserOrderWithMq,代码如下:

/*** 下单接口:异步处理订单* @param sid* @return*/
@RequestMapping(value = "/createUserOrderWithMq", method = {RequestMethod.GET})
@ResponseBody
public String createUserOrderWithMq(@RequestParam(value = "sid") Integer sid,@RequestParam(value = "userId") Integer userId) {try {// 检查缓存中该用户是否已经下单过Boolean hasOrder = orderService.checkUserOrderInfoInCache(sid, userId);if (hasOrder != null && hasOrder) {LOGGER.info("该用户已经抢购过");return "你已经抢购过了,不要太贪心.....";}// 没有下单过,检查缓存中商品是否还有库存LOGGER.info("没有抢购过,检查缓存中商品是否还有库存");Integer count = stockService.getStockCount(sid);if (count == 0) {return "秒杀请求失败,库存不足.....";}// 有库存,则将用户id和商品id封装为消息体传给消息队列处理// 注意这里的有库存和已经下单都是缓存中的结论,存在不可靠性,在消息队列中会查表再次验证LOGGER.info("有库存:[{}]", count);JSONObject jsonObject = new JSONObject();jsonObject.put("sid", sid);jsonObject.put("userId", userId);sendToOrderQueue(jsonObject.toJSONString());return "秒杀请求提交成功";} catch (Exception e) {LOGGER.error("下单接口:异步处理订单异常:", e);return "秒杀请求失败,服务器正忙.....";}
}

createUserOrderWithMq接口整体流程如下:

  1. 检查缓存中该用户是否已经下单过,在消息队列下单成功后写入redis;
  2. 没有下单过,检查缓存中商品是否还有库存;
  3. 缓存中如果有库存,则将用户id和商品id封装为消息体传给消息队列处理;
  4. 这里的有库存和已经下单都是缓存中的结论,存在不可靠性,在消息队列中会查表再次验证,作为兜底逻辑。

我们新建一个消息队列,采用之前使用过的RabbitMQ,写一个RabbitMqConfig:

@Configuration
public class RabbitMqConfig {@Beanpublic Queue orderQueue() {return new Queue("orderQueue");}
}

添加一个消费者:

@Component
@RabbitListener(queues = "orderQueue")
public class OrderMqReceiver {private static final Logger LOGGER = LoggerFactory.getLogger(OrderMqReceiver.class);@Autowiredprivate StockService stockService;@Autowiredprivate OrderService orderService;@RabbitHandlerpublic void process(String message) {LOGGER.info("OrderMqReceiver收到消息开始用户下单流程: " + message);JSONObject jsonObject = JSONObject.parseObject(message);try {orderService.createOrderByMq(jsonObject.getInteger("sid"),jsonObject.getInteger("userId"));} catch (Exception e) {LOGGER.error("消息处理异常:", e);}}
}

真正的下单的操作,在service中完成,我们在orderService中新建createOrderByMq方法:


@Override
public void createOrderByMq(Integer sid, Integer userId) throws Exception {Stock stock;//校验库存(不要学我在trycatch中做逻辑处理,这样是不优雅的。这里这样处理是为了兼容之前的秒杀系统文章)try {stock = checkStock(sid);} catch (Exception e) {LOGGER.info("库存不足!");return;}//乐观锁更新库存boolean updateStock = saleStockOptimistic(stock);if (!updateStock) {LOGGER.warn("扣减库存失败,库存已经为0");return;}LOGGER.info("扣减库存成功,剩余库存:[{}]", stock.getCount() - stock.getSale() - 1);stockService.delStockCountCache(sid);LOGGER.info("删除库存缓存");//创建订单LOGGER.info("写入订单至数据库");createOrderWithUserInfoInDB(stock, userId);LOGGER.info("写入订单至缓存供查询");createOrderWithUserInfoInCache(stock, userId);LOGGER.info("下单完成");
}

可以看到我们真正的下单的操作流程为:

  1. 校验数据库库存
  2. 乐观锁更新库存(其他之前讲到的锁也可以)
  3. 写入订单至数据库
  4. 写入订单和用户信息至缓存供查询:写入后,在外层接口便可以通过判断redis中是否存在用户和商品的抢购信息,来直接给用户返回“你已经抢购过”的消息。

我们这里再redis中使用了set集合记录商品和用户的关系,

    @Overridepublic Boolean checkUserOrderInfoInCache(Integer sid, Integer userId) throws Exception {String key = CacheKey.USER_HAS_ORDER.getKey() + "_" + sid;LOGGER.info("检查用户Id:[{}] 是否抢购过商品Id:[{}] 检查Key:[{}]", userId, sid, key);return stringRedisTemplate.opsForSet().isMember(key, userId.toString());}

key是商品id,value是用户id的集合,这样有一些不合理的地方:

  • 这种结构默认了一个用户只能抢购一次这个商品
  • 使用set集合,在用户过多后,每次检查需要遍历set,用户过多有性能问题

大家知道需要做这种操作就好,具体如何在生产环境的redis中存储这种关系,大家可以深入优化下,我这里只是做个示范。整个上述实现只考虑最精简的流程,不把前几篇文章的限流,验证用户等加入进来,并且默认考虑的是每个用户抢购一个商品就不再允许抢购,我的想法是保证每篇文章的独立性和代码的任务最小化,至于最后的整合我相信小伙伴们自己可以做到。

流程测试

写完了代码以后接下来让我们实际来操作验证一下。为了对比,这里我们使用非异步与异步下单来进行结果的对比,这样也更能看出异步下单的好处。这里为了方便,我把用户购买限制先取消掉,不然还要来模拟多个用户id,直接把接口中的检查缓存中该用户是否已经下单过的检验注释掉即可。

使用常规的非异步下单接口,模拟1000个用户同时抢购,商品库存为500个。可以看到,非异步的情况下,吞吐量是142.8个请求/秒:

而异步情况下,吞吐量为200.7个请求/秒:


这里截图了在500个库存刚刚好消耗完的时候的日志,可以看到,一旦库存没有了,消息队列就完成不了扣减库存的操作,就不会将订单写入数据库,也不会向缓存中记录用户已经购买了该商品的消息。

那么问题来了,我们实现了上面的异步处理后,用户那边得到的结果是怎么样的呢?用户点击了提交订单,收到了消息:您的订单已经提交成功。然后用户啥也没看见,也没有订单号,用户开始慌了,点到了自己的个人中心——已付款。发现居然没有订单!(因为可能还在队列中处理)这样的话,用户可能马上就要开始投诉了!太不人性化了,我们不能只为了开发方便,舍弃了用户体验!所以我们要改进一下,如何改进呢?其实很简单:

  • 让前端在提交订单后,显示一个“排队中”;
  • 同时,前端不断请求 检查用户和商品是否已经有订单 的接口,如果得到订单已经处理完成的消息,页面跳转抢购成功。

实现起来,我们只要在后端加一个独立的接口:

/*** 检查缓存中用户是否已经生成订单* @param sid* @return*/
@RequestMapping(value = "/checkOrderByUserIdInCache", method = {RequestMethod.GET})
@ResponseBody
public String checkOrderByUserIdInCache(@RequestParam(value = "sid") Integer sid,@RequestParam(value = "userId") Integer userId) {// 检查缓存中该用户是否已经下单过try {Boolean hasOrder = orderService.checkUserOrderInfoInCache(sid, userId);if (hasOrder != null && hasOrder) {return "恭喜您,已经抢购成功!";}} catch (Exception e) {LOGGER.error("检查订单异常:", e);}return "很抱歉,你的订单尚未生成,继续排队吧您嘞。";
}

我们来试验一下,首先我们用postman请求两次下单的接口,发现缓存里面已经有数据,但是实际数据库中没有数据,即没有产生实际的订单:


但是却给用户返回的已经秒杀成功,这样显然会让买家产生疑问。


我们加入checkOrderByUserIdInCache接口以后,前端不停调用获取真实的订单信息,第一次请求时:


第二次请求时:

再第二次请求时先去调用以下接口,将以下信息返回

一直刷刷刷接口,数据成功插入以后,接口返回”恭喜您,抢购成功“,这个时候将信息返回给前端进行展示,如下图:

整个流程就走完了。整个秒杀下订单的主流程我们全部介绍完了。当然里面很多东西都非常基础,不过之前也说了这只是一个简单的秒杀系统,供大家入门理解使用,更复杂的业务大家可以在原来的基础上慢慢增加。

猜你感兴趣
教你从0到1搭建秒杀系统-防超卖
教你从0到1搭建秒杀系统-限流
教你从0到1搭建秒杀系统-抢购接口隐藏与单用户限制频率
教你从0到1搭建秒杀系统-缓存与数据库双写一致
教你从0到1搭建秒杀系统-Canal快速入门(番外篇)
教你从0到1搭建秒杀系统-订单异步处理

更多文章请点击:更多…

教你从0到1搭建秒杀系统-订单异步处理相关推荐

  1. 教你从0到1搭建秒杀系统-Canal快速入门(番外篇)

    Canal用途很广,并且上手非常简单,小伙伴们在平时完成公司的需求时,很有可能会用到.本篇介绍一下数据库中间件Canal的使用. 很多时候为了缩短调用延时,我们会对部分接口数据加入了缓存.一旦这些数据 ...

  2. 教你从0到1搭建秒杀系统-缓存与数据库双写一致

    本文是秒杀系统的第四篇,我们来讨论秒杀系统中缓存热点数据的问题,进一步延伸到数据库和缓存的双写一致性问题. 在秒杀实际的业务中,一定有很多需要做缓存的场景,比如售卖的商品,包括名称,详情等.访问量很大 ...

  3. 教你从0到1搭建秒杀系统-抢购接口隐藏与单用户限制频率

    在前两篇文章的介绍下,我们完成了防止超卖商品和抢购接口的限流,已经能够防止大流量把我们的服务器直接搞炸,这篇文章中,我们要开始关心一些细节问题.对于稍微懂点电脑的,点击F12打开浏览器的控制台,就能在 ...

  4. 教你从0到1搭建秒杀系统-限流

    本文是秒杀系统的第二篇,主要讲解接口限流措施.接口限流其实定义也非常广,接口限流本身也是系统安全防护的一种措施,在面临高并发的请购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力,尤其 ...

  5. 教你从0到1搭建秒杀系统-防超卖

    各位读者好,最近笔者学了很多东西,其实都想跟大家进行分享,奈何需要将所学习的知识整理出来需要耗费大量的时间,包括总结,或各种图形以及写代码示例,所以可能更新的速度会比较慢.但大家放心,只要有时间我就会 ...

  6. 如何从0到1搭建物联网系统?

    如何从0到1搭建物联网系统? 2019年是一个好的开端,在互联网行业混迹3年,充分理解互联网行业关于用户思维.平台思维的诠释后,以及对于敏捷研发.协同工作等新思想新管理模式实践后,我准备回身物联网产业 ...

  7. 手把手教你从0到1搭建vue3+ts+vite+element-plus简易后台管理系统

    准备工作 首先请确保你的node.js版本>=12.0.0 因为vite的兼容性,Vite 需要 Node.js 版本 >= 12.0.0. 如果你不知道你的node.js的版本是多少,请 ...

  8. 手把手教你从0到1搭建web ui自动化框架(python3+selenium3+pytest)

    -前期准备 -环境 -实战: 从0开始 前期准备 为更好的学习自动化框架搭建,你需要提前了解以下知识: python基础知识 pytest单元测试框架 PO模式 selenium使用 环境 本次我们自 ...

  9. 万字干货:教新手从0到1搭建完整的增长数据体系(30图)

    本文由作者 杨三季 于社区发布 在实际的业务中,大多数人可能只会遇到以下一种或几种常见的场景,并且对于各个细分场景,要解决的问题和关注的重点都是不一样的. 场景一: 你刚加入一个成熟产品的用户增长部门 ...

最新文章

  1. linux系统被***后处理经历
  2. [MySQL FAQ]系列 -- mysqldump出错一例:打开文件数太多
  3. php 情书,PHP好玩的代码一(笛卡尔的情书)
  4. redis集群学习一些记录
  5. Nginx详细安装部署教程
  6. jQuery disable 的应用
  7. npm install的代理问题
  8. 外设驱动库开发笔记0:EPD总体设计
  9. Scala学习第一篇
  10. 获取android com包名,Android系统中获取进程(和顶端包名)
  11. jeecg中的树形控件demo
  12. java 对象查找_通过Java中的参数集查找最合适的对象
  13. 使用NVIDIA端到端深度学习平台进行缺陷自动检测
  14. 极客大学架构师训练营 微服务架构 Service Mesh 服务网格 RPC 协议实现原理 Dubbo 通讯协议 第19课 听课总结
  15. 计算机数字雨代码,cmd命令数字雨教程
  16. 输入圆半径 计算 直径 周长 面积
  17. 全景视频拼接的关键技术与发展优势、作用、应用。
  18. svm兵王_飞彩网福利彩
  19. ElasticSearch7.x 从部署到开发
  20. puzzle(0133)黄豆计划、线路迷宫

热门文章

  1. php html class,如何使用HTML span标签的class属性?这里有关于class属性的详解
  2. 基于ncat的简易web服务器
  3. 题目2:隐式图的搜索问题(A*算法解决八数码)代码实现
  4. 服务器被入侵了?反手溯源出入侵者画像【网络安全】
  5. 深入分析H2数据库控制台中无需身份验证的RCE漏洞
  6. C++获取windows桌面的路径
  7. 020 Android之so文件动态调试
  8. 【Laravel Cache】 配置redis 存储缓存,通俗易懂,一次就掌握
  9. 【MySQL】 批量修改数据表和数据表中所有字段的字符集
  10. 152. 城市游戏【单调栈】