Java秒杀方案
1.课程介绍
1.1. 技术点介绍
前端:Thymeleaf、Bootstrap、JQuery
后端:SpringBoot、MyBatisPlus、Lombok
中间件:RabbitMQ(异步、流量削峰)、Redis(缓存)
1.2. 课程介绍
Java秒杀方案:项目搭建、分布式Session、秒杀功能、压力测试、页面优化、服务优化、接口安全
2.学习目标
通过本课程的学习,主要是学习到怎么应对大并发:怎么使用异步、怎么使用缓存?如何编写优雅的代码?
分布式会话:用户登录、共享Session
功能开发:商品列表、商品详情、秒杀、订单详情
系统压测:JMeter入门、自定义变量、正式压测
页面优化:缓存、静态化分离
服务优化:RabbitMQ消息队列、接口优化、分布式锁
安全优化:隐藏秒杀地址、验证码、接口限流
3.如何设计一个秒杀系统
秒杀,对我们来说,都不是一个陌生的东西。每年的双11,618以及时下流行的直播等等。秒杀然而,这对于我们系统而言是一个巨大的考验。
那么,如何才能更好地理解秒杀系统呢?我觉得作为一个程序员,你首先需要从高纬度出发,从整体上思考问题。在我看来,秒杀其实主要解决两个问题,一个是并发读,一个是并发写。并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。另外,我们还要针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。
其实,秒杀的整体架构可以概括为“稳、准、快”几个关键字。
所谓“稳”,就是整个系统架构要满足高可用,流量符合预期是肯定要稳定,就是超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。
然后就是“准”,就是秒杀10台iPhone,那就只能成交10台,多一台少一台都不行。一旦库存不对,那平台就要承担损失,所以“准”就是要求保证数据的一致性。
最后再看“快”,“快”其实很好理解,它就是说系统的性能要足够高,否则你怎么支撑这么大的流量呢?不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就完美了。
所以从技术角度上看“稳”、“准”、“快”,就对应了我们架构上的高可用、一致性和高性能的要求
- 高性能。秒杀涉及大量的并发读和并发写。因此支持高并发访问这点非常关键。对应的方案比如动静分离方案、热点的发现与隔离,请求的削峰与分层过滤、服务器的极致优化
- 一致性。秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知
- 高可用。现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,我们还要设计一个PlanB来兜底,以便在最坏情况发生时仍然能够从容应对。
4.项目搭建
4.1. 创建项目
完整目录
添加依赖
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--SpringBoot依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xxxx</groupId>
<artifactId>seckill</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>seckill</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--thymeleaf 组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--web 组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatisplus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--test 组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--md5 依赖-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<!--validation 组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--spring data redis 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--commons-pool2 对象池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
修改配置文件
application.yml
spring:
# thymeleaf配置
thymeleaf:
# 关闭缓存
cache: false
# 数据源配置
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
# 连接池名
pool-name: DateHikariCP
# 最小空闲连接数
minimum-idle: 5
# 空闲连接存货最大时间,默认600000(10分钟)
idle-timeout: 1800000
# 最大连接数,默认10
maximum-pool-size: 10
# 从连接池返回的连接自动提交
auto-commit: true
# 连接最大存活时间,0表示永久存活,默认1800000(30分钟)
max-lifetime: 1800000
# 连接超时时间,默认30000(30秒)
connection-timeout: 30000
# 测试连接是否可用的查询语句
connection-test-query: SELECT 1
# redis配置
redis:
# 服务器地址
host: 121.37.15.219
# 端口
port: 6379
# 数据库
database: 0
# 超时时间
timout: 10000ms
lettuce:
pool:
# 最大连接数,默认8
max-active: 8
# 最大连接阻塞等待时间,默认-1
max-wait: 10000ms
# 最大空闲连接,默认8
max-idle: 200
# 最小空闲连接,默认0
min-idle: 5
# 密码
password: kisen
# Mybatis-plus配置
mybatis-plus:
# 配置Mapper.xml映射文件
mapper-locations: classpath*:/mapper/*Mapper.xml
# 配置MyBatis数据返回类型别名(默认别名是类名)
type-aliases-package: com.xxx.seckill.pojo
# MyBatis SQL打印(方法接口所在的包,不是Mapper.xml所在的包)
logging:
level:
com.xxxx.seckill.mapper: debug
测试
DemoController.java
package com.xxxx.seckill.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 测试
*
* @ClassName: DemoController
* @Title: seckill
* @Package: com.xxxx.seckill.controller
* @Description:
* @Author: Kisen
* @Date: 2021/3/1 22:47
*/
@Controller
@RequestMapping("/demo")
public class DemoController {
/**
* 测试页面跳转
* @param model
* @return
*/
@RequestMapping("/hello")
public String hello(Model model){
model.addAttribute("name", "xxxx");
return "hello";
}
}
hello.html
<!doctype html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body>
<p th:text="'hello'+${name}"></p>
</body>
</html>
测试结果
添加公共结果返回对象
RespBeanEnum.java
package com.xxxx.seckill.vo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
/**
* @ClassName: RespBeanEnum
* @Title: seckill
* @Package: com.xxxx.seckill.vo
* @Description: 公共返回对象枚举
* @Author: Kisen
* @Date: 2021/3/4 22:29
*/
@Getter
@ToString
@AllArgsConstructor
public enum RespBeanEnum {
// 通用
SUCCES(200, "SUCCES"),
ERROR(500, "服务端异常"),
// 登录模块
LOGIN_ERROR(500210, "用户名或密码不正确"),
MOBILE_ERROR(500211, "手机号码格式不正确"),
BIND_ERROR(500212, "参数校验异常");
private final Integer code;
private final String message;
}
RespBean.java
package com.xxxx.seckill.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @ClassName: RespBean
* @Title: seckill
* @Package: com.xxxx.seckill.vo
* @Description: 公共返回对象
* @Author: Kisen
* @Date: 2021/3/4 22:29
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {
private long code;
private String message;
private Object obj;
/**
* 成功返回结果
* @return
*/
public static RespBean success() {
return new RespBean(RespBeanEnum.SUCCES.getCode(),RespBeanEnum.SUCCES.getMessage(),null);
}
public static RespBean success(Object obj) {
return new RespBean(RespBeanEnum.SUCCES.getCode(),RespBeanEnum.SUCCES.getMessage(),obj);
}
/**
* 失败返回结果
* @param respBeanEnum
* @return
*/
public static RespBean error(RespBeanEnum respBeanEnum) {
return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),null);
}
public static RespBean error(RespBeanEnum respBeanEnum, Object obj) {
return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),obj);
}
}
5.分布式会话
5.1. 实现登录功能
5.1.1. 两次MD5加密
用户端:PASS=MD5(明文+固定salt)
服务端:PASS=MD5(用户输入+随机salt)
用户端MD5加密是为了防止用户密码在网络中明文传输,服务端MD5加密是为了提高密码安全性,双重保险。
引入pom.xml
<!--md5 依赖-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
编写MD5工具类
MD5Util.java
package com.xxxx.seckill.utils;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.stereotype.Component;
/**
* MD5工具类
*
* @ClassName: MD5Util
* @Title: seckill
* @Package: com.xxxx.seckill.utils
* @Description:
* @Author: Kisen
* @Date: 2021/3/1 23:24
*/
@Component
public class MD5Util {
private static final String salt = "1a2b3c4d";
public static String md5(String src){
return DigestUtils.md5Hex(src);
}
public static String inputPassToFormPass(String inputPass){
String str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
return md5(str);
}
public static String formPassToDBPass(String formPass, String salt){
String str = "" + salt.charAt(0) + salt.charAt(2) + formPass + salt.charAt(5) + salt.charAt(4);
return md5(str);
}
public static String inputPassToDBPass(String inputPass, String salt){
String formPass = inputPassToFormPass(inputPass);
String dbPass = formPassToDBPass(formPass, salt);
return dbPass;
}
public static void main(String[] args) {
// ce21b747de5af71ab5c2e20ff0a60eea
System.out.println(inputPassToFormPass("123456"));
System.out.println(formPassToDBPass("d3b1294a61a07da9b49b6e22b2cbd7f9", "1a2b3c4d"));
System.out.println(inputPassToDBPass("123456", "1a2b3c4d"));
}
}
5.1.2. 登录功能实现
逆向工程
首先需要通过逆向工程t_user表生产对应POJO、Mapper、Service、ServiceImpl、Controller等类,项目中使用了MyBatisPlus,所以逆向工程也是用了MyBatisPlus提供的AutoGenerator,代码如下。具体可去官网查看
CodeGenerator.java
package com.xxxx.generator;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* @ClassName: CodeGenerator
* @Title: generator
* @Package: com.xxxx.generator
* @Description:
* @Author: Kisen
* @Date: 2021/3/4 21:52
*/
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
//作者
gc.setAuthor("kisen");
//打开输出目录
gc.setOpen(false);
//xml开启BaseResultMap
gc.setBaseResultMap(true);
//xml开启BaseColumnList
gc.setBaseColumnList(true);
//日期格式,采用Date
gc.setDateType(DateType.ONLY_DATE);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.xxxx.seckill")
.setEntity("pojo")
.setMapper("mapper")
.setService("service")
.setServiceImpl("service.impl")
.setController("controller");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/" + "/" + tableInfo.getEntityName() + "Mapper"
+ StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig()
.setEntity("templates/entity2.java")
.setMapper("templates/mapper2.java")
.setService("templates/service2.java")
.setServiceImpl("templates/serviceImpl2.java")
.setController("templates/controller2.java");
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
//数据库表映射到实体的命名策略
strategy.setNaming(NamingStrategy.underline_to_camel);
//数据库表字段映射到实体的命名策略
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
//lombok模型
strategy.setEntityLombokModel(true);
//生成@RestController控制器
// strategy.setRestControllerStyle(true);
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
//表前缀
strategy.setTablePrefix("t_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
ValidatorUtil.java
package com.xxxx.seckill.utils;
import org.apache.commons.lang3.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @ClassName: ValidatorUtil
* @Title: seckill
* @Package: com.xxxx.seckill.utils
* @Description: 手机号码校验
* @Author: Kisen
* @Date: 2021/3/6 15:31
*/
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();
}
}
LoginController.java
package com.xxxx.seckill.controller;
import com.xxxx.seckill.service.IUserService;
import com.xxxx.seckill.vo.LoginVo;
import com.xxxx.seckill.vo.RespBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
/**
* @ClassName: LoginController
* @Title: seckill
* @Package: com.xxxx.seckill.controller
* @Description:
* @Author: Kisen
* @Date: 2021/3/4 22:17
*/
@Controller
@RequestMapping("/login")
@Slf4j
public class LoginController {
@Autowired
private IUserService userService;
/**
* 跳转登录页面
* @return
*/
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
/**
* 登录功能
* @param loginVo
* @return
*/
@RequestMapping("/doLogin")
@ResponseBody
public RespBean doLogin(@Valid LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
return userService.doLogin(loginVo, request, response);
}
}
IUserService.java
package com.xxxx.seckill.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xxxx.seckill.pojo.User;
import com.xxxx.seckill.vo.LoginVo;
import com.xxxx.seckill.vo.RespBean;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>
* 服务类
* </p>
*
* @author kisen
* @since 2021-03-04
*/
public interface IUserService extends IService<User> {
/**
* 登录
* @param loginVo
* @param request
* @param response
* @return
*/
RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response);
/**
* 根据cookie获取用户
* @param userTicket
* @return
*/
User getUserByCookie(String userTicket, HttpServletRequest request, HttpServletResponse response);
}
UserServiceImpl.java
package com.xxxx.seckill.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xxxx.seckill.exception.GlobalException;
import com.xxxx.seckill.mapper.UserMapper;
import com.xxxx.seckill.pojo.User;
import com.xxxx.seckill.service.IUserService;
import com.xxxx.seckill.utils.CookieUtil;
import com.xxxx.seckill.utils.MD5Util;
import com.xxxx.seckill.utils.UUIDUtil;
import com.xxxx.seckill.vo.LoginVo;
import com.xxxx.seckill.vo.RespBean;
import com.xxxx.seckill.vo.RespBeanEnum;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>
* 服务实现类
* </p>
*
* @author kisen
* @since 2021-03-04
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 登录
* @param loginVo
* @param request
* @param response
* @return
*/
@Override
public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
/*// 参数校验
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
return RespBean.error(RespBeanEnum.LOGIN_ERROR);
}
if (!ValidatorUtil.isMobile(mobile)) {
return RespBean.error(RespBeanEnum.MOBILE_ERROR);
}*/
// 根据手机号获取用户
User user = userMapper.selectById(mobile);
if (null == user) {
throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
}
// 判断密码是否正确
if (!MD5Util.formPassToDBPass(password, user.getSalt()).equals(user.getPassword())) {
throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
}
// 生成cookie
String ticket = UUIDUtil.uuid();
// 将用户信息存入redis中
redisTemplate.opsForValue().set("user:" + ticket, user);
// request.getSession().setAttribute(ticket, user);
CookieUtil.setCookie(request, response, "userTicket", ticket);
return RespBean.success();
}
/**
* 根据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;
}
}
login.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
<!-- jquery -->
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- jquery-validator -->
<script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script>
<script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script>
<!-- layer -->
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<!-- md5.js -->
<script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
<!-- common.js -->
<script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<form name="loginForm" id="loginForm" method="post" style="width:50%; margin:0 auto">
<h2 style="text-align:center; margin-bottom: 20px">用户登录</h2>
<div class="form-group">
<div class="row">
<label class="form-label col-md-4">请输入手机号码</label>
<div class="col-md-5">
<input id="mobile" name="mobile" class="form-control" type="text" placeholder="手机号码" required="true"
minlength="11" maxlength="11"/>
</div>
<div class="col-md-1">
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<label class="form-label col-md-4">请输入密码</label>
<div class="col-md-5">
<input id="password" name="password" class="form-control" type="password" placeholder="密码"
required="true" minlength="6" maxlength="16"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-5">
<button class="btn btn-primary btn-block" type="reset" onclick="reset()">重置</button>
</div>
<div class="col-md-5">
<button class="btn btn-primary btn-block" type="submit" onclick="login()">登录</button>
</div>
</div>
</form>
</body>
<script>
function login() {
$("#loginForm").validate({
submitHandler: function (form) {
doLogin();
}
});
}
function doLogin() {
g_showLoading();
var inputPass = $("#password").val();
var salt = g_passsword_salt;
var str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
var password = md5(str);
$.ajax({
url: "/login/doLogin",
type: "POST",
data: {
mobile: $("#mobile").val(),
password: password
},
success: function (data) {
layer.closeAll();
if (data.code == 200) {
layer.msg("成功");
window.location.href="/goods/toList";
} else {
layer.msg(data.message);
}
},
error: function () {
layer.closeAll();
}
});
}
</script>
</html>
测试
手机号码格式不正确
手机号码或密码不正确
正确登录
5.2. 参数校验
每个类都写大量的健壮性判断过于麻烦,我们可以使用validation
简化我们的代码
添加依赖
pom.xml
<!--validation 组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
自定义手机号码验证规则
IsMobileValidator.java
package com.xxxx.seckill.vo;
import com.xxxx.seckill.utils.ValidatorUtil;
import com.xxxx.seckill.validator.IsMobile;
import org.apache.commons.lang3.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* @ClassName: IsMobileValidator
* @Title: seckill
* @Package: com.xxxx.seckill.vo
* @Description: 手机号码校验规则
* @Author: Kisen
* @Date: 2021/3/6 16:48
*/
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
private boolean required = false;
@Override
public void initialize(IsMobile constraintAnnotation) {
required = constraintAnnotation.required();
}
@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);
}
}
}
}
自定义注解
IsMobile.java
package com.xxxx.seckill.validator;
import com.xxxx.seckill.vo.IsMobileValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* @ClassName: IsMobile
* @Title: seckill
* @Package: com.xxxx.seckill.validator
* @Description: 验证手机号
* @Author: Kisen
* @Date: 2021/3/6 16:46
*/
@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 required() default true;
String message() default "手机号码格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
修改LoginVo
LoginVo.java
package com.xxxx.seckill.vo;
import com.xxxx.seckill.validator.IsMobile;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;
/**
* @ClassName: LoginVo
* @Title: seckill
* @Package: com.xxxx.seckill.vo
* @Description: 登录参数
* @Author: Kisen
* @Date: 2021/3/6 15:14
*/
@Data
public class LoginVo {
@NotNull
@IsMobile
private String mobile;
@NotNull
@Length(min = 32)
private String password;
}
其他修改
LoginController.java
入参添加@Valid
/**
* 登录功能
* @param loginVo
* @return
*/
@RequestMapping("/doLogin")
@ResponseBody
public RespBean doLogin(@Valid LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
return userService.doLogin(loginVo, request, response);
}
UserServiceImpl.java
注释掉之前的健壮性判断即可
/**
* 登录
* @param loginVo
* @param request
* @param response
* @return
*/
@Override
public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
/*// 参数校验
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
return RespBean.error(RespBeanEnum.LOGIN_ERROR);
}
if (!ValidatorUtil.isMobile(mobile)) {
return RespBean.error(RespBeanEnum.MOBILE_ERROR);
}*/
// 根据手机号获取用户
User user = userMapper.selectById(mobile);
if (null == user) {
return RespBean.error(RespBeanEnum.LOGIN_ERROR);
}
// 判断密码是否正确
if (!MD5Util.formPassToDBPass(password, user.getSalt()).equals(user.getPassword())) {
return RespBean.error(RespBeanEnum.LOGIN_ERROR);
}
return RespBean.success();
}
测试
5.3. 异常处理
系统中异常包括:编译时异常和运行时异常RuntimeException
,前者通过捕获异常从而获得异常信息,后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。在开发中,不管是dao层、service层还是controller层,都有可能抛出异常,在SpringMVC中,能将所有类型的异常处理从各处理过程解耦处来,既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护。SpringBoot全局异常处理方式主要两种:
使用@ControllerAdvice
和@ExceptionHandler
注解。
使用ErrorController类
来实现
区别:
@ControllerAdvice
方式只能处理控制器抛出的异常。此时请求已经进入控制器中。
ErrorController类
方式可以处理所有的异常,包括未进入控制器的错误,比如404,401等错误
如果应用中两者共同存在,则@ControllerAdvice
方式处理控制器抛出的异常,ErrorController类
方式处理未进入控制器的异常。
@ControllerAdvice
方式可以定义多个拦截方式,拦截不同的异常类,并且可以获取抛出的异常信息,自由度更大。
GlobalException.java
package com.xxxx.seckill.exception;
import com.xxxx.seckill.vo.RespBeanEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @ClassName: GlobalException
* @Title: seckill
* @Package: com.xxxx.seckill.exception
* @Description: 全局异常
* @Author: Kisen
* @Date: 2021/3/7 12:59
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException {
private RespBeanEnum respBeanEnum;
}
GlobalExceptionHandler.java
package com.xxxx.seckill.exception;
import com.xxxx.seckill.vo.RespBean;
import com.xxxx.seckill.vo.RespBeanEnum;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @ClassName: GlobalExceptionHandler
* @Title: seckill
* @Package: com.xxxx.seckill.exception
* @Description: 全局异常处理
* @Author: Kisen
* @Date: 2021/3/7 13:01
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public RespBean ExceptionHandler(Exception e) {
if (e instanceof GlobalException) {
GlobalException ex = (GlobalException) e;
return RespBean.error(ex.getRespBeanEnum());
} else if (e instanceof BindException) {
BindException ex = (BindException) e;
RespBean respBean = RespBean.error(RespBeanEnum.BIND_ERROR);
respBean.setMessage("参数校验异常:" + ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return respBean;
}
return RespBean.error(RespBeanEnum.ERROR);
}
}
修改之前代码
直接返回RespBean改为直接抛GlobalException
异常
/**
* 登录
* @param loginVo
* @param request
* @param response
* @return
*/
@Override
public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
/*// 参数校验
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
return RespBean.error(RespBeanEnum.LOGIN_ERROR);
}
if (!ValidatorUtil.isMobile(mobile)) {
return RespBean.error(RespBeanEnum.MOBILE_ERROR);
}*/
// 根据手机号获取用户
User user = userMapper.selectById(mobile);
if (null == user) {
throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
}
// 判断密码是否正确
if (!MD5Util.formPassToDBPass(password, user.getSalt()).equals(user.getPassword())) {
throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
}
return RespBean.success();
}
测试
6.分布式Session
6.1. 完善登录功能
使用cookie+session记录用户信息
准备工具类
CookieUtil.java
package com.xxxx.seckill.utils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
* @ClassName: CookieUtil
* @Title: seckill
* @Package: com.xxxx.seckill.utils
* @Description: Cookie工具类
* @Author: Kisen
* @Date: 2021/3/7 13:48
*/
public 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
* @param isDecoder
* @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
* @param encodeString
* @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的值 不设置生效时间默认浏览器关闭即生效,也不编码
* @param request
* @param response
* @param cookieName
* @param cookieValue
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue) {
setCookie(request, response, cookieName, cookieValue, -1);
}
/**
* 设置Cookie的值 在指定时间内生效,但不编码
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
}
/**
* 设置Cookie的值 不设置生效时间,但编码
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param isEncode
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, boolean isEncode) {
doSetCookie(request, response, cookieName, cookieValue, -1, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效,编码参数
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage
* @param isEncode
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效,编码参数(指定编码)
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage
* @param encodeString
*/
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域名
* @param request
* @param response
* @param cookieName
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName) {
doSetCookie(request, response, cookieName, "", -1, false);
}
/**
* 设置Cookie的值,并使其在指定时间内生效
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage
* @param isEncode
*/
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 request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage
* @param encodeString
*/
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的域名
* @param request
* @return
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
// 通过request对象获取访问的url地址
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
// 将url地址转换为小写
serverName = serverName.toLowerCase();
// 如果url地址是以http://开头 将http://截取
if (serverName.startsWith("http:/")) {
serverName = serverName.substring(7);
}
int end = serverName.length();
// 判断url地址是否包含"/"
if (serverName.contains("/")) {
// 得到第一个"/"出现的位置
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[] array = domainName.split("\\:");
domainName = array[0];
}
return domainName;
}
}
UUIDUtil.java
package com.xxxx.seckill.utils;
import java.util.UUID;
/**
* @ClassName: UUIDUtil
* @Title: seckill
* @Package: com.xxxx.seckill.utils
* @Description: UUID工具类
* @Author: Kisen
* @Date: 2021/3/7 13:31
*/
public class UUIDUtil {
public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}
UserServiceImpl.java
/**
* 登录
* @param loginVo
* @param request
* @param response
* @return
*/
@Override
public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
/*// 参数校验
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
return RespBean.error(RespBeanEnum.LOGIN_ERROR);
}
if (!ValidatorUtil.isMobile(mobile)) {
return RespBean.error(RespBeanEnum.MOBILE_ERROR);
}*/
// 根据手机号获取用户
User user = userMapper.selectById(mobile);
if (null == user) {
throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
}
// 判断密码是否正确
if (!MD5Util.formPassToDBPass(password, user.getSalt()).equals(user.getPassword())) {
throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
}
// 生成cookie
String ticket = UUIDUtil.uuid();
request.getSession().setAttribute(ticket, user);
CookieUtil.setCookie(request, response, "userTicket", ticket);
return RespBean.success();
}
LoginController.java
/**
* 登录功能
* @param loginVo
* @return
*/
@RequestMapping("/doLogin")
@ResponseBody
public RespBean doLogin(@Valid LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
return userService.doLogin(loginVo, request, response);
}
GoodsController.java
package com.xxxx.seckill.controller;
import com.xxxx.seckill.pojo.User;
import com.xxxx.seckill.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @ClassName: GoodsController
* @Title: seckill
* @Package: com.xxxx.seckill.controller
* @Description: 商品
* @Author: Kisen
* @Date: 2021/3/7 14:47
*/
@Controller
@RequestMapping("/goods")
@Slf4j
public class GoodsController {
@Autowired
private IUserService userService;
@RequestMapping("/toList")
public String toList(Model model, User user) {
if (StringUtils.isEmpty(ticket)) {
return "login";
}
User user = (User) session.getAttribute(ticket);
if (null == user) {
return "login";
}
model.addAttribute("user", user);
return "goodsList";
}
}
login.html
$.ajax({
url: "/login/doLogin",
type: "POST",
data: {
mobile: $("#mobile").val(),
password: password
},
success: function (data) {
layer.closeAll();
if (data.code == 200) {
layer.msg("成功");
window.location.href="/goods/toList";
} else {
layer.msg(data.message);
}
},
error: function () {
layer.closeAll();
}
});
goodsList.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>商品列表</title>
</head>
<body>
<p th:text="'Hello:'+${user.nickname}"></p>
</body>
</html>
测试
7.分布式Session问题
之前的代码在我们之后一台应用系统,所有操作都在一台Tomcat上,没有什么问题。当我们部署多台系统,配合Nginx的时候会出现用户登录的问题。
原因
由于Nginx使用默认负载均衡策略(轮询),请求将会按照时间顺序逐一分发到后端应用上。
也就是说刚开始我们在Tomcat1登录之后,用户信息放在Tomcat1的Session里。过来一会,请求又被Nginx分发到了Tomcat2上,这时Tomcat2上Session里还没有用户信息,于是又要登录。
解决方案:
Session复制
优点:无需修改代码,只需要修改Tomcat配置
缺点:
Session同步传输占用内网带宽
多台Tomcat同步性能指数级下降
Session占用内存,无法有效水平扩展
前端存储
优点:不占用服务端内存
缺点:
存在安全风险
数据大小受cookie限制
占用外网带宽
Session粘滞
优点:
无需修改代码
服务端可以水平扩展
缺点:
增加新机器,会重新Hash,导致重新登录
应用重启,需要重新登录
后端集中存储
优点:
安全
容易水平扩展
缺点:
增加复杂度
需要修改代码
8.Redis安装
下载地址
http://redis.io/
将下载好的安装包上传至服务器
解压
tar zxvf redis-5.0.5.tar.gz
安装依赖
yum -y install gcc-c++ autoconf automake
预编译
# 切换至解压目录
cd redis-5.0.5/
# 预编译
make
安装
# 创建安装目录
mkdir -p /usr/local/redis
# 安装
make PREFIX=/usr/local/redis/ install
修改配置文件
# 复制redis.conf至安装路径下
cp redis.conf /usr/local/redis/
# 修改配置文件
vim /usr/local/redis/redis.conf
修改内容如下
#bind 127.0.0.1
#关闭保护模式
protected-mode no
#后台启动
daemonize yes
#添加访问认证
requirepass root
启动redis
./redis-server redis.conf
9.Redis实现分布式Session
9.1. 方法一:使用SpringSession实现
添加依赖
pom.xml
<!--spring data redis 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--commons-pool2 对象池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--spring-session 依赖-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
添加配置
application.yml
# redis配置
redis:
# 服务器地址
host: 121.37.15.219
# 端口
port: 6379
# 数据库
database: 0
# 超时时间
timout: 10000ms
lettuce:
pool:
# 最大连接数,默认8
max-active: 8
# 最大连接阻塞等待时间,默认-1
max-wait: 10000ms
# 最大空闲连接,默认8
max-idle: 200
# 最小空闲连接,默认0
min-idle: 5
# 密码
password: kisen
测试
其余代码暂时不动,重新登录测试。会发现session已经存储在Redis上
9.2. 方法二:将用户信息存入Redis
依赖
<!--spring data redis 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--commons-pool2 对象池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
添加配置
spring:
# redis配置
redis:
# 服务器地址
host: 121.37.15.219
# 端口
port: 6379
# 数据库
database: 0
# 超时时间
timout: 10000ms
lettuce:
pool:
# 最大连接数,默认8
max-active: 8
# 最大连接阻塞等待时间,默认-1
max-wait: 10000ms
# 最大空闲连接,默认8
max-idle: 200
# 最小空闲连接,默认0
min-idle: 5
# 密码
password: kisen
RedisConfig.java
package com.xxxx.seckill.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @ClassName: RedisConfig
* @Title: seckill
* @Package: com.xxxx.seckill.config
* @Description: Redis配置类
* @Author: Kisen
* @Date: 2021/3/9 0:29
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// key序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
// value序列化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// hash类型 key序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// hash类型 value序列化
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
// 注入连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
修改之前代码
IUserService.java
/**
* 根据cookie获取用户
* @param userTicket
* @return
*/
User getUserByCookie(String userTicket, HttpServletRequest request, HttpServletResponse response);
UserServiceImpl.java
package com.xxxx.seckill.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xxxx.seckill.exception.GlobalException;
import com.xxxx.seckill.mapper.UserMapper;
import com.xxxx.seckill.pojo.User;
import com.xxxx.seckill.service.IUserService;
import com.xxxx.seckill.utils.CookieUtil;
import com.xxxx.seckill.utils.MD5Util;
import com.xxxx.seckill.utils.UUIDUtil;
import com.xxxx.seckill.vo.LoginVo;
import com.xxxx.seckill.vo.RespBean;
import com.xxxx.seckill.vo.RespBeanEnum;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>
* 服务实现类
* </p>
*
* @author kisen
* @since 2021-03-04
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 登录
* @param loginVo
* @param request
* @param response
* @return
*/
@Override
public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
/*// 参数校验
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
return RespBean.error(RespBeanEnum.LOGIN_ERROR);
}
if (!ValidatorUtil.isMobile(mobile)) {
return RespBean.error(RespBeanEnum.MOBILE_ERROR);
}*/
// 根据手机号获取用户
User user = userMapper.selectById(mobile);
if (null == user) {
throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
}
// 判断密码是否正确
if (!MD5Util.formPassToDBPass(password, user.getSalt()).equals(user.getPassword())) {
throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
}
// 生成cookie
String ticket = UUIDUtil.uuid();
// 将用户信息存入redis中
redisTemplate.opsForValue().set("user:" + ticket, user);
// request.getSession().setAttribute(ticket, user);
CookieUtil.setCookie(request, response, "userTicket", ticket);
return RespBean.success();
}
/**
* 根据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;
}
}
GoodsController.java
package com.xxxx.seckill.controller;
import com.xxxx.seckill.pojo.User;
import com.xxxx.seckill.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @ClassName: GoodsController
* @Title: seckill
* @Package: com.xxxx.seckill.controller
* @Description: 商品
* @Author: Kisen
* @Date: 2021/3/7 14:47
*/
@Controller
@RequestMapping("/goods")
@Slf4j
public class GoodsController {
@Autowired
private IUserService userService;
@RequestMapping("/toList")
public String toList(Model model, HttpServletRequest request, HttpServletResponse response, @CookieValue("userTicket") String ticket) {
if (StringUtils.isEmpty(ticket)) {
return "login";
}
// User user = (User) session.getAttribute(ticket);
User user = userService.getUserByCookie(ticket, request, response);
if (null == user) {
return "login";
}
model.addAttribute("user", user);
return "goodsList";
}
}
测试
Web全局校验登录功能
UserArgumentResolver.java
package com.xxxx.seckill.config;
import com.xxxx.seckill.pojo.User;
import com.xxxx.seckill.service.IUserService;
import com.xxxx.seckill.utils.CookieUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @ClassName: UserArgumentResolver
* @Title: seckill
* @Package: com.xxxx.seckill.config
* @Description: 自定义用户参数
* @Author: Kisen
* @Date: 2021/3/9 22:39
*/
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private IUserService userService;
@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);
}
}
WebConfig.java
package com.xxxx.seckill.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* @ClassName: WebConfig
* @Title: seckill
* @Package: com.xxxx.seckill.config
* @Description: MVC配置类
* @Author: Kisen
* @Date: 2021/3/9 22:37
*/
@Configuration
//@EnableWebMvc //添加该注解,则是完全控制MVC
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userArgumentResolver);
}
}
GoodsController.java
package com.xxxx.seckill.controller;
import com.xxxx.seckill.pojo.User;
import com.xxxx.seckill.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @ClassName: GoodsController
* @Title: seckill
* @Package: com.xxxx.seckill.controller
* @Description: 商品
* @Author: Kisen
* @Date: 2021/3/7 14:47
*/
@Controller
@RequestMapping("/goods")
@Slf4j
public class GoodsController {
@Autowired
private IUserService userService;
@RequestMapping("/toList")
public String toList(Model model, User user) {
// if (StringUtils.isEmpty(ticket)) {
// return "login";
// }
//// User user = (User) session.getAttribute(ticket);
// User user = userService.getUserByCookie(ticket, request, response);
// if (null == user) {
// return "login";
// }
model.addAttribute("user", user);
return "goodsList";
}
}
10.秒杀功能
10.1. 商品列表页
用逆向工程生成所需的所有类
GoodsVo
同时查询商品表和秒杀商品表的返回对象
package com.xxxx.seckill.vo;
import com.xxxx.seckill.pojo.Goods;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;
/**
* @ClassName: GoodsVo
* @Title: seckill
* @Package: com.xxxx.seckill.vo
* @Description: 商品返回对象
* @Author: Kisen
* @Date: 2021/3/14 16:09
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GoodsVo extends Goods {
private BigDecimal seckillPrice;
private Integer stockCount;
private Date startDate;
private Date endDate;
}
GoodsMapper.java
package com.xxxx.seckill.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xxxx.seckill.pojo.Goods;
import com.xxxx.seckill.vo.GoodsVo;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author kisen
* @since 2021-03-14
*/
public interface GoodsMapper extends BaseMapper<Goods> {
/**
* 获取商品列表
* @return
*/
List<GoodsVo> findGoodsVo();
}
GoodsMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxxx.seckill.mapper.GoodsMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.xxxx.seckill.pojo.Goods">
<id column="id" property="id" />
<result column="goods_name" property="goodsName" />
<result column="goods_title" property="goodsTitle" />
<result column="goods_img" property="goodsImg" />
<result column="goods_detail" property="goodsDetail" />
<result column="goods_price" property="goodsPrice" />
<result column="goods_stock" property="goodsStock" />
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, goods_name, goods_title, goods_img, goods_detail, goods_price, goods_stock
</sql>
<!-- 获取商品列表 -->
<select id="findGoodsVo" resultType="com.xxxx.seckill.vo.GoodsVo">
SELECT
g.id,
g.goods_name,
g.goods_title,
g.goods_img,
g.goods_detail,
g.goods_price,
g.goods_stock,
sg.seckill_price,
sg.stock_count,
sg.start_date,
sg.end_date
FROM t_goods g
LEFT JOIN t_seckill_goods sg ON g.id=sg.goods_id
</select>
</mapper>
GoodsService
IGoodService.java
package com.xxxx.seckill.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xxxx.seckill.pojo.Goods;
import com.xxxx.seckill.vo.GoodsVo;
import java.util.List;
/**
* <p>
* 服务类
* </p>
*
* @author kisen
* @since 2021-03-14
*/
public interface IGoodsService extends IService<Goods> {
/**
* 获取商品列表
* @return
*/
List<GoodsVo> findGoodsVo();
}
GoodsServiceImpl.java
package com.xxxx.seckill.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xxxx.seckill.mapper.GoodsMapper;
import com.xxxx.seckill.pojo.Goods;
import com.xxxx.seckill.service.IGoodsService;
import com.xxxx.seckill.vo.GoodsVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author kisen
* @since 2021-03-14
*/
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements IGoodsService {
@Autowired
private GoodsMapper goodsMapper;
/**
* 获取商品列表
* @return
*/
@Override
public List<GoodsVo> findGoodsVo() {
return goodsMapper.findGoodsVo();
}
}
GoodsController
package com.xxxx.seckill.controller;
import com.xxxx.seckill.pojo.User;
import com.xxxx.seckill.service.IGoodsService;
import com.xxxx.seckill.service.IUserService;
import com.xxxx.seckill.vo.GoodsVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Date;
/**
* @ClassName: GoodsController
* @Title: seckill
* @Package: com.xxxx.seckill.controller
* @Description: 商品
* @Author: Kisen
* @Date: 2021/3/7 14:47
*/
@Controller
@RequestMapping("/goods")
@Slf4j
public class GoodsController {
@Autowired
private IUserService userService;
@Autowired
private IGoodsService goodsService;
/**
* 跳转商品列表页
* @param model
* @param user
* @return
*/
@RequestMapping("/toList")
public String toList(Model model, User user) {
model.addAttribute("user", user);
model.addAttribute("goodsList", goodsService.findGoodsVo());
return "goodsList";
}
}
MvcConfig
package com.xxxx.seckill.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* @ClassName: WebConfig
* @Title: seckill
* @Package: com.xxxx.seckill.config
* @Description: MVC配置类
* @Author: Kisen
* @Date: 2021/3/9 22:37
*/
@Configuration
//@EnableWebMvc //添加该注解,则是完全控制MVC
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userArgumentResolver);
}
//
// @Override
// public void addResourceHandlers(ResourceHandlerRegistry registry) {
// registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
// }
}
goodsList.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>商品列表</title>
<!-- jquery -->
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- layer -->
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<!-- common.js -->
<script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀商品列表</div>
<table class="table" id="goodslist">
<tr>
<td>商品名称</td>
<td>商品图片</td>
<td>商品原价</td>
<td>秒杀价</td>
<td>库存数量</td>
<td>详情</td>
</tr>
<tr th:each="goods,goodsStat : ${goodsList}">
<td th:text="${goods.goodsName}"></td>
<td><img th:src="@{${goods.goodsImg}}" width="100" height="100"/></td>
<td th:text="${goods.goodsPrice}"></td>
<td th:text="${goods.seckillPrice}"></td>
<td th:text="${goods.stockCount}"></td>
<td><a th:href="'/goods/toDetail/'+${goods.id}">详情</a></td>
</tr>
</table>
</div>
</body>
</html>
测试
10.2. 商品详情页
GoodsMapper
GoodsMapper.java
/**
* 获取商品详情
* @param goodsId
* @return
*/
GoodsVo findGoodsVoByGoodsId(Long goodsId);
GoodsMapper.xml
<!-- 获取商品详情 -->
<select id="findGoodsVoByGoodsId" resultType="com.xxxx.seckill.vo.GoodsVo">
select
g.id,
g.goods_name,
g.goods_title,
g.goods_img,
g.goods_detail,
g.goods_price,
g.goods_stock,
sg.seckill_price,
sg.stock_count,
sg.start_date,
sg.end_date
from t_goods g
left join t_seckill_goods sg on g.id=sg.goods_id
where g.id = #{goodsId}
</select>
GoodsService
IGoodsService.java
/**
* 获取商品详情
* @param goodsId
* @return
*/
GoodsVo findGoodsVoByGoodsId(Long goodsId);
GoodsServiceImpl.java
/**
* 获取商品详情
* @param goodsId
* @return
*/
@Override
public GoodsVo findGoodsVoByGoodsId(Long goodsId) {
return goodsMapper.findGoodsVoByGoodsId(goodsId);
}
GoodsController
/**
* 跳转商品详情页
* @param goodsId
* @param model
* @param user
* @return
*/
@RequestMapping("/toDetail/{goodsId}")
public String toDetail(@PathVariable Long goodsId, Model model, User user) {
model.addAttribute("user", user);
GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
Date startDate = goodsVo.getStartDate();
Date endDate = goodsVo.getEndDate();
Date nowDate = new Date();
// 秒杀状态
int secKillStatus = 0;
// 秒杀还未开始
int remainSeconds = 0;
if (nowDate.before(startDate)) {
remainSeconds = (int) ((startDate.getTime() - nowDate.getTime())/1000);
} else if (nowDate.after(endDate)) {
// 秒杀已结束
secKillStatus = 2;
remainSeconds = -1;
} else {
// 秒杀中
secKillStatus = 1;
remainSeconds = 0;
}
model.addAttribute("remainSeconds", remainSeconds);
model.addAttribute("secKillStatus", secKillStatus);
model.addAttribute("goods", goodsVo);
return "goodsDetail";
}
goodsDetail.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>商品详情</title>
<!-- jquery -->
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- layer -->
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<!-- common.js -->
<script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀商品详情</div>
<div class="panel-body">
<span th:if="${user eq null}"> 您还没有登录,请登陆后再操作<br/></span>
<span>没有收货地址的提示。。。</span>
</div>
<table class="table" id="goods">
<tr>
<td>商品名称</td>
<td colspan="3" th:text="${goods.goodsName}"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="3"><img th:src="@{${goods.goodsImg}}" width="200" height="200"/></td>
</tr>
<tr>
<td>秒杀开始时间</td>
<td th:text="${#dates.format(goods.startDate, 'yyyy-MM-dd HH:mm:ss')}"></td>
<td id="seckillTip">
<input type="hidden" id="remainSeconds" th:value="${remainSeconds}">
<span th:if="${secKillStatus eq 0}">秒杀倒计时:<span id="countDown" th:text="${remainSeconds}"></span>秒</span>
<span th:if="${secKillStatus eq 1}">秒杀进行中</span>
<span th:if="${secKillStatus eq 2}">秒杀已结束</span>
</td>
<td>
<form id="secKillForm" action="/seckill/doSeckill">
<input type="hidden" name="goodsId" th:value="${goods.id}">
<button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button>
</form>
</td>
</tr>
<tr>
<td>商品原价</td>
<td colspan="3" th:text="${goods.goodsPrice}"></td>
</tr>
<tr>
<td>秒杀价</td>
<td colspan="3" th:text="${goods.seckillPrice}"></td>
</tr>
<tr>
<td>库存数量</td>
<td colspan="3" th:text="${goods.stockCount}"></td>
</tr>
</table>
</div>
</body>
<script>
$(function () {
countDown();
});
function countDown(){
var remainSeconds = $("#remainSeconds").val();
var timeout;
// 秒杀还未开始
if (remainSeconds > 0) {
$("#buyButton").attr("disabled", true);
timeout = setTimeout(function () {
$("#countDown").text(remainSeconds-1);
$("#remainSeconds").val(remainSeconds-1);
countDown();
}, 1000);
} else if (remainSeconds == 0) {
$("#buyButton").attr("disabled", false);
// 秒杀进行中
if (timeout) {
clearTimeout(timeout);
}
$("#seckillTip").html("秒杀进行中");
} else {
$("#buyButton").attr("disabled", true);
$("#seckillTip").html("秒杀已经结束");
}
};
</script>
</html>
测试
秒杀未开始
秒杀进行中
秒杀已结束
10.3. 秒杀功能实现
OrderService
IOrderService.java
package com.xxxx.seckill.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xxxx.seckill.pojo.Order;
import com.xxxx.seckill.pojo.User;
import com.xxxx.seckill.vo.GoodsVo;
/**
* <p>
* 服务类
* </p>
*
* @author kisen
* @since 2021-03-14
*/
public interface IOrderService extends IService<Order> {
/**
* 秒杀
* @param user
* @param goods
* @return
*/
Order seckill(User user, GoodsVo goods);
}
OrderServiceImpl.java
package com.xxxx.seckill.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xxxx.seckill.mapper.OrderMapper;
import com.xxxx.seckill.pojo.Order;
import com.xxxx.seckill.pojo.SeckillGoods;
import com.xxxx.seckill.pojo.SeckillOrder;
import com.xxxx.seckill.pojo.User;
import com.xxxx.seckill.service.IOrderService;
import com.xxxx.seckill.service.ISeckillGoodsService;
import com.xxxx.seckill.service.ISeckillOrderService;
import com.xxxx.seckill.vo.GoodsVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* <p>
* 服务实现类
* </p>
*
* @author kisen
* @since 2021-03-14
*/
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {
@Autowired
private ISeckillGoodsService seckillGoodsService;
@Autowired
private OrderMapper orderMapper;
@Autowired
private ISeckillOrderService seckillOrderService;
/**
* 秒杀
* @param user
* @param goods
* @return
*/
@Override
public Order seckill(User user, GoodsVo goods) {
// 秒杀商品表减库存
SeckillGoods seckillGoods = seckillGoodsService.getOne(new QueryWrapper<SeckillGoods>().eq("goods_id", goods.getId()));
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());
orderMapper.insert(order);
// 生成秒杀订单
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setUserId(user.getId());
seckillOrder.setOrderId(order.getId());
seckillOrder.setGoodsId(goods.getId());
seckillOrderService.save(seckillOrder);
return order;
}
}
SeckillController
package com.xxxx.seckill.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xxxx.seckill.pojo.Order;
import com.xxxx.seckill.pojo.SeckillOrder;
import com.xxxx.seckill.pojo.User;
import com.xxxx.seckill.service.IGoodsService;
import com.xxxx.seckill.service.IOrderService;
import com.xxxx.seckill.service.ISeckillOrderService;
import com.xxxx.seckill.vo.GoodsVo;
import com.xxxx.seckill.vo.RespBeanEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @ClassName: SecKillController
* @Title: seckill
* @Package: com.xxxx.seckill.controller
* @Description: 秒杀
* @Author: Kisen
* @Date: 2021/3/20 14:59
*/
@Controller
@RequestMapping("/seckill")
public class SecKillController {
@Autowired
private IGoodsService goodsService;
@Autowired
private ISeckillOrderService seckillOrderService;
@Autowired
private IOrderService orderService;
/**
* 秒杀
* @param model
* @param user
* @param goodsId
* @return
*/
@RequestMapping("/doSeckill")
public String doSeckill(Model model, User user, Long goodsId) {
if (user == null) {
return "login";
}
model.addAttribute("user", user);
GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);
// 判断库存
if (goods.getStockCount() < 1) {
model.addAttribute("errmsg", RespBeanEnum.EMPTY_STOCK.getMessage());
return "secKillFail";
}
// 判断是否重复抢购
SeckillOrder seckillOrder = seckillOrderService.getOne(new QueryWrapper<SeckillOrder>().eq("user_id", user.getId())
.eq("goods_id", goodsId));
if (seckillOrder != null) {
model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
return "secKillFail";
}
Order order = orderService.seckill(user, goods);
model.addAttribute("order", order);
model.addAttribute("goods", goods);
return "orderDetail";
}
}
测试
秒杀成功进入订单详情注意查看库存是否正确扣减,订单是否正确生成
库存不足
重复抢购
10.4. 订单详情页
本课程重点针对秒杀,所以订单详情只做简单页面展示,随后得支付等功能也不在本课程体现
OrderDetail.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>订单详情</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- jquery -->
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}" />
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- layer -->
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<!-- common.js -->
<script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀订单详情</div>
<table class="table" id="order">
<tr>
<td>商品名称</td>
<td th:text="${goods.goodsName}" colspan="3"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="2"><img th:src="@{${goods.goodsImg}}" width="200" height="200" /></td>
</tr>
<tr>
<td>订单价格</td>
<td colspan="2" th:text="${order.goodsPrice}"></td>
</tr>
<tr>
<td>下单时间</td>
<td th:text="${#dates.format(order.createDate, 'yyyy-MM-dd HH:mm:ss')}" colspan="2"></td>
</tr>
<tr>
<td>订单状态</td>
<td >
<span th:if="${order.status eq 0}">未支付</span>
<span th:if="${order.status eq 1}">待发货</span>
<span th:if="${order.status eq 2}">已发货</span>
<span th:if="${order.status eq 3}">已收货</span>
<span th:if="${order.status eq 4}">已退款</span>
<span th:if="${order.status eq 5}">已完成</span>
</td>
<td>
<button class="btn btn-primary btn-block" type="submit" id="payButton">立即支付</button>
</td>
</tr>
<tr>
<td>收货人</td>
<td colspan="2">XXX 18012345678</td>
</tr>
<tr>
<td>收货地址</td>
<td colspan="2">上海市浦东区世纪大道</td>
</tr>
</table>
</div>
</body>
</html>
测试
至此,简单的秒杀功能逻辑就完成了,下面进入优化阶段
11.系统压测
11.1. JMeter入门
11.1.1.安装
官网:https://jmeter.apache.org/
下载地址:https:/jmeter.apache.org/download_jmeter.cgi
下载解压后直接在bin目录里双击jmeter.bat即可启动(Linux系统通过jmeter.sh启动)
修改中文
Choose Language->Chinese(Simplified)
11.1.2.简单使用
我们先使用JMeter测试一下商品列表页的接口。
首先创建线程组,步骤:添加->线程(用户)->线程组
Ramp-up指在几秒之内启动指定线程数
创建HTTP请求默认值,步骤:添加->配置元件->HTTP请求默认值
添加测试接口,步骤:添加->取样器->HTTP请求
查看输出结果,步骤:添加->监听器->聚合报告/图形结果/用表格擦看结果
启动即可在监听器看到对应的结果
Linux压测命令:
./jmeter.sh -n -t first.jmx -l result.jtl
11.2.安装MySQL
11.2.1.安装
wget http://repo.mysql.com/mysql57-community-release-el7-10.noarch.rpm
安装mysql的repo源:
rpm -ivh mysql57-community-release-el7-10.noarch.rpm
查看是否安装成功:
yum repolist enabled | grep "mysql.*-community.*"
开始安装:
yum install mysql-community-server
11.2.2.启动服务
服务开关操作:
systemctl status mysqld #检查mysql运行状态
systemctl start/restart/stop mysqld #启动/重启/停止mysql
systemctl enable mysqld #开机启动mysql服务
下图表示MySQL正常运行
11.2.3.修改密码
修改root本地密码:
#查看初始密码
grep 'temporary password' /var/log/mysqld.log
#进入mysql,输入初始秘密
mysql -uroot -p
#修改密码
set password for 'root'@'localhost'=password('root');
#注意如果你的密码简单,必须修改两个全局参数:
set global validate_password_policy=0;
set global validate_password_length=1;
授权所有其他机器登陆:
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'root' WITH GRANT OPTION;
FLUSH PRIVILEGES;
#创建新的database
create database mmall default character set utf8 collate utf8_general_ci;
配置默认编码为utf8:修改/etc/my.cnf配置文件,在[mysqld]下添加编码配置
character_set_server=utf8
#default=character-set=utf8
init_connect='SET NAMES utf8'
11.2.4.访问数据库
输入以下命令,并输入修改后的密码进行登录
mysql -u root -p
远程连接数据库
附:RabbitMQ相关
下载erlang
wget https://github.com/rabbitmq/erlang-rpm/releases/download/v23.3.1/erlang-23.3.1-1.el7.x86_64.rpm
安装erlang
rpm -ivh erlang-23.3.1-1.el7.x86_64.rpm
安装完成后,运行命令来查看你安装的erl版本
erl -version
yum -y install socat
下载RabbitMQ
wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.14/rabbitmq-server-3.8.14-1.el7.noarch.rpm
安装RabbitMQ
rpm -ivh rabbitmq-server-3.8.14-1.el7.noarch.rpm
启动rabbitmq
systemctl start rabbitmq-server
查看rabbitmq状态
systemctl status rabbitmq-server
设置开机启动rabbitmq
systemctl enable rabbitmq-server
显示所有插件
rabbitmq-plugins list
安装RabbitMQ管控插件(用默认的guest用户)
rabbitmq-plugins enable rabbitmq_management
配置防火墙
查看防火墙开发端口
firewall-cmd --zone=public --list-ports
开发RabbitMQ端口15672以便远程访问
firewall-cmd --zone=public --add-port=15672/tcp --permanent
重启防火墙
firewall-cmd --reload
配置RabbitMQ非本机访问的配置文件
cd /etc/rabbitmq/
vim rabbitmq.config
重启服务
systemctl restart rabbitmq-server
本文来自博客园,作者:冰枫丶,转载请注明原文链接:https://www.cnblogs.com/lqsblog/p/15316462.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· DeepSeek “源神”启动!「GitHub 热点速览」
· 上周热点回顾(2.17-2.23)