SpringBoot项目实现高并发商品秒杀

SpringBoot项目实现高并发商品秒杀

注:该项目使用IDEA+SpringBoot+Maven+Mybatis+Redis+RabbitMQ 等技术实现。本人水平有限,以下代码可能有错误,或者解释不清,希望理解,并且及时下方留言,及时修改,谢谢各位道友!

一、秒杀实现思路

秒杀其实就是一件商品,在某一个时间段内,由于降低了价格,超高的优惠,导致在这一个时间段内购买量大量增加,但是库存有限,产生的一种高并发现象。
秒杀最重要的就是减库存,增订单。同时需要判断用户是否多次秒杀,同时还要防止用户通过恶意软件刷单。

所以需要以下3点:
1、高可用:保证系统的高可用和正确性,设计PlanB备用。
2、一致性:保证秒杀减库存中的数据一致性。
3、高性能:涉及大量并发读写,所以需要支持高并发,从动静分离、热点发现与隔离、请求削峰与分层过滤、服务端极致优化来实现。

具体流程:系统初始化,把商品库存数量等加载到Redis中,用户登录时将用户信息保存到Seesion中,保证用户信息的完整,精确,当用户发送秒杀请求时,判断用户是否已秒杀过,同时前端给用户验证码等判断,将各用户请求时间分开,当确定用户验证通过时,判断库存是否足够,如果不够直接返回请求失败,避免系统压力,如果足够就减库存,Redis预减库存,同时将秒杀请求发送给RabbitMQ ,同时给前端返回状态,显示排队中等状态,同时前端给一个定时根据该商品id去循环请求,后端RabbitMQ监听到消息,就开始操作数据库,修改数据库商品库存和新增订单等操作,前端循环请求返回状态,得到订单代表秒杀成功,或者队列中,否则秒杀失败,秒杀成功后,用户需要在一定时间内付款,不然就自动取消订单,返回库存。

二、部分代码实现

Redis安装教程:https://www.runoob.com/redis/redis-install.html
RabbitMQ安装教程:https://www.linuxprobe.com/install-rabbitmq-on-centos-7.html

1、pom文件和xml配置

<?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>UTF8</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>
    <!-- <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>

yml配置:主要配置了thymeleaf,redis,RabbitMQ,数据库的一些配置,注意redis,RabbitMQ和数据库的端口,ip和用户名,密码,避免错误。

#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=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.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=106.14.252.156
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=StrongPassword
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

2、各部分接口
2_1登录接口

@RequestMapping("/do_login")//作为异步操作
		@ResponseBody
		public Result<Boolean> doLogin(HttpServletResponse response,@Valid LoginVo loginVo) {//0代表成功
			//参数检验成功之后,登录
			CodeMsg cm=miaoshaUserService.login(response,loginVo);
			if(cm.getCode()==0) {
				return Result.success(true);
			}else {
				return Result.error(cm);
			}
		}

2_2、页面缓存接口,返回秒杀商品集合信息, 做页面缓存的list页面,防止同一时间访问量巨大到达数据库,如果缓存时间过长,数据及时性就不高。

@RequestMapping(value="/to_list",produces="text/html")
	@ResponseBody
	public String toListCache(Model model,MiaoshaUser user,HttpServletRequest request,
			HttpServletResponse response) {
		// 1.取缓存
		// public <T> T get(KeyPrefix prefix,String key,Class<T> 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;
	}

2_3、秒杀商品详情页加载接口,当访问商品详情页时,触发该接口,获取商品详情页信息,并且得到秒杀商品当前时间状态

@RequestMapping(value="/detail/{goodsId}")
	@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);		
	}

2_4、获取秒杀的path接口,获取地址,并且验证验证码的值是否正确

@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); 
	}

2_5、订单和消息队列接口

	@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);
		}
		//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) {// 重复下单
			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);
	}

2_6、秒杀轮询接口,判断用户秒杀是否成功,返回状态

	@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);
	}

2_7、订单判断接口,判断订单是否存在,返回订单页面消息,开始支付

@RequestMapping("/detail")
	@ResponseBody
	public Result<OrderDetailVo> info(Model model, MiaoshaUser user,
									  @RequestParam("orderId") long orderId) {
		if(user==null) {
			return Result.error(CodeMsg.SESSION_ERROR);
		}
		OrderInfo order=orderService.getOrderByOrderId(orderId);
		if(order==null) {
			return Result.error(CodeMsg.ORDER_NOT_EXIST);
		}
		//订单存在的情况
		long goodsId=order.getGoodsId();
		GoodsVo gVo=goodsService.getGoodsVoByGoodsId(goodsId);
		OrderDetailVo oVo=new OrderDetailVo();
		oVo.setGoodsVo(gVo);
		oVo.setOrder(order);
		return Result.success(oVo);//返回页面login
	}

三、项目资料等

1、由于代码过多,这里就不全部贴出了
2、项目代码

链接:https://pan.baidu.com/s/1qZjAuce1gRRXHHDHZgDLmw
提取码:iroz

3、该秒杀项目的视频,来源慕课网 https://www.imooc.com

链接:https://pan.baidu.com/s/1vjBlJ82iiIjBSkEN9hbEsA
提取码:33qx

4、借鉴了部分博主的博客,非常感谢

https://blog.csdn.net/Brad_PiTt7/article/details/90717429

发布了13 篇原创文章 · 获赞 48 · 访问量 4301

猜你喜欢

转载自blog.csdn.net/weixin_44209403/article/details/103121961