既然是秒杀系统,那肯定是非常多的人一起买一个商品,首先要做的到的是你的网站需要能承受上万的访问压力,不能像学校的抢课系统那样, 几千人人抢课,系统就崩溃进不去了。 要解决这个问题: 1:前端的页面静态化 ,什么是页面静态化?比如说在秒杀页面,可能除了秒杀按钮,秒杀倒计时,还有比如背景图,商品推荐什么的,像这些页面的数据,应该避免重复去想服务器请求数据,在刷新秒杀页面时,应该用局部刷新的方式,以最小的数据量请求到最新数据。 2:前端接口限流 比如说按钮只能提交一次,之后就置灰色,这样可以简单的防止一般用户页面上的重复提交(无法避免使用工具的请求访问); 3:后端接口限流 后端可以通过自定义自定义注解的方式,通过把用户的id缓存到redis,限制用户在多少秒只能请求多少次 4:后端商品数据缓存 ,用户查询商品的数据,如果每个用户都重数据库去查询,那数据库肯定是顶不住,这个时候就需要使用redis去缓存商品数据 (需要注意redis的缓存击穿,缓存穿透等问题) 5:集群部署,如果是高并发的环境下,单台服务器,单台redis肯定是难以满足需求的,需要做系统的集群部署,reids的集群部署 6:分布式session 用户登录就不能简单的用session去缓存数据,这个时候可以用redis做分布式session
秒杀时间一到,这个时候是用户请求量最大的时候,应该如何去处理这些请求呢?
1、流量削峰; 什么是流量削峰,正常情况下,我们秒杀系统的流量访问大小应该是,
在秒杀前几分钟,需要参与秒杀的人陆陆续续的进来,请求的流量慢慢增大,到秒杀开始时间的一瞬间,流量达到最大(这就是峰), 然后流量在慢慢的降下来,
削峰就是让它的访问流量不要这一瞬间突然变大,而是在控制在一个区间里慢慢变大 为什么要削峰 通常秒杀系统为什么要进行削峰呢?或者说峰值会带来哪些坏处? 我们知道服务器的处理资源是恒定的,你用或者不用它的处理能力都是一样的,所以出现峰值的话, 会让服务器在那一个瞬间特别忙,然后有闲下去,但是由于要保证服务质量,我们很多的处理资源只能按照忙的时候来预估,而这会导致资源的一个浪费。 比如它的峰值需要6台服务器集群才能处理,之后的请求只需要3台就能处理,那大部分时间就有三台服务器是处于闲置状态的 所以我们需要让用户的请求尽可能的平缓 削峰的一些操作思路:排队、答题、分层过滤
1.消息队列解决削峰;2.流量削峰漏斗:层层削峰、3、验证码解决削峰
1.消息队列解决削峰 要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送, 中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。 不过这样虽然保护了系统处流量请求的平缓,但消息队列的入口处流量依然很大,有压垮消息队列的风险
2.流量削峰漏斗:层层削峰 针对秒杀场景还有一种方法,就是对请求进行分层过滤,从而过滤掉一些无效的请求。 系统可以通过一些校验,判断哪些请求可能来自于脚本,哪些请求是重复的无效请求,哪些是来自异常状态用户的请求 或者是哪些请求是是专门的羊毛党,把这些请求过滤出去 分层过滤其实就是采用“漏斗”式设计来处理请求的,如下图所示:
3、验证码 在用户秒杀按钮时,弹出验证码后在发起请求,这样做有什么好处呢? 用户输入验证码可能会需要1-3s,这样之后服务器收到的请求就在1-3s这个区间
三、使用mq异步下单 异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。
比如这段代码,在确认库存量后,没有直接调用secKill方法生成订单,而是发送一条消息到mq消息队列,让消息队列去异步处理下订单
每次下单前先检查redis里缓存的商品数量,如果大于0,就扣减库存,下订单,如一下代码
decrement方法的作用是读取这个redis的key的值并减1,在减1的过程中,其他线程读取redis的值时,不会读到相同的
//预减库存
Long decrement = valueOperations.decrement("seckillGoods:" + goodsId);
if (decrement<0){
//如果小于0,说明没有库存了,内存中标记商品为true,表示卖完
emptyStockMap.put(goodsId,true);
valueOperations.increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
单redis做分布式锁的时候需要注意其原则性的问题,比如如下代码: 这就是一段有问题的锁,如果在商品还剩下1个的时候,在第三行代码执行前,有多个线程执行了第一行, 那就会有多个线程以为还有1个商品,同时走下单的流程,导致商品超卖
Integer o = (Integer)valueOperations.get("seckillGoods:" + goodsId);
o--;
valueOperations.set("seckillGoods:" + goodsId,o);
if (o < 0) {
//如果小于0,说明没有库存了,内存中标记商品为true,表示卖完
emptyStockMap.put(goodsId,true);
valueOperations.increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
抢购接口的url不应该是固定的,如果是固定的,那如果有人知道抢购的接口地址,那么他就能提前开脚本,写定时任务,在时间到的时候就发起请求, 一般脚本的速度是远大于人手指点击的数度的,所以很有可能导致抢购商品全部都被黄牛抢走 如何去解决,可以通过动态生成url的方式,每个用户动态生成自己唯一的url 实现步骤是,当发起抢购的时候,不去请求真正的访问地址,而是去获取真正的动态访问地址,再去请求真正的地址