商品秒杀核心功能及优化

1. 正常秒杀流程

  1. 在商品详情页面等待秒杀倒计时–http://localhost:8080/goodsDetail.htm?goodsId=2
  2. 倒计时为0,开始秒杀,点【秒杀】按钮开始秒杀 --http://localhost:8080/seckill/doSeckill?goodsId=2
  3. 服务端收到秒杀请求,首先判断是不是在秒杀期间,再判断秒杀商品是否有库存
  4. 上面条件都满足,则进入下单流程,下单流程分3步,1.减库存 2.新增订单信息 3.新增秒杀订单信息

2. 秒杀存在问题

1.商品超卖 ,库存减为负数

  1. 商品超卖:10个商品库存,会出现超卖200多的订单
  2. 秒杀商品库存数会变成负数:因为进入下单流程后,扣减库存是直接减1,大量请求过来,就容易被减为负数
  3. 解决办法:1.通过下面的接口优化,减少过滤服务端的请求压力 2.优化sql,避免出现商品超卖,库存为负数的情况
  4. 如果用户秒杀成功,把秒杀订单信息放在redis里面(key-order:userId:goodsId),这样可以防止重复下单

2.商品库存全部秒杀之后,快速响应秒杀请求

  1. 通过在redis中设置 isStockEmpty:goodsId的值,有值,表示该商品已经全部秒杀结束,直接返回
  2. 通过内存标记:Map<Long,Boolean> EmptyStockMap,在SeckillController初始化时,把秒杀商品<goodsId,false>放在内存中,如果商品库存为空,设置EmptyStockMap<goodsId,true>,后面直接查这个HashMap的值,如果为true,直接返回,不需要再次访问redis
  3. 在SeckillController初始化时,把每个秒杀商品的库存放在redis的seckillGoods:goodsId中,以方便后面在redis预减库存

商品超卖,通过sql优化,同时放在同一用户反复抢购,设置唯一索引

OrderServiceImpl.seckill
boolean result = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().setSql("stock_count=stock_count-1")
                .eq("id", seckillGoods.getId()).gt("stock_count", 0));
        if(!result){
            return null;
        }

唯一索引 t_seckill_order

在这里插入图片描述

3.功能要点(接口优化)

1.redis预减库存

SeckillController afterPropertiesSet()

SeckillController初始化时,把每个秒杀商品的库存放在redis的seckillGoods:goodsId中

@Override
    public void afterPropertiesSet() throws Exception {
        //初始化执行方法 商品库存数量加载到redis
        List<GoodsVo> list = goodsService.findGoodsVo();
        if(CollectionUtils.isEmpty(list)){
            return;
        }
        list.forEach(goodsVo ->{
            redisTemplate.opsForValue().set("seckillGoods:"+goodsVo.getId(),goodsVo.getStockCount());
            EmptyStockMap.put(goodsVo.getId(),false);
                }

        );
    }
SeckillController.doSeckill() seckillGoods:goodsId

原来是先decrement,如果库存<=0,再increment,防止redis中库存为0 ,为了保证redsi事务一致,用lua脚本

Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
log.info("stock:"+stock);
if(stock<=0){
    EmptyStockMap.put(goodsId,true);
    valueOperations.increment("seckillGoods:" + goodsId);
    return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}

lua脚本可以防止redis中库存变成-1

//lua脚本可以防止redis中库存变成-1
Long stock=(Long)redisTemplate.execute(redisScript, Collections.singletonList("seckillGoods:" + goodsId),Collections.emptyList());
log.info("stock:"+stock);
if(stock<=0){
  EmptyStockMap.put(goodsId,true);
    //valueOperations.increment("seckillGoods:" + goodsId);
    return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
stock.lua

放在resources目录下面,与static 同级

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;


2. 内存标记减少redis访问

初始化以及更新

见SeckillController afterPropertiesSet()和 SeckillController.doSeckill() 关于EmptyStockMap.put操作
校验场景

SeckillController.doSeckill
//内存标记,减少读取redis的次数  当redis中库存为0,在内存中吧map对应goodsid的库存为空状态设置为true
        if(EmptyStockMap.get(goodsId)){
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }

3. RabbitMQ异步下单

SeckillController.doSeckill 中校验库存,判断是否重复抢购,以及预减库存之后,发送消息,再通过MQ接受消息,异步下单

准备下单消息,发送
 //下单
        SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);

        mqSender.sendSeckillMessage(JSON.toJSONString(seckillMessage));

        //正在排队中 0
        return RespBean.success(0);
接受消息,校验判断后,下单

MQReceiver

@RabbitListener(queues = "seckillQueue")
 public void receive(String message){
 }

4.功能要点(安全优化)

1. 隐藏秒杀地址

通过准备每个商品,每个用户一个秒杀地址,方式代刷
seckillPath:userId:goodsId redis中的值,已经有效期
在这里插入图片描述

通过 /seckill/path 获取秒杀地址

SeckillController.path()

再通过 /seckill/"+path+"/doSeckill 这个真正的秒杀地址,秒杀

2. 验证码

通过验证码图片,校验每次提交操作是不是人手动操作,屏蔽机器人自动提交
<img src=""id="captchashow" alt="验证码">
<script>
$("#captchashow").click(function (){
    getCaptcha();
  })
  function getCaptcha(){
    console.log("getCaptcha...");
    $("#captchashow").attr("src","/seckill/captcha?goodsId="+goodsId+"&time="+(new Date()).getTime());
  }
</script>

SeckillController.captcha()

3. 接口防刷

@AccessLimit(second=5,maxCount=5,needLogin=true)

通过设置接口AccessLimit注解,设置每5秒钟,最大请求次数为5次,需要有用户登录信息
通过redis计数器实现

String key=request.getRequestURI()+ user.getId();
通过设置5秒杀的有效期,累加当前5秒钟内的总的访问次数

 /**
     * 获取秒杀地址
     * */
    @RequestMapping(value = "/path",method = RequestMethod.GET)
    @ResponseBody
    @AccessLimit(second=5,maxCount=5,needLogin=true)
    //通用接口限流
    public RespBean path(Model model, User user,Long goodsId,Long captcha,HttpServletRequest request) {
    }

AccessLimitInterceptor.preHandle()

Integer count = (Integer)valueOperations.get(key);
            if(null==count){
                //第一次 设置count值 设置超时时间 5秒
                valueOperations.set(key ,1,5, TimeUnit.SECONDS);
            }else if(count>5){
                log.info("AccessLimitInterceptor:"+"短时间之内请求次数太多");
                render(response,RespBeanEnum.REQUEST_LIMITED);
                return false;
            }else {
                valueOperations.increment(key);
            }

5.代码实现

SeckillController

@RequestMapping(value = “/path”,method = RequestMethod.GET)
path()

@RequestMapping(value = “/{path}/doSeckill”,method = RequestMethod.POST)
doSeckill()

@RequestMapping("/getResult")
getResult()

@RequestMapping("/captcha")
captcha()

package com.example.miaosha.controller;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.miaosha.config.AccessLimit;
import com.example.miaosha.exception.GlobalException;
import com.example.miaosha.pojo.Order;
import com.example.miaosha.pojo.SeckillMessage;
import com.example.miaosha.pojo.SeckillOrder;
import com.example.miaosha.pojo.User;
import com.example.miaosha.rabbitmq.MQSender;
import com.example.miaosha.service.IGoodsService;
import com.example.miaosha.service.IOrderService;
import com.example.miaosha.service.ISeckillOrderService;
import com.example.miaosha.vo.GoodsVo;
import com.example.miaosha.vo.RespBean;
import com.example.miaosha.vo.RespBeanEnum;
import com.wf.captcha.ArithmeticCaptcha;
import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.base.Captcha;
import lombok.extern.slf4j.Slf4j;
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.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Controller
@Slf4j
@RequestMapping("/seckill")
public class SeckillController implements InitializingBean {
    @Autowired
    private IGoodsService goodsService;

    @Autowired
    private ISeckillOrderService seckillOrderService;

    @Autowired
    private IOrderService orderService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private MQSender mqSender;

    @Autowired
    private RedisScript<Long>   redisScript;

    private Map<Long,Boolean> EmptyStockMap=new HashMap<>();


    /**
     * 获取秒杀地址
     * */
    @RequestMapping(value = "/path",method = RequestMethod.GET)
    @ResponseBody
    @AccessLimit(second=5,maxCount=5,needLogin=true)
    //通用接口限流
    public RespBean path(Model model, User user,Long goodsId,Long captcha,HttpServletRequest request) {
        log.info("captchaPage:"+captcha);
        if(null==user){
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }

        ValueOperations valueOperations = redisTemplate.opsForValue();


        //现在访问次数 5秒内,访问5次 为了便于次数,验证码默认为0
        captcha=0L;

        /*
        String captchaRedisStr=(String) valueOperations.get("captcha:"+user.getId()+":"+goodsId);
        Long captchaRedis=Long.valueOf(captchaRedisStr);
        log.info("captchaRedis:"+captchaRedis);

         */
        Long captchaRedis=0L;

        if(captchaRedis!=captcha){
            return RespBean.error(RespBeanEnum.CAPTCHA_ERROR);
        }

        String str=orderService.createPath(user,goodsId);
        return RespBean.success(str);
    }

    /**
     * 获取秒杀地址
     * */
    @RequestMapping(value = "/path0",method = RequestMethod.GET)
    @ResponseBody
    //通用接口限流
    public RespBean path0(Model model, User user,Long goodsId,Long captcha,HttpServletRequest request) {
        log.info("captchaPage:"+captcha);
        if(null==user){
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }

        ValueOperations valueOperations = redisTemplate.opsForValue();

        String  url=request.getRequestURI();
        log.info("url:"+url);
        //现在访问次数 5秒内,访问5次 为了便于次数,验证码默认为0
        captcha=0L;

        /*
        String captchaRedisStr=(String) valueOperations.get("captcha:"+user.getId()+":"+goodsId);
        Long captchaRedis=Long.valueOf(captchaRedisStr);
        log.info("captchaRedis:"+captchaRedis);

         */
        Long captchaRedis=0L;

        if(captchaRedis!=captcha){
            return RespBean.error(RespBeanEnum.CAPTCHA_ERROR);
        }

        //
        Integer count = (Integer)valueOperations.get(url + ":" + user.getId());
        if(null==count){
            //第一次 设置count值 设置超时时间 5秒
            valueOperations.set(url + ":" + user.getId(),1,5,TimeUnit.SECONDS);
        }else if(count>5){
            return RespBean.error(RespBeanEnum.REQUEST_LIMITED);
        }else {
            valueOperations.increment(url + ":" + user.getId());
        }



        String str=orderService.createPath(user,goodsId);
        return RespBean.success(str);
    }


    //秒杀静态化,在商品详情页,直接ajax请求秒杀,成功后,跳转秒杀成功静态页面
    @RequestMapping(value = "/{path}/doSeckill",method = RequestMethod.POST)
    @ResponseBody
    public RespBean doSeckill(Model model, User user,Long goodsId,@PathVariable String path) {
        //判断秒杀库存 判断是否重复秒杀 秒杀(减库存 添加订单信息,添加秒杀订单信息)
        if(null==user){
            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);
        }
        //判断库存是否>0
        //预减库存
        //内存标记,减少读取redis的次数  当redis中库存为0,在内存中吧map对应goodsid的库存为空状态设置为true
        if(EmptyStockMap.get(goodsId)){
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }

        //Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
        //redis lua脚本返回的库存 stokc是减之前的库存,如果库存是8,那返回的是8,实际上redis里面已经变成7,如果redis里面stock 是0,那直接返回0
        //lua脚本可以防止redis中库存变成-1
        Long stock=(Long)redisTemplate.execute(redisScript, Collections.singletonList("seckillGoods:" + goodsId),Collections.emptyList());

        log.info("stock:"+stock);
        if(stock<=0){
            EmptyStockMap.put(goodsId,true);
            //valueOperations.increment("seckillGoods:" + goodsId);
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }

        // 判断是否重复抢购

        SeckillOrder seckillOrder = (SeckillOrder)valueOperations.get("order:" + user.getId() + ":" + goodsId);

        if(null!=seckillOrder){
            return RespBean.error(RespBeanEnum.REPEATE_ERROR);
        }

        //下单
        SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);

        mqSender.sendSeckillMessage(JSON.toJSONString(seckillMessage));

        //正在排队中 0
        return RespBean.success(0);

        /*
        GoodsVo goodsVo = goodsService.findGoodsVoById(goodsId);
        if(goodsVo.getStockCount()<1){
            model.addAttribute("errorMsg", RespBeanEnum.EMPTY_STOCK.getMessage());
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }
        SeckillOrder seckillOrder = (SeckillOrder)redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);

        //SeckillOrder seckillOrder = seckillOrderService.getOne(new QueryWrapper<SeckillOrder>().eq("goods_id", goodsId).eq("user_id", user.getId()));
        if(null!=seckillOrder){
            model.addAttribute("errorMsg", RespBeanEnum.REPEATE_ERROR.getMessage());
            return RespBean.error(RespBeanEnum.REPEATE_ERROR);
        }

        Order order= orderService.seckill(goodsVo,user);
        return RespBean.success(order);
        */


    }


    @RequestMapping("/getResult")
    @ResponseBody
    public RespBean getResult(Model model, User user, @RequestParam Long goodsId){
        if(null==user){
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }
        //从秒杀订单表中搜索当前用户对应的goodsId有没有订单,有订单表示秒杀成功

        Long orderId=orderService.getResult(user,goodsId);


        return RespBean.success(orderId);
    }

    @RequestMapping("/captcha")
    public void captcha(HttpServletRequest request, HttpServletResponse response,User user,Long goodsId) throws Exception {

        if(user==null||goodsId<0){
            throw new GlobalException(RespBeanEnum.REQUEST_ILLEGAL);
        }
        // 设置请求头为输出图片类型
        response.setContentType("image/gif");
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);

        // 算术类型
        ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 48,3);
        //captcha.setLen(3);  // 几位数运算,默认是两位
        //captcha.getArithmeticString();  // 获取运算的公式:3+2=?
        //captcha.text();  // 获取运算的结果:5

        redisTemplate.opsForValue().set("captcha:"+user.getId()+":"+goodsId,captcha.text(),300, TimeUnit.SECONDS);
        log.info("captcha公式:"+captcha.getArithmeticString());
        log.info("captcha结果:"+captcha.text());

        captcha.out(response.getOutputStream());  // 输出验证码
    }



    //原始秒杀
    @RequestMapping("/doSeckill0")
    public String doSeckill0(Model model, User user,Long goodsId){
        //判断秒杀库存 判断是否重复秒杀 秒杀(减库存 添加订单信息,添加秒杀订单信息)
        GoodsVo goodsVo = goodsService.findGoodsVoById(goodsId);
        if(goodsVo.getStockCount()<1){
            model.addAttribute("errorMsg", RespBeanEnum.EMPTY_STOCK.getMessage());
            return "seckillFail";
        }


        SeckillOrder seckillOrder = seckillOrderService.getOne(new QueryWrapper<SeckillOrder>().eq("goods_id", goodsId).eq("user_id", user.getId()));
        if(null!=seckillOrder){
            model.addAttribute("errorMsg", RespBeanEnum.REPEATE_ERROR.getMessage());
            return "seckillFail";
        }

        Order order= orderService.seckill(goodsVo,user);

        model.addAttribute("order",order);
        return "orderDetail";
    }




    @Override
    public void afterPropertiesSet() throws Exception {
        //初始化执行方法 商品库存数量加载到redis
        List<GoodsVo> list = goodsService.findGoodsVo();
        if(CollectionUtils.isEmpty(list)){
            return;
        }
        list.forEach(goodsVo ->{
            redisTemplate.opsForValue().set("seckillGoods:"+goodsVo.getId(),goodsVo.getStockCount());
            EmptyStockMap.put(goodsVo.getId(),false);
                }

        );
    }
}

IOrderService

seckill
getResult
createPath
checkPath

package com.example.miaosha.service;

import com.example.miaosha.pojo.Order;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.miaosha.pojo.SeckillOrder;
import com.example.miaosha.pojo.User;
import com.example.miaosha.vo.GoodsVo;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author cch
 * @since 2021-11-17
 */
public interface IOrderService extends IService<Order> {

    Order seckill(GoodsVo goodsVo, User user);

    Long getResult(User user, Long goodsId);

    String createPath(User user, Long goodsId);

    Boolean checkPath(User user, Long goodsId,String path);
}

OrderServiceImpl

seckill
getResult
createPath
checkPath

package com.example.miaosha.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.example.miaosha.mapper.SeckillOrderMapper;
import com.example.miaosha.pojo.Order;
import com.example.miaosha.mapper.OrderMapper;
import com.example.miaosha.pojo.SeckillGoods;
import com.example.miaosha.pojo.SeckillOrder;
import com.example.miaosha.pojo.User;
import com.example.miaosha.service.IOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.miaosha.service.ISeckillGoodsService;
import com.example.miaosha.service.ISeckillOrderService;
import com.example.miaosha.utils.MD5Util;
import com.example.miaosha.utils.UUIDUtil;
import com.example.miaosha.vo.GoodsVo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author cch
 * @since 2021-11-17
 */
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {

    @Autowired
    private ISeckillGoodsService seckillGoodsService;

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private SeckillOrderMapper seckillOrderMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    @Transactional
    public Order seckill(GoodsVo goodsVo, User user) {

        SeckillGoods seckillGoods = seckillGoodsService.getOne(new QueryWrapper<SeckillGoods>().eq("goods_id",goodsVo.getId()));
        seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
        //seckillGoodsService.updateById(seckillGoods);
        //单独更新库存 设置条件 id= stock_count>0
        /*
        boolean result = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().set("stock_count", seckillGoods.getStockCount())
                .eq("id", seckillGoods.getId()).gt("stock_count", 0));
        if(!result){
            return null;
        }

         */

        boolean result = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().setSql("stock_count=stock_count-1")
                .eq("id", seckillGoods.getId()).gt("stock_count", 0));
        if(!result){
            return null;
        }

        Order order = new Order();
        order.setUserId(user.getId());
        order.setGoodsId(goodsVo.getId());
        order.setDeliveryAddrId(1L);
        order.setGoodsName(goodsVo.getGoodsName());
        order.setGoodsCount(1);
        order.setGoodsPrice(goodsVo.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());
        seckillOrderMapper.insert(seckillOrder);

        //秒杀成功,表秒杀订单存入redis uid+gid
        redisTemplate.opsForValue().set("order:"+user.getId()+":"+goodsVo.getId(),seckillOrder);
        return order;
    }

    /*
    * return orderId 成功 id  /秒杀失败-1 /排队中 0
    * */
    @Override
    public Long getResult(User user, Long goodsId) {
        //改为从redis中获取秒杀成功信息
        SeckillOrder seckillOrder =(SeckillOrder)redisTemplate.opsForValue().get("order:"+user.getId()+":"+goodsId);

        /*
        SeckillOrder seckillOrder = seckillOrderMapper.selectOne(new QueryWrapper<SeckillOrder>().eq("goods_id", goodsId)
                .eq("user_id", user.getId()));

         */
        if(null !=seckillOrder){
            return seckillOrder.getOrderId();
        }
        //如果没有redis中没有查到订单消息,看是否秒杀结束,如果已经秒杀结束,由于本次秒杀结束,所以判断没有秒杀到
        //特殊情况:如果秒杀100,只有10人参与,那肯定都会秒杀到,所以不存在活动没有结束,一直查询秒杀结果的情况
        if(redisTemplate.hasKey("isStockEmpty:"+goodsId)){
            //秒杀结束
            return  -1L;
        }

        return 0L;
    }

    @Override
    public String createPath(User user, Long goodsId) {
        //获取秒杀地址
        String str = MD5Util.md5(UUIDUtil.uuid() + "123456");
        ValueOperations valueOperations = redisTemplate.opsForValue();
        valueOperations.set("seckillPath:"+user.getId()+":"+goodsId,str,60, TimeUnit.SECONDS);



        return str;
    }

    @Override
    public Boolean checkPath(User user, Long goodsId,String path) {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        String redisPath = (String)valueOperations.get("seckillPath:" + user.getId() + ":" + goodsId);
        if(user==null||goodsId<0||StringUtils.isEmpty(redisPath)){
            return false;
        }

        if(redisPath.equals(path)){
            return true;
        }

        return false;
    }
}

RedisConfig

redisTemplate
redisScript

package com.example.miaosha.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String,Object> redisTemplate= new RedisTemplate<>();

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }

    @Bean
    public DefaultRedisScript<Long> script(){
        DefaultRedisScript<Long> redisScript =new DefaultRedisScript<>();
        //lock.lua脚本位置和application.yml 同级目录
        redisScript.setLocation(new ClassPathResource("stock.lua"));
        redisScript.setResultType(Long.class);
        return redisScript;
    }

}

SeckillMessage

异步下单,发送消息体

package com.example.miaosha.pojo;


import com.example.miaosha.vo.GoodsVo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SeckillMessage {
    private User user;
    private Long goodsId;
}

MQSender

异步下单,发送消息

package com.example.miaosha.rabbitmq;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
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 sendSeckillMessage(String message){
        log.info("sendSeckill发送消息:"+message);
        rabbitTemplate.convertAndSend("seckillExchange","seckill.message",message);
    }

    /*

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

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

    public void sendDirect01(Object msg){
        log.info("directExchange发送消息:"+msg);
        rabbitTemplate.convertAndSend("direct_Exchange","queue.red",msg);
    }

    public void sendDirect02(Object msg){
        log.info("directExchange发送消息:"+msg);
        rabbitTemplate.convertAndSend("direct_Exchange","queue.green",msg);
    }

    public void sendTopic01(Object msg){
        log.info("topic_Exchange发送消息:"+msg);
        rabbitTemplate.convertAndSend("topic_Exchange","aaa.queue.bbb",msg);
    }

    public void sendTopic02(Object msg){
        log.info("topic_Exchange发送消息:"+msg);
        rabbitTemplate.convertAndSend("topic_Exchange","queue.red.message",msg);
    }

    public void sendHead01(String msg){
        log.info("topic_Exchange发送消息:"+msg);
        MessageProperties properties=new MessageProperties();
        properties.setHeader("color","red");
        properties.setHeader("speed","low");
        Message message=new Message(msg.getBytes(),properties);
        rabbitTemplate.convertAndSend("head_Exchange","",message);
    }

    public void sendHead02(String msg){
        log.info("topic_Exchange发送消息:"+msg);
        MessageProperties properties=new MessageProperties();
        properties.setHeader("color","red");
        properties.setHeader("speed","fast");
        Message message=new Message(msg.getBytes(),properties);
        rabbitTemplate.convertAndSend("head_Exchange","",message);
    }
    */


}

MQReceiver

异步下单,接受消息,进入下单流程
@RabbitListener(queues = “seckillQueue”)
public void receive(String message){}

package com.example.miaosha.rabbitmq;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.miaosha.pojo.SeckillMessage;
import com.example.miaosha.pojo.SeckillOrder;
import com.example.miaosha.pojo.User;
import com.example.miaosha.service.IGoodsService;
import com.example.miaosha.service.IOrderService;
import com.example.miaosha.service.ISeckillOrderService;
import com.example.miaosha.utils.JsonUtil;
import com.example.miaosha.vo.GoodsVo;
import com.example.miaosha.vo.RespBean;
import com.example.miaosha.vo.RespBeanEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
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.web.bind.annotation.RequestMapping;

@Service
@Slf4j
public class MQReceiver {

    @Autowired
    private IGoodsService goodsService;
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ISeckillOrderService seckillOrderService;

    @Autowired
    private IOrderService orderService;

    @RabbitListener(queues = "seckillQueue")
    public void receive(String message){
        log.info("seckillQueue接受消息:"+message);
        SeckillMessage seckillMessage = JSON.parseObject(message, SeckillMessage.class);
        Long goodsId = seckillMessage.getGoodsId();
        User user = seckillMessage.getUser();
        //下单操作
        GoodsVo goodsVo = goodsService.findGoodsVoById(goodsId);
        if(goodsVo.getStockCount()<1){
            //如果有key,表示,没有库存,结束
            redisTemplate.opsForValue().set("isStockEmpty:"+goodsId,"0");
            return;
        }

        //1. 判断是否重复抢购 从redis中判断
        ValueOperations valueOperations = redisTemplate.opsForValue();
        SeckillOrder seckillOrder = (SeckillOrder)valueOperations.get("order:" + user.getId() + ":" + goodsId);
        if(null!=seckillOrder){
            return;
        }
        //重复抢购

        //2.从t_seckill_order中获取
        SeckillOrder seckillOrderDB = seckillOrderService.getOne(new QueryWrapper<SeckillOrder>().eq("user_id", user.getId()).eq("goods_id", goodsId));

        if(null!=seckillOrderDB){
            return;
        }
        //重复抢购
        orderService.seckill(goodsVo,user);

    }


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

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

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

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

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

    @RabbitListener(queues = "topic_queue01")
    public void receive5(Message msg){
        log.info("topic_queue01接受消息:"+msg);
    }

    @RabbitListener(queues = "topic_queue02")
    public void receive6(Message msg){
        log.info("topic_queue02接受消息:"+msg);
    }


    @RabbitListener(queues = "head_queue01")
    public void receive7(Message msg){
        log.info("head_queue01接受消息:"+new String(msg.getBody()));
    }
    @RabbitListener(queues = "head_queue02")
    public void receive8(Message msg){
        log.info("head_queue02接受消息:"+new String(msg.getBody()));
    }
    */

}

RabbitMQConfig

初始化 队列,交换机 路由键,已经绑定

QUEUE
EXCHANGE
ROUTINGKEY

在这里插入图片描述

package com.example.miaosha.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class RabbitMQConfig {

    private static final  String QUEUE="seckillQueue";
    private static final  String EXCHANGE="seckillExchange";
    private static final  String ROUTINGKEY="seckill.#";



    @Bean
    public Queue seckillQueue(){
        return new Queue(QUEUE);
    }


    @Bean
    public TopicExchange seckillExchange(){
        return new TopicExchange(EXCHANGE);
    }

    @Bean
    public Binding binding(){
        return BindingBuilder.bind(seckillQueue()).to(seckillExchange()).with(ROUTINGKEY);
    }


}

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐