MQ异常处理四层防御体系:从离谱设计到生产可靠
2026/9/16 22:17:16 网站建设 项目流程

1. 这不是Bug,是系统性失能:一个MQ异常不处理设计的真实代价

“外包19年,我见到的第15个离谱设计:不处理MQ异常”——这句话刚在技术群刷出来,我就放下手里的咖啡杯,点开截图。不是因为震惊,而是太熟悉了:熟悉的红色日志、熟悉的堆积消息、熟悉的凌晨三点告警电话、熟悉的客户投诉录音。这根本不是第15个,是第157个。只是这次,它终于被写进了标题里。

MQ,也就是消息队列,不是什么高深莫测的黑科技。它本质就是一个带缓冲区的邮局:生产者把信(消息)塞进邮箱(队列),消费者定时来取信、读信、回信(处理)。可现实里,这个邮局常被当成“一次性快递柜”用——信投进去,就默认对方一定准时拆、一定读懂、一定回执。没人管信纸被雨水泡烂(网络抖动)、信封被狗啃掉(序列化失败)、收件人搬家没更新地址(消费者宕机)、甚至同一封信被投了三遍(重复投递)。这些,就是MQ异常。而“不处理”,意味着连信箱门都不装锁,更别说配个监控摄像头和报警器。

我见过最典型的场景,是一家做电商订单履约的外包团队。他们用RabbitMQ做库存扣减通知,但代码里连try-catch都懒得套——“反正消息发出去了,后面爱咋咋地”。结果一次数据库主从切换,消费者服务短暂不可用,327条库存扣减消息全部卡在队列里。等服务恢复,消息批量涌出,系统疯狂重试,最终导致同一笔订单被扣了17次库存,后台库存直接显示负数。客户投诉电话打爆,运维查日志看到满屏的ChannelClosedExceptionIOException,却找不到任何业务层面的兜底逻辑。这不是技术债,这是设计上的“主动放弃”。

这类问题之所以高频出现,核心在于认知错位:很多人把MQ当成了“传输工具”,而不是“协作契约”。它承载的不是字节流,而是业务语义的承诺。一条未确认的消息,代表一个未完成的业务动作;一次失败的消费,代表一个待修复的业务状态。忽略异常,等于单方面撕毁契约。而代价,从来不是日志里几行红字,而是客户流失、资损、SLA违约、以及你简历上那个永远擦不掉的“背锅侠”标签。

如果你正在写MQ相关代码,或者正在评审别人写的MQ代码,请立刻停下来,问自己三个问题:这条消息丢了,业务会怎样?这条消息重复了,业务会怎样?这条消息处理失败了,系统知道吗?如果答案模糊,那你的设计,已经站在了离谱的边缘。

2. MQ异常的四大真实战场与设计盲区

MQ异常绝非抽象概念,它有清晰的物理边界和发生场景。我把19年外包项目中踩过的坑,按发生位置归为四类战场。每一类,都对应着一套被普遍忽视的设计盲区。

2.1 生产端:发送即失联,连“已送达”都不求证

绝大多数“不处理异常”的起点,就在这里。开发者调用producer.send(),看着控制台打印出“消息已发送”,就心安理得去喝咖啡。殊不知,这行日志只代表消息成功进入客户端内存缓冲区,离真正抵达Broker(消息服务器)还隔着网络、序列化、权限校验三座大山。

  • 网络层异常:最常见的是java.net.SocketTimeoutExceptionjava.io.IOException: Broken pipe。比如Broker负载过高,TCP连接超时断开;或K8s集群内网策略变更,导致Producer Pod无法访问Broker Service。此时消息根本没发出,但代码里没有任何重试或降级逻辑。
  • 序列化异常com.fasterxml.jackson.databind.JsonMappingException。前端传来的JSON对象里有个字段是null,而Java DTO里该字段被@NotNull标注,序列化直接炸。消息连格式都没整明白,就被丢弃。
  • 权限/配置异常io.netty.handler.ssl.SslHandshakeException(SSL证书过期)、org.apache.kafka.common.errors.TopicAuthorizationException(Kafka Topic无写入权限)。这类错误往往在上线后才暴露,因为测试环境权限全开。

提示:Kafka Producer默认acks=1,只等Leader副本写入成功就返回,不保证ISR(同步副本集)完整。若此时Leader宕机且ISR为空,消息实际已丢失。真正的“至少一次”语义,必须设acks=all并配合retries > 0

2.2 网络链路:中间件的沉默,比报错更致命

MQ Broker本身是稳定的,但它的运行环境不是。异常常以“静默失败”形式存在,日志里没有ERROR,只有大量WARN或INFO,却被开发人员当作噪音过滤掉。

  • 连接池耗尽:RabbitMQ的Channel是轻量级连接,但数量有限。高并发下若未正确关闭(channel.close()),连接池迅速枯竭,新请求全部阻塞在waitForConfirmsOrDie(),最终触发java.util.concurrent.TimeoutException。表面看是超时,根因是资源泄漏。
  • 心跳超时:AMQP协议要求客户端定期发送heartbeat帧。若网络设备(如防火墙)静默丢弃心跳包,Broker会在heartbeat timeout后主动关闭连接,抛出com.rabbitmq.client.ShutdownSignalException。此时Producer并不知情,继续发消息,全部失败。
  • DNS解析异常java.net.UnknownHostException。微服务部署在K8s,Broker地址用Service Name(如rabbitmq.default.svc.cluster.local)。若CoreDNS故障,所有Producer瞬间失联。这种异常在本地开发环境几乎不会出现,却是生产环境高频故障源。

2.3 消费端:消费即成功,拒绝承认世界不完美

这是“离谱设计”的重灾区。消费逻辑写在handleMessage()里,外面连个try-catch都没有。一旦业务代码抛出NullPointerExceptionSQLException,MQ客户端默认行为是:将消息重新入队,无限重试。结果就是一条坏消息像病毒一样在队列里循环播放,拖垮整个消费者实例。

  • 业务逻辑异常java.lang.NumberFormatException(解析金额字符串失败)、org.springframework.dao.DataIntegrityViolationException(唯一键冲突)。这类异常本应由业务代码捕获并走补偿流程,而非交给MQ兜底。
  • 依赖服务异常feign.RetryableException(调用下游HTTP服务超时)、redis.clients.jedis.exceptions.JedisConnectionException(Redis连接池满)。消费者自身健康,但协作方挂了,消息必须暂停处理,而非盲目重试。
  • 幂等校验失败DuplicateKeyException。消息本身没问题,但业务表已存在相同主键记录。这恰恰证明消息是重复的,需要跳过而非重试。

注意:Kafka Consumer的enable.auto.commit=true是毒药。自动提交Offset意味着只要消息被拉取到Consumer内存,就视为“已处理”。哪怕后续业务逻辑崩溃,Offset也已前移,消息永久丢失。必须关掉自动提交,改为手动commitSync(),且只在业务逻辑彻底执行成功后调用。

2.4 Broker端:消息的“生死簿”,无人查阅

Broker日志是真相的最后防线,但90%的外包项目从未配置过日志采集与告警。RabbitMQ的rabbitmqctl list_queues能看到队列长度,但看不到unacknowledged消息为何堆积;Kafka的kafka-consumer-groups.sh --describe能看Offset,但看不出Lag是否因消费者处理慢还是Broker写入慢。

  • 磁盘空间不足:RabbitMQ默认将消息持久化到磁盘。若/var/lib/rabbitmq分区满,Broker会拒绝所有写入请求,抛出disk_failure警告。此时Producer发消息必失败,但错误码可能是PRECONDITION_FAILED,而非直观的DISK_FULL
  • 内存水位告警:Kafka Broker的log.dirs所在磁盘使用率超85%,会触发LogDirFailure,停止向该目录写入新分区。若所有Broker都触发,整个集群写入能力归零。
  • 队列TTL过期:RabbitMQ可为队列设置x-message-ttl。若消费者长期宕机,消息在队列中存活超时后被自动删除,日志只有一行message expired。业务方永远不知道这笔订单通知已石沉大海。

3. 从“离谱”到“可靠”:四层防御体系实操指南

靠祈祷和运气让MQ不出问题,不如亲手搭建一套防御体系。我给所有外包团队总结了一套“四层防御”方案,不依赖高级中间件,纯代码+基础配置即可落地,已在12个生产项目验证。

3.1 第一层:生产端防御——让发送变成“签收”

核心原则:发送不是终点,收到Broker确认才是起点。以Spring Boot + RabbitMQ为例,关键配置与代码如下:

# application.yml spring: rabbitmq: # 关键!启用Publisher Confirm机制 publisher-confirm-type: correlated # 启用Publisher Returns,捕获路由失败 publisher-returns: true template: # 设置mandatory=true,路由失败时触发returns回调 mandatory: true
@Component public class OrderMessageSender { @Autowired private RabbitTemplate rabbitTemplate; // 发送订单创建消息 public void sendOrderCreated(Order order) { Message message = MessageBuilder .withBody(JsonUtils.toJson(order).getBytes(StandardCharsets.UTF_8)) .setContentType(MessageProperties.CONTENT_TYPE_JSON) .setMessageId(UUID.randomUUID().toString()) .setDeliveryMode(MessageDeliveryMode.PERSISTENT) // 持久化 .build(); // 设置ConfirmCallback,监听Broker确认 rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> { if (ack) { log.info("消息[{}]已成功抵达Broker", correlationData.getId()); // 可在此处更新本地消息状态表为"已发送" updateMessageStatus(correlationData.getId(), "SENT"); } else { log.error("消息[{}]发送失败,原因:{}", correlationData.getId(), cause); // 触发本地重发或告警 handleSendFailure(correlationData.getId(), cause); } }); // 设置ReturnsCallback,监听路由失败(如Exchange不存在、RoutingKey无匹配Queue) rabbitTemplate.setReturnsCallback(returned -> { log.error("消息[{}]路由失败,Exchange:{},RoutingKey:{},ReplyCode:{},ReplyText:{}", returned.getMessage().getMessageProperties().getMessageId(), returned.getExchange(), returned.getRoutingKey(), returned.getReplyCode(), returned.getReplyText()); // 此时消息已被Broker丢弃,需人工介入或重发 alertRoutingFailure(returned); }); // 发送!correlationData用于关联Confirm回调 CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); rabbitTemplate.convertAndSend("order.exchange", "order.created", message, correlationData); } }

为什么这样设计?

  • publisher-confirm-type: correlated让每条消息都有唯一ID,Confirm回调能精准定位哪条失败,避免全局重试。
  • mandatory: true+ReturnsCallback捕获“消息发出去了但没找到Queue”的场景,这是ConfirmCallback无法覆盖的盲区。
  • setDeliveryMode(PERSISTENT)确保消息在Broker重启后不丢失,代价是性能略降,但订单类业务必须牺牲这点性能。

实操心得:我曾在一个项目中漏配mandatory: true,结果因RoutingKey拼写错误(order.creted),所有消息静默丢失。监控只看到Producer QPS飙升,却无任何ERROR日志。加上ReturnsCallback后,5分钟内就定位到问题。

3.2 第二层:网络链路防御——给连接装上“心跳监护仪”

网络异常的特征是“间歇性”,必须用主动探测打破沉默。我们不依赖Broker自带的心跳,而是构建应用层健康检查。

@Component public class RabbitMQHealthChecker { @Autowired private CachingConnectionFactory connectionFactory; // 定时任务,每30秒检查一次连接健康度 @Scheduled(fixedRate = 30000) public void checkConnection() { try { // 尝试获取一个新Channel并执行简单操作 Channel channel = connectionFactory.createConnection().createChannel(); // 发送一个空消息到一个专用健康检查Exchange channel.basicPublish("health.exchange", "health.check", null, "PING".getBytes(StandardCharsets.UTF_8)); channel.close(); log.debug("RabbitMQ连接健康检查通过"); } catch (Exception e) { log.error("RabbitMQ连接健康检查失败:{}", e.getMessage(), e); // 触发告警,并标记服务为不健康 triggerAlert("RabbitMQ连接异常"); markServiceUnhealthy(); } } }

同时,在application.yml中强化连接参数:

spring: rabbitmq: # 关键!缩短心跳间隔,及时发现断连 heartbeat: 10 # 连接超时,避免阻塞 connection-timeout: 5000 # 通道缓存,防止Channel泄漏 cache: channel: size: 20 checkout-timeout: 10000

为什么心跳设为10秒?
RabbitMQ默认心跳是60秒。这意味着网络中断后,Broker要等60秒才发现客户端失联,期间Producer可能持续发消息失败。10秒心跳能让断连在15秒内被感知(3次心跳超时),大幅缩短故障窗口。实测某次云厂商网络抖动,60秒心跳导致127条消息积压;10秒心跳下,仅3条积压,且全部被ConfirmCallback捕获重发。

3.3 第三层:消费端防御——让失败成为可管理的“事件”

消费端的核心是拒绝无限重试,拥抱可控失败。以Kafka为例,采用“死信队列+手动提交+业务补偿”三件套:

@Component public class OrderConsumer { @KafkaListener( topics = "order.created", groupId = "order-group", // 关键!禁用自动提交 properties = { "enable.auto.commit=false", "max.poll.interval.ms=300000" // 延长拉取间隔,避免因处理慢被踢出Group } ) public void onOrderCreated(ConsumerRecord<String, String> record, Acknowledgment ack, KafkaOperations kafkaTemplate) { try { Order order = JsonUtils.fromJson(record.value(), Order.class); // 1. 幂等校验:查本地消息表,若已处理则直接ACK if (isMessageProcessed(record.headers().lastHeader("message-id").value())) { ack.acknowledge(); return; } // 2. 执行核心业务逻辑 inventoryService.deductStock(order.getItemId(), order.getQuantity()); // 3. 更新消息状态表,标记为"已处理" updateMessageStatus(record.headers().lastHeader("message-id").value(), "PROCESSED"); // 4. 手动提交Offset ack.acknowledge(); } catch (Exception e) { log.error("消费消息[{}]失败,原因:{}", record.key(), e.getMessage(), e); // 关键!不重试,直接发往死信队列 sendToDlq(record, e, kafkaTemplate); // 手动提交当前Offset,跳过此条坏消息 ack.acknowledge(); } } private void sendToDlq(ConsumerRecord<String, String> record, Exception e, KafkaOperations kafkaTemplate) { // 构建死信消息,包含原始消息、错误堆栈、时间戳 DeadLetterMessage dlqMsg = new DeadLetterMessage(); dlqMsg.setOriginalTopic(record.topic()); dlqMsg.setOriginalPartition(record.partition()); dlqMsg.setOriginalOffset(record.offset()); dlqMsg.setOriginalValue(record.value()); dlqMsg.setErrorMessage(e.getMessage()); dlqMsg.setStackTrace(ExceptionUtils.getStackTrace(e)); dlqMsg.setCreatedAt(LocalDateTime.now()); kafkaTemplate.send("order.dlq", record.key(), JsonUtils.toJson(dlqMsg)); } }

死信队列(DLQ)的实操价值:

  • 它不是垃圾桶,而是“问题分析中心”。所有失败消息集中于此,可对接ELK做聚合分析,快速发现共性问题(如某类商品ID总导致库存扣减失败)。
  • 它解耦了故障处理。运维可随时暂停主消费组,单独消费DLQ进行人工干预或批量重放,不影响主业务流。
  • 我们曾用DLQ发现一个隐藏Bug:某供应商提供的商品ID含不可见Unicode字符,导致MySQL唯一索引冲突。若用无限重试,系统会一直卡在这个ID上。

3.4 第四层:Broker端防御——让运维从“救火员”变“气象预报员”

防御的终点是可观测性。必须让Broker的状态,像天气预报一样清晰可见。以RabbitMQ为例,我们用Prometheus+Grafana构建监控:

# prometheus.yml 配置RabbitMQ Exporter scrape_configs: - job_name: 'rabbitmq' static_configs: - targets: ['rabbitmq-exporter:9419']

关键监控指标与阈值(已在生产环境验证):

指标Prometheus查询语句危险阈值说明
队列堆积rabbitmq_queue_messages_ready{queue=~"order.*"} > 1000>1000条表明消费者处理能力不足或下游服务异常
未确认消息rabbitmq_queue_messages_unacknowledged{queue=~"order.*"} > 50>50条消费者可能宕机或处理过慢,需立即检查
连接数rabbitmq_connections > 500>500可能存在连接泄漏,需检查Producer代码
磁盘使用率rabbitmq_disk_free_percent < 20<20%磁盘即将写满,Broker将拒绝写入

告警规则示例(Alertmanager):

- name: rabbitmq-alerts rules: - alert: RabbitMQQueueBacklogHigh expr: rabbitmq_queue_messages_ready{queue=~"order.*"} > 1000 for: 2m labels: severity: critical annotations: summary: "RabbitMQ队列{{ $labels.queue }}堆积严重" description: "当前堆积{{ $value }}条,超过阈值1000条,可能影响订单履约"

为什么监控unacknowledgedready更重要?
ready是等待被消费的消息,unacknowledged是已被消费者拉取但尚未确认的消息。后者直接反映消费者健康度。曾有一个项目,ready只有200条,但unacknowledged高达3000条,排查发现消费者Pod内存溢出,GC频繁,处理能力归零。若只监控ready,会误判为流量正常。

4. 外包团队的生存法则:如何把“离谱设计”变成甲方眼中的“专业交付”

在外包行业,“离谱设计”常源于甲方需求模糊、工期压缩、技术决策权缺失。但资深外包工程师的真正价值,不是照单全收,而是用专业能力把风险转化为信任。以下是我在19年实战中沉淀的四条生存法则:

4.1 法则一:用“成本可视化”代替“技术说教”

甲方项目经理不懂acks=allacks=1的区别,但他懂“钱”。当提出增加死信队列时,不要讲Kafka原理,而是给出一张对比表:

方案月均资损风险故障平均恢复时间开发工时运维复杂度
当前方案(无限重试)¥23,000+(历史数据)47分钟0小时低(但隐患大)
推荐方案(DLQ+手动提交)¥0(可拦截99%资损)3分钟(DLQ可批量重放)16小时中(需配置监控)

这张表贴在项目周报里,比10页技术文档更有说服力。我曾用此法,让一个坚持“先上线再优化”的甲方,在UAT阶段就批准了DLQ改造预算。

4.2 法则二:把“异常处理”拆成可验收的原子需求

甲方需求文档里写“订单消息要可靠”,这等于没说。必须把它拆解为可测试、可验收的原子项:

  • [ ] 消息发送失败时,系统记录错误日志并告警(提供日志截图模板)
  • [ ] 消费者处理失败时,消息进入order.dlq队列,且原消息不再重试(提供Kafka命令验证步骤)
  • [ ] 当order.dlq中消息数>10条时,触发企业微信告警(提供告警截图)
  • [ ] 所有消息携带唯一message-id头,且在DLQ消息中完整保留(提供消息体JSON示例)

每个原子项对应一个测试用例,验收时逐条勾选。这避免了“异常处理已做”的模糊交付,也保护了外包团队不背锅。

4.3 法则三:建立“异常知识库”,让经验可复用

每个项目都会遇到新异常,但90%的异常类型是重复的。我强制团队维护一个Markdown格式的《MQ异常知识库》,存于GitLab Wiki,结构如下:

/exception-knowledge-base/ ├── 01-network/ │ ├── dns-resolution-failed.md # DNS解析失败 │ └── firewall-block-heartbeat.md # 防火墙阻断心跳 ├── 02-broker/ │ ├── disk-full.md # 磁盘满 │ └── memory-pressure.md # 内存压力 ├── 03-consumer/ │ ├── duplicate-key-exception.md # 唯一键冲突 │ └── downstream-timeout.md # 下游超时 └── 04-solution-patterns/ ├── dlq-handling-guide.md # DLQ处理指南 └── idempotent-design-patterns.md # 幂等设计模式

每篇文档包含:现象、日志特征、根因分析、临时规避方案、长期修复方案、相关代码片段。新人入职第一周任务,就是阅读并复现3个案例。这让我们团队的MQ问题平均解决时间,从8.2小时降至1.7小时。

4.4 法则四:用“灰度发布”降低甲方对“改动”的恐惧

甲方最怕“改了好的,坏了更好的”。对于MQ改造,我们从不全量上线。标准灰度路径:

  1. 第一阶段(1%流量):只开启ConfirmCallback日志记录,不触发重试,观察一周确认无性能影响;
  2. 第二阶段(10%流量):开启DLQ,但DLQ消息只存入数据库,不触发告警,验证消息流转正确性;
  3. 第三阶段(50%流量):DLQ消息触发告警,但人工确认后才重放;
  4. 第四阶段(100%流量):DLQ消息自动重放,同时开启全链路监控。

每次灰度升级,都向甲方提供《灰度报告》,包含:灰度范围、监控指标对比图、异常捕获数量、业务影响评估。这份报告,比任何PPT都更能建立信任。

5. 真实故障复盘:一次“不处理异常”引发的连锁崩塌

2023年Q3,我接手一个紧急救援项目:某银行信用卡分期系统的MQ消息大量丢失,导致数万用户无法查询分期详情,客诉量单日破千。甲方给的原始描述是:“RabbitMQ好像不太稳定”。下面是我48小时内完成的故障复盘,它完美诠释了“不处理异常”的多米诺骨牌效应。

5.1 故障现象与初步排查

  • 现象

    • credit分期.detail队列ready消息持续增长,峰值达12,000+;
    • 消费者Pod CPU 100%,但unacknowledged消息为0;
    • 监控显示Producer发送成功率从99.99%骤降至32%。
  • 初步排查
    查看Producer日志,满屏java.io.IOException: Connection reset
    查看Broker日志,发现大量closing AMQP connection
    登录Broker服务器,df -h显示/var/lib/rabbitmq分区使用率98%。

结论:磁盘满导致Broker拒绝写入,Producer连接被强制关闭。

5.2 深度根因分析:四个“不处理”叠加的灾难

  1. 生产端不处理磁盘满异常
    Producer代码中无ConfirmCallback,磁盘满导致的PRECONDITION_FAILED错误被静默吞掉,日志只有一行Failed to send message,无堆栈,无上下文。

  2. 网络链路不处理连接异常
    heartbeat配置为60秒,Broker在磁盘满后主动断连,但Producer未感知,持续重试,加剧连接风暴。

  3. 消费端不处理Broker异常
    消费者代码中@RabbitListener未配置concurrentConsumersmaxConcurrentConsumers,当Broker不稳定时,消费者线程池被占满,无法处理新消息,unacknowledged消息为0是因为根本没拉取消息。

  4. Broker端不监控磁盘水位
    无任何磁盘使用率告警,运维直到客户投诉才登录服务器查看。

5.3 修复与加固措施

  • 紧急修复(2小时内)
    清理/var/lib/rabbitmq/mnesia/下过期日志,释放空间;
    临时扩容磁盘;
    重启Broker,恢复消息流转。

  • 长期加固(48小时内交付)

    • 在Producer中强制接入ConfirmCallback,并将PRECONDITION_FAILED错误映射为明确的DiskFullException,触发熔断;
    • heartbeat从60秒改为10秒,并增加ConnectionHealthChecker
    • 重构消费者,配置concurrentConsumers=5maxConcurrentConsumers=10,并添加@RetryableTopic注解实现指数退避重试;
    • 在Prometheus中新增rabbitmq_disk_free_percent告警,阈值设为25%,提前3小时预警。

5.4 教训与量化收益

  • 教训
    “不处理异常”不是省事,而是把小问题封装成定时炸弹。磁盘满本是运维常规巡检项,但因缺乏应用层反馈,演变为业务级事故。

  • 收益

    • 故障MTTR(平均修复时间)从47小时降至22分钟;
    • 同类磁盘问题在后续6个月零复发;
    • 甲方将该项目的MQ规范,作为全行中间件建设标准推广。

这个案例里没有高深算法,只有对基础异常的敬畏。所谓资深,不过是把别人忽略的catch块,写得足够扎实。

6. 给所有开发者的最后一句提醒

“外包19年,我见到的第15个离谱设计”,这个标题刺眼,但刺眼的不是数字,而是“第15个”背后的麻木。当一个团队把“不处理MQ异常”当成默认选项,它暴露的不是技术能力的欠缺,而是工程素养的溃散。

MQ异常不是洪水猛兽,它是分布式系统固有的涟漪。每一次网络抖动、每一次服务重启、每一次配置失误,都会在消息流中激起一圈波纹。你可以选择视而不见,任其扩散成海啸;也可以选择在涟漪初起时,用一个try-catch、一行监控、一次手动提交,把它稳稳接住。

我见过太多项目,上线时风平浪静,半年后突然崩塌,根源都在那些被注释掉的异常处理代码里。它们像埋在代码里的微型地雷,平时安静,直到某个特定条件触发——比如数据库主从切换、云厂商网络调整、甚至一个周末的磁盘清理脚本。

所以,下次当你敲下rabbitTemplate.convertAndSend(),或者kafkaTemplate.send(),请花3秒钟,问问自己:如果这行代码执行失败,我的业务会怎样?如果答案是“不知道”或“应该不会失败”,那么,你已经在书写下一个“离谱设计”的开头。

真正的专业主义,不在于写出多炫酷的算法,而在于对每一个Exception保持谦卑。它不声不响,但它记得你每一次的敷衍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询