基于RabbitMQ、Redis、Springboot秒杀实现

秒杀应用场景
秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
秒杀业务流程比较简单,一般就是下订单减库存。
秒杀的业务场景跟其他业务场景不一样,主要是秒杀的瞬间,并发非常大,如何针对此大并发是我们需要取解决的。秒杀业务,是典型的短时大量突发访问。

秒杀架构设计理念
前端限流:抢购的时候限制每个用户的抢购次数,限制同一个用户多次抢购。
削峰:使用中间件、缓存等技术。
持久化:使用异步处理秒杀是一个高并发系统,可以采用异步处理提高系统效率。
内存缓存:内存的读写效率比写入磁盘块。

秒杀系统架构设计思路
将请求拦截在系统上游,降低下游压力:秒杀系统特点是并发量极大,但实际秒杀成功的请求数量却很少,所以如果不在前端拦截很可能造成数据库读写锁冲突,最终请求超时。
利用缓存:利用缓存可极大提高系统读写速度。
消息队列:消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。

二 安装
redis安装教程
RabbitMQ (otp win64 19.3 rabbitmq-server-3.7.7我的环境)
RabbitMQ安装教程
注意事项:
1.安装时电脑用户名不能为中文,如果你使用的是中文用户,那么请百度如何跳过中文用户安装
2.安装目录不能有中文
3.rabbitmq-plugins enable rabbitmq_management是安装RabbitMQ视图需要连接网络


我使用的是maven所有先引入依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.lw</groupId>
  <artifactId>lovelw</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>demo1</name>
  <url>http://maven.apache.org</url>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.9.RELEASE</version>
  </parent>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.3.1</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.0.5</version>
    </dependency>
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.7.3</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.38</version>
    </dependency>
    <dependency>
      <groupId>commons-codec</groupId>
      <artifactId>commons-codec</artifactId>
      <version>1.9</version>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.6</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
  </dependencies>
  <build>
    <finalName>${project.artifactId}</finalName>
    <plugins>
      <!-- 打jar包插件 -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
    <pluginManagement>
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.7.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>3.0.0</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

设置配置信息application.properties

#thymeleaf
spring.thymeleaf.cache=false
spring.thymeleaf.content-type=text/html
spring.thymeleaf.enabled=true
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.mode=HTML5
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
mybatis.type-aliases-package=com.ljs.miaosha.domain
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.default-fetch-size=100
mybatis.configuration.default-statement-timeout=3000
mybatis.mapper-locaitons=classpath:com/ljs/miaosha/dao/*.xml
spring.datasource.url=jdbc:mysql://localhost/miaosha?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.filters=stat
# 初始化大小,最小,最大
spring.datasource.initialSize=100
spring.datasource.minIdle=500
spring.datasource.maxActive=1000
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=30000
spring.datasource.validationQuery=select 'x'
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
#redis  配置服务器等信息
redis.host=127.0.0.1
redis.port=6379
#redis.timeout=10
#如果redis没有设置密码就不需要给密码
#redis.password=123456
redis.poolMaxTotal=1000
redis.poolMaxldle=500
redis.poolMaxWait=500
spring.resources.add-mappings=true
spring.resources.cache-period=3600 
spring.resources.chain.cache=true
spring.resources.chain.enabled=true
spring.resources.chain.gzipped=true 
spring.resources.chain.html-application-cache=true
spring.resources.static-locations=classpath:/static/ 
#RabbitMQ配置
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
#消费者数量
spring.rabbitmq.listener.simple.concurrency=10
#消费者最大数量
spring.rabbitmq.listener.simple.max-concurrency=10
#消费,每次从队列中取多少个,取多了,可能处理不过来
spring.rabbitmq.listener.simple.prefetch=1
spring.rabbitmq.listener.auto-startup=true
#消费失败的数据重新压入队列
spring.rabbitmq.listener.simple.default-requeue-rejected=true
#发送,队列满的时候,发送不进去,启动重置
spring.rabbitmq.template.retry.enabled=true
#一秒钟之后重试
spring.rabbitmq.template.retry.initial-interval=1000
#
spring.rabbitmq.template.retry.max-attempts=3
#最大间隔 10s
spring.rabbitmq.template.retry.max-interval=10000
spring.rabbitmq.template.retry.multiplier=1.0

用户登陆验证

@RequestMapping("/login")
@Controller
public class LoginController {
	@Autowired
	UserService userService;
	@Autowired
	RedisService redisService;
	@Autowired
	MiaoshaUserService miaoshaUserService;
//调整登陆
	@RequestMapping("/to_login")
	public String toLogin() {
		return "login";//返回页面login
	}
	
	
}

MiaoshaController .class

@RequestMapping("/gbf")
@Controller
public class MiaoshaController implements InitializingBean{
	@Autowired
	GoodsService goodsService;
	@Autowired
	RedisService redisService;
	@Autowired
	MiaoshaUserService miaoshaUserService;
	//作为秒杀功能事务的Service
	@Autowired
	MiaoshaService miaoshaService;
	@Autowired
	OrderService orderService;
	@Autowired
	MQSender mQSender;
	
	//标记
	Map <Long,Boolean>localMap=new HashMap<Long,Boolean>();
	/**
	 * 系统初始化的时候做的事情。
	 * 在容器启动时候,检测到了实现了接口InitializingBean之后,
	 */
	public void afterPropertiesSet() throws Exception {
		List<GoodsVo> goodslist=goodsService.getGoodsVoList();
		if(goodslist==null) {
			return;
		}
		for(GoodsVo goods:goodslist) {
			//如果不是null的时候,将库存加载到redis里面去 prefix---GoodsKey:gs ,	 key---商品id,	 value
			redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
		}
	}
	
	/**
	 * 生成图片验证码
	 */
	@RequestMapping(value ="/vertifyCode")
	@ResponseBody
	public Result<String> getVertifyCode(Model model, MiaoshaUser user,
										 @RequestParam("goodsId") Long goodsId, HttpServletResponse response) {
		model.addAttribute("user", user);
		//如果用户为空,则返回至登录页面
		if(user==null){
			return Result.error(CodeMsg.SESSION_ERROR);
		}
		BufferedImage img=miaoshaService.createMiaoshaVertifyCode(user, goodsId);
		try {
			OutputStream out=response.getOutputStream();
			ImageIO.write(img,"JPEG", out);
			out.flush();
			out.close();
			return null; 
		} catch (IOException e) {
			e.printStackTrace();
			return Result.error(CodeMsg.MIAOSHA_FAIL);
		}
	}
	
	/**
	 * 获取秒杀的path,并且验证验证码的值是否正确
	 */
	//@AccessLimit(seconds=5,maxCount=5,needLogin=true)
	//加入注解,实现拦截功能,进而实现限流功能
	//@AccessLimit(seconds=5,maxCount=5,needLogin=true)
	@RequestMapping(value ="/getPath")
	@ResponseBody
	public Result<String> getMiaoshaPath(HttpServletRequest request,Model model,MiaoshaUser user,
			@RequestParam("goodsId") Long goodsId,
			@RequestParam(value="vertifyCode",defaultValue="0") int vertifyCode) {
		model.addAttribute("user", user);
		//如果用户为空,则返回至登录页面
		if(user==null){
			return Result.error(CodeMsg.SESSION_ERROR);
		}
		//限制访问次数
		String uri=request.getRequestURI();
		String key=uri+"_"+user.getId();
		//限定key5s之内只能访问5次
		Integer count=redisService.get(AccessKey.access, key, Integer.class);
		if(count==null) {
			redisService.set(AccessKey.access, key, 1);
		}else if(count<5) {
			redisService.incr(AccessKey.access, key);
		}else {//超过5次
			return Result.error(CodeMsg.ACCESS_LIMIT);
		}
		
		//验证验证码
		boolean check=miaoshaService.checkVCode(user, goodsId,vertifyCode );
		if(!check) {
			return Result.error(CodeMsg.REQUEST_ILLEAGAL);
		}
		System.out.println("通过!");
		//生成一个随机串
		String path=miaoshaService.createMiaoshaPath(user,goodsId);
		System.out.println("@MiaoshaController-tomiaoshaPath-path:"+path);
		return Result.success(path); 
	}
	
	/**
	 * 客户端做一个轮询,查看是否成功与失败,失败了则不用继续轮询。
	 * 秒杀成功,返回订单的Id。
	 * 库存不足直接返回-1。
	 * 排队中则返回0。
	 * 查看是否生成秒杀订单。
	 */
	@RequestMapping(value = "/result", method = RequestMethod.GET)
	@ResponseBody
	public Result<Long> doMiaoshaResult(Model model, MiaoshaUser user,
			@RequestParam(value = "goodsId", defaultValue = "0") long goodsId) {
		long result=miaoshaService.getMiaoshaResult(user.getId(),goodsId);
		System.out.println("轮询 result:"+result);
		return Result.success(result);
	}
	
	
	/**
	 * 563.1899076368552
	 * 做缓存+消息队列
	 * 1.系统初始化,把商品库存数量加载到Redis上面来。
	 * 2.收到请求,Redis预减库存。
	 * 3.请求入队,立即返回排队中。
	 * 4.请求出队,生成订单,减少库存(事务)。
	 * 5.客户端轮询,是否秒杀成功。
	 * 
	 * 不能是GET请求,GET
	 */
	//POST请求 
	@RequestMapping(value="/{path}/do_miaosha_ajaxcache",method=RequestMethod.POST)
	@ResponseBody
	public Result<Integer> doMiaoshaCache(Model model,MiaoshaUser user,
			@RequestParam(value="goodsId",defaultValue="0") long goodsId,
			@PathVariable("path")String path) {
		model.addAttribute("user", user);
		//1.如果用户为空,则返回至登录页面
		if(user==null){
			return Result.error(CodeMsg.SESSION_ERROR);
		}
		//验证path,去redis里面取出来然后验证。
		boolean check=miaoshaService.checkPath(user,goodsId,path);
		if(!check) {
			return Result.error(CodeMsg.REQUEST_ILLEAGAL);
		}
		//内存标记,减少对redis的访问 localMap.put(goodsId,false);
//		boolean over=localMap.get(goodsId);
//		//在容量满的时候,那么就打标记为true
//		if(over) {
//			return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);
//		}
		//2.预减少库存,减少redis里面的库存
		long stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
		//3.判断减少数量1之后的stock,区别于查数据库时候的stock<=0
		if(stock<0) {
			return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);
		}
		//4.判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品
		MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId);
		if (order != null) {// 重复下单
			// model.addAttribute("errorMessage", CodeMsg.REPEATE_MIAOSHA);
			return Result.error(CodeMsg.REPEATE_MIAOSHA);
		}
		//5.正常请求,入队,发送一个秒杀message到队列里面去,入队之后客户端应该进行轮询。
		MiaoshaMessage mms=new MiaoshaMessage();
		mms.setUser(user);
		mms.setGoodsId(goodsId);
		mQSender.sendMiaoshaMessage(mms);
		//返回0代表排队中
		return Result.success(0);
	}
	/**
	 * 1000*10
	 * QPS 703.4822370735138
	 * @param model
	 * @param user
	 * @return
	 */
	@RequestMapping("/do_miaosha")
	public String toList(Model model,MiaoshaUser user,@RequestParam("goodsId") Long goodsId) {
		model.addAttribute("user", user);
		//如果用户为空,则返回至登录页面
		if(user==null){
			return "login";
		}
		GoodsVo goodsvo=goodsService.getGoodsVoByGoodsId(goodsId);
		//判断商品库存,库存大于0,才进行操作,多线程下会出错
		int  stockcount=goodsvo.getStockCount();		
		if(stockcount<=0) {//失败			库存至临界值1的时候,此时刚好来了加入10个线程,那么库存就会-10
			model.addAttribute("errorMessage", CodeMsg.MIAOSHA_OVER_ERROR);
			return "miaosha_fail";
		}
		//判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品 
		MiaoshaOrder order=orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(),goodsId);
		if(order!=null) {//重复下单
			model.addAttribute("errorMessage", CodeMsg.REPEATE_MIAOSHA);
			return "miaosha_fail";
		}
		//可以秒杀,原子操作:1.库存减1,2.下订单,3.写入秒杀订单--->是一个事务
		OrderInfo orderinfo=miaoshaService.miaosha(user,goodsvo);
		//如果秒杀成功,直接跳转到订单详情页上去。
		model.addAttribute("orderinfo", orderinfo);
		model.addAttribute("goods", goodsvo);
		return "order_detail";//返回页面login
	}
	
	
	/**
	 * 
	 * 做了页面静态化的,直接返回订单的信息
	 * @param model
	 * @param user
	 * @param goodsId
	 * @return
	 * 
	 * 不能是GET请求,GET,
	 */
	//POST请求 
	@RequestMapping(value="/do_miaosha_ajax",method=RequestMethod.POST)
	@ResponseBody
	public Result<OrderInfo> doMiaosha(Model model,MiaoshaUser user,@RequestParam(value="goodsId",defaultValue="0") long goodsId) {
		model.addAttribute("user", user);
		System.out.println("do_miaosha_ajax");
		System.out.println("goodsId:"+goodsId);
		//如果用户为空,则返回至登录页面
		if(user==null){
			return Result.error(CodeMsg.SESSION_ERROR);
		}
		GoodsVo goodsvo=goodsService.getGoodsVoByGoodsId(goodsId);
		//判断商品库存,库存大于0,才进行操作,多线程下会出错
		int  stockcount=goodsvo.getStockCount();		
		if(stockcount<=0) {//失败			库存至临界值1的时候,此时刚好来了加入10个线程,那么库存就会-10
			//model.addAttribute("errorMessage", CodeMsg.MIAOSHA_OVER_ERROR);
			return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);
		}
		//判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品 
		MiaoshaOrder order=orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(),goodsId);
		if(order!=null) {//重复下单
			//model.addAttribute("errorMessage", CodeMsg.REPEATE_MIAOSHA);
			return Result.error(CodeMsg.REPEATE_MIAOSHA);
		}
		//可以秒杀,原子操作:1.库存减1,2.下订单,3.写入秒杀订单--->是一个事务
		OrderInfo orderinfo=miaoshaService.miaosha(user,goodsvo);
		//如果秒杀成功,直接跳转到订单详情页上去。
		model.addAttribute("orderinfo", orderinfo);
		model.addAttribute("goods", goodsvo);
		return Result.success(orderinfo);
	}
	
}

RabbitMQ:创建消息生产类、消息消费类、信息交换类

Redis:将秒杀对象初始化到内存中,设置对象

用户开始秒杀

限制每位用户秒杀次数

秒杀成功
到这里秒杀流程就完成了。


版权声明:本文为qq_44021541原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。