分享架构领域干货,洞察互联网前沿架构
技术成就梦想

秒杀系统设计要点和优化思路

来源:https://www.tuicool.com/articles/JzQvUb

秒杀系统设计的 5 个要点:前端三板斧+后端两条路!

 

秒杀系统涉及到的知识点

 

  • 高并发,cache,锁机制

  • 基于缓存架构redis,Memcached的先进先出队列。

  • 稍微大一点的秒杀,肯定是分布式的集群的,并发来自于多个节点的JVM,synchronized所有在JVM上加锁是不行了

  • 数据库压力

  • 秒杀超卖问题

  • 如何防止用户来刷, 黑名单?IP限制?

  • 利用memcached的带原子性特性的操作做并发控制

秒杀简单设计方案

比如有10件商品要秒杀,可以放到缓存中,读写时不要加锁。 当并发量大的时候,可能有25个人秒杀成功,这样后面的就可以直接抛秒杀结束的静态页面。进去的25个人中有15个人是不可能获得商品的。所以可以根据进入的先后顺序只能前10个人购买成功。后面15个人就抛商品已秒杀完。

 

假设我们的秒杀场景

比如某商品10件物品待秒. 假设有100台web服务器(假设web服务器是Nginx + Tomcat),n台app服务器,n个数据库

第一步 如果Java层做过滤, 可以在每台web服务器的业务处理模块里做个计数器AtomicInteger(10)=待秒商品总数,decreaseAndGet()>=0的继续做后续处理, <0的直接返回秒杀结束页面,这样经过第一步的处理只剩下100台*10个=1000个请求。

第二步, memcached 里以商品id作为key的value放个10, 每个web服务器在接到每个请求的同时, 向memcached服务器发起请求, 利用memcached的decr(key,1)操作返回值>=0的继续处理, 其余的返回秒杀失败页面,这样经过第二步的处理只剩下100台中最快速到达的10个请求。

第三步, 向App服务器发起下单操作事务。

第四步, App服务器向商品所在的数据库请求减库存操作(操作数据库时可以 “update table set count=count-1 where id=商品id and count>0;” update 成功记录数为1, 再向订单数据库添加订单记录, 都成功后提交整个事务, 否则的话提示秒杀失败,用户进入支付流程。

 

看看淘宝的秒杀

一、前端

面对高并发的抢购活动,前端常用的三板斧是【扩容】【静态化】【限流】

  • 扩容:加机器,这是最简单的方法,通过增加前端池的整体承载量来抗峰值。

  • 静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。

  • 限流:一般都会采用IP级别的限流,即针对某一个IP,限制单位时间内发起请求数量。或者活动入口的时候增加游戏或者问题环节进行消峰操作。

  • 有损服务:最后一招,在接近前端池承载能力的水位上限的时候,随机拒绝部分请求来保护活动整体的可用性。

 

二、那么后端的数据库在高并发和超卖下会遇到什么问题呢

  • 首先MySQL自身对于高并发的处理性能就会出现问题,一般来说,MySQL的处理性能会随着并发thread上升而上升,但是到了一定的并发度之后会出现明显的拐点,之后一路下降,最终甚至会比单thread的性能还要差。

  • 其次,超卖的根结在于减库存操作是一个事务操作,需要先select,然后insert,最后update -1。最后这个-1操作是不能出现负数的,但是当多用户在有库存的情况下并发操作,出现负数这是无法避免的。

  • 最后,当减库存和高并发碰到一起的时候,由于操作的库存数目在同一行,就会出现争抢InnoDB行锁的问题,导致出现互相等待甚至死锁,从而大大降低MySQL的处理性能,最终导致前端页面出现超时异常。

针对上述问题,如何解决呢? 淘宝的高大上解决方案:

I:关闭死锁检测,提高并发处理性能。

II:修改源代码,将排队提到进入引擎层前,降低引擎层面的并发度。

III:组提交,降低server和引擎的交互次数,降低IO消耗。

解决方案1:将存库从MySQL前移到Redis中,所有的写操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。然后通过队列等异步手段,将变化的数据异步写入到DB中。

优点:解决性能问题

缺点:没有解决超卖问题,同时由于异步写入DB,存在某一时刻DB和Redis中数据不一致的风险。

解决方案2:引入队列,然后将所有写DB操作在单队列中排队,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。

优点:解决超卖问题,略微提升性能。

缺点:性能受限于队列处理机处理性能和DB的写入性能中最短的那个,另外多商品同时抢购的时候需要准备多条队列。

解决方案3:将写操作前移到MC中,同时利用MC的轻量级的锁机制CAS来实现减库存操作。

优点:读写在内存中,操作性能快,引入轻量级锁之后可以保证同一时刻只有一个写入成功,解决减库存问题。

缺点:没有实测,基于CAS的特性不知道高并发下是否会出现大量更新失败?不过加锁之后肯定对并发性能会有影响。

解决方案4:将提交操作变成两段式,先申请后确认。然后利用Redis的原子自增操作,同时利用Redis的事务特性来发号,保证拿到小于等于库存阀值的号的人都可以成功提交订单。然后数据异步更新到DB中。

优点:解决超卖问题,库存读写都在内存中,故同时解决性能问题。

缺点:由于异步写入DB,可能存在数据不一致。另可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。

总结

1、前端三板斧【扩容】【限流】【静态化】

2、后端两条路【内存】+【排队】

另一篇:秒杀系统优化思路

一、秒杀业务为什么这么难做

秒杀系统,库存只有一份,所有人会在集中的时间读和写这些数据。

例如:

  • 小米手机每周二的秒杀,可能手机只有1万部,但瞬间进入的流量可能是几百几千万。
  • 12306抢票,票是有限的,但是抢票的人很多,都读取相同的库存。读写冲突,锁非常严重,这是业务难的地方。

那我们怎么优化秒杀业务呢?

二、优化方向

(以上的两个场景要优化有两个方向)

  • 将请求尽量拦截在系统上游(不要让锁冲突落到数据库上去)。传统的秒杀系统之所以挂,是因为请求都压到后端数据层,数据读写冲突严重,并发高响应慢,几乎所有请求都超时,流量最大,下单成功的有效流量非常小。以12306为例,一趟火车其实只有2000张票,但是抢到的人却有200万,基本没人能买票成功,请求有效率为0.
  • 充分利用缓存,秒杀买票,这是一个典型的读多写少的应用场景,大部分请求是车次/票查询,下单和支付才是写请求。一趟火车只有2000张票,200万人来买,最多2000人下单成功,其他人都是查库存,写入操作的比例是0.1%,而读取的操作比例是99.9%,非常适合缓存来做优化。

三、常见秒杀架构

常见的秒杀架构基本是这样的

  • 浏览器端,最上层,会执行一些JS代码
  • 站点层,这一层会访问后端数据,将操作响应返回给浏览器
  • 服务层,向上游屏蔽底层数据细节,提供数据访问
  • 数据层,最终的“库存”会存放在这里,mysql是一个典型(当然还有缓存),这张图虽然简单,但是能形象的说明大流量高并发的秒杀业务架构(根据笔者从业的经验,基本所有公司的软件架构都脱离不了这几层,大同小异),后面详细解  析各层级怎么优化。

四、各层优化细节

  • 第一层:客户端怎么优化(浏览器层,APP层)

大家应该都玩过微信摇一摇抢红包,是每一次摇一摇,就会往后端发送请求么?

回顾一下我们12306刚出来那年抢票的场景,点击“查询”按钮之后,系统卡在那里或者响应非常慢,这时用户就会再次点击”查询“,继续点点点,可是这样有用么?徙增系统负载,如果真实购买用户只有200W,那一个用户多点击5次,

就有1000万,多出来80%的用户怎么整?

    • 产品层面优化:用户点击查询或者购票操作后,按钮置灰,禁止用户重复提交。
    • JS层面优化:限制用户在x秒内只能提交一次请求。

上面说到摇红包,就算我们疯狂的把手机甩飞了,系统也只是在x秒才向系统发送请求。

这就是所谓的“将请求尽量拦截在系统上游”,越上游越好,浏览器层,APP层就给拦住,这样就挡住了多出那80%的用户请求。

但是,这种办法只能拦截住普通的用户,对于高端的程序猿们来说是拦不住的,firebug一抓包,http长啥样都知道了,js是拦不住程序员写for循环调用http接口的,这部分请求如何处理?

  • 第二层:站点层面的请求拦截

怎么防止程序猿们for循环请求呢?有去重依据么?ip?cookie-id?这类“秒杀”业务都需要登录,用我们加了密的uid即可。在站点层面,对uid进行请求计数和去重,一个uid在5秒内只允许1个请求(例如生成uid时加入时间戳),

这样就可以拦截住程序猿们的for循环请求。

5秒只透过一个请求,那其他请求怎么办?缓存,页面缓存,同一个uid访问频度做页面缓存,x秒内到达站点请求,均返回同一个页面。 如此限流,既能保证用户体验又能保证系统的健壮性。

(页面缓存不一定要保证所有站点返回一致的页面,直接放在每个站点的内存也可以,优点是简单。缺点是http请求落到不同的站点,返回的车票数据可能不一样。)

这是站点层请求拦截和缓存的优化

如果,有黑客控制了10万个肉鸡,不同的uid,同时发送请求的话,我们怎么办?站点层按照uid限流已经拦截不住了。

  • 第三层:服务层拦截

(反正不要让请求落到数据层上)

服务层如何来拦截呢?请求队列,对于写入操作的请求,每次只透有限的请求去数据层,这个有限取决于有多少部小米手机或多少张火车票。如果库存不够则全部返回“已售完”。

对于读取的请求如何优化?用cache抗 ,不管是mecached还是redis,单机抗个每秒10万都没问题,如此限流,只有非常少的写入请求,和非常少的读取缓存mis的请求会透到数据层去,又有99.9%的请求被拦住了。

  • 第四层:数据层

浏览器拦截了80%,站点层拦截了99.9%并做了页面缓存,服务层又做了请求队列与数据缓存,每次透到数据层的请求都是可控的。db基本没什么压力了,还是那句话,库存是有限的,透这么多请求来数据库没有意义。

全部透到数据库,100万个下单,0个成功,透3000到数据库,全部成功。请求有效率为100%。

总结:

再重复一下关于秒杀系统的两个优化思路:

  • 尽量将请求拦截在系统上游(越上游越好)
  • 读多写少的应用多使用缓存(缓存抗读压力)
  • 浏览器和APP:做限速
  • 站点层:按照uid限速,做页面缓存
  • 服务器:按照业务做写请求队列控制流量,做数据缓存
  • 数据层:闲庭信步
  • 并且结合业务做优化。

文章内容来源于微信公众号“架构师之路”,欢迎大家关注。

赞(2)
转载请保留本文链接:http://www.jiagouc.com/the-second-kill-system-design-points-and-optimization-ideas/
分享到: 更多 (0)