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

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

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

快速排序(QuickSort)是一种高效的排序算法,它的平均时间复杂度为O(nlogn)。它是通过将待排序数组分成两部分,其中一部分的所有元素都比另一部分的所有元素小,然后对这两部分分别进行递归排序,最终将整个数组排序。 以下是Java实现的快速排序代码: ```java public class QuickSort { public static void quickSort(int[] arr, int start, int end) { if (start < end) { int pivot = partition(arr, start, end); quickSort(arr, start, pivot - 1); quickSort(arr, pivot + 1, end); } } private static int partition(int[] arr, int start, int end) { int pivot = arr[end]; int i = start - 1; for (int j = start; j < end; j++) { if (arr[j] <= pivot) { i++; swap(arr, i, j); } } swap(arr, i + 1, end); return i + 1; } private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } public static void main(String[] args) { int[] arr = {5, 3, 8, 4, 2, 7, 1, 10}; quickSort(arr, 0, arr.length - 1); for (int i : arr) { System.out.print(i + " "); } } } ``` 在这个实现中,我们使用了递归来进行快速排序。首先,我们将数组分成两部分,然后对这两部分分别进行递归排序。在partition函数中,我们选择了数组的最后一个元素作为枢轴(pivot),并通过比较其他元素和枢轴的大小进行交换,将比枢轴小的元素放在枢轴的左边,比枢轴大的元素放在枢轴的右边。最后,我们将枢轴放在数组的正确位置上,并返回该位置。 在这个实现中,我们使用了swap函数来交换数组中的元素。我们也可以使用Java自带的Arrays.sort()函数来进行快速排序,它的实现也是基于快速排序算法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值