【高并发】秒杀业务场景详解

时间:2024-06-02 14:26:34

一、秒杀场景的特点

       秒杀的商品具有价格低、库存有限、定时开始的特点,因此秒杀场景最大的特点就是高并发。数以千万的用户的流量集中在某个时间点上(即秒杀开始时),给后端服务器造成很大压力,如果不能进行有效削峰、限流,所有请求一次性打到某一台服务器或数据库上,必然造成服务的不可用,给用户造成不良体验。

二、整体架构设计

【高并发】秒杀业务场景详解
       在整体架构上,用户的请求首先通过一个网关进行负载均衡,根据负载均衡算法的不同,应用不同的策略将请求转发给对应的应用服务器,服务器对用户的请求进行限流,拒绝大部分的请求。根据Redis中缓存的商品信息,判断当前秒杀是否已结束,如果秒杀结束,拒接请求并设置内存标记,后续的请求直接拒绝,无需再查询Redis缓存;如果秒杀未结束,将请求入队MQ,进行异步处理,进一步削峰。
       多个消费者从MQ中取消息,进行处理,即检查对应商品的库存情况,减库存并生成订单,要注意这里需要原子操作的控制,也有多种解决方案,在后面会详细讨论。对秒杀成功的订单,设置有效期,用户在有效期内成功付款,则生成支付订单,持久化到MySQL,否则会随着Redis缓存中key的过期而失效。

三、秒杀系统常见的问题

       秒杀系统由于高并发的特点,会带来一系列问题,这些问题不仅是秒杀系统独有,任何高并发系统都需要考虑这些问题的解决方案。

  • 高并发带来的响应缓慢甚至服务不可用

       MySQL由于数据都存放在磁盘中,并发量十分有限,并且为了保证业务逻辑的正确性,每个请求都需要对商品数据进行一次加锁和解锁操作(行锁的效率低),更加降低了效率,造成不好的用户体验。更糟的情况是MySQL在高并发环境下,还可能会崩溃,造成服务不可用。

  • 超卖、少卖

       超卖问题来源于核减库存的操作其实不具备原子性,它分为了三步:查询库存->检查库存不为0->扣减库存。那么设想一个场景:当服务查询库存为1,但还未扣减库存时,另一个服务查询库存也为1,两个服务都会进行扣减库存、生成订单的操作,造成了超卖。
       少卖问题来源于已经成功扣减库存,但生成秒杀订单因为各种原因失败了,导致库存被扣减却没有订单生成的情况。该问题出现的根本原因是扣减库存和生成订单两步操作没有保证原子性。

  • 用户作弊

       某些不法用户可能通过自动化脚本模拟HTTP请求的方式反复刷秒杀接口,既对系统造成了更大的流量压力也产生了造成了不公平。

四、优化方案

  • 将大部分请求拦截在上游

       通过随机算法、哈希算法或根据当前负载情况动态地拦截请求,快速失败,只将少部分的请求放入MQ等待消费者消费。

  • 验证码机制

       通过验证码机制能够很好限制用户的请求速度,同时也能防止作弊。验证码的选择上可以选择图片、算式、滑块等,为了防止验证码识别工具,尽可能选择较复杂的验证码。

  • 限制用户的每秒请求次数

       每次用户请求后在缓存中进行计数,并设置相应有效期,当用户请求达到阈值直接拒绝请求,实现限制单用户每秒请求次数的功能,防止单一用户的高频访问给系统造成更大压力,这个策略根据业务需求的不同可对账号的限制、IP的限制或账号和IP共同限制。

  • 页面静态化、页面缓存

       通过页面静态化、页面缓存的方式降低响应的时间。
       页面静态化是进行页面缓存的第一步,将一个动态页面分离成静态的页面模板和动态的数据部分,将静态的部分拆分出来进行缓存,动态的部分根据服务器的业务处理返回json数据再进行渲染。比如在秒杀页面,页面的模板框架是静态资源,可以进行缓存,而商品名称、库存数量、价格、秒杀剩余时间、验证码等信息根据请求的响应结果进行动态渲染。
       静态页面抽离出来之后就要进行缓存,缓存可以放在用户浏览器、服务端或CDN,放在浏览器上的缓存具有不可控性,如果用户不进行及时刷新,很可能看到错误的不一致信息,对于秒杀系统而言,信息的实时性非常重要,这点看来放在浏览器上的缓存并不合适。另外服务端主要进行业务逻辑的计算和加载,不擅长处理大量连接,如果进行页面的缓存和加载会带来性能的降低。因此页面缓存常放在CDN上。
【高并发】秒杀业务场景详解
       CDN的节点一般选择访问量集中的地区附近且要保证节点与主站之间的网络通信,即考虑缓存的命中率和及时失效的问题,这点对于高并发、信息变化快的秒杀系统而言及其重要。

  • 利用MQ异步削峰

       大量的请求并不直接到达应用服务器,而是先进入MQ队列,多个消费者根据处理能力从MQ中拿到请求消息并进行业务处理,类似于著名的“漏桶算法”。MQ还可以通过设置最大队列的长度或消息的有效期TTL来进行限流,通过设置消息超时快速失败,防止大量的消息堆积给用户带来不好的体验。

  • Redis缓存提高并发量
           Redis作为一个内存数据库比MySQL的QPS高100倍以上,并且由于底层操作处理是单线程的,在高并发、分布式架构下无需反复的加锁、解锁和线程上下文的切换,更进一步提高了效率。通常将热点数据缓存到Redis中,比如秒杀系统中商品的ID、库存等信息,秒杀过程中进行核减库存等操作都是在Redis中,能显著提高效率。Redis通常以集群部署且设置若干哨兵以保证服务的高可用性。

  • 定时任务进行缓存预热
           秒杀场景比较特殊(相较于微博热搜等热点数据而言),大流量会集中在秒杀开始的时间点上,如果此时缓存中没有相关商品数据,会导致瞬间请求全部转向数据库,造成缓存击穿。因此,秒杀场景的缓存预热非常有必要,由于秒杀都有一个确定的开始时间,可以通过定时任务在秒杀开始前的某个时刻将商品的相关信息预热到缓存中,这样当秒杀开始时,在缓存中就有相关的数据了,这个定时任务可以不需要非常频繁进行检查,性能消耗并不大,比如规定在秒杀开始前5分钟进行缓存预热,那么每1分钟进行1次检查完全足够。