秒杀的特点

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个

lua脚本如何编写定时器 lua脚本实现秒杀_java-rocketmq

创建一万多个用户,并且登录,把所有的token存放到一个txt文件中,jmeter运行的时候一行一行读取token模拟每一个用户操作

lua脚本如何编写定时器 lua脚本实现秒杀_lua脚本如何编写定时器_02


创建10999个线程

lua脚本如何编写定时器 lua脚本实现秒杀_lua脚本如何编写定时器_03


设置从文件读取token,填写txt文件地址

lua脚本如何编写定时器 lua脚本实现秒杀_java-rocketmq_04


设置header请求头中添加authorization

lua脚本如何编写定时器 lua脚本实现秒杀_lua脚本如何编写定时器_05


执行jmeter,执行结束后 库存变成0,生成了100个订单

lua脚本如何编写定时器 lua脚本实现秒杀_redis_06


lua脚本如何编写定时器 lua脚本实现秒杀_ide_07