聊聊秒杀设计

134 阅读6分钟

秒杀场景是一个对综合能力要求非常高的场景,对于普通应用场景而言,开发时不必做过多考虑,因为它的流量相对平稳;但是一旦进入秒杀,你就不得不考虑高流量、高并发场景下的应对措施。如果能应对秒杀场景,那么对于普通应用场景的开发,就会显得游刃有余。

虽然我们未必真的会去开发一套秒杀系统,但是我们对它的应对策略做一做了解,也是非常有益的。

废话不多说,直接切入正题。

1. 库存

在秒杀场景中,我们最先想到的就是如何处理库存? 在普通的应用中,我们把库存放在关系数据库中,然后通过数据库的锁机制来控制库存的减少。

但是在秒杀场景下,这么做是不行的;通常而言,单mysql实例的QPS在几千左右; 秒杀场景会有大量的请求去查询库存,很容易就超过这个QPS。

单redis实例的QPS一般在8-10w左右,基本是mysql的1-2个量级;因此我们选择把库存放到redis中。

在并发场景下,库存涉及两个操作:

  1. 查询库存
  2. 扣减库存

如果分成两步,第一步查询库存、第二步扣减库存;会出现第一步有多个请求查询都有库存,但是扣减的时候缺不够扣的情况;究其原因在于,它们不是原子操作,因此我们采取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中存一份么?

理论而言,可以存一份,对于普通应用场景我们也是这么做的;但是在秒杀场景下,存在两个问题:

  1. redis和mysql中库存数据很难保持一致
  2. 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. 灾备

这个对一般的公司一般也用不到,要求非常高,要做的话可以同城多活,异地多活非常难,了解下就行。