关于秒杀系统设计的思考

秒杀业务挑战及解决方案

此部分摘自《大型网络技术架构-核心原理与案例分析》。

  1. 对现有网站业务造成冲击

    秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致整个网站瘫痪。

    解决方案:秒杀系统独立部署。

    将秒杀系统独立部署,甚至使用独立域名,使其与网站完全隔离

  2. 高并发下的应用、数据库负载

    用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果按照一般的网站应用架构,访问应用服务器、连接数据库,会对应用服务器和数据库服务器造成负载压力。

    解决方案:秒杀商品页面静态化。

    重新设计秒杀商品页面,不使用网站原来的商品详细页面,页面内容静态化,用户请求不需要经过应用服务

  3. 突然增加的网络及服务器带宽

    假设商品页面大小200K(主要是商品图片大小),那么需要的网络和服务器带宽是2G(200K×10000),这些网络带宽是因为秒杀活动新增的,超过网站平时使用的带宽。

    解决方案:租借秒杀活动网络带宽。

    因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器的压力,需要将秒杀商品页面缓存在CDN,同样需要和CDN服务商临时租借新增的出口带宽。

  4. 直接下单

    秒杀的游戏规则是到了秒杀才能开始对商品下单购买,在此时间点之前,只能浏览商品信息,不能下单。而下单页面也是一个普通的URL,如果得到这个URL,不用等到秒杀开始就可以下单了。

    解决方案:动态生成随机下单页面URL

    为了避免用户直接访问下单页面URL,需要将改URL动态化,即使秒杀系统的开发者也无法在秒杀开始前访问下单页面的URL。办法是在下单页面URL加入由服务器端生成的随机数作为参数,在秒杀开始的时候才能得到

要解决的核心问题

超卖

超卖问题就是说,售出数量多于库存数量。发生此种情况是由于库存并发更新的问题,导致在实际库存已经不足的情况下,库存依然在减,导致卖家的商品卖得件数超过秒杀的预期。比如要销售的商品数量为10个,结果最终有效订单是20个,超卖出10个。

1、悲观锁

那么为了解决这个问题,在数据库端可以采取的方式有悲观锁。具体来说,就是在利用mysql自带的行锁机制,在减库存时加上判断库存够减的条件。如:

1
update seckill_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0

那么数据库本来就是系统中最容易达到性能瓶颈的部分,我们肯定会采用缓存的方式来尽可能延缓其到达瓶颈。

2、乐观锁
3、队列

在缓存数据库中解决锁的问题,有两种方式,第一个是乐观锁,第二个是队列。

看这篇:https://www.jianshu.com/p/c5f94afa57fb

缓存与数据库的一致性

1、cache aside

首先说一下错误方法:更新时,先删除缓存,然后再更新数据库,而后续读操作会把数据再装载到缓存中。这个逻辑是错误的。试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

针对这种情况,我们可以调换删缓存和更新数据库的顺序,避免上述情况的发生。

mark

这就是cache aside方式。

可能有人会问,为什么更新MySQL之后是删除缓存,而不是更新缓存?

这个问题举个反例就可以说明了:如果A、B两个线程同时做数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。如果在更新缓存的时候,B先更新了缓存,而A后更新了缓存,那么现在缓存里就是A的数据。B是后更新的,但是最终缓存中确是A的数据,出现了不一致。

其实这种方式还会出现一个问题,就是如果更新完数据库之后,删除缓存失败了,那么此时其他的读操作就会读到缓存中的脏数据。虽然这种可能性比较低,但是这说明此方法仍不是一个较好的方法。

2、异步更新缓存(基于订阅binlog的同步机制)

https://blog.csdn.net/weixin_45132238/article/details/93490308

  1. 技术整体思路:

    MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

    读Redis:热数据基本都在Redis
    写MySQL:增删改都是操作MySQL
    更新Redis数据:MySQL的数据操作binlog,来更新到Redis

  2. Redis更新

    (1)数据操作主要分为两大块:

    一个是全量(将全部数据一次写入到redis)
    一个是增量(实时更新)
    这里说的是增量,指的是mysql的update、insert、delate变更数据。

    (2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。

    这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

    其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

    这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

    当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis!

性能优化方案

1、流量削峰

为什么要削峰:稳定服务端,节省资源。其本质是延缓用户请求发出,减少和过滤无用请求(遵循请求数尽量少原则)。

削峰思路:排队、验证码(答题)、分层过滤

  • 排队:消息队列缓存大量并发,把原来的一步操作变成两步。虽然违背了增加访问路径原则,但是防止了系统崩溃。

    什么是访问路径原则?这里访问路径表示请求传输经过的节点,每个节点都会创建一个socket连接,多一个节点就会少一分可用性。

  • 答题或验证码:可防止技术实现的自动抢购的脚本,如连点器、爬虫等。延缓请求从之前的1s内延缓到2-10s,对事件进行了分片,减缓服务器压力,如微信的摇一摇。也可限制答题时间间隔。

  • 分层过滤:分层为:CDN -> 前台读系统(商品详情系统) -> 后台写系统(交易系统)->DB

    大部分数据和流量都在CDN获取,拦截了大部分读的数据。

    经过第二层(前台读系统)尽量走Cache。

    到第三层(后台写系统),做数据校验、限流,进一步减少数据量和请求。

    最后在数据层完成强一致性校验。

2、动静数据分离

将HTML中的动态数据分离出来,令文件静态化。然后将这些静态数据放到浏览器缓存,或者是CDN当中,进行流量控制,降低服务器端的带宽消耗。例如,原来前端页面获取数据是通过thymeleaf模板引擎,现在均通过ajax来获取,html文件没有动态数据了。

项目中的配置:

1
2
3
4
5
6
7
8
9
10
11
 # static
resources:
add-mappings: true # 开启默认的资源处理
# cache-period: 3600
chain:
cache: true # 查询链的缓存
enabled: true
gzipped: true # 对静态页面压缩
html-application-cache: true
static-locations: classpath:/static/
cache-period: 3600 # 客户端缓存静态资源的时间,资源的缓存时效,以秒为单位.

要解决的其他问题

1、缓存击穿、雪崩、穿透

我的另一篇博客:https://blog.csdn.net/makersy/article/details/99455023

秒杀架构设计

1、前端部分设计

首先,要有一个展示秒杀商品的页面, 在这个页面上做一个秒杀活动开始的倒计时, 在准备阶段内用户会陆续打开这个秒杀的页面, 并且可能不停的刷新页面。对于秒杀页面的展示,我们采取前面提到的方式,动静数据分离。将静态数据(如HTML等)放到浏览器缓存,或是与服务器分开存放,然后放到cdn节点上分散压力,由于CDN节点遍布全国各地,能缓冲掉绝大部分的压力,而且还比机房带宽便宜。

然后,是页面上的倒计时问题。我们知道,客户端时钟与服务器时钟是不一致的,就算是服务器之间也有这种可能。所以我们可以直接从服务器上获取秒杀开始剩余时间,做倒计时,这个过程并不涉及到后端逻辑,因此速度很快。

最后就是请求的拦截,我们需要通过拦截无效请求来降低后端的压力。在前端,可以通过验证码、答题等方式,延缓请求;用户点击秒杀后,按钮置灰,禁止用户重复提交请求…

2、服务端部分设计

  • server层(JVM):

    一些简单逻辑判断,如是否登录,是否重复点击。。

  • 缓存层:

    秒杀系统是一个非常典型的“读多写少”的模型,大部分请求到来都是读库存,只有少部分才会到数据库层去更新库存。所以可以通过缓存的形式,将库存供请求查询,提高读取速度。

    同时也可以在缓存中预减库存,如果缓存中库存都没了,那么就拒绝后面所有的请求。这样的话,就可以过滤很大一部分请求,只有一小部分会进入到下一层。注意:预减库存并不能解决超卖问题,所以可能会有超出当前库存数量的请求到达下一层,要处理他们就是后面的逻辑了。

  • 队列层:

    上述优化方案中有提到,这里的队列的作用是流量削峰,不让数据库被请求洪峰冲垮;同时,在请求进入队列层之前,服务端就会返回结果(如排队中),不会让客户端干等着服务端处理完,因此还有提升用户体验的作用。

  • 数据库层:

    用来处理队列中的请求,这一部分就会使用一些手段来保证不会发生超卖问题,正确地减库存和下订单。

总结

这篇文章呢,主要从产生场景,核心问题,项目架构,性能优化等方面介绍了秒杀系统。说实话,现在的我接触到的全是皮毛,就算这样,我还是觉得秒杀系统要考虑的东西太多太杂了,哈哈。

文章中一些地方是我根据其他资料得出的(出处均有注明),大部分是我的个人理解,如果有不对的地方,还请大家指出。