摘要:你有没有过这样的经历?接手一个电商项目的订单模块时,发现 “订单创建后发送通知” 的功能总出问题 —— 要么通知延迟半天才到,要么偶尔直接丢消息,查日志却找不到明确报错?我身边不少做 Java 开发的朋友都遇到过类似情况,最近同事阿凯就踩了这个坑,而他最终用
你有没有过这样的经历?接手一个电商项目的订单模块时,发现 “订单创建后发送通知” 的功能总出问题 —— 要么通知延迟半天才到,要么偶尔直接丢消息,查日志却找不到明确报错?我身边不少做 Java 开发的朋友都遇到过类似情况,最近同事阿凯就踩了这个坑,而他最终用 Spring Boot3 整合 Redis 消息队列解决了问题。今天咱们就从这个真实案例出发,拆解整合步骤、分析常见问题,再结合行业专家的建议避坑,最后也欢迎你聊聊自己的实践经验。
阿凯负责的是一款生鲜电商的后端开发,前段时间项目迭代时,产品提了个需求:用户下单支付成功后,要立即发送 “订单确认通知” 到用户手机号,同时触发 “库存锁定” 和 “商家接单提醒”。最开始他用了简单的同步调用,结果高峰期时订单接口响应变慢,还出现过 “用户付了钱却没收到通知” 的投诉 —— 查下来是同步流程里某一个环节超时,导致后续通知逻辑没执行。
后来他想改用消息队列解耦,但团队考虑到项目目前是中小型规模,专门部署 RabbitMQ 或 Kafka 成本太高,运维也麻烦。领导提了个建议:“咱们项目本来就用 Redis 做缓存,能不能用 Redis 的消息队列功能?轻量又不用额外加组件。” 阿凯这才开始研究 Spring Boot3 整合 Redis 消息队列,花了两天时间落地,不仅解决了同步调用的性能问题,还通过优化配置避免了消息丢失,现在订单通知的成功率稳定在 99.9% 以上。
这个案例其实很典型 —— 很多中小型互联网项目在消息队列选型时,都会优先考虑 “现有组件复用”,Redis 消息队列因为部署成本低、与 Spring 生态适配好,成了不少开发的首选。但阿凯说,整合过程中他踩了好几个坑,比如 “消息消费重复”“队列堵塞”,这些也是咱们开发时容易忽略的点,接下来咱们就详细分析。
在拆解整合步骤前,咱们先把阿凯遇到的问题和行业里常见的坑拎出来,你在实操时可以提前规避。毕竟技术落地不是 “跑通 Demo 就行”,要考虑生产环境的稳定性。
阿凯最开始用 Redis 的 List 结构做消息队列(LPUSH 存消息,BRPOP 取消息),测试时发现偶尔会出现 “同一订单发了两条通知” 的情况。查了半天原因,发现是消费者服务重启时,之前已经取出来但没处理完的消息,因为没做 “消费确认”,导致重新被其他消费者获取。
其实这是 Redis List 做消息队列的一个短板:它是 “拉取式消费”,消息被 BRPOP 取出后就会从队列中删除,如果消费过程中服务宕机,消息就会丢失;但如果为了避免丢失,先把消息标记为 “待消费” 再处理,又可能因为标记失败导致重复消费。阿凯后来用了 Redis 的 Stream 结构,自带 “消息 ID” 和 “消费组” 机制,才解决了这个问题 —— 每个消息有唯一 ID,消费组会记录已消费的消息位置,即使服务重启也不会重复消费。
刚上线那几天,恰逢平台做 “周末促销”,订单量比平时多了 3 倍,阿凯发现 Redis 的 CPU 使用率突然飙升到 70% 以上。排查后发现,Redis 消息队列的消费速度跟不上生产速度,消息在队列里堆积了近万条,而 Redis 的 List 和 Stream 结构在处理大量堆积消息时,会频繁操作内存,导致性能下降。
这里要注意:Redis 本质是缓存,不是专门的消息队列,它的消息存储是基于内存的,没有持久化到磁盘的优化(虽然 Stream 支持持久化,但频繁写入磁盘也会影响性能)。阿凯后来做了两个优化:一是给消费者服务加了 “线程池”,提高消费并发度;二是设置了 “消息过期时间”,对于超过 24 小时还没消费的订单通知,直接丢弃(因为这类消息已经失去业务意义),避免无效堆积。
阿凯最开始把 Redis 消息队列部署在单节点上,结果有一次 Redis 服务器断电,整个消息队列瘫痪了,订单通知停了近 20 分钟。虽然项目有 Redis 主从复制,但主节点宕机后,从节点切换需要时间,而且切换过程中消息可能会丢失 —— 这也是很多开发在整合时容易忽略的 “高可用” 问题。
行业里的常规做法是:如果用 Redis 做消息队列,一定要部署 Redis 集群(至少 3 主 3 从),同时开启 Stream 的 “持久化” 功能(RDB+AOF 混合持久化),确保主节点宕机后,从节点能快速切换,并且消息不会丢失。阿凯后来按照这个方案优化了 Redis 集群,之后再没出现过单点故障导致的队列瘫痪问题。
Spring Boot3 整合 Redis 消息队列,4 个实操要点要记牢针对上面的问题,我特意咨询了有 10 年 Java 开发经验的架构师老周,他给出了 4 个实操建议,这些也是咱们在整合时需要重点关注的点,比单纯看文档更实用。
老周说,很多新手刚开始会用 Redis 的 List 或 Pub/Sub 做消息队列,但这两种结构都有明显缺陷:List 没有消费确认机制,容易丢消息或重复消费;Pub/Sub 是 “广播模式”,消息不持久化,消费者离线后会丢失消息。而 Redis 5.0 推出的 Stream 结构,专门为消息队列设计,支持 “消费组”“消息持久化”“消息回溯”,更适合生产环境。
所以在 Spring Boot3 整合时,建议直接用 Redis Stream,而不是纠结于 List 或 Pub/Sub。Spring Data Redis 3.x 版本已经对 Stream 提供了完善的支持,能很方便地实现生产者和消费者。
老周遇到过不少开发因为 “消息序列化” 出问题 —— 比如生产者用 Java 的 Serializable 序列化消息,消费者用 Jackson 反序列化,结果导致解析失败。他建议:在 Spring Boot3 中,统一用 “JSON 序列化”(比如 Jackson2JsonRedisSerializer),把消息转换成 JSON 字符串存储到 Redis 中,这样不仅避免乱码,还方便后续排查问题时,直接在 Redis 客户端查看消息内容。
具体配置也很简单:在 Spring Boot 的配置类中,注入 RedisTemplate 时,指定 Key 和 Value 的序列化器,代码示例如下(你可以直接复制到项目中用):
@Configurationpublic class RedisConfig { @Bean public RedisTemplateredisTemplate(RedisConnectionFactory factory) { RedisTemplatetemplate = new RedisTemplate; template.setConnectionFactory(factory); // 设置Key的序列化器 template.setKeySerializer(new StringRedisSerializer); // 设置Value的序列化器(JSON序列化) Jackson2JsonRedisSerializer jacksonSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper; objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); jacksonSerializer.setObjectMapper(objectMapper); template.setValueSerializer(jacksonSerializer); // 设置Hash的序列化器 template.setHashKeySerializer(new StringRedisSerializer); template.setHashValueSerializer(jacksonSerializer); template.afterPropertiesSet; return template; }}老周强调,生产环境中难免会遇到 “网络波动”“数据库临时不可用” 等问题,这时消费者处理消息会失败,如果直接丢弃消息,会导致业务损失。他建议:在消费者中加入 “异常重试机制”,比如用 Spring 的 @Retryable 注解,设置重试次数(比如 3 次)和重试间隔(比如 1 秒),如果重试 3 次还失败,就把消息转移到 “死信队列”,后续人工排查处理。
举个例子,阿凯在处理 “订单通知” 时,就加了重试逻辑:当调用短信接口失败时,重试 3 次,每次间隔 1 秒;如果还是失败,就把消息存到 “order_notify_dead” 死信队列,每天定时查看死信队列,手动处理失败的消息。这样既避免了消息丢失,又不会因为频繁重试影响服务性能。
老周说,很多开发把消息队列上线后就不管了,直到出现性能问题才去排查,这其实很被动。他建议:在项目中加入 Redis 队列的监控,比如用 Prometheus+Grafana 监控 “队列长度”“消费延迟时间”“消息生产 / 消费速率”,当队列长度超过阈值(比如 1000 条)或消费延迟超过 10 秒时,触发告警(比如发送邮件或钉钉通知),这样能提前发现堆积问题,及时扩容消费者。
比如阿凯就监控了 “order_notify_stream” 这个 Stream 队列的长度,当长度超过 500 条时,钉钉群会收到告警,他就能及时检查消费者是否正常运行,或者是否需要增加消费线程。
讲完案例、问题和专家建议,其实技术分享最有价值的还是大家的实践经验。我想问问你:你在项目中用过 Redis 做消息队列吗?有没有遇到过 “消息丢失”“重复消费” 这类问题?你是怎么解决的?或者你觉得 Redis 消息队列和 RabbitMQ、Kafka 比,有哪些优势和不足?
欢迎在评论区留言分享你的经历,咱们一起交流避坑技巧。如果这篇文章对你有帮助,也别忘了点赞 + 收藏,后续我还会分享更多 Spring Boot3 的实战技巧,比如整合 Elasticsearch、Shiro 等,关注我不迷路~
来源:从程序员到架构师