黑马点评中将消息队列由Redis实现换为RabbitMQ实现
Erlang与RabbitMQ的版本兼容问题
作者基于RabbitMQ 4.0.3与Erlang 26.2实现。
不同版本的RabbitMQ兼容的Erlang版本也不相同,如果版本不匹配可能会导致报错。具体版本对应关系可查看:Erlang Version Requirements | RabbitMQ
导入pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
添加yaml配置文件
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
编写RabbitConfig文件
@Bean
public Queue makeQueue() {
/**
* 1、name: 队列名称
* 2、durable: 是否持久化
如果为 true,队列会在 RabbitMQ 重启后保持存在,即队列是持久化的。
如果为 false,队列在 RabbitMQ 重启后会丢失,属于临时队列。
* 3、exclusive: 是否独享、排外的。
如果为 true,该队列仅对创建它的连接有效,且其他连接无法访问。
如果为 false,队列可以被多个连接共享。
* 4、autoDelete: 是否自动删除。
如果为 true,当最后一个消费者断开连接时,队列会被自动删除。
如果为 false,队列即使没有消费者,也不会被自动删除。
**/
return new Queue(RabbitConstants.SECKILL_ORDER_QUEUE, true, false, false);
}
@Bean
public DirectExchange makeDirectExchange() {
/**
* 1、name: 交换机名称
* 2、durable: 是否持久化
如果为 true,交换机会在 RabbitMQ 重启后仍然存在。
如果为 false,交换机会在 RabbitMQ 重启后丢失。
* 3、autoDelete: 是否自动删除。
如果为 true,当没有队列与该交换机绑定时,交换机会自动被删除。
如果为 false,交换机不会自动删除,即使没有队列与之绑定。
**/
return new DirectExchange(RabbitConstants.SECKILL_ORDER_EXCHANGE, true, false);
}
@Bean
public Binding bindDirect() {
//链式写法,绑定交换机和队列,并设置匹配路由键
return BindingBuilder
//绑定队列
.bind(makeQueue())
//到交换机
.to(makeDirectExchange())
//设置匹配路由键
.with(RabbitConstants.SECKILL_ORDER_ROUTING_KEY);
}
编写RabbitConstant文件
public class RabbitConstants {
// 队列名称
public static final String SECKILL_ORDER_QUEUE = "seckill.order.queue";
// 交换机名称
public static final String SECKILL_ORDER_EXCHANGE = "seckill.order.exchange";
// 路由键名称
public static final String SECKILL_ORDER_ROUTING_KEY = "seckill.order.routing.key";
}
修改VoucherOrderServiceImpl类中的秒杀逻辑
@Override
public Long seckill(Long voucherId) {
Long userId = UserHolder.getUser().getId();
// 1.执行Lua脚本
Long result = stringRedisTemplate.execute(
//参数1: 脚本
SECKILL_SCRIPT,
//参数2: key集合(已经在seckill脚本中写死,不需要传参)
Collections.emptyList(),
//参数3: 参数(优惠券id 和 用户id)
voucherId.toString(), userId.toString()
);
// 2.判断结果
int r = result.intValue();
if (r != 0) {
throw new RuntimeException(r == 1? "库存不足" : "同一用户不可重复下单");
}
// 3.将下单信息保存到阻塞队列
long orderId = redisIdWorker.nextId("order");
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
// 异步下单,三个参数分别为:交换机名称、路由键、传递的数据
rabbitTemplate.convertAndSend(RabbitConstants.SECKILL_ORDER_EXCHANGE, RabbitConstants.SECKILL_ORDER_ROUTING_KEY, voucherOrder);
// 4.返回订单id
return orderId;
}
这里有一个需要注意的地方:我们使用的是stringRedisTemplate,所以在执行Lua脚本时需要可变长参数(这里我们传的是优惠券id和用户id)全部转换为String类型(第12行),否则会报错。
编写异步下单工具类(消费者类)
@Component
@Slf4j
@RabbitListener(queues = RabbitConstants.SECKILL_ORDER_QUEUE)
public class AsyncRabbitListener {
@Resource
private IVoucherOrderService voucherOrderService;
@Resource
private ISeckillVoucherService seckillVoucherService;
// 消息处理方法:异步下单
@RabbitHandler
public void AsyncSave(VoucherOrder voucherOrder)
{
log.info("接收到存储订单信息的消息:{}", JSONUtil.toJsonStr(voucherOrder));
boolean success = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0).update();
voucherOrderService.save(voucherOrder);
log.info("订单信息存储完成:{}",success);
}
}
对于单方法消费者类:可以直接使用@RabbitListener来指定某个方法作为消息监听器,这个方法会接收来自指定队列的消息。常用参数为queues,用于指定监听的队列名,可以是单个队列或多个队列。
对于多方法消费者类:可以在类上使用@RabbitListener来指定某个类作为消息监听器,在类内使用@RabbitHandler来区分不同的消息处理方法。举例如下:
@RabbitListener(queues = "myQueue")
public class MyQueueListener {
@RabbitHandler
public void handleStringMessage(String message) {
System.out.println("Received String message: " + message);
}
@RabbitHandler
public void handleOrderMessage(Order order) {
System.out.println("Received Order message: " + order);
}
}