削峰限流防刷和令牌桶算法少卖问题解决方式

Posted Zephyr丶J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了削峰限流防刷和令牌桶算法少卖问题解决方式相关的知识,希望对你有一定的参考价值。

削峰、限流

削峰限流要解决什么问题呢,要解决OrderController中create(),下单这个方法,也可以称之为前端的接口,下单时流量过大的问题
虽然说用了异步扣减库存,但是承载力是有限的,如果流量过大,服务器撑不住
另外,秒杀商品有限,将有限的人放进来就可以了,不用把所有参与秒杀的人都放进来
那么怎么限制

我们把业务接口叫做交易,怎么解决流量问题呢?把交易之前的验证迁移出来,单独作为一步
为什么?交易只是解决交易的问题,100个库存只放1000个人进来,而且是有权限的进来,他们来了就能交易,因为已经验证过了
现在限制验证环节的流量,通过验证环节来削减流量,而不是在交易环节,因为都已经挤进来了

验证成功了,就能直接交易吗?也不是,还要给用户颁发一个令牌,有这个令牌才可以去交易;在交易环节,就看有没有这个令牌,有令牌可以交易,没有出去

但是验证成功了就直接发令牌吗,100个商品,只需要让进来1000个人就足够了,1000个人入围去抢就可以了,所以要限制令牌的个数;
所以在中间加一个大闸,大闸就是一个参数,每次我们看这个参数,比如说这个商品开放的流量是1000,那么初始化这个大闸就是1000,颁发一个令牌就减一,最终颁发1000个令牌就行了
所以当用户验证通过的时候,需要看大闸有没有余量,有就颁发令牌,没有退出

有了令牌就能交易吗?也不是,因为有可能商品很多,那么大闸也很多,所以大闸相当于不能起到限流的作用了,因为基本上人人都会得到一个令牌;这个时候,就得想别的办法?
所以需要去限流,避免单机挂掉,所以需要加限流器,限制单机的TPS(每秒处理多少请求);

那么有了限流器就可以了吗?也不见得,因为限流器限制的流量肯定是按单机比较高的值来限制,比如服务器承载力最大是10000,限流器就写10000甚至多一点,因为想让服务器发挥最大能耗,所以往往写的是服务器接近极限的值;
有可能某些线程由于某些原因阻塞,被卡住了,可能交易环节就变慢了

为了让交易环节有一个缓冲,所以加了一个队列来处理,队列就是线程池,里面有多个线程;超过线程池的承载能力,还有等待队列可以等待;
本来交易环节是单线程处理,现在相当于提交给线程池,多线程处理,线程池处理能力肯定更强,可以复用一些线程,可以利用CPU多核的优势,并发处理,把单线程处理的瓶颈拓宽了

上面都是发送请求以后做的限制,那么能不能在请求之前做限制?我们在申请令牌之前,加验证码,可以平滑流量,因为秒杀的时候,很多人都在点下单的按钮,压力很大;所以加上验证码,要求输入验证码,输入验证码需要几秒钟,所以我们就实现了把一秒1000万的流量平滑到三五秒

总结,先提交验证码,去抢令牌,如果手速慢了,令牌可能就没抢到;即使抢到了,如果商品很多,服务器承载力有限,就加了一个限流器,限制服务器TPS,不让服务器挂掉,哪怕很多人失败,把很多请求拒绝掉,重新来一遍,也不会让服务器挂掉;这时候单机服务器达到了瓶颈,服务器处理很慢,所以加了一个线程池,缓冲一下,这样单机的运行就会比较健康,不容易挂掉
再加上是分布式部署,有很多服务器,每个服务器承载一些流量,多一点服务器流量就大了

验证码用easy capcha
大闸用一个变量,在redis中,promition:gate:161 XXX
限流器是用guava中的RateLimiter,限制的是1s的流量,不是总的流量,用的是令牌桶算法
队列就是线程池

令牌桶算法

限流,限制单机的TPS,我们用的是RateLimiter,限制的是一个比例,一个速率,不是限制一个死的流量,而是限制1s能处理多少,项目里设置的是1000;
限流器底层实现的时候,采用的是令牌桶算法;还有一种漏桶算法

限流器管理了一个容器,这个容器叫做令牌桶,令牌是字符串之类的东西;令牌桶容量是有上限的,假设有100个令牌
令牌桶算法的关键是,有一个令牌生成器,不断的生成令牌
1.一开始令牌桶中没有令牌,当我们初始化限流器的时候,内部会初始化若干个令牌,不是满的,比如10个,
2.然后令牌生成器每个一段时间,就生成一些令牌,放在令牌桶中,如果满了就丢弃多余的令牌。

限流器初始化以后,客户端向服务器发请求,被限流器拦截,限流器看令牌桶有没有令牌,如果没有了,那么就拒绝客户端的请求,给一个错误提示;如果获取到了令牌,令牌数减一,就可以访问业务组件了

漏桶算法

有一个漏桶,桶中装的是水滴,比如这个桶容量是100,那么就是装100滴水;漏桶直接对接业务组件;
漏桶关键是:
1.每秒会漏出若干滴水,漏出的水给业务组件,10滴水就是10个业务请求,不管现在桶中有多少滴水,每秒我只漏10滴水给业务组件,保证业务组件每秒只能处理10个业务请求
2.注水时水满则溢

客户端去访问限流器的时候,一个请求认为是一滴水,限流器尝试往桶中加水,桶的容量是有限制的,如果满了,拒绝请求;如果没有满,就加入一滴水;加进去的水不是立刻给业务组件,桶和业务组件的关系永远都是每秒出十滴水,没有到整秒,不会被处理,可能先到的还会先处理

区别

漏桶算法业务组件承担的压力是固定的,图中例子,每秒只处理10个请求,处理速率恒定,不可能出现爆发的情况,这种方式很稳健
令牌桶算法可能出现爆发的情况,有可能一开始没有请求,但是令牌生成器会一直生成令牌,导致桶满了,而在某一时刻,请求爆发,会把令牌全都获取到,导致服务器在某一刻压力很大,但是在后续时刻因为每秒生成的令牌是有限制的,后续的压力就稳定了;令牌桶算法可能出现业务高峰和业务低峰的情况。
如果追求服务器速率永远恒定,用漏桶算法;如果希望服务器某些时刻可以处理突发流量,但是不能长期的,就用令牌桶
秒杀的时刻一大波流量涌入,我们希望在这一刻处理比较多的请求,下面马上恢复到常态,所以用令牌桶

防刷

削峰限流都是正常的流量
防刷不是写几行代码就能解决的,往往是整个系统级别的,甚至是更高级别的一个处理,安全级别的处理,防刷就是防黄牛;黄牛可能用几个设备几十个设备反复刷,把优惠力度最大的商品抢到;即便是没有抢到,因为你已经做了限制,不断刷会让正常流量进不去接口中。
黄牛不太好防,防不胜防,因为双方都在不断进化

需要了解防黄牛的策略,往往企业都是将多个维度的策略组合再一起使用;而小的企业没有精力做,所以一般是购买这种服务,总体来说有几种主流的方式:
防黄牛就是防止黄牛不断刷我们的服务器,导致崩溃或者让正常流量无法进入

  1. 从用户角度来限制,比如下单这种借口,用户需要登录才能下单,这种接口适合用这种方式;而查询不用登录,不适用;服务器判断当前用户(通过cookie或者token)固定时间内访问了这个接口多少次,存到redis里;
    但是这种方法很局限,就像查询这种操作,限制不了;专业黄牛有很多账号
  2. 限制IP,限制IP固定时间内多少次访问;
    但是可能黄牛会虚拟设备,虚拟出很多IP;而且这种方式还可能误杀,比如一个单位有很多人,但是对外只用一个公网IP访问,用这种方式的话就会误杀
  3. 现在更建议的方式,小规模的秒杀活动,可以放到web应用、网页中来做,大的活动都会用移动端,手机,手机支付环境要比网页安全很多;因为网页可以看到所有请求的过程,请求信息都是一目了然的;但是手机的APP是定制化的,不像浏览器,所有行业都是统一的,APP在编译的时候能进行混淆加密,反编译也不容易解密,里面数据也可以加密;相对来说,手机安全
    一般大规模支付、秒杀,建议在手机端上进行,在手机终端上访问Server的时候,Server可以采集设备的指纹,就是设备的一些信息,包括硬件信息、软件信息、APP信息等,通过这些信息组合成指纹,唯一识别终端,然后分析终端行为,看是否可疑;根据终端活动,看是否可疑,如果正常,就颁发一个凭证,以后访问就正常访问,不受约束;如果可疑,那么就发起验证,通过验证码或者短信等手段(我们去外地登录,经常会出现这种情况)
    但是这样也不是百分之百杜绝,黄牛可以用模拟器,设备牧场

少卖问题怎么解决?

下单的时候锁库存,不是付款的时候锁库存,而下单时候锁库存有可能就会发生少卖
那么怎么解决?可以用延时队列来解决,下单这一刻,可以把这个处理加到延时队列里,比如过半个小时再消费一下,消费的时候去检查是否进行了付款,付款就移除队列,没有付款就把订单作废,然后移除队列

延时队列可以有两种实现方式:
第一种redis的数据类型,zset,有序集合,相当于一个简单的消息队列;

生产者,下单的时候,往zset中存数据,也就是订单信息,将时间来当做集合对象的分数;消费者定期去看时间最早的订单,检查是否到时了(定时器驱动),到时间了进行处理

第二种方案就是直接用RocketMQ,因为它天生就支持延时队列,这种方式更方便

生产者发一个延时的消息(等一段时间才能消费),放在延时队列里;到时间了,MQ就会把这个消息放在目标队列里,然后消费者就可以在目标队列里消费了

以上是关于削峰限流防刷和令牌桶算法少卖问题解决方式的主要内容,如果未能解决你的问题,请参考以下文章

削峰限流与防刷

通过注解实现接口限流防刷

mysql数据库怎么解决高并发问题

如何解决mysql innodb高并发的问题

coding++:高并发解决方案限流技术-使用RateLimiter实现令牌桶限流-Demo

限流限流算法