概念
rabbitmq是消息中间件,是基于erlang语言创建的,改语言本身就支持并发
采用AMQP高级消息队列协议,最大的特点就是消费不需要确保提供方存在,实现了服务间的高度解耦
如何确保数据不丢失
数据丢失可能发生在三个地方
- 发送时数据丢失,可能是由于网络等原因
解决方法:
1. 同步。采用事务管理,开启事务,在消息发送的时候,如果mq没有接收到消息则抛出异常,本地捕获异常并重试
channel.txSelect 开启事务
channel.txCommit 提交事务
channel.txRollback 事务回滚
2. 确认机制。(建议)
- channel.waitForConfirms()普通发送方确认模式;
- channel.waitForConfirmsOrDie()批量确认模式;
- channel.addConfirmListener()异步监听发送方确认模式;
// 发送数据时,同步
// 开启事务
channel.txSelect
try {
// 这里发送消息
} catch (Exception e) {
channel.txRollback
// 这里再次重发这条消息
}
// 提交事务
channel.txCommit
//发送数据时,异步
// 开启Confirm模式
AMQP.Confirm.SelectOk ok = channel.confirmSelect();
// 普通发送方确认模式;
if (channel.waitForConfirms()) {
System.out.println("消息发送成功" );
}
//直到所有信息都发布,只要有一个未确认就会IOException
channel.waitForConfirmsOrDie();
// 异步:实现此接口以获得Confirm事件的通知。Acks表示成功处理的消息;Nacks表示代理丢失的消息。注意,丢失的消息仍然可以传递给使用者,但是代理不能保证这一点。
channel.addConfirmListener(new ConfirmListener() {
// 成功处理
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("-------服务端 ACK Success--------");
}
// 丢失消息
// 最好解决方法就是把未确认的消息放到一个基于内存的能发布线程访问的对列,比如说ConcurrentLinkedQueue,这个对列在confire callbacks与发布线程之间进行消息传递
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.err.println("-------服务端 ACK Failed--------");
}
});
- mq本身宕机,消费者还没接收消息,服务端就挂了
解决方法:持久化操作
1. 创建队列时,设置为持久化
交换机持久化:是通过在声明Exchange是将durable参数置为 true 实现的,如果交换器不设置持久化,那么在 RabbitMQ服务重启之后,相关的交换器元数据会丢失。持久化到磁盘。
对列持久化:如果队列不设置持久化,那么在 RabbitMQ 服务重启之后,相关队列的元数据会丢失
2. 消息的deliveryMode 设置为2
// 交换机持久化
/**
* 声明交换机
* @params1 交换机名字
* @params2 交换机类型,写死
* @params3 是否持久化
*/
channel.exchangeDeclare("logs_direct","direct",true);
// 对列持久化
/**
* 通道绑定消息队列
* @params1 队列名称 对列不存在就自动创建
* @params2 队列是否要持久化,durable=false ,是否存盘,不持久化也会存盘,随着服务器重启然后丢失
* @params3 排他性,是否独占独立的队列
* @params4 是否自动化删除,随着最后一个消费者消费完毕后,是否删除队列
* @params5 携带附属参数
*/
channel.queueDeclare(queueName,true,false,false,null);
// 消息持久化
// props:传递消息的额外设置,MessageProperties.PERSISTENT_TEXT_PLAIN,BasicProperties MINIMAL_PERSISTENT_BASIC,BasicProperties PERSISTENT_BASIC消息持久化
channel.basicPublish("",queueName, MessageProperties.PERSISTENT_TEXT_PLAIN,info.getBytes());
- 消费的时候,数据丢失
解决方法:消费者接收到消息,先执行性业务代码,结束之后,返回ack
/**
* 通道绑定消息队列
* @params1 队列名称
* @params2 开启消费确认机制,false不会自动确认
* @params3 消费时的回调接口
* @params4 是否自动化删除,随着最后一个消费者消费完毕后,是否删除队列
*/
channel.basicConsume(queueName, false, new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
System.out.println("消费者1:收到消息" + new String(message.getBody(), "UTF-8"));
try {
Thread.sleep(10000);
}catch (Exception e){
e.printStackTrace();
}
/**
* 消息手动确认
* @params1 确认的是哪一个消息的标志
* @params2 是否开启多个消息同时确认
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
}
}, new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
System.out.println("失败");
}
});
如何避免消息重复投递或重复消费
先说为什么会重复消费:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
解决思路是:保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;保证消息等幂性;
1. 在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列;
2. 在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费。
mq的缺点
- 系统可用性降低
系统引入外部依赖越多,越容易挂掉,本来你就是A系统调用BCD三个系统的接口就好了,人ABCD四个系统好好的,没啥问题,你引入一个mq,万一mq挂了,整套系统就奔溃了 - 系统复杂性提高
硬生生加一个mq进来,怎么保证消息没有重复消费?怎么处理消息丢失等情况? - 一致性问题
预取值
// 不写时,表示公平分发
// 为1时表示不公平分发(能者多劳)
// 为2-65535,表示预取值
channel.basicQos(1);
死信队列
对列中的消息无法被消费,这样的消息就是死信。当消息无法被消费时,将消息投入死信队列。
来源
- 消息TTL过期
- 队列达到最大长度(队列满了,无法再添加到mq中)
- 消息被拒绝,并且 requeue=false
// 由于过期时间成为死信,设置ttl
// 设置普通对列的参数
HashMap<String, Object> arguments = new HashMap<>();
// 设置过期时间
arguments.put("x-message-ttl",10000);
// 设置死信交换机
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
// 设置死信RoutingKey
arguments.put("x-dead-letter-routing-key","dead");
channel.queueDeclare(NARMAL_QUEUE,true,false,false,arguments);
// 设置最大长度
arguments.put("x-max-length",10);
// 拒绝消息
扩展arguments的参数的意义:
- Message TTL(x-message-ttl):设置对列中消息的生存周期(统一为整个队列的所有消息设置生命周期),也可以在发布消息时,单独为某一条消息指定剩余生存时间,单位ms,时间到了,消息就会从队列中删除,单独为某条消息设置过期时间Features=TTL
// 单独为某条消息设置过期时间
AMQP.BasicProperties.Builder expiration = new AMQP.BasicProperties.Builder().expiration("60000");
channel.basicPublish("","",expiration.build(),"".getBytes());
- Auto Expire(x-expires):当队列在指定的时间没有被访问就会被删除,Features=Exp
- Max Length(x-max-length):限定队列的消息的最大长度,超过指定长度将会把最早的几条删除掉,类似于maongdb中的固定集合。RabbitMQ在淘汰消息时,采用了LRU算法,这一点和Redis很相似,Redis的淘汰算法同样也采用了LRUFeatures=Lim
- Max Length Bytes(x-max-length-bytes):限定队列最大的占用空间大小,一般受限于内存磁盘的大小,Features=Lim B
- Dead Letter exchange(x-dead-letter-exchange):设置死信交换机 Features=DLX
- Dead Letter Routing Key(x-dead-letter-routing-key):设置死信交换机的routingkey,Features=DLK
- maximum priority(x-max-priority):优先级队列,声明队列时,先定义最大优先级值(定义最大值一般不要太大),在发布消息时指定该消息的优先级,优先级更高的消息先被消费范围是0-255
- Lazy mode(x-queue-mode=lazy):先将消息保存到磁盘上,不放在内存中,当消费者开始消费的时候在加载到内存中
- alternate-exchange:备份交换机(+名)
延时对列
死信中的ttl过期构成延时对列
对列内部是有序的
死信对列做延时对列的缺点:
- rabbitmq只会监测第一个消息是否过期,如果第二条消息先过期,并不会先执行第二条,只有等第一条执行后才会执行第二条。
解决办法:通过rabbitmq插件实现延时对列,rabbit_delayed_message_exchange,将交换机类型变为x-delayed-message(一个交换机一个对列)
// 自定义交换机
@Bean
public CustomExchange customExchange(){
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("x-delated-type","direct");
/**
* 姓名
* 类型
* 是否持久化
* 是否自动删除
* 参数
*/
return new CustomExchange("delayed","x-delayed-message",true,false,hashMap);
}
//绑定
BindingBuilder.bind(deadLetterQueue).to(deadLetterExchange).with(DEAD_LETTER_ROUTING_KEY).noargs;
其他延时对列的选择:
- Java的DelayQueue
- 利用redis的zset
- 利用kafka的时间轮
- 利用Quartz
发布确认 集成springboot版
使用return-callback时必须设置mandatory为true,或者在配置中设置mandatory-expression的值为true,可针对每次请求的消息去确定’mandatory’的boolean值,只能在提供’return -callback’时使用,与mandatory互斥
// 1、保证不可路由,会有回退信息
rabbitTemplate.setMandatory(true);
// 2、默认禁用,correlated表示发布成功后出发回调方法,simple和correlated一样,但是如果是调用waitForConfirmsOrDie且返回值是false,会关闭信道,无法发送之后的消息,保证交换机宕机,会有回调
spring.rabbitmq.publisher-confirm-type=correlated
/**
* id和推送的信息,convertAndSend中有一个参数就是correlationData
* 发送是否成功
* 失败的原因,成功为null
*/
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack){
log.info("成功:{}",correlationData.getReturned().getMessage().toString());
}else{
log.info("失败:{}",cause);
}
}
});
// 推送到对列失败
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback(){
@Override
public void returnedMessage(ReturnedMessage returned) {
log.info("消息回退:{}",returned.getMessage().toString());
}
});
// 或者实现RabbitTemplate.ConfirmCallback接口(执行步骤)
//1、类上注解
@Componet
// 2、引入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
// 3、初始化
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
}
备份交换机
如果交换机无法接收消息,会将消息推送给备份交换机
作用:
- 备份消息
- 报警
// 创建交换机时,绑定备份交换机
@Bean("exchange")
public DirectExchange getExchange(){
return ExchangeBuilder.directExchange(confire_exchange_name).durable(true)
.alternate("alternate_exchange_name").build();
}
// 或者
mandatory数据回退,回调和备份交换机同时存在时,以备份交换机为准,备份交换机的优先级高
幂等性
对同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多点几次而产生副作用,及消息被重复消费了。
解决方法:
- 使用全局id或者写一个唯一标识,消费者每次处理消息时,先判断是否已经消费
在业务的高峰期,生产端可能会产生重复的发送消息,目前主流的幂等性有两种操作
- 唯一ID+指纹码机制
指纹码:一些规则或者时间戳加别的服务给到的唯一信息码,并不是系统生成的,基本都是由业务规则拼接而来,但是要保证唯一性,然后利用查询语句进行判断这个id是否存在数据库中。优势是简单的拼接,然后查询判断是否重复;劣势是高并发时,如果是单个数据库就会有写入性能瓶颈,可以用分库分表提升性能 - 利用redis的原子性
利用redis执行setnx命令,天然具有幂等性,从而实现不重复消费
优先级队列
优先处理部分数据。优先级区间范围是0-255越大优先级越高,一般设置0-10,不然太浪费CPU了
// 1、创建队列时,声明最大优先级
return QueueBuilder.durable(confire_exchange_queue).maxPriority(10).build();
// 2、发送消息
rabbitTemplate.convertAndSend(RabbitConfig.NARMAL_EXCHANGE,RabbitConfig.NARMAL1_ROUTING_KEY,message,msg -> {
//发送消息的延迟时长
msg.getMessageProperties().setExpiration(ttlTime);
// 发送消息时声明当前消息的优先级
msg.getMessageProperties().setPriority(10);
return msg;
});
惰性队列
指的是消息保存在内存中还是在磁盘上,正常情况下消息是存在内存中,因为这样获取消息速度快,惰性队列是将消息保存在磁盘中,当消费者消费消息时,现将消息从磁盘保存到内存中,然后再去消费。
惰性队列一般使用在消费者宕机、关闭等情况导致长时间不能消费消息导致消息堆积时
队列有两中模式,default和lazy模式,lazy模式就是惰性队列的模式。有两种设计方法,一个是声明队列时在参数中设置,一个是通过Policy的方式设置,如果以上两种同时使用的话,第二种优先级更高
// 声明惰性队列
return QueueBuilder.durable(confire_exchange_queue).lazy().build();
集群
- 配置各个节点的hosts文件,让各个节点互相识别
- 为了确保各个节点的cookie文件使用同一个值,在主rabbitmq服务器上执行以下命令
scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
- 启动所有的服务,执行(表示服务器和erlang都重启)
rabbitmq-server -detached
- 在从节点执行
rabbitmqctl stop_app// 只关闭rabbitmq的服务
rabbitmqctl reset // 重置服务
rabbitmqctl join_cluster rabbit@node1 // 加入到node1节点的集群
rabbitmqctl start_app // 启动rabbitmq服务
- 集群状态
rabbitmqctl cluster_status
// Basics 当前节点
// Disk Node 所有节点
// Running Node 运行中的节点
// Version 版本
等等
- 重新创建账户
- 脱离集群
// 要解除的节点执行
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
// 主节点执行
rabbitmqctl cluster_status
rabbitmqctl forget_cluster_node rabbit@node2
缺点:
- 哪台机器创建的交换机和对列,只会在本机上,一旦宕机,交换机和对列都将删除,导致信息丢失
所以引入镜像对列
镜像对列
在任意节点添加策略,设置规则Pattem:“abc” 以abc为前缀的交换机或者对列
ha-mode=exactly 指定模式
ha-params=2 备份2分
hs-sync-mode=automatic 自动同步