前端 : Thymeleaf | Bootstrap | Jquery
后端 : SpringBoot | MyBatisPlus | Lombok
中间件 : Redis | RabbitMQ
本短文完成项目搭建, 分布式 Session 和秒杀功能 三个小模块;
秒杀其实主要解决两个问题,一个是并发读,一个是并发写。
并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。另外,我们还要针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。
秒杀的整体架构可以概括为“稳、准、快”几个关键字。
所谓“稳”,就是整个系统架构要满足高可用,流量符合预期时肯定要稳定,就是超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。
然后就是**“准”,就是秒杀 10 台 手机,那就只能成交 10 台,多一台少一台都不行。一旦库存不对,那平台就要承担损失,所以“准”就是要求保证数据的一致性。
最后再看“快”**,“快”其实很好理解,它就是说系统的性能要足够高,否则你怎么支撑这么大的流量呢?不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就完美了。
所以从技术角度上看“稳、准、快”,就对应了我们架构上的高可用、一致性和高性能的要求
Java秒杀上篇
业务层
package com.itkaka.seckill.service;
import com.itkaka.seckill.pojo.Order;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itkaka.seckill.pojo.User;
import com.itkaka.seckill.vo.GoodsVo;
/**
*
* 服务类
*
*
* @author itkaka
* @since 2023-05-11
*/
public interface IOrderService extends IService<Order> {
// 简单 秒杀功能
Order seckill(User user, GoodsVo goodsVo);
}
package com.itkaka.seckill.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.itkaka.seckill.pojo.Order;
import com.itkaka.seckill.mapper.OrderMapper;
import com.itkaka.seckill.pojo.SeckillGoods;
import com.itkaka.seckill.pojo.SeckillOrder;
import com.itkaka.seckill.pojo.User;
import com.itkaka.seckill.service.IGoodsService;
import com.itkaka.seckill.service.IOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itkaka.seckill.service.ISeckillGoodsService;
import com.itkaka.seckill.service.ISeckillOrderService;
import com.itkaka.seckill.vo.GoodsVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Date;
/**
*
* 服务实现类
*
*
* @author itkaka
* @since 2023-05-11
*/
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {
@Resource
private ISeckillGoodsService seckillGoodsService;
@Autowired
private OrderMapper orderMapper;
@Autowired
private ISeckillOrderService seckillOrderService;
@Autowired
private IGoodsService goodsService;
@Autowired
private RedisTemplate redisTemplate;
// 秒杀
@Override
@Transactional
public Order seckill(User user, GoodsVo goodsVo) {
// 秒杀商品减库存
SeckillGoods seckillGoods = seckillGoodsService.getOne(new
QueryWrapper<SeckillGoods>().eq("goods_id",goodsVo.getId()));
seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
seckillGoodsService.updateById(seckillGoods);
// 生成订单
Order order = new Order();
order.setUserId(user.getId());
order.setGoodsId(goodsVo.getId());
order.setDeliverAddrId(0);
order.setGoodsName(goodsVo.getGoodsName());
order.setGoodsCount(1);
order.setGoodsPrice(seckillGoods.getSeckillPrice());
order.setOrderChannel(1);
order.setStatus(0);
order.setCreateDate(new Date());
orderMapper.insert(order);
//生成秒杀订单
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setUserId(user.getId());
seckillOrder.setOrderId(order.getId());
seckillOrder.setGoodsId(goodsVo.getId());
seckillOrderService.save(seckillOrder);
//valueOperations.set("order:" + user.getId() + ":" + goods.getId(), seckillOrder);
return order;
}
}
控制层 : 新建 SeckillController 接口
package com.itkaka.seckill.controller;
/**
* 秒杀
*/
@Slf4j
@Controller
@RequestMapping("/seckill")
public class SecKillController {
@Autowired
private IGoodsService goodsService;
@Autowired
private ISeckillOrderService seckillOrderService;
@Autowired
private IOrderService orderService;
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/doSeckill")
public String doSeckill(Model model,User user,Long goodsId){
if (null == user){
return "login";
}
model.addAttribute("user",user);
GoodsVo goodsVo = goodsService.queryGoodsVoByGoodsId(goodsId);
// 判断库存
if (goodsVo.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,goodsVo);
model.addAttribute("order",order);
model.addAttribute("goods",goodsVo);
return "orderDetail";
}
}
测试 :
秒杀成功进去定安详情 [注意查看库存是否正确扣减,订单是否正确生成]
两个限制条件 : ①限制超卖(库存不足) ②单品每人限购一件(重复抢购)
下载安装
官网 : https://jmeter.apache.org/
下载地址 : https://jmeter.apache.org/download_jmeter.cgi
下载解压后直接在 bin 目录里双击 jmeter.bat 即可启动(Lunix系统通过 jmeter.sh 启动)
修改中文
Options --> Choose Language --> Chinese(Simplified)
测试demo
我们先使用JMeter测试一下跳转商品列表页的接口。
首先创建线程组,步骤:添加–> 线程(用户) --> 线程组
Ramp-up 指在几秒之内启动指定线程数
创建HTTP请求默认值,步骤:添加–> 配置元件 --> HTTP请求默认值
添加测试接口,步骤:添加 --> 取样器 --> HTTP请求
查看输出结果,步骤:添加 --> 监听器 --> 聚合报告/图形结果/用表格察看结果
启动即可在监听器看到对应的结果
准备测试接口
package com.itkaka.seckill.controller;
import com.itkaka.seckill.pojo.User;
import com.itkaka.seckill.vo.RespBean;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
*
* 前端控制器
*
*
* @author itkaka
* @since 2023-05-11
*/
@RestController
@RequestMapping("/user")
public class UserController {
// 用户信息(测试)
@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次 压测商品列表接口,测试 三次,查看结果
HTTP 请求
结果
压测秒杀接口
新建用户
使用工具类往数据库插入5000用户,并且调用登录接口获取token,写入config.txt
config.txt
配置秒杀接口测试
线程组
HTTP 请求默认值
CVS 数据文件设置
HTTP Cookie管理器
HTTP 请求
结果
此时, 已经出现超卖问题
GoodsController 修改完善 ;
// 将页面存入缓存 Redis 提速
// 跳转商品列表页
@RequestMapping(value = "/toList", produces = "text/html;charset=utf-8")
@ResponseBody
//public String toList(Model model, @CookieValue("userTicket") String ticket, HttpServletRequest request,HttpServletResponse response){
public String toList(Model model, User user,
HttpServletRequest request, HttpServletResponse response) {
// Redis中获取页面,如果不为空,直接返回页面
ValueOperations valueOperations = redisTemplate.opsForValue();
String html = (String) valueOperations.get("goodList");
if (!StringUtils.isEmpty(html)){
return html;
}
model.addAttribute("user",user);
model.addAttribute("goodLList",goodsService.queryGoodsVo());
// 如果为空,手动渲染,先存 redis 并返回
WebContext context = new WebContext(request,response, request.getServletContext(),request.getLocale(),model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goodList",context);
if (!org.springframework.util.StringUtils.isEmpty(html)) {
valueOperations.set("goodsList", html, 60, TimeUnit.SECONDS);
}
return html;
// 跳转商品详情页
@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 valueOperations = redisTemplate.opsForValue();
//Redis中获取页面,如果不为空,直接返回页面
String html = (String) valueOperations.get("goodsDetail:" + goodsId);
if (!org.springframework.util.StringUtils.isEmpty(html)) {
return html;
}
model.addAttribute("user", user);
GoodsVo goodsVo = goodsService.queryGoodsVoByGoodsId(goodsId);
Date startDate = goodsVo.getStartDate();
Date endDate = goodsVo.getEndDate();
Date nowDate = new Date();
//秒杀状态
int secKillStatus = 0;
//秒杀倒计时
int remainSeconds = 0;
//秒杀还未开始
if (nowDate.before(startDate)) {
remainSeconds = ((int) ((startDate.getTime() - nowDate.getTime()) / 1000));
} else if (nowDate.after(endDate)) {
// 秒杀已结束
secKillStatus = 2;
remainSeconds = -1;
} else {
//秒杀中
secKillStatus = 1;
remainSeconds = 0;
}
model.addAttribute("remainSeconds", remainSeconds);
model.addAttribute("secKillStatus", secKillStatus);
model.addAttribute("goods", goodsVo);
WebContext context = new WebContext(request, response, request.getServletContext(), request.getLocale(),
model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goodsDetail", context);
if (!org.springframework.util.StringUtils.isEmpty(html)) {
valueOperations.set("goodsDetail:" + goodsId, html, 60, TimeUnit.SECONDS);
}
return html;
// return "goodsDetail";
}
重新运行项目查看效果
测试发现 : 结果对比之前的 QPS提升比较明显
更新对象返回枚举类
package com.itkaka.seckill.vo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
/*
* 公共返回对象枚举类
* */
@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, "密码更新失败"),
;
private final Integer code;
private final String message;
}
更新 IUserService 业务层接口和实现类
// 更新密码
RespBean updatePassword(String userTicket, String password, HttpServletRequest request,
HttpServletResponse response);
// 更新密码
@Override
public RespBean updatePassword(String userTicket, String password, HttpServletRequest request, HttpServletResponse response) {
User user = getUserByCookie(userTicket, request, response);
if (user == null) {
throw new GlobalException(RespBeanEnum.MOBILE_NOT_EXIST);
}
user.setPassword(MD5Util.inputPassToDBPass(password, user.getSlat()));
int result = userMapper.updateById(user);
if (1 == result) {
//删除Redis
redisTemplate.delete("user:" + userTicket);
return RespBean.success();
}
return RespBean.error(RespBeanEnum.PASSWORD_UPDATE_FAIL);
}
package com.itkaka.seckill.vo;
import com.itkaka.seckill.pojo.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
// 商品详情返回对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DetailVo {
private User user;
private GoodsVo goodsVo;
private int secKillStatus;
private int remainSeconds;
}
// 跳转商品详情页
@RequestMapping("/detail/{goodsId}")
@ResponseBody
public RespBean toDetail(User user,@PathVariable Long goodsId){
GoodsVo goodsVo = goodsService.queryGoodsVoByGoodsId(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;
}
DetailVo detailVo = new DetailVo();
detailVo.setUser(user);
detailVo.setGoodsVo(goodsVo);
detailVo.setSecKillStatus(secKillStatus);
detailVo.setRemainSeconds(remainSeconds);
return RespBean.success(detailVo);
}
//展示loading
function g_showLoading(){
var idx = layer.msg('处理中...', {icon: 16,shade: [0.5, '#f5f5f5'],scrollbar: false,offset: '0px', time:100000}) ;
return idx;
}
//salt
var g_passsword_salt="1a2b3c4d"
// 获取url参数
function g_getQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if(r != null) return unescape(r[2]);
return null;
};
//设定时间格式化函数,使用new Date().format("yyyy-MM-dd HH:mm:ss");
Date.prototype.format = function (format) {
var args = {
"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 (var i in args) {
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;
};
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>商品详情title>
<script type="text/javascript" src="/js/jquery.min.js">script>
<link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css"/>
<script type="text/javascript" src="/bootstrap/js/bootstrap.min.js">script>
<script type="text/javascript" src="/layer/layer.js">script>
<script type="text/javascript" src="/js/common.js">script>
head>
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀商品详情div>
<div class="panel-body">
<span id="userTip"> 您还没有登录,请登陆后再操作<br/>span>
<span>没有收货地址的提示。。。span>
div>
<table class="table" id="goods">
<tr>
<td>商品名称td>
<td colspan="3" id="goodsName">td>
tr>
<tr>
<td>商品图片td>
<td colspan="3"><img id="goodsImg" width="200" height="200"/>td>
tr>
<tr>
<td>秒杀开始时间td>
<td id="startTime">td>
<td>
<input type="hidden" id="remainSeconds">
<span id="seckillTip">span>
td>
<td>
<div class="row">
<div class="form-inline">
<img id="captchaImg" width="130" height="32" onclick="refreshCaptcha()" style="display: none"/>
<input id="captcha" class="form-control" style="display: none">
<button class="btn btn-primary" type="button" id="buyButton"
onclick="getSeckillPath()">立即秒杀
<input type="hidden" name="goodsId" id="goodsId">
button>
div>
div>
td>
tr>
<tr>
<td>商品原价td>
<td colspan="3" id="goodsPrice">td>
tr>
<tr>
<td>秒杀价td>
<td colspan="3" id="seckillPrice">td>
tr>
<tr>
<td>库存数量td>
<td colspan="3" id="stockCount">td>
tr>
table>
div>
body>
<script>
$(function () {
// countDown();
getDetails();
});
function refreshCaptcha() {
$("#captchaImg").attr("src", "/seckill/captcha?goodsId=" + $("#goodsId").val() + "&time=" + new Date());
}
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) {
var path = 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("客户端请求错误");
}
})
}
function getResult(goodsId) {
g_showLoading();
$.ajax({
url: "/seckill/result",
type: "GET",
data: {
goodsId: goodsId,
},
success: function (data) {
if (data.code == 200) {
var result = data.obj;
if (result < 0) {
layer.msg("对不起,秒杀失败!");
} else if (result == 0) {
setTimeout(function () {
getResult(goodsId);
}, 50);
} else {
layer.confirm("恭喜你,秒杀成功!查看订单?", {btn: ["确定", "取消"]},
function () {
window.location.href = "/orderDetail.htm?orderId=" + result;
},
function () {
layer.close();
})
}
}
},
error: function () {
layer.msg("客户端请求错误");
}
})
}
function getDetails() {
var goodsId = g_getQueryString("goodsId");
$.ajax({
url: '/goods/detail/' + goodsId,
type: 'GET',
success: function (data) {
if (data.code == 200) {
render(data.obj);
} else {
layer.msg("客户端请求出错");
}
},
error: function () {
layer.msg("客户端请求出错");
}
});
}
function render(detail) {
var user = detail.user;
var goods = detail.goodsVo;
var remainSeconds = detail.remainSeconds;
if (user) {
$("#userTip").hide();
}
$("#goodsName").text(goods.goodsName);
$("#goodsImg").attr("src", goods.goodsImg);
$("#startTime").text(new Date(goods.startDate).format("yyyy-MM-dd HH:mm:ss"));
$("#remainSeconds").val(remainSeconds);
$("#goodsId").val(goods.id);
$("#goodsPrice").text(goods.goodsPrice);
$("#seckillPrice").text(goods.seckillPrice);
$("#stockCount").text(goods.stockCount);
countDown();
}
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);
// 秒杀进行中
} else if (remainSeconds == 0) {
$("#buyButton").attr("disabled", false);
if (timeout) {
clearTimeout(timeout);
}
$("#seckillTip").html("秒杀进行中")
$("#captchaImg").attr("src", "/seckill/captcha?goodsId=" + $("#goodsId").val() + "&time=" + new Date());
$("#captchaImg").show();
$("#captcha").show();
} else {
$("#buyButton").attr("disabled", true);
$("#seckillTip").html("秒杀已经结束");
$("#captchaImg").hide();
$("#captcha").hide();
}
};
script>
html>
/**
* 功能描述: 秒杀
* windows优化前QPS:785
* 缓存QPS:1356
* 优化QPS:2454
*/
@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);
}
// 判断是否重复抢购
SeckillOrder seckillOrder =
(SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
if (seckillOrder != null) {
return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}
// 内存标记,减少Redis的访问
if (EmptyStockMap.get(goodsId)) {
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
// 预减库存
// Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
Long stock = (Long) redisTemplate.execute(script, Collections.singletonList("seckillGoods:" + goodsId),
Collections.EMPTY_LIST);
if (stock < 0) {
EmptyStockMap.put(goodsId, true);
valueOperations.increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage));
return RespBean.success(0);
DOCTYPE HTML>
<html>
<head>
<title>订单详情title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<script type="text/javascript" src="/js/jquery.min.js">script>
<link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css"/>
<script type="text/javascript" src="/bootstrap/js/bootstrap.min.js">script>
<script type="text/javascript" src="/layer/layer.js">script>
<script type="text/javascript" src="/js/common.js">script>
head>
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀订单详情div>
<table class="table" id="order">
<tr>
<td>商品名称td>
<td id="goodsName" colspan="3">td>
tr>
<tr>
<td>商品图片td>
<td colspan="2"><img id="goodsImg" width="200" height="200"/>td>
tr>
<tr>
<td>订单价格td>
<td colspan="2" id="goodsPrice">td>
tr>
<tr>
<td>下单时间td>
<td id="createDate" colspan="2">td>
tr>
<tr>
<td>订单状态td>
<td id="status">
td>
<td>
<button class="btn btn-primary btn-block" type="submit" id="payButton">立即支付button>
td>
tr>
<tr>
<td>收货人td>
<td colspan="2">XXX 18012345678td>
tr>
<tr>
<td>收货地址td>
<td colspan="2">上海市浦东区世纪大道td>
tr>
table>
div>
<script>
$(function () {
getOrderDetail();
});
function getOrderDetail() {
var orderId = 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) {
var goods = detail.goodsVo;
var order = detail.order;
$("#goodsName").text(goods.goodsName);
$("#goodsImg").attr("src", goods.goodsImg);
$("#goodsPrice").text(order.goodsPrice);
$("#createDate").text(new Date(order.createDate).format("yyyy-MM-dd HH:mm:ss"));
var status = order.status;
var statusText = "";
switch (status) {
case 0:
statusText = "未支付";
break;
case 1:
statusText = "待发货";
break;
case 2:
statusText = "已发货";
break;
case 3:
statusText = "已收货";
break;
case 4:
statusText = "已退款";
break
case 5:
statusText = "已完成";
break;
}
$("#status").text(statusText);
}
script>
body>
html>
同步修改配置文件,注意缩进
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/
测试
订单接口的 控制层
package com.itkaka.seckill.controller;
import com.itkaka.seckill.pojo.User;
import com.itkaka.seckill.service.IOrderService;
import com.itkaka.seckill.vo.OrderDetailVo;
import com.itkaka.seckill.vo.RespBean;
import com.itkaka.seckill.vo.RespBeanEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
* 前端控制器
*
*
* @author itkaka
* @since 2023-05-11
*/
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private IOrderService orderService;
// 订单详情
@RequestMapping("detail")
public RespBean detail(User user,Long orderId){
if (user == null){
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
OrderDetailVo detailVo = orderService.detail(orderId);
return RespBean.success(detailVo);
}
}
订单业务层的接口和实现类修改
package com.itkaka.seckill.service;
import com.itkaka.seckill.pojo.Order;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itkaka.seckill.pojo.User;
import com.itkaka.seckill.vo.GoodsVo;
import com.itkaka.seckill.vo.OrderDetailVo;
/**
*
* 服务类
*
*
* @author itkaka
* @since 2023-05-11
*/
public interface IOrderService extends IService<Order> {
// 简单 秒杀功能
Order seckill(User user, GoodsVo goodsVo);
OrderDetailVo detail(Long orderId); // 订单详情
}
// 订单详情
@Override
public OrderDetailVo detail(Long orderId) {
if (orderId == null){
throw new GlobalException(RespBeanEnum.ORDER_NOT_EXIST);
}
Order order = orderMapper.selectById(orderId);
GoodsVo goodsVo = goodsService.queryGoodsVoByGoodsId( (order.getGoodsId()).longValue());
OrderDetailVo detailVo = new OrderDetailVo();
detailVo.setOrder(order);
detailVo.setGoodsVo(goodsVo);
return detailVo;
}
package com.itkaka.seckill.vo;
// 订单详情返回对象
import com.itkaka.seckill.pojo.Order;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderDetailVo {
private Order order;
private GoodsVo goodsVo;
}
解决库存超卖
扣减库存时候 判断库存是否足够
//秒杀商品表减库存
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);
解决同一个用户同时秒杀多件商品
可以通过数据库建立唯一索引避免
将秒杀订单信息存入 Redis ,方便判断是都重复抢购时进行查询
// 秒杀
@Override
@Transactional
public Order seckill(User user, GoodsVo goodsVo) {
ValueOperations valueOperations = redisTemplate.opsForValue();
// 秒杀商品减库存
SeckillGoods seckillGoods = seckillGoodsService.getOne(new
QueryWrapper<SeckillGoods>().eq("goods_id",goodsVo.getId()));
seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
//seckillGoodsService.updateById(seckillGoods);
// 解决库存超卖 扣减库存时候判断库存是否足够
// seckillGoodsService.update(new UpdateWrapper().set("stock_count",
// seckillGoods.getStockCount()).eq("id",seckillGoods.getId()).gt("stock_count",0));
//
boolean seckillGoodsResult = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().set("stock_count",
seckillGoods.getStockCount()).eq("id",seckillGoods.getId()).gt("stock_count",0));
if ((seckillGoods.getStockCount())<1){
// 判断是否还有库存
valueOperations.set("isStockEmpty:"+ goodsVo.getId(),"0");
return null;
}
// 生成订单
Order order = new Order();
order.setUserId(user.getId());
order.setGoodsId(goodsVo.getId());
order.setDeliverAddrId(0);
order.setGoodsName(goodsVo.getGoodsName());
order.setGoodsCount(1);
order.setGoodsPrice(seckillGoods.getSeckillPrice());
order.setOrderChannel(1);
order.setStatus(0);
order.setCreateDate(new Date());
orderMapper.insert(order);
//生成秒杀订单
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setUserId(user.getId());
seckillOrder.setOrderId(order.getId());
seckillOrder.setGoodsId(goodsVo.getId());
seckillOrderService.save(seckillOrder);
valueOperations.set("order:" + user.getId() + ":" + goodsVo.getId(), seckillOrder);
return order;
}
@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().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);
}
安装 : 官网 https://www.erlang-solutions.com/resources/download.html
安装erlang Linux命令: yum -y install esl-erlang_23.0.2-1_centos_7_amd64.rpm
检测erlang
安装RabbitMQ
官网下载地址:http://www.rabbitmq.com/download.html
安装rabbitmq yum -y install rabbitmq-server-3.8.5-1.el7.noarch.rpm
安装UI插件 rabbitmq-plugins enable rabbitmq_management
启用 rabbitMQ服务 systemctl start rabbitmq-server.service
检测服务 systemctl status rabbitmq-server.service
访问 guest用户默认只可以localhost(本机)访问
在rabbitmq的配置文件目录下(默认为:/etc/rabbitmq)创建一个rabbitmq.config文件。
文件中添加如下配置(请不要忘记那个“.”): [{rabbit, [{loopback_users, []}]}].
重启rabbitmq服务 systemctl restart rabbitmq-server.service
重新访问
使用
依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
配置
spring:
#RabbitMQ
rabbitmq:
#服务器地址
host: 192.168.10.100
#用户名
username: 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: 1000
#重试最大次数,默认3次
max-attempts: 3
#重试最大间隔时间,默认10000ms
max-interval: 10000
#重试间隔的乘数。比如配2.0 第一次等10s,第二次等20s,第三次等40s
multiplier: 1.0
package com.itkaka.seckill.rabbitmq;
import com.itkaka.seckill.pojo.SeckillMessage;
import com.itkaka.seckill.pojo.SeckillOrder;
import com.itkaka.seckill.pojo.User;
import com.itkaka.seckill.service.IGoodsService;
import com.itkaka.seckill.service.IOrderService;
import com.itkaka.seckill.utils.JsonUtil;
import com.itkaka.seckill.vo.GoodsVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
/**
* 消息消费者
*/
@Service
@Slf4j
public class MQReceiver {
// @RabbitListener(queues = "queue")
// public void receive(Object msg) {
// log.info("接收消息:" + msg);
// }
//
//
// @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);
// }
//
//
// @RabbitListener(queues = "queue_direct01")
// public void receive03(Object msg) {
// log.info("QUEUE01接收消息:" + msg);
// }
//
// @RabbitListener(queues = "queue_direct02")
// public void receive04(Object msg) {
// log.info("QUEUE02接收消息:" + msg);
// }
//
// @RabbitListener(queues = "queue_topic01")
// private void receive05(Object msg) {
// log.info("QUEUE01接收消息:" + msg);
// }
//
// @RabbitListener(queues = "queue_topic02")
// private void receive06(Object msg) {
// log.info("QUEUE02接收消息:" + msg);
// }
//
// @RabbitListener(queues = "queue_header01")
// public void receive07(Message message) {
// log.info("QUEUE01接收Message对象:" + message);
// log.info("QUEUE01接收消息:" + new String(message.getBody()));
// }
//
// @RabbitListener(queues = "queue_header02")
// public void receive08(Message message) {
// log.info("QUEUE02接收Message对象:" + message);
// log.info("QUEUE02接收消息:" + new String(message.getBody()));
// }
@Autowired
private IGoodsService goodsService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private IOrderService orderService;
/**
* 下单操作
*/
@RabbitListener(queues = "seckillQueue")
public void receive(String message) {
log.info("接收的消息:" + message);
SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(message, SeckillMessage.class);
Long goodId = seckillMessage.getGoodId();
User user = seckillMessage.getUser();
//判断库存
GoodsVo goodsVo = goodsService.queryGoodsVoByGoodsId(goodId);
if (goodsVo.getStockCount() < 1) {
return;
}
//判断是否重复抢购
SeckillOrder seckillOrder =
(SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodId);
if (seckillOrder != null) {
return;
}
//下单操作
orderService.seckill(user, goodsVo);
}
}
package com.itkaka.seckill.rabbitmq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 消息发送者
*/
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
// public void send(Object msg) {
// log.info("发送消息:" + msg);
// rabbitTemplate.convertAndSend("fanoutExchange", "", msg);
// }
//
//
// 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);
// }
//
//
// public void send03(Object msg) {
// log.info("发送消息(QUEUE01接收):" + msg);
// rabbitTemplate.convertAndSend("topicExchange", "queue.red.message", msg);
// }
//
// public void send04(Object msg) {
// log.info("发送消息(被两个queue接收):" + msg);
// rabbitTemplate.convertAndSend("topicExchange", "message.queue.green.abc", msg);
// }
//
//
// public void send05(String msg) {
// log.info("发送消息(被两个queue接收):" + msg);
// MessageProperties properties = new MessageProperties();
// properties.setHeader("color", "red");
// properties.setHeader("speed", "fast");
// Message message = new Message(msg.getBytes(), properties);
// rabbitTemplate.convertAndSend("headersExchange", "", message);
// }
//
// public void send06(String msg) {
// log.info("发行消息(被QUEUE01接收):" + msg);
// MessageProperties properties = new MessageProperties();
// properties.setHeader("color", "red");
// properties.setHeader("speed", "normal");
// Message message = new Message(msg.getBytes(), properties);
// rabbitTemplate.convertAndSend("headersExchange", "", message);
// }
/**
* 发送秒杀信息
*
* @param message
*/
public void sendSeckillMessage(String message) {
log.info("发送消息:" + message);
rabbitTemplate.convertAndSend("seckillExchange", "seckill.message", message);
}
}
package com.itkaka.seckill.controller;
import com.itkaka.seckill.pojo.User;
import com.itkaka.seckill.rabbitmq.MQSender;
import com.itkaka.seckill.vo.RespBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
*
* 前端控制器
*
*
* @author itkaka
* @since 2023-05-11
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private MQSender mqSender;
// 用户信息(测试)
@RequestMapping("/info")
@ResponseBody
public RespBean info(User user) {
return RespBean.success(user);
}
// /**
// * 功能描述: 测试发送RabbitMQ消息
// */
// @RequestMapping("/mq")
// @ResponseBody
// public void mq() {
// mqSender.send("Hello");
// }
//
//
// /**
// * 功能描述: Fanout模式
// */
// @RequestMapping("/mq/fanout")
// @ResponseBody
// public void mq01() {
// mqSender.send("Hello");
// }
//
//
// /**
// * 功能描述: Direct模式
// */
// @RequestMapping("/mq/direct01")
// @ResponseBody
// public void mq02() {
// mqSender.send01("Hello,Red");
// }
//
// /**
// * 功能描述: Direct模式
// */
// @RequestMapping("/mq/direct02")
// @ResponseBody
// public void mq03() {
// mqSender.send02("Hello,Green");
// }
//
//
// /**
// * 功能描述: Topic模式
// */
// @RequestMapping("/mq/topic01")
// @ResponseBody
// public void mq04() {
// mqSender.send03("Hello,Red");
// }
//
//
// /**
// * 功能描述: Topic模式
// */
// @RequestMapping("/mq/topic02")
// @ResponseBody
// public void mq05() {
// mqSender.send04("Hello,Green");
// }
//
//
// /**
// * 功能描述: Header模式
// */
// @RequestMapping("/mq/header01")
// @ResponseBody
// public void mq06() {
// mqSender.send05("Hello,Header01");
// }
//
// /**
// * 功能描述: Header模式
// */
// @RequestMapping("/mq/header02")
// @ResponseBody
// public void mq07() {
// mqSender.send06("Hello,Header02");
// }
}
添加配置类,测试
package com.itkaka.seckill.config;// package com.xxxx.seckill.config;
//
// import org.springframework.amqp.core.Binding;
// import org.springframework.amqp.core.BindingBuilder;
// import org.springframework.amqp.core.FanoutExchange;
// import org.springframework.amqp.core.Queue;
// import org.springframework.context.annotation.Bean;
// import org.springframework.context.annotation.Configuration;
//
// /**
// * RabbitMQ配置类
// */
// @Configuration
// public class RabbitMQConfig {
//
// private static final String QUEUE01 = "queue_fanout01";
// private static final String QUEUE02 = "queue_fanout02";
// private static final String EXCHANGE = "fanoutExchange";
//
// @Bean
// public Queue queue(){
// return new Queue("queue",true);
// }
//
// @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());
// }
// }
package com.itkaka.seckill.config;// package com.xxxx.seckill.config;
//
// import org.springframework.amqp.core.Binding;
// import org.springframework.amqp.core.BindingBuilder;
// import org.springframework.amqp.core.DirectExchange;
// import org.springframework.amqp.core.Queue;
// import org.springframework.context.annotation.Bean;
// import org.springframework.context.annotation.Configuration;
//
// /**
// * RabbitMQ配置类-Direct模式
// */
// @Configuration
// public class RabbitMQDirectConfig {
//
// 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);
// }
// }
package com.itkaka.seckill.config;// package com.xxxx.seckill.config;
//
// import org.springframework.amqp.core.Binding;
// import org.springframework.amqp.core.BindingBuilder;
// import org.springframework.amqp.core.HeadersExchange;
// import org.springframework.amqp.core.Queue;
// import org.springframework.context.annotation.Bean;
// import org.springframework.context.annotation.Configuration;
//
// import java.util.HashMap;
// import java.util.Map;
//
//
// /**
// * RabbitMQ配置类-Headers模式
// */
// @Configuration
// public class RabbitMQHeadersConfig {
//
// 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 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 map = new HashMap<>();
// map.put("color", "red");
// map.put("speed", "fast");
// return BindingBuilder.bind(queue02()).to(headersExchange()).whereAll(map).match();
// }
// }
package com.itkaka.seckill.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ配置类-Topic
*
*/
@Configuration
public class RabbitMQTopicConfig {
// 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);
// }
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 binding() {
return BindingBuilder.bind(queue()).to(topicExchange()).with("seckill.#");
}
}
思路:减少数据库访问
package com.itkaka.seckill.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.itkaka.seckill.config.AccessLimit;
import com.itkaka.seckill.pojo.SeckillMessage;
import com.itkaka.seckill.rabbitmq.MQSender;
import com.wf.captcha.ArithmeticCaptcha;
import com.itkaka.seckill.exception.GlobalException;
import com.itkaka.seckill.pojo.Order;
import com.itkaka.seckill.pojo.SeckillOrder;
import com.itkaka.seckill.pojo.User;
import com.itkaka.seckill.service.IGoodsService;
import com.itkaka.seckill.service.IOrderService;
import com.itkaka.seckill.service.ISeckillOrderService;
import com.itkaka.seckill.utils.JsonUtil;
import com.itkaka.seckill.vo.GoodsVo;
import com.itkaka.seckill.vo.RespBean;
import com.itkaka.seckill.vo.RespBeanEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 秒杀
*/
@Slf4j
@Controller
@RequestMapping("/seckill")
public class SecKillController {
@Autowired
private IGoodsService goodsService;
@Autowired
private ISeckillOrderService seckillOrderService;
@Autowired
private IOrderService orderService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private MQSender mqSender;
@Autowired
private RedisScript<Long> script;
private Map<Long, Boolean> EmptyStockMap = new HashMap<>();
/**
* 功能描述: 秒杀
* windows优化前QPS:785
* 缓存QPS:1356
* 优化QPS:2454
*/
@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);
}
// 判断是否重复抢购
SeckillOrder seckillOrder =
(SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
if (seckillOrder != null) {
return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}
// 内存标记,减少Redis的访问
if (EmptyStockMap.get(goodsId)) {
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
// 预减库存
// Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
Long stock = (Long) redisTemplate.execute(script, Collections.singletonList("seckillGoods:" + goodsId),
Collections.EMPTY_LIST);
if (stock < 0) {
EmptyStockMap.put(goodsId, true);
valueOperations.increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage));
return RespBean.success(0);
/*GoodsVo goods = goodsService.queryGoodsVoByGoodsId(goodsId);
//判断库存
if (goods.getStockCount() < 1) {
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
//判断是否重复抢购
// SeckillOrder seckillOrder =
// seckillOrderService.getOne(new QueryWrapper().eq("user_id", user.getId()).eq("goods_id",
// goodsId));
String seckillOrderJson =
(String) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
// if (seckillOrder != null) {
// return RespBean.error(RespBeanEnum.REPEATE_ERROR);
// }
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);*/
}
/**
* 系统初始化,把商品库存数量加载到Redis
*
*/
public void afterPropertiesSet() throws Exception {
List<GoodsVo> list = goodsService.queryGoodsVo();
if (CollectionUtils.isEmpty(list)) {
return;
}
list.forEach(goodsVo -> {
redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
EmptyStockMap.put(goodsVo.getId().longValue(), false);
}
);
}
}
package com.itkaka.seckill.pojo;
// 秒杀信息
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SeckillMessage {
private User user;
private Long goodId;
}
@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.#");
}
}
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendsecKillMessage(String message) {
log.info("发送消息:" + message);
rabbitTemplate.convertAndSend("seckillExchange", "seckill.msg", message);
}
}
@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);
}
}
/**
* 功能描述: 获取秒杀结果
* 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);
}
package com.itkaka.seckill.service;
import com.itkaka.seckill.pojo.SeckillOrder;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itkaka.seckill.pojo.User;
/**
*
* 服务类
*
*
* @author itkaka
* @since 2023-05-11
*/
public interface ISeckillOrderService extends IService<SeckillOrder> {
// 获取秒杀结果
Long getResult(User user,Long goodsId);
}
package com.itkaka.seckill.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.itkaka.seckill.pojo.SeckillOrder;
import com.itkaka.seckill.mapper.SeckillOrderMapper;
import com.itkaka.seckill.pojo.User;
import com.itkaka.seckill.service.ISeckillOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
/**
*
* 服务实现类
*
*
* @author itkaka
* @since 2023-05-11
*/
@Service
public class SeckillOrderServiceImpl extends ServiceImpl<SeckillOrderMapper, SeckillOrder> implements ISeckillOrderService {
@Autowired
private SeckillOrderMapper seckillOrderMapper;
@Autowired
private RedisTemplate redisTemplate;
@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().longValue();
} else if (redisTemplate.hasKey("isStockEmpty:" + goodsId)) {
return -1L;
} else {
return 0L;
}
}
}
package com.itkaka.seckill.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.itkaka.seckill.exception.GlobalException;
import com.itkaka.seckill.pojo.Order;
import com.itkaka.seckill.mapper.OrderMapper;
import com.itkaka.seckill.pojo.SeckillGoods;
import com.itkaka.seckill.pojo.SeckillOrder;
import com.itkaka.seckill.pojo.User;
import com.itkaka.seckill.service.IGoodsService;
import com.itkaka.seckill.service.IOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itkaka.seckill.service.ISeckillGoodsService;
import com.itkaka.seckill.service.ISeckillOrderService;
import com.itkaka.seckill.utils.MD5Util;
import com.itkaka.seckill.utils.UUIDUtil;
import com.itkaka.seckill.vo.GoodsVo;
import com.itkaka.seckill.vo.OrderDetailVo;
import com.itkaka.seckill.vo.RespBeanEnum;
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.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
*
* 服务实现类
*
*
* @author itkaka
* @since 2023-05-11
*/
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {
@Resource
private ISeckillGoodsService seckillGoodsService;
@Autowired
private OrderMapper orderMapper;
@Autowired
private ISeckillOrderService seckillOrderService;
@Autowired
private IGoodsService goodsService;
@Autowired
private RedisTemplate redisTemplate;
// 秒杀
@Override
@Transactional
public Order seckill(User user, GoodsVo goodsVo) {
ValueOperations valueOperations = redisTemplate.opsForValue();
// 秒杀商品减库存
SeckillGoods seckillGoods = seckillGoodsService.getOne(new
QueryWrapper<SeckillGoods>().eq("goods_id",goodsVo.getId()));
seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
//seckillGoodsService.updateById(seckillGoods);
// 解决库存超卖 扣减库存时候判断库存是否足够
// seckillGoodsService.update(new UpdateWrapper().set("stock_count",
// seckillGoods.getStockCount()).eq("id",seckillGoods.getId()).gt("stock_count",0));
//
boolean seckillGoodsResult = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().set("stock_count",
seckillGoods.getStockCount()).eq("id",seckillGoods.getId()).gt("stock_count",0));
if ((seckillGoods.getStockCount())<1){
// 判断是否还有库存
valueOperations.set("isStockEmpty:"+ goodsVo.getId(),"0");
return null;
}
// 生成订单
Order order = new Order();
order.setUserId(user.getId());
order.setGoodsId(goodsVo.getId());
order.setDeliverAddrId(0);
order.setGoodsName(goodsVo.getGoodsName());
order.setGoodsCount(1);
order.setGoodsPrice(seckillGoods.getSeckillPrice());
order.setOrderChannel(1);
order.setStatus(0);
order.setCreateDate(new Date());
orderMapper.insert(order);
//生成秒杀订单
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setUserId(user.getId());
seckillOrder.setOrderId(order.getId());
seckillOrder.setGoodsId(goodsVo.getId());
seckillOrderService.save(seckillOrder);
valueOperations.set("order:" + user.getId() + ":" + goodsVo.getId(), seckillOrder);
return order;
}
}
项目启动,Redis预加载库存
秒杀成功,数据库及Redis库存数量正确
QPS 相对而言有一定的提升
数据库以及 Redis 库存数量和订单都正确
发现小问题 : Redis 的库存会有问题, 原因是 Redis 没有做到原子性; 采用锁去解决
分布式锁
// 先进来的线程先占位,当别的线程进来操作,发现已经有人占位,会放弃或者稍后再尝试
// 线程操作执行完成以后,需要调用 del 指令释放位置
@SpringBootTest
class SeckillApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testLock01(){
ValueOperations valueOperations = redisTemplate.opsForValue();
Boolean isLock = valueOperations.setIfAbsent("k1", "v1");
if (isLock){
valueOperations.set("name","有锁测试1");
String name = (String) valueOperations.get("name");
System.out.println(name);
redisTemplate.delete("k1");
}else {
System.out.println("有线程在使用,请稍后");
}
}
}
// 为了防止业务执行过程中抛异常或者挂机导致 del 指令没办法调用形成死锁,可以添加超时时间
@Test
public void testLock02(){
ValueOperations valueOperations = redisTemplate.opsForValue();
Boolean isLock = valueOperations.setIfAbsent("k1","v1",5, TimeUnit.SECONDS);
if (isLock){
valueOperations.set("name","有锁测试2");
String name = (String) valueOperations.get("name");
System.out.println(name);
redisTemplate.delete("k1");
}else {
System.out.println("有线程在使用,请稍后");
}
}
上面例子,如果业务非常耗时会紊乱。举例:第一个线程首先获得锁,然后执行业务代码,但是业务代码耗时8秒,这样会在第一个线程的任务还未执行成功锁就会被释放,这时第二个线程会获取到锁开始执行,在第二个线程开执行了3秒,第一个线程也执行完了,此时第一个线程会释放锁,但是注意,他释放的第二个现成的锁,释放之后,第三个线程进来。
解决方案 :
Lua 脚本
Lua脚本优势:
使用Lua脚本思路:
创建 Lua 脚本( 在resources 目录下 )
lock.lua
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
调用掉本
package com.itkaka.seckill.config;
// redis 配置类
// @Bean
// public DefaultRedisScript script() {
// DefaultRedisScript redisScript = new DefaultRedisScript<>();
// //lock.lua脚本位置和application.yml同级目录
// redisScript.setLocation(new ClassPathResource("lock.lua"));
// redisScript.setResultType(Boolean.class);
// return redisScript;
// }
@Test
public void testLock03(){
ValueOperations valueOperations = redisTemplate.opsForValue();
String value = UUID.randomUUID().toString();
//给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
Boolean isLock = valueOperations.setIfAbsent("k1",value,5, TimeUnit.SECONDS);
//没人占位
if (isLock){
valueOperations.set("name","xxxx");
String name = (String) valueOperations.get("name");
System.out.println(name);
System.out.println(valueOperations.get("k1"));
//释放锁
Boolean result = (Boolean) redisTemplate.execute(script,Collections.singletonList("k1"), value);
System.out.println(result);
}else {
//有人占位,停止/暂缓 操作
System.out.println("有线程在使用,请稍后");
}
}
优化 Redis 预减库存
stock.lua
if (redis.call('exists', KEYS[1]) == 1) then
local stock = tonumber(redis.call('get', KEYS[1]));
if (stock > 0) then
redis.call('incrby', KEYS[1], -1);
return stock;
end;
return 0;
end;
@Bean
public DefaultRedisScript<Long> script() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//放在和application.yml 同层目录下
redisScript.setLocation(new ClassPathResource("stock.lua"));
redisScript.setResultType(Long.class);
return redisScript;
}
SeckillController.java
// 秒杀
@RequestMapping(value = "/doSeckill", method = RequestMethod.POST)
@ResponseBody
public RespBean doSeckill(User user, Long goodsId) {
if (user == null) {
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
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 = (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);
}
秒杀开始前,先去请求接口获取秒杀地址
/**
* 功能描述: 获取秒杀地址
*/
@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);
}
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);
}
IOrderService.java
// 校验秒杀地址
boolean checkPath(User user,Long goodsId,String path);
// 获取秒杀地址
String createPath(User user,Long goodsId);
OrderServiceImpl.java
// 获取秒杀地址
@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;
}
// 校验秒杀地址
@Override
public boolean checkPath(User user, Long goodsId, String path) {
if (user == null || goodsId < 0 || StringUtils.isEmpty(path)) {
return false;
}
String redisPath = (String) redisTemplate.opsForValue().get("seckillPath:" + user.getId() + ":" + goodsId);
return path.equals(redisPath);
}
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) {
var path = 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.whvcsegroupId>
<artifactId>easy-captchaartifactId>
<version>1.6.2version>
dependency>
SeckillController.java
// 验证码
@RequestMapping(value = "/captcha", method = RequestMethod.GET)
public void verifyCode(User user, Long goodsId, HttpServletResponse response) {
if (user == null || goodsId < 0) {
throw new GlobalException(RespBeanEnum.REQUEST_ILLEGAL);
}
//设置请求头为输出图片的类型
response.setContentType("image/jpg");
response.setHeader("Pargam", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
//生成验证码,将结果放入Redis
ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 32, 3);
redisTemplate.opsForValue().set("captcha:" + user.getId() + ":" + goodsId, captcha.text(), 300,
TimeUnit.SECONDS);
try {
captcha.out(response.getOutputStream());
} catch (IOException e) {
log.error("验证码生成失败", e.getMessage());
}
}
验证验证码
SeckillController.java
/**
* 获取秒杀地址
*
* @param user
* @param goodsId
* @return
*/
@RequestMapping(value = "/path", method = RequestMethod.GET)
@ResponseBody
public RespBean getPath(User user, Long 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.ERROR_CAPTCHA);
}
String str = orderService.createPath(user, goodsId);
return RespBean.success(str);
}
IOrderService.java
boolean checkCaptcha(User user, Long goodsId, String captcha); // 校验验证码
OrderServiceImpl.java
// 校验验证码
@Override
public boolean checkCaptcha(User user, Long goodsId, String captcha) {
if (StringUtils.isEmpty(captcha) || user == null || goodsId < 0) {
return false;
}
String redisCaptcha = (String) redisTemplate.opsForValue().get("captcha:" + user.getId() + ":" + goodsId);
return captcha.equals(redisCaptcha);
}
测试
输入错误验证码,提示错误并且无法秒杀
SeckillController.java
@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);
}
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);
}else if (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.itkaka.seckill.config;
import com.itkaka.seckill.pojo.User;
/**
*
*/
public class UserContext {
private static ThreadLocal<User> userHolder = new ThreadLocal<User>();
public static void setUser(User user) {
userHolder.set(user);
}
public static User getUser() {
return userHolder.get();
}
}
UserArgumentResolver.java
package com.itkaka.seckill.config;
import com.itkaka.seckill.pojo.User;
import com.itkaka.seckill.service.IUserService;
import com.itkaka.seckill.utils.CookieUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义用户参数
*/
@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) throws Exception {
//
// HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
// HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
// String ticket = CookieUtil.getCookieValue(request,"userTicket");
// if (StringUtils.isEmpty(ticket)){
// return null;
// }
//
// return userService.getUserByCookie(ticket,request,response);
// }
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return UserContext.getUser();
}
}
AccessInterceptor.java
package com.itkaka.seckill.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itkaka.seckill.pojo.User;
import com.itkaka.seckill.service.IUserService;
import com.itkaka.seckill.utils.CookieUtil;
import com.itkaka.seckill.vo.RespBean;
import com.itkaka.seckill.vo.RespBeanEnum;
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.util.StringUtils;
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 AccessLimitInterceptor 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) {
User 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 needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if (needLogin) {
if (user == null) {
render(response, RespBeanEnum.SESSION_ERROR);
return false;
}
key += ":" + user.getId();
}
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;
}
/**
* 功能描述: 构建返回对象
*
*/
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();
}
/**
* 功能描述: 获取当前登录用户
*
*/
private User getUser(HttpServletRequest request, HttpServletResponse response) {
String ticket = CookieUtil.getCookieValue(request, "userTicket");
if (StringUtils.isEmpty(ticket)) {
return null;
}
return userService.getUserByCookie(ticket, request, response);
}
}
WebConfig.java
package com.itkaka.seckill.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* MVC配置类
*/
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserArgumentResolver userArgumentResolver;
@Autowired
private AccessLimitInterceptor accessLimitInterceptor;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userArgumentResolver);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessLimitInterceptor);
}
}
AccessLimit.java
package com.itkaka.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 needLogin() default true;
}
SeckillController.java
/**
* 功能描述: 获取秒杀地址
*/
@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);
}
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);
}
至此 , 本次 Java 秒杀 以及简单优化基本结束;
整体的项目目录结构如下图
后端接口基本 写完了,剩余部分前台页面和 静态资源 尚未同步上传。