SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见。
程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCloud 微服务系列项目开发
SpringBoot RabbitMQ 延时队列取消订单【SpringBoot系列14】 本文章 基于这个项目来开发
本文章是系列文章 ,每节文章都有对应的代码,每节的源码都是在上一节的基础上配置而来,对应的视频讲解课程正在火速录制中。
如下图所示是本项目实现的一个秒杀下单流程的主要过程:

本项目限流限制的是每个用户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);}
}
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所修饰的对象范围
@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;}
用户获取到秒杀地址后,使用秒杀地址发起秒杀
/*** 开始秒杀* @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);}
前端查询到下单成功后,加载显示订单详情,发起支付。
消息队列、交换机的定义如下:
@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