Java 秒杀方案(下)

技术点

前端 : Thymeleaf | Bootstrap | Jquery
后端 : SpringBoot | MyBatisPlus | Lombok
中间件 : Redis | RabbitMQ

秒杀方案简介

image.png
本短文完成项目搭建, 分布式 Session 和秒杀功能 三个小模块;

秒杀系统设计

秒杀其实主要解决两个问题,一个是并发读,一个是并发写
并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。另外,我们还要针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。
秒杀的整体架构可以概括为“稳、准、快”几个关键字。
所谓
“稳”
,就是整个系统架构要满足高可用,流量符合预期时肯定要稳定,就是超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。
然后就是**“准”,就是秒杀 10 台 手机,那就只能成交 10 台,多一台少一台都不行。一旦库存不对,那平台就要承担损失,所以“准”就是要求保证数据的一致性。
最后再看
“快”**,“快”其实很好理解,它就是说系统的性能要足够高,否则你怎么支撑这么大的流量呢?不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就完美了。
所以从技术角度上看“稳、准、快”,就对应了我们架构上的高可用、一致性和高性能的要求

  • 高性能。 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。对应的方案比如动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化
  • 一致性。 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知
  • 高可用。 现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,我们还要设计一个 B 计划 来兜底,以便在最坏情况发生时仍然能够从容应对

环境搭建(这部分见上一篇文章)

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;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @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;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @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";
	}
}

测试 :
秒杀成功进去定安详情 [注意查看库存是否正确扣减,订单是否正确生成]
两个限制条件 : ①限制超卖(库存不足) ②单品每人限购一件(重复抢购)

优化阶段目录

系统压测

JMeter 入门

下载安装
官网 : https://jmeter.apache.org/
下载地址 : https://jmeter.apache.org/download_jmeter.cgi
下载解压后直接在 bin 目录里双击 jmeter.bat 即可启动(Lunix系统通过 jmeter.sh 启动)
image.png
修改中文
Options --> Choose Language --> Chinese(Simplified)
image.png
测试demo
我们先使用JMeter测试一下跳转商品列表页的接口。
首先创建线程组,步骤:添加–> 线程(用户) --> 线程组
image.png
Ramp-up 指在几秒之内启动指定线程数
创建HTTP请求默认值,步骤:添加–> 配置元件 --> HTTP请求默认值
image.png
添加测试接口,步骤:添加 --> 取样器 --> HTTP请求
image.png
查看输出结果,步骤:添加 --> 监听器 --> 聚合报告/图形结果/用表格察看结果
image.png
启动即可在监听器看到对应的结果
image.png
image.png

自定义变量

准备测试接口

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;

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @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 请求用户信息
image.png
查看聚合结果
image.png
配置不同用户测试
准备配置文件 config.txt
#具体用户和userTicket
18012345678,bd055fb14eef4d1ea2933ff8d6e44575
添加 --> 配置元件 --> CSV Data Set Config
image.png
添加 --> 配置元件 --> HTTP Cookie管理器
image.png
修改 HTTP 请求用户信息
image.png
查看结果
image.png

正式压测

压测商品列表接口
准备5000线程,循环10次 压测商品列表接口,测试 三次,查看结果
image.png
image.png
HTTP 请求
image.png
结果
image.png
压测秒杀接口
新建用户
使用工具类往数据库插入5000用户,并且调用登录接口获取token,写入config.txt
config.txt
image.png
配置秒杀接口测试
线程组
image.png
HTTP 请求默认值
image.png
CVS 数据文件设置
image.png
HTTP Cookie管理器
image.png
HTTP 请求
image.png
结果
image.png
此时, 已经出现超卖问题

页面优化

缓存

页面缓存

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";
    }

重新运行项目查看效果
image.png
image.png
image.png
测试发现 : 结果对比之前的 QPS提升比较明显
image.png

对象缓存

更新对象返回枚举类

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>
    <!-- jquery -->
    <script type="text/javascript" src="/js/jquery.min.js"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css"/>
    <script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script>
    <!-- layer -->
    <script type="text/javascript" src="/layer/layer.js"></script>
    <!-- common.js -->
    <script type="text/javascript" src="/js/common.js"></script>
</head>
<body>
<div class="panel panel-default">
    <div class="panel-heading">秒杀商品详情</div>
    <div class="panel-body">
        <span id="userTip"> 您还没有登录,请登陆后再操作<br/></span>
        <span>没有收货地址的提示。。。</span>
    </div>
    <table class="table" id="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 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>-->
                <span id="seckillTip"></span>
            </td>
            <td>
                <!--<form id="secKillForm" method="post" action="/seckill/doSeckill">
                    <input type="hidden" name="goodsId" id="goodsId">
                    <button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button>
                </form>-->
                <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"/>
    <!-- jquery -->
    <script type="text/javascript" src="/js/jquery.min.js"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css"/>
    <script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script>
    <!-- layer -->
    <script type="text/javascript" src="/layer/layer.js"></script>
    <!-- common.js -->
    <script type="text/javascript" src="/js/common.js"></script>
</head>
<body>
<div class="panel panel-default">
    <div class="panel-heading">秒杀订单详情</div>
    <table class="table" id="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">
                <!--<span if="${order.status eq 0}">未支付</span>
                <span if="${order.status eq 1}">待发货</span>
                <span if="${order.status eq 2}">已发货</span>
                <span if="${order.status eq 3}">已收货</span>
                <span if="${order.status eq 4}">已退款</span>
                <span if="${order.status eq 5}">已完成</span>-->
            </td>
            <td>
                <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>
<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;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @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;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @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);

解决同一个用户同时秒杀多件商品
可以通过数据库建立唯一索引避免
image.png
将秒杀订单信息存入 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<SeckillGoods>().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<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);
 }

服务优化

RabbitMQ 入门

安装 : 官网 https://www.erlang-solutions.com/resources/download.html
安装erlang Linux命令: yum -y install esl-erlang_23.0.2-1_centos_7_amd64.rpm
检测erlang image.png
安装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
image.png
启用 rabbitMQ服务 systemctl start rabbitmq-server.service
检测服务 systemctl status rabbitmq-server.service
访问 guest用户默认只可以localhost(本机)访问
image.png
在rabbitmq的配置文件目录下(默认为:/etc/rabbitmq)创建一个rabbitmq.config文件。
文件中添加如下配置(请不要忘记那个“.”): [{rabbit, [{loopback_users, []}]}].
重启rabbitmq服务 systemctl restart rabbitmq-server.service
重新访问
使用
依赖

<!-- AMQP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</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;

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @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");
    // }

}

RabbitMQ交换机

添加配置类,测试

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<String, Object> map = new HashMap<>();
// 		map.put("color", "red");
// 		map.put("speed", "low");
// 		return BindingBuilder.bind(queue01()).to(headersExchange()).whereAny(map).match();
// 	}
//
// 	@Bean
// 	public Binding binding02() {
// 		Map<String, Object> map = new HashMap<>();
// 		map.put("color", "red");
// 		map.put("speed", "fast");
// 		return BindingBuilder.bind(queue02()).to(headersExchange()).whereAll(map).match();
// 	}
// }

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
 * <p>
 */
@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.#");
	}
}

接口优化

思路:减少数据库访问

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

Redis 操作库存

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<SeckillOrder>().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);
				}
		);
	}
}

RabbitMQ 秒杀

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;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @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;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @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;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @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<SeckillGoods>().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预加载库存
image.png
秒杀成功,数据库及Redis库存数量正确

压测秒杀

QPS 相对而言有一定的提升
image.png
数据库以及 Redis 库存数量和订单都正确
image.png

优化 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秒,第一个线程也执行完了,此时第一个线程会释放锁,但是注意,他释放的第二个现成的锁,释放之后,第三个线程进来。
解决方案 :

  1. 尽量避免在获取锁之后,执行耗时操作
  2. 将锁的value设置为一个随机字符串,每次释放锁的时候,都去比较随机字符串是否一致,如果一致,再去释放,否则不释放。
  3. 释放锁时要去查看所得value,比较value是否正确,释放锁总共三个步骤,这三个步骤不具备原子性

Lua 脚本
Lua脚本优势:

  1. 使用方便,Redis内置了对Lua脚本的支持
  2. Lua脚本可以在Rdis服务端原子的执行多个Redis命令
  3. 由于网络在很大程度上会影响到Redis性能,使用Lua脚本可以让多个命令一次执行,可以有效解决网络给Redis带来的性能问题

使用Lua脚本思路:

  1. 提前在Redis服务端写好Lua脚本,然后在java客户端去调用脚本
  2. 可以在java客户端写Lua脚本,写好之后,去执行。需要执行时,每次将脚本发送到Redis上去执行

创建 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<Boolean> script() {
    // 	DefaultRedisScript<Boolean> 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("客户端请求错误");
   }
 })
}

先去请求接口获取秒杀地址
image.png
秒杀真正地址
image.png

图形验证码

点击秒杀开始前, 先输入验证码,分散用户请求
生成验证码
引入依赖 pom.xml

<!-- 验证码 -->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</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 秒杀 以及简单优化基本结束;
整体的项目目录结构如下图
image.png
后端接口基本 写完了,剩余部分前台页面和 静态资源 尚未同步上传。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
java实现秒杀系统@Controller @RequestMapping("seckill")//url:/模块/资源/{id}/细分 /seckill/list public class SeckillController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SeckillService seckillService; @RequestMapping(value="/list",method = RequestMethod.GET) public String list(Model model){ //获取列表页 List list=seckillService.getSeckillList(); model.addAttribute("list",list); //list.jsp+model = ModelAndView return "list";//WEB-INF/jsp/"list".jsp } @RequestMapping(value = "/{seckillId}/detail",method = RequestMethod.GET) public String detail(@PathVariable("seckillId") Long seckillId, Model model){ if (seckillId == null){ return "redirect:/seckill/list"; } Seckill seckill = seckillService.getById(seckillId); if (seckill == null){ return "forward:/seckill/list"; } model.addAttribute("seckill",seckill); return "detail"; } //ajax json @RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult exposer(@PathVariable("seckillId") Long seckillId){ SeckillResult result; try { Exposer exposer =seckillService.exportSeckillUrl(seckillId); result = new SeckillResult(true,exposer); } catch (Exception e) { logger.error(e.getMessage(),e); result = new SeckillResult(false,e.getMessage()); } return result; } @RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"} ) @ResponseBody public SeckillResult execute(@PathVariable("seckillId")Long seckillId,

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值