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


    1.何为高并发

    高并发:在短时间内涌入超量的请求
    那么如果出现这几种情况,可能会导致的后果

    1. 服务宕机
    2. 商品库存,红包金额超量

    2.何为高并发秒杀?

    这是一个高频面试题,问题虽然简单,但是里面的细节有很多,考察的是高并发场景下,前端到后端多方面的知识。

    秒杀一般出现在某些电商网站中,例如:淘宝双十一,京东的618,直播带货,通俗点来数就是固定的商品以极低的价格让大量用户来抢购,虽然只有少数用户能够购买成功,但这类活动大部分商家是不赚钱的,说白了就是为了宣传

    秒杀虽然只是一个促销活动,但其中的细节确是不少

    3.秒杀系统细节

    3.1 瞬间高并发

    指的是一般在秒杀时间点(例如凌晨0点)的前几分钟,用户并发量突然飙升,到达秒杀点后,会到达顶峰
    这类活动通常都是狼多肉少,只会有少部分用户能够成功。只有当用户收到抢购失败的通知后才会离开,并发量也就逐渐降低了。

    问题就在于如果这些流量都是直接访问服务端,那么服务端会因为承受不住这么大的压力,而直接挂掉
    在这里插入图片描述

    那么为了减少不必要的服务端请求,应该从以下几个方面进行控制

    3.2 页面控制

    抢购流程
    加个弹窗确认流程,或者加一层真正的抢购页面
    在这里插入图片描述
    按钮控制
    为避免秒杀时间之前的无效请求,前端可以在按钮上做控制,到时间前多少才开放点击,让请求真正的到服务端

    限流控制
    例如上一次请求成功的话,需要间隔几秒后才能继续点击,否者就提示,使用定时任务即可

    3.3 服务端读多写少

    由于大量用户抢购少量的商品,只有极少部分能够成功,那么必然就会出现库存不足的情况,但如果出现大量查询和扣减库存
    在这里插入图片描述
    如果是先查询再扣减,那可能会出现库存数量不对,因为每个请求扣减在不同的事务。例如下面的操作并不是原子的

    long stock = mapper.getStockById(12);
    if(stock > 0){
    	//update xx set stock=stock-1 where id=12
    	mapper.updateStockById(12);
    	addOrder();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果要者这个基础保证库存不被超卖,那可以加个乐观锁

    update xx set stock=stock-1 where id=12 and stock > 0
    
    • 1

    如果请求量足够的大,会导致数据库雪崩,影响太大,这个时候应该要考虑到Redis了

    3.4 服务端缓存问题

    首先Redis是完全可以支持高并发的,性能好一点的机器上Redis的QPS是能达到秒10W+的,另外Redis是一个复杂的多Recator模型,读指令是多线程,但写指令是主线程操作的。
    那么流程图就可以是如下操作:
    在这里插入图片描述
    这里就可以借助Redis的incr自减来保证库存了。
    注意是直接使用Redis的incr自减,不是先查询,再自减

    其他的实现方案:令牌桶算法限流,Lua脚本,对活动商品的缓存,库存完了直接删掉。进来可以先校验商品是否存在。

    3.5 服务端异步处理问题

    在处理完上述控制后,应该只有少部分请求能够进入系统了,但商品数量足够大的时候,突然涌入如此多的请求,那也是会对服务造成一定影响的,这个时候就要考虑异步了
    在这里插入图片描述
    在这个过程要注意消息丢失的处理,例如发送失败,网络问题,broker挂了,磁盘满了等问题。最好再加一个消息记录表,由状态区分,定时回调到mq中,最终保证完成状态一致。

    消费者在消费是保证幂等,避免重复消费

    3.6 服务端订单超时问题

    抢购成功的订单,肯定会存在支付超时的问题,那么怎么处理呢?

    上面已经分析到mq分担压力进行最后的入库,可能因为不想要了而放弃支付,那么这个时候还需要把库存加回来的
    在这里插入图片描述
    例如:京东淘宝的秒杀活动,基本上误差时间在1秒内,那是怎么实现的呢,这可以从redis的回收算法上借鉴了

    1. 主动过期
      一般在抢购成功后,可以在前端页面上显示待支付的倒计时,如果过时间可以有前端通知过期
    2. 惰性过期
      抢购到待支付界面的一定时已经生成了订单的,那么查询订单的时候控制不下发并更新状态
      3.定时过期
      这里又分为时间轮和定时任务扫描了
      在这里插入图片描述
      ps: 不想画了,图片来源于网图,如有侵权,请联系删除

    时间轮java构建

    HashedWheelTimer hashedWheelTimer=new HashedWheelTimer(
    new DefaultThreadFactory("wheel-time"),100, TimeUnit.MILLISECONDS,60,false);
    
     @GetMapping("/{time}")
     public void tick(@PathVariable Long time){
    	 System.out.println("time:"+new Date());
    	 hashedWheelTimer.newTimeout(timeout -> {
    	 System.out.println("延时n秒后执行这任务:"+new Date());
    	 },time,TimeUnit.SECONDS);
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    tickDuration:100,每次指针的跳动间隔100ms
    ticksPerWheel:60,表示时间轮上一个多少个数组,分的数组越多,占用内存空间越大,一圈执行完需要 100*60/1000=6s
    leakDetection:开始内存泄漏检测

    当添加一个3分钟的延时任务时,计算规则如下

    //计算指针跳动的次数 3* 60 * 1000 /100 = 1800
    long count = 3*60*1000/ tickDuration;
    //根据取模计算下标位置 1800 % 60 = 0
    long round = count % ticksPerWheel;
    //计算当前任务需要经历的圈数 1800 / 60 = 30
    long rounds =  count / ticksPerWheel
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    最终会存储在第0格中,但标识的圈数为30,计算规则仅为还没开始运行的时间轮
    时间轮存在的问题

    1. 从上图也可看出问题,存储的结构肯定是链表,如果同一个时间有多个任务,那么需要执行完第一个才会执行第二个,这是会导致误差所在
    2. 时间轮是基于存内存操作的,如果服务宕机或者重启将不复存在,所以补救策略还是不能少

    定时过期
    服务启动时开启一个每次检测一次的定时任务,保证过期订单能被回收,库存能复原
    可以借助中间间实现延时通知,例如rabbitMq的死信队列(时间固定且不可更改),rocketMq的延时队列,zookeeper临时节点的过期通知等等
    在这里插入图片描述

    3.7 限流

    秒杀活动可能不止局限于手动点,想京东抢酒程序,Github上一大堆源码,这时候能跳过前端控制,并且程序的速度往往高于手速N倍,那么可能会导致这种操作的抢购都成功了,那么必要的限流策略肯定不能少的,例如ip限流,uuid限流
    这里不多分析了,可以参考SpringCloud第五话 – Gateway实现负载均衡、熔断、限流这里面有详细的记录

    原理也是基于令牌桶算法,基于redis lua脚本实现

    4.总结

    前面说了那么多,这里总结一下

    1. 前端页面控制,点击按钮控制
    2. 服务端缓存控制
    3. 分布式处理订单
    4. 主动、惰性、定期过期

    这里还记录一个骚操作,例如:抢红包或者抽奖算法
    为避免每次请求都去走计算,可以提前生成好每个位置的概率或者金额,通过redis list的随机或者顺序取,然后位置空了,则重新计算后缓存

    如果还有其他的方式实现的,欢迎评论区留言哦

    以上就是本章的全部内容了。

    上一篇:随手记录第一话 – Java中的单点登录都有哪些实现方式?
    下一篇:随手记录第三话 – 学习中?

    旧书不厌百回读,熟读精思子自知

  • 相关阅读:
    未找到你需要的内容?请看这里!!!
    Chrome 扩展教程之如何使用 React 构建 Chrome 扩展(教程含源码)
    puppeteer 爬虫初探
    体验SOLIDWORKS钣金切口工具增强 硕迪科技
    .NET 6 实现滑动验证码(四)、扩展类
    opencv dnn模块 示例(16) 目标检测 object_detection 之 yolov4
    C/C++Hello World
    【Linux系统编程:信号】产生信号 | 阻塞信号 | 处理信号 | 可重入函数
    后端API接口性能优化的10种方案,真有用!
    四川竹哲电子商务有限公司培训服务引领你走向成功
  • 原文地址:https://blog.csdn.net/qq_35551875/article/details/125897435