龙空技术网

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

众星十一 662

前言:

而今同学们对“java秒杀超卖”大致比较关心,兄弟们都想要知道一些“java秒杀超卖”的相关内容。那么小编同时在网络上搜集了一些有关“java秒杀超卖””的相关文章,希望各位老铁们能喜欢,大家一起来了解一下吧!

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

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

一、秒杀实现思路

秒杀其实就是一件商品,在某一个时间段内,由于降低了价格,超高的优惠,导致在这一个时间段内购买量大量增加,但是库存有限,产生的一种高并发现象。

秒杀最重要的就是减库存,增订单。同时需要判断用户是否多次秒杀,同时还要防止用户通过恶意软件刷单。

所以需要以下3点:

1、高可用:保证系统的高可用和正确性,设计PlanB备用。

2、一致性:保证秒杀减库存中的数据一致性。

3、高性能:涉及大量并发读写,所以需要支持高并发,从动静分离、热点发现与隔离、请求削峰与分层过滤、服务端极致优化来实现。

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

二、部分代码实现

Redis安装教程:

RabbitMQ安装教程:

1、pom文件和xml配置

<?xml version="1.0" encoding="UTF-8"?><project xmlns="; xmlns:xsi=";         xsi:schemaLocation=" ;>  <modelVersion>4.0.0</modelVersion>  <groupId>com.ljs</groupId>  <artifactId>miaosha_idea</artifactId>  <version>1.0-SNAPSHOT</version>  <name>miaosha_idea</name>  <url>;/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  -->        <plugin>          <artifactId>maven-clean-plugin</artifactId>          <version>3.1.0</version>        </plugin>        <!-- default lifecycle, jar packaging: see  -->        <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  -->        <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	}

原文链接:?

标签: #java秒杀超卖