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);
}
}