秒杀场景是一个对综合能力要求非常高的场景,对于普通应用场景而言,开发时不必做过多考虑,因为它的流量相对平稳;但是一旦进入秒杀,你就不得不考虑高流量、高并发场景下的应对措施。如果能应对秒杀场景,那么对于普通应用场景的开发,就会显得游刃有余。
虽然我们未必真的会去开发一套秒杀系统,但是我们对它的应对策略做一做了解,也是非常有益的。
废话不多说,直接切入正题。
1. 库存
在秒杀场景中,我们最先想到的就是如何处理库存? 在普通的应用中,我们把库存放在关系数据库中,然后通过数据库的锁机制来控制库存的减少。
但是在秒杀场景下,这么做是不行的;通常而言,单mysql实例的QPS在几千左右; 秒杀场景会有大量的请求去查询库存,很容易就超过这个QPS。
单redis实例的QPS一般在8-10w左右,基本是mysql的1-2个量级;因此我们选择把库存放到redis中。
在并发场景下,库存涉及两个操作:
- 查询库存
- 扣减库存
如果分成两步,第一步查询库存、第二步扣减库存;会出现第一步有多个请求查询都有库存,但是扣减的时候缺不够扣的情况;究其原因在于,它们不是原子操作,因此我们采取lua脚本的方式去扣减库存。redis的lua脚本是原子操作,因此可以保证库存查询、和扣减是原子的。
-- Lua脚本用于Redis
local stock = redis.call('GET', KEYS[1]) -- 从Redis获取库存
if stock and tonumber(stock) > 0 then
redis.call('DECR', KEYS[1]) -- 原子地减少库存
return true
else
return false
end
需要注意的是:lua脚本它保证的只是将多个命令作为原子操作;本身不保证事务处理,也就是如果脚本中途出现错误,对已执行的部分不做回滚。但是幸运的是,我们这里不用考虑回滚的问题,因为第一步检查库存执行和没执行没差别,第二步失败也没影响。
你可能会问,即然我们将库存查询和扣减放在redis中了,那么我们还需要在关系性数据库比如mysql中存一份么?
理论而言,可以存一份,对于普通应用场景我们也是这么做的;但是在秒杀场景下,存在两个问题:
- redis和mysql中库存数据很难保持一致
- mysql数据库存扣减库存,需要加锁(乐观锁/悲观锁),而加锁就会导致性能的下降
另外,在高并发场景中,redis中已经有一份库存了,而且也能做到高并发原子扣减,我们没有必要非得再在mysql中存一份,变相去提供复杂度,降低性能。
试想下,现在我们把库存放到redis中,单redis性能假设是10wQPS,我们有5个redis实例,那么也就是每秒能抗住50w的请求。如果秒杀请求高于50w,redis还是扛不住;在这种场景下,我们还需要再应用实例再做一层本地缓存。
这样让应用层本地缓存替redis挡住一部分流量,本地缓存的过期时间可以设置的短一点。
2. 订单生成与支付
库存上面我们考虑的差不多了,现在我们来考虑订单的生成与支付。
一般而言,在库存校验和扣减成功后,我们会生成订单,然后支付。在秒杀场景中,如果库存成功扣减后,就同步生成订单。如果此时流量还是很大,还是容易击垮我们的数据库。因此我们建议以异步的方式生成订单,在库存扣减成功后,往MQ中生成一条订单生成消息。让数据库平稳的去拉取消息生成订单,这也是削峰的一种方式
对于客户端而言,如果库存扣减成功,直接返回秒杀中的提示即可。服务端订单生成后,可以向客户端推送消息的方式让客户端去查询订单状态。
如果客户端在订单生成后,没有及时支付,取消订单时需要考虑库存的回填redis.
3. 削峰
我们知道秒杀场景,瞬时流量非常大。作为服务器而言,我们的处理能力总是有限的,我们总希望流量能稍微平稳一点进来。因此,那么还有些什么方式可以做到削峰呢?
在秒杀活动开始时,我们可以在用户发出秒杀请求前,增加一些操作比如:答题、验证码等等。
用户在做这些操作是需要时间的,有的人快、有的人慢,这样变相摊平了瞬时流量。
还有一种削峰手段就是消息队列,上面已说过。
4. 限流与限购
我们还可以通过限流的方式,进一步保护我们的服务器,我们可以在nginx或应用层做限流。
nginx
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
// 限制每个ip地址每秒请求1次
}
应用层限流 我们可以利用redis做限流,限制每个用户,多少时间只能发起多少个请求
除了限流我还可以限购,我可以定义一些商业规则,比如:每个用户最多秒杀一个商品。
我们希望流量能够层层限制,过滤掉无效流量,让最终达到数据库层的流量越小越好。
5. 降级
降级本质上是一种弃车保帅的策略。一旦流量达到服务器的极限,我们可以考虑保证核心业务的运行,而丢弃非核心业务。
比如在秒杀中,我们优先保证商品的库存、订单生成和支付;而对其它的服务比如:短信、商品评论、非核心业务,我们选择降级。
6. 隔离
用我们四川话说就是,"不要因为一颗耗子屎,坏了一锅饭";我们不能因为一个秒杀活动,导致公司整个服务崩溃。
我们可以对秒杀部分服务进行隔离,比如:为秒杀单独建一个数据库,这样即使秒杀数据库崩了,也不影响公司正常的数据服务。
6. 灾备
这个对一般的公司一般也用不到,要求非常高,要做的话可以同城多活,异地多活非常难,了解下就行。