秒杀系统设计

175 阅读8分钟

四要一不要

1.数据尽量要少 一方面是网络传输,数据越少就越快。 一方面是两边机器发送数据的时候需要对数据进行拼接/计算/分析/处理 + 解压缩/编解码 + 序列化。

2.请求连接数量尽量要少 因为连接握手建立始终是重资源消耗。比如说,前端请求的时候,可以尽量减少请求数量或者放在一个请求里面。不管是代码层面尽量在一个文件里,还是请求路径url里包含了多个资源地址然后在服务器端收到url进行解析出来多个资源请求url。

3.请求路径尽量要短 这个路径不是具体的url,而是经过的路由的机器节点,两个原因1.节点越多,越不稳定2.也越慢

4.依赖业务服务尽量要少 特别是非核心业务服务,一旦核心业务不行撑不住,要自动降级非核心业务服务。确保核心业务可以正常执行。

5.避免单点 集群

避免单点

核心目标
避免服务持有数据/状态,否则很难保证数据一致性问题。


两种情况
1.业务服务
比如,service类、dao类 //集群
2.存储服务
比如,缓存中间件、消息中间件 //每个单点,都要做冗余,两种方法1.主从备份2.多主从

nginx的作用

第三,让谁来缓存静态数据也很重要。不同语言写的 Cache 软件处理缓存数据的效率也各不相同。以 Java 为例,因为 Java 系统本身也有其弱点(比如不擅长处理大量连接请求,每个连接消耗的内存较多,Servlet 容器解析 HTTP 协议较慢),所以你可以不在 Java 层做缓存,而是直接在 Web 服务器层上做,这样你就可以屏蔽 Java 语言层面的一些弱点;而相比起来,Web 服务器(如 Nginx、Apache、Varnish)也更擅长处理大并发的静态文件请求。

前后端项目分离、动静资源文件分离的区别

1.动静资源文件分离
一个是固定的静态资源,这个是不变的,比如,一个图片、一个不常变动数据的html的等等。会放到nginx代理服务器上,或者缓存到商用cdn服务器上。

你可能会问,存储在浏览器或 CDN 上,有多大区别?我的回答是:区别很大!因为在 CDN 上,我们可以做主动失效(文件路径version机制),而在用户的浏览器里就更不可控,如果用户不主动刷新的话,你很难主动地把消息推送给用户的浏览器。

2.前后端项目分离 就是按项目的维度,而不是单个资源文件的维度。

动态静态资源文件分离

现在不都是前后端分离嘛, 不知道为啥还要将动静分离拿出来说事, 前端页面部署CDN早就标配了, 对于秒杀操作, 没有哪个平台是让用户刷新页面的, 都是到时间后点击按钮来秒杀, 而且这个缓存策略不应该是第一次访问不到再查询缓存, 应该在商家创建完秒杀产品后, 缓存就有了, 如果修改内容, 就直接更新缓存, 秒杀开始后, 商家就不能再修改了, 不想抬杠, 反正我认为这篇文章没啥干货

热点数据-二八原则

20%的数据被80%的请求访问。

削峰、填谷

削峰

是什么
峰就是类似上班的早高峰 晚高峰。人流量特别大。
削就是减少人流量,其实就是数据的流量。


解决方案
1.慢慢排队 //把瞬时的请求数量缓存起来,按顺序处理 不让你一下所有的请求——服务器,而是先暂时保存起来到请求队列。基于消息队列实现。然后,采用异步回调来响应数据。

总结
本质上仍然还是缓存。只不过不同的缓存实现不一样,有的是缓存中间件,有的是消息中间件,有的是直接机器内存。

2.验证码 //减少瞬时的请求数量,不管是让人少刷次数还是让机器人少刷次数,都是为了减少同一个用户单位时间的请求次数
各种类型的验证码,比如手机短信验证码、答题、图片、识别数字


消息队列-排队方案

  1. 利用线程池加锁等待也是一种常用的排队方式;
  2. 先进先出、先进后出等常用的内存排队算法的实现方式;
  3. 把请求序列化到文件中,然后再顺序地读文件(例如基于 MySQL binlog 的同步机制)来恢复请求等方式。

填谷

库存

主要是库存的线程安全问题。

如何解决库存的线程安全问题

两种解决方案
1.数据库本身的锁(悲观锁) //优点是透明,缺点是对高并发不友好
所谓线程安全,就是多个线程同时访问同一个商品product的同一个数据(即库存数据),其实也就是多线程对同一行记录的并发访问。

其实数据库本身就支持锁机制,就是多个事务同时操作访问同一个行记录,就上锁。原理和程序里的锁是一样的。也就是说,同一时刻,只能有一个线程的事务在更新同一行记录,其他线程的事务排队。

所以,数据库本身就是线程安全,即多事务安全。

2.基于数据库的乐观锁 //优点是高并发,缺点是需要自己额外实现乐观锁功能
一般来说基于数据库的乐观锁实现有三种方式,
1)基于数据库的主键id约束,同时只能有一个事务写成功。
2)基于数据库的唯一性约束(需要显式加读锁-互斥锁),同时只能有个事务读成功。
3)基于数据库的version字段,输入数据比数据库数据大才更新成功。//最佳实践

最佳实践
第二种解决方案的第三种方法,即基于数据库version字段更新实现乐观锁。


7.4 乐观锁思路
这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。

有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

减库存的三种时机

什么时候减?
首先我们明白一个问题,就是网络购物分两步
1.下单
2.付款

其实,最终肯定是必须付款,才真正的会扣库存。

有三种方案
1.下单之后就减库存
优点是准确。
缺点是下单之后,不一定付款购买。
2.付款之后就减库存
优点是确保付款扣库存。
缺点是下单数量可能太大超过库存,后面下单成功的可能会骂娘,用户体验不好。
3.支付时间范围内(比如15分钟),减库存;超时,即恢复库存


不同场景适合不同的方案
1.火车票 飞机票
下单之后,很可能不付款,所以适合付款之后才扣库存。
2.秒杀
一般抢到就是赚到,所以这个时候适合下单即扣库存。

把库存数据放到缓存redis

目的是提高高并发,因为数据库访问太慢太耗费资源。


缓存redis和数据库的同步问题

单机数据库mysql-每秒请求多少?

不到1万。几千个连接,千位数级别。

解决方案

1.前端优化
产品层面:提交按钮置灰
js层面:X秒之内只能提交一次

2.后台优化
X秒之内只能提交一次 //基于缓存<uid,时间>

读多写少,使用缓存中间件 //因为秒数商品或票数量有限,可能就几千个,但是有几万 几十万 几百万人同时抢


请求连接线程队列
因为后台接受请求的时候,本来就是先来先执行,所以没有必要搞什么请求队列去排队。

参考

极客时间
time.geekbang.org/column/arti… //评论写的也很好,可以看一下。

mp.weixin.qq.com/s?__biz=MjM…

www.cnblogs.com/study-every… //写的很好 很完整