秒杀的概念

秒杀是电商网站的一种销售方式,以特定时间段内较低的商品价格来吸引消费者购买,并对销售库存进行限制。这样必然会造成特定时间段(一般几秒到几十秒不等)大量的用户对某商品进行抢购,并会有库存不足,抢购失败的情况。

秒杀的特点

  1. 大量用户在秒杀时间点发起购买请求,造成网站流量瞬间激增;
  2. 秒杀的商品一般库存较少,只有少数用户能够购买,要控制好库存,防止超卖;
  3. 整个系统关键在于支撑短时间内的高并发,降低数据库压力,业务和普通商品购买区别不大。

秒杀系统架构设计

  • 限流,通过前端页面减少并发访问,比如在抢购的时候需要输入验证码,降低同一时间访问量;
  • 减少数据库读写,对商品库存的控制,放到内存数据库中,增加读取效率;
  • 削峰,短时间内大量的请求进入系统,并发访问服务器和数据库,肯定会拖垮整个系统,可以使用消息队列的方式削减并发请求,把瞬间的高流量变成一段时间内的平稳流量。

具体设计方案

方案一:mysql排他锁

排他锁,又称写锁,顾名思义,就是排他锁不能和其他锁共存。在一个事务获取了一个数据行的排他锁,那么其他事务就不能获取该行的排他锁,只有获取到该数据行的排他锁的事务,才可以对该行进行读取和修改。这样我们在读取商品库存的时候,就可以使用排他锁的方式,即在一个事务对库存进行读取的时候,另一个事务无法读取库存,相当于在数据行层面进行串行化,让读取商品库存的请求顺序执行,保证库存读到的准确性,也就是解决“卖超”问题。

排他锁使用方式,mysql默认在innoDB引擎下update、insert、delete语句都会使用排他锁,另一种常用的方式是SELECT...FOR TABLE UPDATE查询添加排他锁;

mysql排他锁方式能够应对并发量较小的商品抢购,因为每次抢购请求都会读取数据库,所以无法应对高并发。

方案二:redis存储商品库存

高并发下,性能瓶颈在于数据库,因为java服务可以通过扩展机器的方式,部署多套分担服务压力,但是数据库不好扩展。所以如果能够解决数据库瓶颈,秒杀业务的高并发压力也就得以解决。

数据库瓶颈的关键又在于每次秒杀请求都访问mysql数据库获取库存,因此重点在于优化读取库存操作。

Redis作为高性能NoSQL数据库,读写操作都在内存中进行,其官方说每秒能处理10万左右的读写。因此可以使用redis来缓存商品库存,秒杀请求都会先去Redis中查询库存是否足够,如果足够,再进行下一步订单操作。

但是这种方案也有一定局限性,在并发量一定的情况下可以使用。Redis本身并不具备并发处理能力,在超高并发的秒杀业务中其实Redis也会出现性能瓶颈;另一方面,如果单纯用redis挡住没有库存的请求,那么对于库存充足时透过redis的请求,还是会马上打到mysql数据库上,如果直接打在mysql的请求量大的话,数据库还是会面临并发压力。

所以,单用redis还不能解决高并发业务。

方案三:redis和MQ消息队列

消息队列是解决高并发的有效工具,方案二中透过redis的大量请求,不直接请求数据库下单,采用异步下单方式,先进入消息队列,然后排队消费,将瞬间并发访问变成一段时间正常访问数据库。

对于秒杀请求,redis中库存不足的请求直接返回库存不足;库存足够的请求,表示可以进行下单操作,那么进入消息队列,异步下单,同时秒杀请求返回“排队中”状态,许多平台采用这种方式来应对并发。返回“排队中”状态的秒杀请求,客户端要想知道秒杀结果,就需要调用查询秒杀结果接口,此时查询有三种状态“仍然排队中”、“秒杀失败”和“秒杀成功”。对于“仍然排队中”的秒杀请求,客户端需要轮询调用秒杀结果查询接口直到返回结果状态“失败”或者“成功”,秒杀结果查询接口去查询redis中是否生成了对应订单,查询redis中库存是否足够,以此来判断用户是否秒杀成功。

商品秒杀业务的并发也不只是秒杀接口,还有秒杀商品详情页的访问、秒杀结果查询等。处理并发,要合理利用缓存机制,减少数据库的压力。

限流、防刷、接口地址隐藏优化

限流:图形验证码方式,请求秒杀接口需要验证图形验证码正确性,正确识别验证码的时间不一致,能够缓解瞬间并发压力;

防刷:一个用户对一个路径的访问次数在一定时间内有限制,使用redis可以解决;

接口地址隐藏:接口地址传参,保证秒杀接口不是一个固定路径,防止接口被刷,同时也可以有效隐藏秒杀地址。