最近出于公司业务需要,做了拼团抢购,秒杀的业务。

秒杀系统场景特点

  • 秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
  • 秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
  • 秒杀业务流程比较简单,一般就是下订单减库存。
  • 秒杀的业务场景跟其他业务场景不一样,主要是秒杀的瞬间,并发非常大,如何针对此大并发是我们需要取解决的。秒杀业务,是典型的短时大量突发访问在瞬间涌入,造成服务器瘫痪,宕机,用户体验差,想必大家都经历过早期春运抢火车票的痛。

秒杀为什么会成为技术难点,系统瓶颈

秒杀与其他业务最大的区别在于,在秒杀的瞬间,系统的并发量和吞吐量会非常大,与此同时,网络的流量也会瞬间变大。会导致访问变慢、商品超卖等问题。

访问慢不需要解释,我们来看看商品超卖现象是怎么产生的

  • 这个图,其实很清晰了,假设订单系统部署两台机器上,不同的用户都要同时买10台iphone,分别发了一个请求给订单系统。

  • 接着每个订单系统实例都去数据库里查了一下,当前iphone库存是12台。 俩大兄弟一看,乐了,12台库存大于了要买的10台数量啊!

  • 于是乎,每个订单系统实例都发送SQL到数据库里下单,然后扣减了10个库存,其中一个将库存从12台扣减为2台,另外一个将库存从2台扣减为-8台。

  • 现在完了,库存出现了负数!问题来了,没有20台iphone发给两个用户啊!

怎么做

对于系统并发量变大问题

  • 接口限流防刷,限制用户在单位时间内访问接口的频率
  • 这里的核心在于如何在大并发的情况下保证数据库能扛得住压力,因为大并发的瓶颈在于数据库。如果用户的请求直接从前端传到数据库,显然,数据库是无法承受几十万上百万甚至上千万的并发量的。因此,我们能做的只能是减少对数据库的访问。例如,前端发出了100万个请求,通过我们的处理,最终只有10个会访问数据库,这样就会大大提升系统性能。再针对秒杀这种场景,因为秒杀商品的数量是有限的,因此这种做法刚好适用。

怎么解决库存超卖问题?

  • 如果商品数量比较多,比如1万件商品参与秒杀,那么就有1万*10=10万个请求并发去访问数据库,数据库的压力还是会很大,直接读数据库的库存,可能造成超卖。这里就用到了另外一个非常重要的组件:消息队列。我们不是把请求直接去访问数据库,而是先把请求写到消息队列中,做一个缓存,然后再去慢慢的更新数据库。这样做之后,前端用户的请求可能不会立即得到响应是成功还是失败,很可能得到的是一个排队中的返回值,这个时候,需要客户端去服务端轮询,因为我们不能保证一定就秒杀成功了。
  • 这样处理以后,我们的应用是可以很简单的进行分布式横向扩展的,以应对更大的并发。

笔者关于秒杀架构的设计理念

  1. 限流: 鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。(限制用户在单位时间内访问接口的频率)。
  2. 削峰:对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。(利用消息队列进行异步处理,流量削峰)。
  3. 异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。当服务端出队,生成订单以后,把用户ID和商品ID写到缓存中,来应对客户端的轮询就可以了。
  4. 内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。(利用redis减少对数据库的访问)。

为什么要这么做:
假如,我们有10W用户同时抢10台手机,服务层并发请求压力至少为10W。

采用消息队列缓存请求:既然服务层知道库存只有10台手机,那完全没有必要把10W个请求都传递到数据库,那么可以先把这些请求都写到消息队列缓存一下,数据库层订阅消息减库存,减库存成功的请求返回秒杀成功,失败的返回秒杀结束。

利用缓存应对读请求:对类似于12306等购票业务和商品秒杀场景,是典型的读多写少业务,大部分请求是查询请求,所以可以利用缓存分担数据库压力。

利用缓存应对写请求:缓存也是可以应对写请求的,比如我们就可以把数据库中的库存数据转移到Redis缓存中,所有减库存操作都在Redis中进行,然后再通过后台进程把Redis中的用户秒杀请求同步到数据库中。
数据库层
数据库层是最脆弱的一层,一般在应用设计时在上游就需要把请求拦截掉,数据库层只承担“能力范围内”的访问请求。所以,上面通过在服务层引入队列和缓存,让最底层的数据库高枕无忧。

核心代码

/*** 系统初始化时把商品库存加入到缓存中*/@Overridepublic void afterPropertiesSet() throws Exception {//查询库存数量List<Map<String, Object>> stockList = miaoshaService.queryAllGoodStock();System.out.println("系统初始化:"+stockList);if(stockList.size() <= 0){return;}for(Map<String, Object> m : stockList) {//将库存加载到redis中redisUtil.getRedisTemplate().opsForValue().set(m.get("goodsId")+"", m.get("goods_stock").toString());//添加内存标记localOverMap.put(m.get("goodsId").toString(), false);}}
    /*** 请求秒杀,redis+rabbitmq方式*/@SuppressWarnings("unchecked")@RequestMapping(value="/go")@ResponseBodypublic ResultVo miaosharabbitmq(HttpServletRequest request){ConcurrentHashMap<String, String> parameterMap = ParameterUtil.getParameterMap(request);if(ToolUtil.isEmpty( parameterMap.get("userId")) || ToolUtil.isEmpty( parameterMap.get("goodsId"))|| ToolUtil.isEmpty( parameterMap.get("num"))){return ResultVo.error("参数不全");}String userid = parameterMap.get("userId").toString();String goodsId = parameterMap.get("goodsId").toString();long num = Long.parseLong(parameterMap.get("num").toString());//TODO // 1.根据需求   校验是否频繁请求// 2.         校验是否重复下单
//        Map<String,Object> map = redisService.get("order"+userid+"_"+goodsId,Map.class);
//        if(map != null) {//            return "重复下单";
//        }boolean over = localOverMap.get(goodsId);if(over) {return ResultVo.error("秒杀结束");}long stock = redisUtil.decr(goodsId,num);if(stock < 0) {localOverMap.put(goodsId, true);return ResultVo.error("库存不足");}System.out.println("剩余库存:" + stock);//加入到队列中,返回0:排队中,客户端轮询或延迟几秒后查看结果Map<String,Object> msg = new HashMap<>();msg.put("user_id", userid);msg.put("goods_id", goodsId);msg.put("num", num);mQSender.send(msg);return ResultVo.success("排队中!");}
   //查询秒杀结果(orderId:成功,-1:秒杀失败,0: 排队中)@RequestMapping(value="/result", method=RequestMethod.GET)@ResponseBodypublic ResultVo miaoshaResult(HttpServletRequest request) {ConcurrentHashMap<String, String> parameterMap = ParameterUtil.getParameterMap(request);String userid = parameterMap.get("userId").toString();String goodsId = parameterMap.get("goodsId").toString();String result = miaoshaService.getMiaoshaResult(userid, goodsId);if(!result.equals("0") && !result.equals("-1")){return ResultVo.success("秒杀成功!  "+result);}else{return ResultVo.error("秒杀失败!");}}

消息队列生产消息:

//生产者
@Service
public class MQSender {@AutowiredAmqpTemplate amqpTemplate;//Direct模式public void send(Map<String,Object> msg) {//第一个参数队列的名字,第二个参数发出的信息amqpTemplate.convertAndSend(MQConfig.QUEUE, msg);}}

消息队列消费消息:

//消费者
@Service
public class MQReceiver {@Autowiredprivate MiaoshaService miaoshaService;private static Logger log = LoggerFactory.getLogger(MQReceiver.class);@RabbitListener(queues= MQConfig.QUEUE)//指明监听的是哪一个queuepublic void receive(Map<String,Object> msg) {//log.info("监听到队列消息,用户id为:{},商品id为:{},购买数量:{}", msg.get("user_id"),msg.get("goods_id"),msg.get("num"));int stock = 0;//查数据库中商品库存Map<String, Object> m = miaoshaService.queryGoodStockById(msg);if(m != null && m.get("goods_stock") != null){stock = Integer.parseInt(m.get("goods_stock").toString());}if(stock <= 0){//库存不足log.info("用户:{}秒杀时商品的库存量没有剩余,秒杀结束", msg.get("user_id"));return;}//这里业务是同一用户同一商品只能购买一次,所以判断该商品用户是否下过单
//        List<Map<String, Object>> list = miaoshaService.queryOrderByUserIdAndCoodsId(msg);
//        if(list != null && list.size() > 0){//重复下单
//            return;
//        }//减库存,下订单log.info("用户:{}秒杀该商品:{}库存有余:{},可以进行下订单操作", msg.get("user_id"),msg.get("goods_id"),stock);miaoshaService.miaosha(msg);}

执行数据库减库存操作

@Service
public class MiaoshaService {@Autowiredprivate MiaoshaDao miaoshaDao;@Autowiredprivate RedisService redisService;@Autowiredprivate  RedisUtil redisUtil;//查询全部商品库存数量public List<Map<String, Object>> queryAllGoodStock(){return  miaoshaDao.queryAllGoodStock();};//通过商品ID查询库存数量public Map<String, Object> queryGoodStockById(Map<String, Object> m){return  miaoshaDao.queryGoodStockById(m);};//根据用户ID和商品ID查询是否下过单public List<Map<String, Object>> queryOrderByUserIdAndCoodsId(Map<String, Object> m){return  miaoshaDao.queryOrderByUserIdAndCoodsId(m);};//减少库存,下订单,是一个事务@Transactionalpublic void miaosha(Map<String, Object> m){//减少库存int count = miaoshaDao.updateGoodStock(m);if(count > 0){try {//减少库存成功后下订单,由于一件商品同一用户只能购买一次,所以需要建立用户ID和商品ID的联合索引m.put("id", UUID.randomUUID().toString().replaceAll("-", ""));miaoshaDao.insertOrder(m);//将生成的订单放入缓存redisService.set("order"+m.get("user_id")+"_"+m.get("goods_id"), m);} catch (Exception e) {//出现异常手动回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();redisService.incr("goods"+m.get("goods_id"));}}else {//减少库存失败做一个标记,代表商品已经卖完了redisService.set("goodsover"+m.get("goods_id"), true);}}//获取秒杀结果@SuppressWarnings("unchecked")public String getMiaoshaResult(String userId, String goodsId) {Map<String, Object> orderMap = redisService.get("order"+userId+"_"+goodsId, Map.class);if(orderMap != null) {//秒杀成功return orderMap.get("id").toString();}else {boolean isOver = getGoodsOver(goodsId);if(isOver) {return "-1";}else {return "0";}}}//查询是否卖完了private boolean getGoodsOver(String goodsId) {return redisService.exists("goodsover"+goodsId);}}

效果如下

1.用jemeter模拟创建1W个并发线程数

2.执行请求,查看后台输出日志,这里可以看到程序执行时,先从内存中扣减库存,再排队消费


3.消息队列消费情况

4.查看后台日志:最后一次日志输出时库存尚有一个。

5.当库存扣减完后,别的线程的响应结果:

6.查看数据库的库存和订单是否有超卖现象:

  • 每个用户买1个商品,共生成了1000条订单记录;
  • 商品表中,商品id为2的库存全部卖光,未出现负数,大功告成!



6.查看请求的平均耗时,平均耗时1.3s,在本机上的运行情况可以接受。

完整代码地址:https://gitee.com/Chrishecd/chrisProject.git

后续思考

  • 秒杀是有很多种技术解决方案的,比如悲观锁,分布式锁,乐观锁,队列串行化,Redis原子操作,等等。在高并发场景下如何优化并发的性能也应该是我们一直思考的问题。

写文章不易,点个赞再走吧

基于SpringBoot的高并发秒杀(限时秒杀)相关推荐

  1. batocera中文整合包_分享一个整合 SSM 框架的高并发和商品秒杀项目

    点击上方"Java基基",选择"设为星标" 做积极的人,而不是积极废人! 源码精品专栏 中文详细注释的开源项目 RPC 框架 Dubbo 源码解析 网络应用框架 ...

  2. api商品分享源码_SSM框架高并发和商品秒杀项目高并发秒杀API源码免费分享

    前言: 一个整合SSM框架的高并发和商品秒杀项目,学习目前较流行的Java框架组合实现高并发秒杀API 源码获取:关注头条号转发文章之后私信[秒杀]查看源码获取方式! 项目的来源 项目的来源于国内IT ...

  3. 91免费视频Redis+Lua解决高并发场景在线秒杀问题

    为何要使用Lua脚本解决商品超卖的问题呢? Redis在2.6版本后原生支持Lua脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行. 将复杂的或者多步的redis操作,写为一个脚本,一次 ...

  4. 高并发redis实现秒杀商品

    高并发redis实现秒杀 ps:直接在redis读取插入操作 只是插入的时候要加锁 怎么加锁 http://newmiracle.cn/?p=488 public function miaoshate ...

  5. [SpringBoot][15][SpringBoot处理高并发]

    第 15 章 SpringBoot处理高并发 在企业实际应用中,会遇到很多高并发场景,最典型的例子就是双十一的抢购.这时候,如果仅仅按照之前简单的方式进行处理,不仅性能无法保证,而且有可能导致数据库某 ...

  6. springboot实现高并发红包系统(java 全网最全包括语音口令 文字口令 普通 拼手气)

    博主技术笔记 博主开源微服架构前后端分离技术博客项目源码地址,欢迎各位star springboot实现高并发红包系统(全网最全) 下面的业务处理请根据你们实际的场景进行处理 1.sql设计 CREA ...

  7. 随手记录第二话 -- 高并发情况下秒杀、抢红包都有哪些实现方式?

    1.何为高并发? 高并发:在短时间内涌入超量的请求 那么如果出现这几种情况,可能会导致的后果 服务宕机 商品库存,红包金额超量 2.何为高并发秒杀? 这是一个高频面试题,问题虽然简单,但是里面的细节有 ...

  8. 高并发大流量秒杀方案思路

    概念:什么是秒杀 秒杀场景一般会在电商举行一些活动或者节假日在12306网站上抢票时遇到.对于网站中一些稀缺或者特价的产品,电商网站一般会在约定的时间对其进行限量销售,因为这些产品的特殊性,会吸引大量 ...

  9. 分布式锁?我一手synchronized 什么高并发,什么秒杀通通拿下(狗头)

    分布式锁 1.分布式锁 2.传统锁 2.1.经典问题--卖票 2.2.并发导致超卖现象 2.3.JVM锁 2.4.事务与JVM锁 2.5.MySql锁 2.5.1.一个SQL 2.5.2.悲观锁 2. ...

最新文章

  1. python装饰器原理-Python函数装饰器原理与用法详解
  2. js中执行到一个if就停止的代码_Node.JS实战64:ES6新特性:Let和Const。
  3. 【机器学习算法-python实现】最大似然估计(Maximum Likelihood)
  4. 使用 Drools 规则引擎实现业务逻辑,可调试drl文件
  5. 数学:乘法逆元-拓展GCD
  6. 第二季-专题6-点亮指路灯
  7. 楚留香服务器维护时间,《一梦江湖》手游官方网站_《楚留香》现已全面升级重制-3月9日维护更新公告...
  8. DEA数据包络分析python代码记录
  9. Exploring Architectural Ingredients of Adversarially Robust Deep Neural Networks
  10. 解决谷歌浏览器 Google Chrome不能拖拽安装离线插件的办法
  11. Mac各种问题的万能解决方法:重置SMC与重置NVRAM(PRAM)
  12. 俄罗斯方块游戏(Python实现)
  13. 计算机综合应用教材,系统测评计算机综合应用技能期末作业教材.doc
  14. VMware Ubuntu18.10与Win10共享文件夹
  15. CSS——网易云音乐之下载客户端页面的实现
  16. 华为云PB级数据库GaussDB(for Redis)揭秘第八期:用高斯 Redis 进行计数
  17. python 调用航空公司的接口 获取机票数据 api简单案例
  18. 微信小程序如何设置跳转企业微信
  19. 推荐几个优质的公众号!
  20. 加湿器工作原理与电路介绍(共19页pdf下载)附电路原理图(转)

热门文章

  1. 01路径规划问题的相关理论
  2. HashMap结构图及特点
  3. STM32中断分配——抢占优先级与响应优先级
  4. CreateProcess 的正确关闭
  5. request Headers字段详解
  6. python写用用户名密码程序_python写用’户登录程序‘的过程
  7. 关于NetBios的简单应用
  8. 简单工厂模式的实现及优缺点
  9. RISC-V指令集架构------RV32C压缩指令集
  10. Glide的使用回收内存问题