秒杀的特点
1.业务简单(下单>扣库存)
2.瞬时流量高,常态流量低
技术实现
1.先把商品数据预热到redis里,扣减redis的库存
2.把购买过商品的用户id放进set中判断有没有购买过(一人一单)
3.没有购买过,进行下单
4.把用户id放进set中
5.发送异步消息,进行数据库扣除库存,生成订单,下游服务慢慢消费
判断库存是否充足,是否有购买资格,扣减库存必须是原子性操作,所以要放lua脚本中执行
主要代码 最后会贴git地址
创建项目,引入redis,redisson,rocketmq
rocketmq生产者
@Slf4j
@Component
public class MQProducerService {
@Value("${rocketmq.producer.send-message-timeout}")
private Integer messageTimeOut;
// 建议正常规模项目统一用一个TOPIC
private static final String topic = "RLT_TEST_TOPIC";
// 直接注入使用,用于发送消息到broker服务器
@Autowired
private RocketMQTemplate rocketMQTemplate;
public MQProducerService(RocketMQTemplate rocketMQTemplate) {
this.rocketMQTemplate = rocketMQTemplate;
DefaultMQProducer producer = new DefaultMQProducer();
}
/**
* 发送同步消息(阻塞当前线程,等待broker响应发送结果,这样不太容易丢失消息)
* (msgBody也可以是对象,sendResult为返回的发送结果)
*/
public SendResult sendMsg(String msgBody) {
SendResult sendResult = rocketMQTemplate.syncSend(topic, MessageBuilder.withPayload(msgBody).build());
log.info("【sendMsg】sendResult={}", JSON.toJSONString(sendResult));
return sendResult;
}
/**
* 发送异步消息(通过线程池执行发送到broker的消息任务,执行完后回调:在SendCallback中可处理相关成功失败时的逻辑)
* (适合对响应时间敏感的业务场景)
*/
public void sendAsyncMsg(String msgBody,String tag) {
rocketMQTemplate.asyncSend(topic+":"+tag, MessageBuilder.withPayload(msgBody).build(), new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// 处理消息发送成功逻辑
log.info("消息发送成功!!");
}
@Override
public void onException(Throwable throwable) {
// 处理消息发送异常逻辑
}
});
}
/**
* 发送延时消息(上面的发送同步消息,delayLevel的值就为0,因为不延时)
* 在start版本中 延时消息一共分为18个等级分别为:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
*/
public void sendDelayMsg(String msgBody, int delayLevel) {
rocketMQTemplate.syncSend(topic, MessageBuilder.withPayload(msgBody).build(), messageTimeOut, delayLevel);
}
/**
* 发送单向消息(只负责发送消息,不等待应答,不关心发送结果,如日志)
*/
public void sendOneWayMsg(String msgBody) {
rocketMQTemplate.sendOneWay(topic, MessageBuilder.withPayload(msgBody).build());
}
/**
* 发送带tag的消息,直接在topic后面加上":tag"
*/
public SendResult sendTagMsg(String msgBody) {
return rocketMQTemplate.syncSend(topic + ":tag2", MessageBuilder.withPayload(msgBody).build());
}
}
controller代码
@PostMapping("/{productId}")
public R sec(@PathVariable("productId") Long productId){
UserDTO user = UserHolder.getUser();
SeckillDTO seckillDTO = new SeckillDTO(user.getId(),productId);
return seckillService.seckill(seckillDTO);
}
/**
秒杀实体类
*/
@Data
@NoArgsConstructor
public class SeckillDTO {
private Long userId;
private Long productId;
public SeckillDTO(Long userId, Long productId) {
this.userId = userId;
this.productId = productId;
}
}
秒杀具体业务
public R seckill(SeckillDTO seckillDTO) {
Long productId = seckillDTO.getProductId();
Long userId = seckillDTO.getUserId();
//使用redis生成订单id
long orderId = redisIdWorker.nextId(RedisConstants.ORDER_ID_PREFIX);
/**
* 0.生成订单id
* 1.执行lua脚本
*/
Long res = stringRedisTemplate.execute(SECKILL_SCRIPT,
Collections.emptyList(),
userId.toString(), //对应lua脚本中的ARGV[1]
productId.toString() //对应lua脚本中的ARGV[2]
);
if(null == res) throw new ServiceException("lua脚本出错");
if(res == 1) return R.fail("商品库存不足");
else if(res == 2) return R.fail("不能重复下单");
else {
//发送mq消息异步扣库存,生成订单
CreateOrderDTO createOrderDTO = new CreateOrderDTO();
createOrderDTO.setOrderId(orderId);
createOrderDTO.setUserId(userId);
createOrderDTO.setProductId(productId);
String msgBody = JSONUtil.toJsonStr(createOrderDTO);
log.info("msgBody:{}",msgBody);
mqProducerService.sendAsyncMsg(msgBody,"create_order");
}
return R.ok();
}
lua脚本
---
--- Generated by Luanalysis
--- Created by shentong.
--- DateTime: 2023/3/23 15:30
--- 库存不足返回1,已购买过返回2 下单成功返回0
local userId = ARGV[1]
local productId = ARGV[2]
local stockKey = 'seckill:stock:' .. productId
local orderKey = 'seckill:order:' .. productId
---1.判断库存是否充足
if(tonumber(redis.call('get',stockKey)) <=0 ) then
return 1
end
---2.判断是否已经购买过
if(redis.call('sismember',orderKey,userId) == 1) then
return 2
end
---3.扣减库存 添加已购用户id
redis.call('incrby',stockKey,-1)
redis.call('sadd',orderKey,userId)
return 0
消费者异步扣库存,下单
/**
* @author shentong
* @since 2023/3/23 16:03
* 异步下单
*/
@Component
@Slf4j
@RocketMQMessageListener(topic = "RLT_TEST_TOPIC", selectorExpression = "create_order", consumerGroup = "Con_Group_Two")
public class CreateOrderConsumer implements RocketMQListener<MessageExt> {
@Resource
private SeckillService seckillService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void onMessage(MessageExt message) {
byte[] body = message.getBody();
String msg = new String(body);
CreateOrderDTO createOrderDTO = JSONUtil.toBean(msg, CreateOrderDTO.class);
if(message.getReconsumeTimes() == 3){
log.error("{}消费了3次都消费失败",createOrderDTO.toString());
//消息入库,人工干预
}
log.info("createOrderDTO:{}",createOrderDTO.toString());
if (Boolean.FALSE.equals(stringRedisTemplate.opsForSet().isMember(RedisConstants.MSG_CONSUMED_SET, createOrderDTO.getOrderId().toString()))) {
seckillService.createSeckillOrder(createOrderDTO);
stringRedisTemplate.opsForSet().add(RedisConstants.MSG_CONSUMED_SET,createOrderDTO.getProductId().toString());
}
}
}
下扣库存业务
public R createSeckillOrder(CreateOrderDTO dto){
Long userId = dto.getUserId();
//使用redisson分布式锁
RLock lock = redissonClient.getLock("lock:order:" + userId);
boolean res = lock.tryLock();
if(!res){
return R.fail("已经购买过");
}
try {
SeckillService proxy = (SeckillService) AopContext.currentProxy();
return proxy.handlerOrder(dto);
} finally {
lock.unlock();
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<Object> handlerOrder(CreateOrderDTO dto) {
Long userId = dto.getUserId();
Long orderId = dto.getOrderId();
Long productId = dto.getProductId();
stockService.decutStock(productId,1);
log.info("productId:{}",productId);
Product product = productService.getById(productId);
if(null == product) throw new ServiceException("商品不存在!");
Order order = new Order();
order.setId(orderId);
order.setCount(1);
order.setPrice(product.getPrice());
order.setProductId(productId);
order.setUserId(userId);
orderService.save(order);
return R.ok(order);
}
用jmeter测试提前把id为1的商品库存预热到redis中 100个
创建一万多个用户,并且登录,把所有的token存放到一个txt文件中,jmeter运行的时候一行一行读取token模拟每一个用户操作
创建10999个线程
设置从文件读取token,填写txt文件地址
设置header请求头中添加authorization
执行jmeter,执行结束后 库存变成0,生成了100个订单