SpringBoot RabbitMQ 商品秒杀【SpringBoot系列15】

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

1 项目准备

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

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

如下图所示是本项目实现的一个秒杀下单流程的主要过程:
SpringBoot RabbitMQ 商品秒杀【SpringBoot系列15】_第1张图片

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】 本文章 基于这个项目来开发

你可能感兴趣的:(SpringBoot,java点滴积累,java-rabbitmq,rabbitmq,spring,boot)