1. 引言

在高并发场景下,如何保证优惠券秒杀的原子性和消息的可靠性是一个重要的问题。本篇文章介绍如何使用 Redisson 分布式锁 + Lua 脚本保证原子性,并结合 RabbitMQ 进行异步处理,来实现一个高效的优惠券秒杀任务。

2. 添加依赖

在 Spring Boot 项目中,我们需要引入 Redisson 和 RabbitMQ 相关依赖。

<!-- Redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

<!-- RabbitMQ -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

3. 使用 Docker 安装 RabbitMQ

docker pull rabbitmq:management

docker run \
 -e RABBITMQ_DEFAULT_USER=root \
 -e RABBITMQ_DEFAULT_PASS=123456 \
 -v mq-plugins:/plugins \
 --name mq \
 --hostname mq \
 -p 15672:15672 \
 -p 5672:5672 \
 --network mynet\
 -d \
 rabbitmq:3.8-management

安装完成后,访问 http://主机地址:15672,使用 root/123456 登录管理界面。

4. 配置 RabbitMQ

spring:
  rabbitmq:
    host: 你的主机
    port: 5672
    username: root
    password: 123456
    virtual-host: /

5. 编写 Lua 脚本保证原子性

在resources创建 seckill_mq.lua 脚本:

-- 获取优惠券Id
local voucherId = ARGV[1]
-- 获取用户Id
local userId = ARGV[2]
-- 库存 key
local stockKey = "seckill:stock:" .. voucherId
-- 订单 key
local orderKey = "seckill:order:" .. voucherId

-- 判断库存是否为空
if (tonumber(redis.call("get", stockKey)) <= 0) then
    return 1
end

-- 判断用户是否重复购买
if (redis.call("sismember", orderKey, userId) == 1) then
    return 2
end

-- 扣减库存
redis.call("incrby", stockKey, -1)
-- 记录用户购买信息
redis.call("sadd", orderKey, userId)
return 0

6. 定义 RabbitMQ 配置

@Configuration
public class RabbitMQConfig {
    public static final String EXCHANGE_NAME = "seckill.exchange";
    public static final String QUEUE_NAME = "seckill.queue";

    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange(EXCHANGE_NAME);
    }

    @Bean
    public Queue queue() {
        return new Queue(QUEUE_NAME, true); // 持久化队列
    }

    @Bean
    public Binding binding() {
        return BindingBuilder.bind(queue()).to(directExchange()).with("seckill.routingKey");
    }
}

7. 发送秒杀消息

@Service
@Slf4j
public class VoucherOrderRabbitServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderRabbitService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;
    @Autowired
    private RedisIdWorker redisIdWorker;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RabbitTemplate rabbitTemplate;

    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;

    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill_mq.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }

    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = 1010L;
        long orderId = redisIdWorker.nextId("order");

        // 执行 Lua 脚本
        Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString());

        int r = result.intValue();
        if (r != 0) {
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }

        // 发送消息到 RabbitMQ
        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "seckill.routingKey", voucherOrder);

        return Result.ok(orderId);
    }
}

8. 监听消息并处理订单

@Component
@Slf4j
public class SeckillOrderConsumer {

    @Autowired
    private IVoucherOrderRabbitService voucherOrderService;
    @Autowired
    private RedissonClient redissonClient;

    @RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
    public void receiveMessage(VoucherOrder voucherOrder, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        log.info("收到订单消息: {}", voucherOrder);
        try {
            RLock lock = redissonClient.getLock("order:" + voucherOrder.getUserId());
            if (lock.tryLock()) {
                try {
                    voucherOrderService.createVoucherOrder(voucherOrder);
                } finally {
                    lock.unlock();
                }
            }
            channel.basicAck(tag, false); // 手动确认
        } catch (Exception e) {
            log.error("处理订单失败: ", e);
        }
    }
}

9. 预加载 Redis 数据

在秒杀开始前,需要将优惠券库存等信息提前存入 Redis。

stringRedisTemplate.opsForValue().set("seckill:stock:" + voucherId, String.valueOf(stock));

更多推荐