rabbitmq消费者消费消息为什么变成数字了?

本文探讨了在Spring Boot项目中,由于消息发送方将content-type从text/plain改为application/json,导致RabbitMQ队列阻塞和消费者接收到字节数组的问题。通过代码追踪和配置分析,揭示了MessageConverter的作用,以及如何解决content-type不匹配引发的消费异常。

温馨提示:图片看不清按ctrl+鼠标滚动放大网页

  1. 问题描述

2020年11月28号,rabbitmq队列visitLog一直处于阻塞状态,重启consumer,抛出org.springframework.amqp.AmqpException: No method found for class [B。临时解决方案,将@RabbitListener注解移动到方法上:

 

 按照上图方法操作后,consumer能够消费消息,但有500多条消息消费错误,消费者在接受消息时,其中有的消息为字节数组

初次判断原因为前端发送请求参数出现了错误,在项目中,有个埋点报了如下错误:

因此,定下结论,前端发送了图上参数埋点,导致了rabbitmqvisitLog阻塞。

2020年124号,线上环境consumer消费异常,从11:30开始,不断发送错误邮件

consumer为什么接受的消息莫名其妙变成了数字呢?

2.    问题跟踪

停掉消费者,通过rabbitmq management查看,mq所接受到的消息:

如上图红框所示,content-type变成了application/json ,以前一直是text/plain。查看consumer端的代码:

接受消息为字符串类型,原因就是mq里所存的消息content-typeapplication/json,而consumer接受的消息为字符串,没有对消息进行转换,即配置额外的MessageConverter。通过查阅文档,

spring-amqp5.1.2开始已经对这个点进行了优化,即不需要配置额外的MessageConverter,原因在之后的resolveArgument环节,匹配到了RabbitListenerAnnotationBeanPostProcessor$BytesToStringConverter。这个Converter就可以将String类型Payloadbyte[]可以正常convertString字符串。

在周五临时解决方案中,升级springboot版本,并StringEscapeUtils.unescapeJava,处理掉字符串中的转义字符,最终临时上线。但是后面一直出现json解析错误。

3.    问题原因

       问题的根本原因就是消息的发送方发送消息content-type从原来的text/plain变成了application/json,并且当天临时上线解决后,content-type又变成了text/plain,而consumer只能消费text/plain的消息。

4.    代码追踪

4.1  生产者代码追踪

       查看visitLog消息的生产者:

       点击convertAndSend分析,直到doSend方法浏览代码:

       如上图所示:红框中有message.getMessageProperties(),获取消息属性,那消息属性是在哪里设置的呢?

        查看amqpTemplate配置

        查看RabbitTemplate类源码,浏览属性:
 

 

浏览方法:


 

看到RabbitTemplate的默认MessageConverterSimpleMessageConverter 点击SimpleMessageConverter源码:

查看createMessage方法注释,创建一个AMQP消息,从生产者;
如果消息类型为字节数组,则content-typeapplication/octet-stream
如果消息类型为字符串,则content-typetext/plain;
如果消息类型为序列化对象,则content-typeapplication/x-java-serialized-object。
查看convertAndSend发送的类型:

为字符串类型,明明是字符串类型,为什么content-type莫名其妙变成了application/json,搜索MessageConverter子类下的所有application/json,即MessageProperties类下的成员变量CONTENT_TYPE_JSON


 

 Jackson2JsonMessageConverterJsonMessageConverter中设置了content-typeapplication/json

由此推断,是amqpTemplate的消息转换器被更改了,全局搜索Jackson2JsonMessageConverterJsonMessageConverter,发下了如下代码:

 

果然amqpTemplateMessageConverter被更改,查看spring配置



MessageQueueServiceImplAgoraPushServiceImpl中使用的RabbitTemplate都是这里配置的同一个对象,springIOC注入的对象默认是单例的,故在其他地方调用sendMsgToAgora后,会将amqpTemplateMessageConverter改成Jackson2JsonMessageConverter

4.2 消费者代码追踪
               查看MessageConsumer代码,在MessageConsumer类上注解了@RabbitListener,在handleMessage上注解了@RabbitHandler,故当队列有消息时,会通过handleMessage方法来消费。那为什么会出现org.springframework.amqp.AmqpException: No method found for class [B这个异常呢? 

查询相关博客:https://blog.youkuaiyun.com/u013905744/article/details/86736536

为什么一个普通的方法加上@RabbitListener注解就能接收消息了呢?

先总结来说,有一个BeanPostProcessor来处理这个注解,把注解相关的内容取出来,封装成一个RabbitListenerEndPoint。然后给每个Endpoint创建一个MessageListenerContainer,在这个container中注册一个MessageListener,在这个MessageListener中创建了一个HandlerAdapter,这个adapterrabbitmq broker建立一个connection,接收rabbitmq broker push过来的message,放到一个blocking queue中。至此完成消息的接收。


根据上述博客描述内容,点开MessageListenerContainer源码:

从上面能看到设置消费者,设置队列等信息,仔细查看doStart方法:

可以看到,@Listener接受消息实现在这里。关键代码this.initializeConsumers();

 

关键代码this.taskExecutor.execute(new AsyncMessageProcessingConsumer(consumer));
点击AsyncMessageProcessingConsumer构造方法:
 

 

在点开BlockingQueueConsumer类,查看handle方法:


 

消费者的消息类型设置在红框中,故可以查看consumer接受消息时的content-type类型,查看MessageListenerContainer属性,使用DefaultMessagePropertiesConverter


点开MessageProperties


 

查看代码得知,消息的属性设置来源于BasicProperties,而BasicProperties中的消息来源于消息本身的属性。但这样说,消息的接受最终会和消息的发送方适配?

查阅博客:https://www.jianshu.com/p/382d6f609697

@RabbitListener可以标注在类上面,当使用在类上面的时候,需要配合@RabbitHandler注解一起使用,@RabbitListener标注在类上面表示当有收到消息的时候,就交给带有@RabbitHandler的方法处理,具体找哪个方法处理,需要跟进MessageConverter转换后的java对象。
 

@RabbitListener注解指定目标方法来作为消费消息的方法,通过注解参数指定所监听的队列或者Binding。使用@RabbitListener可以设置一个自己明确默认值的RabbitListenerContainerFactory对象

如果消息属性中没有指定content_type,则接收消息的处理方法接收类型是byte[],如果消息属性中指定content_type为text,则接收消息的处理方法的参数类型是String类型。不管有没有指定content_type,处理消息方法的参数类型是Message都不会报错


由此我们得知,当@RabbitListener@RabbitHandler配合使用时,会将不同的消息适配到合适的方法上,当消息content-type变成application/json时,而我们处理消息的方法只有图下方法,没有适配到其他方法,故会抛出No method found for class [B,导致队列没有消费者,致使mq阻塞。

 

@RabbitListener注解到目标方法时,此异常解决。但消息类型缺被转成一串数字?为什么呢?点开MessagingMessageListenerAdapter 

 

 

 如果没有配置MessageConverterMessagingMessageListenerAdapter使用的MessagingMessageConverter会初始化一个SimpleMessageConverter,点开SimpleMessageConverter,查看fromMessage方法:



 

如果contentTypetext开头,消息会初始化为字符串

content = new String(message.getBody(), encoding);

如果等于application/x-java-serialized-object,消息会反序列化

content = SerializationUtils.deserialize(this.createObjectInputStream(new ByteArrayInputStream(message.getBody()), this.codebaseUrl));

如果上面全部不满足:content = message.getBody();

最终以object方式返回,因此最终接受消息是字节数组。

5. 问题复现
①停掉开发环境consumer,清空visitLog队列
②调用/api/test/push接口
③测试/visitlog/newLog接口 
④查看消息
看到消息content-type变成application/json
⑥本地启动consumer,抛出异常
⑦将@RabbitListener注解到方法上,重启consumer,出现字节数组

 

 

 

 

6. 总结与反思
根本原因,queue的生产者与消费者content-type不一致。如果在后面代码中,消息发送方变成发送序列化对象,同样会抛出No method found for class [B,造成消息队列阻塞。
    在开发中,应该限定每个queuecontent-type,不能发送其他类型数据;同一项目一个队列应该配置唯一对应的RabbitTemplate,而不是RabbitTemplate对应多个队列。
<think>我们正在讨论跨进程通信(IPC)中高效可靠的消息传递方法。根据用户提供的引用和之前关于Windows消息传递的讨论,我们可以总结出几种高效可靠的IPC技术方案。注意,用户特别提到需要高效和可靠,因此我们将重点放在低延迟、高吞吐量且保证消息可靠传递的方案上。 ### 1. **共享内存(Shared Memory)** 共享内存是最快的IPC方式之一,因为进程直接访问同一块内存区域,无需数据复制。但需要配合同步机制(如互斥锁、信号量)来避免竞争条件。 - **高效性**:内存级速度,适合大数据传输。 - **可靠性**:需要应用层确保同步和错误处理。 - **适用场景**:实时系统、高频交易、日志聚合等[^2]。 **实现示例(Windows API)**: ```cpp // 创建共享内存 HANDLE hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BUF_SIZE, L"Global\\MySharedMemory"); LPSTR pBuf = (LPSTR)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUF_SIZE); // 写入数据 CopyMemory(pBuf, data.c_str(), data.size() * sizeof(wchar_t)); // 读取数据 std::wstring received(pBuf); ``` ### 2. **消息队列(Message Queues)** 消息队列提供异步通信能力,支持消息持久化和可靠传递。常见实现包括: - **POSIX消息队列**(Unix/Linux) - **MSMQ**(Windows) - **ZeroMQ**、**RabbitMQ**(第三方库) - **高效性**:中等,但支持批量处理和流量控制。 - **可靠性**:支持持久化存储,确保消息不丢失。 - **适用场景**:分布式系统、日志收集、任务调度。 ### 3. **命名管道(Named Pipes)** 命名管道支持双向通信,提供可靠的字节流传输。Windows还支持异步I/O操作以提升性能。 - **高效性**:适合中小数据量,支持阻塞/非阻塞模式。 - **可靠性**:内置错误检测和重传机制。 - **适用场景**:客户端-服务器架构,如数据库连接。 **Windows示例**: ```cpp // 服务器端 HANDLE hPipe = CreateNamedPipe(L"\\\\.\\pipe\\MyPipe", PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE, 1, 0, 0, 0, NULL); ConnectNamedPipe(hPipe, NULL); WriteFile(hPipe, data.c_str(), data.size() * sizeof(wchar_t), &bytesWritten, NULL); // 客户端 HANDLE hPipe = CreateFile(L"\\\\.\\pipe\\MyPipe", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); ReadFile(hPipe, buffer, BUF_SIZE, &bytesRead, NULL); ``` ### 4. **WM_COPYDATA消息(Windows特有)** 通过Windows消息机制传递数据,底层使用共享内存但无需手动管理同步。适合小到中等数据量。 - **高效性**:中等,数据量较大时性能下降。 - **可靠性**:由操作系统保证传递完整性。 - **适用场景**:同一机器上的GUI程序通信[^4]。 **示例**: ```cpp // 发送方 COPYDATASTRUCT cds; cds.dwData = 1; // 标识数据类型 cds.cbData = (data.size() + 1) * sizeof(wchar_t); cds.lpData = const_cast<wchar_t*>(data.c_str()); SendMessage(hWndTarget, WM_COPYDATA, (WPARAM)hWndSender, (LPARAM)&cds); // 接收方(窗口过程) case WM_COPYDATA: { COPYDATASTRUCT* pcds = reinterpret_cast<COPYDATASTRUCT*>(lParam); if (pcds->dwData == 1) { std::wstring received(static_cast<wchar_t*>(pcds->lpData)); } break; } ``` ### 5. **内存映射文件(Memory-Mapped Files)** 与共享内存类似,但通过文件系统抽象。支持跨持久化存储和进程通信。 - **高效性**:接近共享内存速度。 - **可靠性**:可配置为持久化存储。 - **适用场景**:大数据处理(如Mappedbus库)[^2]。 **Mappedbus示例(Java)**: ```java // 写入消息 MappedBusWriter writer = new MappedBusWriter("/tmp/test", 100000L, 32); writer.open(); writer.write(new PriceUpdate(100, 9.99)); // 读取消息 MappedBusReader reader = new MappedBusReader("/tmp/test", 100000L, 32); reader.open(); while (true) { if (reader.next()) { PriceUpdate update = reader.read(PriceUpdate.class); } } ``` ### 6. **RPC(Remote Procedure Call)** 允许像调用本地函数一样调用远程进程的函数。现代RPC框架如gRPC、Thrift提供高效序列化和网络传输。 - **高效性**:依赖序列化协议(如Protobuf)。 - **可靠性**:支持重试、超时、负载均衡。 - **适用场景**:微服务、分布式计算。 --- ### **方案选择建议** | 场景 | 推荐方案 | 理由 | |-----------------------|-------------------|----------------------------------------------------------------------| | 低延迟实时系统 | 共享内存/Mappedbus | 纳秒级延迟,无锁设计[^2] | | 跨平台分布式系统 | gRPC/RabbitMQ | 支持多种语言和操作系统 | | Windows GUI程序通信 | WM_COPYDATA | 简单易用,无需额外配置[^4] | | 大数据流处理 | 内存映射文件 | 高效处理GB级数据,支持持久化 | | 金融交易系统 | 共享内存+RDMA | 超低延迟(微秒级),避免CPU复制 | --- ### **关键优化技术** 1. **零拷贝(Zero-Copy)** 如Mappedbus通过内存映射直接读写,避免数据复制[^2]。 2. **批处理(Batching)** 消息队列中合并多条消息减少I/O次数。 3. **无锁数据结构** 共享内存中使用原子操作替代锁,减少上下文切换。 4. **异步I/O** 重叠计算与通信,最大化吞吐量。 --- ### **相关问题** 1. 如何在Linux下实现类似WM_COPYDATA的进程通信? 2. 共享内存通信中如何设计无锁同步机制? 3. gRPC和消息队列在可靠性设计上有哪些差异? 4. 如何确保跨进程消息传递的线程安全性? 5. 内存映射文件在突然断电时如何保证数据完整性?[^2] > 引用来源: > [^1]: 安卓开发艺术探索 - 进程线程通信与同步 > [^2]: Mappedbus项目文档 - 高效IPC库设计 > [^3]: Android Messenger - 基于Binder的IPC > [^4]: Windows WM_COPYDATA实战</think>### 跨进程通信(IPC)高效可靠的消息传递方案 在进程间通信中实现高效可靠的消息传递,需结合低延迟、高吞吐量和消息可靠性。以下是经过验证的技术方案: --- #### **1. 共享内存 + 无锁同步** **原理**:进程直接访问同一块物理内存区域,避免内核复制开销 **高效性**: - 内存级访问速度(纳秒级延迟) - 支持零拷贝数据传输 - 多进程并发写入能力 **可靠性保障**: ```cpp // Windows API 示例 HANDLE hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BUF_SIZE, L"MySharedMem"); char* pBuf = (char*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUF_SIZE); // 使用原子操作实现无锁同步 InterlockedExchangeAdd((LONG*)&pBuf[index], value); // 线程安全操作 ``` **适用场景**:高频交易系统、实时数据处理[^2] **优势**:70纳秒级延迟,支持TB级数据吞吐[^2] --- #### **2. 内存映射文件 (Memory-Mapped Files)** **原理**:将文件映射到进程虚拟地址空间 **高效设计**: - 按需分页加载(仅访问时载入物理内存) - 操作系统自动缓存管理 **可靠性机制**: ```cpp // 跨进程持久化示例 HANDLE hFile = CreateFile(L"data.bin", GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, 0, NULL); HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 1024*1024, NULL); void* pData = MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, 0); // 写入时自动刷新到磁盘 FlushViewOfFile(pData, dataSize); ``` **适用场景**:日志聚合、大数据处理[^2] --- #### **3. 消息队列中间件** **架构**:生产者-消费者模型 + 持久化存储 **高效特性**: - 批量消息压缩传输 - 多消费者负载均衡 **可靠性协议**: ``` [生产者] --(ACK确认)--> [消息代理] --(持久化存储)--> [消费者] ``` **实现方案**: - **ZeroMQ**:支持10万+ TPS,提供REQ-REP可靠模式 - **RabbitMQ**:AMQP协议保证At-Least-Once投递 - **Kafka**:分区日志+副本机制保证数据不丢失 --- #### **4. Windows 专用通道** **(1) WM_COPYDATA 消息** ```cpp // 发送端 COPYDATASTRUCT cds; cds.dwData = 1; // 消息标识符 cds.cbData = (wstr.size() + 1) * sizeof(wchar_t); cds.lpData = (void*)wstr.c_str(); SendMessage(hWndTarget, WM_COPYDATA, (WPARAM)hWndSender, (LPARAM)&cds); // 接收端 case WM_COPYDATA: { PCOPYDATASTRUCT pcds = reinterpret_cast<PCOPYDATASTRUCT>(lParam); wstring data = static_cast<wchar_t*>(pcds->lpData); return TRUE; // 确认接收 } ``` **优势**:内核自动管理内存,无泄漏风险[^4] **限制**:单次传输上限约64KB **(2) 命名管道 (Named Pipes)** ```csharp // C# 异步示例(跨.NET进程) using (NamedPipeServerStream pipe = new NamedPipeServerStream("MyPipe", PipeDirection.InOut, 1, PipeTransmissionMode.Message)) { pipe.WaitForConnection(); byte[] buffer = new byte[1024]; int bytesRead = pipe.Read(buffer, 0, buffer.Length); // 自动重连机制保障可靠性 } ``` **性能**:支持双向流式传输,吞吐量可达2GB/s --- #### **5. Android Binder 框架** **架构**:Linux内核驱动 + 内存映射 **高效设计**: - 一次拷贝优化(发送方→内核→接收方) - 线程池自动调度 **可靠性实现**: ```java // Messenger 跨进程通信 Message msg = Message.obtain(null, MSG_DATA); msg.replyTo = mMessenger; // 包含IBinder引用 Bundle data = new Bundle(); data.putString("key", "value"); msg.setData(data); mService.send(msg); // 自动重试机制 ``` **优势**:内核级死亡通知,自动清理资源[^3] --- ### 方案选型指南 | 场景 | 推荐方案 | 吞吐量 | 延迟 | 可靠性保障 | |---------------------|------------------------|--------------|------------|--------------------------| | 金融交易系统 | 共享内存+原子操作 | >1M TPS | 70ns | 事务日志+校验和[^2] | | 跨平台微服务 | gRPC+Protobuf | 100K-500K TPS| 微秒级 | HTTP/2流控+重试 | | Windows GUI通信 | WM_COPYDATA | 中等 | 毫秒级 | 内核托管内存[^4] | | 安卓跨进程 | Binder/Messenger | 10K-50K TPS | 毫秒级 | 死亡通知+自动回收[^3] | | 大数据日志收集 | 内存映射文件 | GB/s级 | 可变 | fsync强制落盘 | --- ### 关键可靠性技术 1. **消息确认机制** - 发送方超时重传(指数退避算法) ```math \text{重试间隔} = \text{base} \times 2^{\text{attempt}} ``` 2. **持久化存储** - Write-Ahead Logging (WAL) 预写日志 - 副本同步机制(如Kafka ISR) 3. **端到端校验** - CRC32校验码:$$ \text{CRC} = \sum_{i=0}^{n} \text{data}[i] \oplus \text{polynomial} $$ - 数字签名:RSA或ECC算法 --- ### 相关问题 1. 如何在共享内存方案中实现无锁并发控制? 2. WM_COPYDATA消息传递大文件的优化策略有哪些? 3. 安卓Binder机制如何避免内存泄漏?[^3] 4. 消息队列的持久化策略如何影响系统性能? 5. 跨平台IPC通信中如何解决字节序差异问题? > 引用来源: > [^1]: 安卓进程线程通信与同步原理 > [^2]: Mappedbus低延迟IPC设计 > [^3]: Android Messenger跨进程机制 > [^4]: WM_COPYDATA实战优化方案
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值