毕业设计回顾:基于分布式的优惠券秒杀系统的设计与实现

秒杀系统总结

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.在分布式项目中,我们该如何验证用户身份呢?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值