ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot发布订阅模式实战:从Spring Event到Redis Streams与RabbitMQ

SpringBoot发布订阅模式实战:从Spring Event到Redis Streams与RabbitMQ 今年接了一个大促项目下单接口后面挂了六个业务动作扣库存、发优惠券、送积分、发短信、写订单快照、同步给ERP。最初全部是同步串行调用接口响应时间从150ms一路涨到900ms更麻烦的是每加一个动作都要去订单服务主流程里改代码测试要回归的点越来越多线上某一路抖动整条链路全跟着遭殃。后来我把这套耦合流程整体改造成发布订阅模式问题才算彻底解决。如果你也在SpringBoot项目里想落地发布订阅模式这篇文章值得看完。我不打算讲那些“官方文档里也有”的入门概念而是把我实际用过的三条路线、配置细节、踩过的深坑完整写出来——从Spring自带的事件机制到Redis Pub/Sub、Redis Streams再到RabbitMQ广播模型每一套方案的适用边界是什么、什么场景该用哪一个、配置的时候哪一步最容易翻车都会讲到。1. 一次下单接口的痛点同步调用为什么撑不住业务膨胀先说说我这边的业务背景。最初订单服务是单体架构下单接口内部直接调用库存服务、会员服务、短信服务代码长这样public void createOrder(OrderDTO order) { // 1. 核心订单逻辑 orderMapper.insert(order); // 2. 扣库存 stockService.deduct(order.getSkuId(), order.getCount()); // 3. 发优惠券 couponService.issue(order.getUserId()); // 4. 送积分 memberService.addPoints(order.getUserId(), order.getTotalPrice()); // 5. 发短信 smsService.send(order.getUserId(), 下单成功); // 6. 同步ERP erpService.sync(order); }这种代码在业务量小的时候没有任何问题但业务膨胀后问题会一个一个冒出来。第一是性能问题。同步串行把每一步的耗时全部累加接口RT响应时间随下游服务个数线性增长。库存服务慢200ms短信服务超时重试3秒用户端的感受就是转圈圈。第二是耦合问题。每个新增动作都要修改订单服务主流程订单服务逐渐变得“什么都知道”但它其实只需要负责订单本身。后来换了个产品经理要接一个物流预警服务我改完一版测试组提了十几条回归用例全链路手工回归一次就得大半天。第三是稳定性问题。下游任意一个服务出故障主流程就跟着失败。短信服务第三方通道偶发抖动用户下单直接失败这个锅背得太冤了。发布订阅模式解决的就是这三个问题。它的核心思想是下单成功后订单服务只负责发布“订单创建成功”这个事件至于谁关心这个事件、事件发生后要执行什么动作订单服务一概不管。库存、优惠券、积分、短信、ERP都是事件的订阅者各自监听、各自处理。在正式开始改造之前有必要把“观察者模式”和“发布订阅模式”这两个概念分清楚。网上很多文章把它们混在一起讲实际区别还是很明显的对比项观察者模式发布订阅模式耦合关系Subject直接持有Observer列表Publisher与Subscriber互不认识中间角色没有主题对象直接通知观察者有事件通道/Broker负责消息路由通知时机主题状态变化时同步通知事件发布后由代理异步/同步分发扩展性新增观察者需要在Subject里注册新增订阅者不影响发布者发布者甚至不知道订阅者存在Spring里的ApplicationEvent本质上是“类观察者模式”的框架实现但也已经具备发布订阅的松耦合特性Redis Pub/Sub和RabbitMQ广播则是标准的发布订阅模型。这篇文章里的用法统一按发布订阅模式来理解即可。2. 发布订阅模式落地SpringBoot的三条路线与选型思路我刚做改造那会儿第一反应是去引入RabbitMQ毕竟它的广播模型非常契合发布订阅。但后来跟团队聊完才发现公司几百个服务里真正把MQ用在核心链路上的场景并不多很多时候引入MQ只是为了解决一个可以用更轻量方案解决的问题。所以在SpringBoot里落地发布订阅我一般按下面三条路线来选。2.1 三条路线的核心差异路线一是Spring ApplicationEvent进程内事件。事件通过Spring容器内部的多播器分发订阅者和发布者在同一个JVM进程里。优点是零额外依赖跟着SpringBoot一起走配置量极小缺点是跨不了进程多实例部署时天然失效。路线二是Redis Pub/Sub与Streams跨进程轻量广播。Redis作为独立的Broker多个SpringBoot实例共享事件通道。优点是大多数公司已经有Redis不用新增中间件缺点是Pub/Sub模式下消息不持久化消费者不在线消息就丢了。Redis Streams则补上了持久化和消费组能力。路线三是消息队列RabbitMQ Fanout / Kafka。这是真正意义上的企业级事件总线。消息支持持久化、重试、死信队列、消费组可靠性最强缺点是引入额外组件部署运维成本高生产环境还要考虑高可用。2.2 我的选型判断逻辑用一张对比表说明维度Spring EventRedis Pub/SubRedis StreamsRabbitMQ/Kafka跨进程不支持支持支持支持消息持久化无无支持支持消息可靠投递同步调用可感知失败即发即弃丢失风险高可ACK消费组可靠最强消费者能力单JVM内每个订阅者都收全体消息消费组做数据分摊消费组/广播都有引入成本零低低中高最适合的场景单体应用内部解耦在线通知、简单广播需要追溯记录的事件流核心业务事件、跨团队集成实际项目里我习惯这么排单体阶段先上Spring Event零成本解耦如果系统拆成多个实例还共享同一套Redis且业务能容忍消息偶尔丢失用Redis Pub/Sub消息一旦丢了会影响核心数据一致性优先考虑Redis Streams靠消费组做到可靠处理如果公司本来就有MQ集群且事件链路涉及跨团队、需要重试和死信直接走MQ。有个容易踩的坑是很多人觉得“反正有Redis直接上Pub/Sub不就行了”。但如果事件订阅方不在线、或者代码发布瞬间有实例重启Pub/Sub消息直接消失。去年我就因为这个问题在配置变更广播上丢过事件后来才切到Streams。3. Spring ApplicationEvent进程内发布订阅的优雅解法Spring从很早就支持事件机制SpringBoot完全继承了这套能力。如果你只想在单体应用内部解耦用这个方案最省事。3.1 事件类与发布器的三板斧先定义一个事件类。很多人会继承ApplicationEvent这在老代码里很常见但Spring 4.2以后推荐直接用普通POJO发布器可以发布任意Object监听器按类型匹配即可。public class OrderCreateEvent { private final OrderDTO order; private final long timestamp; public OrderCreateEvent(OrderDTO order) { this.order order; this.timestamp System.currentTimeMillis(); } public OrderDTO getOrder() { return order; } public long getTimestamp() { return timestamp; } }然后注入ApplicationEventPublisher发布事件Component public class OrderEventPublisher { Resource private ApplicationEventPublisher eventPublisher; public void publishOrderCreated(OrderDTO order) { eventPublisher.publishEvent(new OrderCreateEvent(order)); } }在订单服务里只需一行调用orderEventPublisher.publishOrderCreated(order);这就是发布端的全部代码。库存、优惠券、积分这些服务各自写监听器Component public class InventoryListener { private static final Logger log LoggerFactory.getLogger(InventoryListener.class); EventListener public void onOrderCreated(OrderCreateEvent event) { OrderDTO order event.getOrder(); // 这里写扣减库存逻辑 log.info(订单[{}]扣减库存成功, order.getOrderId()); } }用EventListener注解标注的public方法Spring会自动识别并注册为事件监听器。方法参数类型决定了监听哪类事件所以事件类尽量用业务语义明确的名称不要一个类当多个用途。3.2 监听器的三种写法与执行细节除了EventListener还有两种常见写法。一种是实现ApplicationListener接口Component public class CouponListener implements ApplicationListenerOrderCreateEvent { Override public void onApplicationEvent(OrderCreateEvent event) { // 发优惠券逻辑 } }另一种是EventListener结合SpEL表达式做条件过滤。比如我只想对线上支付订单发短信可以这样写EventListener(condition #event.order.payType online) public void onOnlineOrder(OrderCreateEvent event) { smsService.send(event.getOrder().getUserId(), 线上支付下单成功); }这里#event对应event参数名condition表达式在事件分发前求值不满足条件的监听器根本不会进入方法体性能上比方法内部判断要好一点点。有一点要提醒默认情况下所有监听器的执行是同步的。也就是说publishEvent方法会阻塞直到所有监听器执行结束。如果某个监听器内部有慢调用比如短信服务超时主流程照样被拖住。所以实际项目中能异步的监听器一定要异步这个下一章会重点讲。3.3 SpringBoot 3.x升级后的包名大坑如果你用SpringBoot 3.x这章内容务必看一下。Spring Boot 3.0基于Jakarta EE 9原来javax开头的包名全部迁移到了jakarta开头。最典型的就是// SpringBoot 2.x import javax.annotation.Resource; // SpringBoot 3.x import jakarta.annotation.Resource;很多老项目从SpringBoot 2.x升到3.x编译报错一搜全是javax找不到就是因为这个。包名迁移本身不难难的是第三方依赖是否兼容。一些老版本的MyBatis Starter、Redis客户端、ShardingSphere组件在SpringBoot 3下可能会直接启动失败。这也是网上“springboot版本太高”这个吐槽的高频来源。我的建议是手头有老项目要升级先看核心依赖有没有发过适配SpringBoot 3的版本再动手新项目直接上SpringBoot 3.2以上没必要用2.x。4. 异步事件失效与事务边界最容易被忽视的两个深坑这章我单独拿出来写是因为我在生产环境里连续踩过两次而且问题都不太好排查。4.1 同步监听器在事务里的“天然优势”先说一个容易被忽略的点同步监听器在事务里是有“优势”的。如果你在事务方法里发布事件同步监听器会在事务提交前执行。此时数据库连接还绑在同一个事务上下文里监听器里做的数据库操作可以直接看到事务内的未提交数据比如订单主记录刚insert进去监听器里再查这条订单是查得到的。这看起来是个优点但也是一个隐患。如果监听器里抛了运行时异常事务会被标记为rollback-only整个业务方法都回滚——哪怕是订单已经写入成功也要跟着一起回滚。所以同步监听器里的异常处理要格外小心。4.2 Async没有生效的几类原因想让监听器异步执行最直接的方式是开启Spring的异步支持。第一步在配置类上加EnableAsyncConfiguration EnableAsync public class AsyncConfig { }然后监听器方法加上AsyncAsync(eventExecutor) EventListener public void onOrderCreated(OrderCreateEvent event) { // 异步执行 }这里有个大坑Async默认使用SimpleAsyncTaskExecutor它对每个任务都会新建一个线程根本不复用线程。高并发下线程数量会暴增甚至拖垮整个应用。生产环境必须自定义线程池。Bean(eventExecutor) public ThreadPoolTaskExecutor eventExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(16); executor.setQueueCapacity(100); executor.setThreadNamePrefix(event-exec-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }线程池参数要根据业务量来定。我这边下单峰值大约是每秒几百笔核心线程数4、最大线程16、队列容量100跑下来够用。拒绝策略务必选CallerRunsPolicy它能让线程池满时任务回退到调用线程执行虽然可能阻塞主流程但至少不会静默丢消息。如果你选DiscardPolicy事件就悄悄消失了排查的时候连日志都没有。Async还有一种失效场景同一个类内部调用。比如监听器方法里调用了同类里的另一个Async方法这个是代理机制问题Spring的AOP代理不拦截内部自调用Async直接不生效。解决方法是把异步方法单独放到另一个Bean里或者用ApplicationContext.getBean(XX.class)调用。4.3 TransactionalEventListener的提交时机如果事件发布在事务方法里推荐使用TransactionalEventListener替代EventListener。它可以精确控制在事务的哪个阶段处理事件TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT, fallbackExecution true) public void onOrderCreated(OrderCreateEvent event) { // 事务提交后执行 }AFTER_COMMIT的含义是当前事务成功提交后才触发监听器。这样能避免“事务还没提交监听器就先去处理业务结果读了半截数据”的问题。fallbackExecution非常重要。它表示当前没有事务时也执行监听器。如果没有这个参数你在非事务方法里发布事件监听器永远不会触发而且Spring不会给你任何明显报错只是静默跳过。我第一次用的时候就在这种场景上栽过。这个知识也是个高频面试点面试官通常问“事务事件和普通事件有什么区别”。你可以直接回答普通事件在事务提交前就可能触发事务事件可以指定在提交后或回滚后触发避免读到未提交数据。4.4 线程池与消息丢失的权衡最后补充一个异步事件的取舍问题。异步事件的本质是“发布者不等监听器结果”所以发布者永远无法知道监听器到底成功还是失败。监听器里一旦有异常默认只会打日志数据可能就错了。我的处理习惯是对核心链路的事件监听器内部一定要有完整try/catch并且把失败数据写入本地补偿表由定时任务扫描补偿对非核心事件宁可异常打日志也不要把异常往上抛因为抛出也不会有人处理反而可能污染日志。5. Redis Pub/Sub跨系统广播的轻量方案与序列化陷阱进程内事件有一个硬伤应用部署多个实例时A实例发布的Spring事件B实例完全感知不到。而且订单服务和库存服务如果已经拆成了两个SpringBoot应用Spring Event天然跨不过去。这时候如果公司已经有Redis用Redis Pub/Sub做事件广播是成本最低的跨进程方案。5.1 发布端convertAndSend的序列化Redis Pub/Sub的使用接口非常简单。我推荐直接用StringRedisTemplate把消息序列化成JSON字符串再发Component public class RedisOrderEventPublisher { private static final String ORDER_CHANNEL event:order:created; Resource private StringRedisTemplate stringRedisTemplate; public void publish(OrderDTO order) { String json JSON.toJSONString(order); stringRedisTemplate.convertAndSend(ORDER_CHANNEL, json); } }这里要重点强调一下序列化的坑。如果你用RedisTemplateString, ObjectSpring默认的Value序列化器是JdkSerializationRedisSerializer它会把Java对象序列化成二进制。消费者反序列化的时候如果类路径、serialVersionUID不一致直接报错。更麻烦的是二进制消息里携带了完整的类全限定名发布方和消费方必须用同一个包名类名这对多系统协作很不友好。我有一次排障消费端一直报ClassNotFoundException最后发现是发布方和服务方的订单DTO包名不一致导致的。从那以后跨系统传输的消息我统一用JSON字符串不碰Jdk序列化。这是最稳妥的做法。5.2 消费端MessageListener与容器配置消费端要配置一个RedisMessageListenerContainer它负责维持与Redis的连接订阅把收到的消息转发到业务监听器Configuration public class RedisSubConfig { Bean public RedisMessageListenerContainer redisMessageListenerContainer( RedisConnectionFactory connectionFactory, OrderMessageListener listener) { RedisMessageListenerContainer container new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.addMessageListener(listener, new ChannelTopic(event:order:created)); return container; } }监听器实现MessageListener接口Component public class OrderMessageListener implements MessageListener { private static final Logger log LoggerFactory.getLogger(OrderMessageListener.class); Override public void onMessage(Message message, byte[] pattern) { String payload new String(message.getBody(), StandardCharsets.UTF_8); try { OrderDTO order JSON.parseObject(payload, OrderDTO.class); // 处理下单后的业务逻辑 log.info(收到订单创建事件{}, order.getOrderId()); } catch (Exception e) { log.error(处理订单创建事件失败payload{}, payload, e); } } }注意onMessage方法里如果抛出异常默认情况下RedisMessageListenerContainer会把该监听器停掉导致后续消息全部接收不到。这是很隐蔽的坑。所以监听器里必须自己捕获所有异常绝不能往外抛。5.3 即发即弃的典型适用场景Redis Pub/Sub的定位是“即发即弃”。因为它不做持久化消息一旦发出当时不在线的订阅者就永远收不到。我实际用的场景主要包括配置变更广播各实例监听一个config:change通道收到通知后主动去配置中心拉最新配置。在线用户通知比如大促活动开始、客户端弹窗推送。缓存失效广播多实例部署时A实例改了缓存通知其他实例删除本地缓存。这些场景的共同点是消息丢了影响不大下一次心跳、下一次拉取、下一次主动刷新都能补偿。如果业务事件严格要求不丢别用Pub/Sub看下一章的Streams。6. Redis Streams让事件从“即发即弃”变成可追溯Redis 5.0引进了Streams数据结构真正解决了Pub/Sub“不持久化、不可追溯、无法消费组分摊”的问题。如果你一直在用Redis又不想为此引入MQStreams是很折中的选择。6.1 Streams和Pub/Sub的本质差异很多初学者会把Streams当成Pub/Sub的升级版其实两者底层差别很大对比项Pub/SubStreams消息存储不存储通道即发即弃存储在Stream里可随时按ID读取离线消费不支持订阅时消息已丢支持新消费者可以从最早消息开始读消息确认无有XACK机制可确认已处理消费组不支持支持同组内消费者分摊消息数据清理无可设置MAXLEN自动裁剪一句话总结Pub/Sub是即时广播Streams是持久化事件流。前者适合“通知一下就行”后者适合“这事不能记漏”。6.2 一个简单的Streams发布订阅Demo使用Spring Data Redis操作Streams发布端代码如下Resource private RedisTemplateString, String redisTemplate; public void publish(OrderDTO order) { MapString, String body new HashMap(); body.put(orderId, order.getOrderId()); body.put(userId, order.getUserId()); body.put(status, CREATED); ObjectRecordString, String record StreamRecords .objectBacked(JSON.toJSONString(body)) .withStreamKey(stream:order:created); redisTemplate.opsForStream().add(record); }消费端有几种方式。最简单的是用命令轮询ListMapRecordString, Object, Object records redisTemplate.opsForStream() .read(StreamReadOptions.empty().count(10).block(Duration.ofSeconds(2)), StreamOffset.create(stream:order:created, ReadOffset.lastConsumed())); for (MapRecordString, Object, Object record : records) { // 处理消息 MapObject, Object value record.getValue(); String orderJson (String) value.get(payload); // 处理完毕确认消息 redisTemplate.opsForStream().acknowledge(stream:order:created, group-order, record.getId()); }生产环境我建议用Spring Data Redis提供的StreamMessageListenerContainer它会启动后台线程持续消费类似Kafka的Consumer还能自动ACKBean public StreamMessageListenerContainerString, ObjectRecordString, String streamContainer( RedisConnectionFactory connectionFactory) { StreamMessageListenerContainerString, ObjectRecordString, String container StreamMessageListenerContainer.create(connectionFactory, StreamMessageListenerContainerOptions.builder() .pollTimeout(Duration.ofSeconds(1)) .targetType(String.class) .build()); container.receive(Consumer.from(group-order, instance-1), StreamOffset.create(stream:order:created, ReadOffset.lastConsumed()), streamListener); container.start(); return container; }这个方案能保证消息不会因为Reids重启而丢失前提是Redis开启了AOF持久化消费端宕机后重启还能从上一次未ACK的位置继续消费。6.3 Consumer Group与负载均衡刚接触Streams的时候很容易把消费者和消费者组搞混。Streams里的消费者组机制很强大同一个Stream可以创建多个组每个组都能收到全部消息这就是发布订阅效果组内如果有多个消费者消息会分摊到不同消费者实例这就是负载均衡效果。构建订单事件时如果库存服务和短信服务属于不同组两边都能收到事件同一组内的多个库存服务实例只会有一个实例处理某条消息。这个模型跟Kafka基本一致理解了它以后上Kafka也会很顺畅。我在实际项目里用Streams处理扫码登录通知和消息推送效果稳定而且Redis本来就是基础设施不需要额外占一台机器跑MQ。7. 消息队列方案是最终归属吗RabbitMQ广播模型的取舍如果公司已经有RabbitMQ或者Kafka而且事件链路涉及多个团队协作直接用MQ确实更省心。这里简单讲一下RabbitMQ如何用Fanout交换机实现发布订阅广播。7.1 Fanout交换机广播的配置先加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-amqp/artifactId /dependencyFanout交换机广播模型很好理解它不关心RoutingKey把到达交换机的每条消息分发给所有绑定了它的队列。每个订阅方声明自己的队列绑定到同一个Fanout交换机上就能各自收到全量消息。Configuration public class RabbitBroadcastConfig { Bean public FanoutExchange orderEventExchange() { return new FanoutExchange(exchange.order.event, true, false); } Bean public Queue inventoryQueue() { return new Queue(queue.order.inventory, true); } Bean public Queue smsQueue() { return new Queue(queue.order.sms, true); } Bean public Binding inventoryBinding() { return BindingBuilder.bind(inventoryQueue()).to(orderEventExchange()); } Bean public Binding smsBinding() { return BindingBuilder.bind(smsQueue()).to(orderEventExchange()); } }发送消息rabbitTemplate.convertAndSend(exchange.order.event, , orderJson);因为Fanout交换机忽略RoutingKey第二参数传空字符串即可。消费端用RabbitListenerRabbitListener(queues queue.order.inventory) public void handleInventory(String message) { // 扣库存逻辑 }7.2 重试与失败补偿的必要性MQ比Redis方案强的地方在于消息不会丢。但“消息不丢”不等于“处理成功”。消费端如果逻辑出错消息会一直进入死信循环。生产环境必须配置重试策略和死信队列比如一条消息处理失败后最多重试3次3次过后转入DeadLetterQueue由专门的任务去人工处理或定时补偿。我在接入RabbitMQ时一开始没有配死信队列结果线上一条畸形消息导致消费端反复抛异常业务被卡了十分钟。后来痛定思痛所有消费端都统一加了重试上限和死信转发遇到灵异问题才不用再半夜爬起来手动清队列。对于绝大多数SpringBoot项目如果Redis能覆盖需求我不会主动建议上MQ。中间件每多一个就意味着多一分运维成本。MQ最大的价值是“高可靠多团队协作”这些都没有的话Redis Streams足够好用。8. 事件驱动改造落地时我的排序与实操建议经过这几个项目的实践我总结了一套比较稳妥的落地顺序供参考。如果项目是单体架构优先用Spring ApplicationEvent。原因很简单零额外依赖SpringBoot天然支持事务事件、异步事件都很成熟代码可读性也好。唯一的注意点是异步监听器必须配线程池别用默认的SimpleAsyncTaskExecutor。如果应用已经拆成多实例、多服务但公司有现成的Redis事件不要求绝对不丢可以用Redis Pub/Sub。代码量很小配置也少适合做状态变更通知、缓存失效广播这类轻量场景。如果同样基于Redis但事件要求“不能丢、需要追溯、需要消费组分摊”直接用Redis Streams。它比Pub/Sub复杂一丢丢但可靠性提升明显是“轻量级可靠事件流”的最佳选择。如果事件链路涉及核心交易数据、跨团队协作、需要完善的失败重试和死信机制才考虑上RabbitMQ或Kafka。这里再多说几个实操层面的建议都是踩过坑换来的。第一事件命名建议用过去时态。OrderCreated、PaymentSucceeded而不是CreateOrder、PaySuccess。过去时态表意是“已经发生的事实”能避免概念混淆。第二事件消息体里一定要带eventId和timestamp。排查线上问题的时候没有eventId你很难把一条消息的发布和消费串起来。我习惯在事件结构里都带上traceId这样日志追踪能直接对到全链路。第三监听器里业务必须try/catch并且对失败数据做补偿。Spring Event的同步监听器如果抛异常会影响主流程事务Redis监听器抛异常可能导致订阅容器停止MQ消费端抛异常会导致重试风暴。无论哪条路线监听器内部异常都要自己兜住核心业务失败要落库补偿非核心业务失败打日志告警即可。第四事件处理逻辑尽量幂等。同一个事件因为重试、重复投递可能出现多次处理。比如发短信事件重发两次用户就会收到两条短信。处理前先查一下“是否有处理记录”这个习惯能省掉很多麻烦。第五发布订阅改造不是银弹它把“同步调用的确定性”换成了“异步执行的复杂性”。事件多了以后一个用户操作会触发十几个事件整个系统的数据链路会变得隐蔽排查问题难度直线上升。所以每个事件发布和消费的日志一定要规范从第一天就养成好事否则后面补真的很难。说回开头的下单项目改造后订单服务发布完OrderCreated事件就返回了库存、优惠券、积分各自监听处理后面接入物流预警服务只是新增一个监听器下单主流程一行代码都没改。这种“加功能不用改主链路”的感觉经历过的人都会觉得值。最后再分享一个小技巧Spring Event和Redis Pub/Sub并不冲突可以在ApplicationEvent的监听器里再转发到Redis通道实现“单体内部解耦 跨进程广播”双层结构。我自己就这么干过订单服务发布Spring事件监听器收到后转成JSON发到Redis其他服务订阅通道消费整个过程Spring事件负责进程内Redis负责跨服务。这样单体阶段写好的业务逻辑到了微服务阶段不用重写复制一份监听器改改通道即可。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进