秒杀业务挑战及解决方案
此部分摘自《大型网络技术架构-核心原理与案例分析》。
对现有网站业务造成冲击
秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致整个网站瘫痪。
解决方案:秒杀系统独立部署。
将秒杀系统独立部署,甚至
使用独立域名,使其与网站完全隔离
。高并发下的应用、数据库负载
用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果按照一般的网站应用架构,访问应用服务器、连接数据库,会对应用服务器和数据库服务器造成负载压力。
解决方案:秒杀商品页面静态化。
重新设计秒杀商品页面,不使用网站原来的商品详细页面,
页面内容静态化,用户请求不需要经过应用服务
。突然增加的网络及服务器带宽
假设商品页面大小200K(主要是商品图片大小),那么需要的网络和服务器带宽是2G(200K×10000),这些网络带宽是因为秒杀活动新增的,超过网站平时使用的带宽。
解决方案:租借秒杀活动网络带宽。
因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器的压力,需要将秒杀商品页面缓存在CDN,同样需要和CDN服务商临时租借新增的出口带宽。
直接下单
秒杀的游戏规则是到了秒杀才能开始对商品下单购买,在此时间点之前,只能浏览商品信息,不能下单。而下单页面也是一个普通的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
首先说一下错误方法:更新时,先删除缓存,然后再更新数据库,而后续读操作会把数据再装载到缓存中。这个逻辑是错误的。试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
针对这种情况,我们可以调换删缓存和更新数据库的顺序,避免上述情况的发生。
这就是cache aside
方式。
可能有人会问,为什么更新MySQL之后是删除缓存,而不是更新缓存?
这个问题举个反例就可以说明了:如果A、B两个线程同时做数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。如果在更新缓存的时候,B先更新了缓存,而A后更新了缓存,那么现在缓存里就是A的数据。B是后更新的,但是最终缓存中确是A的数据,出现了不一致。
其实这种方式还会出现一个问题,就是如果更新完数据库之后,删除缓存失败了,那么此时其他的读操作就会读到缓存中的脏数据。虽然这种可能性比较低,但是这说明此方法仍不是一个较好的方法。
2、异步更新缓存(基于订阅binlog的同步机制)
https://blog.csdn.net/weixin_45132238/article/details/93490308
技术整体思路:
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis
读Redis:热数据基本都在Redis
写MySQL:增删改都是操作MySQL
更新Redis数据:MySQL的数据操作binlog,来更新到RedisRedis更新
(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 | # static |
要解决的其他问题
1、缓存击穿、雪崩、穿透
我的另一篇博客:https://blog.csdn.net/makersy/article/details/99455023
秒杀架构设计
1、前端部分设计
首先,要有一个展示秒杀商品的页面, 在这个页面上做一个秒杀活动开始的倒计时, 在准备阶段内用户会陆续打开这个秒杀的页面, 并且可能不停的刷新页面。对于秒杀页面的展示,我们采取前面提到的方式,动静数据分离。将静态数据(如HTML等)放到浏览器缓存,或是与服务器分开存放,然后放到cdn节点上分散压力,由于CDN节点遍布全国各地,能缓冲掉绝大部分的压力,而且还比机房带宽便宜。
然后,是页面上的倒计时问题。我们知道,客户端时钟与服务器时钟是不一致的,就算是服务器之间也有这种可能。所以我们可以直接从服务器上获取秒杀开始剩余时间,做倒计时,这个过程并不涉及到后端逻辑,因此速度很快。
最后就是请求的拦截,我们需要通过拦截无效请求来降低后端的压力。在前端,可以通过验证码、答题等方式,延缓请求;用户点击秒杀后,按钮置灰,禁止用户重复提交请求…
2、服务端部分设计
server层(JVM):
一些简单逻辑判断,如是否登录,是否重复点击。。
缓存层:
秒杀系统是一个非常典型的“读多写少”的模型,大部分请求到来都是读库存,只有少部分才会到数据库层去更新库存。所以可以通过缓存的形式,将库存供请求查询,提高读取速度。
同时也可以在缓存中预减库存,如果缓存中库存都没了,那么就拒绝后面所有的请求。这样的话,就可以过滤很大一部分请求,只有一小部分会进入到下一层。注意:预减库存并不能解决超卖问题,所以可能会有超出当前库存数量的请求到达下一层,要处理他们就是后面的逻辑了。
队列层:
上述优化方案中有提到,这里的队列的作用是流量削峰,不让数据库被请求洪峰冲垮;同时,在请求进入队列层之前,服务端就会返回结果(如排队中),不会让客户端干等着服务端处理完,因此还有提升用户体验的作用。
数据库层:
用来处理队列中的请求,这一部分就会使用一些手段来保证不会发生超卖问题,正确地减库存和下订单。
总结
这篇文章呢,主要从产生场景,核心问题,项目架构,性能优化等方面介绍了秒杀系统。说实话,现在的我接触到的全是皮毛,就算这样,我还是觉得秒杀系统要考虑的东西太多太杂了,哈哈。
文章中一些地方是我根据其他资料得出的(出处均有注明),大部分是我的个人理解,如果有不对的地方,还请大家指出。