高并发:在短时间内涌入超量的请求
那么如果出现这几种情况,可能会导致的后果
这是一个高频面试题,问题虽然简单,但是里面的细节有很多,考察的是高并发场景下,前端到后端多方面的知识。
秒杀一般出现在某些电商网站中,例如:淘宝双十一,京东的618,直播带货,通俗点来数就是固定的商品以极低的价格让大量用户来抢购,虽然只有少数用户能够购买成功,但这类活动大部分商家是不赚钱的,说白了就是为了宣传
秒杀虽然只是一个促销活动,但其中的细节确是不少
指的是一般在秒杀时间点(例如凌晨0点)的前几分钟,用户并发量突然飙升,到达秒杀点后,会到达顶峰
这类活动通常都是狼多肉少,只会有少部分用户能够成功。只有当用户收到抢购失败的通知后才会离开,并发量也就逐渐降低了。
问题就在于如果这些流量都是直接访问服务端,那么服务端会因为承受不住这么大的压力,而直接挂掉
那么为了减少不必要的服务端请求,应该从以下几个方面进行控制
抢购流程
加个弹窗确认流程,或者加一层真正的抢购页面
按钮控制
为避免秒杀时间之前的无效请求,前端可以在按钮上做控制,到时间前多少才开放点击,让请求真正的到服务端
限流控制
例如上一次请求成功的话,需要间隔几秒后才能继续点击,否者就提示,使用定时任务即可
由于大量用户抢购少量的商品,只有极少部分能够成功,那么必然就会出现库存不足的情况,但如果出现大量查询和扣减库存
如果是先查询再扣减,那可能会出现库存数量不对,因为每个请求扣减在不同的事务。例如下面的操作并不是原子的
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了
首先Redis是完全可以支持高并发的,性能好一点的机器上Redis的QPS是能达到秒10W+的,另外Redis是一个复杂的多Recator模型,读指令是多线程,但写指令是主线程操作的。
那么流程图就可以是如下操作:
这里就可以借助Redis的incr自减来保证库存了。
注意是直接使用Redis的incr自减,不是先查询,再自减
其他的实现方案:令牌桶算法限流,Lua脚本,对活动商品的缓存,库存完了直接删掉。进来可以先校验商品是否存在。
在处理完上述控制后,应该只有少部分请求能够进入系统了,但商品数量足够大的时候,突然涌入如此多的请求,那也是会对服务造成一定影响的,这个时候就要考虑异步了
在这个过程要注意消息丢失的处理,例如发送失败,网络问题,broker挂了,磁盘满了等问题。最好再加一个消息记录表,由状态区分,定时回调到mq中,最终保证完成状态一致。
消费者在消费是保证幂等,避免重复消费
抢购成功的订单,肯定会存在支付超时的问题,那么怎么处理呢?
上面已经分析到mq分担压力进行最后的入库,可能因为不想要了而放弃支付,那么这个时候还需要把库存加回来的
例如:京东淘宝的秒杀活动,基本上误差时间在1秒内,那是怎么实现的呢,这可以从redis的回收算法上借鉴了
时间轮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,计算规则仅为还没开始运行的时间轮
时间轮存在的问题
定时过期
服务启动时开启一个每次检测一次的定时任务,保证过期订单能被回收,库存能复原
可以借助中间间实现延时通知,例如rabbitMq的死信队列(时间固定且不可更改),rocketMq的延时队列,zookeeper临时节点的过期通知等等
秒杀活动可能不止局限于手动点,想京东抢酒程序,Github上一大堆源码,这时候能跳过前端控制,并且程序的速度往往高于手速N倍,那么可能会导致这种操作的抢购都成功了,那么必要的限流策略肯定不能少的,例如ip限流,uuid限流
这里不多分析了,可以参考SpringCloud第五话 – Gateway实现负载均衡、熔断、限流这里面有详细的记录
原理也是基于令牌桶算法,基于redis lua脚本实现
前面说了那么多,这里总结一下
这里还记录一个骚操作,例如:抢红包或者抽奖算法
为避免每次请求都去走计算,可以提前生成好每个位置的概率或者金额,通过redis list的随机或者顺序取,然后位置空了,则重新计算后缓存
如果还有其他的方式实现的,欢迎评论区留言哦
以上就是本章的全部内容了。
上一篇:随手记录第一话 – Java中的单点登录都有哪些实现方式?
下一篇:随手记录第三话 – 学习中?
旧书不厌百回读,熟读精思子自知