一、如何传递对象类型参数?
在RabbitMQ中,如果Pushlisher发送的消息是基本数据类型或String类型,可以在Consumer中直接使用对应类型或可以转换的类型进行接收。
如果发送的消息是对象或集合这种复杂类型,旧版本RabbitMQ会把这些数据进行序列化后放入到Message的body中。Consumer接收时方法参数应该为Message,并对Message中body进行反序列获取到对象或集合数据。新版本则直接可以用实体类作为参数。
- 创建实体类,并序列化。如果消息是对象类型,此对象的类型必须进行序列化,且需要给定序列化值。
/**
1. 用户表
2. 3. @date 2022/4/18
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
//@TableName("rbac_user")
public class User implements Serializable {
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String username;
private String password;
private Integer age;
private String phone;
private String status;
private String salt;
private String email;
private Date createTime;
@TableField(exist = false)
private Date start;
@TableField(exist = false)
private Date end;
private int isDeleted;
public User(String username, Integer age) {
this.username = username;
this.age = age;
}
public User() {
}
}
- 生产方发送消息
/**
* 对象类型的参数传递
*
* @Return:
* @Author: wxy
*/
@Override
public String objectMsg() {
amqpTemplate.convertAndSend("test.topic", "com.hetuan.send.object.message", new User("王五", 60));
return "发送完毕";
}
- 消费方接收消息
/**
* 对象类型消息的接收 <p>实体类的包名和类名必须保持一致</p>
*
* @Param: msg
* @Return:
* @Author: wxy
*/
@RabbitListener(queues = {"topic3"})
public void fanout3(User msg) {
// 扇形交换器可以不写路由键名称
System.out.println("topic3消费了消息:" + msg);
}
注意: 正常情况下,应该有一个公共项目,里面写上实体类,这个项目分别被消息发送服务和消息接收服务去依赖。如果是直接在Publisher服务和Consumer服务中新建的实体类,也必须保证两个服务中类的全限定路径完全一致,否则无法进行获取对象消息。
二、RabbitMQ发送消息并接收反馈结果
使用RabbitMQ很多情况都需要使用RabbitMQ的队列功能对数据进行排序。如果使用异步类型消息,Publisher发送完成消息后是没有任何反馈结果的,如果需要反馈结果就需要使用AmqpTemplate中convertSendAndReceive,并Consumer项目监听方法必须有返回值。
场景:秒杀、抢红包等功能时都适用。
当使用convertSendAndReceive消息由异步变成同步,阻塞主线程。发送给队列消息后需要Consumer返回ACK值(监听方法返回值),所以在使用这个功能时,都是先启动Consumer后发送消息。
- 生产方发送消息并接收消费方返回值
@Override
public Object sendAndReceive() {
Object result = amqpTemplate.convertSendAndReceive("test.direct", "queue-sync", new User("王五", 20));
if (result != null) {
return result;
}
return result != null ? result : "未接收到消费方返回值";
}
- 消费方接收消息
@RabbitListener(queues = "queue-sync")
public String sync(User msg) {
return msg.getUsername();
}
因为同步消费消息时,生产者会等待消费者的返回值,如果没有收到consumer的返回值,线程会一直阻塞,为了不在继续阻塞主线程,让队列继续向下执行,RabbitMQ设了默认等待时间,超过等待时间则返回值为null,也可自己设置等待时间。
spring:
rabbitmq:
template:
# 同步操作时、设置等待消费者返回值的时间 ,单位毫秒
reply-timeout: 10000
三、ACK消息确认机制
ACK 机制是消费者从 RabbitMa 收到消息并处理完成后,反馈给 RabbitMQ,RabbitMQ收到反馈后才将此消息从队列中删除。
如果一个消费者在处理消息出现了网络不稳定、服务器异常等现象(如int a = 10/0),那么就不会有 ACK反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放入队列中,容易出现重复消费情况。
如果在集群的情况下,RabbitMQ 会立即将这个消息推送给这个在线的其他消费者。这种机制保证了在消费者服务端故障的时候,不丢失任何消息和任务。
消息永远不会从 RabbitMQ 中删除,只有当消费者正确发送 ACK 反馈,RabbitMQ 确认收到后,消息才会从 RabbitMQ 服务器的数据中删除消息的ACK 确认机制默认是打开的。
如果希望关闭ACK机制,可以开启重试,通过设置重试次数,到达指定次数后删除消息。
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 默认false,开启重试。
max-attempts: 2 # 默认3次
注意: 不建议关闭ack保护机制。这么做虽然不会让当前消息一直阻塞下去,但是会造成消息丢失,因为消息会被从队列中删除了。
四、消息幂等性(重复发送和重复消费问题)
在MQ中可能出现消息幂等性的情况:
- (重复发送)Publisher给MQ发送消息的时候,MQ在给Publisher返回ACK时由于网络中断等问题,没有成功返回。Publisher会认为消息没有发送成功,在网络恢复后会重新发送消息。
- (重复消费)Consumer接收到消息后,在给MQ返回ACK时由于网络问题,MQ没有成功接收ACK,MQ会认为此消息没有正确消费。在网络重连后会把消息重新发送给此消费者,或重新广播给其他所有消费者。
解决办法
- 解决重复发送问题。MQ内部会给每个消息生成一个唯一ID。当消息接收到后会判断此ID。
public Object repeatSend() {
// RabbitMQ会自己内置的去比较全局的uuid是否存在,只要页面不刷新,产生的uuid一定不会变,检测到生产者发送过这个id,就不再发送消息
MessageProperties mp = new MessageProperties();
mp.setMessageId(IdUtil.randomUUID().toLowerCase());
User user = getData();
Message message = new Message("消息内容".getBytes(), mp);
amqpTemplate.send("test.direct", "queue-sync", message);
return null;
}
- 解决重复消费问题。在Consumer中可以通过消息的唯一ID进行判断是否已经消费过(借助Redis等工具每次消费都要记录已经消费过),也可以在每条消息中自定义唯一标识,判断是否已经消费过。
/**
* 消费者避免消息重复消费
*
* @Param: msg
* @Return:
* @Author: wxy
*/
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(name = "queue-sync"),
exchange = @Exchange(name = "test.direct", type = "direct"), key = "queue-sync"))
public void sync(Message msg) {
MessageProperties properties = msg.getMessageProperties();
// 1、获取messageId
String messageId = properties.getMessageId();
// 2、根据messageId去redis查,如果key存在,说明已经被消费
// 3、正常的进行消息消费
// 4、如果消费成功,以messageId作为键,存入到redis
System.out.println(messageId);
}
五、如何保证消息的可靠性传输
生产者弄丢了数据
开启confirm机制或者RabbitMQ事务(不推荐,吞吐量会下降)
事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息RabbitMQ接收了之后会异步回调你一个接口通知你这个消息接收到了。
RabbitMQ弄丢了数据
开启持久化
消费端弄丢了数据
ack确认机制
六、如何保证消息的顺序性
七、消息积压怎么办?
消息的生产速度大于消息的消费速度
- 临时扩充几个消费者服务。
大量消息在mq里积压了几个小时了还没解决几千万条数据在MQ里积压了七八个小时,从下午4点多,积压到了晚上很晚,10点多,11点多。这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复consumer的问题,让他恢复消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不能在面试的时候说吧。
一个消费者一秒是1000条,一秒3个消费者是3000条,一分钟是18万条,1000多万条,所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概1小时的时间才能恢复过来。
一般这个时候,只能操作临时紧急扩容了,具体操作步骤和思路如下:
1、先修复consumer的问题,确保其恢复消费速度,然后将现有cnosumer都停掉。
2、新建一个topic,partition是原来的10倍,临时建立好原先10倍或者20倍的queue数量。
3、然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue。
4、接着临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据。
5、这种做法相当于是临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据。等快速消费完积压数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息。
- 给消息设置过期时间。
八、消息队列过期失效问题
假设你用的是rabbitmq,rabbitmq是可以设置过期时间的,就是TTL,如果消息在queue中积压超过一定的时间就会被rabbitmq给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在mq里,而是大量的数据会直接搞丢。
这个情况下,就不是说要增加consumer消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。
这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入mq里面去,把白天丢的数据给他补回来。也只能是这样了。
假设1万个订单积压在mq里面,没有处理,其中1000个订单都丢了,你只能手动写程序把那1000个订单给查出来,手动发到mq里去再补一次。