什么是秒杀
秒杀场景一般会在电商网站举行一些活动或者节假日在12306网站上抢票时遇到。对于电商网站中一些稀缺或者特价商品,电商网站一般会在约定时间点对其进行限量销售,因为这些商品的特殊性,会吸引大量用户前来抢购,并且会在约定的时间点同时在秒杀页面进行抢购。如本例中的银行产品。
秒杀系统场景特点
- 秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
- 秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
- 秒杀业务流程比较简单,一般就是下订单减库存。
有了上面的情景以及引出来的问题,来看看秒杀方案的设计思路,我们服务器如何应对这巨量的TPS(Transactions Per Second,每秒处理的事务数目)呢?
首先想到的是扩容,但这是不现实的,因为扩容需要很多很多机器,TPS 增加一万倍对物理服务器的性能要求远远不止一万倍。
另外对于一个商家来说,为了这一次促销活动购置服务器是不划算的,平时势必有众多的机器处于闲置状态。
没法扩容,那么也就意味着要使用其他方法,如果所有请求访问一台物理机器肯定不行,一百万的数据访问无论如何分库分表都无济于事,因为面对的每一条都是热点数据,所以要用到分布式架构的思路。
很明显,要让大规模用户能够同时打开抢货的网页,势必要用要到 CDN。同时利用我们分布式中限流、网关等知识,将请求层层筛选,降低最后连接到数据库的请求。即使用 CDN 的边缘结点来扛流量,然后过滤用户请求(限流用户请求),来保护数据中心的系统。
CDN 主要作用有两个:
一方面是将一些不会改变的静态资源放到离客户端较近的边缘服务器上。
这样客户端请求数据的时候可以直接从边缘服务器获取,降低中心服务器的压力。
另外一方面可以把小服务部署到 CDN 结点上去,这样,当前端页面来问开没开始时,这个小服务除了告诉前端开没开始外,它还可以统计下有多少人在线。
每个小服务会把当前在线等待秒杀的人数每隔一段时间就回传给我们的数据中心,于是我们就知道全网总共在线的人数有多少。
利用 CDN 将静态资源分发在边缘服务器上,当进行服务请求时,先进行鉴权,鉴权主要是筛选机器人等非人工抢购,根据实际经验,鉴权可以筛选很大一部分用户,例如是否登录。
当鉴权确定是真实有效的用户之后,通过负载均衡,也就是 LVS+Keepalived 将请求分配到不同的 Nginx 上。
一般会建立 Nginx 集群,然后再通过网关集群,即使这样还是要增加一些限流措施。
如果到这一步还是有很多请求压到数据库势必撑不住,那么可以采取服务限流、服务降级等措施,进行削峰处理。
到这儿理论上流量就不高了,如果还是很高,后面就将热点数据放进缓存集群中进行预热,同时设置定时任务。
一方面关注数据库与缓存的一致性,另一方面关闭超时未支付的订单,当订单提交之后交给任务队列,生成订单、修改数据库、做好持久化工作。
架构图如下(可点击查看大图):
1. 超卖的原因
假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致超发。
在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。
2. 悲观锁思路
解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。
悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。
3. 乐观锁思路
这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。
有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。
限流: 鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。
削峰:对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。
异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。
内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。
可拓展:当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了。像淘宝、京东等双十一活动时会增加大量机器应对交易高峰。
将请求拦截在系统上游,降低下游压力:秒杀系统特点是并发量极大,但实际秒杀成功的请求数量却很少,所以如果不在前端拦截很可能造成数据库读写锁冲突,甚至导致死锁,最终请求超时。
充分利用缓存:利用缓存可极大提高系统读写速度。
消息队列:消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。
然后请求到达时通过要秒杀的id号到Redis中去查询预减库存,只要商品库存不到0就执 行下一步,(因为Redis的操作是具有原子性所以不会出现超卖的问题)(重点)
关键的decr方法(redis中的原子递减,根据key减value值)decr key将指定key的value原子性递减1,就相当于java中的–i。 如果该key不存在,其 初始值为0,在decr之后其值为-1。如果value的值不能转成整型,如helllo,该操作将 执行失败并返回相应的错误信息。
需要了解的问题
1、什么是漏桶流,
NGINX限流使用漏桶算法(leaky bucket algorithm),该算法广泛应用于通信和基于包交换计算机网络中,用来处理当带宽被限制时的突发情况。和一个从上面进水,从下面漏水的桶的原理很相似;如果进水的速率大于漏水的速率,这个桶就会发生溢出。
在请求处理过程中,水代表从客户端来的请求,而桶代表了一个队列,请求在该队列中依据先进先出(FIFO)算法等待被处理。漏的水代表请求离开缓冲区并被服务器处理,溢出代表了请求被丢弃并且永不被服务。
2、什么是MQ的削峰
MQ的削峰也和漏桶流有点相似道理,只不过他是一个永远不会漏的桶,请求在多没关系,我先接着,我慢慢来,不管你多少请求我都按照我的速度慢慢来,保证消息不丢失。
3、Tomcat最多支持并发多少用户?
Tomcat 默认配置的最大请求数是 150,也就是说同时支持 150 个并发,当然了,也可以将其改大。
当某个应用拥有 250 个以上并发的时候,应考虑应用服务器的集群。
具体能承载多少并发,需要看硬件的配置,CPU 越多性能越高,分配给 JVM 的内存越多性能也就越高,但也会加重 GC 的负担。
操作系统对于进程中的线程数有一定的限制:
Windows 每个进程中的线程数不允许超过 2000
Linux 每个进程中的线程数不允许超过 1000
另外,在 Java 中每开启一个线程需要耗用 1MB 的 JVM 内存空间用于作为线程栈之用。
Tomcat的最大并发数是可以配置的,实际运用中,最大并发数与硬件性能和CPU数量都有很大关系的。更好的硬件,更多的处理器都会使Tomcat支持更多的并发。
4、Nginx支持多少并发
单个Nginx并发尽量不要超过2万,如果超过了就要做集群了,虽然官方监测能支持5万并发
5、MySql最大的并发
MySql最大的并发量500-1000,好一点的服务可以支持1000-2000,超过了还是要集群
核心问题
1、库存超卖
只有10个库存,但是一秒钟有1k个订单,怎么能不超卖呢?
核心思想就是保证库存递减是原子性操作,10–返回9,9–返回8,8–返回7。
而不能是读取出来库存10,10-1=9再更新回去。因为这个读取和更新是并发执行的,很可能就会有1k个订单都成功了,而库存实际只有10。
那么,怎么保证原子性操作呢?
1 .数据库判断库存不能小于0(我们使用到了,比他吊一点,乐观锁)
update product set left_num=left_num-1 where left_num>0;
2.分布式锁(我们没有使用)
用redis来做一个分布式锁,reids->setnx(‘lock’, 1) 设置一个锁,程序执行完成再del这个锁。
锁定的过程,不利于并发执行,大家都在等待锁解开,不建议使用。
3 .消息队列(我们使用了)
将订单请求全部放入消息队列,然后另外一个后台程序一个个处理队列中的订单请求。
并发不受影响,但是用户等待的时间较长,进入队列的订单也会很多,体验上并不好,也不建
4 redis递减(我们使用了,decr方法就是原子性递减)
通过 redis的:decr(‘key’)方法 以原子性的方式得到递减之后的库存数。
性能方面很好,同时体验上也很好
2、集群怎么来规划
前端服务器不用管,集群的数量不受影响。
redis的性能可以达到每秒几万次响应,所以一个集群的规模,也就是redis服务可以承载的数量。
比如:一台前端服务器是1-2k的qps(有库存时),那么10台+1台redis就可以是一个独立的集群,可以支撑1-2w每秒订单量。
10个上述的集群就可以做到一秒钟处理10w~20w的有效订单。
如果秒杀活动的库存量在1w以内,预计参与的人数在百万左右,那么有一个集群也就可以搞定。
如果秒杀参与的人数超过千万,那么就要用到不止一个集群了。
3、多个集群的数据怎么保持一致性啊
不要做多集群的数据同步,而是用散列,每个集群的数据是独立存在的。
假设,有10个商品,每个商品有1w库存,规划用10个集群,那么每个集群有10个商品,每个商品是1k库存。
每个集群只需要负责把自己的库存卖掉即可,至于说,会不会有用户知道有10个集群,然后每个集群都去抢。
这种情况就不要用程序来处理了,利用运营规则,活动结束后汇总订单的时候再去处理就好了。
如果担心散列的不合理,比如:某个集群用户访问量特别少,那么可以引入一个中控服务,来监控各个集群的库存,然后再做平衡。
4、机器人抢购怎么办
没什么太好的办法,类似DDOS攻击,只能是让自身更强大才是王道。
运营策略上,可以严格控制用户注册,必须登录,提交订单的时候引入图像验证码,问答,交互式验证等。
5.你的秒杀系统可以做到多少并发量。
前台我没做,
我就光说后台吧,使用JMeter测试的吧
首先Nginx
1000个并发的秒杀,俩台台tomcat,几秒就搞定,具体的我不记得了,Jm测试工具是时间戳显示
假如做10000并发的秒杀就要很多台服务器
我就假如做10000并发的秒杀,资源是5台tomcat的话,Nginx就不怕,Nginx2万并发随随便便,超过2万并发最好使用集群
在Nginx定义限流维度,每秒1000个请求漏下去,漏到Tomcat,大概呢就是十几二十秒秒杀完。
在说Tomcat,Tomcat可以接受150-250并发之间,超过250并发最好也使用集群
redis的性能可以达到每秒几万次响应,所以一个集群的规模,也就是redis服务可以承载的数量。
比如:一台前端服务器是1-2k的qps(有库存时),那么10台+1台redis就可以是一个独立的集群,可以支撑1-2w每秒订单量。