秒杀系统总结
1.前言
大学毕业设计,秒杀系统设计了花费了很多心思。
未来几天也要准备考研面试,难免会被问到过去写过的项目。
所以计划对毕业设计中的内容进行全面的复盘。
如果你有设计一个秒杀系统的想法,而又不知从何开始,你可以看下去。
如果你希望他指导你的实际项目开发,那这篇文章大概率不适合你。
2.系统组成部分概述
系统是分布式系统,主要使用的技术栈是springcloud(使用的阿里巴巴开发的组件nacos sentinal,也可以更换传统组件,这无关紧要),springboot,redis,rabbitmq,mysql。
系统分为多个服务,其中包括:电商服务,基于雪花算法的唯一ID生成服务,秒杀请求处理服务,邮件发送及数据落库服务,秒杀定时启动服务。
其中最为重要的是秒杀请求处理服务,秒杀定时启动与关闭服务,后续着重介绍展开介绍以上两个服务。
以下所展示的代码只做讲解,复制粘贴并无效果。具体使用的第三方库文件可自行百度。
3.秒杀请求处理服务讲解
之所以从该服务开始讲起,是因为它是整个秒杀项目的入口。
你可以把自己想象成一个具体参与秒杀的客户。
你的一次点击就从这里开始
/*
* 使用令牌桶算法进行限流
* 令牌桶算法从应用的角度来说就是服务端设置一个阈值,代表当前服务可以处理的请求数量。这一步主要是为了防止单机接收大量请求导致服务宕机。
*
* */
if(!rateLimiter.tryAcquire(2,TimeUnit.SECONDS)){
return "请求超时,可再次尝试";
}
/*
* 这里进行第一次访问redis
*
* 1.防止缓存穿透(缓存穿透是某些黑客可能会操控大量计算机发出请求,这些请求所请求的响应数据并不存在,此时大量请求访问redis还访问数据库,导致数据库宕机)
* 解决方法:使用布隆过滤器。(布隆过滤器作用:记录数据库中存在的数据,过滤器放在redis中,如果请求的数据不存在于数据库,那么就无需访问数据库)
* 2.获取在redis中所存放的剩余商品库存
*/
String num = redisUtil.get("grabcoupon-"+key);
//如果获取的数字为null,说明redis中并没有对应的秒杀。就查询以下过滤器,看数据库中有没有对应的秒杀计划
if(num == null){
//bloomfilter进行过滤
if(!redisBloomFilter.contains(key)){
return "秒杀不存在或还未开始";
}
//走到这里就可以查询数据库 通过查数据库决定秒杀是已经结束 还是秒杀还未开始 并返回开始时间
return secKillService.validateKey(key).getMessage();
}
//如果返回值为-1则说明秒杀已经结束。
if(Integer.valueOf(num) == -1){
return "你手慢了,秒杀已经结束"+key;
}
/*
*走到这里,意味着秒杀目前还在进行中。下一步就要考虑扣减库存的操作了。
*库存的扣减是一个需要进行先读后写的操作,因此必须保证这一操作的原子性。
*这里有两个处理办法:
*1.使用分布式锁。(java中内置的锁是不起作用的,java中的锁只能用于管理在JVM中存放的数据,而不能管理不同服务器中的数据)
*2.使用lua脚本。(redis可以使用lua脚本实现将一个包含多步操作的函数视作一个原语的功能)
*/
//开始抢购 (int) (Math.random()*(10000-1)+1)
Long result = null;
result = secKillService.seckill(userId, key);//此处函数功能主要是调用lua脚本 内部有查看库存,扣减库存的功能
//返回值为0 表示库存为0,秒杀失败
if(result == 0){
return "你手慢了,秒杀已经结束"+key;
}
//返回值为1 表示秒杀成功以下步骤在数据库中添加用户实际获得的商品的数据,并且发送邮件给用户,提示其秒杀成功。
else if(result == 1){
//此处使用消息队列 使用direct方式配置
mqSender.sendPerson(secKillService.findGrabCouponMessage(key, userId));
return "抢购成功:"+key;
}
//返回值为2 表示已经秒杀过了,不能够重复进行秒杀。
else if(result == 2){
return "您已经抢购过了"+key;
}
//其他情况代表服务器错误 事实上lua脚本内部没有这个选项 但是实际项目中带代码应该及时处理错误情况,或记录日志或直接抛出错误
else{
return "服务器错误,可尝试再次请求";
}
}
lua脚本代码(如果打算自行实现秒杀,可以参考以下代码进行简单学习,我也是这样做的,这是我修改后的代码):
local userid=KEYS[1];
local gcid=KEYS[2];
local gcKey='grabcoupon-'..gcid;
local usersKey='SecKill:'..gcid;
local userExists = redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then
return 2;
end
local num=redis.call("get",gcKey);
if tonumber(num)<=0 then
return 0;
else
redis.call("decr",gcKey);
redis.call("sadd",usersKey,userid);
end
return 1
4.秒杀定时启动与关闭服务讲解
该服务包括秒杀活动的启动和关闭。使用了定时任务插件Quartz(一个第三方库,简单学习以下很好用)。
//这段代码是写在定时任务中的,到达指定时间服务就会启动。
//下面这行代码用于在redis中添加库存
redisutil.set("grabcoupon-"+gc.getId(), gc.getCount().toString());
//秒杀活动都是有时间限制的,可能只有那几秒的时间用户可以疯狂点击 这里设置一个秒杀时间 5秒
Thread.sleep(5000);
/*
*这里需要解决两个问题:
*1.防止缓存击穿(缓存击穿主要是指热点数据在redis中突然消失,导致大量请求打到数据库上,导致数据库宕机)
*2.防止缓存雪崩(缓存雪崩是指redis中大量数据到期了,请求都访问数据库,导致数据库宕机)
*3.必须获取当前redis中剩余的库存,并且将数据状态更新为秒杀已经结束(即 值-1)。这是一个先读后写的操作。
*解决方案:
*1.redis中存放的库存数据是热点数据。秒杀结束后不直接删除数据,而是将值置为-1。大量请求不会直接访问数据库,而是访问redis,获得值-1。
*2.在库存数据设置的到期时间之上在添加一个随机的到期时间
*3.获取剩余库存有多种解决方案:使用分布式锁,使用lua脚本,使用redis内置的原子性操作getAndSet(即先获取在更新)
*getAndSet操作是最优雅的解决方案,实际运行速度快,实现简单。
*/
surplus = redisutil.getSet("grabcoupon-"+gc.getId(),"-1");//这里使用getAndSet操作获取剩余的库存surplus并且将库存值更新为-1 此处不删除键值对,为的是防止缓存击穿
//这里设置一个300秒以上的过期时间,防止出现缓存雪崩
setExpire_flag = redisutil.setExpire("grabcoupon-"+gc.getId(), (long)(Math.random()*(10-1)+1)+ 300);
5.其他问题思考
当我们将这个服务部署之后也会出现一些新的问题,下面抛出这些问题供读者思考和解决:
1.布隆过滤器是无法删除的,我们应该想办法定期更新它。
2.当我们的服务器支持横向扩展(横向扩展是指通过添加多个服务器提升请求的处理效率)的时候,此时就会出现新的瓶颈,比如某些地方需要自增的key值
3.在分布式项目中,我们该如何验证用户身份呢?