定义
RabbitMQ 是一种消息中间件,用来接收、存储、转发数据库。类似一个快递站,我们把快递给到这个快递站、快递站暂存我的快递,然后转发到别的地方。
核心概念
四大组成部分
生产者(Producer):产生消息的程序;
交换机(Exchange) : 生成者产生的消息首先会发到交换机中,然后把消息推送到队列中,可以指定推送到多个队列、单个队列或者丢弃消息;
队列(Queue):接收来自交换机的消息,本质是一个消息缓冲区,消息真正存储在队列中;
消费者(Consumner):处理来自指定队列的消息;
核心名称
RabbitMQ 整体框架
Broker :接收和转发消息的应用,RabbitMQ Server 就是 Broker Server;
Connection :Producer 与 Consumer 和 Broker 之间的 TCP 连接;
Channel :每一次访问 Broker 都会建立一个 Channel,Channel 是在 Connection 中建立的逻辑连接,AMQP method
包含了 channel id 帮助客 户端和 message broker 识别 channel,所以 channel 之间是完全隔离的,极大减少了建立 TCP 连接的开销;
Exchange :message 到达 Broker 的第一站,根据 routing key 的分发规则把消息分发到不同的 Queue 中
Queue:消息存储的地方,等待被 consumer 取走;
Binding:exchange 与 queue 之间的虚拟连接,bingding 中包含 routing key ,routing key 放在 exchange 的查询表中,用于 message 的分发;
消息分发模式
简单模式
简单模式是最基本的工作模式,一个生产者将消息发送到一个队列,一个消费者从队列中获取消息进行消费
工作队列模式
工作队列模式是一个任务在多个消费者之间并发处理,一个消息只能被一个消费者消费,这种模式可以提供系统的吞吐量和处理能力
发布订阅模式
发布订阅模式是实现一个消息同时被多个消费者处理,生产者将消息发送到交换机,交换机消息广播 到所有的绑定的队列上,每个队列对应一个消费者,这种模式适合一个消息被多个消费者消费
路由模式
路由模式能够实现将消息传递给指定的队列去消费,实现不同的消息发送到不同的队列中去处理
主题模式
主题模式能够实现复杂的消息的分发,生产者发送消息到交换机,然后根据路由键指定的匹配规则将消息分发到不同的队列中
RPC 模式
RPC 是一种实现分布式系统中远程调用的工作模式,指的是通过 RabbitMQ 来实现的一种 RPC 的能力
整合 SpringBoot
编写配置 yml 文件
yaml
rabbitmq:
host: localhost
port: 5672
username: admin
password: 123456
publisher-confirm-type: correlated # 交换机回退消息
template:
mandatory: true # 队列回退消息
引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
Config 文件绑定交换机和队列
在 RabbitMqConfig.java 编写注册交换机和队列,以及配置两者之间的关系
kotlin
@Configuration
public class RabbitMqConfig {
/**
* 注册交换机
* @return 交换机
*/
@Bean("lotteryActivityResultExchange")
public DirectExchange lotteryActivityResultExchange() {
return ExchangeBuilder
.directExchange("LOTTERY_ACTIVITY_RESULT_EXCHANGE")
.durable(true)
.build();
}
/**
* 注册队列
* @return 队列
*/
@Bean("lotteryActivityResultQueue")
public Queue lotteryActivityResultQueue() {
return QueueBuilder
.durable("LOTTERY_ACTIVITY_RESULT_QUEUE")
.build();
}
/**
* 配置绑定关系
* @param exchange 交换机
* @param queue 队列
* @return 绑定关系
*/
@Bean
public Binding lotteryActivityResultBinding(@Qualifier("lotteryActivityResultExchange") DirectExchange exchange,
@Qualifier("lotteryActivityResultQueue") Queue queue) {
return BindingBuilder.bind(queue).to(exchange).with("LOTTERY_ACTIVITY_RESULT");
}
}
生产者 Producer
typescript
@Component
public class LotteryActivityStockProducer implements RabbitTemplate.ConfirmCallback,
RabbitTemplate.ReturnCallback {
private final static Logger logger = LoggerFactory.getLogger(LotteryActivityStockProducer.class);
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
public void sendUpdateActivityStock(Long goodId) {
CorrelationData correlationData = new CorrelationData();
correlationData.setId(goodId.toString());
Message message = new Message(ByteUtil.longToBytes(goodId));
rabbitTemplate.convertAndSend(Code.RabbitMqMessage.LOTTERY_ACTIVITY_STOCK_EXCHANGE,
Code.RabbitMqMessage.LOTTERY_ACTIVITY_STOCK_RK, message, correlationData);
logger.info("LotteryActivityStockProducer|sendUpdateActivityStock|发送消息,更新秒杀活动表的库存|activityId: {}", goodId);
}
/**
* 消息到达交换器的确认回调
*
* @param correlationData
* @param ack
* @param cause
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
logger.info("LotteryActivityStockProducer|sendUpdateActivityStock|消息到达交换机,更新秒杀活动表的库存|activityId: {}", correlationData.getId());
return;
}
logger.error("LotteryActivityStockProducer|sendUpdateActivityStock|消息未到达交换机,更新库存失败|activityId: {}", correlationData.getId());
}
/**
* 消息到达队列的回调
*
* @param message
* @param replyCode
* @param replyText
* @param exchange
* @param routingKey
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
logger.error("LotteryActivityStockProducer|sendUpdateActivityStock|未到达指定队列,更新库存失败|errMsg: {}, exchange: {}, routingKey: {}"
, replyText, exchange, routingKey);
}
}
消费者 Consumer
java
@Component
public class LotteryActivityConsumer {
private static final Logger logger = LoggerFactory.getLogger(LotteryActivityConsumer.class);
@Resource
private LotteryActivityMapper lotteryActivityMapper;
@RabbitListener(queues = Code.RabbitMqMessage.LOTTERY_ACTIVITY_STOCK_QUEUE)
public void resultLotteryActivityStockConsumer(Message message, Channel channel) {
byte[] body = message.getBody();
Long activityId = null;
try {
// 其实数据量太大也会拖垮数据库,可以使用 xxljob 定时任务去扫描缓存中的库存数量进行更新库粗
activityId = ByteUtil.bytesToLong(body);
logger.info("LotteryActivityConsumer|resultLotteryActivityConsumer|收到活动id|activityId: {}", activityId);
int stock = lotteryActivityMapper.decreaseActivityStock(activityId);
if (stock > 0) {
long tag = message.getMessageProperties().getDeliveryTag();
channel.basicAck(tag, false);
logger.info("LotteryActivityConsumer|resultLotteryActivityConsumer|更新成功|activityId: {}", activityId);
return;
}
logger.info("LotteryActivityConsumer|resultLotteryActivityConsumer|更新失败|activityId: {}", activityId);
// ... 补偿措施
} catch (Exception e) {
logger.error("LotteryActivityConsumer|resultLotteryActivityConsumer|更新失败|activityId: {}", activityId, e);
// ... 补偿措施
}
}
}
高级特性
消费模式
推模式
MQ 主动将消息推送给消费者,这种方法需要消费者设置一个缓冲区去接收消息,对于消费者而言,内存中总是有一堆需要处理的消息,效率较高
typescript
@Component
public class ConsumerDemo {
@RabbitListener(queues = RabbitConfig.JAVABOY_QUEUE_NAME)
public void handle(String msg) {
System.out.println("msg = " + msg);
}
}
使用 @RabbitListener 注解主动接收来自生产者的消息
拉模式
消费者主动从队列中拉取消息,这种效率不是很高
java
public void test01() throws UnsupportedEncodingException {
Object o = rabbitTemplate.receiveAndConvert(RabbitConfig.JAVABOY_QUEUE_NAME);
}
receiveAndConvert 消费者主动从队列中拉取消息,如果结果为空则表示没有消息
RabbitMQ 发送的可靠性
消息发送成功要同时满足一下两个条件
- 消息发送到指定的 Exchange
- 消息发送到指定的 Queue
如果能达到这两步,就可以认为消息成功发送了,因为 Queue 有持久化机制
开启事务机制
基于 SpringBoot 的配置
设置事务管理器
typescript
@Bean
RabbitTransactionManager transactionManager(ConnectionFactory connectionFactory) {
return new RabbitTransactionManager(connectionFactory);
}
在消息生产者上面做两件事:添加事务注解并设置通信信道为事务模式:
java
@Service
public class MsgService {
@Autowired
RabbitTemplate rabbitTemplate;
@Transactional
public void send() {
rabbitTemplate.setChannelTransacted(true);
rabbitTemplate.convertAndSend(RabbitConfig.JAVABOY_EXCHANGE_NAME,RabbitConfig.JAVABOY_QUEUE_NAME,"hello rabbitmq!".getBytes());
int i = 1 / 0;
}
}
这里注意两点:
- 发送消息的方法上添加
@Transactional
注解标记事务。 - 调用
setChannelTransacted
方法设置为true
开启事务模式。
事务交互过程
- 客户端发送请求,把信道设置为事务模式;
- 服务端给出回复,同意将信道设置为事务模式;
- 客户端发送消息;
- 客户端提交事务;
- 服务端给出回应并且确认事务提交
发送方确认机制
基于 SpringBoot 的配置
ini
spring.rabbitmq.publisher-confirm-type=correlated # Exchange 的回退消息
spring.rabbitmq.publisher-returns=true # 队列的回退消息
第一个是消息到达交换机的确认回调(Publisher Comfirm),无论成功或者失败都可以回调,其中有三个取值
- none:表示禁用发布确认模式,默认值;
- correlated:表示成功发布消息到交换机后执行回调方法;
- simple:类似 correlated,并且支持
waitForConfirms()
和waitForConfirmsOrDie()
方法的调用
第二个是队列的确认回调(Publisher Return),若根据 routing key 没有找到指定的队列,则会执行该回调函数
代码实现
- 实现接口:
RabbitTemplate.ConfirmCallback
和RabbitTemplate.ReturnCallback
前者是交换机的确认回调接口,后者是队列的确认回调接口 - 使用 @PostConstruct 把两个接口注入到 rabbitTemplate 中;
typescript
@Component
public class LotteryActivityGoodStockProducer implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
private Logger logger = LoggerFactory.getLogger(LotteryActivityGoodStockProducer.class);
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendUpdateActivityGoodStock(Long goodId) {
CorrelationData correlationData = new CorrelationData();
correlationData.setId(goodId.toString());
// 发送扣库存消息
Message message = new Message(ByteUtil.longToBytes(goodId));
// 消息持久化
MessageProperties messageProperties = message.getMessageProperties();
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
rabbitTemplate.convertAndSend(Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_STOCK_EXCHANGE,
Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_STOCK_RK, message, correlationData);
logger.info("LotteryActivityGoodStockProducer|sendUpdateActivityGoodStock|发送消息,更新秒杀活动表的库存|goodId: {}", goodId);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
logger.info("LotteryActivityGoodStockProducer|sendUpdateActivityGoodStock|消息到达交换机,更新秒杀活动表的库存|goodId: {}", correlationData.getId());
return;
}
logger.error("LotteryActivityGoodStockProducer|sendUpdateActivityGoodStock|消息未到达交换机,更新库存失败|goodId: {}", correlationData.getId());
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
logger.error("LotteryActivityGoodStockProducer|sendUpdateActivityGoodStock|未到达指定队列,更新库存失败|errMsg: {}, exchange: {}, routingKey: {}"
, replyText, exchange, routingKey);
}
}
失败重试
自带失败重试
当发送方连接不上 MQ 时,Spring 会使用 retry 机制进行重试
ini
spring.rabbitmq.template.retry.enabled=true
spring.rabbitmq.template.retry.initial-interval=1000ms
spring.rabbitmq.template.retry.max-attempts=10
spring.rabbitmq.template.retry.max-interval=10000ms
spring.rabbitmq.template.retry.multiplier=2
enabled
:是否开启;
initial-interval
:重试起始间隔时间
max-attempts
:最大尝试次数
max-interval
:最大重试间隔
multiplier
:间隔时间乘数(这里配置间隔时间乘数为 2,则第一次间隔时间 1 秒,第二次重试间隔时间 2 秒,第三次 4 秒,以此类推)
业务重试
在表中加入 MqState,MsgId 的字段
- MqState:0-发送中 1-发送成功 2-发送失败
- MsgId:消息的唯一标识,进行幂等处理
重试过程
- 当我们开始发送消息时,将 MqState 状态设置为 0,
- 在 confim 的回调函数中,如果返回成功,则把该状态设置为 1,若是失败则设置为 2;
- 启动一个定时器,定时捞起 MqState 值为 2 的记录,进行重新发送
利用 xxl-job 重试
在 admin 控制台创建 执行器
配置 xxl-job 的 XxlJobConfig.java 文件
kotlin
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
/**
* 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
*
* 1、引入依赖:
* <dependency>
* <groupId>org.springframework.cloud</groupId>
* <artifactId>spring-cloud-commons</artifactId>
* <version>${version}</version>
* </dependency>
*
* 2、配置文件,或者容器启动变量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、获取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/
}
配置 xxl-job 的 yml 文件
yaml
xxl:
job:
admin:
addresses: http://112.74.73.126:8080/xxl-job-admin # admin 终端
executor:
appname: draw-result-order # 执行器名称
ip: # 为空表示自动获取
port: 9999 # 执行器端口
logpath: /data/applogs/xxl-job/jobhandler # 日志
logretentiondays: 30
accessToken: default_token
编写定时任务 @XxlHandler
ini
@XxlJob("scanDrawResultHandler")
public void scanDrawResultHandler() {
// 每次捞10条
List<Long> ids = userDrawResultMapper.selectResultByErrorMqState(beginId, pageSize);
if (ids.size() == 0) {
beginId = 1L;
logger.info("DrawResultJob|scanDrawResultHandler|没有错误的记录|beginId: {}", beginId);
return;
}
List<DrawResult> drawResults = userDrawResultMapper.selectDrawResultsByIds(ids);
for (DrawResult drawResult : drawResults) {
lotteryActivitySendOrderProducer.sendLotteryActivityOrderMessage(drawResult);
}
logger.info("DrawResultJob|scanDrawResultHandler|完成扫描,并重新发送|ids: {}", ids);
if (ids.size() < pageSize) {
beginId = 1L;
} else {
beginId = ids.get(pageSize - 1) + 1;
}
}
将该 handler 注册到某个执行器中,并开启该任务
RabbitMQ 消费的可靠性
自动应答
消息发送后就是立即被认为发送成功了,在消息接收到之前,消费者出现连接 或者 connection 关闭,则消息就丢失了
在 SpringBoot 中,消息发送后立即被认为是已经传送成功了。通过 @Component 注解把该类注入到容器中,然后通过 @RabbitMQListener 注解标注某个消费方法,若发送消息过程中出现了异常,则该消息会重回队列重新消费。
手动应答
为了保证消息能够可靠的到达消息消费者,RabbitMQ 中提供了消息消费确认机制。当消费者去消费消息的时候,可以通过指定 autoAck 参数来表示消息消费的确认方式。
-
当 autoAck 为 false 的时候,此时即使消费者已经收到消息了,RabbitMQ 也不会立马将消息移除,而是等待消费者显式的回复确认信号后,才会将消息打上删除标记,然后再删除。
-
当 autoAck 为 true 的时候,此时消息消费者就会自动把发送出去的消息设置为确认,然后将消息移除(从内存或者磁盘中),即使这些消息并没有到达消费者。
当我们将 autoAck 设置为 false 的时候,对于 RabbitMQ 而言,消费分成了两个部分:
- 待消费的消息
- 已经投递给消费者,但是还没有被消费者确认的消息
肯定应答 :void basicAck(long deliveryTag, boolean multiple)
deliveryTag
:消息的标记
multiple
:是否批量确认消息
否定应答 :void basicNack(long deliveryTag, boolean multiple, boolean requeue)
deliveryTag
:消息的标记
multiple
:是否批量否定消息(true:拒绝所有消息,false:仅拒绝提供的标签的消息)
requeue
:是否重新入队或者进入丢弃/死信队列(true:重新入队,false:丢弃/进入死信队列)
拒绝应答 :void basicReject(long deliveryTag, boolean requeue)
deliveryTag
:消息的标记
requeue
:是否重新入队或者进入丢弃/死信队列(true:重新入队,false:丢弃/进入死信队列)
否定应答批量拒绝,而拒绝应答只能拒绝一个消息
springboot 开启手动应答:spring.rabbitmq.listener.simple.acknowledge-mode=manual
推模式确认
typescript
public void handle3(Message message,Channel channel) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//消息消费的代码写到这里
String s = new String(message.getBody());
System.out.println("s = " + s);
//消费完成后,手动 ack
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
//手动 nack
try {
channel.basicNack(deliveryTag, false, true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
拉模式确认
csharp
public void receive2() {
Channel channel = rabbitTemplate.getConnectionFactory().createConnection().createChannel(false);
long deliveryTag = 0L;
try {
GetResponse getResponse = channel.basicGet(RabbitConfig.JAVABOY_QUEUE_NAME, false);
deliveryTag = getResponse.getEnvelope().getDeliveryTag();
System.out.println("o = " + new String((getResponse.getBody()), "UTF-8"));
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
try {
channel.basicNack(deliveryTag, false, true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
RabbitMQ 幂等性问题
幂等:用户通过同一操作发出的一次或者多次请求的结果是一致的,不会因为多次点击而产生副作用
场景:
消费者在成功消费了一条数据后,向 MQ 发送 ACK,但此时遭遇网络故障,MQ 没有接收到 ACK。网络恢复后,MQ 认为该消息没有成功发送,故向消费者再次推送这条消息,造成消息的重复消费
幂等通用解决方法
-
先加锁,一定要是互斥锁,分布式锁或者悲观锁都可以
-
判断是否存在幂等,基于流水表,唯一索引等
-
然后进行数据更新,即持久化
RabbitMQ 消息有效期
在默认情况下,队列中的消息是不会过期的,只要没有宕机(消息没有持久化处理),消息就一直会存在队列中
TTL
TTL(Time To Live),消息的存活时间,即消息的有效期。如果消息的存活时间超过了 TTL 且没有被消费,那么这条消息就会变成 死信 ,进入 死信队列 中
TTL 有两种设置方式(以最小的为准):
- 创建队列时,可以对队列设置一个 TTL,表示该队列中所有消息的过期时间;
- 生产者发送消息时,可以对该消息设置一个 TTL ,表示这个消息的过期时间;
TTL 删除的方式
- 对于第一种情况,TTL 到了之后直接删除,因为队列头部的就是最早过期的消息,每次只要判断该队列头部的消息即可;
- 对于第二种情况,TTL 到了之后是不会被直接删除,而是等到该消息投递给消费者后才会被删除,避免遍历整个队列判断消息是否过期(因为用户设置的消息过期时间是不同的,队头的消息过期的时间可能比对尾的还长)
消息 TTL 过期
ini
public void sendLotteryActivityOrderMessage(DrawResult drawResult) {
SendOrderMessage sendOrderMessage = new SendOrderMessage();
sendOrderMessage.setGoodId(drawResult.getGoodId());
sendOrderMessage.setGoodName(drawResult.getGoodName());
sendOrderMessage.setGoodCounts(1);
sendOrderMessage.setUserId(drawResult.getUserId());
String jsonString = GsonUtil.ObjectToJsonString(sendOrderMessage);
// 设置过期时间
Message message = new Message(jsonString.getBytes(StandardCharsets.UTF_8));
message.getMessageProperties().setExpiration("10000");
CorrelationData correlationData = new CorrelationData();
correlationData.setId(sendOrderMessage.getGoodId().toString());
rabbitTemplate.convertAndSend(Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_ORDER_EXCHANGE,
Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_ORDER_RK, message,correlationData);
}
队列 TTL 过期
typescript
@Bean
Queue queue() {
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 10000);
return new Queue(JAVABOY_QUEUE_DEMO, true, false, false, args);
}
死信队列
死信交换机
死信交换机来接受死信消息的,死信消息有以下的情况:
- 消息被拒绝且 requeue 为 false;
- 消息过期
- 队列已满
死信交换机本质上跟普通的交换机没有什么不同,只是用来接收死信消息的
死信队列
绑定了死信交换机的队列,我们可以指定任何队列绑定死信交换机
基于 SpringBoot 配置
typescript
/**
* 普通消息队列
* @return
*/
@Bean
Queue javaboyQueue() {
Map<String, Object> args = new HashMap<>();
//设置消息过期时间
args.put("x-message-ttl", 1000*10);
//设置死信交换机
args.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
//设置死信 routing_key
args.put("x-dead-letter-routing-key", DLX_ROUTING_KEY);
return new Queue(JAVABOY_QUEUE_NAME, true, false, false, args);
}
/**
* 普通交换机
* @return
*/
@Bean
DirectExchange javaboyExchange() {
return new DirectExchange(JAVABOY_EXCHANGE_NAME, true, false);
}
/**
* 绑定普通队列和与之对应的交换机
* @return
*/
@Bean
Binding javaboyBinding() {
return BindingBuilder.bind(javaboyQueue())
.to(javaboyExchange())
.with(JAVABOY_ROUTING_KEY);
}
就两个参数:
- x-dead-letter-exchange:配置死信交换机。
- x-dead-letter-routing-key:配置死信
routing_key
。
将来发送到这个消息队列上的消息,如果发生了 nack、reject 或者过期等问题,就会被发送到 DLX 上,进而进入到与 DLX 绑定的消息队列上。
RabbitMQ 持久化
交换机持久化
kotlin
/**
* 注册交换机
* @return 交换机
*/
@Bean("decreaseApiUsedCountExchange")
public DirectExchange decreaseApiUsedCountExchange() {
return ExchangeBuilder
.directExchange("decreaseApiUsedCountExchange")
.durable(true)
.build();
}
队列持久化
如果 rabbit 重启会造成之前的队列被删除,如果要实现持久化就要把 durable 设置为 true,代表开启持久化
kotlin
/**
* 注册队列
* @return 队列
*/
@Bean("lotteryActivityOrderQueue")
public Queue lotteryActivityOrderQueue() {
return QueueBuilder
.durable(Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_ORDER_QUEUE)
.build();
}
消息持久化
在消息生产者发布消息的时候,开启消息持久化
ini
public void sendUpdateActivityGoodStock(Long goodId) {
CorrelationData correlationData = new CorrelationData();
correlationData.setId(goodId.toString());
// 发送扣库存消息
Message message = new Message(ByteUtil.longToBytes(goodId));
// 消息持久化
MessageProperties messageProperties = message.getMessageProperties();
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
rabbitTemplate.convertAndSend(Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_STOCK_EXCHANGE,
Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_STOCK_RK, message, correlationData);
logger.info("LotteryActivityGoodStockProducer|sendUpdateActivityGoodStock|发送消息,更新秒杀活动表的库存|goodId: {}", goodId);
}
但也不是完全保持持久化,如果在持久化过程中宕机了,也是不会将消息完整持久化到磁盘中的
如何保证 RabbitMQ 消息的可靠性
RabbitMQ 的消息链路是生产者 -> 交换机 -> 队列 -> 消费者
在生产者端,发送消息丢失的情况有两种:生产者 -> 交换机,交换机 -> 队列
在这两个过程中,我们可以分别开启消息 confirm 机制和消息 return 机制,分别判断消息是否到达交换机和队列
其中 confirm 机制能够返回发送成功还是没有发送成功的回退消息,而 return 机制是在 exchange 没有找到对应的 queue 时才会执行
在传输过程 中,可能面临 RabbitMQ 宕机的问题,一旦宕机 RabbitMQ 中的交换机、队列、消息都会消失,这是就需要我们开启持久化机制
RabbitMQ 对交换机、队列、消息都提供了持久化机制的实现。在创建交换机或者队列时,我们可以通过 durable 方法实现持久化,而消息的持久化则需要在消息发送时选择 PERSISTENT
Mode。但是引入持久化机制可能会增加磁盘的 I/O
但是持久化机制不能 100% 保证消息不丢失,如果在写入磁盘的过程中宕机,那么消息时也会丢失的
在消费者端,我们可以通过消息的确认机制来保证消费者处理掉消息,即手动 ACK,当没有消费者没有发送 ACK , RabbitMQ 就会选择重新发送或者进入死信队列(由消费者端选择具体的处理方式)
注意保证消息的幂等