基于springboot+redis+rabbitmq的高并发秒杀系统实现-1

项目源码下载地址:

https://github.com/wangqianlong513/springboot-redis-rabbitmq-seckill

声明:

本秒杀系统是在https://open.21ic.com/open/video/15844课程的基础上改进的。主要有如下修改

I、原版本中,springboot整合的单机版redis,我修改成了redis集群,6个redis实例,其中创建集群的时候,设置了主从比例为1,所以6个redis实例中,有3个master、3个slave,保障了redis的高可用。

II、原版本中仅仅通过rabbitmq实现了异步生成订单功能,我加入了异步邮件提醒功能和异步短信提醒(购买了阿里大于短信服务)功能

III、原版本中没有订单失效功能,我在系统中整合了rabbitmq死信队列(延迟队列),设置了TTL时间,使得队列中的消息(订单信息)超过TTL时间就会修改订单状态并增加库存,目的是模拟订单超时未支付功能。

V、原版中的rabbitmq是单机rabbitmq实例,不能保证消息队列的高可用。我修改成了rabbitmq集群模式,rabbitmq集群有两种:普通集群(仅仅能提高消息队列的吞吐量,不能保证高可用)和镜像集群(可以保证高可用)。而且,还搭建论文两台HAProxy实例对消息队列进行负载均衡,同时搭建了两个Keepalivced对HAProxy进行监控,保证了这个消息队列系统的高可用、负载均衡。暂时,keepalived遇到了一些问题,还没有完全解决,后续会补上。

 

1、项目简介

       本项目使用springboot框架构建一个高并发(使用Jmeter测试过秒杀接口,通过在Jmeter安装插件可以看到秒杀接口的QPS达到接近4000的水平)的秒杀系统,系统业务很简单,主要包括商品列表展示、商品详情查看、商品秒杀、异步生成订单、异步发送邮件提醒、异步短信通知等功能。使用的技术栈包括:

数据库:MySQL

缓存:redis集群,缓存用户登录信息(token)、商品库存量(预减库存要使用到)、下单后生成的订单

前端:thymeleaf,jquery,javascript

部署服务器:Linux

消息中间件:rabbitmq,在系统中主要两个作用,流量销峰(高并发情况下缓冲用户请求从而减少对后台的瞬时访问请求)和异步通信(下订单、邮件提醒和短信提醒)

 

2、pom.xml文件:

<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.imooc</groupId>
  <artifactId>miaosha</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>
  
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.8.RELEASE</version>
</parent>

  <name>miaosha_2</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- thymeleaf依赖-->
    <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>
     <!-- redis依赖-->
     <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
     </dependency>
   <dependency>
       <groupId>redis.clients</groupId>
       <artifactId>jedis</artifactId>
   </dependency>
    <!-- 序列化工具-->
   <dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>fastjson</artifactId>
       <version>1.2.38</version>
   </dependency>

<!-- commons-codec是Apache开源组织提供的用于摘要运算、编码解码的包。常见的编码解码工具Base64、MD5、Hex、SHA1、DES等-->
   <dependency>
       <groupId>commons-codec</groupId>
       <artifactId>commons-codec</artifactId>
   </dependency>

<!--提供了一些通用的工具,比如StringUtils,DateUtil、RandomUtil等工具-->
   <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>
    <!-- rabbitmq依赖-->
    <dependency>  
      <groupId>org.springframework.boot</groupId>  
      <artifactId>spring-boot-starter-amqp</artifactId>  
   </dependency>  

     <!--邮件服务,项目中使用了mail提醒-->
     <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
     </dependency>

     <!--短信服务,项目中使用了短息提醒服务-->
     <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
        <version>1.0.0-SNAPSHOT</version>
     </dependency>

     <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-core</artifactId>
        <version>3.2.5</version>
     </dependency>
  </dependencies>
  
  <build>
      <finalName>${project.artifactId}</finalName>  
       <plugins>
            <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
      </plugins>
</build>
</project>
 

3、application.properties文件

#thymeleaf
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false
spring.thymeleaf.content-type=text/html
spring.thymeleaf.enabled=true
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.mode=HTML5

# mybatis
mybatis.type-aliases-package=com.imooc.miaosha.domain
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.default-fetch-size=100
mybatis.configuration.default-statement-timeout=3000
mybatis.mapperLocations = classpath:com/imooc/miaosha/dao/*.xml

# druid
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/moocseckill?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=L05217a8w
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.filters=stat
spring.datasource.maxActive=1000
spring.datasource.initialSize=100
spring.datasource.maxWait=60000
spring.datasource.minIdle=500
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=select 'x'
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxOpenPreparedStatements=20

# 单机版redis 暂时不用
#redis.host=192.168.40.136
#redis.port=6666
#redis.timeout=10
#redis.password=rabbitbb
#redis.poolMaxTotal=1000
#redis.poolMaxIdle=500
#redis.poolMaxWait=500

#static  静态资源
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=192.168.40.137
spring.rabbitmq.port=5672
spring.rabbitmq.username=rabbitbb
spring.rabbitmq.password=123456
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.simple.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
spring.rabbitmq.template.retry.max-interval=10000
spring.rabbitmq.template.retry.multiplier=1.0

#邮件服务配置
spring.mail.username=1187674187@qq.com

# 坑:这个password不是qq邮箱的登录密码,需要登录pc端发送短信获取的,特别注意
spring.mail.password=oxrmvjxtwplhjdea
spring.mail.host=smtp.qq.com
spring.mail.properties.mail.smtp.ssl.enable=true

#短息服务配置

#下面四个参数,需要自己登录到自己的阿里云账号查看自己的
accessKeyId= XXXXXX
accessKeySecret=XXXXXX
template_code=XXXXXX
sign_name=XXXXXX

#redis集群
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8  
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1  
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8  
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=1  
# 连接超时时间(毫秒)
spring.redis.timeout=1
spring.redis.commandTimeout=5000
# redis.cluster
spring.redis.cluster.nodes=192.168.213.129:7001,192.168.213.129:7002,192.168.213.129:7003,192.168.213.129:7004,192.168.213.129:7005,192.168.213.129:7006
 

4、一些公共类

(1)返回结果类Result.java,把返回结果统一封装到Result类的对象中,主要目的是规范化返回结果。尤其团队开发中,规范很重要。包括状态码、结果描述和返回数据,和http报文头的格式很类似,比如http中返回状态码“404”,返回信息“资源不存在”。还要注意,因为在整个系统中,可能存在一些通用的状态码和结果描述,所以又额外定义了一个类CodeMsg,CodeMsg类中封装了静态的常用的通用状态码和结果描述,因为是静态的,所以可以通过CodeMsg直接调用,用来对result进行赋值。

package com.imooc.miaosha.result;

public class Result<T> {
   // 状态码
   private int code;
  // 返回结果描述
   private String msg;
  // 返回数据,此处使用了泛型
   private T data;
   
   /**
    *  成功时候的调用
    * */
   public static  <T> Result<T> success(T data){
      return new Result<T>(data);
   }
   
   /**
    *  失败时候的调用
    * */
   public static  <T> Result<T> error(CodeMsg codeMsg){
      return new Result<T>(codeMsg);
   }
   
   private Result(T data) {
      this.data = data;
   }
   
   private Result(int code, String msg) {
      this.code = code;
      this.msg = msg;
   }
   // 通过类CodeMsg的实例对象来对result进行初始化
   private Result(CodeMsg codeMsg) {
      if(codeMsg != null) {
         this.code = codeMsg.getCode();
         this.msg = codeMsg.getMsg();
      }
   }
      
   public int getCode() {
      return code;
   }
   public void setCode(int code) {
      this.code = code;
   }
   public String getMsg() {
      return msg;
   }
   public void setMsg(String msg) {
      this.msg = msg;
   }
   public T getData() {
      return data;
   }
   public void setData(T data) {
      this.data = data;
   }
}
 

(2)CodeMsg.java

package com.imooc.miaosha.result;

public class CodeMsg {
   
   private int code;
   private String msg;
   
   //通用的错误码
   public static CodeMsg SUCCESS = new CodeMsg(0, "success");
   public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服务端异常");
   public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s");
   public static CodeMsg REQUEST_ILLEGAL = new CodeMsg(500102, "请求非法");
   public static CodeMsg ACCESS_LIMIT_REACHED= new CodeMsg(500104, "访问太频繁!");
   //登录模块 5002XX
   public static CodeMsg SESSION_ERROR = new CodeMsg(500210, "Session不存在或者已经失效");
   public static CodeMsg PASSWORD_EMPTY = new CodeMsg(500211, "登录密码不能为空");
   public static CodeMsg MOBILE_EMPTY = new CodeMsg(500212, "手机号不能为空");
   public static CodeMsg MOBILE_ERROR = new CodeMsg(500213, "手机号格式错误");
   public static CodeMsg MOBILE_NOT_EXIST = new CodeMsg(500214, "手机号不存在");
   public static CodeMsg PASSWORD_ERROR = new CodeMsg(500215, "密码错误");
   
   //订单模块 5004XX
   public static CodeMsg ORDER_NOT_EXIST = new CodeMsg(500400, "订单不存在");
   
   //秒杀模块 5005XX
   public static CodeMsg MIAO_SHA_OVER = new CodeMsg(500500, "商品已经秒杀完毕");
   public static CodeMsg REPEATE_MIAOSHA = new CodeMsg(500501, "不能重复秒杀");
   public static CodeMsg MIAOSHA_FAIL = new CodeMsg(500502, "秒杀失败");
   
   
   private CodeMsg( ) {
   }
         
   private CodeMsg( int code,String msg ) {
      this.code = code;
      this.msg = msg;
   }
   
   public int getCode() {
      return code;
   }
   public void setCode(int code) {
      this.code = code;
   }
   public String getMsg() {
      return msg;
   }
   public void setMsg(String msg) {
      this.msg = msg;
   }
   
   public CodeMsg fillArgs(Object... args) {
      int code = this.code;
      String message = String.format(this.msg, args);
      return new CodeMsg(code, message);
   }

   @Override
   public String toString() {
      return "CodeMsg [code=" + code + ", msg=" + msg + "]";
   }
   
   
}
 

(3)数据库相关的工具类DBUti.java

package com.imooc.miaosha.util;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Properties;

public class DBUtil {
   private static Properties props;
   static {
      try {
         InputStream in = DBUtil.class.getClassLoader().getResourceAsStream("application.properties");
         props = new Properties();
         props.load(in);
         in.close();
      }catch(Exception e) {
         e.printStackTrace();
      }
   }
   // 返回数据库连接
   public static Connection getConn() throws Exception{
      String url = props.getProperty("spring.datasource.url");
      String username = props.getProperty("spring.datasource.username");
      String password = props.getProperty("spring.datasource.password");
      String driver = props.getProperty("spring.datasource.driver-class-name");
      Class.forName(driver);
      return DriverManager.getConnection(url,username, password);
   }
}
 

(4)加密工具MD5Util.java

package com.imooc.miaosha.util;

import org.apache.commons.codec.digest.DigestUtils;

public class MD5Util {
   public static String md5(String src) {
      return DigestUtils.md5Hex(src);
   }
   // 加密的盐值
   private static final String salt = "1a2b3c4d";
   // 对输入的密码进行第一次加密
   public static String inputPassToFormPass(String inputPass) {
      String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
      System.out.println(str);
      return md5(str);
   }
   // 对经过了一次md5加密后的密码再进行一次加密
   public static String formPassToDBPass(String formPass, String salt) {
      String str = ""+salt.charAt(0)+salt.charAt(2) + formPass +salt.charAt(5) + salt.charAt(4);
      return md5(str);
   }
   // 对输入的初始密码进行两次md5加密
   public static String inputPassToDbPass(String inputPass, String saltDB) {
      String formPass = inputPassToFormPass(inputPass);
      String dbPass = formPassToDBPass(formPass, saltDB);
      return dbPass;
   }
   
   public static void main(String[] args) {
      System.out.println(inputPassToFormPass("123456"));//d3b1294a61a07da9b49b6e22b2cbd7f9
   }
   
}
 

(5)随机产生字符工具

public class UUIDUtil {
   public static String uuid() {
      return UUID.randomUUID().toString().replace("-", "");
   }
}

说明:

重点在于梳理整个系统的业务流程和技术实现过程,所以接下来,从登陆界面开始,按照一个下单的全过程梳理整个系统,就不像上面这样对一些具体的类进行“搬运了”。接下来几篇博客将梳理整个系统的秒杀过程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值