1环境搭建 pox.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--mybatisplus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependencies>
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
1.1通用公共结果返回对象
RespBeanEnum.java
@Getter
@ToString
@AllArgsConstructor
public enum RespBeanEnum {
//通用
SUCCESS(200, "SUCCESS"),
ERROR(500, "服务端异常"),
//登录模块5002xx
LOGIN_ERROR(500210, "用户名或密码不正确"),
MOBILE_ERROR(500211, "手机号码格式不正确"),
BIND_ERROR(500212, "参数校验异常"),
MOBILE_NOT_EXIST(500213, "手机号码不存在"),
PASSWORD_UPDATE_FAIL(500214, "密码更新失败"),
SESSION_ERROR(500215, "用户不存在"),
//秒杀模块5005xx
EMPTY_STOCK(500500, "库存不足"),
REPEATE_ERROR(500501, "该商品每人限购一件"),
REQUEST_ILLEGAL(500502, "请求非法,请重新尝试"),
ERROR_CAPTCHA(500503, "验证码错误,请重新输入"),
ACCESS_LIMIT_REAHCED(500504, "访问过于频繁,请稍后再试"),
//订单模块5003xx
ORDER_NOT_EXIST(500300, "订单信息不存在"),
;
private final Integer code;
private final String message;
}
RespBean.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {
private long code;
private String message;
private Object obj;
/**
* 功能描述: 成功返回结果
*
*/
public static RespBean success(){
return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBeanEnum.SUCCESS.getMessage(),null);
}
/**
* 功能描述: 成功返回结果
*
* @param:
* @return:
*
*/
public static RespBean success(Object obj){
return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBean.success().getMessage(),obj);
}
/**
* 功能描述: 失败返回结果
*
* @param:
* @return:
*
*/
public static RespBean error(RespBeanEnum respBeanEnum){
return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),null);
}
/**
* 功能描述: 失败返回结果
*
* @param:
* @return:
*/
public static RespBean error(RespBeanEnum respBeanEnum,Object obj){
return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),obj);
}
}
1.2总体结构图
2 分布式会话
2.1 实现登录功能
两次明文加密
客户端:PASS=MD5(明文+固定Salt)
服务端:PASS=MD5(用户属于+随机Salt)
导入MD5依赖
<!-- 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
@Component
public class MD5Util {
public static String md5(String src){
return DigestUtils.md5Hex(src);
}
private static final String salt="1a2b3c4d";
public static String inputPassToFromPass(String inputPass){
String str = "" +salt.charAt(0)+salt.charAt(2)+inputPass+salt.charAt(5)+salt.charAt(4);
return md5(str);
}
public static String formPassToDBPass(String formPass,String salt){
String str = "" +salt.charAt(0)+salt.charAt(2)+formPass+salt.charAt(5)+salt.charAt(4);
return md5(str);
}
public static String inputPassToDBPass(String inputPass,String salt){
String fromPass = inputPassToFromPass(inputPass);
String dbPass = formPassToDBPass(fromPass, salt);
return dbPass;
}
public static void main(String[] args) {
// d3b1294a61a07da9b49b6e22b2cbd7f9
System.out.println(inputPassToFromPass("123456"));
System.out.println(formPassToDBPass("d3b1294a61a07da9b49b6e22b2cbd7f9","1a2b3c4d"));
System.out.println(inputPassToDBPass("123456","1a2b3c4d"));
}
}
2.2 登录功能实现
逆向工程 生成实体类和mapper、service、controller、xml(参考Mybatis-plus)
ValidatorUtil.Class 自定义手机号格式校验
public class ValidatorUtil {
private static final Pattern mobile_pattern = Pattern.compile("[1]([3-9])[0-9]{9}$");
public static boolean isMobile(String mobile){
if (StringUtils.isEmpty(mobile)){
return false;
}
Matcher matcher = mobile_pattern.matcher(mobile);
return matcher.matches();
}
}
@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(LoginVo loginVo) {
log.info(loginVo.toString());
return userService.login(loginVo);
}
}
2.2.1登录接口实现
VO实体类
LoginVo.Class
@Data
public class LoginVo {
@NotNull
@IsMobile
private String mobile;
@NotNull
@Length(min = 32)
private String password;
}
UserServiceImpl
@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>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
<!-- jquery -->
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- jquery-validator -->
<script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script>
<script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script>
<!-- layer -->
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<!-- md5.js -->
<script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
<!-- common.js -->
<script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<form name="loginForm" id="loginForm" method="post" style="width:50%; margin:0 auto">
<h2 style="text-align:center; margin-bottom: 20px">用户登录</h2>
<div class="form-group">
<div class="row">
<label class="form-label col-md-4">请输入手机号码</label>
<div class="col-md-5">
<input id="mobile" name="mobile" class="form-control" type="text" placeholder="手机号码" required="true"
minlength="11" maxlength="11"/>
</div>
<div class="col-md-1">
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<label class="form-label col-md-4">请输入密码</label>
<div class="col-md-5">
<input id="password" name="password" class="form-control" type="password" placeholder="密码"
required="true" minlength="6" maxlength="16"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-5">
<button class="btn btn-primary btn-block" type="reset" onclick="reset()">重置</button>
</div>
<div class="col-md-5">
<button class="btn btn-primary btn-block" type="submit" onclick="login()">登录</button>
</div>
</div>
</form>
</body>
<script>
function login() {
$("#loginForm").validate({
submitHandler: function (form) {
doLogin();
}
});
}
function doLogin() {
g_showLoading();
var inputPass = $("#password").val();
var salt = g_passsword_salt;
var str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
var password = md5(str);
$.ajax({
url: "/login/doLogin",
type: "POST",
data: {
mobile: $("#mobile").val(),
password: password
},
success: function (data) {
layer.closeAll();
if (data.code == 200) {
layer.msg("成功");
window.location.href="/goods/toList";
} else {
layer.msg(data.message);
}
},
error: function () {
layer.closeAll();
}
});
}
</script>
</html>
2.3参数校验
使用validation简化我们的代码
pom.xml
<!-- validation组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
自定义手机号码验证规则
IsMobileValidator.Class
*/
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 context) {
if (required){
return ValidatorUtil.isMobile(value);
}else {
if (StringUtils.isEmpty(value)){
return true;
}else {
return ValidatorUtil.isMobile(value);
}
}
}
}
自定义注解
/**
* 验证手机号
*
* @author zhoubin
* @since 1.0.0
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
public @interface IsMobile {
boolean required() default true;
String message() default "手机号码格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2.4 异常处理
异常处理主要分为编译时异常和运行时异常RuntimeException,前置时通过捕获异常获取到异常信息,后者是通过代码规范来减少运行时异常的发生,Springboot的全局异常处理有两种
使用
@ControllerAdvice
和
@ExceptionHandler
注解。
使用
ErrorController
类
来实现
定义全局异常处理类
GlobalException
/**
* 全局异常
*
* @author zhoubin
* @since 1.0.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException {
private RespBeanEnum respBeanEnum; }
GlobalExceptionHandler
@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);
}
}
2.5 优化登录功能
UserArgumentResolver 判断用户是否登录 拦截器完成
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private IUserService service;
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
Class<?> clazz = methodParameter.getParameterType();
return clazz==User.class ;
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = nativeWebRequest.getNativeRequest(HttpServletResponse.class);
String ticket = CookieUtil.getCookieValue(request, "userTicket");
if (StringUtils.isEmpty(ticket)){
return null;
}
return service.getUserByCookie(ticket,request,response);
}
}
WebConfig 配置类
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userArgumentResolver);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
}
}
3 秒杀功能
通过Mybatis-Plus自动生成商品对象,秒杀商品对象,商品订单,秒杀商品订单
3.1商品列表页
GoodsMapper.java
/**
* <p>
* Mapper 接口
* </p>
*
* @author zhoubin
* @since 1.0.0
*/
public interface GoodsMapper extends BaseMapper<Goods> {
/**
* 获取商品列表
* @return
*/
List<GoodsVo> findGoodsVo();
}
GoodsMapper.xml
<select id="findGoodsVo" resultType="com.yrh.seckill.vo.GoodsVo">
SELECT *
FROM t_goods g
LEFT JOIN t_seckill_goods sg
ON g.id=sg.goods_id
</select>
GoodsController 商品控制层
@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";
}
}
goodsList.html 商品信息
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>商品列表</title>
<!-- jquery -->
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- layer -->
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<!-- common.js -->
<script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀商品列表</div>
<table class="table" id="goodslist">
<tr>
<td>商品名称</td>
<td>商品图片</td>
<td>商品原价</td>
<td>秒杀价</td>
<td>库存数量</td>
<td>详情</td>
</tr>
<tr th:each="goods,goodsStat : ${goodsList}">
<td th:text="${goods.goodsName}"></td>
<td><img th:src="@{${goods.goodsImg}}" width="100" height="100"/></td>
<td th:text="${goods.goodsPrice}"></td>
<td th:text="${goods.seckillPrice}"></td>
<td th:text="${goods.stockCount}"></td>
<td><a th:href="'/goodsDetail.htm?goodsId='+${goods.id}">详情</a></td>
</tr>
</table>
</div>
</body>
</html>
效果图
3.2 详情页面
GoodsMapper
/**
* 根据商品id获取商品详情
* @param goodsId
* @return
*/
GoodsVo findGoodsVoByGoodsId(Long goodsId);
GoodsMapper.xml
<select id="findGoodsVoGoodsId" resultType="com.yrh.seckill.vo.GoodsVo">
SELECT *
FROM t_goods g
LEFT JOIN t_seckill_goods sg
ON g.id=sg.goods_id
where g.id=#{goodsId}
</select>
GoodsController
根据商品id跳转到商品详情
/**
* 跳转商品详情页
*
* @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 = new Date();
//秒杀状态
int secKillStatus = 0;
//剩余开始时间
int remainSeconds = 0;
//秒杀还未开始
if (nowDate.before(startDate)) {
remainSeconds = (int) ((startDate.getTime()-nowDate.getTime())/1000);
// 秒杀已结束
} else if (nowDate.after(endDate)) {
secKillStatus = 2;
remainSeconds = -1;
// 秒杀中
} else {
secKillStatus = 1;
remainSeconds = 0;
}
model.addAttribute("secKillStatus",secKillStatus);
model.addAttribute("remainSeconds",remainSeconds);
return "goodsDetail"; }
详情页面
goodsDetail.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>商品详情</title>
<!-- jquery -->
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- layer -->
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<!-- common.js -->
<script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀商品详情</div>
<div class="panel-body">
<span th:if="${user eq null}"> 您还没有登录,请登陆后再操作<br/></span>
<span>没有收货地址的提示。。。</span>
</div>
<table class="table" id="goods">
<tr>
<td>商品名称</td>
<td colspan="3" th:text="${goods.goodsName}"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="3"><img th:src="@{${goods.goodsImg}}" width="200" height="200"/></td>
</tr>
<tr>
<td>秒杀开始时间</td>
<td th:text="${#dates.format(goods.startDate,'yyyy-MM-dd HH:mm:ss')}"></td>
<td id="seckillTip">
<input type="hidden" id="remainSeconds" th:value="${remainSeconds}">
<span th:if="${secKillStatus eq 0}">秒杀倒计时:<span id="countDown" th:text="${remainSeconds}"></span>秒
</span>
<span th:if="${secKillStatus eq 1}">秒杀进行中</span>
<span th:if="${secKillStatus eq 2}">秒杀已结束</span>
</td>
<td>
<form id="secKillForm" method="post" action="/secKill/doSecKill">
<input type="hidden" name="goodsId" th:value="${goods.id}">
<button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button>
</form>
</td>
</tr>
<tr>
<td>商品原价</td>
<td colspan="3" th:text="${goods.goodsPrice}"></td>
</tr>
<tr>
<td>秒杀价</td>
<td colspan="3" th:text="${goods.seckillPrice}"></td>
</tr>
<tr>
<td>库存数量</td>
<td colspan="3" th:text="${goods.stockCount}"></td>
</tr>
</table>
</div>
</body>
<script>
$(function () {
countDown();
});
function countDown() {
var remainSeconds = $("#remainSeconds").val();
var timeout;
//秒杀还未开始
if (remainSeconds > 0) {
$("#buyButton").attr("disabled", true);
timeout = setTimeout(function () {
$("#countDown").text(remainSeconds - 1);
$("#remainSeconds").val(remainSeconds - 1);
countDown();
}, 1000);
// 秒杀进行中
} else if (remainSeconds == 0) {
if (timeout) {
clearTimeout(timeout);
}
$("#buyButton").attr("disabled", false);
$("#seckillTip").html("秒杀进行中")
} else {
$("#seckillTip").html("秒杀已经结束");
$("#buyButton").attr("disabled", true);
}
};
</script>
</html>
效果图 秒杀未开始
秒杀进行中
秒杀结束
3.3 秒杀功能实现
IOrderService.java
/**
* 秒杀
* @param user
* @param goods
* @return
*/
Order seckill(User user, GoodsVo goods);
订单接口实现类
OrderServiceImpl
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
IOrderService {
@Autowired
private ISeckillGoodsService seckillGoodsService;
@Autowired
private IGoodsService goodsService;
@Autowired
private OrderMapper orderMapper;
@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(new Date());
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 秒杀控制层
思路:判断用户是否登录
判断商品是否还有库存
判断改商品用户是否重复购买
@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";
}
}
订单详情页
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>商品详情</title>
<!-- jquery -->
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- layer -->
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<!-- common.js -->
<script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀订单详情</div>
<table class="table" id="order">
<tr>
<td>商品名称</td>
<td th:text="${goods.goodsName}" colspan="3"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="2"><img th:src="@{${goods.goodsImg}}" width="200"
height="200"/></td>
</tr>
<tr>
<td>订单价格</td>
<td colspan="2" th:text="${order.goodsPrice}"></td>
</tr>
<tr>
<td>下单时间</td>
<td th:text="${#dates.format(order.createDate, 'yyyy-MM-dd HH:mm:ss')}" colspan="2"></td>
</tr>
<tr>
<td>订单状态</td>
<td>
<span th:if="${order.status eq 0}">未支付</span>
<span th:if="${order.status eq 1}">待发货</span>
<span th:if="${order.status eq 2}">已发货</span>
<span th:if="${order.status eq 3}">已收货</span>
<span th:if="${order.status eq 4}">已退款</span>
<span th:if="${order.status eq 5}">已完成</span>
</td>
<td>
<button class="btn btn-primary btn-block" type="submit" id="payButton">立即支付
</button>
</td>
</tr>
<tr>
<td>收货人</td>
<td colspan="2">XXX 18012345678</td>
</tr>
<tr>
<td>收货地址</td>
<td colspan="2">上海市浦东区世纪大道</td>
</tr>
</table>
</div>
</body>
</html>
4 页面优化
4.1 页面缓存
/**
* @author: yrh
* @Description: 商品列表
* * window优化前 吞吐量:452.55558136798504
* 页面静态化
* * window优化后 吞吐量:950.8475220913575
* @return: null
*/
@RequestMapping(value = "/toList",produces = "text/html;charset=utf-8")
@ResponseBody
public String toList(Model model,User user,
HttpServletRequest request,
HttpServletResponse response){
// if (StringUtils.isEmpty(ticket)){
// return "login";
// }
// User user = service.getUserByCookie(ticket, request, response);
// if (null==user){
// return "login";
// }
//获取缓存
ValueOperations operations = redisTemplate.opsForValue();
String html = (String) operations.get("goodsList");
if (!StringUtils.isEmpty(html)){
return html;
}
model.addAttribute("user",user);
model.addAttribute("goodsList",goodsService.findGoodsVo());
//缓存为空
WebContext context=new WebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap());
html= thymeleafViewResolver.getTemplateEngine().process("goodsList",context);
if (!StringUtils.isEmpty(html)){
operations.set("goodsList",html,60, TimeUnit.SECONDS);
}
return html;
}
详情页面缓存
/**
* @author: yrh
* @Description: 商品详情
*
* @return: null
*/
@RequestMapping(value = "/toDetail2/{goodsId}",produces = "text/html;charset=utf-8")
@ResponseBody
public String toDetail2(Model model,User user,@PathVariable Long goodsId,
HttpServletRequest request,
HttpServletResponse response){
ValueOperations operations = redisTemplate.opsForValue();
//是否在缓存当中
String html = (String) operations.get("goodsDetail:" + goodsId);
if (!StringUtils.isEmpty(html)){
return html;
}
model.addAttribute("user",user);
GoodsVo goodsVo = goodsService.findGoodsVoGoodsId(goodsId);
Date startDate = goodsVo.getStartDate();
Date endDate = goodsVo.getEndDate();
Date nowDate = new Date();
//秒杀状态
int secKillStatus = 0;
//秒杀倒计时
int remainSeconds = 0;
//秒杀还未开始
if (nowDate.before(startDate)) {
remainSeconds = ((int) ((startDate.getTime() - nowDate.getTime()) / 1000));
} else if (nowDate.after(endDate)) {
// 秒杀已结束
secKillStatus = 2;
remainSeconds = -1;
} else {
//秒杀中
secKillStatus = 1;
remainSeconds = 0;
}
model.addAttribute("goods",goodsVo);
model.addAttribute("secKillStatus",secKillStatus);
model.addAttribute("remainSeconds",remainSeconds);
WebContext context=new WebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap());
html= thymeleafViewResolver.getTemplateEngine().process("goodsDetail",context);
if (!StringUtils.isEmpty(html)){
operations.set("goodsDetail:"+ goodsId,html,60,TimeUnit.SECONDS);
}
return html;
}
4.2 解决库存超卖
减库存时判断库存是否足够
OrderServiceImpl.java
//秒杀商品表减库存
SeckillGoods seckillGoods = seckillGoodsService.getOne(new
QueryWrapper<SeckillGoods>().eq("goods_id",
goods.getId()));
seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().set("stock_count",
seckillGoods.getStockCount()).eq("id", seckillGoods.getId()).gt("stock_count",
0));
// seckillGoodsService.updateById(seckillGoods);
解决同一用户同时秒杀多件商品。
可以通过数据库建立唯一索引避免
商品表创建唯一索引
![](https://i-blog.csdnimg.cn/blog_migrate/3f5a5201a70c811f90a7a3466ef0a49d.png)
将秒杀订单存放在redis中,方便查询是否重复
OrderServiceImpl.java
@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()));
seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
// seckillGoodsService.updateById(seckillGoods);
//修改后的秒杀商品表减库存
boolean seckillGoodsResult = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>()
.setSql("stock_count=" + "stock_count-1").eq("goods_id", goods.getId()).gt("stock_count", 0));
if (seckillGoods.getStockCount() < 1) {
//判断是否还有库存
valueOperations.set("isStockEmpty:" + goods.getId(), "0");
return null;
}
//生成订单
Order order = new Order();
order.setUserId(user.getId());
order.setGoodsId(goods.getId());
order.setDeliveryAddrId(0L);
order.setGoodsName(goods.getGoodsName());
order.setGoodsCount(1);
order.setGoodsPrice(seckillGoods.getSeckillPrice());
order.setOrderChannel(1);
order.setStatus(0);
order.setCreateDate(new Date());
orderMapper.insert(order);
//生成秒杀订单
//生成秒杀订单
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.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
@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);
}
}
5. 接口优化
思路:减少数据库的访问
1,系统初始化的时候把商品库存压入到缓存当中
2,收到请求redis预减库存,库存不足时直接返回,否则进入下一步
3,请求入队,立刻返回排队中
4,请求出队,生成订单和秒杀订单
5,客服端轮询,是否秒杀成功
5.1 Redis操作库存
实现InitializingBean接口 重新初始方法 向redis中压入库存
public class SecKillController implements InitializingBean
/**
* @author: yrh
* @Description: 系统初始化,把商品库存数量加载到Redis
* throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
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);
});
}
5.2 RabbitMQ秒杀
SeckillMessage.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SeckillMessage {
private User user;
private Long goodsId;
}
配置RabbitMQ队列和交换机规则
RabbitTopicConfig.class
@Configuration
public class RabbitTopicConfig {
private static final String QUEUE="seckillQueue";
private static final String EXCHANGE="seckillExchange";
private static final String ROUTINGKEY="seckill.#";
//秒杀队列
@Bean
public Queue queue(){
return new Queue(QUEUE);
}
//声明交换机
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(EXCHANGE);
}
//绑定交换机和队列
@Bean
public Binding binding(){
return BindingBuilder.bind(queue()).to(topicExchange()).with(ROUTINGKEY);
}
}
信息发送者
MQSender.class
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendSecKillMessage(Object msg){
log.info("发送消息:"+msg);
rabbitTemplate.convertAndSend("seckillExchange","seckill.msg",msg);
}
}
消息消费者 用来异步下单
MQReceiver.class
@Service
@Slf4j
public class MQReceiver {
@Autowired
private IGoodsService goodsService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private IOrderService orderService;
// @RabbitListener(queues = "queue")
// public void receive(Object msg){
// log.info("接收消息:"+msg);
// }
// @RabbitListener(queues = "queue_topic03")
// public void receive01(Object msg){
// log.info("接收消息:"+msg);
// }
// @RabbitListener(queues = "queue_topic04")
// public void receive02(Object msg){
// log.info("接收消息:"+msg);
// }
@RabbitListener(queues = "seckillQueue")
public void receive(String msg) {
log.info("接收消息:" + msg);
//json转对象
SeckillMessage message = JsonUtil.jsonStr2Object(msg, SeckillMessage.class);
User user = message.getUser();
Long goodsId = message.getGoodsId();
GoodsVo goodsVo = goodsService.findGoodsVoGoodsId(goodsId);
//判断库存
if (goodsVo.getStockCount() < 1) {
return;
}
//判断是否是重复抢购
String seckillOrderJson = (String) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
if (!StringUtils.isEmpty(seckillOrderJson)){
return;
}
orderService.seckill(user,goodsVo);
}
}
修改秒杀接口 下单操作先进入队列
@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(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);
}
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 = new SeckillMessage(user, goodsId);
mqSender.sendsecKillMessage(JsonUtil.object2JsonStr(message));
return RespBean.success(0);
}
5.3 客户端轮询秒杀结果
SeckillController.class
/**
* @author: yrh
* @Description: 轮询秒杀接口
* @return: orderId :成功,-1失败,0排队中
*/
@RequestMapping(value = "/result", method = RequestMethod.GET)
@ResponseBody
public RespBean result(User user, Long goodsId) {
if (null == user) {
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
Long orderId = seckillOrderService.getResult(user, goodsId);
return RespBean.success(orderId);
}
ISeckillOrderService.class
Long getResult(User user, Long goodsId);
接口实现类
SeckillOrderServiceImpl.class
@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.getOrderId();
} else {
if ( redisTemplate.hasKey("isStockEmpty:" + goodsId)){
return -1L;
}else {
return 0L;
}
}
}