Java - springboot从零打造高并发秒杀系统 (附代码)

Java秒杀方案

全套源码链接: 

https://download.csdn.net/download/weixin_43652507/85545680

http://yes

一.课程介绍

技术点介绍

 课程介绍

 学习目标

如何设计一个秒杀系统

秒杀,对我们来说,都不是一个陌生的东西。每年的双11,618以及时下流行的直播等等。秒杀然而,这对于我们系统而言是一个巨大的考验。那么,如何才能更好地理解秒杀系统呢?我觉得作为一个程序员,你首先需要从高维度出发,从整体上思考问题。在我看来,秒杀其实主要解决两个问题,一个是并发读,一个是并发写。并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。另外,我们还要针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。 其实,秒杀的整体架构可以概括为“稳、准、快”几个关键字。

所谓“稳”,就是整个系统架构要满足高可用,流量符合预期时肯定要稳定,就是超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。然后就是“准”,就是秒杀 10 台 iPhone,那就只能成交 10 台,多一台少一台都不行。一旦库存不对,那平台就要承担损失,所以“准”就是要求保证数据的一致性。最后再看“快”,“快”其实很好理解,它就是说系统的性能要足够高,否则你怎么支撑这么大的流量呢?不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就完美了。
所以从技术角度上看“稳、准、快”,就对应了我们架构上的高可用、一致性和高性能的要求
高性能。 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。对应的方案比如
动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化一致性。 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知高可用。 现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,我们还要设计一个 PlanB 来兜底,以便在最坏情况发生时仍然能够从容应对。

二.项目搭建

创建项目

完整目录

添加依赖 

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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xxxx</groupId>
<artifactId>seckill</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>seckill</name>
 <description>seckill</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>
<!--mysql 依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--mybatis-plus 依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

修改配置文件 

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: root
 hikari:
   # 连接池名
  pool-name: DateHikariCP
   # 最小空闲连接数
  minimum-idle: 5
   # 空闲连接存活最大时间,默认600000(10分钟)
  idle-timeout: 180000
   # 最大连接数,默认10
  maximum-pool-size: 10
   # 从连接池返回的连接的自动提交
  auto-commit: true
   # 连接最大存活时间,0表示永久存活,默认1800000(30分钟)
  max-lifetime: 1800000
   # 连接超时时间,默认30000(30秒)
  connection-timeout: 30000
   # 测试连接是否可用的查询语句
  connection-test-query: SELECT 1
# Mybatis-plus配置
mybatis-plus:
 #配置Mapper映射文件
mapper-locations: classpath*:/mapper/*Mapper.xml
 # 配置MyBatis数据返回类型别名(默认别名是类名)
type-aliases-package: com.xxxx.seckill.pojo
## Mybatis SQL 打印(方法接口所在的包,不是Mapper.xml所在的包)
logging:
level:
 com.xxxx.seckill.mapper: debug

测试 

packagecom.xxxx.seckill.controller;
importorg.springframework.stereotype.Controller;
importorg.springframework.ui.Model;
importorg.springframework.web.bind.annotation.RequestMapping;

/**
*测试
*
* @author xiao pan
* @since 1.0.0
*/
@Controller
@RequestMapping("/demo")
public class DemoController {
  /**
    *测试页面跳转
    *
    * @return
    */
  @RequestMapping("/hello")
  public String hello(Modelmodel) {
      model.addAttribute("name","xxxx");
      return"hello";
   }
}

hello.html

<!DOCTYPE html>
<htmllang="en"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <metacharset="UTF-8">
    <title>测试</title>
</head>
<body>
<pth:text="'hello:'+${name}"></p>
</body>
</html>

 测试结果

添加公共结果返回对象 

RespBeanEnum.java

packagecom.xxxx.seckill.vo;
importlombok.AllArgsConstructor;
importlombok.Getter;
importlombok.ToString;

/**
*返回状态枚举
*
* @author xiao pan
* @since 1.0.0
*/
@ToString
@Getter
@AllArgsConstructor
public enum RespBeanEnum {
  //通用状态码
  SUCCESS(200,"success"),
  ERROR(500,"服务端异常"),
  //登录模块5002xx
  SESSION_ERROR(500210,"session不存在或者已经失效"),
  LOGINVO_ERROR(500211,"用户名或者密码错误"),
  MOBILE_ERROR(500212,"手机号码格式错误");
  private final Integer code;
  private final String message;
}

RespBean.java

packagecom.xxxx.seckill.vo;
importlombok.AllArgsConstructor;
importlombok.Data;
importlombok.NoArgsConstructor;

/**
*通用返回结果对象
*
* @author xiao pan
* @since 1.0.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {
    private long code;
    private String message;
    private Object obj;

/**
*成功返回结果
*/
public static RespBeansuccess() {
    return new RespBean(RespBeanEnum.SUCCESS.getCode(),
    RespBeanEnum.SUCCESS.getMessage(),null);
    }
/**
*成功返回结果
*
* @param obj
*/
public static RespBeansuccess(Objectobj) {
    return new RespBean(RespBeanEnum.SUCCESS.getCode(),
    RespBeanEnum.SUCCESS.getMessage(),obj);
    }

/**
*失败返回结果
*
* @param respBeanEnum
* @return
*/
public static RespBeanerror(RespBeanEnumrespBeanEnum) {
    return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),
    null);
    }
}

三.分布式会话

实现登录功能

两次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.6</version>
</dependency>

编写MD5工具类

MD5Util.java

packagecom.xxxx.seckill.util;
importorg.apache.commons.codec.digest.DigestUtils;
importorg.springframework.stereotype.Component;

/**
* MD5工具类
*
* @author xiao pan
* @since 1.0.0
*/
@Component
public class MD5Util {
    public static Stringmd5(String src) {
        return DigestUtils.md5Hex(src);
    }
    private static final Stringsalt="1a2b3c4d";

    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 StringformPassToDBPass(StringformPass,Stringsalt) {
        String str=""+salt.charAt(0)+salt.charAt(2)+formPass+salt.charAt(5)
        +salt.charAt(4);
        return md5(str);
    }
    public static String inputPassToDbPass(StringinputPass,StringsaltDB) {
        String formPass=inputPassToFormPass(inputPass);
        String dbPass=formPassToDBPass(formPass,saltDB);
        return dbPass;
    }
}

登录功能实现

逆向工程

首先需要通过逆向工程基于t_user表生产对应的POJO、Mapper、Service、ServiceImpl、Controller等类,项目中使用了MybatisPlus,所以逆向工程也是用了MybatisPlus提供的AutoGenerator,代码如下。具体可去官网查看

CodeGenerator.java

packagecom.xxxx.autogenerator;
import com.baomidou.mybatisplus.annotation.IdType;
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;
/**
*执行 main方法控制台输入模块表名回车自动生成对应项目目录中
*
* @author xiao pan
* @since 1.0.0
*/
public class CodeGenerator {

	public static void main(String[] args) {
//		String[] tables = new String[] {"adjustmenterror","adjustmenttime","detailedlist","employee","escortcar","escortemp","escortorg" ,
//				"fincoffers","finorgrole","finpur","function","moneymatch","moneyratio","moneysectionr","moneysubscribe","peoplefinorginfo" ,
//				"syslogger","updateinfo"};
		String[] tables = new String[] {"t_goods","t_order","t_sckill_goods","t_sckill_order"};
		String[] tablePrefixs = new String[] {"t_"};
		executeCode("com.xxx.seckill",tables,tablePrefixs);
	}
	
	private static void executeCode(String pack,String[] tables,String[] tablePrefixs) {
		// 代码生成器
		AutoGenerator mpg = new AutoGenerator();

		// 全局配置
		GlobalConfig gc = new GlobalConfig();
		// 是否覆盖已有文件
		gc.setFileOverride(true);
		// 生成文件的输出目录
		String projectPath = System.getProperty("user.dir");
		gc.setOutputDir(projectPath + "/src/main/java");
		gc.setEntityName("%sModel");
		// 开发人员
		gc.setAuthor("xiao pan");
		// 是否打开输出目录
		gc.setOpen(false);
		// 开启 BaseResultMap
		gc.setBaseResultMap(true);
		// 指定生成的主键的ID类型
		gc.setIdType(IdType.ID_WORKER);
		// 时间类型对应策略: 只使用 java.util.date 代替
		gc.setDateType(DateType.ONLY_DATE);
		mpg.setGlobalConfig(gc);

		// 数据源配置
		DataSourceConfig dsc = new DataSourceConfig();
		// 从试图获取
		//dsc.setSchemaName("V_LAW_CAMERA");

//		dsc.setUrl("jdbc:mysql://localhost:3306/test");
//		dsc.setDriverName("com.mysql.jdbc.Driver");
//		dsc.setUsername("root");
//		dsc.setPassword("test");
		dsc.setUrl("jdbc:mysql://localhost/seckill");
		dsc.setDriverName("com.mysql.jdbc.Driver");
		dsc.setUsername("root");
		dsc.setPassword("admin");
		mpg.setDataSource(dsc);

		// 包配置
		PackageConfig pc = new PackageConfig();
		// 父包名。如果为空,将下面子包名必须写全部, 否则就只需写子包名
		pc.setParent(pack);
		// Entity包名
		pc.setEntity("pojo");
		mpg.setPackageInfo(pc);

		// 自定义配置
		InjectionConfig cfg = new InjectionConfig() {
			@Override
			public void initMap() {
				// to do nothing
			}
		};
		List<FileOutConfig> focList = new ArrayList<>();
		focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
			@Override
			public String outputFile(TableInfo tableInfo) {
				// 自定义输入文件名称
				if (StringUtils.isEmpty(pc.getModuleName())) {
					return projectPath + "/src/main/resources/mapper/" + tableInfo.getXmlName() + StringPool.DOT_XML;
				}else {
					return projectPath + "/src/main/resources/mapper/" + pc.getModuleName() + "/" + tableInfo.getXmlName() + StringPool.DOT_XML;
				}
			}
		});
		cfg.setFileOutConfigList(focList);
		mpg.setCfg(cfg);
		mpg.setTemplate(new TemplateConfig().setXml(null));

		// 策略配置
		StrategyConfig strategy = new StrategyConfig();
		// 数据库表映射到实体的命名策略: 下划线转驼峰命名
		strategy.setNaming(NamingStrategy.underline_to_camel);
		// 数据库表字段映射到实体的命名策略: 下划线转驼峰命名
		strategy.setColumnNaming(NamingStrategy.underline_to_camel);
		// 【实体】是否为lombok模型(默认 false)
		strategy.setEntityLombokModel(true);
		// 需要包含的表名,允许正则表达式(与exclude二选一配置)
		strategy.setInclude(tables);
		// 驼峰转连字符
		strategy.setControllerMappingHyphenStyle(true);
		// 表前缀
		strategy.setTablePrefix(tablePrefixs);
		mpg.setStrategy(strategy);
		mpg.setTemplateEngine(new FreemarkerTemplateEngine());
		mpg.execute();
	}

}

ValidatorUtil

package com.xxx.seckill.utils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 校验工具类
 */
public class ValidatorUtil {

    /**
     * 手机校验
     *
     * @param phone
     * @return
     */
    public static boolean isMobile(String phone) {
        String regex = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$";
        if (phone.length() != 11) {
            return false;
        } else {
            Pattern p = Pattern.compile(regex);
            Matcher m = p.matcher(phone);
            boolean isMatch = m.matches();
            return isMatch;
        }
    }
}

LoginController

packagecom.xxxx.seckill.controller;
importcom.xxxx.seckill.service.IUserService;
importcom.xxxx.seckill.vo.LoginVo;
importcom.xxxx.seckill.vo.RespBean;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Controller;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.ResponseBody;

/**
*登录
*
* @author xiao pan
* @since 1.0.0
*/
@Controller
@RequestMapping("/login")
@Slf4j
public class LoginController {
  @Autowired
  private IUserService userService;
  /**
    *跳转登录页
    *
    * @return
    */
  @RequestMapping("/toLogin")
  public String toLogin() {
      return"login";
   }
/**
    *登录
    * @return
    */
  @RequestMapping("/doLogin")
  @ResponseBody
  public RespBean doLogin(LoginVologinVo) {
      log.info(loginVo.toString());
      return userService.login(loginVo);
   }
}

IUserService

packagecom.xxxx.seckill.service;
importcom.baomidou.mybatisplus.extension.service.IService;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.vo.LoginVo;
importcom.xxxx.seckill.vo.RespBean;

/**
* <p>
*服务类
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
public interface IUserService extends IService<User> {

    /**
    *登录
    * @param loginVo
    * @return
    */
    RespBean login(LoginVo loginVo);
}

UserServiceImpl

packagecom.xxxx.seckill.service.impl;
importcom.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
importcom.xxxx.seckill.mapper.UserMapper;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.service.IUserService;
importcom.xxxx.seckill.util.MD5Util;
importcom.xxxx.seckill.util.ValidatorUtil;
importcom.xxxx.seckill.vo.LoginVo;
importcom.xxxx.seckill.vo.RespBean;
importcom.xxxx.seckill.vo.RespBeanEnum;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Service;
importorg.springframework.util.StringUtils;

/**
* <p>
*服务实现类
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User>implements IUserService {
  @Autowired
  private UserMapper userMapper;

 /**
    *登录
    * @param loginVo
    * @return
    */
  @Override
  public RespBean login(LoginVo loginVo) {
      String mobile=loginVo.getMobile();
      String password=loginVo.getPassword();
      if (StringUtils.isEmpty(mobile)||StringUtils.isEmpty(password)){
        return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
      }
      if (!ValidatorUtil.isMobile(mobile)){
        return RespBean.error(RespBeanEnum.MOBILE_ERROR);
      }
      //根据手机号获取用户
      User user=userMapper.selectById(mobile);
      if (null==user){
        return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
      }
      //校验密码
      if
(!MD5Util.formPassToDBPass(password,user.getSalt()).equals(user.getPassword())){
        return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
      }
      return RespBean.success();
   }
}

login.html

<!DOCTYPE html>
<htmllang="en"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <metacharset="UTF-8">
    <title>登录</title>
    <!-- jquery -->
    <scripttype="text/javascript"th:src="@{/js/jquery.min.js}"></script>
    <!-- bootstrap -->
    <linkrel="stylesheet"type="text/css"
th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <scripttype="text/javascript"th:src="@{/bootstrap/js/bootstrap.min.js}">
</script>
    <!-- jquery-validator -->
    <scripttype="text/javascript"th:src="@{/jquery-
validation/jquery.validate.min.js}"></script>
    <scripttype="text/javascript"th:src="@{/jquery-
validation/localization/messages_zh.min.js}"></script>
    <!-- layer -->
    <scripttype="text/javascript"th:src="@{/layer/layer.js}"></script>
    <!-- md5.js -->
    <scripttype="text/javascript"th:src="@{/js/md5.min.js}"></script>
    <!-- common.js -->
    <scripttype="text/javascript"th:src="@{/js/common.js}"></script>
</head>
<body>
<formname="loginForm"id="loginForm"method="post"style="width:50%; margin:0
auto">
 <h2style="text-align:center; margin-bottom: 20px">用户登录</h2>
    <divclass="form-group">
        <divclass="row">
            <labelclass="form-label col-md-4">请输入手机号码</label>
            <divclass="col-md-5">
                <inputid="mobile"name="mobile"class="form-control"
type="text"placeholder="手机号码"required="true"
                      minlength="11"maxlength="11"/>
            </div>
            <divclass="col-md-1">
            </div>
        </div>
    </div>
    <divclass="form-group">
        <divclass="row">
            <labelclass="form-label col-md-4">请输入密码</label>
            <divclass="col-md-5">
                <inputid="password"name="password"class="form-control"
type="password"placeholder="密码"
                      required="true"minlength="6"maxlength="16"/>
            </div>
        </div>
    </div>
    <divclass="row">
        <divclass="col-md-5">
            <buttonclass="btn btn-primary btn-block"type="reset"
onclick="reset()">重置</button>
        </div>
        <divclass="col-md-5">
            <buttonclass="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);
        varpassword=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("成功");
                }else {
                    layer.msg(data.message);
                }
            },
            error:function () {
                layer.closeAll();
            }
        });
    }
</script>
</html>

测试

手机号码格式不正确

手机号码或者密码不正确

正确登录

参数校验

每个类都写大量的健壮性判断过于麻烦,我们可以使用validation简化我们的代码

添加依赖

pom.xml

<!-- validation组件 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

 定义手机号码验证规则

IsMobileValidator.java

package com.xxx.seckill.validator;

import com.xxx.seckill.utils.ValidatorUtil;
import com.xxx.seckill.validator.IsMobile;
import org.apache.commons.lang.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * 手机号码校验规则
 */
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 value, ConstraintValidatorContext constraintValidatorContext) {
        if (required) {
            return ValidatorUtil.isMobile(value);
        } else {
            if (StringUtils.isEmpty(value)) {
                return true;
            } else {
                return ValidatorUtil.isMobile(value);
            }
        }
    }
}

自定义注解

IsMobile.java

packagecom.xxxx.seckill.validator;
import com.xxx.seckill.vo.IsMobileValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

/**
 * 验证手机号
 */
@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

packagecom.xxxx.seckill.vo;
importcom.xxxx.seckill.validator.IsMobile;
importlombok.AllArgsConstructor;
importlombok.Data;
importlombok.NoArgsConstructor;
importorg.hibernate.validator.constraints.Length;
importjavax.validation.constraints.NotNull;

/**
*登录入参
*
* @author xiao pan
* @since 1.0.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
publicclassLoginVo {

  @NotNull
  @IsMobile
  private String mobile;

  @NotNull
  @Length(min=32)
  private String password;
}

其他修改

LoginController

入参添加@Valid

/**
*登录
* @return
*/
@RequestMapping("/doLogin")
@ResponseBody
public RespBean doLogin(@Valid LoginVo loginVo) {
  log.info(loginVo.toString());
  return userService.login(loginVo);
}

UserServiceImpl

注释掉之前的健壮性判断即可

/**
*登录
* @param loginVo
* @return
*/
@Override
public RespBean login(LoginVo loginVo) {
    String mobile=loginVo.getMobile();
    String password=loginVo.getPassword();
  // if (StringUtils.isEmpty(mobile)||StringUtils.isEmpty(password)){
  //     return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
  // }
  // if (!ValidatorUtil.isMobile(mobile)){
  //     return RespBean.error(RespBeanEnum.MOBILE_ERROR);
  // }
  //根据手机号获取用户
  User user=userMapper.selectById(mobile);
  if (null==user){
      return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
   }
  //校验密码
  if
(!MD5Util.formPassToDBPass(password,user.getSalt()).equals(user.getPassword())){
      return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
   }
  return RespBean.success();
}

测试

异常处理

我们知道,系统中异常包括:编译时异常和运行时异常RuntimeException,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。在开发中,不管是dao层、service层还是controller层,都有可能抛出异常,在Springmvc中,能将所有类型的异常处理从各处理过程解耦出来,既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护。SpringBoot全局异常处理方式主要两种: 

使用@ControllerAdvice和@ExceptionHandler注解。
使用ErrorController类来实现
区别:

1.@ControllerAdvice方式只能处理控制器抛出的异常。此时请求已经进入控制器中。

2.ErrorController类方式可以处理所有的异常,包括未进入控制器的错误,比如404,401等错误

3.如果应用中两者共同存在,则@ControllerAdvice方式处理控制器抛出的异常,ErrorController类方式处理未进入控制器的异常。

4.@ControllerAdvice方式可以定义多个拦截方法,拦截不同的异常类,并且可以获取抛出的异常,自由度更大。

GlobalException

package com.xxx.seckill.exception;

import com.xxx.seckill.vo.RespBeanEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;


/**
 * 全局异常
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GlobalException extends RuntimeException{

    private RespBeanEnum respBeanEnum;
}

GlobalExceptionHandler

package com.xxx.seckill.exception;


import com.xxx.seckill.vo.RespBean;
import com.xxx.seckill.vo.RespBeanEnum;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理类
 */
@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
* @return
*/
@Override
public RespBean login(LoginVologinVo) {
  String mobile=loginVo.getMobile();
  String password=loginVo.getPassword();
  //根据手机号获取用户
  User user=userMapper.selectById(mobile);
  if (null==user){
      throw new GlobalException(RespBeanEnum.LOGINVO_ERROR);
   }
  //校验密码
  if
(!MD5Util.formPassToDBPass(password,user.getSalt()).equals(user.getPassword())){
      throw new GlobalException(RespBeanEnum.LOGINVO_ERROR);
   }
  return RespBean.success();
}

测试

 分布式Session

完善登录功能

使用cookie+session记录用户信息

准备工具类

CookieUtil.java

package com.xxx.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;

/**
 * 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,
                false);
    }

    /**
     *   * 设置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;
        // 通过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[] ary = domainName.split("\\:");
            domainName = ary[0];
        }
        return domainName;
    }
}

UUIDUtil.java

package com.xxx.seckill.utils;


import java.util.UUID;

/**
 * UUID工具类
 */
public class UUIDUtil {
    public static String uuid() {
        return UUID.randomUUID().toString().replace("-", "");
    }
}

IUserService

packagecom.xxxx.seckill.service;
importcom.baomidou.mybatisplus.extension.service.IService;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.vo.LoginVo;
importcom.xxxx.seckill.vo.RespBean;
importjavax.servlet.http.HttpServletRequest;
importjavax.servlet.http.HttpServletResponse;

/**
* <p>
*服务类
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
public interface IUserService extends IService<User> {
  /**
    *登录
    * @param loginVo
    * @return
    */
  RespBean login(HttpServletRequest request,HttpServletResponse response,LoginVo loginVo);
}

UserServiceImpl

/**
*登录
* @param loginVo
* @return
*/
@Override
public RespBean login(HttpServletRequest request,HttpServletResponse response,LoginVo loginVo) {
  String mobile=loginVo.getMobile();
  String password=loginVo.getPassword();
  //根据手机号获取用户
  User user=userMapper.selectById(mobile);
  if (null==user){
      throw new GlobalException(RespBeanEnum.LOGINVO_ERROR);
   }
  //校验密码
  if
(!MD5Util.formPassToDBPass(password,user.getSalt()).equals(user.getPassword())){
      throw new GlobalException(RespBeanEnum.LOGINVO_ERROR);
   }
  //生成cookie
  String ticket=UUIDUtil.uuid();
  request.getSession().setAttribute(ticket,user);
  CookieUtil.setCookie(request,response,"userTicket",ticket);
  return RespBean.success(ticket);
}

LoginController

/**
*登录
*
* @return
*/
@RequestMapping("/doLogin")
@ResponseBody
public RespBean doLogin(HttpServletRequest request,HttpServletResponse
response,@Valid LoginVo loginVo) {
  log.info(loginVo.toString());
  return userService.login(request,response,loginVo);
}

GoodsController

packagecom.xxxx.seckill.controller;
importcom.xxxx.seckill.pojo.User;
importorg.springframework.stereotype.Controller;
importorg.springframework.ui.Model;
importorg.springframework.util.StringUtils;
importorg.springframework.web.bind.annotation.CookieValue;
importorg.springframework.web.bind.annotation.RequestMapping;
importjavax.servlet.http.HttpSession;

/**
 * 商品
 *
 * @author xiao pan
 * @since 1.0.0
 */
@Controller
@RequestMapping("/goods")
public class GoodsController {
    /**
     * 跳转登录页
     *
     * @return
     */
    @RequestMapping("/toList")
    publicStringtoLogin(HttpSessionsession, Modelmodel,
                        @CookieValue("userTicket")Stringticket) {
        if (StringUtils.isEmpty(ticket)) {
            return "login";
        }
        Useruser = (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>
<htmllang="en"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <metacharset="UTF-8">
    <title>商品列表</title>
</head>
<body>
<pth:text="'hello:'+${user.username}"></p>
</body>
</html>

测试

分布式Session问题

之前的代码在我们之后一台应用系统,所有操作都在一台Tomcat上,没有什么问题。当我们部署多台系统,配合Nginx的时候会出现用户登录的问题 

原因
由于 Nginx使用默认负载均衡策略(轮询),请求将会按照时间顺序逐一分发到后端应用上。
也就是说刚开始我们在 Tomcat1登录之后,用户信息放在 Tomcat1的Session里。过了一会,请求
又被 Nginx分发到了 Tomcat2上,这时 Tomcat2上Session里还没有用户信息,于是又要登录。

解决方案:

Session复制

        优点
                无需修改代码,只需要修改Tomcat配置
        缺点

                Session同步传输占用内网带宽
                多台Tomcat同步性能指数级下降
                Session占用内存,无法有效水平扩展
前端存储
        优点
                不占用服务端内存
        缺点
                存在安全风险
                数据大小受cookie限制
                占用外网带宽
Session粘滞
        优点
                无需修改代码
                服务端可以水平扩展
        缺点
                增加新机器,会重新Hash,导致重新登录
                应用重启,需要重新登录
后端集中存储
        优点
                安全
                容易水平扩展
        缺点
                增加复杂度
                需要修改代码 

Redis安装 

下载地址 Redis

将下载好的安装包上传至服务器

解压

tar zxvf redis-5.0.3.tar.gz

安装依赖

yum-y install gcc-c++ autoconf automake

预编译

#切换到解压目录
cd redis-5.0.5/
#预编译
make

安装

#创建安装目录
mkdir-p /usr/local/redis
#安装
makePREFIX=/usr/local/redis/ install

修改配置文件

        #复制redis.conf至安装路径下
        cp redis.conf /usr/local/redis/bin/
        #修改配置文件
        vim /usr/local/redis/bin/redis.conf

修改内容如下

        #方便学习,注释掉该行。可以使所有ip访问redis
        #bind 127.0.0.1
        #关闭保护模式
        protected-mode no
        #后台启动
        daemonize yes
        #添加访问认证
        requirepass root 

 启动redis

        ./redis-server redis.conf

Redis实现分布式Session

方法一:使用SpringSession实现

添加依赖

<!-- 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

spring:
  redis:
    #超时时间
    timeout:10000ms
    #服务器地址
    host:192.168.10.100
    #服务器端口
    port:6379
    #数据库
    database:0
    #密码
    password:root
    lettuce:
      pool:
        #最大连接数,默认8
        max-active:1024
        #最大连接阻塞等待时间,默认-1
        max-wait:10000ms
        #最大空闲连接
        max-idle:200
        #最小空闲连接
        min-idle:5

测试
其余代码暂时不动,重新登录测试。会发现session已经存储在Redis上

方法二:将用户信息存入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>

 添加配置

application.yml

spring:
  redis:
    #超时时间
    timeout:10000ms
    #服务器地址
    host:192.168.10.100
    #服务器端口
    port:6379
    #数据库
    database:0
    #密码
    password:root
    lettuce:
      pool:
        #最大连接数,默认8
        max-active:1024
        #最大连接阻塞等待时间,默认-1
        max-wait:10000ms
        #最大空闲连接
        max-idle:200
        #最小空闲连接
        min-idle:5

RedisConfig.java

package com.xxx.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;

/**
 * redis配置类
 */
@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;
    }
}

工具类

JsonUtil.java

package com.xxx.seckill.utils;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.util.List;

/**
 * json工具类
 */
public class JsonUtil {

    private static ObjectMapper objectMapper = new ObjectMapper();


    /**
     * 将对象转换成json字符串
     * @param obj
     * @return
     */
    public static String object2JsonStr(Object obj){
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将字符串转换成对象
     * @param jsonStr
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T jsonStr2object(String jsonStr, Class<T> clazz){
        try {
            return objectMapper.readValue(jsonStr.getBytes("UTF-8"),clazz);
        } catch (JsonParseException e){
            e.printStackTrace();
        }catch (JsonMappingException e){
            e.printStackTrace();
        }catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json数据转换成pojo对象list
     * @param jsonStr
     * @param beanType
     * @param <T>
     * @return
     */
    public static <T>List<T> jsonToList(String jsonStr, Class<T> beanType){
        JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, beanType);
        try {
            List<T> list = objectMapper.readValue(jsonStr,javaType);
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

修改之前代码

IUserService.java

/**
*根据cookie获取用户
* @param userTicket
* @param request
* @param response
* @return
*/
User getByUserTicket(String userTicket,HttpServletRequest
request,HttpServletResponse response);

UserServiceImpl.java

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author xiao pan
 * @since 1.0.0
 */

import com.xxx.seckill.service.IUserService;
import com.xxx.seckill.utils.CookieUtil;
import com.xxx.seckill.utils.JsonUtil;
import com.xxx.seckill.utils.UUIDUtil;
import com.xxx.seckill.vo.RespBeanEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Service
public class UserService Implextends ServiceImpl<UserMapper, User>implements IUserService{
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 登录
     *
     * @param loginVo
     * @return
     */
    @Override
    public RespBean login(HttpServletRequest request,HttpServletRe sponse
        response,LoginVo loginVo){
        String mobile=loginVo.getMobile();
        String password=loginVo.getPassword();
        //根据手机号获取用户
        User user=userMapper.selectById(mobile);
        if(null==user){
        throw new GlobalException(RespBeanEnum.LOGINVO_ERROR);
        }
        //校验密码
        if(!MD5Util.formPassToDBPass(password,
        user.getSalt()).equals(user.getPassword())){
        throw new GlobalException(RespBeanEnum.LOGINVO_ERROR);
        }
        //生成cookie
        String ticket=UUIDUtil.uuid();
        redisTemplate.opsForValue().set("user:"+ticket,
        JsonUtil.object2JsonStr(user));
        CookieUtil.setCookie(request,response,"userTicket",ticket);
        return RespBean.success(ticket);
        }

    /**
     * 根据cookie获取用户
     *
     * @param userTicket
     * @param request
     * @param response
     * @return
     */
    @Override
    public User getByUserTicket(String userTicket,HttpServletRequest request,
        HttpServletResponse response){
        if(StringUtils.isEmpty(userTicket)){
            return null;
        }
        String userJson=(String)redisTemplate.opsForValue().get("user:"+
        userTicket);
        User user=JsonUtil.jsonStr2Object(userJson,User.class);
        if(null!=user){
            CookieUtil.setCookie(request,response,"userTicket",userTicket);
        }
            return user;
        }
}

GoodsController.java

packagecom.xxxx.seckill.controller;
importcom.xxxx.seckill.pojo.User;
importorg.springframework.stereotype.Controller;
importorg.springframework.ui.Model;
importorg.springframework.util.StringUtils;
importorg.springframework.web.bind.annotation.CookieValue;
importorg.springframework.web.bind.annotation.RequestMapping;
importjavax.servlet.http.HttpSession;

/**
 * 商品
 *
 * @author xiao pan
 * @since 1.0.0
 */
@Controller
@RequestMapping("/goods")
public class GoodsController {
    @Autowired
    private IUserService userService;

    /**
     * 跳转登录页
     *
     * @return
     */
    @RequestMapping("/toList")
    public String toLogin(HttpServletRequest request, HttpServletResponse response, Model model, @CookieValue("userTicket") String ticket) {
        if (StringUtils.isEmpty(ticket)) {
            return "login";
        }
        User user = userService.getByUserTicket(ticket, request, response);
        if (null == user) {
            return "login";
        }
        model.addAttribute("user", user);
        return "goodsList";
    }
}

测试

优化登录功能 

UserArgumentResolver.java

packagecom.xxxx.seckill.config;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.service.IUserService;
importcom.xxxx.seckill.util.CookieUtil;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.core.MethodParameter;
importorg.springframework.stereotype.Component;
importorg.springframework.util.StringUtils;
importorg.springframework.web.bind.support.WebDataBinderFactory;
importorg.springframework.web.context.request.NativeWebRequest;
importorg.springframework.web.method.support.HandlerMethodArgumentResolver;
importorg.springframework.web.method.support.ModelAndViewContainer;
importjavax.servlet.http.HttpServletRequest;
importjavax.servlet.http.HttpServletResponse;

@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
    @Autowired
    private IUserService userService;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?>clazz=parameter.getParameterType();
        return clazz==User.class;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                WebDataBinderFactory binderFactory)throwsException {
        HttpServletRequest request= webRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response= webRequest.getNativeResponse(HttpServletResponse.class);
        String ticket= CookieUtil.getCookieValue(request,"userTicket");
        if (StringUtils.isEmpty(ticket)) {
            returnnull;
        }
        returnu serService.getByUserTicket(ticket,request,response);
    }
}

WebConfig.java

packagecom.xxxx.seckill.config;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.web.method.support.HandlerMethodArgumentResolver;
importorg.springframework.web.servlet.config.annotation.EnableWebMvc;
importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;
importjava.util.List;

/**
* MVC配置类
*
* @author xiao pan
* @since 1.0.0
*/
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
  @Autowired
  private UserArgumentResolver userArgumentResolver;
  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver>
resolvers) {
      resolvers.add(userArgumentResolver);
   }
}

GoodsController.java

packagecom.xxxx.seckill.controller;
importcom.xxxx.seckill.pojo.User;
importorg.springframework.stereotype.Controller;
importorg.springframework.ui.Model;
importorg.springframework.web.bind.annotation.RequestMapping;

/**
*商品
*
* @author xiao pan
* @since 1.0.0
*/
@Controller
@RequestMapping("/goods")
public class GoodsController {
  /**
    *跳转登录页
    *
    * @return
    */
  @RequestMapping("/toList")
  public String toLogin(Model model,User user) {
      model.addAttribute("user",user);
      return"goodsList";
   }
}

四.秒杀功能

商品列表页

用逆向工程生成所需的所有类

GoodsVo

同时查询商品表和秒杀商品表的返回对象

packagecom.xxxx.seckill.vo;
importcom.xxxx.seckill.pojo.Goods;
importlombok.AllArgsConstructor;
importlombok.Data;
importlombok.NoArgsConstructor;
importjava.math.BigDecimal;
importjava.util.Date;

/**
*商品返回对象
*
* @author xiao pan
* @since 1.0.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GoodsVo extends Goods {
  private BigDecimal seckillPrice;
  private Integer stockCount;
  private Date startDate;
  private Date endDate;
}

GoodsMapper

GoodsMapper.java

packagecom.xxxx.seckill.mapper;
importcom.baomidou.mybatisplus.core.mapper.BaseMapper;
importcom.xxxx.seckill.pojo.Goods;
importcom.xxxx.seckill.vo.GoodsVo;
importjava.util.List;

/**
* <p>
*  Mapper接口
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
public interface GoodsMapper extends BaseMapper<Goods> {
  /**
    *获取商品列表
    * @return
    */
  List<GoodsVo> findGoodsVo();
}

GoodsMapper.xml

<!--获取商品列表 -->
<selectid="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 AS g
      LEFT JOIN t_seckill_goods AS sg ON g.id = sg.goods_id
</select>

GoodsService

IGoodService.java

packagecom.xxxx.seckill.service;
importcom.baomidou.mybatisplus.extension.service.IService;
importcom.xxxx.seckill.pojo.Goods;
importcom.xxxx.seckill.vo.GoodsVo;
importjava.util.List;

/**
* <p>
*服务类
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
public interface IGoodsService extends IService<Goods> {
  /**
    *获取商品列表
    * @return
    */
  List<GoodsVo> findGoodsVo();
}

GoodsServiceImpl.java

packagecom.xxxx.seckill.service.impl;
importcom.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
importcom.xxxx.seckill.mapper.GoodsMapper;
importcom.xxxx.seckill.pojo.Goods;
importcom.xxxx.seckill.service.IGoodsService;
importcom.xxxx.seckill.vo.GoodsVo;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Service;
importjava.util.List;

/**
* <p>
*服务实现类
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper,Goods>implements
IGoodsService {
    @Autowired
    private GoodsMapper goodsMapper;
    /**
    *获取商品列表
    * @return
    */
    @Override
    public List<GoodsVo> findGoodsVo() {
        return goodsMapper.findGoodsVo();
    }
}

GoodsController

packagecom.xxxx.seckill.controller;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.service.IGoodsService;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Controller;
importorg.springframework.ui.Model;
importorg.springframework.web.bind.annotation.RequestMapping;
/**
*商品
*
* @author xiao pan
* @since 1.0.0
*/
@Controller
@RequestMapping("/goods")
public class GoodsController {
    @Autowired
    private IGoodsService goodsService;
    /**
    *跳转商品列表页
    *
    * @return
    */
  @RequestMapping("/toList")
  public String toLogin(Model model,User user) {
      model.addAttribute("user",user);
      model.addAttribute("goodsList",goodsService.findGoodsVo());
      return"goodsList";
   }
}

MvcConfig

如果出现图片无法访问的情况需要修改此配置类。否则无需修改此配置类

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
    }

goodsList.html

<!DOCTYPE html>
<htmllang="en"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <metacharset="UTF-8">
    <title>商品列表</title>
    <!-- jquery -->
    <scripttype="text/javascript"th:src="@{/js/jquery.min.js}"></script>
    <!-- bootstrap -->
    <linkrel="stylesheet"type="text/css"
th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <scripttype="text/javascript"th:src="@{/bootstrap/js/bootstrap.min.js}">
</script>
    <!-- layer -->
    <scripttype="text/javascript"th:src="@{/layer/layer.js}"></script>
    <!-- common.js -->
    <scripttype="text/javascript"th:src="@{/js/common.js}"></script>
</head>
<body>
<divclass="panel panel-default">
    <divclass="panel-heading">秒杀商品列表</div>
    <tableclass="table"id="goodslist">
        <tr>
            <td>商品名称</td>
            <td>商品图片</td>
            <td>商品原价</td>
            <td>秒杀价</td>
            <td>库存数量</td>
            <td>详情</td>
        </tr>
        <trth:each="goods,goodsStat : ${goodsList}">
            <tdth:text="${goods.goodsName}"></td>
            <td><imgth:src="@{${goods.goodsImg}}"width="100"height="100"/>
</td>
            <tdth:text="${goods.goodsPrice}"></td>
            <tdth:text="${goods.seckillPrice}"></td>
            <tdth:text="${goods.stockCount}"></td>
            <td><ath:href="'/goods/toDetail/'+${goods.id}">详情</a></td>
        </tr>
    </table>
</div>
</body>

测试

 商品详情页

GoodsMapper

GoodsMapper.java

/**
*根据商品id获取商品详情
* @param goodsId
* @return
*/
GoodsVo findGoodsVoByGoodsId(Long goodsId);

GoodsMapper.xml

<!--根据商品id获取商品详情 -->
<selectid="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 AS g
         LEFT JOIN t_seckill_goods AS sg ON g.id = sg.goods_id
       WHERE g.id = #{goodsId}
</select>

GoodsService 

IGoodsService.java

/**
*根据商品id获取商品详情
* @param goodsId
* @return
*/
GoodsVo findGoodsVoByGoodsId(Long goodsId);

GoodsServiceImpl.java

/**
*根据商品id获取商品详情
* @param goodsId
* @return
*/
@Override
public GoodsVo findGoodsVoByGoodsId(Long goodsId) {
  return goodsMapper.findGoodsVoByGoodsId(goodsId);
}

GoodsControlle

/**
*跳转商品详情页
*
* @param model
* @param user
* @param goodsId
* @return
*/
@RequestMapping("/toDetail/{goodsId}")
public String toDetail(Model model,User user,@PathVariable Long goodsId) {
  model.addAttribute("user",user);
  GoodsVo goods=goodsService.findGoodsVoByGoodsId(goodsId);
  model.addAttribute("goods",goods);
  Date startDate=goods.getStartDate();
  Date endDate=goods.getEndDate();
  Date nowDate=newDate();
  //秒杀状态
  int secKillStatus=0;
  //剩余开始时间
  int remainSeconds=0;
  //秒杀还未开始
  if (nowDate.before(startDate)) {
      remainSeconds= (int) ((startDate.getTime()-nowDate.getTime())/1000);
  //秒杀已结束
   }elseif (nowDate.after(endDate)) {
      secKillStatus=2;
      remainSeconds=-1;
  //秒杀中
   }else {
      secKillStatus=1;
      remainSeconds=0;
   }
  model.addAttribute("secKillStatus",secKillStatus);
  model.addAttribute("remainSeconds",remainSeconds);
  return"goodsDetail";
}

goodsDetail.html

<!DOCTYPE html>
<htmllang="en"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <metacharset="UTF-8">
    <title>商品详情</title>
    <!-- jquery -->
    <scripttype="text/javascript"th:src="@{/js/jquery.min.js}"></script>
    <!-- bootstrap -->
    <linkrel="stylesheet"type="text/css"
th:href="@{/bootstrap/css/bootstrap.min.css}"/>
 <scripttype="text/javascript"th:src="@{/bootstrap/js/bootstrap.min.js}">
</script>
    <!-- layer -->
    <scripttype="text/javascript"th:src="@{/layer/layer.js}"></script>
    <!-- common.js -->
    <scripttype="text/javascript"th:src="@{/js/common.js}"></script>
</head>
<body>
<divclass="panel panel-default">
    <divclass="panel-heading">秒杀商品详情</div>
    <divclass="panel-body">
        <spanth:if="${user eq null}">您还没有登录,请登陆后再操作<br/></span>
        <span>没有收货地址的提示。。。</span>
    </div>
    <tableclass="table"id="goods">
        <tr>
            <td>商品名称</td>
            <tdcolspan="3"th:text="${goods.goodsName}"></td>
        </tr>
        <tr>
            <td>商品图片</td>
            <tdcolspan="3"><imgth:src="@{${goods.goodsImg}}"width="200"
height="200"/></td>
        </tr>
        <tr>
            <td>秒杀开始时间</td>
            <tdth:text="${#dates.format(goods.startDate, 'yyyy-MM-dd
HH:mm:ss')}"></td>
            <tdid="seckillTip">
                <inputtype="hidden"id="remainSeconds"
th:value="${remainSeconds}"/>
                <spanth:if="${secKillStatus eq 0}">秒杀倒计时:<span
id="countDown"
                                                              
 th:text="${remainSeconds}"></span>秒</span>
                <spanth:if="${secKillStatus eq 1}">秒杀进行中</span>
                <spanth:if="${secKillStatus eq 2}">秒杀已结束</span>
            </td>
            <td>
<formid="seckillForm"method="post"
action="/seckill/doSeckill">
                    <buttonclass="btn btn-primary btn-block"type="submit"
id="buyButton">立即秒杀</button>
                    <inputtype="hidden"name="goodsId"th:value="${goods.id}"/>
                </form>
            </td>
        </tr>
        <tr>
            <td>商品原价</td>
            <tdcolspan="3"th:text="${goods.goodsPrice}"></td>
        </tr>
        <tr>
            <td>秒杀价</td>
            <tdcolspan="3"th:text="${goods.seckillPrice}"></td>
        </tr>
        <tr>
            <td>库存数量</td>
            <tdcolspan="3"th:text="${goods.stockCount}"></td>
</tr>
    </table>
</div>
</body>
<script>
    $(function(){
        countDown();
    });
    functioncountDown(){
        varremainSeconds=$("#remainSeconds").val();
        vartimeout;
        //秒杀还没开始,倒计时
        if(remainSeconds>0){
            $("#buyButton").attr("disabled",true);
            timeout=setTimeout(function(){
                $("#countDown").text(remainSeconds-1);
                $("#remainSeconds").val(remainSeconds-1);
                countDown();
            },1000);
            //秒杀进行中
        }elseif(remainSeconds==0){
            $("#buyButton").attr("disabled",false);
            if(timeout){
                clearTimeout(timeout);
            }
            $("#seckillTip").html("秒杀进行中");
            //秒杀已经结束
        }else{
            $("#buyButton").attr("disabled",true);
            $("#seckillTip").html("秒杀已经结束");
        }
    }
</script>
</html>

测试

秒杀未开始

秒杀进行中

秒杀已结束 

秒杀功能实现 

OrderService

IOrderService.java

packagecom.xxxx.seckill.service;
importcom.baomidou.mybatisplus.extension.service.IService;
importcom.xxxx.seckill.pojo.Order;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.vo.GoodsVo;

/**
* <p>
*服务类
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
public interface IOrderService extends IService<Order> {
  /**
    *秒杀
    * @param user
    * @param goods
    * @return
    */
  Order seckill(User user,GoodsVo goods);
}

OrderServiceImpl.java

packagecom.xxxx.seckill.service.impl;
importcom.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
importcom.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
importcom.xxxx.seckill.mapper.OrderMapper;
importcom.xxxx.seckill.pojo.Order;
importcom.xxxx.seckill.pojo.SeckillGoods;
importcom.xxxx.seckill.pojo.SeckillOrder;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.service.IGoodsService;
importcom.xxxx.seckill.service.IOrderService;
importcom.xxxx.seckill.service.ISeckillGoodsService;
importcom.xxxx.seckill.service.ISeckillOrderService;
importcom.xxxx.seckill.vo.GoodsVo;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Service;
importorg.springframework.transaction.annotation.Transactional;
importjava.util.Date;

/**
* <p>
*服务实现类
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper,Order>implements
IOrderService {
  @Autowired
  private ISeckillGoodsService seckillGoodsService;
  @Autowired
  private IGoodsService goodsService;
  @Autowired
  private OrderMappe rorderMapper;
  @Autowired
  private ISeckillOrderService seckillOrderService;
    /**
    *秒杀
    * @param user
    * @param goods
    * @return
    */
  @Override
  @Transactional
  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(newDate());
      orderMapper.insert(order);
      //生成秒杀订单
      SeckillOrder seckillOrder=new SeckillOrder();
      seckillOrder.setOrderId(order.getId());
      seckillOrder.setUserId(user.getId());
      seckillOrder.setGoodsId(goods.getId());
      seckillOrderService.save(seckillOrder);
      return order;
   }
}

SeckillController

packagecom.xxxx.seckill.controller;
importcom.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
importcom.xxxx.seckill.pojo.Order;
importcom.xxxx.seckill.pojo.SeckillOrder;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.service.IGoodsService;
importcom.xxxx.seckill.service.IOrderService;
importcom.xxxx.seckill.service.ISeckillOrderService;
importcom.xxxx.seckill.vo.GoodsVo;
importcom.xxxx.seckill.vo.RespBeanEnum;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Controller;
importorg.springframework.ui.Model;
importorg.springframework.web.bind.annotation.RequestMapping;

/**
* <p>
*前端控制器
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
@Controller
@RequestMapping("/seckill")
public class SeckillController {
  @Autowired
  private IGoodsService goodsService;
  @Autowired
  private ISeckillOrderService seckillOrderService;
  @Autowired
  private IOrderService orderService;
  @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";
   }
}

测试

秒杀成功进入订单详情注意查看库存是否正确扣减,订单是否正确生成

 库存不足

重复抢购

订单详情页

本课程重点针对秒杀,所以订单详情只做简单页面展示,随后的支付等功能也不在本课程体现

OrderDetail.html 

<!DOCTYPE HTML>
<htmlxmlns:th="http://www.thymeleaf.org">
<head>
    <title>订单详情</title>
    <metahttp-equiv="Content-Type"content="text/html; charset=UTF-8"/>
    <!-- jquery -->
    <scripttype="text/javascript"th:src="@{/js/jquery.min.js}"></script>
    <!-- bootstrap -->
    <linkrel="stylesheet"type="text/css"
th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <scripttype="text/javascript"th:src="@{/bootstrap/js/bootstrap.min.js}">
</script>
    <!-- layer -->
    <scripttype="text/javascript"th:src="@{/layer/layer.js}"></script>
    <!-- common.js -->
    <scripttype="text/javascript"th:src="@{/js/common.js}"></script>
</head>
<body>
<divclass="panel panel-default">
    <divclass="panel-heading">秒杀订单详情</div>
    <tableclass="table"id="order">
        <tr>
            <td>商品名称</td>
            <tdth:text="${goods.goodsName}"colspan="3"></td>
        </tr>
        <tr>
            <td>商品图片</td>
            <tdcolspan="2"><imgth:src="@{${goods.goodsImg}}"width="200"
height="200"/></td>
        </tr>
        <tr>
            <td>订单价格</td>
            <tdcolspan="2"th:text="${order.goodsPrice}"></td>
        </tr>
        <tr>
            <td>下单时间</td>
            <tdth:text="${#dates.format(order.createDate, 'yyyy-MM-dd
HH:mm:ss')}"colspan="2"></td>
        </tr>
        <tr>
 <td>订单状态</td>
            <td>
                <spanth:if="${order.status eq 0}">未支付</span>
                <spanth:if="${order.status eq 1}">待发货</span>
                <spanth:if="${order.status eq 2}">已发货</span>
                <spanth:if="${order.status eq 3}">已收货</span>
                <spanth:if="${order.status eq 4}">已退款</span>
                <spanth:if="${order.status eq 5}">已完成</span>
            </td>
            <td>
                <buttonclass="btn btn-primary btn-block"type="submit"
id="payButton">立即支付</button>
            </td>
        </tr>
        <tr>
            <td>收货人</td>
            <tdcolspan="2">XXX  18012345678</td>
        </tr>
    <tr>
            <td>收货地址</td>
            <tdcolspan="2">上海市浦东区世纪大道</td>
        </tr>
    </table>
</div>
</body>
</html>

测试

至此,简单的秒杀功能逻辑就完成了,下面进入优化阶段

五.系统压测 

JMeter入门

安装

官网: Apache JMeter - Apache JMeter™

下载地址: Apache JMeter - Download Apache JMeter

下载解压后直接在bin目录里双击jmeter.bat即可启动(Lunix系统通过jmeter.sh启动)

修改中文

Options-->Choose Language-->Chinese(Simplified)

 简单使用 

我们先使用JMeter测试一下跳转商品列表页的接口。
首先创建线程组,步骤:添加-->线程(用户) -->线程组

Ramp-up指在几秒之内启动指定线程数

创建HTTP请求默认值,步骤:添加-->配置元件 --> HTTP请求默认值

 添加测试接口,步骤:添加 -->取样器 --> HTTP请求

查看输出结果,步骤:添加 -->监听器 -->聚合报告/图形结果/用表格察看结果

启动即可在监听器看到对应的结果

  

自定义变量 

准备测试接口

UserController.java

packagecom.xxxx.seckill.controller;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.vo.RespBean;
importorg.springframework.stereotype.Controller;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.ResponseBody;

/**
* <p>
*前端控制器
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
@Controller
@RequestMapping("/user")
public class UserController {
  /**
    *用户信息(测试)
    * @param user
    * @return
    */
  @RequestMapping("/info")
  @ResponseBody
  public RespBean info(User user){
      return RespBean.success(user);
   }
}

配置同一用户测试

添加HTTP请求用户信息

查看聚合结果

配置不同用户测试 

准备配置文件config.txt

#具体用户和userTicket
18012345678,bd055fb14eef4d1ea2933ff8d6e44575

添加 -->配置元件 --> CSV Data Set Config

添加 -->配置元件 --> HTTP Cookie管理器

 修改HTTP请求用户信息

查看结果

正式压测 

压测商品列表接口

准备5000个线程,循环10次。压测商品列表接口,测试3次,查看结果。

线程组

HTTP请求默认值

HTTP请求

结果

压测秒杀接口 

创建用户

使用工具类往数据库插入5000用户,并且调用登录接口获取token,写入config.txt
 
UserUtil.java

package com.xxx.seckill.utils;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxx.seckill.http.HttpClientUtil;
import com.xxx.seckill.pojo.UserModel;
import com.xxx.seckill.vo.RespBean;

import java.io.File;
import java.io.RandomAccessFile;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

/**
 * 生成用户工具类
 */
public class UserUtil {

    private static void createUser(int count) throws Exception {
        List<UserModel> users = new ArrayList<>(count);
        //生成用户
        for (int i = 0; i < count; i++) {
            UserModel user = new UserModel();
            user.setMobile(String.valueOf(13000000000L + i));
            user.setLoginCount(1);
            user.setNickname("user" + i);
            user.setRegisterDate(new Date());
            user.setSalt("1a2b3c4d");
            user.setPassword(MD5Uilt.inputPassTODBPass("123456", user.getSalt()));
            users.add(user);
        }
        System.out.println("create user");
        //插入数据库
        Connection conn = getConn();
        String sql = "insert into t_user(login_count, nickname, register_date, salt, password, mobile)values(?,?,?,?,?,?)";
        PreparedStatement pstmt = conn.prepareStatement(sql);
        for (int i = 0; i < users.size(); i++) {
            UserModel 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.getSalt());
            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, 最终插入格式: mobile,token
        String urlString = "http://localhost:8080/login/doLogin";
        File file = new File("C:\\Users\\pande\\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++) {
            UserModel user = users.get(i);

            //设置请求参数
            HashMap<String, String> map = new HashMap<>();
            map.put("mobile",user.getMobile());
            map.put("password",MD5Uilt.inputPasToFromPass("123456"));
            //post请求
            String response = HttpClientUtil.doPost(urlString, map);

            ObjectMapper mapper = new ObjectMapper();
            RespBean respBean = mapper.readValue(response, RespBean.class);
            String userTicket = ((String) respBean.getObj());
            System.out.println("create userTicket : " + userTicket);

            // 13000000000,83a474c174ac43e795342a476f5be68f
            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 Exception {
        String url = "jdbc:mysql://localhost:3306/seckill?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false";
        String username = "root";
        String password = "admin";
        String driver = "com.mysql.jdbc.Driver";
        Class.forName(driver);
        return DriverManager.getConnection(url, username, password);
    }

    public static void main(String[] args) throws Exception {
        // 创建5000个用户
        createUser(5000);
    }
}

config.txt

配置秒杀接口测试

线程组

HTTP请求默认值

 CVS数据文件设置

HTTP Cookie管理器

HTTP请求

结果

可以看出已经出现了库存超卖的情况

六.页面优化

缓存

页面缓存

GoodsController.java

packagecom.xxxx.seckill.controller;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.service.IGoodsService;
importcom.xxxx.seckill.vo.GoodsVo;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.data.redis.core.ValueOperations;
importorg.springframework.stereotype.Controller;
importorg.springframework.ui.Model;
importorg.springframework.util.StringUtils;
importorg.springframework.web.bind.annotation.PathVariable;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.ResponseBody;
importorg.thymeleaf.context.WebContext;
importorg.thymeleaf.spring5.view.ThymeleafViewResolver;
importjavax.servlet.http.HttpServletRequest;
importjavax.servlet.http.HttpServletResponse;
importjava.util.Date;
importjava.util.concurrent.TimeUnit;

@Controller
@RequestMapping("/goods")
public class GoodsController {
    @Autowired
    private IGoodsService goodsService;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private ThymeleafViewResolver thymeleafViewResolver;

    /**
     * 跳转商品列表页
     *
     * @return
     */
    @RequestMapping(value = "/toList", produces = "text/html;charset=utf-8")
    @ResponseBody
    public String toLogin(HttpServletRequest request, HttpServletResponse
            response, Model model, User user) {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //Redis中获取页面,如果不为空,直接返回页面
        String html = (String) valueOperations.get("goodsList");
        if (!StringUtils.isEmpty(html)) {
            return html;
        }
        model.addAttribute("user", user);
        model.addAttribute("goodsList", goodsService.findGoodsVo());
        // return "goodsList";
        //如果为空,手动渲染,存入Redis并返回
        WebContext context = new WebContext(request, response,
                model.asMap());
        html = thymeleafViewResolver.getTemplateEngine().process("goodsList",
                context);
        if (!StringUtils.isEmpty(html)) {
            valueOperations.set("goodsList", html, 60, TimeUnit.SECONDS);
        }
        return html;
    }

    /**
     * 跳转商品详情页
     *
     * @param model
     * @param user
     * @param goodsId
     * @return
     */
    @RequestMapping(value = "/toDetail/{goodsId}", produces =
            "text/html;charset=utf-8")
    @ResponseBody
    public String toDetail(HttpServletRequest request, HttpServletResponse
            response, Model model, User user, @PathVariable Long goodsId) {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //Redis中获取页面,如果不为空,直接返回页面
        String html = (String) valueOperations.get("goodsDetail:" + goodsId);
        if (!StringUtils.isEmpty(html)) {
            return html;
        }
        model.addAttribute("user", user);
        GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);
        model.addAttribute("goods", goods);
        Date startDate = goods.getStartDate();
        Date endDate = goods.getEndDate();
        Date nowDate = newDate();
        //秒杀状态
        intsecKillStatus = 0;
        //剩余开始时间
        intremainSeconds = 0;
        //秒杀还未开始
        if (nowDate.before(startDate)) {
            remainSeconds = (int) ((startDate.getTime() - nowDate.getTime()) /
                    1000);
            //秒杀已结束
        }
        elseif(nowDate.after(endDate)) {
            secKillStatus = 2;
            remainSeconds = -1;
            //秒杀中
        }else{
            secKillStatus = 1;
            remainSeconds = 0;
        }
        model.addAttribute("secKillStatus", secKillStatus);
        model.addAttribute("remainSeconds", remainSeconds);
        // return "goodsDetail";
        //如果为空,手动渲染,存入Redis并返回
        WebContext context = new WebContext(request, response,
                request.getServletContext(), request.getLocale(),
                model.asMap());
        html = thymeleafViewResolver.getTemplateEngine().process("goodsDetail",
                context);
        if (!StringUtils.isEmpty(html)) {
            valueOperations.set("goodsDetail:" + goodsId, html, 60,
                    TimeUnit.SECONDS);
        }
        return html;
    }
}

 重新运行项目查看效果

测试,可以发现对比之前QPS提升明显

对象缓存 

RespBeanEnum.java

MOBILE_NOT_EXIST(500213,"手机号码不存在"),
PASSWORD_UPDATE_FAIL(500214,"密码更新失败"),

IUserService.java

/**
*更新密码
* @param userTicket
* @param id
* @param password
* @return
*/
RespBean updatePassword(String userTicket,Long id,String password);

UserServiceImpl.java

/**
*更新密码
*
* @param userTicket
* @param id
* @param password
* @return
*/
@Override
public RespBean updatePassword(Stringuser Ticket,Long id,String password) {
  User user=userMapper.selectById(id);
  if (user==null) {
      throw new GlobalException(RespBeanEnum.MOBILE_NOT_EXIST);
   }
  user.setPassword(MD5Util.inputPassToDbPass(password,user.getSalt()));
  int result=userMapper.updateById(user);
  if (1==result) {
      //删除Redis
      redisTemplate.delete("user:"+userTicket);
      return RespBean.success();
   }
  return RespBean.error(RespBeanEnum.PASSWORD_UPDATE_FAIL);
}

页面静态化

商品详情静态化

DetailVo.java

package com.xxx.seckill.vo;

import com.xxx.seckill.pojo.UserModel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 详情返回对象
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DetailVo {

    private User user;
    private GoodsVo goodsVo;
    private int secKillStatus;
    private int remainSeconds;
}

GoodsController.java

    /**
     * 跳转商品详情页
     *
     * @param model
     * @param user
     * @param goodsId
     * @return
     */
    @RequestMapping(value = "/detail/{goodsId}")
    @ResponseBody
    public RespBean toDetail(HttpServletRequest request,HttpServletResponse
        response,Model model,User user,@PathVariable Long goodsId){
         GoodsVo goods=goodsService.findGoodsVoByGoodsId(goodsId);
         Date startDate=goods.getStartDate();
         Date endDate=goods.getEndDate();
         Date nowDate=newDate();
         //秒杀状态
         int secKillStatus=0;
         //剩余开始时间
         int remainSeconds=0;
        //秒杀还未开始
         if(nowDate.before(startDate)){
             remainSeconds=(int)((startDate.getTime()-nowDate.getTime())/1000);
             //秒杀已结束
          }elseif(nowDate.after(endDate)){
             secKillStatus=2;
             remainSeconds=-1;
             //秒杀中
          }else{
             secKillStatus=1;
             remainSeconds=0;
          }
         DetailVo detailVo=newDetailVo();
         detailVo.setGoodsVo(goods);
         detailVo.setUser(user);
         detailVo.setRemainSeconds(remainSeconds);
         detailVo.setSecKillStatus(secKillStatus);
         return RespBean.success(detailVo);
        }

common.js

//获取url参数
functiong_getQueryString(name) {
  var reg=newRegExp("(^|&)"+name+"=([^&]*)(&|$)");
  var r=window.location.search.substr(1).match(reg);
  if(r!=null)returnunescape(r[2]);
  return null;
};
//设定时间格式化函数,使用new Date().format("yyyy-MM-dd HH:mm:ss");
Date.prototype.format=function (format) {
  varargs= {
      "M+":this.getMonth()+1,
      "d+":this.getDate(),
      "H+":this.getHours(),
      "m+":this.getMinutes(),
      "s+":this.getSeconds(),
   };
  if (/(y+)/.test(format))
      format=format.replace(RegExp.$1, (this.getFullYear()+"").substr(4-
RegExp.$1.length));
  for (variinargs) {
      var n=args[i];
      if (new RegExp("("+i+")").test(format))
        format=format.replace(RegExp.$1,RegExp.$1.length==1?n : ("00"+
n).substr((""+n).length));
   }
  return format;
};

goodsDetail.htm

<!DOCTYPE html>
<htmllang="en">
<head>
    <metacharset="UTF-8">
    <title>商品详情</title>
    <!-- jquery -->
    <scripttype="text/javascript"src="/js/jquery.min.js"></script>
    <!-- bootstrap -->
    <linkrel="stylesheet"type="text/css"
href="/bootstrap/css/bootstrap.min.css"/>
    <scripttype="text/javascript"src="/bootstrap/js/bootstrap.min.js">
</script>
    <!-- layer -->
    <scripttype="text/javascript"src="/layer/layer.js"></script>
    <!-- common.js -->
    <scripttype="text/javascript"src="/js/common.js"></script>
</head>
<body>
<divclass="panel panel-default">
    <divclass="panel-heading">秒杀商品详情</div>
    <divclass="panel-body">
        <spanid="userTip">您还没有登录,请登陆后再操作<br/></span>
        <span>没有收货地址的提示。。。</span>
    </div>
    <tableclass="table"id="goods">
        <tr>
            <td>商品名称</td>
            <tdcolspan="3"id="goodsName"></td>
        </tr>
        <tr>
            <td>商品图片</td>
            <tdcolspan="3"><imgid="goodsImg"width="200"height="200"/></td>
        </tr>
        <tr>
<td>秒杀开始时间</td>
            <tdid="startTime"></td>
            <td>
                <inputtype="hidden"id="remainSeconds"/>
                <!-- <span if="secKillStatus eq 0">秒杀倒计时:<span
id="countDown"
                                                          text="remainSeconds">
</span>秒</span>
                <span if="secKillStatus eq 1">秒杀进行中</span>
                <span if="secKillStatus eq 2">秒杀已结束</span>-->
                <spanid="seckillTip"></span>
            </td>
            <td>
                <formid="seckillForm"method="post"
action="/seckill/doSeckill">
                    <buttonclass="btn btn-primary btn-block"type="submit"
id="buyButton">立即秒杀</button>
                    <inputtype="hidden"name="goodsId"id="goodsId"/>
                </form>
            </td>
        </tr>
        <tr>
            <td>商品原价</td>
            <tdcolspan="3"id="goodsPrice"></td>
        </tr>
<tr>
            <td>秒杀价</td>
            <tdcolspan="3"id="seckillPrice"></td>
        </tr>
        <tr>
            <td>库存数量</td>
            <tdcolspan="3"id="stockCount"></td>
        </tr>
    </table>
</div>
</body>
<script>
    $(function () {
            // countDown();
            getDetails();
        }
    );
    functiongetDetails() {
        vargoodsId=g_getQueryString("goodsId");
        $.ajax({
            url:"/goods/detail/"+goodsId,
            type:"GET",
            success:function (data) {
                if (data.code==200) {
                    render(data.obj);
                }else {
                    layer.msg(data.message);
                }
            },
            error:function () {
                layer.msg("客户端请求错误");
            }
        })
    }
functionrender(detail) {
        var user=detail.user;
        var goods=detail.goodsVo;
        var remainSeconds=detail.remainSeconds;
        if (user) {
            $("#userTip").hide();
        }
        $("#goodsName").text(goods.goodsName);
        $("#goodsImg").attr("src",goods.goodsImg);
        $("#startTime").text(newDate(goods.startDate).format("yyyy-MM-dd
HH:mm:ss"));
        $("#remainSeconds").val(remainSeconds);
        $("#goodsId").val(goods.id);
        $("#goodsPrice").text(goods.goodsPrice);
        $("#seckillPrice").text(goods.seckillPrice);
        $("#stockCount").text(goods.stockCount);
        countDown();
    }
    functioncountDown() {
        var remainSeconds=$("#remainSeconds").val();
        var timeout;
//秒杀还没开始,倒计时
        if (remainSeconds>0) {
            $("#buyButton").attr("disabled",true);
            $("#seckillTip").html("秒杀倒计时:"+remainSeconds+"秒");
            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>

测试

秒杀未开始

秒杀进行中 

秒杀已结束

 秒杀静态化

SeckillController.java

packagecom.xxxx.seckill.controller;
importcom.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
importcom.xxxx.seckill.pojo.Order;
importcom.xxxx.seckill.pojo.SeckillOrder;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.service.IGoodsService;
importcom.xxxx.seckill.service.IOrderService;
importcom.xxxx.seckill.service.ISeckillOrderService;
importcom.xxxx.seckill.vo.GoodsVo;
importcom.xxxx.seckill.vo.RespBean;
importcom.xxxx.seckill.vo.RespBeanEnum;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Controller;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RequestMethod;
importorg.springframework.web.bind.annotation.ResponseBody;

* <p>
*前端控制器
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
@Controller
@RequestMapping("/seckill")
public class SeckillController {

  @Autowired
  private IGoodsService goodsService;
  @Autowired
  private ISeckillOrderService seckillOrderService;
  @Autowired
  private IOrderService orderService;

  @RequestMapping(value="/doSeckill",method=RequestMethod.POST)
  @ResponseBody
  public RespBean doSeckill(User user,Long goodsId) {
      if (user==null) {
        return RespBean.error(RespBeanEnum.SESSION_ERROR);
      }
      GoodsVo goods=goodsService.findGoodsVoByGoodsId(goodsId);
      //判断库存
      if (goods.getStockCount()<1) {
        return RespBean.error(RespBeanEnum.EMPTY_STOCK);
      }
      //判断是否重复抢购
      SeckillOrder seckillOrder=seckillOrderService.getOne(new
QueryWrapper<SeckillOrder>().eq("user_id",
            user.getId()).eq(
            "goods_id",
            goodsId));
      if (seckillOrder!=null) {
        return RespBean.error(RespBeanEnum.REPEATE_ERROR);
      }
      Order order=orderService.seckill(user,goods);
      return RespBean.success(order);
   }
}

goodsDetail.htm

<td>
    <!--<form id="seckillForm" method="post" action="/seckill/doSeckill">
        <button class="btn btn-primary btn-block" type="submit" id="buyButton">
立即秒杀</button>
        <input type="hidden" name="goodsId" id="goodsId"/>
    </form>-->
    <buttonclass="btn btn-primary btn-block"type="button"id="buyButton"
onclick="doSeckill()">立即秒杀
    </button>
    <inputtype="hidden"name="goodsId"id="goodsId"/>
<script>
    functiondoSeckill() {
        $.ajax({
            url:"/seckill/doSeckill",
            type:"POST",
            data: {
                goodsId:$("#goodsId").val(),
            },
            success:function (data) {
                if (data.code==200) {
                    window.location.href="/orderDetail.htm?orderId="+
data.obj.id;
                }else {
                    layer.msg(data.message);
                }
            },
            error:function () {
                layer.msg("客户端请求错误");
            }
        })
    }
</script>

orderDetail.htm

<!DOCTYPE HTML>
<html>
<head>
    <title>订单详情</title>
    <metahttp-equiv="Content-Type"content="text/html; charset=UTF-8"/>
    <!-- jquery -->
    <scripttype="text/javascript"src="/js/jquery.min.js"></script>
    <!-- bootstrap -->
    <linkrel="stylesheet"type="text/css"
href="/bootstrap/css/bootstrap.min.css"/>
    <scripttype="text/javascript"src="/bootstrap/js/bootstrap.min.js">
</script>
    <!-- layer -->
    <scripttype="text/javascript"src="/layer/layer.js"></script>
    <!-- common.js -->
    <scripttype="text/javascript"src="/js/common.js"></script>
</head>
<body>
<divclass="panel panel-default">
    <divclass="panel-heading">秒杀订单详情</div>
    <tableclass="table"id="order">
        <tr>
            <td>商品名称</td>
            <tdid="goodsName"colspan="3"></td>
        </tr>
        <tr>
            <td>商品图片</td>
            <tdcolspan="2"><imgid="goodsImg"width="200"height="200"/></td>
        </tr>
        <tr>
            <td>订单价格</td>
            <tdcolspan="2"id="goodsPrice"></td>
        </tr>
        <tr>
            <td>下单时间</td>
            <tdid="createDate"colspan="2"></td>
        </tr>
        <tr>
            <td>订单状态</td>
            <tdid="status">
                <!--<span if="order.status eq 0">未支付</span>
                <span if="order.status eq 1">待发货</span>
                <span if="order.status eq 2">已发货</span>
                <span if="order.status eq 3">已收货</span>
                <span if="order.status eq 4">已退款</span>
                <span if="order.status eq 5">已完成</span>-->
            </td>
            <td>
                <buttonclass="btn btn-primary btn-block"type="submit"
id="payButton">立即支付</button>
            </td>
        </tr>
        <tr>
            <td>收货人</td>
            <tdcolspan="2">XXX  18012345678</td>
        </tr>
        <tr>
            <td>收货地址</td>
            <tdcolspan="2">上海市浦东区世纪大道</td>
        </tr>
    </table>
</div>
</body>
</html>

 applictaion.yml

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:/static/

测试

订单详情静态化 

OrderController.java

packagecom.xxxx.seckill.controller;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.service.IOrderService;
importcom.xxxx.seckill.vo.OrderDetailVo;
importcom.xxxx.seckill.vo.RespBean;
importcom.xxxx.seckill.vo.RespBeanEnum;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Controller;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.ResponseBody;

/**
* <p>
*前端控制器
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
@Controller
@RequestMapping("/order")
public class OrderController {

  @Autowired
  private IOrderService orderService;


 /**
    *订单详情
    * @param user
    * @param orderId
    * @return
    */
  @RequestMapping("/detail")
  @ResponseBody
  public RespBean detail(User user,Long orderId){
      if (null==user){
        return RespBean.error(RespBeanEnum.SESSION_ERROR);
      }
      OrderDetailVo detail=orderService.detail(orderId);
      return RespBean.success(detail);
   }
}

IOrderService.java

/**
*订单详情
* @param orderId
* @return
*/
OrderDetailV odetail(Long orderId);

OrderServiceImpl.java

/**
*订单详情
* @param orderId
* @return
*/
@Override
public OrderDetailVo detail(Long orderId) {
  if (null==orderId){
      throw new GlobalException(RespBeanEnum.ORDER_NOT_EXIST);
   }
  Order order=orderMapper.selectById(orderId);
  GoodsVo goodsVo=goodsService.findGoodsVoByGoodsId(order.getGoodsId());
  OrderDetailVo detail=newOrderDetailVo();
  detail.setGoodsVo(goodsVo);
  detail.setOrder(order);
  return detail;
}

OrderDetailVo.java

package com.xxx.seckill.vo;

import com.xxx.seckill.pojo.OrderModel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 订单详情返回对象
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderDetailVo {

    private OrderModel order;
    private GoodsVo goodsVo;
}

orderDetail.htm

<!DOCTYPE HTML>
<html>
<head>
    <title>订单详情</title>
    <metahttp-equiv="Content-Type"content="text/html; charset=UTF-8"/>
    <!-- jquery -->
    <scripttype="text/javascript"src="/js/jquery.min.js"></script>
    <!-- bootstrap -->
    <linkrel="stylesheet"type="text/css"
href="/bootstrap/css/bootstrap.min.css"/>
    <scripttype="text/javascript"src="/bootstrap/js/bootstrap.min.js">
</script>
    <!-- layer -->
    <scripttype="text/javascript"src="/layer/layer.js"></script>
    <!-- common.js -->
    <scripttype="text/javascript"src="/js/common.js"></script>
</head>
<body>
<divclass="panel panel-default">
    <divclass="panel-heading">秒杀订单详情</div>
    <tableclass="table"id="order">
        <tr>
            <td>商品名称</td>
            <tdid="goodsName"colspan="3"></td>
        </tr>
        <tr>
            <td>商品图片</td>
            <tdcolspan="2"><imgid="goodsImg"width="200"height="200"/></td>
        </tr>
        <tr>
            <td>订单价格</td>
            <tdcolspan="2"id="goodsPrice"></td>
        </tr>
        <tr>
            <td>下单时间</td>
            <tdid="createDate"colspan="2"></td>
        </tr>
        <tr>
            <td>订单状态</td>
            <tdid="status">
                <!--<span if="order.status eq 0">未支付</span>
                <span if="order.status eq 1">待发货</span>
                <span if="order.status eq 2">已发货</span>
                <span if="order.status eq 3">已收货</span>
                <span if="order.status eq 4">已退款</span>
                <span if="order.status eq 5">已完成</span>-->
            </td>
            <td>
                <buttonclass="btn btn-primary btn-block"type="submit"
id="payButton">立即支付</button>
            </td>
        </tr>
        <tr>
            <td>收货人</td>
            <tdcolspan="2">XXX 18012345678</td>
        </tr>
        <tr>
            <td>收货地址</td>
            <tdcolspan="2">上海市浦东区世纪大道</td>
        </tr>
    </table>
</div>
<script>
 $(function () {
        getOrderDetail();
    });
    function getOrderDetail() {
        varorderId=g_getQueryString("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) {
        vargoods=detail.goodsVo;
        varorder=detail.order;
        $("#goodsName").text(goods.goodsName);
        $("#goodsImg").attr("src",goods.goodsImg);
        $("#goodsPrice").text(order.goodsPrice);
        $("#createDate").text(newDate(order.createDate).format("yyyy-MM-dd
HH:mm:ss"));
        var status=order.status;
        var statusText=""
        switch (status) {
            case0:
                statusText="未支付";
                break;
            case1:
                statusText="待发货";
                break;
            case2:
                statusText="已发货";
                break;
            case3:
                statusText="已收货";
                break;
            case4:
                statusText="已退款";
                break;
            case5:
                statusText="已完成";
                break;
        }
        $("#status").text(statusText);
    }
</script>
</body>
</html>

效果

解决库存超卖 

减库存时判断库存是否足够

OrderServiceImpl.java

//秒杀商品表减库存
SeckillGoods seckillGoods=seckillGoodsService.getOne(new
QueryWrapper<SeckillGoods>().eq("goods_id",
      goods.getId()));
seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
seckillGoodsService.update(newUpdateWrapper<SeckillGoods>().set("stock_count",
seckillGoods.getStockCount()).eq("id",seckillGoods.getId()).gt("stock_count",
0));
// seckillGoodsService.updateById(seckillGoods);

解决同一用户同时秒杀多件商品。
可以通过数据库建立唯一索引避免

将秒杀订单信息存入Redis,方便判断是否重复抢购时进行查询

OrderServiceImpl.java

/**
*秒杀
*
* @param user
* @param goods
* @return
*/
@Override
@Transactional
public Order seckill(User user,GoodsVo goods) {
  //秒杀商品表减库存
  SeckillGoods seckillGoods=seckillGoodsService.getOne(new
QueryWrapper<SeckillGoods>().eq("goods_id",
        goods.getId()));
  seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
  booleanseckillGoodsResult=seckillGoodsService.update(new
UpdateWrapper<SeckillGoods>().set("stock_count",
        seckillGoods.getStockCount()).eq("id",
seckillGoods.getId()).gt("stock_count",0));
  // seckillGoodsService.updateById(seckillGoods);
  if (!(goodsResult&&seckillGoodsResult)){
      return null;
   }
  //生成订单
  Order order=newOrder();
  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(newDate());
  orderMapper.insert(order);
  //生成秒杀订单
  SeckillOrder seckillOrder=newSeckillOrder();
  seckillOrder.setOrderId(order.getId());
  seckillOrder.setUserId(user.getId());
  seckillOrder.setGoodsId(goods.getId());
  seckillOrderService.save(seckillOrder);
  redisTemplate.opsForValue().set("order:"+user.getId()+":"+
goods.getId(),
        JsonUtil.object2JsonStr(seckillOrder));
  return order;
}

 seckillController.java

packagecom.xxxx.seckill.controller;
importcom.xxxx.seckill.pojo.Order;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.service.IGoodsService;
importcom.xxxx.seckill.service.IOrderService;
importcom.xxxx.seckill.service.ISeckillOrderService;
importcom.xxxx.seckill.vo.GoodsVo;
importcom.xxxx.seckill.vo.RespBean;
importcom.xxxx.seckill.vo.RespBeanEnum;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.stereotype.Controller;
importorg.springframework.util.StringUtils;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RequestMethod;
importorg.springframework.web.bind.annotation.ResponseBody;


/**
* <p>
*前端控制器
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
@Controller
@RequestMapping("/seckill")
public class SeckillController {
  @Autowired
  private IGoodsService goodsService;
  @Autowired
  private ISeckillOrderService seckillOrderService;
  @Autowired
  private IOrderService orderService;
  @Autowired
  private RedisTemplate redisTemplate;

  @RequestMapping(value="/doSeckill",method=RequestMethod.POST)
  @ResponseBody
  public RespBean doSeckill(User user,Long goodsId) {
      if (user==null) {
        return RespBean.error(RespBeanEnum.SESSION_ERROR);
      }
      GoodsVo goods=goodsService.findGoodsVoByGoodsId(goodsId);
      //判断库存
      if (goods.getStockCount()<1) {
        return RespBean.error(RespBeanEnum.EMPTY_STOCK);
      }
      //判断是否重复抢购
      // SeckillOrder seckillOrder = seckillOrderService.getOne(new
QueryWrapper<SeckillOrder>().eq("user_id",
      //        user.getId()).eq(
      //        "goods_id",
      //        goodsId));
      String seckillOrderJson= (String)
redisTemplate.opsForValue().get("order:"+user.getId()+":"+goodsId);
      if (!StringUtils.isEmpty(seckillOrderJson)) {
        return RespBean.error(RespBeanEnum.REPEATE_ERROR);
      }
      Order order=orderService.seckill(user,goods);
      if (null!=order) {
        return RespBean.success(order);
      }
      return RespBean.error(RespBeanEnum.ERROR);
   }
}

SeckillOrder.java

packagecom.xxxx.seckill.controller;
importcom.xxxx.seckill.pojo.Order;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.service.IGoodsService;
importcom.xxxx.seckill.service.IOrderService;
importcom.xxxx.seckill.service.ISeckillOrderService;
importcom.xxxx.seckill.vo.GoodsVo;
importcom.xxxx.seckill.vo.RespBean;
importcom.xxxx.seckill.vo.RespBeanEnum;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.stereotype.Controller;
importorg.springframework.util.StringUtils;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RequestMethod;
importorg.springframework.web.bind.annotation.ResponseBody;

/**
* <p>
*前端控制器
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
@Controller
@RequestMapping("/seckill")
public class SeckillController {
  @Autowired
  private IGoodsService goodsService;
  @Autowired
  private ISeckillOrderService seckillOrderService;
  @Autowired
  private IOrderService orderService;
  @Autowired
  private RedisTemplate redisTemplate;

  @RequestMapping(value="/doSeckill",method=RequestMethod.POST)
  @ResponseBody
  public RespBean doSeckill(User user,Long goodsId) {
      if (user==null) {
        return RespBean.error(RespBeanEnum.SESSION_ERROR);
      }
      GoodsVo goods=goodsService.findGoodsVoByGoodsId(goodsId);
      //判断库存
      if (goods.getStockCount()<1) {
        return RespBean.error(RespBeanEnum.EMPTY_STOCK);
      }
      //判断是否重复抢购
      // SeckillOrder seckillOrder = seckillOrderService.getOne(new
QueryWrapper<SeckillOrder>().eq("user_id",
      //        user.getId()).eq(
      //        "goods_id",
      //        goodsId));
      String seckillOrderJson= (String)
redisTemplate.opsForValue().get("order:"+user.getId()+":"+goodsId);
      if (!StringUtils.isEmpty(seckillOrderJson)) {
        returnRespBean.error(RespBeanEnum.REPEATE_ERROR);
      }
      Order order=orderService.seckill(user,goods);
      return RespBean.success(order);
}

测试

QPS提升并不明显,重点在于是否出现库存超卖现象

七.服务优化 

RabbitMQ入门

安装

官网提示: Erlang and Elixir Packages Download - Erlang Solutions

安装erlang

        yum-y install esl-erlang_23.0.2-1_centos_7_amd64.rpm

检测erlang

安装RabbitMQ

官网下载地址: Downloading and Installing RabbitMQ — RabbitMQ 

安装rabbitmq

        yum-y install rabbitmq-server-3.8.5-1.el7.noarch.rpm

安装UI插件

         rabbitmq-plugins enable rabbitmq_management

启用rabbitmq服务 

        systemctlstart rabbitmq-server.service

检测服务

        systemctl status rabbitmq-server.service

访问

guest用户默认只可以localhost(本机)访问

在rabbitmq的配置文件目录下(默认为:/etc/rabbitmq)创建一个rabbitmq.config文件。
文件中添加如下配置(请不要忘记那个“.”):

        [{rabbit, [{loopback_users, []}]}].

重启rabbitmq服务

        systemctlrestart rabbitmq-server.service

重新访问

使用

依赖

<!-- AMQP依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

 配置

application.ym

  # rabbitmq
  rabbitmq:
    # 服务器
    host: 8.134.102.252
    # 用户名(默认guest)
    username: guest
    # 密码(默认guest)
    password: guest
    # 虚拟主机
    virtual-host: /
    # 端口
    port: 5672
    listener:
      simple:
        # 消费者最小数量
        concurrency: 10
        # 消费者最大数量
        max-concurrency: 10
        # 限制消费者每次只处理一条消息,处理完再继续下一条消息
        prefetch: 1
        # 启动时是否默认启动容器, 默认true
        auto-startup: true
        # 被拒绝时重新进入队列
        default-requeue-rejected: true
    template:
      retry:
        # 发布重试, 默认false
        enabled: true
        # 重试时间, 默认1000ms
        initial-interval: 1000ms
        # 重试最大次数, 默认3
        max-attempts: 3
        # 重试最大间隔, 默认10000ms
        max-interval: 10000ms
        # 重试的间隔乘数, 默认1
        multiplier: 1

RabbitMQConfig.java

packagecom.xxxx.seckill.config;
importorg.springframework.amqp.core.Queue;
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;


/**
* @author xiao pan
* @since 1.0.0
*/
@Configuration
public class RabbitMQConfig {
  @Bean
  public Queue queue(){
      return new Queue("queue",true);
   }

MQSender.java

packagecom.xxxx.seckill.rabbitmq;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.amqp.rabbit.core.RabbitTemplate;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Service;

/**
* @author xiao pan
* @since 1.0.0
*/
@Service
@Slf4j
public class MQSender {
  @Autowired
  private RabbitTemplate rabbitTemplate;

  public void send(Objectmsg) {
      log.info("发送消息:"+msg);
      rabbitTemplate.convertAndSend("queue",msg);
   }
}

MQReceiver.java

packagecom.xxxx.seckill.rabbitmq;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.amqp.rabbit.annotation.RabbitListener;
importorg.springframework.stereotype.Service;


/**
* @author xiao pan
* @since 1.0.0
*/
@Service
@Slf4j
public class MQReceiver {

  @RabbitListener(queues="queue")
  public void receive(Object msg) {
      log.info("接受消息:"+msg);
   }
}

UserController.java

/**
*测试发送RabbitMQ消息
*/
@RequestMapping("/mq")
@ResponseBody
public void mq() {
  mqSender.send("Hello");
}

结果

 RabbitMQ交换机

Fanout模式

        不处理路由键,只需要简单的将队里绑定到交换机上
        发送到交换机的消息都会被转发到与该交换机绑定的所有队列上
        Fanout交换机转发消息是最快的

实现代码

RabbitMQConfig.java

packagecom.xxxx.seckill.config;
importorg.springframework.amqp.core.Binding;
importorg.springframework.amqp.core.BindingBuilder;
importorg.springframework.amqp.core.FanoutExchange;
importorg.springframework.amqp.core.Queue;
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;


/**
* @author xiao pan
* @since 1.0.0
*/
@Configuration
publicclassRabbitMQConfig {
  private static final String QUEUE01="queue_fanout01";
  private static final String QUEUE02="queue_fanout02";
  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);
   }
  @Bean
  public Binding binding01(){
      return BindingBuilder.bind(queue01()).to(fanoutExchange());
   }
  @Bean
  public Binding binding02(){
      return BindingBuilder.bind(queue02()).to(fanoutExchange());
   }
}

MQSender.java

packagecom.xxxx.seckill.rabbitmq;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.amqp.rabbit.core.RabbitTemplate;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Service;


/**
* @author xiao pan
* @since 1.0.0
*/
@Service
@Slf4j
public class MQSender {
  @Autowired
  private RabbitTemplate rabbitTemplate;

  public void send(Object msg) {
    log.info("发送消息:"+msg);
      rabbitTemplate.convertAndSend("fanoutExchange","",msg);
   }
}

MQReceiver.java

packagecom.xxxx.seckill.rabbitmq;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.amqp.rabbit.annotation.RabbitListener;
importorg.springframework.stereotype.Service;


/**
* @author xiao pan
* @since 1.0.0
*/
@Service
@Slf4j
public class MQReceiver {
  @RabbitListener(queues="queue_fanout01")
  public void receive01(Object msg) {
      log.info("QUEUE01接受消息:"+msg);
   }
  @RabbitListener(queues="queue_fanout02")
  public void receive02(Object msg) {
      log.info("QUEUE02接受消息:"+msg);
   }
}

UserController.java

/**
*测试发送RabbitMQ消息
*/
@RequestMapping("/mq/fanout")
@ResponseBody
public void mq() {
  mqSender.send("Hello");
}

测试

调用mq/direct01接口,消息经由交换机转发到绑定该交换机的所有队列

Direct模式 

 所有发送到Direct Exchange的消息被转发到RouteKey中指定的Queue
注意:Direct模式可以使用RabbitMQ自带的Exchange:default Exchange,所以不需要将
Exchange进行任何绑定(binding)操作,消息传递时,RouteKey必须完全匹配才会被队列接收,否
则该消息会被抛弃。
重点:routing key与队列queues的key保持一致,即可以路由到对应的queue中。

代码实现

RabbitMQConfig.java

packagecom.xxxx.seckill.config;
importorg.springframework.amqp.core.Binding;
importorg.springframework.amqp.core.BindingBuilder;
importorg.springframework.amqp.core.DirectExchange;
importorg.springframework.amqp.core.Queue;
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;


/**
* @author xiao pan
* @since 1.0.0
*/
@Configuration
public class RabbitMQConfig {
    private static final String QUEUE01="queue_direct01";
    private static final String QUEUE02="queue_direct02";
    private static final String EXCHANGE="directExchange";
    private static final String ROUTINGKEY01="queue.red";
    private static final String ROUTINGKEY02="queue.green";


    @Bean
    public Queue queue01(){
        return new Queue(QUEUE01);
    }
    @Bean
    public Queue queue02(){
        return new Queue(QUEUE02);
    }
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange(EXCHANGE);
    }
    @Bean
    public Binding binding01(){
    return BindingBuilder.bind(queue01()).to(directExchange()).with(ROUTINGKEY01);
    }
    @Bean
    public Binding binding02(){
    return BindingBuilder.bind(queue02()).to(directExchange()).with(ROUTINGKEY02);
    }
}

MQSender.java

packagecom.xxxx.seckill.rabbitmq;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.amqp.rabbit.core.RabbitTemplate;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Service;

/**
* @author xiao pan
* @since 1.0.0
*/
@Service
@Slf4j
public class MQSender {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void send01(Object msg) {
        log.info("发送red消息:"+msg);
        rabbitTemplate.convertAndSend("directExchange","queue.red",msg);
    }
    public void send02(Object msg) {
        log.info("发送green消息:"+msg);
        rabbitTemplate.convertAndSend("directExchange","queue.green",msg);
    }
}

MQReceiver.java

packagecom.xxxx.seckill.rabbitmq;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.amqp.rabbit.annotation.RabbitListener;
importorg.springframework.stereotype.Service;

/**
* @author xiao pan
* @since 1.0.0
*/
@Service
@Slf4j
public class MQReceiver {

    @RabbitListener(queues="queue_direct01")
    public void receive01(Object msg) {
        log.info("QUEUE01接受消息:"+msg);
    }
    @RabbitListener(queues="queue_direct02")
    public void receive02(Object msg) {
        log.info("QUEUE02接受消息:"+msg);
    }
}

UserController.java

/**
*测试发送RabbitMQ消息
*/
@RequestMapping("/mq/direct01")
@ResponseBody
public void mq01() {
  mqSender.send01("Hello,Red");
}

/**
*测试发送RabbitMQ消息
*/
@RequestMapping("/mq/direct02")
@ResponseBody
public void mq02() {
  mqSender.send02("Hello,Green");
}

测试

调用mq/direct01接口,消息经由交换机绑定的queue.redRoutingKey转发到queue_direct01队

调用mq/direct02接口,消息经由交换机绑定的queue.greenRoutingKey转发到queue_direct02
队列

Topic模式
        所有发送到Topic Exchange的消息被转发到所有管线RouteKey中指定Topic的Queue上
        Exchange将RouteKey和某Topic进行模糊匹配,此时队列需要绑定一个Topic
        对于routing key匹配模式定义规则举例如下:
        routing key为一个句点号.分隔的字符串(我们将被句点号.分隔开的每一段独立的字符串称为
        一个单词),如“stock.usd.nyse”、“nyse.vmw”、“quick.orange.rabbit”
        routing key中可以存在两种特殊字符*与#,用于做模糊匹配,其中*用于匹配一个单词,#用
        于匹配多个单词(可以是零个) 

代码实现

RabbitMQConfig.java

packagecom.xxxx.seckill.config;
importorg.springframework.amqp.core.Binding;
importorg.springframework.amqp.core.BindingBuilder;
importorg.springframework.amqp.core.Queue;
importorg.springframework.amqp.core.TopicExchange;
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;


/**
* @author xiao pan
* @since 1.0.0
*/
@Configuration
public class RabbitMQConfig {
    private static final String QUEUE01="queue_topic01";
    private static final String QUEUE02="queue_topic02";
    private static final String EXCHANGE="topicExchange";
    private static final String ROUTINGKEY01="#.queue.#";
    private static final String ROUTINGKEY02="*.queue.#";


    @Bean
    public Queue queue01(){
        return new Queue(QUEUE01);
    }
    @Bean
    public Queue queue02(){
        return new Queue(QUEUE02);
    }
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange(EXCHANGE);
    }
    @Bean
    public Binding binding01(){
        return BindingBuilder.bind(queue01()).to(topicExchange()).with(ROUTINGKEY01);
    }
    @Bean
    public Binding binding02(){
        return BindingBuilder.bind(queue02()).to(topicExchange()).with(ROUTINGKEY02);
    }
}

MQSender.java

packagecom.xxxx.seckill.rabbitmq;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.amqp.rabbit.core.RabbitTemplate;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Service;


/**
* @author xiao pan
* @since 1.0.0
*/
@Service
@Slf4j
public class MQSender {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void send01(Object msg) {
        log.info("发送消息(被01队列接受):"+msg);
        rabbitTemplate.convertAndSend("topicExchange","queue.red.message",msg);
    }
    public void send02(Object msg) {
        log.info("发送消息(被两个queue接受):"+msg);
        rabbitTemplate.convertAndSend("topicExchange","message.queue.green.abc",msg);
    }
}

MQReceiver.java

packagecom.xxxx.seckill.rabbitmq;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.amqp.rabbit.annotation.RabbitListener;
importorg.springframework.stereotype.Service;


/**
* @author xiao pan
* @since 1.0.0
*/
@Service
@Slf4j
public class MQReceiver {
    @RabbitListener(queues="queue_topic01")
    public void receive01(Object msg) {
        log.info("QUEUE01接受消息:"+msg);
    }
    @RabbitListener(queues="queue_topic02")
    public void receive02(Object msg) {
        log.info("QUEUE02接受消息:"+msg);
    }
}

UserController.java

/**
*测试发送RabbitMQ消息
*/
@RequestMapping("/mq/topic01")
@ResponseBody
public void mq01() {
  mqSender.send01("Hello,Red");
}

/**
*测试发送RabbitMQ消息
*/
@RequestMapping("/mq/topic02")
@ResponseBody
public void mq02() {
  mqSender.send02("Hello,Green");
}

测试

调用mq/topic01接口,消息经由交换机绑定的#.queue.#RoutingKey转发到queue_topic01队列

调用mq/topic02接口,消息经由交换机绑定的*.queue.#和#.queue.# RoutingKey转发到
queue_topic01和queue_topic02队列

Headers模式

        不依赖routingkey,使用发送消息时basicProperties对象中的headers匹配队列
        headers是一个键值对类型,键值对的值可以是任何类型
        在队列绑定交换机时用x-match来指定,all代表定义的多个键值对都要满足,any则代表只要          满足一个可以了 

代码实现

RabbitMQConfig.java

packagecom.xxxx.seckill.config;
importorg.springframework.amqp.core.Binding;
importorg.springframework.amqp.core.BindingBuilder;
importorg.springframework.amqp.core.HeadersExchange;
importorg.springframework.amqp.core.Queue;
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;
importjava.util.HashMap;
importjava.util.Map;


/**
* @author xiao pan
* @since 1.0.0
*/
@Configuration
public class RabbitMQConfig {
    private static final String QUEUE01="queue_header01";
    private static final String QUEUE02="queue_header02";
    private static final String EXCHANGE="headersExchange";

    @Bean
    public Queue queue01(){
        return new Queue(QUEUE01);
    }
    @Bean
    public Queue queue02(){
        return new Queue(QUEUE02);
    }
    @Bean
    public HeadersExchange headersExchange(){
        return new HeadersExchange(EXCHANGE);
    }
    @Bean
    public Binding binding01(){
        Map<String,Object> map=new HashMap<>();
        map.put("color","red");
        map.put("speed","low");
        return     
       BindingBuilder.bind(queue01()).to(headersExchange()).whereAny(map).match();
    }
    @Bean
    public Binding binding02(){
        Map<String,Object> map=new HashMap<>();
        map.put("color","red");
        map.put("speed","fast");
        return
        BindingBuilder.bind(queue02()).to(headersExchange()).whereAll(map).match();
    }
}

MQSender.java

packagecom.xxxx.seckill.rabbitmq;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.amqp.core.Message;
importorg.springframework.amqp.core.MessageProperties;
importorg.springframework.amqp.rabbit.core.RabbitTemplate;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Service;


/**
* @author xiao pan
* @since 1.0.0
*/
@Service
@Slf4j
public class MQSender {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send01(String msg) {
        log.info("发送消息(被两个queue接受):"+msg);
        MessageProperties properties=new MessageProperties();
        properties.setHeader("color","red");
        properties.setHeader("speed","fast");
        Messagemessage=newMessage(msg.getBytes(),properties);
        rabbitTemplate.convertAndSend("headersExchange","",message);
    }
    public void send02(String msg) {
        log.info("发送消息(被01队列接受):"+msg);
        MessageProperties properties=new MessageProperties();
        properties.setHeader("color","red");
        properties.setHeader("speed","normal");
        Messagemessage=newMessage(msg.getBytes(),properties);
        rabbitTemplate.convertAndSend("headersExchange","",message);
    }
}

MQReceiver.java

packagecom.xxxx.seckill.rabbitmq;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.amqp.core.Message;
importorg.springframework.amqp.rabbit.annotation.RabbitListener;
importorg.springframework.stereotype.Service;

/**
* @author xiao pan
* @since 1.0.0
*/
@Service
@Slf4j
public class MQReceiver {

    @RabbitListener(queues="queue_header01")
    public void receive01(Message message) {
        log.info("QUEUE01接受Message对象:"+message);
        log.info("QUEUE01接受消息:"+newString(message.getBody()));
    }
    @RabbitListener(queues="queue_header02")
    public void receive02(Message message) {
        log.info("QUEUE02接受Message对象:"+message);
        log.info("QUEUE02接受消息:"+newString(message.getBody()));
    }
}

UserController.java

/**
*测试发送RabbitMQ消息
*/
@RequestMapping("/mq/header01")
@ResponseBody
public void mq01() {
  mqSender.send01("Hello,header01");
}

/**
*测试发送RabbitMQ消息
*/
@RequestMapping("/mq/header02")
@ResponseBody
public void mq02() {
  mqSender.send02("Hello,header02");
}

测试

queue_header01设置x-match为any,queue_header02设置x-match为all。因此调用mq/header01
接口,可以匹配两个队列

调用mq/header02接口,只能匹配queue_header01队列

接口优化

思路:减少数据库访问 

        1.系统初始化,把商品库存数量加载到Redis
        2.收到请求,Redis预减库存。库存不足,直接返回。否则进入第3步
        3.请求入队,立即返回排队中
        4.请求出队,生成订单,减少库存
        5.客户端轮询,是否秒杀成功

Redis操作库存

SeckillController.java

packagecom.xxxx.seckill.controller;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.rabbitmq.MQSender;
importcom.xxxx.seckill.rabbitmq.SeckillMessage;
importcom.xxxx.seckill.service.IGoodsService;
importcom.xxxx.seckill.service.IOrderService;
importcom.xxxx.seckill.service.ISeckillOrderService;
importcom.xxxx.seckill.util.JsonUtil;
importcom.xxxx.seckill.vo.GoodsVo;
importcom.xxxx.seckill.vo.RespBean;
importcom.xxxx.seckill.vo.RespBeanEnum;
importorg.springframework.beans.factory.InitializingBean;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.data.redis.core.ValueOperations;
importorg.springframework.stereotype.Controller;
importorg.springframework.util.CollectionUtils;
importorg.springframework.util.StringUtils;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RequestMethod;
importorg.springframework.web.bind.annotation.ResponseBody;
importjava.util.HashMap;
importjava.util.List;
importjava.util.Map;


/**
* <p>
*前端控制器
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
@Controller
@RequestMapping("/seckill")
public class SeckillController implements InitializingBean {
    @Autowired
    private IGoodsService goodsService;
    @Autowired
    private ISeckillOrderService seckillOrderService;
    @Autowired
    private IOrderService orderService;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private MQSender mqSender;
    private Map<Long,Boolean> EmptyStockMap = new HashMap<>();


    /**
    *秒杀
    *
    * @param user
    * @param goodsId
    * @return
    */
    @RequestMapping(value="/doSeckill",method=RequestMethod.POST)
    @ResponseBody
    public RespBean doSeckill(Useruser,LonggoodsId) {
    if (user==null) {
        return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }
    /*GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);
    //判断库存
    if (goods.getStockCount() < 1) {
        return RespBean.error(RespBeanEnum.EMPTY_STOCK);
    }
    //判断是否重复抢购
    // SeckillOrder seckillOrder = seckillOrderService.getOne(new
    QueryWrapper<SeckillOrder>().eq("user_id",
    //      user.getId()).eq(
    //      "goods_id",
    //      goodsId));
    String seckillOrderJson = (String)
    redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
    if (!StringUtils.isEmpty(seckillOrderJson)) {
        return RespBean.error(RespBeanEnum.REPEATE_ERROR);
    }
    Order order = orderService.seckill(user, goods);
    if (null != order) {
        return RespBean.success(order);
    }*/
    ValueOperations valueOperations=redisTemplate.opsForValue();
    //判断是否重复抢购
    String seckillOrderJson= (String)valueOperations.get("order:"+
    user.getId()+":"+goodsId);
    if (!StringUtils.isEmpty(seckillOrderJson)) {
        return RespBean.error(RespBeanEnum.REPEATE_ERROR);
    }
    //内存标记,减少Redis访问
    if (EmptyStockMap.get(goodsId)) {
        return RespBean.error(RespBeanEnum.EMPTY_STOCK);
    }
    //预减库存
    Long stock=valueOperations.decrement("seckillGoods:"+goodsId);
    if (stock<0) {
        EmptyStockMap.put(goodsId,true);
        valueOperations.increment("seckillGoods:"+goodsId);
        return RespBean.error(RespBeanEnum.EMPTY_STOCK);
    }
    //请求入队,立即返回排队中
    SeckillMessage message=newSeckillMessage(user,goodsId);
    mqSender.sendsecKillMessage(JsonUtil.object2JsonStr(message));
    return RespBean.success(0);
    }
    /**
    *系统初始化,把商品库存数量加载到Redis
    *
    * @throws Exception
    */
    @Override
    public void afterPropertiesSet()throwsException {
    List<GoodsVo>list=goodsService.findGoodsVo();
    if (CollectionUtils.isEmpty(list)) {
        return;
    }
    list.forEach(goodsVo-> {
        redisTemplate.opsForValue().set("seckillGoods:"+goodsVo.getId(),
        goodsVo.getStockCount());
        EmptyStockMap.put(goodsVo.getId(),false);
        });
    }
}

RabbitMQ秒杀

SeckillMessage.java

packagecom.xxxx.seckill.rabbitmq;
importcom.xxxx.seckill.pojo.User;
importlombok.AllArgsConstructor;
importlombok.Data;
importlombok.NoArgsConstructor;


/**
* @author xiao pan
* @since 1.0.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SeckillMessage {
  private User user;
  private Long goodsId;
}

RabbitMQConfig.java

packagecom.xxxx.seckill.config;
importorg.springframework.amqp.core.Binding;
importorg.springframework.amqp.core.BindingBuilder;
importorg.springframework.amqp.core.Queue;
importorg.springframework.amqp.core.TopicExchange;
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;


/**
* @author xiao pan
* @since 1.0.0
*/
@Configuration
public class RabbitMQConfig {
  private static final String QUEUE="seckillQueue";
  private static final String EXCHANGE="seckillExchange";

  @Bean
  public Queue queue(){
      return new Queue(QUEUE);
   }
  @Bean
  public TopicExchange topicExchange(){
      return new TopicExchange(EXCHANGE);
   }
  @Bean
  public Binding binding01(){
      return BindingBuilder.bind(queue()).to(topicExchange()).with("seckill.#");
   }
}

MQSender.java

packagecom.xxxx.seckill.rabbitmq;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.amqp.rabbit.core.RabbitTemplate;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Service;


/**
* @author xiao pan
* @since 1.0.0
*/
@Service
@Slf4j
public class MQSender {
  @Autowired
  private RabbitTemplate rabbitTemplate;

  public void sendsecKillMessage(String message) {
      log.info("发送消息:"+message);
      rabbitTemplate.convertAndSend("seckillExchange","seckill.msg",message);
   }
}

MQReceiver.java

packagecom.xxxx.seckill.rabbitmq;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.service.IGoodsService;
importcom.xxxx.seckill.service.IOrderService;
importcom.xxxx.seckill.util.JsonUtil;
importcom.xxxx.seckill.vo.GoodsVo;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.amqp.rabbit.annotation.RabbitListener;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.stereotype.Service;
importorg.springframework.util.StringUtils;


/**
* @author xiao pan
* @since 1.0.0
*/
@Service
@Slf4j
public class MQReceiver {
  @Autowired
  private IGoodsService goodsService;
  @Autowired
  private RedisTemplate redisTemplate;
  @Autowired
  private IOrderService orderService;

  @RabbitListener(queues="seckillQueue")
  public void receive(String msg) {
      log.info("QUEUE接受消息:"+msg);
      SeckillMessage message=JsonUtil.jsonStr2Object(msg,
      SeckillMessage.class);
      Long goodsId=message.getGoodsId();
      User user=message.getUser();
      GoodsVo goods=goodsService.findGoodsVoByGoodsId(goodsId);
      //判断库存
      if (goods.getStockCount()<1) {
        return;
      }
      //判断是否重复抢购
      // SeckillOrder seckillOrder = seckillOrderService.getOne(new
QueryWrapper<SeckillOrder>().eq("user_id",
      //        user.getId()).eq(
      //        "goods_id",
      //        goodsId));
      String seckillOrderJson= (String)
redisTemplate.opsForValue().get("order:"+user.getId()+":"+goodsId);
      if (!StringUtils.isEmpty(seckillOrderJson)) {
        return;
      }
      orderService.seckill(user,goods);
   }
}

客户端轮询秒杀结果

SeckillController.java

/**
*获取秒杀结果
*
* @param user
* @param goodsId
* @return orderId:成功,-1:秒杀失败,0:排队中
*/
@RequestMapping(value="/result",method=RequestMethod.GET)
@ResponseBody
public RespBean getResult(User user,Long goodsId) {
  if (user==null) {
      return RespBean.error(RespBeanEnum.SESSION_ERROR);
   }
  Long orderId=seckillOrderService.getResult(user,goodsId);
  return RespBean.success(orderId);
}

ISeckillOrderService.java

packagecom.xxxx.seckill.service;
importcom.baomidou.mybatisplus.extension.service.IService;
importcom.xxxx.seckill.pojo.SeckillOrder;
importcom.xxxx.seckill.pojo.User;

/**
* <p>
*服务类
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
public interface ISeckillOrderService extends IService<SeckillOrder> {
  /**
    *获取秒杀结果
    * @param user
    * @param goodsId
    * @return
    */
  Long getResult(User user,Long goodsId);
}

SeckillOrderServiceImpl.java

packagecom.xxxx.seckill.service.impl;
importcom.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
importcom.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
importcom.xxxx.seckill.mapper.SeckillOrderMapper;
importcom.xxxx.seckill.pojo.SeckillOrder;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.service.ISeckillOrderService;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.stereotype.Service;



/**
* <p>
*服务实现类
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
@Service
public class SeckillOrderServiceImpl extends ServiceImpl<SeckillOrderMapper,
SeckillOrder>implements ISeckillOrderService {
  @Autowired
  private SeckillOrderMapper seckillOrderMapper;
  @Autowired
  private RedisTemplate redisTemplate;

  /**
    *获取秒杀结果
    *
    * @param user
    * @param goodsId
    * @return
    */
  @Override
  public Long getResult(User user,Long goodsId) {
      SeckillOrder seckillOrder=seckillOrderMapper.selectOne(new
QueryWrapper<SeckillOrder>().eq("user_id",user.getId()).eq("goods_id",
goodsId));
      if (null!=seckillOrder) {
        return seckillOrder.getId();
      }else {
        if (redisTemplate.hasKey("isStockEmpty:"+goodsId)) {
            return-1L;
         }else {
            return0L;
         }
      }
   }
}

OrderServiceImpl.java

/**
*秒杀
*
* @param user
* @param goods
* @return
*/
@Override
@Transactional
public Order seckill(User user,GoodsVo goods) {
  ValueOperations valueOperations=redisTemplate.opsForValue();
  //秒杀商品表减库存
  SeckillGoods seckillGoods=seckillGoodsService.getOne(new
QueryWrapper<SeckillGoods>().eq("goods_id",goods.getId()));
  boolean seckillGoodsResult=seckillGoodsService.update(
        new UpdateWrapper<SeckillGoods>().setSql("stock_count = stock_count-
1").eq("goods_id",goods.getId()).gt("stock_count",0));
  // seckillGoodsService.updateById(seckillGoods);
  if (seckillGoods.getStockCount()<1) {
      //判断是否还有库存
      valueOperations.set("isStockEmpty:"+goods.getId(),"0");
      return null;
   }
  //生成订单
  Order order=newOrder();
  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(newDate());
  orderMapper.insert(order);
  //生成秒杀订单
  SeckillOrderseckillOrder=newSeckillOrder();
  seckillOrder.setOrderId(order.getId());
  seckillOrder.setUserId(user.getId());
  seckillOrder.setGoodsId(goods.getId());
  seckillOrderService.save(seckillOrder);
  valueOperations.set("order:"+user.getId()+":"+goods.getId(),
        JsonUtil.object2JsonStr(seckillOrder));
  return order;
}

goodsDetail.htm

function doSeckill() {
    $.ajax({
        url:"/seckill/doSeckill",
        type:"POST",
        data: {
            goodsId:$("#goodsId").val(),
        },
   success:function (data) {
            if (data.code==200) {
                // window.location.href = "/orderDetail.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) {
                varresult=data.obj;
                if (result<0) {
                    layer.msg("对不起,秒杀失败!");
                }elseif (result==0) {
                    setTimeout(function () {
                        getResult(goodsId);
                    },50);
                }else {
                    layer.confirm("恭喜你,秒杀成功!查看订单?", {btn: ["确定","取
消"]},
                        function () {
                            window.location.href="/orderDetail.htm?orderId="+
result;
                        },
                        function () {
                            layer.close();
                        });
                }
            }else {
                layer.msg(data.message);
            }
        },
        error:function () {
            layer.msg("客户端请求错误");
        }
    })
}

测试

项目启动,Redis预加载库存

秒杀成功,数据库及Redis库存数量正确

压测秒杀 

QPS相比之前有一定提升

数据库以及Redis库存数量和订单都正确

八.安全优化

秒杀接口地址隐藏

秒杀开始之前,先去请求接口获取秒杀地址

SeckillController.java

packagecom.xxxx.seckill.controller;
importcom.xxxx.seckill.pojo.User;
importcom.xxxx.seckill.rabbitmq.MQSender;
importcom.xxxx.seckill.rabbitmq.SeckillMessage;
importcom.xxxx.seckill.service.IGoodsService;
importcom.xxxx.seckill.service.IOrderService;
importcom.xxxx.seckill.service.ISeckillOrderService;
importcom.xxxx.seckill.util.JsonUtil;
importcom.xxxx.seckill.vo.GoodsVo;
importcom.xxxx.seckill.vo.RespBean;
importcom.xxxx.seckill.vo.RespBeanEnum;
importorg.springframework.beans.factory.InitializingBean;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.data.redis.core.ValueOperations;
importorg.springframework.data.redis.core.script.DefaultRedisScript;
importorg.springframework.stereotype.Controller;
importorg.springframework.util.CollectionUtils;
importorg.springframework.util.StringUtils;
importorg.springframework.web.bind.annotation.PathVariable;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RequestMethod;
importorg.springframework.web.bind.annotation.ResponseBody;
importjava.util.Collections;
importjava.util.HashMap;
importjava.util.List;
importjava.util.Map;


/**
* <p>
*前端控制器
* </p>
*
* @author xiao pan
* @since 1.0.0
*/
@Controller
@RequestMapping("/seckill")
public class SeckillController implements InitializingBean {
  @Autowired
  private IGoodsService goodsService;
  @Autowired
  private ISeckillOrderService seckillOrderService;
  @Autowired
  private IOrderService orderService;
  @Autowired
  private RedisTemplate redisTemplate;
  @Autowired
  private MQSender mqSender;
  @Autowired
  private DefaultRedisScript<Long>script;
  private Map<Long,Boolean> EmptyStockMap=new HashMap<>();


    /**
    *秒杀
    *
    * @param user
    * @param goodsId
    * @return
    */
  @RequestMapping(value="/{path}/doSeckill",method=RequestMethod.POST
  @ResponseBody
  public RespBean doSeckill(@PathVariable String path,User user,Long goodsId){
      if (user==null) {
        return RespBean.error(RespBeanEnum.SESSION_ERROR);
      }
      ValueOperations valueOperations=redisTemplate.opsForValue();
      boolean check=orderService.checkPath(user,goodsId,path);
      if (!check){
        return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);
      }
      //判断是否重复抢购
      String seckillOrderJson= (String)valueOperations.get("order:"+
user.getId()+":"+goodsId);
      if (!StringUtils.isEmpty(seckillOrderJson)) {
        return RespBean.error(RespBeanEnum.REPEATE_ERROR);
      }
      //内存标记,减少Redis访问
      if (EmptyStockMap.get(goodsId)) {
        return RespBean.error(RespBeanEnum.EMPTY_STOCK);
      }
      //预减库存
      // Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
      // if (stock < 0) {
      //     EmptyStockMap.put(goodsId,true);
      //     valueOperations.increment("seckillGoods:" + goodsId);
      //     return RespBean.error(RespBeanEnum.EMPTY_STOCK);
      // }
      Long stock= (Long)redisTemplate.execute(script,
Collections.singletonList("seckillGoods:"+goodsId),
            Collections.EMPTY_LIST);
      if (stock<=0) {
        EmptyStockMap.put(goodsId,true);
        return RespBean.error(RespBeanEnum.EMPTY_STOCK);
      }
      //请求入队,立即返回排队中
      SeckillMessage message=new SeckillMessage(user,goodsId);
      mqSender.sendsecKillMessage(JsonUtil.object2JsonStr(message));
      return RespBean.success(0);
   }

  /**
    *获取秒杀地址
    *
    * @param user
    * @param goodsId
    * @return
    */
  @RequestMapping(value="/path",method=RequestMethod.GET)
  @ResponseBody
  public RespBean getPath(User user,Long goodsId) {
      if (user==null) {
        return RespBean.error(RespBeanEnum.SESSION_ERROR);
      }
      String str=orderService.createPath(user,goodsId);
      return RespBean.success(str);
}

IOrderService.java

/**
*验证秒杀地址
* @param user
* @param goodsId
* @param path
* @return
*/
boolean checkPath(User user,Long goodsId,String path);

/**
*生成秒杀地址
* @param user
* @param goodsId
* @return
*/
String createPath(User user,Long goodsId);

OrderServiceImpl.java

/**
*验证请求地址
*
* @param user
* @param goodsId
* @param path
* @return
*/
@Override
public boolean checkPath(User user,Long goodsId,String path) {
  if (user==null||StringUtils.isEmpty(path)){
      return false;
   }
  String redisPath= (String)redisTemplate.opsForValue().get("seckillPath:"+
 user.getId()+":"+goodsId);
  return path.equals(redisPath);
}

/**
*生成秒杀地址
*
* @param user
* @param goodsId
* @return
*/
@Override
public String createPath(User user,Long goodsId) {
  String str=MD5Util.md5(UUIDUtil.uuid()+"123456");
  redisTemplate.opsForValue().set("seckillPath:"+user.getId()+":"+
  goodsId,str,60,TimeUnit.SECONDS);
  return str;
}

goodsDetail.htm

function getSeckillPath() {
    var goodsId=$("#goodsId").val();
    g_showLoading();
    $.ajax({
        url:"/seckill/path",
        type:"GET",
        data: {
            goodsId:goodsId,
        },
        success:function (data) {
            if (data.code==200) {
                varpath=data.obj;
                doSeckill(path);
            }else {
                layer.msg(data.message);
            }
        }
        ,
        error:function () {
            layer.msg("客户端请求错误");
        }
    })
}

function doSeckill(path) {
    $.ajax({
        url:"/seckill/"+path+"/doSeckill",
        type:"POST",
        data: {
            goodsId:$("#goodsId").val(),
        },
        success:function (data) {
            if (data.code==200) {
                // window.location.href = "/orderDetail.htm?orderId=" +
data.obj.id;
                getResult($("#goodsId").val());
            }else {
                layer.msg(data.message);
            }
        },
        error:function () {
            layer.msg("客户端请求错误");
        }
    })
}

先去请求接口获取秒杀地址

秒杀真正地址

图形验证码 

点击秒杀开始前,先输入验证码,分散用户的请求

生成验证码

引入依赖pom.xml

<!--验证码 -->
<dependency>
    <groupId>com.github.whvcse</groupId>
    <artifactId>easy-captcha</artifactId>
    <version>1.6.2</version>
</dependency>

SeckillController.java

    /**
     * 验证码
     *
     * @param user
     * @param goodsId
     * @return
     */
    @RequestMapping(value = "/captcha", method = RequestMethod.GET)
    public void verifyCode(UserModel user, Long goodsId, HttpServletResponse response) {
        if (null == user || goodsId < 0) {
            throw new GlobalException(RespBeanEnum.REQUEST_ILLEGAL);
        }
        // 设置请求头为输出图片类型
        response.setContentType("image/jpg");
        // 不需要缓存, 每次获取确保是新的验证码
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "no-cache");
        // 永不失效
        response.setDateHeader("Expires", 0);
        // 生成验证码,将结果放入redis
        ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 32, 3);
        // 存入redis, 设置过期时间为60秒
        redisTemplate.opsForValue().set("captcha:" + user.getMobile() + ":" + goodsId, captcha.text(),60, TimeUnit.SECONDS);
        try {
            captcha.out(response.getOutputStream());
        } catch (IOException e) {
            log.error("验证码生成失败", e.getMessage());
        }
    }

goodsDetail.htm

divclass="row">
    <divclass="form-inline">
        <imgid="captchaImg"width="130"height="32"style="display: none"
onclick="refreshCaptcha()"/>
        <inputid="captcha"class="form-control"style="display: none"/>
        <buttonclass="btn btn-primary"type="button"id="buyButton"
                onclick="getSeckillPath()">立即秒杀
        </button>
    </div>
</div>
<script>
function refreshCaptcha() {
        $("#captchaImg").attr("src","/seckill/captcha?goodsId="+
$("#goodsId").val()+"&time="+newDate())
    }
    
    function countDown() {
        var remainSeconds=$("#remainSeconds").val();
        var timeout;
        //秒杀还没开始,倒计时
        if (remainSeconds>0) {
            $("#buyButton").attr("disabled",true);
            $("#seckillTip").html("秒杀倒计时:"+remainSeconds+"秒");
            timeout=setTimeout(function () {
                    // $("#countDown").text(remainSeconds - 1);
                    $("#remainSeconds").val(remainSeconds-1);
                    countDown();
                },
                1000
            );
        }
        //秒杀进行中
        elseif (remainSeconds==0) {
            $("#buyButton").attr("disabled",false);
            if (timeout) {
                clearTimeout(timeout);
            }
            $("#seckillTip").html("秒杀进行中");
            $("#captchaImg").attr("src","/seckill/captcha?goodsId="+
$("#goodsId").val())
            $("#captchaImg").show();
            $("#captcha").show();
            //秒杀已经结束
        }else {
            $("#buyButton").attr("disabled",true);
            $("#seckillTip").html("秒杀已经结束");
            $("#captchaImg").hide();
            $("#captcha").hide();
        }
    }
</script>

测试

验证验证码 

SeckillController.java

    /**
     * 获取秒杀地址
     *
     * @param user
     * @param goodsId
     * @param captcha
     * @return
     */
    @RequestMapping(value = "/path", method = RequestMethod.GET)
    @ResponseBody
    public RespBean path(UserModel user, Integer goodsId, String captcha) {
        if (user == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }
        boolean check = orderService.checkCaptcha(user, goodsId, captcha);
        if (!check) {
            return RespBean.error(RespBeanEnum.CAPTCHA_ERROR);
        }
        String str = orderService.createPath(user, goodsId);
        return RespBean.success(str);
    }

IOrderService.java

/**
*校验验证码
*
* @param user
* @param goodsId
* @param captcha
* @return
*/
boolean checkCaptcha(User user,Long goodsId,String captcha);

OrderServiceImpl.java

/**
*校验验证码
*
* @param user
* @param goodsId
* @param captcha
* @return
*/
@Override
public boolean checkCaptcha(User user,Long goodsId,String captcha) {
  if (StringUtils.isEmpty(captcha)||null==user||goodsId<0){
       return false;
   }
  String redisCaptcha= (String)redisTemplate.opsForValue().get("captcha:"+
user.getId()+":"+goodsId);
  return redisCaptcha.equals(captcha);
}

goodsDetail.htm

function getSeckillPath() {
    var goodsId=$("#goodsId").val();
    var captcha=$("#captcha").val();
    g_showLoading();
    $.ajax({
        url:"/seckill/path",
        type:"GET",
        data: {
            goodsId:goodsId,
            captcha:captcha
        },
        success:function (data) {
            if (data.code==200) {
                varpath=data.obj;
                doSeckill(path);
            }else {
                layer.msg(data.message);
            }
        }
        ,
        error:function () {
            layer.msg("客户端请求错误");
        }
    })
}

测试

输入错误验证码,提示错误并且无法秒杀

输入正确验证码,正常秒杀

接口限流 

简单接口限流

SeckillController.java

/**
*获取秒杀地址
*
* @param user
* @param goodsId
* @return
*/
@RequestMapping(value="/path",method=RequestMethod.GET)
@ResponseBody
publicRespBeangetPath(User user,Long goodsId,String captcha,
HttpServletRequest request) {
  if (user==null) {
      return RespBean.error(RespBeanEnum.SESSION_ERROR);
   }
  ValueOperations valueOperations=redisTemplate.opsForValue();
  //限制访问次数,5秒内访问5次
  String uri=request.getRequestURI();
  //方便测试
  captcha="0";
  Integer count= (Integer)valueOperations.get(uri+":"+user.getId());
  if (count==null){
      valueOperations.set(uri+":"+user.getId(),1,5,TimeUnit.SECONDS);
   }elseif (count<5){
      valueOperations.increment(uri+":"+user.getId());
   }else {
      return RespBean.error(RespBeanEnum.ACCESS_LIMIT_REACHED);
   }
  boolean check=orderService.checkCaptcha(user,goodsId,captcha);
  if (!check){
      return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
  }
  String str=orderService.createPath(user,goodsId);
  return RespBean.success(str);
}

测试

通用接口限流 

UserContext.java

package com.xxx.seckill.config;

import com.xxx.seckill.pojo.UserModel;

public class UserContext {

    private static ThreadLocal<UserModel> userHolder = new ThreadLocal<>();

    public static void setUser(UserModel user){
        userHolder.set(user);
    }

    public static UserModel gettUser(){
        return userHolder.get();
    }
}

UserArgumentResolver.java

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        return UserContext.gettUser();
    }

AccessInterceptor.java

package com.xxx.seckill.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxx.seckill.pojo.UserModel;
import com.xxx.seckill.service.IUserService;
import com.xxx.seckill.utils.CookieUtil;
import com.xxx.seckill.vo.RespBean;
import com.xxx.seckill.vo.RespBeanEnum;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;

/**
 *拦截器
 */
@Component
public class AccessInterceptor implements HandlerInterceptor {

    @Autowired
    private IUserService userService;
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            UserModel user = getUser(request,response);
            UserContext.setUser(user);
            HandlerMethod hm = (HandlerMethod) handler;
            AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
            if (accessLimit == null) {
                return true;
            }
            int second = accessLimit.second();
            int maxCount = accessLimit.maxCount();
            boolean meedLogin = accessLimit.meedLogin();
            // /seckill/path
            String key = request.getRequestURI();
            // seckill:path
            key = (key.substring(1,key.length())).replace("/",":");
            if (meedLogin) {
                if (user == null) {
                    render(response, RespBeanEnum.SESSION_ERROR);
                    return false;
                }
                key+=":"+user.getMobile();
            }
            ValueOperations valueOperations = redisTemplate.opsForValue();
            Integer count = (Integer) valueOperations.get(key);
            if (count == null) {
                valueOperations.set(key,1,second, TimeUnit.SECONDS);
            } else if (count < maxCount) {
                valueOperations.increment(key);
            } else {
                render(response,RespBeanEnum.ACCESS_LIMIT_REAHCED);
                return false;
            }
        }
        return true;
    }

    /**
     * 构建返回对象
     *
     * @param response
     * @param respBeanEnum
     */
    private void render(HttpServletResponse response, RespBeanEnum respBeanEnum) throws IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        RespBean respBean = RespBean.error(respBeanEnum);
        out.write(new ObjectMapper().writeValueAsString(respBean));
        out.flush();
        out.close();
    }

    /**
     * 获取当前登录用户
     *
     * @param request
     * @param response
     * @return
     */
    private UserModel getUser(HttpServletRequest request, HttpServletResponse response) {
        String ticket = CookieUtil.getCookieValue(request, "userTicket");
        if (StringUtils.isEmpty(ticket)) {
            return null;
        }
        return userService.getUserByCookie(ticket,request,response);
    }
}

WebConfig.java

    /**
     * 添加拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(accessLimitInterceptor);
    }

AccessLimit.java

package com.xxx.seckill.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {

    // 秒
    int second();

    // 次数
    int maxCount();

    // 默认要登录
    boolean meedLogin() default true;
}

SeckillController.java

/**
*获取秒杀地址
*
* @param user
* @param goodsId
* @return
*/
@AccessLimit(second=5,maxCount=5,needLogin=true)
@RequestMapping(value="/path",method=RequestMethod.GET)
@ResponseBody
public RespBean getPath(User user,Long goodsId,String captcha,
HttpServletRequest request) {
  if (user==null) {
      return RespBean.error(RespBeanEnum.SESSION_ERROR);
   }
  //方便测试
  captcha="0";
  boolean check=orderService.checkCaptcha(user,goodsId,captcha);
  if (!check) {
      return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
   }
  String str=orderService.createPath(user,goodsId);
  return RespBean.success(str);
}

测试

发送内容: "秒杀系统"

  • 4
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yixian123.com

谢谢打赏,祝老板心想事成

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值