秒杀项目01
技术点介绍
前端: Thymeleaf,Bootstrap,JQuery
后端: SpringBoot,JSR303,MyBatis
中间件: RabbitMQ,Redis,Druid
秒杀系统实现
学习目标
**应对大并发: **
- 如何利用缓存
- 如何使用异步
- 如何编写优雅的代码
一、项目框架搭建
1.1 SpringBoot环境搭建
父工程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.hmx</groupId>
<artifactId>miaosha</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>miaosha01</module>
</modules>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<version>2.5.0</version>
<artifactId>spring-boot-starter-parent</artifactId>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
子工程pom.xml
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
创建启动类,三层
1.2 集成Thymeleaf,Result结果封装
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
application.properties
# 允许thymeleaf使用缓存
spring.thymeleaf.cache=true
# 是否为web框架启动thymeleaf视图解析
spring.thymeleaf.enabled=true
# 写入 HTTP 响应的 Content-Type 值。
spring.thymeleaf.servlet.content-type=text/html
# 模板文件编码
spring.thymeleaf.encoding=UTF-8
# 要应用于模板的模板模式
spring.thymeleaf.mode=HTML5
# 在构建 URL 时预先添加到查看名称的前缀。
spring.thymeleaf.prefix=classpath:/templates/
# 在构建 URL 时预先添加到查看名称的后缀。
spring.thymeleaf.suffix=.html
Result.java
@Data
public class Result<T> {
private int code;
private String msg;
private T data;
private Result(T data) {
this.code = CodeMsg.SUCCESS.getCode();
this.msg = CodeMsg.SUCCESS.getMsg();
this.data = data;
}
private Result(CodeMsg cm) {
if (cm == null) {
return;
}
this.code = CodeMsg.SERVER_ERROR.getCode();
this.msg = CodeMsg.SERVER_ERROR.getMsg();
}
/**
* 成功时候调用
* @param <T>
* @return
*/
public static <T> Result<T> success(T data) {
return new Result<T>(data);
}
/**
* 失败时候调用
* @param <T>
* @return
*/
public static <T> Result<T> fail(CodeMsg cm) {
return new Result<T>(cm);
}
}
CodeMsg.java
public enum CodeMsg {
//通用异常
SUCCESS(0,"success"),
SERVER_ERROR(500100, "服务端异常");
//登录模块 5002XX
//商品模块 5003XX
//订单模块 5004XX
//秒杀模块 5005XX
private int code;
private String msg;
private CodeMsg(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
1.3 集成Mybatis+Druid
导入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
mybatis相关配置
# mybatis
# 搜索类别名的包
mybatis-plus.type-aliases-package=com.hmx.miaosha.domain
# 下划线转成驼峰
mybatis-plus.configuration.map-underscore-to-camel-case=true
# 为驱动程序设置一个提示,以控制返回结果的获取大小。此参数值可以被查询设置覆盖。
mybatis-plus.configuration.default-fetch-size=100
# 设置驱动程序等待数据库响应的秒数。
mybatis-plus.configuration.default-statement-timeout=3000
# Mapper xml配置文件的位置
mybatis-plus.mapper-locations=classpath:com/hmx/miaosha/mapper/*.xml
Druid相关配置
spring.datasource.url=jdbc:mysql:///miaosha?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=hmx123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.initialSize=1
spring.datasource.maxActive=2
spring.datasource.maxWait=60000
spring.datasource.minIdle=1
1.4 集成Jedis+Redis安装+通用缓存Key封装
Redis安装
- 官网下载压缩包
- 放到linux中
- 解压到后移动到/usr/local/redis下
- make && makeinstall
在vim编辑器中查找内容通过"/字符串"查找,n下一个 - 修改redis.conf配置文件
允许所有ip访问redis
bind 0.0.0.0
# 后台启动redis
daemonize yes
# 设置redis的密码
requirepass 123456
- 启动redis
redis-server ./redis.conf
- 客户端连接redis-server
redis-cli
- 设置密码后需要验证才能操作
# auth 后面为你设置的密码
auth 123456
- 生成redis的系统服务
cd /usr/local/redis/utils
./install_server.sh
若出现如下错误:
Please take a look at the provided example service unit files in this direct
则注释掉该文件中的
#bail if this system is managed by systemd
#_pid_1_exe="$(readlink -f /proc/1/exe)"
#if [ "${_pid_1_exe##*/}" = systemd ]
#then
# echo "This systems seems to use systemd."
# echo "Please take a look at the provided example service unit files in this directory, and adapt and install them. Sorry!"
# exit 1
#fi
10. 查看系统服务列表
chkconfig --list | grep redis
- 启动redis系统服务
systemctl start redis_6379
集成Redis
- 添加Jedis依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
- 添加Fastjson依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
- redis相关配置
# redis
spring.redis.host=192.168.174.128
spring.redis.port=6379
spring.redis.database=0
spring.redis.timeout=3
spring.redis.password=123456
spring.redis.jedis.pool.max-active=10
# 最大空闲 10个
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=3
通用缓存Key封装
RedisConfig.java
@Component
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedisConfig {
private String host;
private int port = 6379;
//秒
private int timeout;
// 使用几号库
private int database = 0;
private String password;
private int maxActive = 10;
private int maxIdle = 10;
//秒
private int maxWait = 3;
}
RedisService.java
@Service
public class RedisService {
@Autowired
private JedisPool jedisPool;
public <T>Boolean set(String key, T value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String str = beanToString(value);
jedis.set(key, str);
return true;
} finally {
returnToPool(jedis);
}
}
public <T> T get(String key, Class<T> clazz) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String str = jedis.get(key);
T t = stringToBean(str,clazz);
return t;
} finally {
returnToPool(jedis);
}
}
private <T> String beanToString(T value) {
if (value == null) {
return null;
}
Class<?> clazz = value.getClass();
if (clazz == Integer.class) {
return "" + value;
} else if (clazz == String.class) {
return (String) value;
} else if (clazz == Long.class) {
return "" + value;
} else {
return JSON.toJSONString(value);
}
}
private <T> T stringToBean(String str, Class<T> clazz) {
if (StringUtils.isBlank(str) || clazz == null) {
return null;
}
if (clazz == Integer.class) {
return (T)Integer.valueOf(str);
} else if (clazz == String.class) {
return (T)str;
} else if (clazz == Long.class) {
return (T) Long.valueOf(str);
} else {
return JSON.parseObject(str,clazz);
}
}
/**
* 将Jedis返回到连接池里
* @param jedis
*/
private void returnToPool(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
}
JedisPoolFactory.java
@Service
public class RedisPoolFactory {
@Autowired
private RedisConfig redisConfig;
@Bean
public JedisPool jedisPoolFactory() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(redisConfig.getMaxIdle());
poolConfig.setMaxTotal(redisConfig.getMaxActive());
poolConfig.setMaxWaitMillis(redisConfig.getMaxWait() * 1000L);
JedisPool jp = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getTimeout() * 1000, redisConfig.getPassword(), redisConfig.getDatabase());
return jp;
}
}
注意: 本来把RedisPoolFactory中的内容放在RedisService中的,启动会报循环依赖,因为RedisService依赖JedisPool,JedisPool依赖RedisService中的RedisConfig
测试
@Controller
@RequestMapping("/demo")
public class DemoController {
@Autowired
private UserService userService;
@Autowired
private RedisService redisService;
@RequestMapping("/redis/get")
@ResponseBody
public Result<Long> redisGet() {
return Result.success(redisService.get("key1", Long.class));
}
@RequestMapping("/redis/set")
@ResponseBody
public Result<Boolean> redisSet() {
return Result.success(redisService.set("key2", "hello hmx"));
}
@RequestMapping("/db/tx")
@ResponseBody
public Result<Boolean> tx() {
return userService.tx();
}
@RequestMapping("/thymeleaf")
public String thymeleaf(Model model) {
model.addAttribute("name", "HMX");
return "hello";
}
@ResponseBody
@RequestMapping("/hello")
public Result<String> hello() {
return Result.success("hello springboot");
}
@ResponseBody
@RequestMapping("/error")
public Result<String> error() {
return Result.fail(CodeMsg.SERVER_ERROR);
}
}
完善并优化redis相关类
在redis中set key时可能会出现key被覆盖的操作,所以我们使用前缀来避免这个情况,不同的模块使用不同的前缀
KeyPrefix.java
public interface KeyPrefix {
int expireSeconds();
String getPrefix();
}
BasePrefix.java
public abstract class BasePrefix implements KeyPrefix{
private int expireSeconds;
private String prefix;
public BasePrefix(String prefix) {
this(0, prefix);
}
public BasePrefix() {
}
public BasePrefix(int expireSeconds, String prefix) {
this.expireSeconds = expireSeconds;
this.prefix = prefix;
}
/**
* 默认0代表永不过期
* @return
*/
@Override
public int expireSeconds() {
return expireSeconds;
}
@Override
public String getPrefix() {
return getClass().getSimpleName() + ":" + prefix;
}
}
UserKey.java
public class UserKey extends BasePrefix{
private UserKey(String prefix) {
super(prefix);
}
public static UserKey getById = new UserKey("id");
public static UserKey getByName = new UserKey("name");
}
RedisService.java
@Service
public class RedisService {
@Autowired
private JedisPool jedisPool;
/**
* 设置对象
* @param prefix
* @param key
* @param value
* @param <T>
* @return
*/
public <T>Boolean set(KeyPrefix prefix, String key, T value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
String str = beanToString(value);
int seconds = prefix.expireSeconds();
if (seconds <= 0) {
jedis.set(realKey, str);
} else {
jedis.setex(realKey, (long)seconds, str);
}
return true;
} finally {
returnToPool(jedis);
}
}
/**
* 获取对象
* @param prefix
* @param key
* @param clazz
* @param <T>
* @return
*/
public <T> T get(KeyPrefix prefix, String key, Class<T> clazz) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
String str = jedis.get(realKey);
T t = stringToBean(str,clazz);
return t;
} finally {
returnToPool(jedis);
}
}
/**
* 判断key是否存在
* @param prefix
* @param key
* @return
*/
public Boolean exists(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
return jedis.exists(realKey);
} finally {
returnToPool(jedis);
}
}
/**
* 增加值
* @param prefix
* @param key
* @return
*/
public Long incr(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
return jedis.incr(realKey);
} finally {
returnToPool(jedis);
}
}
/**
* 减少值(如果是number类型,则减一,否则置value为0再-1)
* @param prefix
* @param key
* @return
*/
public Long decr(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
return jedis.decr(realKey);
} finally {
returnToPool(jedis);
}
}
/**
* 对象转成字符串
* @param value
* @param <T>
* @return
*/
private <T> String beanToString(T value) {
if (value == null) {
return null;
}
Class<?> clazz = value.getClass();
if (clazz == Integer.class) {
return "" + value;
} else if (clazz == String.class) {
return (String) value;
} else if (clazz == Long.class) {
return "" + value;
} else {
return JSON.toJSONString(value);
}
}
/**
* 字符串转成对象
* @param str
* @param clazz
* @param <T>
* @return
*/
private <T> T stringToBean(String str, Class<T> clazz) {
if (StringUtils.isBlank(str) || clazz == null) {
return null;
}
if (clazz == Integer.class) {
return (T)Integer.valueOf(str);
} else if (clazz == String.class) {
return (T)str;
} else if (clazz == Long.class) {
return (T) Long.valueOf(str);
} else {
return JSON.parseObject(str,clazz);
}
}
/**
* 将Jedis返回到连接池里
* @param jedis
*/
private void returnToPool(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
}
DemoController.java
@Controller
@RequestMapping("/demo")
public class DemoController {
@Autowired
private UserService userService;
@Autowired
private RedisService redisService;
@RequestMapping("/redis/get")
@ResponseBody
public Result<User> redisGet() {
return Result.success(redisService.get(UserKey.getById, "2", User.class));
}
@RequestMapping("/redis/set")
@ResponseBody
public Result<Boolean> redisSet() {
User user = new User();
user.setId(2);
user.setName("hmx2");
return Result.success(redisService.set(UserKey.getById, "2", user));
}
@RequestMapping("/db/tx")
@ResponseBody
public Result<Boolean> tx() {
return userService.tx();
}
@RequestMapping("/thymeleaf")
public String thymeleaf(Model model) {
model.addAttribute("name", "HMX");
return "hello";
}
@ResponseBody
@RequestMapping("/hello")
public Result<String> hello() {
return Result.success("hello springboot");
}
@ResponseBody
@RequestMapping("/error")
public Result<String> error() {
return Result.fail(CodeMsg.SERVER_ERROR);
}
}