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