秒杀,对我们来说,都不是一个陌生的东西。每年的双11,618以及时下流行的直播等等。秒杀然而,这对于我们系统而言是一个巨大的考验。 那么,如何才能更好地理解秒杀系统呢?我觉得作为一个程序员,你首先需要从高纬度出发,从整体上思考 问题。在我看来,秒杀其实主要解决两个问题,一个 并发读,一个并发写。并发读的核心优化理念是尽量减少用户到服务来"读"数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。另外,我们还要针对秒杀系统做一些保护,针对意料之外的情况兜底方案,以防止最坏的情况发生。 其实,秒杀的整体架构可以概况为:"稳、准、快"几个关键字。 "稳",就是整个系统架构要满足高可用,流量符合预期肯定要稳定,就是超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。 "准",就是秒杀10台手机,那就是只能交10台,多一台都不行。一旦库存不对,那平台就要承担损失,所以"准"就是要求保证数量的一致性。 "快",就是说系统的性能要足够的高,否则你怎么支撑这么大的流量呢。不光服务端做到极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整一个系统就完美了。
创建springboot项目
设置项目信息
选择Lombok、Spring Web、Thymeleaf、MySQL Driver依赖
写好项目名称和路径
最后完成项目的创建。
引入mybatis-plus依赖
<!-- mybatisPlus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
配置application.yml文件
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
在com.hsb.seckill包下分别如下包名:
公共返回对象枚举
/**
* 公共返回对象枚举
*/
@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;
}
公共返回对象
/**
* 公共返回对象
*/
@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);
}
}
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);
通过EasyCode插件自动生成user表的entity、dao、service、impl、controller和mapper映射文件
整体加密流程
MD5(MD5(pass明文+固定salt)+随机salt) 第一次固定salt写死在前端 第二次加密采用随机的salt 并将每次生成的salt保存在数据库中
登录流程
前端对用户输入的密码进行md5加密(固定的salt) 将加密后的密码传递到后端 后端使用用户id取出用户信息 后端对加密后的密码在进行md5加密(取出盐),然后与数据库中存储的密码进行对比, ok登录成功,否则登录失败
注册流程
前端对用户输入的密码进行md5加密(固定的salt) 将加密后的密码传递到后端 后端随机生成一个salt, 使用生成salt对前端传过来的密码进行加密,然后将加密后密码和salt一起保存到db中
引入依赖
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
创建MD5加密工具类MD5Util
/**
* 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"));
}
}
引入登录页面和静态资源文件
<!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>
<!-- 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/to_list";
}else{
layer.msg(data.message);
}
},
error:function(){
layer.closeAll();
}
});
}
</script>
</html>
实现登录页面的跳转
@Controller
@RequestMapping("/login")
@Slf4j
public class LoginController {
/**
* 跳转登录页面
* @return
*/
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
}
根据用户手机查询用户
dao
/**
* 根据手机号码查询用户
* @return
*/
User queryByMobile(String mobile);
service
/**
* 登录
* @param loginVo
* @return
*/
ResBean doLogin(LoginVo loginVo);
impl
/**
* 登录
* @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
/**
* 登录功能
* @param loginVo
* @return
*/
@ResponseBody
@RequestMapping("/doLogin")
public ResBean doLogin(@Valid LoginVo loginVo){
// log.info("{}",loginVo);
return userService.doLogin(loginVo);
}
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
在参数类属性上添加注解
/**
* 登录参数
*/
@Data
public class LoginVo {
@NotNull
@IsMobile
private String mobile;
@NotNull
@Length(min = 32)
private String password;
}
校验手机号码校验注解
/**
* 校验手机号注解
*/
@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 {};
}
手机号码校验规则类
/**
* 校验手机号码规则
*/
public class IsMobileValidator implements ConstraintValidator<IsMobile,String> {
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);
}
}
}
}
手机号校验工具类
/**
* 手机号码校验
*/
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();
}
}
我们知道,系统异常包括:编译时异常和运行时异常RuntimeException,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。在开发中,不管时dao层、service层还是controller层,都有可能抛出异常,在springmvc中,能将所有类型的异常处理过程解耦出来,既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护。 springboot全局异常处理方式主要有两种:
区别: 1、@ControllerAdvice方式只能处理控制器抛出异常,此时请求已经进行控制器中。 2、ErrorController类方式可以处理所有的异常,包括未进入控制器的异常,比如404、401等错误。 3、如果应用中两者公同存在,则@ControllerAdvice方式处理控制器的异常,ErrorController方式处理为进入控制器的异常。 4、@ControllerAdvice方式可以定义多个拦截方法,拦截不同的异常类,并且可以获取异常信息,自由度更大。
公共返回结果类
/**
* 公共返回对象
*/
@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);
}
}
自定义异常枚举类
/**
* 公共返回对象枚举
*/
@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;
}
}
全局异常类
/**
* 全局异常
*/
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;
}
}
全局异常处理类
/**
* 全局异常处理类
*/
@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);
}
}
登录校验
/**
* 登录
* @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();
}
UUID工具类
/**
* uuid工具类
*/
public class UUIDUtil {
// 生成uuid
public static String uuid(){
return UUID.randomUUID().toString().replace("-","");
}
}
cookie工具类
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
//设置session和cookie
String ticket = UUIDUtil.uuid();
request.getSession().setAttribute(ticket,user);
CookieUtil.setCookie(request,response,"userTicket",ticket);
登录成功后,检验seesion和cookie
/**
* 检验用户,跳转商品页面
* @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";
}
首先要安装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 session依赖 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
配置redis
# 配置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
去除spring session依赖
将session存储到redis中
//设置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获取用户
/**
* 根据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;
}
自定义用户参数
/**
* 自定义用户参数
*/
@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配置类
/**
* MVC配置类
*/
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserArgumentResolver userArgumentResolver;
//设置参数解析器
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userArgumentResolver);
}
}
检验用户
/**
* 检验用户,跳转商品页面
* @param model
* @return
*/
@RequestMapping("/toList")
public String toList(Model model,User user){
model.addAttribute("user",user);
return "goodsList";
}
商品表
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`)
)
订单表
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`)
)
秒杀商品表
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`)
)
秒杀订单表
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`)
)
商品列表页面
<!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>
<!-- 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>
<style type="text/css">
html,body{
height:100%;
width:100%;
}
body{
/*background:url('/img/bg.jpg') no-repeat;*/
background-size:100% 100%;
}
#goodslist td{
border-top:1px solid #39503f61;
}
</style>
</head>
<body>
<div class="panel panel-default" style="height:100%;">
<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.goodsStock}"></td>
<td><a th:href="'/goods/toDetail/'+${goods.id}">详情</a></td>
</tr>
</table>
</div>
</body>
</html>
商品返回对象
/**
* 商品返回对象
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GoodsVo extends Goods {
private BigDecimal seckillPrice;
private Integer stockCount;
private Date startDate;
private Date endDate;
}
GoodsController
/**
* 检验用户,跳转商品页面
* @param model
* @return
*/
@RequestMapping("/toList")
public String toList(Model model,User user){
if (null == user){
return "login";
}
// 查询所有秒杀商品
List<GoodsVo> goodsList = goodsService.querySeckillGoods();
model.addAttribute("goodsList",goodsList);
model.addAttribute("user",user);
return "goods_list";
}
商品详情页面
<!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>
<!-- 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>
<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="goodslist">
<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="miaoshaTip">
<input type="hidden" id="remainSeconds" th:value="${remainSeconds}" />
<input type="hidden" id="betweenSeconds" th:value="${betweenSeconds}" />
<span th:if="${miaoshaStatus eq 0}">秒杀倒计时:<span id="countDown" th:text="${remainSeconds}"></span>秒</span>
<span th:if="${miaoshaStatus eq 1}">秒杀进行中</span>
<span th:if="${miaoshaStatus eq 2}">秒杀已结束</span>
</td>
<td>
<form id="miaoshaForm" method="post" action="/miaosha/do_miaosha">
<button disabled="disabled" class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button>
<input type="hidden" name="goodsId" th:value="${goods.id}" />
</form>
</td>
</tr>
<tr>
<td>秒杀结束时间</td>
<td th:text="${#dates.format(goods.endDate, 'yyyy-MM-dd HH:mm:ss')}"></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 betweenSeconds = $("#betweenSeconds").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 && betweenSeconds>0){//秒杀进行中
$("#buyButton").attr("disabled", false);
if(timeout){
clearTimeout(timeout);
}
$("#miaoshaTip").html("秒杀进行中");
//秒杀结束
setTimeout(function(){
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀已经结束");
},betweenSeconds*1000);
}
}
</script>
</html>
GoodsController
/**
* 秒杀商品详情页
* @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";
}
订单详情页面
<!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>
<!-- 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>
<div class="panel panel-default">
<div class="panel-heading">秒杀订单详情</div>
<table class="table" id="goodslist">
<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 18812341234</td>
</tr>
<tr>
<td>收货地址</td>
<td colspan="2">北京市昌平区回龙观龙博一区</td>
</tr>
</table>
</div>
</body>
</html>
SeckillController
@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<SeckillOrder>().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
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, Order> 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<SeckillGoods>().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;
}
}
解决用户重复抢购同一件商品的问题
方案1:
// 将订单缓存到redis中
redisTemplate.opsForValue().set(("order:"+user.getId()+":"+goods.getId()),seckillOrder,60, TimeUnit.SECONDS);
// 从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";
}
方案二:(推荐)
Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("seckillCount:" + user.getId() + ":" + goodsId, user.getId());
if (!ifAbsent){
return ResBean.error(ResBeanEnum.REPEATE_ERROR);
}
解决商品超卖问题
//判断库存
if (goods.getStockCount()<1){
model.addAttribute("errMsg", ResBeanEnum.EMPTY_STOCK.getMessage());
return "seckill_fail";
}
// 解决库存超卖
boolean result = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>()
.setSql("stock_count = stock_count-1")
.eq("goods_id", goods.getId())
.gt("stock_count", 0));
if (!result){
return null;
}
到Apache官网下载JMeter压缩包,解压到文件夹下,打开bin目录,打开jmeter配置文件jmeter.properies
修改语言
修改编码
启动jmeter
打开bin目录下的jmeter.bat,即可打开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 管理器 添加名称,设置值,域,路径
UserUtil
public class UserUtil {
private static void createUser(int count) throws SQLException, ClassNotFoundException, IOException {
ArrayList<User> 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<users.size();i++) {
User user = users.get(i);
pstmt.setInt(1, user.getLoginCount());
pstmt.setString(2, user.getNickname());
pstmt.setTimestamp(3, new Timestamp(user.getRegisterDate().getTime()));
pstmt.setString(4, user.getSlat());
pstmt.setString(5, user.getPassword());
pstmt.setString(6, user.getMobile());
pstmt.addBatch();
}
pstmt.executeBatch();
pstmt.close();
conn.close();
System.out.println("insert to db");
//登录,生成token
String urlString = "http://localhost:8080/login/doLogin";
File file = new File("C:/Users/H/Desktop/config.txt");
if(file.exists()) {
file.delete();
}
RandomAccessFile raf = new RandomAccessFile(file, "rw");
file.createNewFile();
raf.seek(0);
for(int i=0;i<users.size();i++) {
User user = users.get(i);
URL url = new URL(urlString);
HttpURLConnection co = (HttpURLConnection)url.openConnection();
co.setRequestMethod("POST");
co.setDoOutput(true);
OutputStream out = co.getOutputStream();
String params = "mobile="+user.getMobile()+"&password="+MD5Util.inputPassToFromPass("123456");
out.write(params.getBytes());
out.flush();
InputStream inputStream = co.getInputStream();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte buff[] = new byte[1024];
int len = 0;
while((len = inputStream.read(buff)) >= 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);
}
}
设置HTTP请求
压测结果
设置HTTP请求
压测结果
商品出现
商品列表页面
@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<GoodsVo> 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;
}
商品详情页面
@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;
}
用户对象缓存
/**
* 根据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;
}
更新用户密码并删除缓存
/**
* 更新用户密码
* @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);
}
配置
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
/**
* 商品详情返回对象
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GoodsDetailVo {
private User user;
private GoodsVo goodsVo;
private int remainSeconds;
private int miaoshaStatus;
private int betweenSeconds;
}
goods_detail.htm
<!DOCTYPE HTML>
<html>
<head>
<title>商品详情</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- jquery -->
<script type="text/javascript" src="/js/jquery.min.js"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" />
<script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script>
<!-- jquery-validator -->
<script type="text/javascript" src="/jquery-validation/jquery.validate.min.js"></script>
<script type="text/javascript" src="/jquery-validation/localization/messages_zh.min.js"></script>
<!-- layer -->
<script type="text/javascript" src="/layer/layer.js"></script>
<!-- md5.js -->
<script type="text/javascript" src="/js/md5.min.js"></script>
<!-- common.js -->
<script type="text/javascript" src="/js/common.js"></script>
</head>
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀商品详情</div>
<div class="panel-body">
<span id="userTip"> 您还没有登录,请登陆后再操作<br/></span>
<span>没有收货地址的提示。。。</span>
</div>
<table class="table" id="goodslist">
<tr>
<td>商品名称</td>
<td colspan="3" id="goodsName"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="3"><img id="goodsImg" width="200" height="200" /></td>
</tr>
<tr>
<td>秒杀开始时间</td>
<td id="startTime"></td>
<td>
<input type="hidden" id="remainSeconds" />
<input type="hidden" id="betweenSeconds" />
<span id="miaoshaTip"></span>
</td>
<td>
<button class="btn btn-primary btn-block" type="button" id="buyButton" onclick="doSeckill()">立即秒杀</button>
<input type="hidden" name="goodsId" id="goodsId" />
</td>
</tr>
<tr>
<td>秒杀结束时间</td>
<td id="endDate"></td>
</tr>
<tr>
<td>商品原价</td>
<td colspan="3" id="goodsPrice"></td>
</tr>
<tr>
<td>秒杀价</td>
<td colspan="3" id="seckillPrice"></td>
</tr>
<tr>
<td>库存数量</td>
<td colspan="3" id="stockCount"></td>
</tr>
</table>
</div>
</body>
<script>
$(function(){
// countDown();
getDetail()
});
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;
}else{
layer.msg("客户端请求错误");
}
},
error:function () {
layer.msg("客户端请求错误");
}
})
}
function getDetail() {
var goodsId = g_getQueryString("goodsId");
console.log(goodsId)
$.ajax({
url:'/goods/toDetail/'+goodsId,
type:'GET',
success:function (data) {
if (data.code==200){
render(data.obj);
}else{
layer.msg("客户端请求失败")
}
},
error:function (){
layer.msg("客户端请求失败")
}
})
}
function render(detail) {
var user = detail.user;
var goods = detail.goodsVo;
var remainSeconds = detail.remainSeconds;
var miaoshaStatus = detail.miaoshaStatus;
var betweenSeconds = detail.betweenSeconds;
if(user){
$("#userTip").hide();
}
$("#goodsName").text(goods.goodsName);
$("#goodsImg").attr("src",goods.goodsImg);
$("#startTime").text(new Date(goods.startDate).format("yyyy-MM-dd hh:mm:ss"));
$("#endDate").text(new Date(goods.endDate).format("yyyy-MM-dd hh:mm:ss"));
$("#remainSeconds").val(remainSeconds);
$("#betweenSeconds").val(betweenSeconds);
$("#goodsId").val(goods.id);
$("#goodsPrice").text(goods.goodsPrice);
$("#seckillPrice").text(goods.seckillPrice);
$("#stockCount").text(goods.stockCount);
countDown()
}
function countDown(){
var remainSeconds = $("#remainSeconds").val();
var betweenSeconds = $("#betweenSeconds").val();
console.log(betweenSeconds)
var timeout;
if(remainSeconds > 0){//秒杀还没开始,倒计时
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀倒计时:"+remainSeconds+"秒");
timeout = setTimeout(function(){
$("#remainSeconds").val(remainSeconds - 1);
countDown();
},1000);
}else if(remainSeconds == 0 && betweenSeconds>0){//秒杀进行中
$("#buyButton").attr("disabled", false);
if(timeout){
clearTimeout(timeout);
}
$("#miaoshaTip").html("秒杀进行中");
//秒杀结束
setTimeout(function(){
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀已经结束");
},betweenSeconds*1000);
}else {
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀已经结束");
}
}
</script>
</html>
controller
@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
@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);
}
订单详情页面静态化
订单详情返回对象
/**
* 订单详情返回对象
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderDetailVo {
private Order order;
private GoodsVo goodsVo;
}
order_detail.htm
<!DOCTYPE HTML>
<html>
<head>
<title>订单详情</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- jquery -->
<script type="text/javascript" src="/js/jquery.min.js"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" />
<script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script>
<!-- jquery-validator -->
<script type="text/javascript" src="/jquery-validation/jquery.validate.min.js"></script>
<script type="text/javascript" src="/jquery-validation/localization/messages_zh.min.js"></script>
<!-- layer -->
<script type="text/javascript" src="/layer/layer.js"></script>
<!-- md5.js -->
<script type="text/javascript" src="/js/md5.min.js"></script>
<!-- common.js -->
<script type="text/javascript" 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 id="goodsName" colspan="3"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="2"><img id="goodsImg" width="200" height="200" /></td>
</tr>
<tr>
<td>订单价格</td>
<td colspan="2" id="goodsPrice"></td>
</tr>
<tr>
<td>下单时间</td>
<td id="createDate" colspan="2"></td>
</tr>
<tr>
<td>订单状态</td>
<td id="status">
<!-- <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 18812341234</td>
</tr>
<tr>
<td>收货地址</td>
<td colspan="2">北京市昌平区回龙观龙博一区</td>
</tr>
</table>
</div>
<script>
$(function () {
getOrderDetail();
});
function getOrderDetail() {
var orderId = g_getQueryString("orderId");
console.log(orderId)
$.ajax({
url:'/order/detail',
type:'GET',
data:{
orderId:orderId
},
success:function (data) {
if (data.code==200){
render(data.obj)
}else{
layer.msg(data.message);
}
},
error:function () {
layer.msg("客户端请求异常");
}
})
}
function render(detail) {
console.log(detail)
var goods = detail.goodsVo;
var order = detail.order;
$("#goodsName").text(goods.goodsName);
$("#goodsImg").attr("src",goods.goodsImg);
$("#goodsPrice").text(order.goodsPrice);
$("#createDate").text(new Date(order.createDate).format("yyyy-MM-dd hh:mm:ss"));
var status = order.status;
var statusText=""
switch (status) {
case 0:
statusText="未支付";
break;
case 1:
statusText="代发货";
break;
case 2:
statusText="已发货";
break;
case 3:
statusText="已收货";
break;
case 4:
statusText="已退款";
break;
case 5:
statusText="已完成";
break;
}
$("#status").text(statusText);
}
</script>
</body>
</html>
orederController
/**
* 订单详情
* @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);
}
安装包下载
rabbitmq3.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
在CenOS安装
yum -y install erlang-23.3.4.6-1.el7.x86_64.rpm 安装成功
yum -y install rabbitmq-server-3.8.5-1.el7.noarch.rpm
rabbitmq-plugins list
rabbitmq-plugins enable rabbitmq_management
systemctl start rabbitmq-server.service
systemctl status rabbitmq-server.service
linux服务器地址+端口15672,用户名和密码都为guest 不允许远程访问
进入/etc/rabbitmq/目录,创建rabbitmq.conf文件,写上[{rabbit,[{loopback_users, []}]}].,保存重启rabbitmq服务systemctl restart rabbitmq-server.service
引入依赖
<!-- AMQP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置rabbitmq
# 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配置类
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);
}
}
消息发送者
/**
* 消息发送者
*/
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(Object msg){
log.info("发送消息:"+msg);
rabbitTemplate.convertAndSend("queue",msg);
}
}
消息接收消费者
/**
* 消息消费者
*/
@Service
@Slf4j
public class MQReceiver {
@RabbitListener(queues = "queue")
public void receiver(Object msg){
log.info("接收消息:"+msg);
}
}
测试
@RestController
@RequestMapping("/mq")
public class RabbitmqController {
@Autowired
private MQSender mqSender;
/**
* 测试发送rabbitmq消息
*/
@RequestMapping("/mq")
@ResponseBody
public void mq(){
mqSender.send("hello rabbitmq");
}
}
rabbitmq配置
/**
* 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());
}
}
消息发送者
/**
* 消息发送者
*/
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(Object msg){
log.info("发送消息:"+msg);
rabbitTemplate.convertAndSend("fanoutExchange","",msg);
}
}
消息消费者
/**
* 消息消费者
*/
@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);
}
}
rabbitmq配置类
/**
* 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);
}
}
消息发送者
/**
* 消息发送者
*/
@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);
}
}
消息接收消费者
/**
* 消息消费者
*/
@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);
}
}
rabbitmq配置类
/**
* 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);
}
}
消息发送者
/**
* 消息发送者
*/
@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);
}
}
消息接收消费者
/**
* 消息消费者
*/
@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);
}
}
系统初始化时,将秒杀商品库存加载到redis中
注:controller类实现InitializingBean接口
/**
* 系统初始化,把商品库存数量加载到redis中
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsVo> 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
//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的访问
//1、创建map存放商品是否有库存
private final Map<Long,Boolean> 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);
秒杀消息类
/**
* 秒杀消息
*/
@Data
@AllArgsConstructor@NoArgsConstructor
public class SeckillMessage {
private User user;
private Long goodsId;
}
配置rabbitmq
/**
* 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);
}
}
秒杀消息发送者
/**
* 消息发送者
*/
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
//发送商品秒杀消息
public void sendSeckillMessage(String msg){
log.info("发送秒杀商品消息:"+msg);
rabbitTemplate.convertAndSend("seckillExchange","seckill.doSeckill",msg);
}
}
秒杀消息接收者
/**
* 消息消费者
*/
@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
/**
* 商品秒杀
*/
@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);
}
前端goods_detail.htm页面处理
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("客户端请求异常")
}
})
}
获取秒杀结果
/**
* 获取秒杀结果
* orderId:成功,-1:失败,0:排队中
* @param user
* @param goodsId
* @return
*/
@Override
public Long getResult(User user, Long goodsId) {
// 查询订单
SeckillOrder seckillOrder = seckillOrderDao.selectOne(new QueryWrapper<SeckillOrder>()
.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;
}
}
返回秒杀结果
/**
* 获取秒杀结果
* 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);
}
test
@SpringBootTest
class SeckillSystemApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private DefaultRedisScript<Boolean> 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脚本
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
redis配置类,配置脚本
@Bean
public DefaultRedisScript<Boolean> defaultRedisScript(){
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
//lock.lua脚本的位置和application.yml同级目录
redisScript.setLocation(new ClassPathResource("lock.lua"));
//设置返回结果类型
redisScript.setResultType(Boolean.class);
return redisScript;
}
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配置类,配置脚本
@Bean
public DefaultRedisScript<Long> defaultRedisScript(){
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//lock.lua脚本的位置和application.yml同级目录
redisScript.setLocation(new ClassPathResource("stock.lua"));
//设置返回结果类型
redisScript.setResultType(Long.class);
return redisScript;
}
执行脚本
Long stock = (Long) redisTemplate.execute(redisScript, Collections.singletonList("seckillGoods:" + goodsId), Collections.EMPTY_LIST);
前端商品详情页面
修改秒杀方法,先获取秒杀路径在进行秒杀
<button class="btn btn-primary btn-block" type="button" id="buyButton" onclick="getSeckillPath()">立即秒杀</button>
前端获取秒杀路径方法
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)
}
}
})
}
前端秒杀方法
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
/**
* 获取秒杀地址
* @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
/**
* 获取秒杀地址
* @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;
}
后端校验秒杀路径
// 校验秒杀路径(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);
}
生成验证码
引入依赖https://gitee.com/lian_jianfeng/EasyCaptcha?_from=gitee_search
<!--验证码-->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
后端生成验证码
/**
* 生成验证码
* @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());
}
前端展示
<div class="row">
<div class="form-inline">
<img id="captchaImg" width="130" height="32" onclick="refreshCaptcha()" style="display: none;">
<input id="captcha" class="form-control" style="display: none;width: 100px">
<button class="btn btn-primary" type="button" id="buyButton" style="width: 200px" onclick="getSeckillPath()">立即秒杀</button>
<input type="hidden" name="goodsId" id="goodsId" />
</div>
</div>
JS渲染
// 刷新验证码
function refreshCaptcha() {
$("#captchaImg").attr("src","/seckill/captcha?goodsId="+$("#goodsId").val()+"&time="+new Date())
}
后台校验验证码
/**
* 校验验证码
* @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);
}
计数器限流
// 限制访问次数,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注解
/**
* 接口限流注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
//时间间隔
int second();
// 最大次数
int maxCount();
//是否需要登录
boolean needLogin() default true;
}
将user设置到线程ThreadLocal中
/**
* 将user设置到线程ThreadLocal中
*/
public class UserContent {
private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
//设置user到当前线程中
public static void setUser(User user){
userThreadLocal.set(user);
}
// 获取当前线程的user
public static User getUser(){
return userThreadLocal.get();
}
}
限流拦截器
/**
* 限流拦截器
*/
@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<maxCount){
// 当每访问一个就进行加一
redisTemplate.opsForValue().increment(uri+":"+user.getId());
}else {
// 当在规定的时间超过最大访问数则抛出异常
render(response,ResBeanEnum.ACCESS_LIMIT_REAHCED);
return false;
}
}
return true;
}
/**
* 构建返回对象
* @param response
* @param resBeanEnum
*/
private void render(HttpServletResponse response,ResBeanEnum resBeanEnum) throws IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
ResBean resBean = ResBean.error(resBeanEnum);
out.write(new ObjectMapper().writeValueAsString(resBean));
out.flush();
out.close();
}
/**
* 获取当前登录用户
* @param request
* @param response
* @return
*/
private User getUser (HttpServletRequest request, HttpServletResponse response){
String ticket = CookieUtil.getCookieValue(request, "userTicket");
if (StringUtils.isEmpty(ticket)) {
return null;
}
return userService.getUserByCookie(ticket, request, response);
}
}
在MVC配置类注册拦截器
@Autowired
private AccessLimitInterceptor accessLimitInterceptor;
//添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessLimitInterceptor);
}
一、项目框架搭建
1.SPringBoot环境搭建
2.集成THymeleaf
3.Mybatis-plus
二、分布式会话
1.用户登录
a.设计数据库
b.返回对象枚举类及其封装
c.明文密码二次MD5加密
d.参数校验
e.全局异常处理
2.共享session
a.SpringSession
b.Redis
三、功能开发
1.商品列表
2.商品详情
3.秒杀
4.订单详情
四、系统压测
1.JMeter使用
2.自定义变量模拟多用户
3.JMeter命令的使用
4.正式压测
a.商品列表
b.秒杀
五、页面优化
1.页面缓存
2.URL缓存
3.对象缓存
4.静态资源优化
5.CDN优化
六、接口优化
1.Redis预减库存减少数据库的访问
2.内存标记减少Redis的访问
3.Redis分布式锁
4.Redis判断是否重复抢购商品
5.RabbitMQ异步下单
a.SpringBoot整合RabbitMQ
b.topic模式
七、安全优化
1.秒杀接口地址隐藏
2.算数验证码
3.接口防刷
a.计数器
b.使用自定义注解对某个方法接口进行限流
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。