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

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();
}

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

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

如果请求量足够的大,会导致数据库雪崩,影响太大,这个时候应该要考虑到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);
 }

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

最终会存储在第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中的单点登录都有哪些实现方式?
下一篇:随手记录第三话 --你见过哪些神乎其乎的存储方式??

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值