秒杀,对我们来说,都不是一个陌生的东西。每年的双11,618以及时下流行的直播等等。秒杀对于我们系统而言是一个巨大的考验。秒杀主要解决两个问题,一个是并发读,一个是并发写。并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。
秒杀的整体架构可以概括为“稳、准、快”几个关键字。稳要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提;“准”就是要求保证数据的一致性,秒杀几台东西就只能卖几台东西;“快”就是要求系统的性能要足够高。
从技术角度上看“稳、准、快”,就对应了我们架构上的高可用、一致性和高性能的要求。
选择Spring Initializr, sdk选择1.8,java版本选择8,添加Lombok,Spring Web,Thymeleaf依赖。
添加依赖: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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.4.RELEASEversion>
<relativePath/>
parent>
<groupId>com.xxxxgroupId>
<artifactId>seckill-demoartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>seckill-demoname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>commons-codecgroupId>
<artifactId>commons-codecartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>com.github.whvcsegroupId>
<artifactId>easy-captchaartifactId>
<version>1.6.2version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
配置文件:application.yml
spring:
#静态资源处理
resources:
#启动默认静态资源处理,默认启用
add-mappings: true
cache:
cachecontrol:
#缓存相应时间,单位秒
max-age: 3600
chain:
#资源链启动缓存,默认启动
cache: true
#启用资源链,默认禁用
enabled: true
#启用压缩资源(gzip,brotli)解析,默认禁用
compressed: true
#启用h5应用缓存,默认禁用
html-application-cache: true
static-locations: classpath:/static/
# thymelaef配置
thymeleaf:
# 关闭缓存
cache: false
# 数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.56.10:3306/seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
#连接池名
pool-name: DateHikariCP
# 最小空闲连接出
minimum-idle: 5
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 600000
#最大连接数,默认10
maximum-pool-size: 10
# 从连接池返回的连接自动提交
auto-commit: true
# 连接最大存活时间,0表示永久存活,默认1800000(30分钟)
max-lifetime: 1800000
# 连接超时时间,默认30000(30秒)
connection-timeout: 30000
# 测试连接是否可用的查询语句
connection-test-query: SELECT 1
# redis配置
redis:
#服务器地址
host: 192.168.56.10
#端口
port: 6379
#数据库
database: 0
#超时时间
timeout: 10000ms
#密码
lettuce:
pool:
#最大连接数,默认8
max-active: 8
#最大连接阻塞等待时间,默认-1
max-wait: 10000ms
#最大空闲连接,默认8
max-idle: 200
#最小空闲连接,默认0
min-idle: 5
# RabbitMQ
rabbitmq:
# 服务器
host: 192.168.56.10
#用户名
username: root
#密码
password: root
# 虚拟主机
virtual-host: /
#端口
port: 5672
listener:
simple:
#消费者最小数量
concurrency: 10
#消费者最大数量
max-concurrency: 10
#限制消费者每次只处理一条消息,处理完再继续下一条消息
prefetch: 1
#启动时是否默认启动容器,默认true
auto-startup: true
#被拒绝时重新进入队列
default-requeue-rejected: true
template:
retry:
#发布重试,默认false
enabled: true
#重试时间,默认1000ms
initial-interval: 1000ms
#重试最大次数,默认3次
max-attempts: 3
#重试最大间隔时间,默认10000ms
max-interval: 10000ms
#重试的间隔乘数。比如配2.0,第一次就等10s,第二次就等20s,第三次就等40s
multiplier: 1
#Mybatis-plus配置
mybatis-plus:
# 配置Mapper.xml映射文件
mapper-locations: classpath*:/mapper/*Mapper.xml
# 配置MyBatis数据返回类型别名(默认别名是类名)
type-aliases-package: com.xxxx.seckill.pojo
# MyBatis SQL打印(方法接口所在的包,不是Mapper.xml所在的包)
#logging:
# level:
# com.xxxx.seckill.mapper: debug
添加公共结果返回对象:
//公共返回对象枚举
public enum RespBeanEnum {
//通用
SUCCESS(200, "SUCCESS"),
ERROR(500, "服务端异常"),
//登录模块5002xx
LOGIN_ERROR(500210, "用户名或密码不正确"),
MOBILE_ERROR(500211, "手机号码格式不正确"),
BIND_ERROR(500212, "参数校验异常"),
MOBILE_NOT_EXIST(500213, "手机号码不存在"),
PASSWORD_UPDATE_FAIL(500214, "密码更新失败"),
SESSION_ERROR(500215, "用户不存在"),
//秒杀模块5005xx
EMPTY_STOCK(500500, "库存不足"),
REPEATE_ERROR(500501, "该商品每人限购一件"),
REQUEST_ILLEGAL(500502, "请求非法,请重新尝试"),
ERROR_CAPTCHA(500503, "验证码错误,请重新输入"),
ACCESS_LIMIT_REAHCED(500504, "访问过于频繁,请稍后再试"),
//订单模块5003xx
ORDER_NOT_EXIST(500300, "订单信息不存在"),
;
private final Integer code;
private final String message;
}
//公共返回对象
public class RespBean {
private long code;
private String message;
private Object obj;
/**
* 功能描述: 成功返回结果
*/
public static RespBean success(){
return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBeanEnum.SUCCESS.getMessage(),null);
}
/**
* 功能描述: 成功返回结果
*/
public static RespBean success(Object obj){
return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBean.success().getMessage(),obj);
}
/**
* 功能描述: 失败返回结果
*/
public static RespBean error(RespBeanEnum respBeanEnum){
return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),null);
}
/**
* 功能描述: 失败返回结果
*/
public static RespBean error(RespBeanEnum respBeanEnum,Object obj){
return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),obj);
}
}
登录功能:两次MD5加密
public class MD5Util {
public static String md5(String src){
return DigestUtils.md5Hex(src);
}
private static final String salt="1a2b3c4d";
public static String inputPassToFromPass(String inputPass){<