SpringBoot RabbitMQ 商品秒杀【SpringBoot系列15】
创始人
2025-05-29 17:28:38

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 {@AutowiredRedisTemplate redisTemplate;@Overridepublic 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 {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate SecKillGoodsService secKillGoodsService;@Autowiredprivate SecKillOrderService secKillOrderService;@Autowiredprivate OrderMQSender mqSender;// 空库存的 map 集合private Map emptyStockMap = new HashMap<>();@AutowiredSnowFlakeCompone snowFlakeCompone;@Overridepublic 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("商品库存不足");}//库存 keyString 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";@Beanpublic Queue seckillQueue() {return new Queue(QUEUE);}@Beanpublic TopicExchange seckillExchange() {return new TopicExchange(EXCHANGE);}@Beanpublic Binding binding() {return BindingBuilder.bind(seckillQueue()).to(seckillExchange()).with("seckill.#");}
}

秒杀预下单消息发送者

@Service
@Slf4j
public class OrderMQSender {@Autowiredprivate RabbitTemplate rabbitTemplate;/*** 秒杀订单走的消息队列* @param msg*/public void sendSecKillMessage(String msg) {log.info("发送消息:{}", msg);//参数一 交换机名称 //参数二 路由名称rabbitTemplate.convertAndSend("seckillExchange", "seckill.message", msg);}}

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

@Service
@Slf4j
public class OrderMQReceiver {@Autowiredprivate SecKillGoodsService secKillGoodsService;@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate 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】 本文章 基于这个项目来开发

相关内容

热门资讯

官宣!四川又一条国家级户外运动... 2月10日,国家体育总局发布2026年第1号公告——“2026年春节假期户外运动精品线路”,我省“乐...
武汉打车全攻略:出租车好打吗?... 武汉打车全攻略:出租车好打吗?如何避免拒载?本地人教你几招! 很多第一次来武汉的朋友,尤其是带着行李...
护航春节!三亚市市场监管局严查... 为严格落实海南省市场监督管理局春节期间特种设备安全风险提示工作要求,全力保障春节假期全市特种设备安全...
蜜雪冰城墨西哥首店开业,民众排... 当地时间2月12日,蜜雪冰城墨西哥首店在该国首都墨西哥城的宪法广场开业,吸引许多当地民众前往排队购买...
别再冤枉巧克力了!这些健康真相... 🍫 你是否曾因为想吃巧克力而感到内心挣扎?巧克力,这份承载着甜蜜与祝福的礼物,常常被视为“增肥”的代...