秒杀介绍
秒杀是商家通过少量库存和超低价格的方式吸引用户的一种营销手段,让用户在特定的时间里对一个热销商品进行低价抢购。“秒杀”第一次出现是在 2008 年淘宝推出的“竞价秒杀”功能,当时淘宝看到营销效果非常喜人。现在最出名的秒杀就是 在京东上 秒杀 茅台酒了,每场次的茅台酒抢购,都有一百万以上的用户来抢数量非常有限的茅台酒,还没有到1秒钟,商品就抢完了,这是非常极快。
秒杀系统目的。
秒杀系统主要目的是:低价促销抢夺用户。伴随着互联网用户、电商用户量爆发式增长,商家低价促销已成为获客的重要方式之一。试想一下,一件原价 200 块的畅销商品,你可能会犹豫;而标价变成 1 块钱的时候,你不会心动吗?正是利用这种占便宜、省钱划算的心理,商家用很少量的库存,获取成千上万甚至几十万用户的关注。
秒杀系统需要解决问题
在大型促销活动中,老系统面临并发性能、可用性、公平性等问题的挑战,秒杀系统需要解决高可用、高性能、高并发的问题。
2、秒杀系统特点商品数量有限,支持大量用户同时下单
瞬时高并发
秒杀商品库存一定是有限且价格超级优惠,一定会在秒杀开始的瞬间就会结束,上文介绍的京东秒杀茅台酒时,瞬时并发访问量突增上百倍,千倍甚至万倍。
热点数据
秒杀商品一定会有很多人关注,访问频率会非常高,需要单独存储。
读多写少
秒杀业务是典型的读多写少的场景,假设秒杀商品是iPhone14秒杀价格是100元,
但库存只有100个,来抢购的人一定是数以几十万计,但最后能成功下单的用户只有100个,
即订单系统实际写成功的次数只有100次。
流程控制问题
由于秒杀商品数量有限,只有极少数用户能抢购到商品,大多数请求都是无效请求,因此尽量把请求拦截在上游服务,然后在根据库存数量,放行和库存数大于等
于库存数的请求。
数据一致性
由于秒杀业务读多写少的特点
秒杀商品必须要用缓存技术,那么该如何在不影响系统吞吐量情况下解决缓存和DB一致性问题?
这里缓存技术我会选用业界主流的redis,根据以下库存超卖解决方案,
当库存扣减成功后,采用MQ异步解耦方式发消息给订单服务处理实际下单扣减库存业务,
只要保证MQ消息可靠性(不丢消息)就可以实现缓存和DB数据的最终一致性。
防止超卖问题
防止出现100个秒杀商品,结果卖出去10000个这种情况的发生,损失谁来承担?必须防止超卖问题的发生
再超大瞬时高并发下,如何保证库存不超卖?
扣减库存需要做2件事:1、扣减库存;2、判断库存是否足量。如果能保证这2件事的原子性就可以
保证库存不超卖问题,那么新问题来了如何在分布式环境下保证扣减库存和判断库存的原子性?
对此也有2个方案:1.分布式锁;2.redis+lua脚本(lua脚本在redis可以保证原子性)。
分布式锁是悲观锁会阻塞其他请求,如果考虑吞吐量这块则我会选择redis+lua脚本的方案。
由于秒杀时,流量巨大,而有效流量很小,需要做到尽量把请求控制在前端,通过层层过滤,达到放行到后台的请求数与秒杀商品库存数一致。
常见的系统架构(前端、nginx、后台服务及数据层)及流量层层筛选,如下图所示:
5.1 前端页面
前端页面静态化,放到距离用户访问就近的nginx或者是cdn上
将大量非关键的请求拦截到上游:抢购按钮按钮置灰,点过一次,不能再点;限制用户提交频次,比如一秒钟最多是5次,前端把大部分流量限制住,控制了大部分无效的流量,提高了系统的吞吐量,提升了后端系统稳定性。
5.2 nginx
高并发,性能很好,可以拦截一些黑名单
5.3 后端服务限流
有效商品只有固定数量,过多请求无意义,可以使用限流策略实现。日常中最常见限流的地方就是上班高峰期的地铁运营,早上上班高峰期时,地铁入口限流,请等三分钟,到了站厅,安检时,又要限流,再等三分钟,目的都是避免,海量乘客进入到站台,而列车已经爆满很拥挤了,再增加很多乘客会出现严重拥堵的事故。
常见的限流算法为:计数器、漏桶算法实现、令牌桶算法实现,下面以令牌桶算法介绍,因为令牌桶的好处之一就是可以方便地应对 突发出口流量(后端能力的提升)
描述:令牌桶算法以一个设定的速率产生令牌并放入令牌桶,每次用户请求都得申请令牌,如果令牌不足,则拒绝请求。
令牌桶算法中新请求到来时会从桶里拿走一个令牌,如果桶内没有令牌可拿,就拒绝服务。当然,令牌的数量也是有上限的。
令牌的数量与时间和发放速率强相关,时间流逝的时间越长,会不断往桶里加入越多的令牌,如果令牌发放的速度比申请速度快,令牌桶会放满令牌,直到令牌占满整个令牌桶。
Guava是Java领域优秀的开源项目,它包含了Google在Java项目中使用一些核心库,Guava的 RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现
redis分布式环境下限流:有三种实现方案:(1)使用zset (2)使用list (3) 使用redis cell,需要额外安装,该模块只有一个命令,查看官方的说明:
CL.THROTTLE user123 15 30 60 1 ▲ ▲ ▲ ▲ ▲ | | | | └───── apply 1 token (default if omitted) | | └──┴─────── 30 tokens / 60 seconds | └───────────── 15 max_burst └─────────────────── key "user123"
user123 就是key
15 是max_burst,就是初始时,最大的容量,就是令牌桶初始时的数量,但是初始化数量是该值加一。
400: 与下一个参数一起,表示在指定时间窗口内允许访问的次数
1:最后一个表示本次要申请的令牌数,不写则默认为 1。
缓存技术是实现高性能架构的基石,在秒杀系统中,需要把需要秒杀的商品信息、商品库存及秒杀订单都缓存起来,在用户查询时,不需要再去数据库查询,提升系统性能。
预扣库存秒杀系统控制库存预扣预存,即:扣减缓存中的商品库存,扣减失败,立即返回,扣减成功,通过发送消息到消息服务器实现异步下单。
预扣库存流程:先判断库存是否足够,足够再扣减库存,这两部操作需要原子化,lua脚本具有原子性,可以通过redis + lua脚本来实现,具体代码如下:
local key=KEYS[1]; ---keylocal subNum = tonumber(ARGV[1]) ; ---value 秒杀数local surplusStock = tonumber(redis.call('get',key)); ---使用get命令获取key的value值 剩余库存if (surplusStock<=0) then return 0 --- 剩余库存<=0 return 0elseif (subNum > surplusStock) then return 1 ---秒杀数量>剩余库存返回1else redis.call('incrby', KEYS[1], -subNum) return 2 --- 扣减成功返回 2end
扣减库存方法
private String subStock="local key=KEYS[1];n" + "local subNum = tonumber(ARGV[1]) ;n" + "local surplusStock = tonumber(redis.call('get',key));n" + "if (surplusStock<=0) then return 0n" + "elseif (subNum > surplusStock) then return 1n" + "elsen" + " redis.call('incrby', KEYS[1], -subNum)n" + " return 2 n" + "end"; @Test public void testDeductionStock(){ //构建redisscript对象,构造方法参数1 执行的lua脚本 参数2 结果返回类型 DefaultRedisscript
异步下单,通过控制消息服务器的消费速率,实现控制下单的速度,保护好后端的数据库不受到很大的冲击,起到削峰填谷的作用,提高系统性能。
6、秒杀系统总体设计 秒杀系统通过:通过前端提高性能、前端拦截无效流量、秒杀系统控制流量、秒杀系统控制库存预扣减、异步下单等,系统架构如下图所示: