前言:
而今同学们对“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秒杀超卖