基于 Redisson 分布式锁 + Lua 脚本 + RabbitMQ 实现优惠券秒杀任务(黑马点评项目)
·
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));


更多推荐



所有评论(0)