(一) Java 秒杀系统方案优化-环境搭建
一、项目介绍
目标
使用浏览器缓存/Nginx缓存/ 页面缓存/ 对象缓存/RabbitMQ队列异步下单,减少网络流量,减轻数据库压力,全面提升系统并发处理能力。
Java 微服务框架
SpringBoot/ RabbitMQ /Redis/ MySQL
安全策略
图形验证码、限流防刷、接口地址隐藏,多种安全机制拒绝机器刷单
1.2 可以学习到
应对大并发
- 如何利用缓存
- 如何使用异步
SpringBoot 环境搭建
-
集成 Thymeleaf,Result 结果封装
-
集成 Mybatis+Druid
-
集成 Jedis+Redis 安装+通用缓存 Key 封装
实现登录功能
- 数据库设计
- 明文密码两次 MD5 处理
- JSR303 参数检验+全局异常处理器
- 分布式 Session
实现秒杀功能
- 数据库设计
- 商品列表页
- 商品详情页
- 订单详情页
JMeter 压测
- JMeter 入门
- 自定义变量模拟多用户
- JMeter 命令行使用
- Spring Boot 打 war 包
页面优化技术
- 页面缓存+URL缓存+对象缓存
- 页面静态化,前后端分离
- 静态资源优化
- CDN 优化
接口优化
- Redis 预减库存减少数据库访问
- 内存标记减少 Redis 访问
- RabbitMQ 队列缓冲,异步下单,增强用户体验
- RabbitMQ 安装与 Spring Boot 集成
- 访问 Nginx 水平扩展
- 压测
安全优化
- 秒杀接口地址隐藏
- 数学公式验证码
- 接口防刷
二、SpringBoot 环境搭建
2.1 创建工程项目,导入对应坐标
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.gx</groupId>
<artifactId>seckill</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>seckill</name>
<description>Seckill project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2 封装结果类
2.2.1 封装状态码与信息类 CodeMsg
package com.gx.seckill.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, "密码错误");
//商品模块 5003XX
//订单模块 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( ) {
}
/**
* 私有构造函数
* @param code 状态码
* @param msg 信息
*/
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 + "]";
}
}
2.2.2 封装返回结果类 Result<T>
package com.gx.seckill.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);
}
/**
* 构造函数要私有,不希望其他类可以创建 Result 对象
* @param data
*/
private Result(T data) {
this.code = 0;
this.msg = "success";
this.data = data;
}
private Result(int code, String msg) {
this.code = code;
this.msg = msg;
}
private Result(CodeMsg codeMsg) {
if(codeMsg != null) {
this.code = codeMsg.getCode();
this.msg = codeMsg.getMsg();
}
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public T getData() {
return data;
}
}
2.3 集成 Thymeleaf
1)导入依赖坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2)添加配置信息
在 application.properties
中添加配置信息
#thymeleaf
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false
spring.thymeleaf.servlet.content-type=text/html
spring.thymeleaf.enabled=true
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.mode=HTML5
3)Controller 中验证
package com.gx.seckill.controller;
import com.gx.seckill.result.CodeMsg;
import com.gx.seckill.result.Result;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/demo")
public class DemoController {
@RequestMapping("/")
@ResponseBody
String home() {
return "Hello World!";
}
//1.rest api json 输出 2.页面
@RequestMapping("/hello")
@ResponseBody
public Result<String> hello() {
return Result.success("hello,imooc");
}
@RequestMapping("/helloError")
@ResponseBody
public Result<String> helloError() {
return Result.error(CodeMsg.SERVER_ERROR);
}
@RequestMapping("/thymeleaf")
public String thymeleaf(Model model) {
model.addAttribute("name", "Joshua");
return "hello";
}
}
2.4 集成 Mybatis
1)导入依赖坐标
<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.1.10</version>
</dependency>
2)添加配置信息
在 application.properties
中添加配置信息
# mybatis
mybatis.type-aliases-package=com.gx.seckill.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/gx/seckill/dao/*.xml
# druid
spring.datasource.url=jdbc:mysql://192.168.56.10:3306/seckill?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.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
3)测试
3.1)创建数据库
创建数据库 scekill
,并创建 user
表
3.2)创建 user 表对应的实体类
package com.gx.seckill.domain;
public class User {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
3.3)创建 UserDao 查询数据库
package com.gx.seckill.dao;
import com.gx.seckill.domain.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserDao {
@Select("select * from user where id = #{id}")
public User getById(@Param("id") int id);
@Insert("insert into user(id, name)values(#{id}, #{name})")
public int insert(User user);
}
3.4)创建 UserService 用户通过 dao 查询数据库的类
package com.gx.seckill.service;
import com.gx.seckill.dao.UserDao;
import com.gx.seckill.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
@Autowired
UserDao userDao;
public User getById(int id) {
return userDao.getById(id);
}
@Transactional
public boolean tx() {
User u1= new User();
u1.setId(2);
u1.setName("2222");
userDao.insert(u1);
User u2= new User();
u2.setId(1);
u2.setName("11111");
userDao.insert(u2);
return true;
}
}
3.5)Controller 中测试
package com.gx.seckill.controller;
import com.gx.seckill.domain.User;
import com.gx.seckill.result.CodeMsg;
import com.gx.seckill.result.Result;
import com.gx.seckill.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/demo")
public class DemoController {
@Autowired
UserService userService;
@RequestMapping("/db/get")
@ResponseBody
public Result<User> dbGet() {
User user = userService.getById(1);
return Result.success(user);
}
@RequestMapping("/db/tx")
@ResponseBody
public Result<Boolean> dbTx() {
userService.tx();
return Result.success(true);
}
}
2.5 集成 Redis
1)导入依赖坐标
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.38</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
2)application.properties
添加配置信息
#redis
redis.host=192.168.56.10
redis.port=6379
redis.timeout=10
#redis.password=123456
redis.poolMaxTotal=1000
redis.poolMaxIdle=500
redis.poolMaxWait=500
3)创建 RedisConfig 类
用于读取 application.properties
中 redis 的配置信息
package com.gx.seckill.redis;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix="redis")
public class RedisConfig {
private String host;
private int port;
private int timeout;//秒
private String password;
private int poolMaxTotal;
private int poolMaxIdle;
private int poolMaxWait;//秒
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getPoolMaxTotal() {
return poolMaxTotal;
}
public void setPoolMaxTotal(int poolMaxTotal) {
this.poolMaxTotal = poolMaxTotal;
}
public int getPoolMaxIdle() {
return poolMaxIdle;
}
public void setPoolMaxIdle(int poolMaxIdle) {
this.poolMaxIdle = poolMaxIdle;
}
public int getPoolMaxWait() {
return poolMaxWait;
}
public void setPoolMaxWait(int poolMaxWait) {
this.poolMaxWait = poolMaxWait;
}
}
4)创建 RedisPoolFactory 类
用于创建 JedisPool 对象
package com.gx.seckill.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@Service
public class RedisPoolFactory {
@Autowired
RedisConfig redisConfig;
@Bean
public JedisPool JedisPoolFactory() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());
poolConfig.setMaxTotal(redisConfig.getPoolMaxTotal());
//unit: ms
poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait() * 1000);
JedisPool jp = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(),
redisConfig.getTimeout()*1000, redisConfig.getPassword(), 0);
return jp;
}
}
5)通用缓存 Key 封装
作用:为缓存中 key 添加前缀
设计:接口<-抽象类<-实现类
5.1)创建 KeyPrefix 接口
package com.gx.seckill.redis;
public interface KeyPrefix {
public int expireSeconds();
public String getPrefix();
}
5.2)创建 BasePrefix 抽象类
package com.gx.seckill.redis;
public abstract class BasePrefix implements KeyPrefix{
private int expireSeconds;
private String prefix;
public BasePrefix(String prefix) {//0代表永不过期
this(0, prefix);
}
public BasePrefix( int expireSeconds, String prefix) {
this.expireSeconds = expireSeconds;
this.prefix = prefix;
}
@Override
public int expireSeconds() {//默认0代表永不过期
return expireSeconds;
}
@Override
public String getPrefix() {
String className = getClass().getSimpleName();
return className+":" + prefix;
}
}
5.3)UserKey
package com.gx.seckill.redis;
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");
}
5.4)OrderKey
package com.gx.seckill.redis;
public class OrderKey extends BasePrefix {
public OrderKey(String prefix) {
super(prefix);
}
public static OrderKey getMiaoshaOrderByUidGid = new OrderKey("moug");
}
5.5)GoodsKey
package com.gx.seckill.redis;
public class GoodsKey extends BasePrefix{
private GoodsKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static GoodsKey getGoodsList = new GoodsKey(60, "gl");
public static GoodsKey getGoodsDetail = new GoodsKey(60, "gd");
public static GoodsKey getMiaoshaGoodsStock= new GoodsKey(0, "gs");
}
5.6)AccessKey
package com.gx.seckill.redis;
public class AccessKey extends BasePrefix{
private AccessKey( int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static AccessKey withExpire(int expireSeconds) {
return new AccessKey(expireSeconds, "access");
}
}
5.7)MiaoshaKey
package com.gx.seckill.redis;
public class MiaoshaKey extends BasePrefix{
private MiaoshaKey( int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static MiaoshaKey isGoodsOver = new MiaoshaKey(0, "go");
public static MiaoshaKey getMiaoshaPath = new MiaoshaKey(60, "mp");
public static MiaoshaKey getMiaoshaVerifyCode = new MiaoshaKey(300, "vc");
}
5.7)MiaoshaUserKey
package com.gx.seckill.redis;
public class MiaoshaUserKey extends BasePrefix{
public static final int TOKEN_EXPIRE = 3600*24 * 2;
private MiaoshaUserKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "tk");
public static MiaoshaUserKey getById = new MiaoshaUserKey(0, "id");
}
6)RedisService
实现 Redis 的具体操作
package com.gx.seckill.redis;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson.JSON;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
@Service
public class RedisService {
@Autowired
JedisPool jedisPool;
/**
* 获取当个对象
* */
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);
}
}
/**
* 设置对象
* */
public <T> boolean set(KeyPrefix prefix, String key, T value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String str = beanToString(value);
if(str == null || str.length() <= 0) {
return false;
}
//生成真正的key
String realKey = prefix.getPrefix() + key;
int seconds = prefix.expireSeconds();
if(seconds <= 0) {
jedis.set(realKey, str);
}else {
jedis.setex(realKey, seconds, str);
}
return true;
}finally {
returnToPool(jedis);
}
}
/**
* 判断key是否存在
* */
public <T> 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);
}
}
/**
* 删除
* */
public boolean delete(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
long ret = jedis.del(realKey);
return ret > 0;
}finally {
returnToPool(jedis);
}
}
/**
* 增加值
* */
public <T> 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);
}
}
/**
* 减少值
* */
public <T> 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);
}
}
public boolean delete(KeyPrefix prefix) {
if(prefix == null) {
return false;
}
List<String> keys = scanKeys(prefix.getPrefix());
if(keys==null || keys.size() <= 0) {
return true;
}
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.del(keys.toArray(new String[0]));
return true;
} catch (final Exception e) {
e.printStackTrace();
return false;
} finally {
if(jedis != null) {
jedis.close();
}
}
}
/**
* 命令格式:SCAN cursor [MATCH pattern] [COUNT count]
* @cursor 游标
* @[MATCH pattern] 返回和给定模式相匹配的元素
* @count 每次迭代所返回的元素数量
* @return SCAN命令返回的是一个游标,从0开始遍历,到0结束遍历。
*/
public List<String> scanKeys(String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
List<String> keys = new ArrayList<String>();
String cursor = "0";
ScanParams sp = new ScanParams();
sp.match("*"+key+"*");
sp.count(100);
do{
ScanResult<String> ret = jedis.scan(cursor, sp);
List<String> result = ret.getResult();
if(result!=null && result.size() > 0){
keys.addAll(result);
}
//再处理cursor
cursor = ret.getStringCursor();
}while(!cursor.equals("0"));
return keys;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
//TODO 转换类型不全 Boolean、Double、Float等
public static <T> String beanToString(T value) {
if(value == null) {
return null;
}
Class<?> clazz = value.getClass();
if(clazz == int.class || clazz == Integer.class) {
return ""+value;
}else if(clazz == String.class) {
return (String)value;
}else if(clazz == long.class || clazz == Long.class) {
return ""+value;
}else {
return JSON.toJSONString(value);
}
}
//TODO 转换类型不全 Boolean、Double、Float等
@SuppressWarnings("unchecked")
public static <T> T stringToBean(String str, Class<T> clazz) {
if(str == null || str.length() <= 0 || clazz == null) {
return null;
}
if(clazz == int.class || clazz == Integer.class) {
return (T)Integer.valueOf(str);
}else if(clazz == String.class) {
return (T)str;
}else if(clazz == long.class || clazz == Long.class) {
return (T)Long.valueOf(str);
}else {
return JSON.toJavaObject(JSON.parseObject(str), clazz);
}
}
private void returnToPool(Jedis jedis) {
if(jedis != null) {
jedis.close();
}
}
}
7)Controller 中测试
package com.gx.seckill.controller;
import com.gx.seckill.domain.User;
import com.gx.seckill.redis.RedisService;
import com.gx.seckill.redis.UserKey;
import com.gx.seckill.result.CodeMsg;
import com.gx.seckill.result.Result;
import com.gx.seckill.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/demo")
public class DemoController {
@Autowired
RedisService redisService;
@RequestMapping("/redis/get")
@ResponseBody
public Result<User> redisGet() {
User user = redisService.get(UserKey.getById, ""+1, User.class);
return Result.success(user);
}
@RequestMapping("/redis/set")
@ResponseBody
public Result<Boolean> redisSet() {
User user = new User();
user.setId(1);
user.setName("1111");
redisService.set(UserKey.getById, ""+1, user);//UserKey:id1
return Result.success(true);
}
}