代码编织梦想

SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见。
程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCloud 微服务系列项目开发

1 项目准备

SpringBoot RabbitMQ 延时队列取消订单【SpringBoot系列14】 本文章 基于这个项目来开发

本文章是系列文章 ,每节文章都有对应的代码,每节的源码都是在上一节的基础上配置而来,对应的视频讲解课程正在火速录制中。

如下图所示是本项目实现的一个秒杀下单流程的主要过程:
在这里插入图片描述

2 限流

本项目限流限制的是每个用户5秒内访问2次获取秒杀地址的接口

@Api(tags="商品秒杀模块")
@RestController()
@RequestMapping("/seckill")
@Slf4j
public class SecKillController {
    /**
     * 获取秒杀地址
     */
    // 接口限流
    @AccessLimit(second = 5, maxCount = 2)
    @GetMapping("/path/{id}")
    public R getPath(@PathVariable("id") Long goodsId, @RequestHeader Long userId) {
        // 创建秒杀地址
        return secKillService.createPath(userId, goodsId);
    }
}
2.1 限流自定义注解 AccessLimit
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;

}

@Retention修饰注解,用来表示注解的生命周期,生命周期的长短取决于@Retention的属性RetentionPolicy指定的值

  • RetentionPolicy.SOURCE 表示注解只保留在源文件,当java文件编译成class文件,就会消失 源文件 只是做一些检查性的操作,

  • RetentionPolicy.CLASS 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期 class文件(默认) 要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife)

  • RetentionPolicy.RUNTIME 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在 运行时也存在 需要在运行时去动态获取注解信息

@Target 说明了Annotation所修饰的对象范围

  • 1.CONSTRUCTOR:用于描述构造器
  • 2.FIELD:用于描述域
  • 3.LOCAL_VARIABLE:用于描述局部变量
  • 4.METHOD:用于描述方法
  • 5.PACKAGE:用于描述包
  • 6.PARAMETER:用于描述参数
  • 7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
2.2 自定义拦截器 处理限流
@Component
@Slf4j
public class AccessLimitInterceptor implements HandlerInterceptor {

    @Autowired
    RedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("==============================AccessLimitInterceptor拦截器==============================");
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;
            AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
            if (Objects.isNull(accessLimit)) {
                return true;
            }
            int second = accessLimit.second();
            int maxCount = accessLimit.maxCount();
            boolean needLogin = accessLimit.needLogin();
            String uri = request.getRequestURI();
            if (needLogin) {
                //需要登录  本项目使用的是 Spring Security 实现安全认证 
                //认证通过后 才会走到这里 
                String userId = request.getHeader("userId");
//                UserInfo userInfo = getUserInfoFromRequest(request);
//                if (Objects.isNull(userInfo)) {
//                    toRender(response, "请登录");
//                    return false;
//                }
                uri = uri + ":" + userId;
            }
            return toLimit(response, second, maxCount, uri);
        }
        return true;
    }

    // 简单计数器限流
    private boolean toLimit(HttpServletResponse response, int second, int maxCount, String uri) throws IOException {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        Integer count = (Integer) valueOperations.get(uri);
        if (Objects.isNull(count)) {
            valueOperations.set(uri, 1, second, TimeUnit.SECONDS);
        } else if (count < maxCount) {
            // 计数器加一
            valueOperations.increment(uri);
        } else {
            log.info("触发限流规则 限流{}秒访问{}次,当前访问{} {}次 ",second,maxCount,count,uri);
            // 超出访问限制
            toRender(response, "当前下单人数排队中 请稍后重试");
            return false;
        }
        return true;
    }

3 发起秒杀

用户获取到秒杀地址后,使用秒杀地址发起秒杀

    /**
     * 开始秒杀
     * @param goodsId
     * @param userId
     * @return
     */
    @GetMapping("/{path}/toSecKill/{id}")
    public R toSecKill(@PathVariable("id") Long goodsId,
                       @PathVariable String path,
                       @RequestHeader Long userId) {
        // 验证路径是否合法
        boolean isLegal = secKillService.checkPath(path, userId, goodsId);
        if (!isLegal) {
            return R.error("路径不合法");
        }
        return secKillService.isToSecKill(goodsId, userId);
    }

首先是校验了一下地址的合法,与上述生成地址的规则一致,然后就是预下单生成订单号的过程:

@Service("secKillService")
@Slf4j
public class SecKillServiceImpl implements SecKillService, InitializingBean {
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private SecKillGoodsService secKillGoodsService;

    @Autowired
    private SecKillOrderService secKillOrderService;

    @Autowired
    private OrderMQSender mqSender;
    // 空库存的 map 集合
    private Map<Long, Boolean> emptyStockMap = new HashMap<>();
    @Autowired
    SnowFlakeCompone snowFlakeCompone;
    @Override
    public R isToSecKill(Long goodsId, Long userId) {

        // 重复抢购
        SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + userId + ":" + goodsId);
        if (!Objects.isNull(seckillOrder)) {
            return R.error("重复抢购");
        }
        // 内存标记,减少 Redis 的访问
        if (emptyStockMap.get(goodsId)) {
            // 库存为空
            return R.error("商品库存不足");
        }
        //库存 key
        String redisStockKey = "seckillGoods:" + goodsId;

        Boolean aBoolean = redisTemplate.hasKey(redisStockKey);
        if(Boolean.FALSE.equals(aBoolean)){
            emptyStockMap.put(goodsId, true);
            return R.error("商品库存不足");
        }

        ValueOperations valueOperations = redisTemplate.opsForValue();

        // 预减库存
        Long stock = valueOperations.decrement(redisStockKey);
        // 库存不足
        if (stock < 0) {
            emptyStockMap.put(goodsId, true);
            valueOperations.increment(redisStockKey);
            return R.error("商品库存不足");
        }
        //生成订单号
        long sn = snowFlakeCompone.getInstance().nextId();
        //保存到redis中 状态 doing 正在处理中
        redisTemplate.opsForValue().set("sn:"+sn, "doing");
        // 秒杀消息
        SecKillMessage message = new SecKillMessage(userId, goodsId,sn);
        mqSender.sendSecKillMessage(JsonUtils.toJson(message));
        //把订单号返回给前端
        return R.okData(sn);
    }

内存中保存的库存信息与Redis中保存的库存信息,是通过定时任务在开始秒杀的前一小时同步进来的,定时任务会在后续的篇章里集成。

订单号返回前端,前端就开始轮循查询订单状态的接口

    /**
     * 查询订单状态与详情
     * 商品-下单入口调用
     * @param sn
     * @return
     */
    @GetMapping("/statues/detail/{sn}")
    public R detailAndStatue(@PathVariable("sn") Long sn) {
        //redis 中查询状态
        Boolean aBoolean = redisTemplate.hasKey("sn:" + sn);
        if(Boolean.FALSE.equals(aBoolean)){
            return R.error("下单失败");
        }
        String snStatues = redisTemplate.opsForValue().get("sn:" +sn).toString();
        //未下单完
        if(snStatues.equals("doing")){
            return R.error(202,"处理中");
        }
        //未下单成功
        if(!snStatues.equals("ok")){
            return R.error(203,snStatues);
        }
        //下单成功 返回订单信息
        OrderVo orderVo = orderService.detailFromSn(sn);
        return R.okData(orderVo);
    }

前端查询到下单成功后,加载显示订单详情,发起支付。

4 消息队列

消息队列、交换机的定义如下:

@Configuration
public class OrderRabbitMQTopicConfig {

    private static final String QUEUE = "seckillQueue";
    private static final String EXCHANGE = "seckillExchange";


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

秒杀预下单消息发送者

@Service
@Slf4j
public class OrderMQSender {


    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 秒杀订单走的消息队列
     * @param msg
     */

    public void sendSecKillMessage(String msg) {
        log.info("发送消息:{}", msg);
        //参数一 交换机名称 
        //参数二 路由名称
        rabbitTemplate.convertAndSend("seckillExchange", "seckill.message", msg);
    }
 }

秒杀订单 消息接收者 ,对订单的库存进行了二次校验

@Service
@Slf4j
public class OrderMQReceiver {

    @Autowired
    private SecKillGoodsService secKillGoodsService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private OrderService orderService;

    @RabbitListener(queues = "seckillQueue")
    public void receiveSecKillMessage(String message) {
        log.info("接收的秒杀订单消息:{}", message);
        SecKillMessage secKillMessage = JsonUtils.toObj(message, SecKillMessage.class);

        Long userId = secKillMessage.getUserId();
        Long goodsId = secKillMessage.getGoodsId();
        Long sn = secKillMessage.getSn();
        //查询秒杀商品
        SeckillGoods seckillGoods = secKillGoodsService.findByGoodsId(goodsId);
        // 库存不足
        if (seckillGoods.getStockCount() < 1) {
            //更新redis订单状态
            redisTemplate.opsForValue().set("sn:" + sn, "秒杀失败 库存不足",1, TimeUnit.DAYS);
            log.error("库存不足");
            return;
        }

        // 判断是否重复抢购
        // 重复抢购
        SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + userId + ":" + goodsId);
        if (!Objects.isNull(seckillOrder)) {
            //更新redis订单状态
            redisTemplate.opsForValue().set("sn:" + sn, "秒杀失败 重复抢购",1, TimeUnit.DAYS);
            log.error("重复抢购 userId:{} goodsId:{}",userId,goodsId);
            return;
        }

        // 下订单
        orderService.toSecKill(goodsId, userId,sn);
    }

}


项目源码在这里 :https://gitee.com/android.long/spring-boot-study/tree/master/biglead-api-11-snow_flake
有兴趣可以关注一下公众号:biglead


  1. 创建SpringBoot基础项目
  2. SpringBoot项目集成mybatis
  3. SpringBoot 集成 Druid 数据源【SpringBoot系列3】
  4. SpringBoot MyBatis 实现分页查询数据【SpringBoot系列4】
  5. SpringBoot MyBatis-Plus 集成 【SpringBoot系列5】
  6. SpringBoot mybatis-plus-generator 代码生成器 【SpringBoot系列6】
  7. SpringBoot MyBatis-Plus 分页查询 【SpringBoot系列7】
  8. SpringBoot 集成Redis缓存 以及实现基本的数据缓存【SpringBoot系列8】
  9. SpringBoot 整合 Spring Security 实现安全认证【SpringBoot系列9】
  10. SpringBoot Security认证 Redis缓存用户信息【SpringBoot系列10】
  11. SpringBoot 整合 RabbitMQ 消息队列【SpringBoot系列11】
  12. SpringBoot 结合RabbitMQ与Redis实现商品的并发下单【SpringBoot系列12】
  13. SpringBoot 雪花算法生成商品订单号【SpringBoot系列13】
  14. SpringBoot RabbitMQ 延时队列取消订单【SpringBoot系列14】 本文章 基于这个项目来开发
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zl18603543572/article/details/129624311

rabbitmq+springboot设计秒杀应用_一行代码的自述的博客-爱代码爱编程

       秒杀业务的核心是库存处理,用户购买成功后会进行减库存操作,并记录购买明细。当秒杀开始时,大量用户同时发起请求,这是一个并行操作,多条更新库存数量的SQL语句会同时竞争秒杀商品所处数据库表里的那行数据,导致库存的减少数量与购买明细的增加数量不一致,因此,我们使用RabbitMQ进行削峰限流并且将请求数据串行处理。     秒杀系统场景特点

springboot集成rabbitmq商品秒杀业务实战(流量削峰)-爱代码爱编程

消息队列如何实现流量削峰? 要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。 这里就不讲springboot和rabbitmq如何集成了,参考文章https://www.cnblogs.com/fantongxue/p/12

springboot rabbitmq 的使用-爱代码爱编程

目录 1、rabbitMQ 2、rabbitMQ的下载、安装、使用 3、使用场景 3.1 异步处理 3.2 应用解耦 3.3 流量削峰 3.4 定时任务 4、rabbitmq管理界面 5、六种队列 5.1 简单队列(hello world) 5.2 工作队列(work queue) 5.3 发布/订阅队列(publish/subsc

springboot集成rabbitmq商品秒杀业务实战(流量削峰),面试官都被搞懵了-爱代码爱编程

我们使用压力测试工具jweter对其进行并发性测试。 二,springboot开始集成rabbitmq 1,加入amqp的依赖 org.springframework.amqp spring-rabbit 【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】 开源分享完整内容戳这里 2,配置

springboot使用rabbitmq6大模式详解_sora33的博客-爱代码爱编程

基本介绍 rabbitmq是一个基于Erlang语言开发且非常好用的一款开源的amqp(高级消息队列)。主要的业务场景有秒杀、消息的订阅分发,抢优惠卷等高并发场景。主要的亮点有三个 三大亮点 解耦:一个系统调用多个模块。互相调用的关系很复杂很麻烦。如果没有消息队列,每当一个新业务接入,我们都要在主系统调用新接口。使用消息队列,我们只需要关心是否送达。

springboot rabbitmq 注解版 基本概念与基本案例_java那点事儿的博客-爱代码爱编程

目录 Windows安装RabbitMQ 环境工具下载 rabbitMQ是Erlang语言开发的所以先下载Erlang; RabbitMQ官网地址: https://www.rabbitmq.com/ Erlang下载: https://www.erlang.org/downloads Erlang环境安装 直接运行: otp_win64_23

springboot整合rabbitmq消息队列-爱代码爱编程

RabbitMQ 一、RabbitMQ介绍 1.1 现存问题 服务调用:两个服务调用时,我们可以通过传统的HTTP方式,让服务A直接去调用服务B的接口,但是这种方式是同步的方式,虽然可以采用SpringBoot提供的@

springboot 整合 rabbitmq高级特性 & 真实业务应用_rabbitmq在springboot中的应用-爱代码爱编程

♨️本篇文章记录的为RabbitMQ知识中高级特性和企业级项目相关内容,适合在学Java的小白,帮助新手快速上手,也适合复习中,面试中的大佬🙉🙉🙉。 ♨️如果文章有什么需要改进的地方还请大佬不吝赐教❤️🧡💛 💖个人

【rabbitmq】federation实现消息传递_rabbitmq_federation-爱代码爱编程

RabbitMQ集群对时间非常敏感,应该在局域网中使用,不应在关于网中使用。而Federation插件可以很好地解决这个问题。这篇文章和大家分享RabbitMQ Federation的使用场景、实现原理和具体用法。 使用场景 Federation插件的目的就是解决RabbitMQ节点之间进行消息传递而不需要建立集群,这个功能在很多场景中都很有用: 在