Redis发布订阅不校验消息类型,业务需自行约定序列化协议

简单来说,Redis的发布订阅(Pub/Sub)机制本身,对消息内容是完全“无感”的。它就像一个只管搬运、不管验货的传送带。这意味着,消息类型的定义、校验和解析,完全落在了业务开发者的肩上。在Spring Boot这类框架中,如果使用不当,很容易踩坑。
Redis发布订阅本身不支持消息类型校验
必须明确一个核心事实:Redis的 PUBLISH 和 SUBSCRIBE 命令,底层处理的只是字节数组。它不关心你塞进去的是结构化的JSON、高效的Protobuf,还是简单的纯文本。对它而言,一切都是 byte[],进进出出,仅此而已。
所以,我们常说的“自定义消息类型”,其实是业务层通过事先约定好的序列化与反序列化规则“模拟”出来的。Redis本身既不识别,更不会做任何验证。
这直接导致了一个现象:假设你向 redis_channel 这个频道,先后发送了一个JSON对象 {"name":"Alice"}、一个整数 42,甚至是一段二进制数据 0x01 0x02,Redis会照单全收,并原封不动地转发给所有订阅者。订阅方拿到这一串字节后,必须自己判断:“这次传来的,到底该用哪种方式解码?”
Spring Boot中用StringRedisTemplate默认只处理字符串
这里有个新手高频踩坑区。很多开发者会下意识地直接调用 stringRedisTemplate.convertAndSend("topic", obj),如果这里的 obj 是一个自定义对象(比如 Student),那么发出去的消息体会变成该对象默认的 toString() 结果,类似 "com.example.Student@1a2b3c"。这种字符串,接收方根本无法进行有效的反序列化。
正确的做法需要三步走:
- 发送端必须显式序列化:在发送前,主动使用如
JSONUtil.toJsonStr()或 Spring Boot 内置的ObjectMapper.writeValueAsString()将对象转换为JSON字符串。 - 接收端需明确编码:接收时,不能简单地
new String(message.getBody())就了事。必须先确认这串字节是UTF-8编码的合法JSON字符串。 - 业务层需类型标识:如果系统间要求强类型消息(例如,消费者只处理
StudentEvent,而拒绝UserEvent),一个常见的实践是在消息体中加入一个type字段。消费者先解析出这个字段,再做分支判断,比如if ("student".equals(json.get("type")))。
用MessageListener反序列化时要防空指针和格式异常
在消息监听器 MessageListener 的 onMessage 方法中,一行看似简单的 new String(message.getBody()),其实暗藏至少三个“陷阱”:
- 空指针风险:
message.getBody()理论上可能返回null(虽然罕见,但在极端网络抖动或客户端实现异常时可能发生)。 - 编码不一致:
new String(...)若不指定字符集,会使用系统默认编码。这在Linux和Windows环境间可能不一致,导致乱码。务必显式指定为StandardCharsets.UTF_8。 - 异常被静默吞没:如果在
onMessage方法中抛出异常,默认情况下Spring会静默处理,导致消息“凭空消失”却无任何错误日志,问题排查极其困难。
因此,一个健壮的监听器实现必须包裹完整的异常处理逻辑。来看一个示例片段:
public void onMessage(Message message, byte[] pattern) {
try {
String body = new String(message.getBody(), StandardCharsets.UTF_8);
Student event = JSONUtil.toBean(body, Student.class);
log.info("收到学生事件:{}", event.getName());
} catch (Exception e) {
log.error("解析消息失败,channel={}, raw={}",
new String(message.getChannel()),
new String(message.getBody(), StandardCharsets.UTF_8), e);
}
}
跨语言场景下序列化协议必须对齐
当消息的生产者和消费者使用不同编程语言时,挑战会进一步升级。即使都约定使用JSON,细节上的差异也可能导致解析失败。
例如,Ja va服务使用Fastjson库序列化一个包含 Date 字段的对象,输出可能是 "2026-04-01T05:30:00";而Go服务使用标准库 encoding/json 解析时,可能期望的是 "2026-04-01 05:30:00" 这种格式。格式不匹配,反序列化就会出错。
要解决这类问题,需要建立严格的约定:
- 时间格式统一:强制使用ISO 8601标准格式(如
"2026-04-01T05:30:00Z")来处理所有时间字段。 - 避免模糊结构:尽量不要使用
Map这类泛型结构作为消息体,改用明确定义字段的DTO类或结构体,约束性更强。 - 二进制协议需严格同步:如果选用Protobuf等二进制协议,必须确保所有语言客户端都基于同一份
.proto文件生成代码,并且版本严格保持一致。
实际开发中,最棘手的问题往往不是主流服务间的对接,而是临时接入的脚本或服务。比如,某天一个Python脚本临时接入,它使用 json.dumps(dict, ensure_ascii=False) 序列化数据,可能无意间引入了额外的空格或特殊字符。这种微小的差异,就可能导致Ja va端的解析器静默失败,日志里只留下一句令人困惑的“空消息”,排查起来费时费力。
所以说,在Redis Pub/Sub的实践中,把序列化协议当作一项重要的“服务间契约”来严格管理,是保证系统稳定性的关键一步。
