面试的重灾区"秒杀"的满分思路

面试的重灾区"秒杀"的满分思路

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

写在前面

无论什么样的公司或者面试官,在面试的时候都喜欢问关于秒杀的场景,也许你就有遇到过这样的面试题:

秒杀中,怎么削减流量高峰?

秒杀中,怎么控制库存,不超卖?

秒杀中,怎么保证一致性?

或许更笼统一点的问题,秒杀场景,你怎么设计?

对于这一系列的问题,只要我们有着清晰的思路,各个环境的组合拳有很多,今天我们就来探讨一下,秒杀,我应该有什么样的思路,希望对你有所帮助,也喜欢你能评论发表想法,我们碰撞出火花。

秒杀的特征

秒杀在业务上来说,无非是商家通过极低的价格来吸引客户,极低的价格意味着极高的成本,因此必须控制好极少的库存,一般而言,通过一段时间的秒杀运营推广之后,在某一个时刻,开闸放水。(这是秒杀的前置原因,对于商家来说,到这一步秒杀已经结束了,因为宣传目的已经达到了,剩下的部分是IT技术的秒杀),因此秒杀慢慢演变成了资源极度稀缺下的一种瞬时抢购。

因此,从技术的角度来看,适配于秒杀场景的系统有这样两个特征:

  • 瞬时超高并发访问量
  • 数据读多写少

所以,个人觉得解决秒杀,就是解决这样两个问题。

秒杀三部曲

1、秒杀前(预热)

我们一直强调瞬时,就是因为秒杀的时间点是确定的,例如定在了双十一的十一点,那么再十点五十九分,用户就开疯狂的刷新秒杀页面,这样商品详情页的请求量,瞬时就会被拉满。

页面模块化、静态化、CDN预热这样一系列的操作,可以将静态的页面的加载压力从服务端移走。这样大量的请求压力给到了CDN和浏览器。

但是并不是商品页的所有信息都可以静态化,例如商品的库存,秒杀的时间开关,这个必须要从服务器实时获取。此时就来到了秒杀的第二阶段。

2、秒杀中(控制)

对于秒杀业务来说,我更愿意把这阶段的操作称之为控制,大体上可以分为三个:校验库存(读多),扣减库存(写少),订单流程(写少)。在这一步,我们需要控制库存不能超卖,订单也能完整下成功,其中较为复杂的是订单流程,包含着支付、状态流转、商品出库、物流信息等等关键节点。同时也可能涉及到多个系统的交互和多个数据库表的CRUD。

很显然,多个系统的交互和数据库表的查询,很容易引发秒杀的雪崩,因此在这一步,我更愿意把订单流程跟库存操作拆解开来。

2.1、在Redis等缓存里面直接进行库存的校验和扣减

我们拿Redis来作为库存存储的介质,基于什么样的考量呢?

首先,对于库存查询来说,一秒的访问量级是万,十万,Redis的访问会更加的高效;

其次,我们在Redis里面保存了库存的值,那就直接在Redis里面进行库存的扣减,在这里我们有两个考量的点,一个是没有额外的交互,如果库存扣减放在了数据库,那么就要维持缓存一致性进行事务控制,这无疑带来很大的开销。另外一个,Redis的原子性避免了库存被击穿的风险,不会因为校验库存的请求读到旧值导致超卖。

既然提到了原子性,那么我们再多提一嘴,Redis的某些操作是天然支持原子性的,比如setnx,但是对于库存的操作是比较复杂的,如何来保证原子性呢?这时候,我们可以采用lua脚本:

# 获取当前商品库存
local counts = redis.call("HMGET", KEYS[1], "total", "ordered"); 
# 总库存转换成数值
local total = tonumber(counts[1])
# 已经秒杀了的库存
local ordered = tonumber(counts[2]) 
# 比较库存是否能够满足
if ordered + k <= total then
# 更新数值
    redis.call("HINCRBY",KEYS[1],"ordered",k)
   return k;
end return 0
复制代码

2.2、 Redis的高并发支持

第一点、Redis本身的高性能天然的支撑着高并发的秒杀场景。最值得一提的是,如果我们商品比较多,我们可以使用Redis的切片集群,这样将不同商品的秒杀流量分散开来,不至于将让某台Redis压力过载。(其实,聊到这里,我们很多操作核心都是,大化小)

第二点、商品级别的分布式锁限制流量。这一步只有客户端拿到了商品级别的key才能对Redis里面的库存校验和扣减,这样大部分流量可以再分布式锁的程度就别过滤掉,极大的减少服务器的压力。你可能再想,那对客户不是公平的呀?事实就是这样,秒杀只有极少数人能够秒到,这里本身很大一部分人,是走不到查看库存的,你是否很多时候,秒杀刚开始,就刷到了卖完了?

2.3、 订单流程处理

这个地方仍然想说两点,第一点是,大流量的削峰,想到最多的就是消息队列或者多线程异步处理,同样在这样的场景也可以采用这种方式;但是如果在这里使用异步的方式,你可能没有了解到秒杀的核心,在前面我们一系列的操作,对于成交的订单已经很少了,那么此时数据库的直接操作,或者同步等待操作完成都是完全可以接受的。

3、 秒杀后

在这个阶段,请求的峰值已经过去了,正常的服务是可以支撑的,例如部分用户刷新商品详情页,尝试等待有其他用户退单或者成功下单的用户会刷新订单详情,跟踪订单的进展。

秒杀思考

  1. 恶意请求的拦截,黑房子
  2. 防止库存信息过期的缓存击穿
  3. 订单流程中的异常回滚重试
  4. 秒杀独立的Redis实例,防止引发业务Redis的雪崩
  5. 接入层并发过,就负载均衡请求

写在后面

至此,秒杀场景的流程思路说完了。当然,秒杀的场景问题有很多,解决思路也很多,总结来说,我们期望把流量削峰,高并发请求放在高性能组件中处理,欢迎你跟我一起探讨不同的思路。

分类:
后端
标签: