SpringBoot+Redis+RabbitMQ 限时秒杀系统

SpringBoot+Redis+RabbitMQ 限时秒杀系统

原理

秒杀与其他业务最大的区别在于,在秒杀的瞬间,系统的并发量和吞吐量会非常大,与此同时,网络的流量也会瞬间变大。
对于网络流量瞬间变大问题,最常用的办法就是将页面静态化,也就是我们常说的前后端分离。把静态页面直接缓存到用户的浏览器中,当用户需要获取数据时,就从服务端接口动态获取。这样会大大节省网络的流量,如果再加上CDN优化,一般都不会有大问题。
对于系统并发量变大问题,这里的核心在于如何在大并发的情况下保证数据库能扛得住压力,因为大并发的瓶颈在于数据库。如果用户的请求直接从前端传到数据库,显然,数据库是无法承受几十万上百万甚至上千万的并发量的。因此,我们能做的只能是减少对数据库的访问。例如,前端发出了100万个请求,通过我们的处理,最终只有10个会访问数据库,这样就会大大提升系统性能。再针对秒杀这种场景,因为秒杀商品的数量是有限的,因此这种做法刚好适用。

具体是如何来减少对数据库的访问的呢?

假如,某个商品可秒杀的数量是10,那么在秒杀活动开始之前,把商品的ID和数量加载到Redis缓存。当服务端收到请求时,首先预减Redis中的数量,如果数量减到小于0时,那么随后的访问直接返回秒杀失败的信息。也就是说,最终只有10个请求会去访问数据库。

如果商品数量比较多,比如1万件商品参与秒杀,那么就有1万*10=10万个请求并发去访问数据库,数据库的压力还是会很大。这里就用到了另外一个非常重要的组件:消息队列。我们不是把请求直接去访问数据库,而是先把请求写到消息队列中,做一个缓存,然后再去慢慢的更新数据库。这样做之后,前端用户的请求可能不会立即得到响应是成功还是失败,很可能得到的是一个排队中的返回值,这个时候,需要客户端去服务端轮询,因为我们不能保证一定就秒杀成功了。当服务端出队,生成订单以后,把用户ID和商品ID写到缓存中,来应对客户端的轮询就可以了。

优化秒杀下单流程:
之前是在控制器里拿到客户端请求后直接入库、减库存。如果碰到羊毛党其实这套机制是不行的。并发量高的时候,库存数量也会不准确。那么引入rabbitmq则在下单时让用户信息产生一条消息入队。然后消费者处理下单(是否重复下单、下单失败、库存不够)。客户端接受到请求已入队列(response引入state处理交互)后发起ajax轮询请求,处理成功则跳转下单成功页或者结束本次交互。

具体代码如下:

配置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
#拼接前缀与后缀,去创建templates目录,里面放置模板文件
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
#mybatis
#是否打印sql语句
#spring.jpa.show-sql= true
mybatis.type-aliases-package=com.ljs.miaosha.domain
#mybatis.type-handlers-package=com.example.typehandler
#下划线转换为驼峰
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.default-fetch-size=100
#ms --3000ms--->3s
mybatis.configuration.default-statement-timeout=3000
#mybatis配置文件路径
#mapperLocaitons
mybatis.mapper-locaitons=classpath:com/ljs/miaosha/dao/*.xml
#druid
spring.datasource.url=jdbc:mysql://localhost/miaosha?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=sasa
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.password=123456
redis.poolMaxTotal=1000
redis.poolMaxldle=500
redis.poolMaxWait=500
#static  静态资源配置,设置静态文件路径css,js,图片等等
#spring.mvc.static-path-pattern=/static/**    spring.mvc.static-path-pattern=/**
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

pom文件:

<?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.ljs</groupId>
  <artifactId>miaosha_idea</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>miaosha_idea</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>

  <!-- FIXME change it to the project's website -->
  <!--<url>http://www.example.com</url>-->
  <!--<properties>-->
    <!--<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>-->
    <!--<maven.compiler.source>1.7</maven.compiler.source>-->
    <!--<maven.compiler.target>1.7</maven.compiler.target>-->
  <!--</properties>-->

  <!--<dependencies>-->
    <!--<dependency>-->
      <!--<groupId>junit</groupId>-->
      <!--<artifactId>junit</artifactId>-->
      <!--<version>4.11</version>-->
      <!--<scope>test</scope>-->
    <!--</dependency>-->
  <!--</dependencies>-->
  <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>
	    <!-- <dependency>
	      <groupId>org.springframework.boot</groupId>
	      <artifactId>spring-boot-starter-tomcat</artifactId>
	      <scope>provided</scope>
	  </dependency>  -->
  </dependencies>

  <build>
    <finalName>${project.artifactId}</finalName>
    <plugins>
      <!-- 打war包插件 -->
      <!-- <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <configuration>
            <failOnMissingWebXml>false</failOnMissingWebXml>
        </configuration>
      </plugin> -->
      <!-- 打jar包插件 -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>

    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
        <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>
        <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
        <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>

下单业务方法(Controller)

package com.ljs.miaosha.controller;

import com.ljs.miaosha.domain.MiaoshaOrder;
import com.ljs.miaosha.domain.MiaoshaUser;
import com.ljs.miaosha.domain.OrderInfo;
import com.ljs.miaosha.rabbitmq.MQSender;
import com.ljs.miaosha.rabbitmq.MiaoshaMessage;
import com.ljs.miaosha.redis.AccessKey;
import com.ljs.miaosha.redis.GoodsKey;
import com.ljs.miaosha.redis.RedisService;
import com.ljs.miaosha.result.CodeMsg;
import com.ljs.miaosha.result.Result;
import com.ljs.miaosha.service.GoodsService;
import com.ljs.miaosha.service.MiaoshaService;
import com.ljs.miaosha.service.MiaoshaUserService;
import com.ljs.miaosha.service.OrderService;
import com.ljs.miaosha.vo.GoodsVo;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RequestMapping("/miaosha")
@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);
	}
}

商品秒杀业务方法(Controller)

package com.ljs.miaosha.controller;

import com.ljs.miaosha.domain.MiaoshaUser;
import com.ljs.miaosha.redis.GoodsKey;
import com.ljs.miaosha.redis.RedisService;
import com.ljs.miaosha.result.Result;
import com.ljs.miaosha.service.GoodsService;
import com.ljs.miaosha.service.MiaoshaUserService;
import com.ljs.miaosha.vo.GoodsDetailVo;
import com.ljs.miaosha.vo.GoodsVo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.thymeleaf.spring4.context.SpringWebContext;
import org.thymeleaf.spring4.view.ThymeleafViewResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

@RequestMapping("/goods")
@Controller
public class GoodsController {
	@Autowired
	GoodsService goodsService;
	@Autowired
	RedisService redisService;
	@Autowired
	MiaoshaUserService miaoshaUserService;
	//注入渲染
	@Autowired
	ThymeleafViewResolver thymeleafViewResolver;
	@Autowired
	ApplicationContext applicationContext;
	/**
	 * 未作页面缓存
	 * 1000*10
	 * QPS 784.9293563579279
	 * @param model
	 * @param user
	 * @return
	 */
	@RequestMapping("/to_list_noCache")
	public String toListnoCache(Model model,MiaoshaUser user) {
		model.addAttribute("user", user);
		//查询商品列表
		List<GoodsVo> goodsList= goodsService.getGoodsVoList();
		model.addAttribute("goodsList", goodsList);
		return "goods_list";//返回页面login
	}
	/**
	 * 做页面缓存的list页面,防止同一时间访问量巨大到达数据库,如果缓存时间过长,数据及时性就不高。
	 * 
	 * 1000*10
	 * QPS 1201.923076923077
	 */
	//5-17
	@RequestMapping(value="/to_list",produces="text/html")
	@ResponseBody
	public String toListCache(Model model,MiaoshaUser user,HttpServletRequest request,
			HttpServletResponse response) {
		// 1.取缓存
		// public  T get(KeyPrefix prefix,String key,Class data)
		String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
		if (!StringUtils.isEmpty(html)) {
			return html;
		}
		model.addAttribute("user", user);
		//查询商品列表
		List<GoodsVo> goodsList= goodsService.getGoodsVoList();
		model.addAttribute("goodsList", goodsList);
		
		//2.手动渲染  使用模板引擎	templateName:模板名称 	String templateName="goods_list";
		SpringWebContext context=new SpringWebContext(request,response,request.getServletContext(),
				request.getLocale(),model.asMap(),applicationContext);
		html=thymeleafViewResolver.getTemplateEngine().process("goods_list", context);
		//保存至缓存
		if(!StringUtils.isEmpty(html)) {
			redisService.set(GoodsKey.getGoodsList, "", html);//key---GoodsKey:gl---缓存goodslist这个页面
		}
		return html;
		//return "goods_list";//返回页面login
	}
	/**
	 * 做了页面缓存的to_detail商品详情页。
	 * @param model
	 * @param user
	 * @param goodsId
	 * @return
	 */
	@RequestMapping(value="/to_detail_html/{goodsId}")  //produces="text/html"
	@ResponseBody
	public String toDetailCachehtml(Model model,MiaoshaUser user,
			HttpServletRequest request,HttpServletResponse response,@PathVariable("goodsId")long goodsId) {//id一般用snowflake算法
		// 1.取缓存
		// public  T get(KeyPrefix prefix,String key,Class data)
		String html = redisService.get(GoodsKey.getGoodsDetail, ""+goodsId, String.class);//不同商品页面不同的详情
		if (!StringUtils.isEmpty(html)) {
			return html;
		}
		//缓存中没有,则将业务数据取出,放到缓存中去。
		model.addAttribute("user", user);
		GoodsVo goods=goodsService.getGoodsVoByGoodsId(goodsId);
		model.addAttribute("goods", goods);
		//既然是秒杀,还要传入秒杀开始时间,结束时间等信息
		long start=goods.getStartDate().getTime();
		long end=goods.getEndDate().getTime();
		long now=System.currentTimeMillis();
		//秒杀状态量
		int status=0;
		//开始时间倒计时
		int remailSeconds=0;
		//查看当前秒杀状态
		if(now<start) {//秒杀还未开始,--->倒计时
			status=0;
			remailSeconds=(int) ((start-now)/1000);  //毫秒转为秒
		}else if(now>end){ //秒杀已经结束
			status=2;
			remailSeconds=-1;  //毫秒转为秒
		}else {//秒杀正在进行
			status=1;
			remailSeconds=0;  //毫秒转为秒
		}
		model.addAttribute("status", status);
		model.addAttribute("remailSeconds", remailSeconds);
		
		// 2.手动渲染 使用模板引擎 templateName:模板名称 String templateName="goods_detail";
		SpringWebContext context = new SpringWebContext(request, response, request.getServletContext(),
				request.getLocale(), model.asMap(), applicationContext);
		html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", context);
		// 将渲染好的html保存至缓存
		if (!StringUtils.isEmpty(html)) {
			redisService.set(GoodsKey.getGoodsDetail, ""+goodsId, html);
		}
		return html;//html是已经渲染好的html文件
		//return "goods_detail";//返回页面login
	}
	
	
	/**
	 * 作页面静态化的商品详情
	 * 页面存的是html
	 * 动态数据通过接口从服务端获取
	 * @param model
	 * @param user
	 * @param goodsId
	 * @return
	 */
	@RequestMapping(value="/detail/{goodsId}")  //produces="text/html"
	@ResponseBody
	public Result<GoodsDetailVo> toDetail_staticPage(Model model, MiaoshaUser user,
													 HttpServletRequest request, HttpServletResponse response, @PathVariable("goodsId")long goodsId) {//id一般用snowflake算法
		System.out.println("页面静态化/detail/{goodsId}");
		model.addAttribute("user", user);
		GoodsVo goodsVo=goodsService.getGoodsVoByGoodsId(goodsId);
		model.addAttribute("goods", goodsVo);
		//既然是秒杀,还要传入秒杀开始时间,结束时间等信息
		long start=goodsVo.getStartDate().getTime();
		long end=goodsVo.getEndDate().getTime();
		long now=System.currentTimeMillis();
		//秒杀状态量
		int status=0;
		//开始时间倒计时
		int remailSeconds=0;
		//查看当前秒杀状态
		if(now<start) {//秒杀还未开始,--->倒计时
			status=0;
			remailSeconds=(int) ((start-now)/1000);  //毫秒转为秒
		}else if(now>end){ //秒杀已经结束
			status=2;
			remailSeconds=-1;  //毫秒转为秒
		}else {//秒杀正在进行
			status=1;
			remailSeconds=0;  //毫秒转为秒
		}
		model.addAttribute("status", status);
		model.addAttribute("remailSeconds", remailSeconds);
		GoodsDetailVo gdVo=new GoodsDetailVo();
		gdVo.setGoodsVo(goodsVo);
		gdVo.setStatus(status);
		gdVo.setRemailSeconds(remailSeconds);
		gdVo.setUser(user);
		//将数据填进去,传至页面
		return Result.success(gdVo);		
	}
	
	
	
	/**
	 * 未作页面缓存
	 * @param model
	 * @param user
	 * @param goodsId
	 * @return
	 */
	@RequestMapping("/to_detail_noChache/{goodsId}")
	public String toDetailnoChache(Model model,MiaoshaUser user,@PathVariable("goodsId")long goodsId) {//id一般用snowflake算法
		model.addAttribute("user", user);
		GoodsVo goods=goodsService.getGoodsVoByGoodsId(goodsId);
		model.addAttribute("goods", goods);
		//既然是秒杀,还要传入秒杀开始时间,结束时间等信息
		long start=goods.getStartDate().getTime();
		long end=goods.getEndDate().getTime();
		long now=System.currentTimeMillis();
		//秒杀状态量
		int status=0;
		//开始时间倒计时
		int remailSeconds=0;
		//查看当前秒杀状态
		if(now<start) {//秒杀还未开始,--->倒计时
			status=0;
			remailSeconds=(int) ((start-now)/1000);  //毫秒转为秒
		}else if(now>end){ //秒杀已经结束
			status=2;
			remailSeconds=-1;  //毫秒转为秒
		}else {//秒杀正在进行
			status=1;
			remailSeconds=0;  //毫秒转为秒
		}
		model.addAttribute("status", status);
		model.addAttribute("remailSeconds", remailSeconds);
		return "goods_detail";//返回页面login
	}
	
	
	/**
	 * 之前的版本  1.0  未作user的参数,即未作UserArgumentResolver时调用的detail请求
	 * @param model
	 * @param cookieToken
	 * @param response
	 * @return
	 */
	
	@RequestMapping("/to_detail1")
	public String toDetail(Model model,@CookieValue(value=MiaoshaUserService.COOKIE1_NAME_TOKEN)String cookieToken
			,HttpServletResponse response) {
		//通过取到cookie,首先取@RequestParam没有再去取@CookieValue
		if(StringUtils.isEmpty(cookieToken)) {
			return "login";//返回到登录界面
		}
		String token=cookieToken;
		System.out.println("goods-token:"+token);
		System.out.println("goods-cookieToken:"+cookieToken);		
		MiaoshaUser user=miaoshaUserService.getByToken(token,response);
		model.addAttribute("user", user);
		return "goods_list";//返回页面login
	}
	@RequestMapping("/to_list1")
	public String toList(Model model,@CookieValue(value=MiaoshaUserService.COOKIE1_NAME_TOKEN)String cookieToken
			,HttpServletResponse response) {
		//通过取到cookie,首先取@RequestParam没有再去取@CookieValue
		if(StringUtils.isEmpty(cookieToken)) {
			return "login";//返回到登录界面
		}
		String token=cookieToken;
		System.out.println("goods-token:"+token);
		System.out.println("goods-cookieToken:"+cookieToken);
		//!!!miaoshaUserService.getByToken(token,response);
		MiaoshaUser user=miaoshaUserService.getByToken(token,response);
		model.addAttribute("user", user);
		return "goods_list";//返回页面login
	}
	
	@RequestMapping("/to_list2")
	public String toLogin(Model model,@CookieValue(value=MiaoshaUserService.COOKIE1_NAME_TOKEN)String cookieToken,
			@RequestParam(value=MiaoshaUserService.COOKIE1_NAME_TOKEN)String paramToken,HttpServletResponse response) {
		//通过取到cookie,首先取@RequestParam没有再去取@CookieValue
		if(StringUtils.isEmpty(paramToken)&&StringUtils.isEmpty(cookieToken)) {
			return "login";//返回到登录界面
		}
		String token=StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
		System.out.println("goods-token:"+token);
		System.out.println("goods-cookieToken:"+cookieToken);
		System.out.println("goods-paramToken:"+paramToken);
		MiaoshaUser user=miaoshaUserService.getByToken(token,response);
		model.addAttribute("user", user);
		return "goods_list";//返回页面login
	}
}

总结
秒杀下单增加排队机制来说对于完整的秒杀系统来说只是其中很少的一部分,这里也只是学习rabbitmq的一个过程。对于秒杀系统来说流量主要是查询多下单少。还需要引入redis,把库存量、商品信息能在秒杀开始前预处理。
源码地址: https://github.com/hfbin/Seckill.

你可能感兴趣的:(SpringBoot+Redis+RabbitMQ 限时秒杀系统)