SpringBoot RabbitMQ 商品秒杀SpringBoot系列15
Posted 早起的年轻人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot RabbitMQ 商品秒杀SpringBoot系列15相关的知识,希望对你有一定的参考价值。
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
- 创建SpringBoot基础项目
- SpringBoot项目集成mybatis
- SpringBoot 集成 Druid 数据源【SpringBoot系列3】
- SpringBoot MyBatis 实现分页查询数据【SpringBoot系列4】
- SpringBoot MyBatis-Plus 集成 【SpringBoot系列5】
- SpringBoot mybatis-plus-generator 代码生成器 【SpringBoot系列6】
- SpringBoot MyBatis-Plus 分页查询 【SpringBoot系列7】
- SpringBoot 集成Redis缓存 以及实现基本的数据缓存【SpringBoot系列8】
- SpringBoot 整合 Spring Security 实现安全认证【SpringBoot系列9】
- SpringBoot Security认证 Redis缓存用户信息【SpringBoot系列10】
- SpringBoot 整合 RabbitMQ 消息队列【SpringBoot系列11】
- SpringBoot 结合RabbitMQ与Redis实现商品的并发下单【SpringBoot系列12】
- SpringBoot 雪花算法生成商品订单号【SpringBoot系列13】
- SpringBoot RabbitMQ 延时队列取消订单【SpringBoot系列14】 本文章 基于这个项目来开发
以上是关于SpringBoot RabbitMQ 商品秒杀SpringBoot系列15的主要内容,如果未能解决你的问题,请参考以下文章
基于Vue + SpringBoot实现的前后端分离的商城项目,包含秒杀模块(毕设)