秒杀系统解决方案

我看了二十篇左右的秒杀系统设计及解决方案的文章,从架构、产品、前端、后端四个层面分别总结了一些解决方案。

要点总结:

1.架构:扩容,业务分离,数据分离

2.产品:下单按钮控制,秒杀答题削峰,简化页面设计

3.前端:限流(反作弊) 静态化

4.后端:内存 队列

一、秒杀一般会带来2个问题:

1、高并发

比较火热的秒杀在线人数都是10w起的,如此之高的在线人数对于网站架构从前到后都是一种考验。

2、超卖(也有叫超发)

任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每个抢购活动都要面临的难题。

实际上超卖问题是高并发带来的一个子问题,但是因为这个问题太过致命,所以我们把他的解决方案单独拿出来说。

解决办法:

1、将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false。

2、采用数据库的悲观锁思路,select ...for update。见《MySQL锁之三:MySQL的共享锁与排它锁编码演示

3、采用数据库的乐观锁思路,常见的就是大多是基于数据版本( Version )记录机制实现。(即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。)

4、采用Redis中的watch。见《Redis事务和watch

5、采用FIFO队列思路。(直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程。因为请求很多,很可能一瞬间将队列内存“撑爆”

6、采用文件锁的思路。单台物理机上是可以,分布式集群下不可行。见《NIO文件锁FileLock

二、如何解决?

1.架构层面:

秒杀架构设计原则:
  1. 尽量将请求拦截在系统上游

  2. 读多写少的常用多使用缓存

扩容

说白了加机器

一、隔离

对于这个促销系统的部署完全与主站的其他系统隔离开,避免大流量冲击下对其他系统造成影响,甚至雪崩。

  • 域名隔离 使用独立的域名对请求做隔离
  • 应用隔离 使用独立的集群机器部署前端应用与服务端应用
  • 数据隔离 使用独立的Redis集群、mysql集群处理数据。将即将被秒杀的热数据维护到redis,秒杀所调用的数据大部分都是热数据,比如会启用单独cache集群或MySQL数据库来放热点数据,目前也是不想0.01%的数据影响另外99.99%。

系统隔离更多是运行时的隔离,可以通过分组部署的方式和另外99%分开。秒杀还申请了单独的域名,目的也是让请求落到不同的集群中。即使秒杀系统崩溃了,也不会对网站造成影响。

 

二、前端流控/产品层面

  • 控制秒杀商品页面抢购按钮的可用/禁用(购买按钮只有在秒杀开始的时候才能点亮,在此之前是灰色的,显示活动未开始。)
  • 秒杀页面设计简化(秒杀场景业务需求与一般购物不同,用户更在意的是能够抢到商品而不是用户体验。所以秒杀商品页面应尽可能简单并且拍下后地址等个人信息应该使用默认信息,减轻秒杀进行时系统负载,若有更改可以在秒杀结束后进行更改。)
  • 基本校验 一些简单的资格校验,如登陆等等
  • 页面静态数据缓存 浏览器缓存静态数据、CDN缓存静态数据。
  • 业务流程削峰 通过使用验证码或者秒杀问答题,拉开秒杀时间,放缓瞬时并发。(秒杀答题一个很重要的目的是为了防止秒杀器。还有一个重要的功能,就是把峰值的下单请求给拉长了,从以前的1s之内延长到2~10s左右,请求峰值基于时间分片了,这个时间的分片对服务端处理并发非常重要,会减轻很大压力,另外由于请求的先后,靠后的请求自然也没有库存了,也根本到不了最后的下单步骤,所以真正的并发写就非常有限了。其实这种设计思路目前也非常普遍,如支付宝的“咻一咻”已及微信的摇一摇。)

三、前端层面

静态化以及页面缓存

将页面能够静态的部分都静态化,并将静态页面缓存于CDN,以及反向代理服务器,可能还要临时租借服务器。 利用 页面静态化、数据静态化,反向代理 等方法可以避免 带宽和sql压力 ,但是随之而来一个问题,页面抢单按钮也不会刷新了,可以把 js 文件单独放在js服务器上,由另外一台服务器写 定时任务 来控制js 推送。 另外还有一个问题,js文件会被大部分浏览器缓存,我们可以使用xxx.js?v=随机数 的方式来避免js被缓存。

限流(反作弊)

1.针对同一个用户id来实现,前端js控制一个客户端几秒之内只能发送同一个请求,后端校验同一个uid在几秒之内返回同一个页面

2.针对同一个ip来实现,进行ip检测,同一个ip几秒之内不发送请求或者只返回同一个页面

3.针对多用户多ip来实现,依靠数据分析

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

重启与过载保护

如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启。如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间。

秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回。

4.后端层面:

1.加入缓存redis:

因为秒杀是典型的读多写少的场景,适合操作内存而非操作硬盘;缓存工具redis本身的操作是保证原子性的,所以可以保证请求了redis的写的操作的线程安全性。

2.加入消息队列,利用队列进行削峰:

将用户请求放置于一个或多个队列中,队列中元素总和等于该商品库存总和,未进入队列的请求均失败。利用多线程轮询分别从一个或多个队列中取出用户请求。操作redis进行减库存操作,成功减库存之后返回成功,并将用户信息与商品信息存入另一个队列当中,进行生成订单的操作。利用两个队列异步处理业务减轻秒杀高峰时期服务器负载。

减库存操作
一种是拍下减库存 另外一种是付款减库存;目前采用的“拍下减库存”的方式,拍下就是一瞬间的事,对用户体验会好些。
3.程序计数器:

队列与缓存为了保证请求redis的次数不超过总的库存量,利用一个程序计数器来这一点。程序计数器用JUC包下原子类可以实现。

4.分布式锁

分布式情况下可以利用分布式锁来解决任务每次只能由一次服务来执行且不能重复执行。 分布式锁的实现:zk、redis 分布式锁的优化:先考虑是否可以去锁,然后考虑尽可能多用乐观锁,少用悲观锁。这里有一个问题,乐观锁如果每一次都会有并发冲突的话性能反而不如悲观锁,那么难道真的多用乐观锁性能会比悲观锁高吗?选举考虑ha,比如心跳检测。

5.分布式去锁 方案

利用集群并发加入队列,选举队列处理服务单点执行,这样可以保证并发实现和加锁一样的并发量但不会影响性能。

1.控制秒杀商品页面抢购按钮的可用/禁用。
原文地址:https://www.cnblogs.com/duanxz/p/3501466.html