在前面针对数据库的优化中,因为数据库行级锁存在竞争形成大量的串行阻塞,咱们使用了存储过程(或者触发器)等技术绑定操做,整个事务在MySQL端完成,把整个热点执行放在一个过程中一次性完成,能够屏蔽掉网络延迟时间,减小行级锁持有时间,提升事务并发访问速度。html
但是问题时并发的流量实际上都是直接穿透让MYSQL本身去抗,好比说库存是否卖完以及用户是否重复秒杀都彻底是靠查询数据库去判断,形成数据库没必要要的负担很是大,然而这些均可以放在缓存作一个标记在服务层进行拦截,对于中小规模的并发还能够,可是真正的超高并发,显然这个还不完善。前端
方向:将请求尽可能拦截在系统上游ajax
传统秒杀系统之因此挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎全部请求都超时,流量虽大,下单成功的有效流量甚小【一趟火车其实只有2000张票,200w我的来买,基本没有人能买成功,请求有效率为0】 redis
思路:限流和削峰算法
限流:屏蔽掉无用的流量,容许少部分流量流向后端。sql
削峰:瞬时大流量峰值容易压垮系统,解决这个问题是重中之重。经常使用的消峰方法有异步处理、缓存和消息中间件等技术。数据库
异步处理:秒杀系统是一个高并发系统,采用异步处理模式能够极大地提升系统并发量,其实异步处理就是削峰的一种实现方式。后端
缓存:秒杀系统自己是一个典型的读多写少的应用场景【一趟火车其实只有2000张票,200w我的来买,最多2000我的下单成功,其余人都是查询库存,写比例只有0.1%,读比例占99.9%】,很是适合使用缓存。浏览器
消息队列:消息队列能够削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据本身的处理能力,从消息队列中主动的拉取请求消息进行业务处理。缓存
1. 页面静态化
对商品详情和订单详情进行页面静态化处理,页面是存在html,动态数据是经过接口从服务端获取,实现先后端分离,静态页面无需链接数据库打开速度较动态页面会有明显提升。
2.页面缓存
经过CDN缓存静态资源,来抗峰值。不使用CDN的话也能够经过在手动渲染获得的html页面缓存到redis。
1. 使用数学公式验证码
描述:点击秒杀前,先让用户输入数学公式验证码,验证正确才能进行秒杀。
好处:
1)防止恶意的机器人和爬虫
2)分散用户的请求
实现:
1)前端经过把商品id做为参数调用服务端建立验证码接口
2)服务端根据前端传过来的商品id和用户id生成验证码,并将商品id+用户id做为key,生成的验证码做为value存入redis,同时将生成的验证码输入图片写入imageIO让前端展现。
3)将用户输入的验证码与根据商品id+用户id从redis查询到的验证码对比,相同就返回验证成功,进入秒杀;不一样或从redis查询的验证码为空都返回验证失败,刷新验证码重试
2. 禁止重复提交
用户提交以后按钮置灰,禁止重复提交
可利用负载均衡(例如反响代理Nginx等)使用多个服务器并发处理请求,减少服务器压力。
限制同一UserID访问频率:尽可能拦截浏览器请求,但针对某些恶意攻击或其它插件,在服务端控制层须要针对同一个访问uid,限制访问频率。
1. 利用缓存
设置缓存有效时间,在缓存中计数,若是在缓存的有效时间内请求的次数超了的话,就返回请求访问太频繁。
2. 利用RateLimiter
RateLimiter是guava提供的基于令牌桶算法的限流实现类,经过调整生成token的速率来限制用户频繁访问秒杀页面,从而达到防止超大流量冲垮系统。(令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而若是请求须要被处理,则须要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
当用户量很是大的时候,拦截流量后的请求访问量仍是很是大,此时仍需进一步优化。
1. 业务分离:将秒杀业务系统和其余业务分离,单独放在高配服务器上,能够集中资源对访问请求抗压。——应用的拆分
2. 采用消息队列缓存请求:将大流量请求写到消息队列缓存,利用服务器根据本身的处理能力主动到消息缓存队列中抓取任务处理请求,数据库层订阅消息减库存,减库存成功的请求返回秒杀成功,失败的返回秒杀结束。
3. 利用缓存应对读请求:对于读多写少业务,大部分请求是查询请求,因此能够读写分离,利用缓存分担数据库压力。
4. 利用缓存应对写请求:缓存也是能够应对写请求的,可把数据库中的库存数据转移到Redis缓存中,全部减库存操做都在Redis中进行,而后再经过后台进程把Redis中的用户秒杀请求同步到数据库中。
能够将缓存和消息中间件 组合起来,缓存系统负责接收记录用户请求,消息中间件负责将缓存中的请求同步到数据库。
方案:本地标记 + redis预处理 + RabbitMQ异步下单 + 客户端轮询
描述:经过三级缓冲保护,一、本地标记 二、redis预处理 三、RabbitMQ异步下单,最后才会访问数据库,这样作是为了最大力度减小对数据库的访问。
实现:
数据库层是最脆弱的一层,通常在应用设计时在上游就须要把请求拦截掉,数据库层只承担“能力范围内”的访问请求。因此,上面经过在服务层引入队列和缓存,让最底层的数据库高枕无忧。但依然能够进行以下方向的优化:
对于秒杀系统,直接访问数据库的话,存在一个【事务竞争优化】问题,可以使用存储过程(或者触发器)等技术绑定操做,整个事务在MySQL端完成,把整个热点执行放在一个过程中一次性完成,能够屏蔽掉网络延迟时间,减小行级锁持有时间,提升事务并发访问速度。
上面的秒杀流程对应的流程图以下:
步骤1到12,主体是redis预减库存,生成消息队列:
步骤13到14是处理消息队列:
步骤15,是客户端请求秒杀结果:
卖超缘由:
(1)一个用户同时发出了多个请求,若是库存足够,没加限制,用户就能够下多个订单。(2)减库存的sql上没有加库存数量的判断,并发的时候也会致使把库存减成负数。
解决办法:
(1):在后端的秒杀表中,对user_id和goods_id加惟一索引,确保一个用户对一个商品绝对不会生成两个订单。
(2):咱们的减库存的sql上应该加上库存数量的判断
数据库自身是有行级锁的,每次减库存的时候判断count>0,它其实是串行的执行update的,所以绝对不会卖超!。
UPDATE seckill
SET number = number-1
WHERE seckill_id=#{seckillId}
AND start_time <#{killTime}
AND end_time >= #{killTime}
AND number > 0;
2. 如何解决少卖问题—Redis预减成功而DB扣库存失败?
前面的方案中会出现一个少卖的问题。Redis在预减库存的时候,在初始化的时候就放置库存的大小,redis的原子减操做保证了多少库存就会减多少,也就会在消息队列中放多少。
如今考虑两种状况:
1)数据库那边出现非库存缘由好比网络等形成减库存失败,而这时redis已经减了。
2)万一一个用户发出多个请求,并且这些请求恰巧比别的请求更早到达服务器,若是库存足够,redis就会减屡次,redis提早进入卖空状态,并拒绝。不过这两种状况出现的几率都是很是低的。
两种状况都会出现少卖的问题,实际上也是缓存和数据库出现不一致的问题!
可是咱们不是非得解决不一致的问题,自己使用缓存就难以保证强一致性:
在redis中设置库存比真实库存多一些就行。
3. 秒杀过程当中怎么保证redis缓存和数据库的一致性?
在其余通常读大于写的场景,通常处理的原则是:缓存只作失效,不作更新。
采用Cache-Aside pattern:
失效:应用程序先从cache取数据,没有获得,则从数据库中取数据,成功后,放到缓存中。
更新:先把数据存到数据库中,成功后,再让缓存失效。
4. Redis中的库存如何与DB中的库存保持一致?
Redis中的数量不是库存,它的做用仅仅时候只是为了阻挡多余的请求透传到db,起到一个保护DB的做用。由于秒杀商品的数量是有限的,好比只有10个,让1万个请求去访问DB是没有意义的,由于最多只有10个请求会下单成功,剩余的9990个请求都是无效的,是能够不用去访问db而直接失败的。
所以,这是一个伪问题,咱们是不须要保持一致的。
5. 为何要隐藏秒杀接口?
html是能够被右键->查看源代码,若是秒杀地址写死在源文件中,是很容易就被恶意用户拿到的,就能够被机器人利用来刷接口,这对于其余用户来讲是不公平的,咱们也不但愿看到这种状况。因此咱们能够控制让用户在没有到秒杀时间的时候不能获取到秒杀地址,只返回秒杀的开始时间。
当到秒杀时间的时候才返回秒杀地址即seckill_id以及根据seckill_id和salt加密的MD5,前端再次拿着seckill_id和MD5才能执行秒杀。假如用户在秒杀开始前猜想到秒杀地址seckill_id去请求秒杀,也是不会成功的,由于它拿不到须要验证的MD5。这里的MD5至关因而用户进行秒杀的凭证。
6. 一个秒杀系统,500用户同时登录访问服务器A,服务器B如何快速利用登陆名(假设是电话号码或者邮箱)作其余查询?
主从复制,读写分离