一、确认机制
1 生产投递丢失问题
- 如果生产者 P 投递消息到交换机 X 的过程中,出现了网络延迟,导致消息丢失,怎么保证消息安全?
1.1通过发布确认机制解决
-
生产者 P 投递消息到交换机 X 的过程中,交换机 X 会给生产者 P 一个 ACK 确认回调,生产者可以根据收到 ACK 值知道是否投递成功
@Configuration
@EnableRabbit
public class RabbitMqConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm-exchange";
public static final String BACK_EXCHANGE_NAME = "back-confirm-exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm-queue";
public static final String BACK_QUEUE_NAME = "back-confirm-queue";
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private Integer port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Bean(name="myRabbitTemplate")
//@Scope
public RabbitTemplate rabbitTemplate(){
RabbitTemplate template = new RabbitTemplate(cachingConnectionFactory());
return template;
}
@Bean
public CachingConnectionFactory cachingConnectionFactory(){
//新建缓存连接工厂
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setHost(host);
factory.setPassword(password);
factory.setUsername(username);
factory.setPort(port);
return factory;
}
//新建确认交换机
@Bean
public DirectExchange confirmExchange(){
return ExchangeBuilder.
directExchange(CONFIRM_EXCHANGE_NAME).
durable(true).
build();
}
//确认队列
@Bean
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
//绑定交换机和队列
@Bean
public Binding confirmBinding(){
return BindingBuilder.bind(confirmQueue()).to(confirmExchange()).with("ack");
}
}
@SpringBootTest
class SpringbootRabbitmqSend02ApplicationTests {
@Autowired
RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
rabbitTemplate.convertAndSend("confirm-exchange","ack","你好");
}
}
2 交换机无法路由问题
- 当生产者 P 投递消息到交换机 X 的过程中,消息确定收到了,但是路由配置错误,或者没有绑定队列,此时又如何保证消息安全性?
2.1回退机制让生产者自行处理
- 仅仅开启确认机制无法保证消息安全性,可以通过回退机制,通知消费者此条消息无法处理,让消费者自行处理消息
- 建立连接工厂、消息队列和交换机并绑定
@Configuration
@EnableRabbit
public class RabbitMqConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm-exchange";
public static final String BACK_EXCHANGE_NAME = "back-confirm-exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm-queue";
public static final String BACK_QUEUE_NAME = "back-confirm-queue";
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private Integer port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Bean(name="myRabbitTemplate")
//@Scope
public RabbitTemplate rabbitTemplate(AckCallBack ackCallBack, ReturnCallBack returnCallBack){
RabbitTemplate template = new RabbitTemplate(cachingConnectionFactory());
template.setConfirmCallback(ackCallBack);
template.setMandatory(true);//true交换机回退 false 直接丢弃
template.setReturnsCallback(returnCallBack);
return template;
}
@Bean
public CachingConnectionFactory cachingConnectionFactory(){
//新建缓存连接工厂
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setHost(host);
factory.setPassword(password);
factory.setUsername(username);
factory.setPort(port);
factory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);
return factory;
}
//新建确认交换机
@Bean
public DirectExchange confirmExchange(){
return ExchangeBuilder.
directExchange(CONFIRM_EXCHANGE_NAME).
durable(true).
build();
}
//确认队列
@Bean
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
//绑定交换机和队列
@Bean
public Binding confirmBinding(){
return BindingBuilder.bind(confirmQueue()).to(confirmExchange()).with("ack");
}
}
- 新建AckCallBack
@Component
@Slf4j
public class AckCallBack implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id = correlationData != null ?correlationData.getId():"";
if(ack){
log.info("交换机已经收到id:{}消息了",id);
}else{
log.info("交换机没有收到id:{}消息,原因是:{}",id,cause);
}
}
}
- 新建ReturnCallBack
@Component
@Slf4j
public class ReturnCallBack implements RabbitTemplate.ReturnsCallback {
@Override
public void returnedMessage(ReturnedMessage returned) {
Message message = returned.getMessage();
log.info("消息:{}被服务器退回,退回原因:{},退回码:{}",
message.getBody(),
returned.getReplyText(),
returned.getReplyCode());
/**
* 处理方式
* 1:尝试重新调用
* 2:落库处理,存入 mysql 数据库中
*/
}
}
2.2 备份交换机解决
- 可以通过给交换机设置备份机的方式,来处理交换机无法路由的消息,备份交换机设置 Fanout 类型,可以添加备份队列,还可以添加告警队列,还可以添加入库队列。
@Configuration
@EnableRabbit
public class RabbitMqConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm-exchange";
public static final String BACK_EXCHANGE_NAME = "back-confirm-exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm-queue";
public static final String BACK_QUEUE_NAME = "back-confirm-queue";
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private Integer port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Bean(name="myRabbitTemplate")
public RabbitTemplate rabbitTemplate(AckCallBack ackCallBack, ReturnCallBack returnCallBack){
RabbitTemplate template = new RabbitTemplate(cachingConnectionFactory());
template.setConfirmCallback(ackCallBack);
template.setMandatory(true);//true交换机回退 false 直接丢弃
template.setReturnsCallback(returnCallBack);
return template;
}
@Bean
public CachingConnectionFactory cachingConnectionFactory(){
//新建缓存连接工厂
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setHost(host);
factory.setPassword(password);
factory.setUsername(username);
factory.setPort(port);
factory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);
return factory;
}
//建立确认交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return ExchangeBuilder.
directExchange(CONFIRM_EXCHANGE_NAME).
durable(true).
withArgument("alternate-exchange",BACK_EXCHANGE_NAME).
build();
}
//确认队列
@Bean
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
//绑定交换机和队列
@Bean
public Binding confirmBinding(){
return BindingBuilder.bind(confirmQueue()).to(confirmExchange()).with("ack");
}
//声明备份交换机
@Bean("backExchange")
public FanoutExchange backExchange(){
return new FanoutExchange(BACK_EXCHANGE_NAME);
}
//声明备份队列
@Bean("backQueue")
public Queue backQueue(){
return QueueBuilder.durable(BACK_QUEUE_NAME).build();
}
//绑定备份交换机和队列
@Bean
public Binding backBinding(){
return BindingBuilder.bind(backQueue()).to(backExchange());
}
}
3.消费者异常导致数据丢失
3.1消费者确认机制
- 通过消费者确认机制避免消息丢失:
- 有三种确认方式:①自动确认:acknowledge=none ②手动确认:acknowledge=manual ③根据异常情况确认:acknowledge=auto
- 手动确认:
- 正常执行:调用channel.basicAck(deliveryTag,false);方法确认签收消息
- 异常执行:在catch中调用 basicNack或basicReject,拒绝消息,让MQ重新发送消息
spring:
rabbitmq:
host: 192.168.245.129
port: 5672
username: wj
password: 123456
listener:
simple:
retry:
enabled: true
acknowledge-mode: manual #手动ack
@Service
@Slf4j
public class MessageReceive {
@RabbitListener(queues = {"back-confirm-queue"})
public void receive(String body, Channel channel, Message message) throws IOException {
try {
log.info("队列消息"+body);
int ret = 1/0;
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
if (message.getMessageProperties().getRedelivered()){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
}else {
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
}
}
}
}
二、死信队列
1.死信队列概述
- 死信:在官网中对应的单词为“Dead Letter”,可以看出翻译确实非常的简单粗暴。死信是RabbitMQ的一种消息机制,如果出现以下情况消息将变成死信:
- 消息在队列的存活时间超过设置的生存时间(TTL)时间
- 消息队列的消息数量已经超过最大队列长度
- 消息被否定确认,使用basicNack或basicReject并且requeue=false
- 死信队列:Dead Letter Queue用来存放死信消息的队列
- 死信交换机:Dead Letter Exchange用来路由死信消息的交换机
2.死信队列架构图
消息满足刚提到的几个条件之后,消息进入死信队列
3.TTL
- TTL概述:用来控制消息或者是队列的最大存活时间,单位是毫秒。
- 设置TTL两种方式:
- 给消息设置TTL
- 给队列设置TTL
- 注意:如果同时配置了队列的 TTL 和消息的 TTL,那么较小的那个值将会被使用。
- 区别:
- 消息设置TTL的方式是在消费时做消息过期判断,如果是顶端的,到期直接移除
- 队列设置过期是直接丢弃或丢到死信队列
4. 死信队列实操
4.1 达到最大长度进入死信队列
- 限制队列最大长度为10,发送11条消息给正常队列
//确认队列
@Bean("confirmQueue")
public Queue confirmQueue() {
// return QueueBuilder.durable(TEST_QUEUE_NAME).build();
Map<String, Object> map = new HashMap<>();
//设置队列过期时间
// map.put("x-message-ttl",5000);
map.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
//设置队列最大长度
// map.put("x-max-length",10);
return QueueBuilder.durable(TEST_QUEUE_NAME).withArguments(map).build();
}
4.2到达过期时间进入死信队列
- 设置10秒过期时间
//确认队列
@Bean("confirmQueue")
public Queue confirmQueue() {
// return QueueBuilder.durable(TEST_QUEUE_NAME).build();
Map<String, Object> map = new HashMap<>();
//设置队列过期时间
// map.put("x-message-ttl",5000);
map.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
//设置队列最大长度
// map.put("x-max-length",10);
return QueueBuilder.durable(TEST_QUEUE_NAME).withArguments(map).build();
}
4.3拒收进入死信队列
- 模拟异常拒收
//拒收演示
@RabbitListener(queues = {"test-queue"})
public void receiveReject(String body, Channel channel, Message message) throws IOException {
try {
if ("3".equals(body)){
log.info("收到异常队列消息"+body);
int ret = 1/0;
}
log.info("队列消息"+body);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
if (message.getMessageProperties().getRedelivered()){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
}else {
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
}
}
}
5.总结
- 死信交换机和死信队列和普通的没有区别
- 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
- 消息成为死信的三种情况:
- 消息在队列的存活时间超过设置的生存时间(TTL)时间
- 消息队列的消息数量已经超过最大队列长度
- 消息被否定确认,使用basicNack或basicReject并且requeue=false
三、延时队列
1.延时队列概述
-
延时队列:消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费,最重要的特征就是延迟上
-
应用场景 :
-
12306购票30分钟内不支付自动取消订单,回滚库存
-
会议设置,15分钟前通知参会人员
-
- 解决方案:
- 通过轮询数据库查询处理
- 通过延迟队列实现
2.延时队列实现
-
RabbitMQ未提供延时队列功能,但是我们可以通过 TTL + 死信队列的方式设计出延时队列
-
RabbitMQ未提供延时队列功能,但是我们可以通过 TTL + 死信队列的方式设计出延时队列
-
出现问题:发现消费不能按最近过期时间消费问题
3.安装插件实现延时队列
- 下载地址: https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
- 上传位置: /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.34/plugins
- 启用插件: rabbitmq-plugins enablerabbitmq_delayed_message_exchange
4.小结
- 延迟队列就是指消息进入队列后需要一定时间才消费
- RabbitMQ本身是没有提供延迟队列功能,可以通过死信队列+过期时间来实现延迟队列
- 自己实现的延迟队列不能按时消费,可能存在滞后问题,安装延时队列 插件使用
四、RabbitMQ实战场景
1.日志与可视化监控查看
- 日志文件: /var/log/rabbitmq/rabbit@localhost.log
- 通过如下操作观察日志:
- 停止服务:/sbin/service rabbitmq-serverstop
- 重新启动:rabbitmqctl start_app
- 开启服务:/bin/systemctl startrabbitmq-server.service
2.消息追踪
- Firehose:生产者给交换机发送消息时,会按照指定的格式发送到amq.rabbitmq.trace(Topic)交换机上。
- 开启Firehose命令:rabbitmqctl trace_on
- 关闭Firehose命令:rabbitmqctl trace_off
- 注意:开启Firehose 会影响性能,不建议一直打开,一般测试时打开
- rabbitmq_tracing:启用插件来实现可视化查看
- 查看插件:rabbitmq-plugins list
- 启用插件:rabbitmq-plugins enable rabbitmq_tracing
- 关闭插件:rabbitmq-plugins disable rabbitmq_tracing
3.幂等性保障
- 幂等性:是分布式中比较重要的一个概念,是指在多作业操作时候避免造成重复影响,其实就是保证同一个消息不被 消费者重复消费两次。但是实际开发中可能存在网络波动等问题,生产者无法接受消费者发送ack信息,因此这条消 息将会被重复发送给其他消费者进行消费,实际上这条消息已经被消费过了,这就是重复消费的问题。
- 如何去避免重复消费问题:
- 数据库乐观锁机制
- update items set count=count-1 where count= #{count} and id = #{id}
- update items set count=count-1,version=version+1 where version=#{version} and id = #{id}
- 生成全局唯一id+redis锁机制:操作之前先判断是否抢占了分布式锁 setNx 命名
五、RabbitMQ集群
1.目前存在的问题
- 单节点的RabbitMQ如果内存崩溃、机器掉电或者主板故障,会影响整个业务线正常使用
- 单机吞吐性能会受内存、带宽大小限制
2.解决方案
2.1使用集群模式来解决
- 搭建RabbitMQ集群
- RabbitMQ集群存在问题
- RabbitMQ集群解决