# 商品秒杀系统的设计与实现
**Repository Path**: H-study/seckill
## Basic Information
- **Project Name**: 商品秒杀系统的设计与实现
- **Description**: 基于SpringBoot、MyBatis-Plus、Redis、RabbitMQ实现的商品秒杀系统,解决商品超卖问题,做了相应的优化,能抵抗一定的并发量,实现商品的秒杀功能。
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 13
- **Forks**: 3
- **Created**: 2021-09-13
- **Last Updated**: 2025-12-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
## 1. 技术点介绍

## 2. 秒杀方案

## 3. 学习目标

## 4. 如何设计
秒杀,对我们来说,都不是一个陌生的东西。每年的双11,618以及时下流行的直播等等。秒杀然而,这对于我们系统而言是一个巨大的考验。
那么,如何才能更好地理解秒杀系统呢?我觉得作为一个程序员,你首先需要从高纬度出发,从整体上思考 问题。在我看来,秒杀其实主要解决两个问题,一个 并发读,一个并发写。并发读的核心优化理念是尽量减少用户到服务来"读"数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。另外,我们还要针对秒杀系统做一些保护,针对意料之外的情况兜底方案,以防止最坏的情况发生。
其实,秒杀的整体架构可以概况为:"稳、准、快"几个关键字。
"稳",就是整个系统架构要满足高可用,流量符合预期肯定要稳定,就是超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。
"准",就是秒杀10台手机,那就是只能交10台,多一台都不行。一旦库存不对,那平台就要承担损失,所以"准"就是要求保证数量的一致性。
"快",就是说系统的性能要足够的高,否则你怎么支撑这么大的流量呢。不光服务端做到极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整一个系统就完美了。
## 5. 项目搭建
### 5.1 创建项目
> 创建springboot项目

> 设置项目信息

> 选择Lombok、Spring Web、Thymeleaf、MySQL Driver依赖

> 写好项目名称和路径

最后完成项目的创建。
### 5.2 引入依赖
> 引入mybatis-plus依赖
```xml
com.baomidou
mybatis-plus-boot-starter
3.4.0
```
### 5.3 配置文件
> 配置application.yml文件
```yaml
spring:
# thymeleaf配置
thymeleaf:
# 关闭缓存
cache: false
# 数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: H8888
# 配置连接池
hikari:
# 连接池名称
pool-name: DataHikariCP
# 最小空闲连接
minimum-idle: 5
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 1800000
# 最大连接数
maximum-pool-size: 18
# 从连接池返回的连接自动提交
auto-commit: true
# 连接最大存活时间,0表示永久存活,默认1800000(30分钟)
max-lifetime: 1800000
# 连接超时间,默认30000(30秒)
connection-timeout: 30000
# 测试连接 是否可用的查询语句
connection-test-query: SELECT 1
# mybatis-plus配置
mybatis-plus:
# 配置mapper.xml映射文件位置
mapper-locations: classpath*:/mapper/*Dao.xml
# 配置mybatis数据返回类型的别名(默认别名是类名)
type-aliases-package: com.hsb.seckill.entity
# mybatis SQL(方法接口所在的包,不是Mapper.xml所在的包)
logging:
level:
com.hsb.seckill.dao: debug
```
### 5.4 创建项目包名
在com.hsb.seckill包下分别如下包名:
- entity:存放实体类
- dao:存放dao类
- service:存放service类
- impl:存放service实现类
- controller:存放控制类
- utils:存放工具类
- 在resource文件下创建mapper文件存放mybatis的映射文件
### 5.5 封装响应结果
> 公共返回对象枚举
```java
/**
* 公共返回对象枚举
*/
@Getter
@ToString
@AllArgsConstructor
public enum ResBeanEnum {
// 通用
SUCCESS(200,"SUCCESS"),
ERROR(500,"服务端异常"),
// 登录异常
LOGIN_ERROR(500210,"用户名或密码错误"),
MOBILE_ERROR(500211,"手机号码格式不正确");
private final Integer code;
private final String message;
}
```
> 公共返回对象
```java
/**
* 公共返回对象
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResBean {
private long code;
private String message;
private Object obj;
/**
* 成功返回结果
* @return
*/
public static ResBean success(){
return new ResBean(ResBeanEnum.SUCCESS.getCode(),ResBeanEnum.SUCCESS.getMessage(),null);
}
/**
* 成功返回结果
* @param obj 传入一个对象
* @return
*/
public static ResBean success(Object obj){
return new ResBean(ResBeanEnum.SUCCESS.getCode(),ResBeanEnum.SUCCESS.getMessage(),obj);
}
/**
* 失败返回结果
* @param resBeanEnum
* @return
*/
public static ResBean error(ResBeanEnum resBeanEnum){
return new ResBean(resBeanEnum.getCode(), resBeanEnum.getMessage(),null);
}
/**
* 失败返回结果
* @param resBeanEnum
* @param obj
* @return
*/
public static ResBean error(ResBeanEnum resBeanEnum,Object obj){
return new ResBean(resBeanEnum.getCode(), resBeanEnum.getMessage(),obj);
}
}
```
## 6. 登录功能
### 6.1 创建user用户表
```sql
CREATE TABLE `t_user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '用户id',
`mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '手机号码',
`nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt)+salt)',
`slat` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`head` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像',
`register_date` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
`last_login_date` datetime NULL DEFAULT NULL COMMENT '最后一次登录时间',
`login_count` int NULL DEFAULT 0 COMMENT '登录次数',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (1, '18476816500', 'admin', 'b7797cce01b4b131b433b6acf4add449', '1a2b3c4d', NULL, '2021-09-04 15:55:11', NULL, 0);
```
### 6.2 逆向工程
通过EasyCode插件自动生成user表的entity、dao、service、impl、controller和mapper映射文件

### 6.3 MD5加密
> 整体加密流程
MD5(MD5(pass明文+固定salt)+随机salt)
第一次固定salt写死在前端
第二次加密采用随机的salt 并将每次生成的salt保存在数据库中
> 登录流程
前端对用户输入的密码进行md5加密(固定的salt)
将加密后的密码传递到后端
后端使用用户id取出用户信息
后端对加密后的密码在进行md5加密(取出盐),然后与数据库中存储的密码进行对比,
ok登录成功,否则登录失败
> 注册流程
前端对用户输入的密码进行md5加密(固定的salt)
将加密后的密码传递到后端
后端随机生成一个salt,
使用生成salt对前端传过来的密码进行加密,然后将加密后密码和salt一起保存到db中
> 引入依赖
```xml
commons-codec
commons-codec
```
> 创建MD5加密工具类MD5Util
```java
/**
* MD5工具类
*/
@Component
public class MD5Util {
// md5加密
public static String md5(String src){
return DigestUtils.md5Hex(src);
}
private static final String salt="1a2b3c4d";
// 客户端到服务端加密
public static String inputPassToFromPass(String inputPass){
String str = "" + salt.charAt(0)+salt.charAt(2)+inputPass+salt.charAt(5)+salt.charAt(4);
return md5(str);
}
// 服务端到数据库加密
public static String fromPassToDBPass(String fromPass,String salt){
String str = "" + salt.charAt(0)+salt.charAt(2)+fromPass+salt.charAt(5)+salt.charAt(4);
return md5(str);
}
// 客户端到数据库,两次加密
public static String inputPassToDBPass(String inputPass,String salt){
String fromPass = inputPassToFromPass(inputPass);
String dbPass = fromPassToDBPass(fromPass, salt);
return dbPass;
}
public static void main(String[] args) {
System.out.println(inputPassToFromPass("123456"));
System.out.println(fromPassToDBPass("d3b1294a61a07da9b49b6e22b2cbd7f9","1a2b3c4d"));
System.out.println(inputPassToDBPass("123456","1a2b3c4d"));
}
}
```
### 6.4 登录页面
> 引入登录页面和静态资源文件
```html
登录
```
> 实现登录页面的跳转
```java
@Controller
@RequestMapping("/login")
@Slf4j
public class LoginController {
/**
* 跳转登录页面
* @return
*/
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
}
```

### 6.5 登录功能实现
根据用户手机查询用户
> dao
```java
/**
* 根据手机号码查询用户
* @return
*/
User queryByMobile(String mobile);
```
> service
```java
/**
* 登录
* @param loginVo
* @return
*/
ResBean doLogin(LoginVo loginVo);
```
> impl
```java
/**
* 登录
* @param loginVo
* @return
*/
@Override
public ResBean doLogin(LoginVo loginVo) {
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
//判断手机号码或密码是否为空
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)){
return ResBean.error(ResBeanEnum.LOGIN_ERROR);
}
//校验手机号码是否合法
if (!ValidatorUtil.isMobile(mobile)){
return ResBean.error(ResBeanEnum.MOBILE_ERROR);
}
//根据手机号查询用户
User user = userDao.queryByMobile(mobile);
//判断用户是否存在
if (null == user){
return ResBean.error(ResBeanEnum.LOGIN_ERROR);
}
//判断密码是否正确
if (!MD5Util.fromPassToDBPass(password,user.getSlat()).equals(user.getPassword())){
return ResBean.error(ResBeanEnum.LOGIN_ERROR);
}
return ResBean.success();
}
```
> controller
```java
/**
* 登录功能
* @param loginVo
* @return
*/
@ResponseBody
@RequestMapping("/doLogin")
public ResBean doLogin(@Valid LoginVo loginVo){
// log.info("{}",loginVo);
return userService.doLogin(loginVo);
}
```
### 6.6 自定义注解参数校验
> 引入依赖
```xml
org.springframework.boot
spring-boot-starter-validation
```
> 在参数类属性上添加注解
```java
/**
* 登录参数
*/
@Data
public class LoginVo {
@NotNull
@IsMobile
private String mobile;
@NotNull
@Length(min = 32)
private String password;
}
```
> 校验手机号码校验注解
```java
/**
* 校验手机号注解
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {IsMobileValidator.class}
)
public @interface IsMobile {
boolean require() default true;
String message() default "手机号码格式错误";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
```
> 手机号码校验规则类
```java
/**
* 校验手机号码规则
*/
public class IsMobileValidator implements ConstraintValidator {
private boolean required = false;
@Override
public void initialize(IsMobile constraintAnnotation) {
required = constraintAnnotation.require();
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (required){
return ValidatorUtil.isMobile(s);
}else {
if (StringUtils.isEmpty(s)){
return true;
}else {
return ValidatorUtil.isMobile(s);
}
}
}
}
```
> 手机号校验工具类
```java
/**
* 手机号码校验
*/
public class ValidatorUtil {
private static final Pattern mobile_pattern = Pattern.compile("[1]([3-9])[0-9]{9}$");
public static boolean isMobile(String mobile){
if (StringUtils.isEmpty(mobile)){
return false;
}
Matcher matcher = mobile_pattern.matcher(mobile);
return matcher.matches();
}
}
```
### 6.7 异常处理
我们知道,系统异常包括:编译时异常和运行时异常RuntimeException,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。在开发中,不管时dao层、service层还是controller层,都有可能抛出异常,在springmvc中,能将所有类型的异常处理过程解耦出来,既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护。
springboot全局异常处理方式主要有两种:
- 使用@ControllerAdvice和@ExceptionHandler注解
- 使用ErrorController类来实现
区别:
1、@ControllerAdvice方式只能处理控制器抛出异常,此时请求已经进行控制器中。
2、ErrorController类方式可以处理所有的异常,包括未进入控制器的异常,比如404、401等错误。
3、如果应用中两者公同存在,则@ControllerAdvice方式处理控制器的异常,ErrorController方式处理为进入控制器的异常。
4、@ControllerAdvice方式可以定义多个拦截方法,拦截不同的异常类,并且可以获取异常信息,自由度更大。
> 公共返回结果类
```java
/**
* 公共返回对象
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResBean {
private long code;
private String message;
private Object obj;
/**
* 成功返回结果
* @return
*/
public static ResBean success(){
return new ResBean(ResBeanEnum.SUCCESS.getCode(),ResBeanEnum.SUCCESS.getMessage(),null);
}
/**
* 成功返回结果
* @param obj 传入一个对象
* @return
*/
public static ResBean success(Object obj){
return new ResBean(ResBeanEnum.SUCCESS.getCode(),ResBeanEnum.SUCCESS.getMessage(),obj);
}
/**
* 失败返回结果
* @param resBeanEnum
* @return
*/
public static ResBean error(ResBeanEnum resBeanEnum){
return new ResBean(resBeanEnum.getCode(), resBeanEnum.getMessage(),null);
}
/**
* 失败返回结果
* @param resBeanEnum
* @param obj
* @return
*/
public static ResBean error(ResBeanEnum resBeanEnum,Object obj){
return new ResBean(resBeanEnum.getCode(), resBeanEnum.getMessage(),obj);
}
}
```
> 自定义异常枚举类
```java
/**
* 公共返回对象枚举
*/
@Getter
@ToString
public enum ResBeanEnum {
// 通用
SUCCESS(200,"SUCCESS"),
ERROR(500,"服务端异常"),
// 登录异常
LOGIN_ERROR(500210,"用户名或密码错误"),
MOBILE_ERROR(500211,"手机号码格式不正确"),
//绑定异常
BIND_ERROR(500212,"参数校验异常");
private final Integer code;
private final String message;
ResBeanEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
```
> 全局异常类
```java
/**
* 全局异常
*/
public class GlobalException extends RuntimeException{
private ResBeanEnum resBeanEnum;
public GlobalException(ResBeanEnum resBeanEnum){
this.resBeanEnum = resBeanEnum;
}
public ResBeanEnum getResBeanEnum() {
return resBeanEnum;
}
public void setResBeanEnum(ResBeanEnum resBeanEnum) {
this.resBeanEnum = resBeanEnum;
}
}
```
> 全局异常处理类
```java
/**
* 全局异常处理类
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResBean ExceptionHandler(Exception e){
if (e instanceof GlobalException){
GlobalException ex = (GlobalException) e;
return ResBean.error(ex.getResBeanEnum());
}else if (e instanceof BindException){
BindException ex = (BindException) e;
ResBean resBean = ResBean.error(ResBeanEnum.BIND_ERROR);
resBean.setMessage("参数校验异常:"+ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return resBean;
}
return ResBean.error(ResBeanEnum.ERROR);
}
}
```
> 登录校验
```java
/**
* 登录
* @param loginVo
* @return
*/
@Override
public ResBean doLogin(LoginVo loginVo) {
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
//根据手机号查询用户
User user = userDao.queryByMobile(mobile);
//判断用户是否存在
if (null == user){
// return ResBean.error(ResBeanEnum.LOGIN_ERROR);
throw new GlobalException(ResBeanEnum.LOGIN_ERROR);
}
//判断密码是否正确
if (!MD5Util.fromPassToDBPass(password,user.getSlat()).equals(user.getPassword())){
// return ResBean.error(ResBeanEnum.LOGIN_ERROR);
throw new GlobalException(ResBeanEnum.LOGIN_ERROR);
}
return ResBean.success();
}
```
### 6.8 设置cookie和session
> UUID工具类
```java
/**
* uuid工具类
*/
public class UUIDUtil {
// 生成uuid
public static String uuid(){
return UUID.randomUUID().toString().replace("-","");
}
}
```
> cookie工具类
```java
public final class CookieUtil {
/**
* 得到Cookie的值, 不编码
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName) {
return getCookieValue(request, cookieName, false);
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
if (isDecoder) {
retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
} else {
retValue = cookieList[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue) {
setCookie(request, response, cookieName, cookieValue, -1);
}
/**
* 设置Cookie的值 在指定时间内生效,但不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage) {
setCookie(request, response, cookieName, cookieValue, cookieMaxage, true);
}
/**
* 设置Cookie的值 不设置生效时间,但编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, boolean isEncode) {
setCookie(request, response, cookieName, cookieValue, -1, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, boolean isEncode) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, String encodeString) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
}
/**
* 删除Cookie带cookie域名
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName) {
doSetCookie(request, response, cookieName, "", -1, false);
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
// System.out.println(domainName);
if (!"localhost".equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
try {
if (cookieValue == null) {
cookieValue = "";
} else {
cookieValue = URLEncoder.encode(cookieValue, encodeString);
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 得到cookie的域名
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
serverName = serverName.toLowerCase();
serverName = serverName.substring(7);
final int end = serverName.indexOf("/");
serverName = serverName.substring(0, end);
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = "." + domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
return domainName;
}
}
```
> 在登录方法上设置cookie和session
```java
//设置session和cookie
String ticket = UUIDUtil.uuid();
request.getSession().setAttribute(ticket,user);
CookieUtil.setCookie(request,response,"userTicket",ticket);
```
> 登录成功后,检验seesion和cookie
```java
/**
* 检验用户,跳转商品页面
* @param session
* @param model
* @param ticket
* @return
*/
@RequestMapping("/toList")
public String toList(HttpSession session, Model model,@CookieValue("userTicket") String ticket){
if (StringUtils.isEmpty(ticket)){
return "login";
}
User user = (User) session.getAttribute(ticket);
if (null == user){
return "login";
}
model.addAttribute("user",user);
return "goodsList";
}
```
### 6.9 SpringSession实现分布式session
> 首先要安装redis
> 引入依赖
```xml
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
org.springframework.session
spring-session-data-redis
```
> 配置redis
```yaml
# 配置redis
redis:
# 服务器地址
host: 192.168.159.200
# 端口
port: 6379
# 操作数据库
database: 0
# 超时时间
timeout: 10000ms
lettuce:
pool:
#最大连接数
max-active: 8
#最大连接阻塞等待时间,默认-1
max-wait: 10000ms
#最大空闲连接,默认8
max-idle: 200
#最小空闲连接,默认0
min-idle: 5
```
最后启动项目即可实现分布式session
### 6.10 Redis存储用户信息
> 去除spring session依赖
> 将session存储到redis中
```java
//设置session和cookie
String ticket = UUIDUtil.uuid();
//request.getSession().setAttribute(ticket,user);
//将用户信息存入redis中
redisTemplate.opsForValue().set("user:"+ticket,user);
CookieUtil.setCookie(request,response,"userTicket",ticket);
```
> 根据cookie获取用户
```java
/**
* 根据cookie获取用户
* @param userTicket
* @return
*/
@Override
public User getUserByCookie(String userTicket,HttpServletRequest request,HttpServletResponse response) {
if (StringUtils.isEmpty(userTicket)){
return null;
}
User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);
if (user != null){
CookieUtil.setCookie(request,response,"userTicket",userTicket);
}
return user;
}
```
### 6.11 登录优化
> 自定义用户参数
```java
/**
* 自定义用户参数
*/
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private UserService userService;
//返回true才执行下面的方法
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
Class> clazz = methodParameter.getParameterType();
return clazz== User.class;
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
String ticket = CookieUtil.getCookieValue(request, "userTicket");
if (StringUtils.isEmpty(ticket)){
return null;
}
return userService.getUserByCookie(ticket,request,response);
}
}
```
> MVC配置类
```java
/**
* MVC配置类
*/
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserArgumentResolver userArgumentResolver;
//设置参数解析器
@Override
public void addArgumentResolvers(List resolvers) {
resolvers.add(userArgumentResolver);
}
}
```
> 检验用户
```java
/**
* 检验用户,跳转商品页面
* @param model
* @return
*/
@RequestMapping("/toList")
public String toList(Model model,User user){
model.addAttribute("user",user);
return "goodsList";
}
```
## 7. 商品功能
### 7.1 创建数据表
> 商品表
```sql
create table `t_goods`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`goods_name` VARCHAR(16) DEFAULT NULL COMMENT '商品名称',
`goods_title` VARCHAR(64) DEFAULT NULl COMMENT '商品标题',
`goods_img` VARCHAR(64) DEFAULT NULL COMMENT '商品图片',
`goods_detail` LONGTEXT COMMENT '商品详情',
`goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品价格',
`goods_stock` INT(11) DEFAULT '0' COMMENT '商品库存,-1表示没有限制',
PRIMARY KEY(`id`)
)
```
> 订单表
```sql
create table `t_order`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',
`user_id` BIGINT(20) DEFAULT NULL COMMENT '用户id',
`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品id',
`delivery_addr_id` BIGINT(20) DEFAULT NULL COMMENT '收货地址',
`goods_name` VARCHAR(16) DEFAULT NULL COMMENT '冗余的商品名称',
`goods_count` INT(11) DEFAULT '0' COMMENT '商品数量',
`goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品单价',
`order_channel` TINYINT(4) DEFAULT '0' COMMENT '1pc,2android,3ios',
`status` TINYINT(4) DEFAULT '0' COMMENT '订单状态,1已支付,2已发货,3已收货,4已退款,5已完成',
`create_date` datetime DEFAULT NULL COMMENT '订单创建时间',
`pay_date` datetime DEFAULT NULL COMMENT '支付时间',
PRIMARY KEY(`id`)
)
```
> 秒杀商品表
```sql
create table `t_seckill_goods`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀商品id',
`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品id',
`seckill_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '秒杀价',
`stock_count` INT(10) DEFAULT NULL COMMENT '库存数量',
`start_date` datetime DEFAULT NULL COMMENT '秒杀开始时间',
`end_date` datetime DEFAULT NULL COMMENT '秒杀结束时间',
PRIMARY KEY(`id`)
)
```
> 秒杀订单表
```sql
create table `t_seckill_order`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀订单id',
`user_id` BIGINT(20) DEFAULT NULL COMMENT '用户id',
`order_id` BIGINT(20) DEFAULT NULL COMMENT '订单id',
`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品id',
PRIMARY KEY(`id`)
)
```
### 7.2 商品列表
> 商品列表页面
```html
商品列表
秒杀商品列表
| 商品名称 | 商品图片 | 商品原价 | 秒杀价 | 库存数量 | 详情 |
|
![]() |
|
|
|
详情 |
```
> 商品返回对象
```java
/**
* 商品返回对象
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GoodsVo extends Goods {
private BigDecimal seckillPrice;
private Integer stockCount;
private Date startDate;
private Date endDate;
}
```
> GoodsController
```java
/**
* 检验用户,跳转商品页面
* @param model
* @return
*/
@RequestMapping("/toList")
public String toList(Model model,User user){
if (null == user){
return "login";
}
// 查询所有秒杀商品
List goodsList = goodsService.querySeckillGoods();
model.addAttribute("goodsList",goodsList);
model.addAttribute("user",user);
return "goods_list";
}
```
### 7.3 商品详情
> 商品详情页面
```html
商品详情
秒杀商品详情
您还没有登录,请登陆后再操作
没有收货地址的提示。。。
| 商品名称 |
|
| 商品图片 |
![]() |
| 秒杀开始时间 |
|
秒杀倒计时:秒
秒杀进行中
秒杀已结束
|
|
| 秒杀结束时间 |
|
| 商品原价 |
|
| 秒杀价 |
|
| 库存数量 |
|
```
> GoodsController
```java
/**
* 秒杀商品详情页
* @param model
* @param user
* @param goodsId
* @return
*/
@RequestMapping("/toDetail/{goodsId}")
public String toDetail(Model model, User user, @PathVariable("goodsId") Integer goodsId){
if (null == user){
return "login";
}
//根据商品id查询秒杀商品
GoodsVo goods = goodsService.querySeckillGoodsById(goodsId);
//秒杀状态
int miaoshaStatus = 0;
//秒杀开始倒计时
int remainSeconds = 0;
//秒杀结束倒计时
int betweenSeconds = 0;
//秒杀开始时间
Date startDate = goods.getStartDate();
//秒杀结束时间
Date endDate = goods.getEndDate();
//获取当前时间
Date nowDate = new Date();
if (nowDate.after(endDate)){//秒杀已结束
miaoshaStatus = 2;
remainSeconds=-1;
}else if (nowDate.before(startDate)){//秒杀倒计时
remainSeconds = (int) ((startDate.getTime()- nowDate.getTime())/1000);
}else {//秒杀中
miaoshaStatus = 1;
}
//秒杀结束倒计时
betweenSeconds = (int) ((endDate.getTime()-startDate.getTime())/1000);
model.addAttribute("betweenSeconds",betweenSeconds);
model.addAttribute("remainSeconds",remainSeconds);
model.addAttribute("miaoshaStatus",miaoshaStatus);
model.addAttribute("goods",goods);
model.addAttribute("user",user);
return "goods_detail";
}
```
### 7.4 秒杀功能
> 订单详情页面
```html
订单详情
秒杀订单详情
| 商品名称 |
|
| 商品图片 |
![]() |
| 订单价格 |
|
| 下单时间 |
|
| 订单状态 |
未支付
待发货
已发货
已收货
已退款
已完成
|
|
| 收货人 |
XXX 18812341234 |
| 收货地址 |
北京市昌平区回龙观龙博一区 |
```
> SeckillController
```java
@Controller
@RequestMapping("/seckill")
public class SeckillController {
@Autowired
private GoodsService goodsService;
@Autowired
private SeckillOrderService seckillOrderService;
@Autowired
private OrderService orderService;
@RequestMapping("/doSeckill")
public String seckill(Model model, User user, long goodsId){
if (null == user){
return "login";
}
//根据商品id查询商品
GoodsVo goods = goodsService.querySeckillGoodsById(goodsId);
//判断库存
if (goods.getStockCount()<1){
model.addAttribute("errMsg", ResBeanEnum.EMPTY_STOCK.getMessage());
return "seckill_fail";
}
//判断是否有重复用户抢购
SeckillOrder seckillOrder = seckillOrderService.getOne(new QueryWrapper().eq("user_id", user.getId()).eq("goods_id", goodsId));
if (seckillOrder != null){
model.addAttribute("errMsg",ResBeanEnum.REPEATE_ERROR.getMessage());
return "seckill_fail";
}
//进行秒杀,创建订单
Order order = orderService.sekill(user,goods);
model.addAttribute("user",user);
model.addAttribute("order",order);
model.addAttribute("goods",goods);
return "order_detail";
}
}
```
> OrderServiceImpl
```java
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Autowired
private SeckillGoodsService seckillGoodsService;
@Autowired
private SeckillOrderService seckillOrderService;
/**
* 秒杀,创建订单
* @param user
* @param goods
* @return
*/
@Override
public Order sekill(User user, GoodsVo goods) {
//根据商品id秒杀商品
SeckillGoods seckillGoods = seckillGoodsService.getOne(new QueryWrapper().eq("goods_id", goods.getId()));
//秒杀商品库存减1
seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
//更新秒杀商品数据表
seckillGoodsService.updateById(seckillGoods);
//创建订单
Order order = new Order();
order.setUserId(user.getId());
order.setGoodsId(goods.getId());
order.setDeliveryAddrId(0L);
order.setGoodsName(goods.getGoodsName());
order.setGoodsCount(1);
order.setGoodsPrice(seckillGoods.getSeckillPrice());
order.setOrderChannel(1);
order.setStatus(0);
order.setCreateDate(new Date());
orderDao.insert(order);
//生产秒杀订单
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setUserId(user.getId());
seckillOrder.setOrderId(order.getId());
seckillOrder.setGoodsId(goods.getId());
seckillOrderService.save(seckillOrder);
return order;
}
}
```
### 7.5 商品超卖
> 解决用户重复抢购同一件商品的问题
- 向秒杀订单表添加user_id和goods_id作为唯一索引

方案1:
- 在redis中缓存用户订单信息
```java
// 将订单缓存到redis中
redisTemplate.opsForValue().set(("order:"+user.getId()+":"+goods.getId()),seckillOrder,60, TimeUnit.SECONDS);
```
- 每次秒杀之前先判断redis中是否存在该商品的订单信息,如果存在返回提示用户不能重复购买此商品
```java
// 从redis获取订单
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goods.getId());
if (seckillOrder != null){
model.addAttribute("errMsg",ResBeanEnum.REPEATE_ERROR.getMessage());
return "seckill_fail";
}
```
方案二:(推荐)
- 当用户进行秒杀之前,先判断redis是否存在key(seckillCount:user.id:goodsId)的值,如果存在则说明前面已经抢购过该商品了,返回提示信息(商品不能重复抢购),否则设置key值进redis
```java
Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("seckillCount:" + user.getId() + ":" + goodsId, user.getId());
if (!ifAbsent){
return ResBean.error(ResBeanEnum.REPEATE_ERROR);
}
```
> 解决商品超卖问题
- 判断库存数量是否大于0,如果大于0就可以进行抢购,否则不能抢购,返回提示商品库存不足
```java
//判断库存
if (goods.getStockCount()<1){
model.addAttribute("errMsg", ResBeanEnum.EMPTY_STOCK.getMessage());
return "seckill_fail";
}
```
- 在更新商品的库存,先判断当前的库存数量是否大于0,如果库存大于0,即可更新库存减一,并创建订单,否则更新失败
```java
// 解决库存超卖
boolean result = seckillGoodsService.update(new UpdateWrapper()
.setSql("stock_count = stock_count-1")
.eq("goods_id", goods.getId())
.gt("stock_count", 0));
if (!result){
return null;
}
```
## 8. 压力测试
### 8.1 JMeter的安装
到Apache官网下载JMeter压缩包,解压到文件夹下,打开bin目录,打开jmeter配置文件jmeter.properies
> 修改语言

> 修改编码

> 启动jmeter
打开bin目录下的jmeter.bat,即可打开jmeter

### 8.2 JMeter的使用
> 创建线程组
右键点击测试计划->添加->线程(用户)->线程组

创建1000个线程,0秒开始启动,循环10次

> 设置HTTP请求默认值
右键点击线程组->添加->配置元件->HTTP请求默认值

设置HTTP协议,服务器名称localhost,端口8080
[图片上传失败(image-bfYgnuuldYBwSBOXkgM9)]
> 设置HTTP请求
右键点击线程组->添加->取样器->HTTP请求
[图片上传失败(image-qUnOl9W7LNwXeb3lKu6K)]
名称为商品列表,GET请求,路径为/goods/toList的HTTP请求

> 聚合报告
右键点击线程组->添加->监听器->聚合


> 设置CSV数据配置文件
右键点击线程组->添加->配置元件->CSV Data Set Config

选择文件,设置文件编码为UTF-8,变量名称

> HTTP Cookie管理器
右键点击线程组->添加->配置元件->HTTP Cookie 管理器

添加名称,设置值,域,路径

### 8.3 使用工具类生产用户
> UserUtil
```java
public class UserUtil {
private static void createUser(int count) throws SQLException, ClassNotFoundException, IOException {
ArrayList users = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
User user = new User();
long num = 13000000000L+i;
user.setMobile(String.valueOf(num));
user.setNickname("user"+i);
user.setSlat("1a2b3c4d");
user.setPassword(MD5Util.inputPassToDBPass("123456",user.getSlat()));
user.setLoginCount(1);
user.setRegisterDate(new Date());
users.add(user);
}
System.out.println("create User");
//插入数据库
Connection conn = getConn();
String sql = "insert into t_user(login_count, nickname, register_date, slat, password, mobile)values(?,?,?,?,?,?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
for(int i=0;i= 0) {
bout.write(buff, 0 ,len);
}
inputStream.close();
bout.close();
String response = new String(bout.toByteArray());
System.out.println(response);
ObjectMapper mapper = new ObjectMapper();
ResBean resBean = mapper.readValue(response, ResBean.class);
String userTicket = (String) resBean.getObj();
System.out.println("create token : " + user.getMobile());
String row = user.getMobile()+","+userTicket;
raf.seek(raf.length());
raf.write(row.getBytes());
raf.write("\r\n".getBytes());
System.out.println("write to file : " + user.getMobile());
}
raf.close();
System.out.println("over");
}
private static Connection getConn() throws ClassNotFoundException, SQLException {
String driver= "com.mysql.cj.jdbc.Driver";
String url ="jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai";
String username= "root";
String password = "H8888";
Class.forName(driver);
return DriverManager.getConnection(url,username,password);
}
public static void main(String[] args) throws SQLException, IOException, ClassNotFoundException {
createUser(1000);
}
}
```
### 8.4 压测商品列表接口
> 设置HTTP请求

> 压测结果

### 8.5 压测商品秒杀接口
> 设置HTTP请求

> 压测结果

商品出现

## 9. 页面优化
### 9.1 页面缓存
> 商品列表页面
```java
@RequestMapping(value = "/toList",produces = "text/html;charset=utf-8")
@ResponseBody
public String toList(Model model, User user, HttpServletRequest request, HttpServletResponse response){
if (null == user){
//return "login";
WebContext context = new WebContext(request, response, request.getServletContext(), request.getLocale());
return thymeleafViewResolver.getTemplateEngine().process("login", context);
}
// 商品列表页面缓存,redis获取页面,如果不为空,直接获取页面
String html = (String) redisTemplate.opsForValue().get("goodsList");
if (!StringUtils.isEmpty(html)){
return html;
}
//redis实现分布式锁
String uuid = UUIDUtil.uuid();
Boolean flag = redisTemplate.opsForValue().setIfAbsent("goods", uuid, 30, TimeUnit.SECONDS);
if (flag){
try{
// 查询所有秒杀商品
List goodsList = goodsService.querySeckillGoods();
model.addAttribute("goodsList",goodsList);
model.addAttribute("user",user);
//如果为空,利用thymeleaf手动渲染页面,存储redis中
WebContext context = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goods_list", context);
if (!StringUtils.isEmpty(html)){
redisTemplate.opsForValue().set("goodsList",html,1, TimeUnit.SECONDS);
}
}finally {
if (uuid.equals(redisTemplate.opsForValue().get("goods"))){
redisTemplate.delete("goods");
}
}
}
return html;
}
```
> 商品详情页面
```java
@RequestMapping(value = "/toDetail/{goodsId}",produces = "text/html;charset=utf-8")
@ResponseBody
public String toDetail(Model model, User user, @PathVariable("goodsId") Integer goodsId,HttpServletRequest request,HttpServletResponse response){
if (null == user){
//return "login";
WebContext context = new WebContext(request, response, request.getServletContext(), request.getLocale());
return thymeleafViewResolver.getTemplateEngine().process("login", context);
}
//详情页面缓存,redis获取页面,如果不为空,直接获取页面
String html = (String) redisTemplate.opsForValue().get("goodsDetail:"+goodsId);
if (!StringUtils.isEmpty(html)){
return html;
}
//redis实现分布式锁
String uuid = UUIDUtil.uuid();
Boolean flag = redisTemplate.opsForValue().setIfAbsent("goodsDetail", uuid, 30, TimeUnit.SECONDS);
if (flag){
try{
//根据商品id查询秒杀商品
GoodsVo goods = goodsService.querySeckillGoodsById(goodsId);
//秒杀状态
int miaoshaStatus = 0;
//秒杀开始倒计时
int remainSeconds = 0;
//秒杀结束倒计时
int betweenSeconds = 0;
//秒杀开始时间
Date startDate = goods.getStartDate();
//秒杀结束时间
Date endDate = goods.getEndDate();
//获取当前时间
Date nowDate = new Date();
if (nowDate.after(endDate)){//秒杀已结束
miaoshaStatus = 2;
remainSeconds=-1;
}else if (nowDate.before(startDate)){//秒杀倒计时
remainSeconds = (int) ((startDate.getTime()- nowDate.getTime())/1000);
}else {//秒杀中
miaoshaStatus = 1;
}
//秒杀结束倒计时
betweenSeconds = (int) ((endDate.getTime()-startDate.getTime())/1000);
model.addAttribute("betweenSeconds",betweenSeconds);
model.addAttribute("remainSeconds",remainSeconds);
model.addAttribute("miaoshaStatus",miaoshaStatus);
model.addAttribute("goods",goods);
model.addAttribute("user",user);
//如果为空,利用thymeleaf手动渲染页面,存储redis中
WebContext context = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goods_detail",context);
if (!StringUtils.isEmpty(html)){
redisTemplate.opsForValue().set("goodsDetail:"+goodsId,html,60,TimeUnit.SECONDS);
}
}finally {
if (uuid.equals(redisTemplate.opsForValue().get("goodsDetail"))){
redisTemplate.delete("goodsDetail");
}
}
}
return html;
}
```
### 9.2 对象缓存
> 用户对象缓存
```java
/**
* 根据cookie获取用户
* @param userTicket
* @return
*/
@Override
public User getUserByCookie(String userTicket,HttpServletRequest request,HttpServletResponse response) {
if (StringUtils.isEmpty(userTicket)){
return null;
}
//将用户对象缓存到redis中
User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);
if (user != null){
CookieUtil.setCookie(request,response,"userTicket",userTicket);
}
return user;
}
```
> 更新用户密码并删除缓存
```java
/**
* 更新用户密码
* @param ticket
* @param password
* @return
*/
@Override
public ResBean updatePassword(String ticket, String password,HttpServletRequest request,HttpServletResponse response) {
User user = getUserByCookie(ticket, request, response);
if (user == null){
throw new GlobalException(ResBeanEnum.MOBILE_NOT_EXIST);
}
user.setPassword(MD5Util.inputPassToDBPass(password,user.getSlat()));
int result = userDao.updateById(user);
if (1 == result){
// 删除redis中的user缓存
redisTemplate.delete("user:"+ticket);
return ResBean.success();
}
return ResBean.error(ResBeanEnum.PASSWORD_UPDATE_FAIL);
}
```
### 9.3 页面静态化
> 配置
```yaml
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:/stataic/
```
> 商品详情页面静态化
GoodsDetail
```java
/**
* 商品详情返回对象
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GoodsDetailVo {
private User user;
private GoodsVo goodsVo;
private int remainSeconds;
private int miaoshaStatus;
private int betweenSeconds;
}
```
goods_detail.htm
```html
商品详情
秒杀商品详情
您还没有登录,请登陆后再操作
没有收货地址的提示。。。
| 商品名称 |
|
| 商品图片 |
![]() |
| 秒杀开始时间 |
|
|
|
| 秒杀结束时间 |
|
| 商品原价 |
|
| 秒杀价 |
|
| 库存数量 |
|
```
controller
```java
@RequestMapping(value = "/toDetail/{goodsId}",method = RequestMethod.GET)
@ResponseBody
public ResBean toDetail(User user, @PathVariable("goodsId") Integer goodsId){
if (null == user){
return ResBean.error(ResBeanEnum.USER_NOT_LOGIN);
}
//根据商品id查询秒杀商品
GoodsVo goods = goodsService.querySeckillGoodsById(goodsId);
//秒杀状态
int miaoshaStatus = 0;
//秒杀开始倒计时
int remainSeconds = 0;
//秒杀结束倒计时
int betweenSeconds = 0;
//秒杀开始时间
Date startDate = goods.getStartDate();
//秒杀结束时间
Date endDate = goods.getEndDate();
//获取当前时间
Date nowDate = new Date();
if (nowDate.after(endDate)){//秒杀已结束
miaoshaStatus = 2;
remainSeconds=-1;
}else if (nowDate.before(startDate)){//秒杀倒计时
remainSeconds = (int) ((startDate.getTime()- nowDate.getTime())/1000);
}else {//秒杀中
miaoshaStatus = 1;
}
//秒杀结束倒计时
betweenSeconds = (int) ((endDate.getTime()-startDate.getTime())/1000);
GoodsDetailVo goodsDetailVo = new GoodsDetailVo();
goodsDetailVo.setUser(user);
goodsDetailVo.setGoodsVo(goods);
goodsDetailVo.setRemainSeconds(remainSeconds);
goodsDetailVo.setMiaoshaStatus(miaoshaStatus);
goodsDetailVo.setBetweenSeconds(betweenSeconds);
return ResBean.success(goodsDetailVo);
}
```
> 秒杀静态化
controller
```java
@RequestMapping(value = "/doSeckill",method = RequestMethod.POST)
@ResponseBody
public ResBean seckill(Model model, User user, long goodsId){
if (null == user){
return ResBean.error(ResBeanEnum.USER_NOT_LOGIN);
}
//根据商品id查询商品
GoodsVo goods = goodsService.querySeckillGoodsById(goodsId);
//判断库存
if (goods.getStockCount()<1){
model.addAttribute("errMsg", ResBeanEnum.EMPTY_STOCK.getMessage());
return ResBean.error(ResBeanEnum.EMPTY_STOCK);
}
// 从redis获取订单
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goods.getId());
if (seckillOrder != null){
model.addAttribute("errMsg",ResBeanEnum.REPEATE_ERROR.getMessage());
return ResBean.error(ResBeanEnum.REPEATE_ERROR);
}
//进行秒杀,创建订单
Order order = orderService.sekill(user,goods);
return ResBean.success(order);
}
```
> 订单详情页面静态化
订单详情返回对象
```java
/**
* 订单详情返回对象
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderDetailVo {
private Order order;
private GoodsVo goodsVo;
}
```
order_detail.htm
```html
订单详情
秒杀订单详情
| 商品名称 |
|
| 商品图片 |
![]() |
| 订单价格 |
|
| 下单时间 |
|
| 订单状态 |
|
|
| 收货人 |
XXX 18812341234 |
| 收货地址 |
北京市昌平区回龙观龙博一区 |
```
orederController
```java
/**
* 订单详情
* @return
*/
@RequestMapping("/detail")
@ResponseBody
public ResBean detail(User user,Long orderId){
if (user == null){
return ResBean.error(ResBeanEnum.USER_NOT_LOGIN);
}
OrderDetailVo detail = orderService.detail(orderId);
return ResBean.success(detail);
}
```
## 10. RabbitMQ
### 10.1 rabbitMQ安装
> 安装包下载
rabbitmq3.8.5
[https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.8.5](https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.8.5)
relang23
[https://github.com/rabbitmq/erlang-rpm/releases/tag/v23.3.4.6](https://github.com/rabbitmq/erlang-rpm/releases/tag/v23.3.4.6)
> 在CenOS安装
- 安装erlang
yum -y install erlang-23.3.4.6-1.el7.x86_64.rpm
安装成功

- 安装rabbitmq
yum -y install rabbitmq-server-3.8.5-1.el7.noarch.rpm
- 展示所有插件
rabbitmq-plugins list

- 安装可视化管理控制台
rabbitmq-plugins enable rabbitmq_management

- 启动rabbitmq服务
systemctl start rabbitmq-server.service
- 查看是否启动成功
systemctl status rabbitmq-server.service
- 访问rabbitmq
linux服务器地址+端口15672,用户名和密码都为guest

不允许远程访问

- 添加配置文件,设置远程登录访问
进入/etc/rabbitmq/目录,创建rabbitmq.conf文件,写上[{rabbit,[{loopback_users, []}]}].,保存重启rabbitmq服务systemctl restart rabbitmq-server.service

- 再次访问登录成功

### 10.2 SpringBoot整合rabbitmq
> 引入依赖
```xml
org.springframework.boot
spring-boot-starter-amqp
```
> 配置rabbitmq
```yaml
# rabbitmq配置
rabbitmq:
# 服务器
host: 192.168.159.200
# 用户名
username: guest
# 密码
password: guest
# 虚拟主机
vritual-host: /
# 端口
port: 5672
# 监听
listener:
simple:
# 消费者最小数量
concurrency: 10
# 消费者最大数量
max-concurrency: 10
# 限制消费者每次只处理一条消息,处理完在继续下一条消息
prefetch: 1
# 启动时是否默认启动容器,默认true
auto-startup: true
# 被拒绝时重新进行入队列
default-requeue-rejected: true
template:
retry:
# 发布重试
enabled: true
# 重试时间,默认1000ms
initial-interval: 1000ms
# 重试最大次数,默认为3
max-attempts: 3
# 重试最大间隔时间 默认10000ms
max-interval: 10000ms
# 重试的时间乘数,比如2.0,第一次等10秒,第二次等20s,第三次等40
multiplier: 1
```
> rabbitmq配置类
```java
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* rabbitmq配置类
*/
@Configuration
public class RabbitMQConfig {
@Bean
public Queue queue(){
//名称、是否持久化
return new Queue("queue",true);
}
}
```
> 消息发送者
```java
/**
* 消息发送者
*/
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(Object msg){
log.info("发送消息:"+msg);
rabbitTemplate.convertAndSend("queue",msg);
}
}
```
> 消息接收消费者
```java
/**
* 消息消费者
*/
@Service
@Slf4j
public class MQReceiver {
@RabbitListener(queues = "queue")
public void receiver(Object msg){
log.info("接收消息:"+msg);
}
}
```
> 测试
```java
@RestController
@RequestMapping("/mq")
public class RabbitmqController {
@Autowired
private MQSender mqSender;
/**
* 测试发送rabbitmq消息
*/
@RequestMapping("/mq")
@ResponseBody
public void mq(){
mqSender.send("hello rabbitmq");
}
}
```
### 10.3 Fanout模式

> rabbitmq配置
```java
/**
* rabbitmq配置类
*/
@Configuration
public class RabbitMQConfig {
private static final String QUEUE01 = "fanout01";//队列1
private static final String QUEUE02 = "fanout02";//队列2
private static final String EXCHANGE = "fanoutExchange";//交换机
@Bean
public Queue queue01(){
return new Queue(QUEUE01);
}
@Bean
public Queue queue02(){
return new Queue(QUEUE02);
}
//创建交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange(EXCHANGE);
}
//交换机绑定队列1
@Bean
public Binding binding01(){
return BindingBuilder.bind(queue01()).to(fanoutExchange());
}
//交换机绑定队列2
@Bean
public Binding binding02(){
return BindingBuilder.bind(queue02()).to(fanoutExchange());
}
}
```
> 消息发送者
```java
/**
* 消息发送者
*/
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(Object msg){
log.info("发送消息:"+msg);
rabbitTemplate.convertAndSend("fanoutExchange","",msg);
}
}
```
> 消息消费者
```java
/**
* 消息消费者
*/
@Service
@Slf4j
public class MQReceiver {
@RabbitListener(queues = "fanout01")
public void receiver01(Object msg){
log.info("接收消息:"+msg);
}
@RabbitListener(queues = "fanout02")
public void receiver02(Object msg){
log.info("接收消息:"+msg);
}
}
```
### 10.4 Direct模式

> rabbitmq配置类
```java
/**
* rabbitmq配置类(direct模式)
*/
@Configuration
public class RabbitMQDirectConfig {
private static final String QUEUE01 = "direct01";
private static final String QUEUE02 = "direct02";
private static final String EXCHANGE = "directExchange";
private static final String ROUTINGKEY01 = "queue.red";
private static final String ROUTINGKEY02 = "queue.green";
//创建队列1
@Bean
public Queue queue01(){
return new Queue(QUEUE01);
}
//创建队列2
@Bean
public Queue queue02(){
return new Queue(QUEUE02);
}
//创建交换机
@Bean
public DirectExchange directExchange(){
return new DirectExchange(EXCHANGE);
}
//交换机绑定队列1
@Bean
public Binding binding01(){
return BindingBuilder.bind(queue01()).to(directExchange()).with(ROUTINGKEY01);
}
//交换机绑定队列2
@Bean
public Binding binding02(){
return BindingBuilder.bind(queue02()).to(directExchange()).with(ROUTINGKEY02);
}
}
```
> 消息发送者
```java
/**
* 消息发送者
*/
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
//direct模式
public void sendDirect01(Object msg){
log.info("发送red消息:"+msg);
rabbitTemplate.convertAndSend("directExchange","queue.red",msg);
}
public void sendDirect02(Object msg){
log.info("发送green消息:"+msg);
rabbitTemplate.convertAndSend("directExchange","queue.green",msg);
}
}
```
> 消息接收消费者
```java
/**
* 消息消费者
*/
@Service
@Slf4j
public class MQReceiver {
//direct模式
@RabbitListener(queues = "direct01")
public void receiverDirect01(Object msg){
log.info("接收消息:"+msg);
}
@RabbitListener(queues = "direct02")
public void receiverDirect02(Object msg){
log.info("接收消息:"+msg);
}
}
```
### 10.5 Topic模式

> rabbitmq配置类
```java
/**
* rabbitmq配置类(topic模式)
*/
@Configuration
public class RabbitMQTopicConfig {
private static final String QUEUE01 = "topic01";
private static final String QUEUE02 = "topic02";
private static final String EXCHANGE = "topicExchange";
private static final String ROUTINGKEY01 = "#.queue.#";
private static final String ROUTINGKEY02 = "*.queue.#";
//创建队列1
@Bean
public Queue queue01(){
return new Queue(QUEUE01);
}
//创建队列2
@Bean
public Queue queue02(){
return new Queue(QUEUE02);
}
//创建交换机
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(EXCHANGE);
}
//交换机绑定队列1
@Bean
public Binding binding01(){
return BindingBuilder.bind(queue01()).to(topicExchange()).with(ROUTINGKEY01);
}
//交换机绑定队列2
@Bean
public Binding binding02(){
return BindingBuilder.bind(queue02()).to(topicExchange()).with(ROUTINGKEY02);
}
}
```
> 消息发送者
```java
/**
* 消息发送者
*/
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
//topic模式
public void sendTopic01(Object msg){
log.info("发送消息:"+msg);
rabbitTemplate.convertAndSend("topicExchange","queue.red.message",msg);
}
public void sendTopic02(Object msg){
log.info("发送消息:"+msg);
rabbitTemplate.convertAndSend("topicExchange","message.queue.green.abc",msg);
}
}
```
> 消息接收消费者
```java
/**
* 消息消费者
*/
@Service
@Slf4j
public class MQReceiver {
//topIC模式
@RabbitListener(queues = "topic01")
public void receiverTopic01(Object msg){
log.info("接收消息:"+msg);
}
@RabbitListener(queues = "topic02")
public void receiverTopic02(Object msg){
log.info("接收消息:"+msg);
}
}
```
## 11. 服务优化
### 11.1 redis预减库存
> 系统初始化时,将秒杀商品库存加载到redis中
```java
注:controller类实现InitializingBean接口
/**
* 系统初始化,把商品库存数量加载到redis中
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
List list = goodsService.querySeckillGoods();
if (CollectionUtils.isEmpty(list)){
return;
}
list.forEach(goodsVo -> {
emptyStockMap.put(goodsVo.getId(),false);
redisTemplate.opsForValue().set("seckillGoods:"+goodsVo.getId(),goodsVo.getStockCount());
});
}
```
> 秒杀时redis的商品库存减1,判断redis中的商品库存是否小于0
```java
//redis中的商品库存减1
Long decrement = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId);
//判断当前商品库存是否小于0
if (decrement<0){
emptyStockMap.put(goodsId,true);
//使库存为0
redisTemplate.opsForValue().increment("seckillGoods:" + goodsId);
return ResBean.error(ResBeanEnum.EMPTY_STOCK);
}
```
> 内存标记,减少redis的访问
```java
//1、创建map存放商品是否有库存
private final Map emptyStockMap = new HashMap<>();
//2、初始化
emptyStockMap.put(goodsVo.getId(),false);
//3、判断该商品是否有库存,通过内存标记,减少redis的访问
if (emptyStockMap.get(goodsId)){
return ResBean.error(ResBeanEnum.EMPTY_STOCK);
}
//4、当商品库存小于0时,设置该商品为true,表示无库存
emptyStockMap.put(goodsId,true);
```
### 11.2 RabbitMQ秒杀操作
> 秒杀消息类
```java
/**
* 秒杀消息
*/
@Data
@AllArgsConstructor@NoArgsConstructor
public class SeckillMessage {
private User user;
private Long goodsId;
}
```
> 配置rabbitmq
```java
/**
* rabbitmq配置类
*/
@Configuration
public class RabbitmqConfig {
private static final String QUEUE = "seckillQueue";
private static final String EXCHANGE = "seckillExchange";
private static final String ROUTINGKEY = "seckill.#";
//创建队列
@Bean
public Queue queue(){
return new Queue(QUEUE);
}
//创建交换机
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(EXCHANGE);
}
//队列绑定交换机
@Bean
public Binding binding(){
return BindingBuilder.bind(queue()).to(topicExchange()).with(ROUTINGKEY);
}
}
```
> 秒杀消息发送者
```java
/**
* 消息发送者
*/
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
//发送商品秒杀消息
public void sendSeckillMessage(String msg){
log.info("发送秒杀商品消息:"+msg);
rabbitTemplate.convertAndSend("seckillExchange","seckill.doSeckill",msg);
}
}
```
> 秒杀消息接收者
```java
/**
* 消息消费者
*/
@Service
@Slf4j
public class MQReceiver {
@Autowired
private GoodsService goodsService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private OrderService orderService;
@RabbitListener(queues = "seckillQueue")
public void receiverSeckillMessage(String msg){
log.info("接收秒杀消息:"+ msg);
SeckillMessage seckillMessage = JSON.parseObject(msg, SeckillMessage.class);
User user = seckillMessage.getUser();
Long goodsId = seckillMessage.getGoodsId();
GoodsVo goods = goodsService.querySeckillGoodsById(goodsId);
System.out.println(goodsId+":"+goods);
//判断库存
if (goods.getStockCount()<1){
return;
}
// 从redis获取订单,判断是否有重复用户抢购
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
if (seckillOrder != null){
return;
}
//进行秒杀,创建订单
Order order = orderService.sekill(user,goods);
}
}
```
> 秒杀controller
```java
/**
* 商品秒杀
*/
@RequestMapping(value = "/doSeckill",method = RequestMethod.POST)
@ResponseBody
public ResBean seckill(Model model, User user, long goodsId){
if (null == user){
return ResBean.error(ResBeanEnum.USER_NOT_LOGIN);
}
//判断该商品是否有库存,通过内存标记,减少redis的访问
if (emptyStockMap.get(goodsId)){
return ResBean.error(ResBeanEnum.EMPTY_STOCK);
}
//redis中的商品库存减1
Long decrement = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId);
//判断当前商品库存是否小于0
if (decrement<0){
emptyStockMap.put(goodsId,true);
//使库存为0
redisTemplate.opsForValue().increment("seckillGoods:" + goodsId);
return ResBean.error(ResBeanEnum.EMPTY_STOCK);
}
// 从redis获取订单,判断是否有重复用户抢购
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
if (seckillOrder != null){
return ResBean.error(ResBeanEnum.REPEATE_ERROR);
}
//rabbitmq发送消息,异步处理下订单
SeckillMessage seckillMessage = new SeckillMessage(user,goodsId);
//将Object转为JSONString
String msg = JSON.toJSONString(seckillMessage);
mqSender.sendSeckillMessage(msg);
return ResBean.success(0);
}
```
### 11.3 客户端轮询秒杀结果
> 前端goods_detail.htm页面处理
```javascript
function doSeckill() {
$.ajax({
url:'/seckill/doSeckill',
type:'POST',
data:{
goodsId:$("#goodsId").val()
},
success:function (data) {
if (data.code==200){
// window.location.href="/order_detail.htm?orderId="+data.obj.id;
getResult($("#goodsId").val());
}else{
layer.msg(data.message);
}
},
error:function () {
layer.msg("客户端请求错误");
}
})
}
function getResult(goodsId) {
g_showLoading();
$.ajax({
url:'/seckill/result',
type:'GET',
data:{
goodsId:goodsId
},
success:function (data) {
if (data.code==200){
var result = data.obj;
if (result<0){
layer.msg("秒杀失败!");
}else if (result==0){
//轮询判断是否秒杀成功
setTimeout(function () {
getResult(goodsId);
},50);
}else{
layer.confirm("秒杀成功!是否查看订单?",{btn:["确定","取消"]},
function () {
window.location.href="/order_detail.htm?orderId="+result;
},
function () {
layer.close();
})
}
}
},
error:function () {
layer.msg("客户端请求异常")
}
})
}
```
> 获取秒杀结果
```java
/**
* 获取秒杀结果
* orderId:成功,-1:失败,0:排队中
* @param user
* @param goodsId
* @return
*/
@Override
public Long getResult(User user, Long goodsId) {
// 查询订单
SeckillOrder seckillOrder = seckillOrderDao.selectOne(new QueryWrapper()
.eq("user_id", user.getId()).eq("goods_id", goodsId));
if (null != seckillOrder){
return seckillOrder.getOrderId();
}else if (redisTemplate.hasKey("isStockEmpty:"+goodsId)){//判断是否存在isStockEmpty
return -1L;
}else {
return 0L;
}
}
```
> 返回秒杀结果
```java
/**
* 获取秒杀结果
* orderId:成功,-1:失败,0:排队中
* @param user
* @param goodsId
* @return
*/
@RequestMapping(value = "/result",method = RequestMethod.GET)
@ResponseBody
public ResBean getResult(User user,Long goodsId){
if (user == null){
return ResBean.error(ResBeanEnum.USER_NOT_LOGIN);
}
Long orderId = seckillOrderService.getResult(user,goodsId);
return ResBean.success(orderId);
}
```
### 11.4 redis实现分布式锁
> test
```java
@SpringBootTest
class SeckillSystemApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private DefaultRedisScript redisScript;
@Test
void contextLoads() {
ValueOperations valueOperations = redisTemplate.opsForValue();
String value = UUIDUtil.uuid();
//占位,如果key不存在才可以设置成功
//给锁设置一个超时时间,防止应用运行过程中抛出异常导致无法释放锁
Boolean isLock = valueOperations.setIfAbsent("k1", value, 10, TimeUnit.SECONDS);
//如果占位成功进行正常操作
if (isLock){
try {
valueOperations.set("name","HSB");
String name = (String)valueOperations.get("name");
System.out.println(name);
} finally {
//操作结束删除锁,执行lua脚本删除锁,保证原子性
//比较当前的value是否为之前设定的value,如果是则进行删除
Boolean result = (Boolean) redisTemplate.execute(redisScript, Collections.singletonList("k1"), value);
System.out.println(result);
}
}else {
System.out.println("有线程正在执行中,请稍后再试");
}
}
}
```
> Lua脚本
```lua
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
```
> redis配置类,配置脚本
```java
@Bean
public DefaultRedisScript defaultRedisScript(){
DefaultRedisScript redisScript = new DefaultRedisScript<>();
//lock.lua脚本的位置和application.yml同级目录
redisScript.setLocation(new ClassPathResource("lock.lua"));
//设置返回结果类型
redisScript.setResultType(Boolean.class);
return redisScript;
}
```
### 11.5 优化redis减库存
> Lua脚本
```lua
// 判断redis是否存在商品的key值,如果存在取出该商品的库存
// 如果库存大于0,则进行减库存操作,返回预减后的库存
if (redis.call("exist",KEYS[1])==1) then
local stock = tonumber(redis.call("get",KEYS[1]);
if(stock>0) then
redis.call("incryby",KEYS[1],-1);
return stock;
end;
return 0;
end;
```
> redis配置类,配置脚本
```java
@Bean
public DefaultRedisScript defaultRedisScript(){
DefaultRedisScript redisScript = new DefaultRedisScript<>();
//lock.lua脚本的位置和application.yml同级目录
redisScript.setLocation(new ClassPathResource("stock.lua"));
//设置返回结果类型
redisScript.setResultType(Long.class);
return redisScript;
}
```
> 执行脚本
```java
Long stock = (Long) redisTemplate.execute(redisScript, Collections.singletonList("seckillGoods:" + goodsId), Collections.EMPTY_LIST);
```
## 12. 接口优化
### 12.1 秒杀接口地址隐藏
> 前端商品详情页面
修改秒杀方法,先获取秒杀路径在进行秒杀
```html
```
前端获取秒杀路径方法
```javascript
function getSeckillPath() {
var goodsId = $("#goodsId").val();
$.ajax({
url:'/seckill/getPath',
type:'GET',
data:{
goodsId:goodsId
},
success:function (data) {
if (data.code == 200){
var path = data.obj;
doSeckill(path);
}else {
layer.msg(data.message)
}
}
})
}
```
> 前端秒杀方法
```javascript
function doSeckill(path) {
$.ajax({
url:'/seckill/'+path+'/doSeckill',
type:'POST',
data:{
goodsId:$("#goodsId").val()
},
success:function (data) {
if (data.code==200){
// window.location.href="/order_detail.htm?orderId="+data.obj.id;
getResult($("#goodsId").val());
}else{
layer.msg(data.message);
}
},
error:function () {
layer.msg("客户端请求错误");
}
})
}
```
> controller
```java
/**
* 获取秒杀地址
* @param user
* @param goodsId
* @return
*/
@RequestMapping(value = "/getPath",method = RequestMethod.GET)
@ResponseBody
public ResBean getPath(User user,Long goodsId){
if (user == null){
return ResBean.error(ResBeanEnum.USER_NOT_LOGIN);
}
String str = orderService.getPath(user,goodsId);
return ResBean.success(str);
}
```
> service
```java
/**
* 获取秒杀地址
* @param user
* @param goodsId
* @return
*/
@Override
public String getPath(User user, Long goodsId) {
// 设置随机生成路径
String s = MD5Util.md5(UUIDUtil.uuid() + "12345");
// 存储到redis
redisTemplate.opsForValue().set("seckillPath:"+user.getId()+":"+goodsId,s,60,TimeUnit.SECONDS );
return s;
}
```
> 后端校验秒杀路径
```java
// 校验秒杀路径(controller)
Boolean check = orderService.checkPath(user,goodsId,path);
if (!check){
return ResBean.error(ResBeanEnum.REQUEST_ILLEGAL);
}
--------------------------------------------------------
/**
* 校验秒杀路径是否合法
* @param user
* @param goodsId
* @param path
* @return
*/
@Override
public Boolean checkPath(User user, long goodsId, String path) {
if (user == null || goodsId <0 || StringUtils.isEmpty(path)){
return false;
}
String str = (String) redisTemplate.opsForValue().get("seckillPath:"+user.getId()+":"+goodsId);
return path.equals(str);
}
```
### 12.2 实现验证码
> 生成验证码
引入依赖[https://gitee.com/lian_jianfeng/EasyCaptcha?_from=gitee_search](https://gitee.com/lian_jianfeng/EasyCaptcha?_from=gitee_search)
```xml
com.github.whvcse
easy-captcha
1.6.2
```
后端生成验证码
```java
/**
* 生成验证码
* @param user
* @param goodsId
* @param request
* @param response
* @throws IOException
*/
@RequestMapping(value = "/captcha",method = RequestMethod.GET)
public void captcha(User user,Long goodsId,HttpServletRequest request, HttpServletResponse response) throws IOException {
// 设置请求头为输出图片类型
response.setContentType("image/jpg");
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
// 算术类型
ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 32);
captcha.setLen(3); // 几位数运算,默认是两位
captcha.getArithmeticString(); // 获取运算的公式:3+2=?
captcha.text(); // 获取运算的结果:5
redisTemplate.opsForValue().set("captcha:"+user.getId()+":"+goodsId,captcha.text());
// 输出图片流
captcha.out(response.getOutputStream());
}
```
前端展示
```html
```
JS渲染
```javascript
// 刷新验证码
function refreshCaptcha() {
$("#captchaImg").attr("src","/seckill/captcha?goodsId="+$("#goodsId").val()+"&time="+new Date())
}
```
> 后台校验验证码
```javascript
/**
* 校验验证码
* @param user
* @param goodsId
* @param captcha
* @return
*/
@Override
public Boolean checkCaptcha(User user, Long goodsId, String captcha) {
if (user == null || goodsId < 0 || StringUtils.isEmpty(captcha)){
return false;
}
//从redis中获取验证码
String s = (String) redisTemplate.opsForValue().get("captcha:"+user.getId()+":"+goodsId);
return captcha.equals(s);
}
------------------------
// 校验验证码
Boolean check = orderService.checkCaptcha(user,goodsId,captcha);
if (!check){
return ResBean.error(ResBeanEnum.CAPTCHA_ERROR);
}
```
### 12.3 接口限流
> 计数器限流
```java
// 限制访问次数,5秒内访问5次
//获取URL
String uri = request.getRequestURI();
Integer count = (Integer) redisTemplate.opsForValue().get(uri+":"+user.getId());
if (count == null){
redisTemplate.opsForValue().set(uri+":"+user.getId(),1,5, TimeUnit.SECONDS);
}else if (count<5){
redisTemplate.opsForValue().increment(uri+":"+user.getId());
}else {
return ResBean.error(ResBeanEnum.ACCESS_LIMIT_REAHCED);
}
```
> 利用注解实现限流
@AccessLimit注解
```java
/**
* 接口限流注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
//时间间隔
int second();
// 最大次数
int maxCount();
//是否需要登录
boolean needLogin() default true;
}
```
> 将user设置到线程ThreadLocal中
```java
/**
* 将user设置到线程ThreadLocal中
*/
public class UserContent {
private static ThreadLocal userThreadLocal = new ThreadLocal<>();
//设置user到当前线程中
public static void setUser(User user){
userThreadLocal.set(user);
}
// 获取当前线程的user
public static User getUser(){
return userThreadLocal.get();
}
}
```
_限流拦截器_
```java
/**
* 限流拦截器
*/
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private RedisTemplate redisTemplate;
// 进入方法前处理,返回true放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断handler是否是要处理的方法
if (handler instanceof HandlerMethod) {
// 获取当前用户
User user = getUser(request, response);
//将user存到当前的线程中
UserContent.setUser(user);
// 处理的方法
HandlerMethod method = (HandlerMethod) handler;
// 获取方法上的AccessLimit注解
AccessLimit accessLimit = method.getMethodAnnotation(AccessLimit.class);
if (accessLimit == null) {
return true;
}
//获取注解的属性值
int second = accessLimit.second();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
//获取uri
String uri = request.getRequestURI();
if (needLogin) {
if (user == null){
render(response,ResBeanEnum.USER_NOT_LOGIN);
return false;
}
}
// 限制访问次数,5秒内访问5次
Integer count = (Integer) redisTemplate.opsForValue().get(uri+":"+user.getId());
if (count == null){
// 在redis设置当前用户的访问次数
redisTemplate.opsForValue().set(uri+":"+user.getId(),1,second, TimeUnit.SECONDS);
}else if (count