项目源码下载地址:
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("-", "");
}
}