前言
之前为了在简历上添些东西,也做过秒杀系统,但仅仅是Demo级别,仅仅考虑了如何减轻数据库压力,如何防止超卖,对于很多在秒杀系统中需要注意的问题实际上并没有去完善,所以当面试官问到的时候,回答常常不能让面试官满意。
今天就来梳理一下秒杀系统究竟要注意什么,该如何去优化。同时希望能够针对这种高并发的场景,提炼出自己的一套基础的方法论,提升自己思考问题的深度,广度以及架构能力。
这次,让我们从问题出发,来进行秒杀系统的设计。
秒杀系统要解决什么问题?
- 秒杀系统是一个非常典型的高并发场景,时间极短,瞬间用户量极大,在这种场景下,我们的服务器,我们的数据库是非常脆弱的。
如果每秒数万的请求打到我们的服务器上,后端数据库几乎肯定会宕机,我们的缓存同样也要考虑缓存穿透,缓存击穿,缓存雪崩的问题。 - “超卖”问题。所谓超卖,就是指我们本想只秒杀100件,结果卖出去1000件,给商家造成了损失,这事关钱的问题,所以也是一个大问题。简单来说的话其实就是秒杀商品的库存该如何维护?
- 服务治理相关的的问题。我们的秒杀系统,显然是隶属于某个大系统,比如说商城系统的,我们希望秒杀系统即使出了问题,也不会影响到整个大的业务,说白了就是如何进行服务的熔断与降级?
- 安全与公平问题。我们如何保证秒杀的公平性呢,即保证不会有“专业”的秒杀团队来通过脚本抢到了所有的商品,另外,秒杀的下单url我们如何保密,防止某些能够提前得到url的消费者提前下单呢?
该如何设计?
让我们一个问题一个问题来看:
-
高并发带来的问题:这个问题,可以细分为对业务逻辑服务器的压力,对缓存的压力,以及对数据库的压力。
对于业务逻辑服务器的压力,我们的解决方法就是加机器,多部几台服务器,同时用我们的反向代理神器Nginx来进行请求分发、负载均衡,同时过滤掉一些恶意的请求,我们对请求的响应能力就得到了大大的提升。
对于缓存也是同样的方法,我们的Redis做集群,主从同步,读写分离,因为后续我们会用Redis来做业务逻辑,因此也要加上哨兵模式和持久化,保证缓存的高可用。
另外,对于一些热点商品的商品信息等这些不会改变的数据,我们可以在每一个业务服务器的内存中做一个更高级的缓存,这样比Redis要来的更高效,同时我们只保存不会改变的信息,也就不用去处理复杂的缓存一致性问题。
对于数据库,实际上数据库是我们的最大瓶颈所在,因为秒杀场景实际上是大量流量对某一个资源(库存)的争夺,而且是更新而非读取,单纯的读写分离并不能解决问题。我们只能从上游减少打到数据库的请求量。显然我们的秒杀场景,最终能秒杀到的是少数,如果库存只有1000条,我们希望只有1000条请求打到MySQL上,这样显然能大大减轻数据库的压力,同时我们用一个MQ将数据库解耦出来,去异步的平稳的操作数据库,也能起到保护数据库的作用。
总结来说的话,对于这种瞬时的大量流量,我们很自然的想法就是搞一个MQ来削峰,异步的操作数据库。同时,进行数据的预热,在秒杀开始前就准备好相应的数据到缓存中。 -
如何维护库存来防止超卖呢?一个很自然的想法是我们在扣减库存之前做个判断,当仍有库存时我们才会扣减库存,订单转到下一个状态。 问题是高并发的场景下,可能在我们判断时仍有库存,但扣减时已经没有了。解决这个问题最直观的方法就是加个锁,那么有没有更好的办法呢?其实我们只是想将判断和扣减库存作为一个原子操作来进行,保证判断后到扣减库存的过程没有其他的资源争夺。这可以通过Redis来实现,因为Redis的lua脚本天然就是原子性的。同时,将库存维护在Redis中显然要比维护在数据库中更高效。
-
服务治理相关:这个就是要维护秒杀服务的相对独立性,秒杀系统使用单独的数据库与缓存,与其他业务隔离开,其实就是微服务的单一职责原则,这样在机器层面避免了秒杀业务对其他业务的影响。
同时,进行服务的熔断,限流,降级,感觉要撑不住了,通过一个分布式开关,开启限流,再不行就要降级,再不行,就只能放弃秒杀服务,直接熔断,防止影响其他服务。 -
安全问题:拦截恶意请求。对于url的暴露问题,我们可以将url动态化,进行MD5加密。