很多优化考虑都不是局部的,是全流程的优化,关于
高并发流量(查询)的应对
可以看我之前写的https://www.jianshu.com/p/f5271c825eb9
本文聚焦下单、回滚、定时取消
本文全是我本人生产环境沉淀下来的干活,没有图(内部产出不能放),都是字可能有点枯燥,请见谅,后面会考虑加些图解
零 补充一个常规下单模式的选型以及优缺点分析
-
下单减库存,即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是
最简单,最精确
的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。
缺点:有些人下完单可能不付款,尤其是恶意下单的人是不会真正付款的,那么既有可能出现想买的人买不成,最终活动也过期了,商家也很烦~
-
付款减库存,即买家下单后,并不立即减库存,而是等到有用户
付款后才真正减库存
,否则库存一直保留给其他买家。
缺点:因为下单时不会减库存,所以也就可能出现下单数远远超过真正库存数
的情况,尤其会发生在做活动的热门商品上。这样一来,就会导致很多买家下单成功但是付不了款,买家的购物体验自然比较差
。 -
预扣库存,这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如 10 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家
付款前
,系统会校验
该订单的库存是否还有保留
:如果没有保留,则再次尝试预扣
;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存。
缺点:这种方案确实可以在一定程度上缓解上面的问题。但是否就彻底解决了呢?其实没有!针对恶意下单这种情况,虽然把有效的付款时间设置为 10 分钟,但是恶意买家完全可以在 10 分钟后再次下单,或者采用一次下单很多件的方式把库存减完。针对这种情况,解决办法还是要结合安全和反作弊的措施来制止。比如给用户打标,恶意用户进入蜜罐(后面会说)
一 下单中的优化
- 前端进行库存可选项的限制,避免用户大部分的
无效库存尝试请求
在涉及到库存的商品一般在进入详情页面或者下单页面的时候把库存给查出来,这时候我们可选的商品数量必然要小于这个库存,不要等用户点了无数次下单,一个个尝试库存是否充足,另外呢即使这样,由于秒杀的高并发高流量,依然可能用户拿到的库存大于服务端库存,因此当用户下单的时候,如果因为库存原因下单失败呢,我们就进行前端拿到的库存值的更新,另外如果库存不足了,直接由下单页返回到详情页,前端置灰下单按钮阻止用户的无效请求; -
风控、网络安全
有时候我们会判断出用户是有问题的,比如某个用户开开团前一个小时开始极其高频的请求数据(1S刷新10次以上或者更高),那这个时候如果我们进行拦截,那他肯定会想各种各样的方法去破解,最后可能又得手了,那么其实我们这里可以诱导防御的方式,比如说有一种叫做蜜罐
的技术,可以理解为直接提供一个仿真环境用于引流恶意用户,给他提供一个啥玩意都有的数据环境,有服务器有缓存有数据库,你可以看详情,然后当他进行下单的时候,咱们沙盒
仿真
环境
也让他下单成功,但是呢 他一看订单(查真实库)就啥也没有,就支付不了,用不了咱们真正库存;
再者呢,可以用多重史诗级难度验证码拦击,咱们可以让网关层在给用户打标签为恶意用户之后呢,咱们让他下单的时候就输入验证码,而且给他的是史诗级难度的验证码,干扰线比字母多多了的那种,而且呢还不止让他验证一次,让他验证个两三次,如果这都难不倒他,就算了; -
库存缓存化
,我们可以把库存做到redis里,我们先采用lua(预查redis库存->再预扣redis的方式)->乐观锁扣除mysql库存- 为什么这里不直接decr预扣redis要用lua先查redis再预扣?主要是考虑到由于秒杀团购等属于赔钱引流的生意,所以大部分请求都是远远大于库存的,因此极有可能买不到,如果我们直接扣redis,那扣失败了,咱们不还是要还库存加回去吗
- 另外呢,要注意由于库存的特殊性,我们要保证redis里的数据和mysql 的数据的一致性,因此我们要使用一些事务来保证,比如TCC或者MQ事务实现;
- 这里咱们将mysql库存扣减放到了最后一步也减少了mysql锁竞争的过程了,这性能飞升啊;
-
下单异步化 or 限流,其实在我们前面讲了数据全部进缓存了,那么其实普通业务仅仅是查询,我们redis集群抗个几十万qps并发不存在任何问题了.
但是下单接口不一样,下单的流程需要操作数据库,那么数据库如果存在大量的行锁必然会造成大量的等待和超时问题,这种情况解决方案也有;
第一种方案: 我们可以采用异步化下单接口,让下单走队列,根据自己系统能力去设置消费线程数,然后轮询下单状态做最终结果,或者直接采用线程池去下单,利用去callback接口带回下单结果.
第二种方案: 限流,这里的限流不是直接做到接口层面
,因为我们是采用是redis预扣库存方式,我们其实不是怕大量流量过来,我们是怕大量流量+大量库存造成了我们redis这时候形同虚设,大量的DML操作在mysql被阻塞,那么这里我们可以进行限流,突破了redis防线的真正扣减mysql操作需要先申请资源,拿到资源再进行下面的操作,其他的进行拦截或者等待,这个操作可以用哨兵来做;并且这里需要做个稍微精准点的限流,比如做到商品层面,每个商品每秒可下单多少多少...这样,因为我们其实不太怕大量DML,而是怕同一行大量DML;
第三种方案: 据我所知目前有些公司会再mysql层面再做一层开发,他会有行锁竞争的时候在行后追加一个队列,把行锁转换为队列,这样其实也可以很大程度上解决性能问题,排队效率必然比并发竞争阻塞要高得多得多(锁竞争情况下 InnoDB 内部的死锁检测,以及 MySQL Server 和 InnoDB 的切换会比较消耗性能); - 下单服务单独集群,尽管我们上面已经做了重重保障,但是我们还是比较担心下单造成阻塞,大量tomcat连接数被下单请求使用,其他正常业务请求无法进来,造成整个系统不可用,那我们就可以让下单服务器单独隔离开,其他的服务器用于服务大量的查询接口;
二 订单定时取消的优化
订单定时取消是一个非常常见的需求,尤其是上面说到的下单减库存模式,因为我们有时候会比较担心用户下单了,但是不支付,这时候又锁住了库存,那其他用户就一直没法购买了,所以我们其实就需要进行订单的自动取消功能,避免长期锁住库存让其他人无法购买;
订单超时取消存在一个
无法在过期的一瞬间即时处理超时订单
的问题
举个例子,比如团购下单接口有个订单15分钟超时取消订单的操作,但是呢我们有时候没有办法一下子处理那么多订单,让他过期,比如有*十万个订单同一时刻过期,不论咋样我们肯定没有办法同时处理完的.但是呢这种超时订单是绝对不能继续让他进行支付的,咋办呢?
我们可以进行数据的另类同步
,我们可以在任何查出到这个订单的地方都对状态进行两种判断,比如1 订单状态为超时取消,2 订单状态为下单中,但是订单下单时间距离现在超过了15min,这两种状态咱们都认定他为超时,这样呢我们就可以做到一定程度上的订单同一时刻超时了.所谓,所有人都认为你牛逼,你就真的牛逼了,毫无破绽-
这种固定15min超时取消的业务,咱们可以直接用市面上常用的MQ进行异步化定时处理
- 一方面进行限流了,我们可以根据系统服务能力,调整消费线程数;
- 另外一方面不需要自己写任务导读框架,不需要去避免重复处理了,如果系统是集群的,也不需要考虑多个系统直接的并发竞争问题,省时省力;
- 至于做法可以给大家提供一种方案,RabbitMQ TTL队列+死信队列实现;
三 回滚逻辑优化
- 把子订单做的稍宽些,把一些信息放到订单表里尤其一些强关联性信息,最好做到一张表内,比如库存主键,商品购买数量,这样在回滚的时候一方面可以精准命中目标,另外一方面减少许多额外的查询操作;
- 加分布式串行锁(如redlock) + 乐观锁保障回滚不会被多次回滚,其实秒杀下单一般稍微多考虑考虑都不会出现超卖情况,但是回滚这个逻辑需要好好考虑,这个极易造成超卖,普通业务单一产品单一库存还好,像我的业务涉及到周期性库存,其实很容易涉及到超卖;
- 异步化,在以下情况下可以采用异步化回滚的方式
- 如果我们对上游的调用量没有一个很好的预估或者上游的取消订单流量极其不规律
- 上游业务不关心返回值或者上游业务不需要立即知晓回滚结果
那么这里我们可以采用异步MQ进行接收回滚,如果上游需要知晓回滚结果,可能会高频查状态那么可以将回滚状态都存入redis
回滚接口我这里优化的比较少
- 一方面是由于其功能确实简单,只需要保障回滚别造成超卖即可
- 另外一方面是因为大部分商品都是优惠力度极大,一般不会取消订单,回滚库存;
目前就到这里了,后面有空我会再补充一些
秒杀一般是大流量少库存,像我目前营销活动这块设计到商品库存的周期库存,设计理念就是想让商品慢慢卖,平均到指定周期的指定时段,一般单商品单个周期多了也就200左右并发的样子,一般主要设计的好下单的时候没啥问题;但是呢,这里存在一个未来可能的问题,那就是商品流量确实很大,商品库存也很多,比如100万人抢1W个小米手机,好家伙,完全是真实情况啊,这个问题其实是一个很现实的问题,在真实的做电商的互联网公司其实都会遇到这个问题,但是呢你看很多人的博客都写的比较夸张,标题都是啥10W并发,一看库存几十个,玩呢老哥,真实有那么大流量的,库存不至于这么低,万一搞个大库存,用你这方案妥妥的宕机啊;
所以这里对于有效订单的大流量,大库存问题的解决也做出了我的一些总结,可以看https://www.jianshu.com/p/552c4093832e