Java 秒杀方案(下)

技术点

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

秒杀方案简介

Java 秒杀方案(下)_第1张图片
本短文完成项目搭建, 分布式 Session 和秒杀功能 三个小模块;
Java 秒杀方案(下)_第2张图片

秒杀系统设计

秒杀其实主要解决两个问题,一个是并发读,一个是并发写
并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。另外,我们还要针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。
秒杀的整体架构可以概括为“稳、准、快”几个关键字。
所谓
“稳”
,就是整个系统架构要满足高可用,流量符合预期时肯定要稳定,就是超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。
然后就是**“准”,就是秒杀 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;

/**
 * 

* 服务类 *

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

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

优化阶段目录
Java 秒杀方案(下)_第3张图片

系统压测

JMeter 入门

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

自定义变量

准备测试接口

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 请求用户信息
Java 秒杀方案(下)_第12张图片
查看聚合结果
Java 秒杀方案(下)_第13张图片
配置不同用户测试
准备配置文件 config.txt
#具体用户和userTicket
18012345678,bd055fb14eef4d1ea2933ff8d6e44575
添加 --> 配置元件 --> CSV Data Set Config
Java 秒杀方案(下)_第14张图片
添加 --> 配置元件 --> HTTP Cookie管理器
Java 秒杀方案(下)_第15张图片
修改 HTTP 请求用户信息
Java 秒杀方案(下)_第16张图片
查看结果
Java 秒杀方案(下)_第17张图片

正式压测

压测商品列表接口
准备5000线程,循环10次 压测商品列表接口,测试 三次,查看结果
Java 秒杀方案(下)_第18张图片
Java 秒杀方案(下)_第19张图片
HTTP 请求
Java 秒杀方案(下)_第20张图片
结果
Java 秒杀方案(下)_第21张图片
压测秒杀接口
新建用户
使用工具类往数据库插入5000用户,并且调用登录接口获取token,写入config.txt
config.txt
Java 秒杀方案(下)_第22张图片
配置秒杀接口测试
线程组
Java 秒杀方案(下)_第23张图片
HTTP 请求默认值
Java 秒杀方案(下)_第24张图片
CVS 数据文件设置
Java 秒杀方案(下)_第25张图片
HTTP Cookie管理器
Java 秒杀方案(下)_第26张图片
HTTP 请求
Java 秒杀方案(下)_第27张图片
结果
Java 秒杀方案(下)_第28张图片
此时, 已经出现超卖问题

页面优化

缓存

页面缓存

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

重新运行项目查看效果
Java 秒杀方案(下)_第29张图片
Java 秒杀方案(下)_第30张图片
Java 秒杀方案(下)_第31张图片
测试发现 : 结果对比之前的 QPS提升比较明显
Java 秒杀方案(下)_第32张图片

对象缓存

更新对象返回枚举类

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);

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

服务优化

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
Java 秒杀方案(下)_第33张图片
启用 rabbitMQ服务 systemctl start rabbitmq-server.service
检测服务 systemctl status rabbitmq-server.service
访问 guest用户默认只可以localhost(本机)访问
Java 秒杀方案(下)_第34张图片
在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"); // } }

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 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.#"); } }

接口优化

思路:减少数据库访问

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

/**
 * 

* 服务类 *

* * @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预加载库存
image.png
秒杀成功,数据库及Redis库存数量正确

压测秒杀

QPS 相对而言有一定的提升
Java 秒杀方案(下)_第35张图片
数据库以及 Redis 库存数量和订单都正确
Java 秒杀方案(下)_第36张图片

优化 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 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("客户端请求错误");
   }
 })
}

先去请求接口获取秒杀地址
Java 秒杀方案(下)_第37张图片
秒杀真正地址
Java 秒杀方案(下)_第38张图片

图形验证码

点击秒杀开始前, 先输入验证码,分散用户请求
生成验证码
引入依赖 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 秒杀 以及简单优化基本结束;
整体的项目目录结构如下图
Java 秒杀方案(下)_第39张图片
后端接口基本 写完了,剩余部分前台页面和 静态资源 尚未同步上传。

你可能感兴趣的:(JAVA,Java秒杀方案,Java实战项目,java,rabbitmq,redis)