
做后端开发的十有八九都碰到过这类需求订单提交后三十分钟内未支付就自动关闭、优惠券到期前二十四小时提醒用户、直播开播前十五分钟推送通知。这些场景的本质都是同一件事——“延迟消息”。我刚入行那会儿处理这种需求第一反应是丢一张表进数据库起个定时任务每分钟扫一遍。数据量小的时候还行等到订单量一天几十万数据库轮询的延迟、压力、误扫全来了这才老老实实回去研究RabbitMQ的延迟消息方案。写这一章之前我特意把常见的三种实现路径都翻出来做了对比又从零开始搭了一套完整流程验证。这篇内容就把我对延迟消息的理解、方案选型的思路、具体落地代码和排查记录一次性整理出来。不管是刚接触RabbitMQ的新人还是准备把延迟消息接进生产项目的开发者照着走都能减少不少弯路。1. 延迟消息的本质是什么解决的又是什么问题RabbitMQ本身并不是一个“定时器”它是一个消息中间件。消息进队列之后消费者能拉走就立刻拉走能不能让消息“等一等再被消费”这就是延迟消息要回答的问题。延迟消息并不是RabbitMQ的消息类型而是一种使用方式本质上是利用消息的TTL存活时间或者插件的路由机制让消息在队列里“睡”一段时间再被投递给消费者。1.1 先看几个最常见的业务场景延迟消息在业务系统里最常见的落点我总结下来基本是四类订单超时自动关闭最常见用户在提交订单后30分钟未支付系统自动把订单状态改成已取消顺便释放库存。支付结果延迟通知第三方支付回调偶尔会晚系统需要预留一段缓冲时间再发起查询。定时提醒或通知比如预约提醒、会员到期提醒、活动开始前提醒。失败重试的退避策略外部接口调用失败隔5分钟、30分钟、2小时分别重试这本质就是一系列延迟消息。这四类场景的共同特征是不需要用户实时触发也不用系统立刻处理但必须在“某个未来时间点”被自动处理。如果没有延迟消息就只能在处理时机上做文章。1.2 为什么不用定时任务轮询数据库早期项目里很多人用定时任务扫表比如每分钟执行一次SQL查所有创建时间超过30分钟且状态为待支付的订单批量更新。这个方案在小规模系统里完全能跑但有几个绕不开的问题扫描频率和实时性矛盾。每分钟扫一次最坏情况下订单会被延迟近一分钟才被处理。如果把频率调到10秒一次数据库压力成倍增长。订单表数据量大的时候全表扫描性能极差必须靠索引和分页去优化越优化代码越复杂。多个服务实例部署时每台机器的定时任务都会跑一遍必须引入分布式锁来保证只有一个实例在执行复杂度直接上升一个台阶。处理失败不好追踪。每一批扫出来的订单都需要记录扫描状态、处理状态否则重复处理或者漏处理都很难排查。这些痛点恰恰是消息中间件擅长解决的。把“扫描数据库”换成“发一条延迟消息”处理逻辑就变成了“到时间自动触发的消息消费”系统结构清晰了扩展性也好了。1.3 延迟消息在RabbitMQ里的实现思路RabbitMQ实现延迟消息归根结底是回答两个问题让消息在哪里等待、等待时间到了之后怎么进入真正的消费队列。围绕这两个问题社区和官方给出了不同思路有的利用死信队列有的利用插件各有优缺点。我在下一节把几种方案放在一起做个对比看完基本就能判断自己的项目该选哪种。2. 延迟消息的三种主流实现方案对比我在选型之前把网上能搜到的方案都过了一遍真正在实际项目中用得上的主要是三种基于死信队列TTL、基于延迟插件、以及基于优先级队列的近似方案。其中第三种限制太多基本只适合特定场景。下面重点拆前两种。2.1 方案一死信队列 消息TTL这个方案不依赖任何额外组件用的是RabbitMQ原生的两个机制。第一个机制是消息TTL。在RabbitMQ里消息可以设置过期时间到达过期时间后如果消息没有被消费就会被判定为死信。第二个机制是死信交换机Dead Letter Exchange。队列可以绑定一个死信交换机队列中的消息变成死信后会被重新投递到死信交换机再由交换机路由到指定的死信队列。延迟消息的实现方式就是消费者不直接监听业务队列而是监听死信队列。生产者把消息发到业务队列并设置一个TTL比如30分钟。消息在业务队列里待30分钟超时后进入死信交换机被转发到死信队列消费者从死信队列取出消息处理。整个过程业务队列只是个“等待区”真正的消费发生在死信队列。2.2 方案二RabbitMQ延迟插件RabbitMQ官方也意识到延迟消息是个高频需求所以出了一个专门的插件叫rabbitmq_delayed_message_exchange。这个插件引入了一种新的交换机类型x-delayed-message。这个交换机的原理可以理解为交换机本身带了一个“临时仓库”。生产者发送消息时在消息属性里带上一个x-delay参数表示延迟多少毫秒。消息到达交换机后交换机不会立刻路由到队列而是先把消息存起来等延迟时间一到再路由到目标队列。对消费者来说它感知不到任何差别就是监听起来一个普通队列。与死信队列方案相比延迟插件最大的区别在于延迟时间由生产者动态指定每条消息可以有不同的延迟时间这在实际业务里非常关键。2.3 三种方案的横向对比对比项死信队列TTL延迟插件优先级队列近似方案额外依赖无RabbitMQ原生支持需要安装插件无原生支持每条消息独立延迟时间受队列统一TTL限制难以实现支持生产者任意指定不支持只能近似队列中消息顺序存在队头阻塞问题无队头阻塞本身就不保证顺序运维复杂度低但要维护额外交换机和队列中等需插件升级配合低但功能弱适合场景同一延迟时间的大量消息动态延迟时间的通用场景极少用不建议我在对比的过程中发现网上对死信队列方案的评价容易走极端。有的说它能用有的说千万别用。其实这两种说法都对关键看场景。如果业务里所有消息的延迟时间都是统一的比如都是30分钟超时关单用死信队列完全没问题。但如果一条业务消息要在5分钟后提醒一次、30分钟后提醒一次、2小时后提醒一次死信队列方案就会非常麻烦。延迟插件最大的坑在于它在RabbitMQ 4.x的早期版本里不是内置的升级RabbitMQ版本时容易遗忘。而且插件在集群环境里必须每个节点都安装漏一个节点就会出现诡异的路由异常。这两点我在后面实操部分会详细讲到。2.4 为什么我最终选择延迟插件我在模拟项目X里最终选了延迟插件方案核心原因是业务里有大量“不同消息不同延迟时间”的需求。比如模拟项目里同时存在15分钟后发送通知、30分钟后订单关单、2小时后二次提醒这些延迟时间各不相同。如果都用死信队列需要为每个延迟时间建一套独立的交换机队列队列数量翻倍维护成本太高。延迟插件让生产者在发送消息时直接指定延迟时间一套交换机就能支撑所有延迟需求代码也直观这条消息延迟5分钟那条消息延迟2小时一目了然。3. 延迟插件方案落地实操选定方案之后就进入具体落地阶段。这一节我把从安装插件到消息收发验证的完整过程记录下来包括我在实际环境中遇到的一些细节问题。3.1 安装延迟插件版本匹配是第一道坎延迟插件的全称是rabbitmq_delayed_message_exchange它不会随着RabbitMQ一起默认安装需要单独下载。下载时要特别关注版本匹配问题插件的版本号规则和RabbitMQ主版本不完全一致必须参考官方发布的版本兼容说明。我这次用的是RabbitMQ 3.13.x版本对应的插件文件是rabbitmq_delayed_message_exchange-3.13.x.ez。下载完成后把.ez文件放到RabbitMQ安装目录的plugins目录下。然后执行rabbitmq-plugins enable rabbitmq_delayed_message_exchange执行完命令可以用以下两个命令确认插件状态rabbitmq-plugins list | grep delayed rabbitmq-diagnostics -q ping如果插件状态显示为e[E]说明已启用。这里我要特别提醒一句在集群环境里必须在每个集群节点上都执行插件启用操作只在一个节点上启用是无效的。我之前在双节点集群上踩过这个坑头一天单机测试好好的第二天接入集群后消息迟迟不进队列排查了半天才发现是另外那个节点没装插件。3.2 声明交换机指定延迟消息类型插件启用后RabbitMQ新增了一种交换机类型在代码里声明时名称就是x-delayed-message。我在Spring Boot项目里通常写一个配置类专门负责交换机和队列的声明。声明延迟交换机时有一个关键参数x-delayed-type它表示消息延迟结束之后交换机以哪种类型去路由。这个参数需要特别注意如果不设置消息到期后会没有路由目标。我把常用写法贴出来Configuration public class DelayRabbitConfig { public static final String DELAY_EXCHANGE delay.exchange; public static final String DELAY_QUEUE delay.queue; public static final String DELAY_ROUTING_KEY delay.routing.key; Bean public CustomExchange delayExchange() { MapString, Object args new HashMap(); args.put(x-delayed-type, direct); return new CustomExchange(DELAY_EXCHANGE, x-delayed-message, true, false, args); } Bean public Queue delayQueue() { return new Queue(DELAY_QUEUE, true); } Bean public Binding delayBinding() { return BindingBuilder.bind(delayQueue()).to(delayExchange()).with(DELAY_ROUTING_KEY).noargs(); } }这里CustomExchange是Spring Amqp提供的类前两个参数分别是交换机名称和类型后面的durable参数表示持久化。x-delayed-type选择direct意味着延迟时间结束后消息按照routing key精确匹配规则投递到队列。3.3 生产者发送消息x-delay头是关键生产者的代码和发送普通消息差别不大唯一的区别是需要在消息属性中带上x-delay参数单位是毫秒。下面这段是我在项目中实际使用的生产者示例Component public class DelayMessageProducer { Autowired private RabbitTemplate rabbitTemplate; public void sendDelayMessage(String message, long delayMillis) { MessagePostProcessor messagePostProcessor new MessagePostProcessor() { Override public Message postProcessMessage(Message message) { message.getMessageProperties().setDelay(Long.valueOf(delayMillis)); return message; } }; rabbitTemplate.convertAndSend( DelayRabbitConfig.DELAY_EXCHANGE, DelayRabbitConfig.DELAY_ROUTING_KEY, message, messagePostProcessor); } }这里要注意setDelay是Spring AMQP封装的方法底层会把Delay设置到消息属性x-delay中。在原生RabbitMQ客户端里对应的写法是AMQP.BasicProperties properties new AMQP.BasicProperties.Builder() .expiration(30000) // 注意这不是x-delay .build();很多刚上手的开发者会把消息过期时间expiration和延迟参数x-delay搞混。在这个插件方案里延迟参数用的是headers里的x-delay而expiration是消息TTL。如果在延迟交换机方案中误设了expiration而没有设置x-delay消息会立刻被路由完全达不到延迟效果。调用生产者时只需要指定延迟时间例如延迟30分钟就是1800000毫秒delayMessageProducer.sendDelayMessage(订单未支付即将关闭, 1800000L);3.4 消费者监听队列与普通队列完全一致延迟插件有一个很友好的特性消费者不需要做任何特殊处理监听的还是普通队列。因为消息被延迟是在交换机层面完成的一旦延迟时间到达交换机把消息投递给队列后消息对消费者来说就是一个普通消息。我习惯单独写一个延迟消息消费者Component public class DelayMessageConsumer { Slf4j RabbitListener(queues DelayRabbitConfig.DELAY_QUEUE) public void handleDelayMessage(String content, Message message) { long delayedTime System.currentTimeMillis() - message.getMessageProperties().getTimestamp().getTime(); log.info(收到延迟消息: {}, 实际延迟时间: {}ms, content, delayedTime); // 这里写真正的业务处理逻辑 } }实测下来这种方案的延迟精度可以控制在毫秒级以内和定时任务巧妙的“每分钟扫一次”相比实时性完全不在一个级别。3.5 验证延迟是否生效的几种调试手段代码写完后我习惯先做一轮本地点对点验证。最简单的验证方式是用RabbitMQ管理界面手动发送一条测试消息把headers里的x-delay设置成5000也就是5秒。消息发出去后如果一切正常5秒内消费者不会收到消息5秒后消费者抛出处理日志。如果发现消息马上就被消费或者消息卡在队列里一直不消费往往说明问题出在交换机类型或者x-delay参数上。我在第一次测试时就踩过坑交换机类型写成了direct没有用x-delayed-message结果消息直接被路由进队列延迟完全没生效。这类问题从界面上看特别容易忽略因为消息确实进了队列也显示被消费了只是时间不对。4. 延迟消息在订单超时关闭场景中的完整落地案例延迟消息方案落地后接下来要解决的就是如何把它和业务真实结合起来。我以最典型的“订单30分钟未支付自动关闭”为案例展示从消息投递到业务处理的完整闭环。一开始我们团队方案评审时有成员提过能不能在创建订单时把订单号丢到一个Redis的过期key里靠Redis过期事件触发关单。这个方案可以跑但Redis过期事件不保证及时而且丢消息后没有补偿机制。所以我最终坚持用延迟消息做主体这种场景下延迟消息的优势是很明显的。4.1 整体链路设计整个链路由四个环节组成用户创建订单后业务服务同时发送一条延迟消息消息在延迟交换机中等待30分钟时间到达后消息进入业务队列消费者收到消息后查询订单当前状态如果是待支付状态就执行关单操作释放库存。这里要特别注意一点消费者收到消息后一定不能无条件执行关单必须反查订单状态。原因很简单用户可能在第25分钟完成了支付消息还没到期到第30分钟消息来了如果消费者直接关单就会把已经支付的订单错误地关闭。所以消费端必须做状态校验。4.2 创建订单时发送延迟消息订单创建成功后的业务代码大致逻辑是public Order createOrder(OrderCreateRequest request) { // 1. 创建订单状态为待支付 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setStatus(OrderStatus.PENDING_PAYMENT); order.setCreateTime(LocalDateTime.now()); orderMapper.insert(order); // 2. 发送延迟消息30分钟后检查订单状态 OrderCloseMessage message new OrderCloseMessage(); message.setOrderNo(order.getOrderNo()); delayMessageProducer.sendDelayMessage(JSON.toJSONString(message), 30 * 60 * 1000L); return order; }这条延迟消息带的是订单号消费者拿到订单号之后再去查库。这里有一个设计细节消息体尽量只携带业务标识订单号不要携带整个订单对象。如果消息里塞了订单创建时的全部字段等30分钟后消费时订单状态可能已经变化了消息里的字段就是脏数据。只带订单号强制消费者以当前数据库为准。4.3 消费者处理状态校验 关闭订单 库存释放消费者收到消息后处理流程拆成三步。第一步解析订单号第二步查询订单当前状态第三步根据状态分支处理。RabbitListener(queues DelayRabbitConfig.DELAY_QUEUE) public void handleOrderClose(String content) { OrderCloseMessage message JSON.parseObject(content, OrderCloseMessage.class); String orderNo message.getOrderNo(); Order order orderMapper.selectByOrderNo(orderNo); // 订单不存在直接忽略 if (order null) { log.warn(关单消息处理失败订单不存在: {}, orderNo); return; } // 已支付忽略已关闭忽略 if (!OrderStatus.PENDING_PAYMENT.equals(order.getStatus())) { log.info(订单状态已变化跳过关单: {}, 当前状态: {}, orderNo, order.getStatus()); return; } // 执行关单操作更新状态 释放库存 int rows orderMapper.compareAndUpdateStatus(orderNo, OrderStatus.PENDING_PAYMENT, OrderStatus.CLOSED); if (rows 1) { stockService.releaseStock(order.getSkuId(), order.getQuantity()); log.info(订单超时已关闭: {}, orderNo); } else { log.warn(订单状态并发变更关单失败: {}, orderNo); } }这里我用了一个compareAndUpdateStatus方法本质就是SQL里带条件更新UPDATE t_order SET status CLOSED, close_time NOW() WHERE order_no #{orderNo} AND status PENDING_PAYMENT通过更新影响行数判断是否成功这是最朴素的乐观锁写法。为什么不用单纯的状态判断再加更新因为极端情况下用户可能在消费者查询状态之后、执行更新之前完成了支付如果不加条件更新就会出现“查询时待支付更新时却把已支付订单关闭”的竞态问题。带状态条件的更新SQL把校验和更新合并为原子操作从根上避免了并发问题。4.4 幂等与消息补偿延迟消息发出去之后如果RabbitMQ服务器重启、网络抖动、消费者异常消息可能丢失或者被重复消费。这里有两个重点。第一消费者在处理关单时不要直接操作数据库要利用条件更新天然保证幂等。上面例子中如果同一个订单的关单消息被消费了两次第一次执行后订单状态已经变成CLOSED第二次进来时条件更新影响行数为0直接跳过不会出现重复关单、重复释放库存的情况。第二延迟消息本身可能因为交换机持久化配置不当而丢失。前面配置里我把交换机和队列都设置成了持久化消息发送时也应该设置消息持久化RabbitTemplate默认deliveryMode是PERSISTENT一般不用额外配置。但是如果业务对最终一致性要求很高我会额外加一层补偿表。比如单独建一张消息发送记录表发送延迟消息前先插入一条记录消费者处理成功后标记完成再配一个定时任务扫一下这张表里长时间未标记完成的数据重新发送。这一层属于兜底方案不是所有项目都需要数据一致性要求高的支付、订单类场景建议加上。5. 延迟消息的常见问题与排查实录延迟消息在运行过程中出过的问题我踩过的和帮别人排过的加起来不少。这一节把典型的几类问题整理成速查表然后再挑几个典型场景展开讲讲排查思路。5.1 问题速查表现象可能原因排查方向消息发出后立刻被消费交换机类型没有配置成x-delayed-message查看交换机类型消息一直等到过期还没被消费插件未启用或版本不匹配检查插件状态和RabbitMQ版本兼容性消息延迟时间不稳定偶尔偏长队列设置了过期时间与x-delay语义冲突检查队列参数是否误设了x-message-ttl集群中消息路由异常某些节点未启用插件逐节点检查插件启用状态消费者重复收到同一条消息消费者处理逻辑抛异常消息被重新requeue查看消费者日志处理消息不ack延迟时间超过一到两天插件的定时器精度受系统时钟影响检查服务器时间同步5.2 消息“立刻被消费”的排查思路这种问题最简单也最容易犯。我遇到过一个真实案例开发环境把交换机类型写成了direct但队列和消费者一切正常消息发出去之后消费者立刻就能收到。当时从界面上看消息完全正常没有任何报错唯一不正常的只是延迟效果没生效。排查方式很简单在RabbitMQ管理界面找到交换机查看Type列。如果是x-delayed-message说明正确如果是direct、topic等普通类型延迟逻辑肯定不存在。需要重建交换机。还有一个隐蔽情况代码里用了两个交换机一个延迟交换机一个普通交换机路由key配错了消息发到了普通交换机上。这种情况从生产者代码很难看出来建议在管理界面的Exchange页面点进对应交换机看Messages下是否有消息路由记录。5.3 消息延迟时间到了却不进队列这个问题比上面那个复杂插件启用状态和版本兼容性都要查。RabbitMQ管理界面可以通过rabbitmq-plugins list命令确认插件启用状态如果插件开关是开启的但行为异常就要看版本兼容性了。插件版本和RabbitMQ版本不兼容通常不会在启动时报错而是运行期行为怪异。比如消息一直在交换机缓存里不路由或者时通时断。这类问题很难通过代码定位建议升级RabbitMQ时把延迟插件同步升级不要只升中间件本身。另外如果使用的是集群模式每个节点都要执行插件启用命令。集群模式下节点之间插件状态不同步这是我印象最深的一个坑某公司在三个节点的集群上部署只有一个节点装了插件结果表现为消息在三段不同的时间点分别被消费非常诡异实际上是因为消息被路由到了不同节点上。5.4 消息重复消费的排查延迟消息场景下消息重复消费通常不是延迟机制本身的问题而是消费端处理逻辑没做好。常见路径有三种消费者处理消息时抛异常RabbitMQ根据配置把消息重新入队导致下一次又被消费消费者处理成功但还没有手动ack消费者进程挂掉消息被重新投递消费者代码里没有做幂等同一个订单被关单两次。对于前两种解决思路是保证消费逻辑的幂等性。就像前面订单案例里用条件更新实现的那样处理逻辑天然幂等重复消费也不会产生副作用。对于ack机制我的建议是尽量使用手动确认模式并且只在业务处理成功后确认。不要使用自动确认模式自动确认是在消息发给消费者之后就立即确认如果消费者处理到一半进程崩溃这条消息就丢了。6. 延迟消息的实操建议与踩坑心得代码跑通了、案例也落地了最后再分享一些我在实际项目中总结出来的经验。这些内容不属于标准文档但往往对稳定运行更重要。6.1 时间精度不是越高越好延迟插件的延迟时间理论上可以精确到毫秒但业务上一般不建议用到这么细。结合我自己的经验30秒以内的延迟需求更适合直接用普通队列配合消费者里的sleep或者调度框架处理。延迟消息的价值在于分钟级到小时级的“长延迟”场景在这个区间内它的优势才足够明显。另外如果要模拟“秒级延迟”来测试建议把延迟时间设为5秒以上。太短的延迟时间会让消息堆积在交换机中你可能还没来得及打开管理界面看消息就已经被消费了不利于验证。6.2 队列消费能力要与延迟消息积压匹配延迟消息天然会造成消息积压。比如你在一分钟内发送了1000条30分钟后关单的消息这些消息会全部堆积在交换机中30分钟后集中涌入队列。如果消费者处理能力不足或者下游数据库执行更新操作太慢就会形成新的性能瓶颈。我在项目里遇到过一次很典型的线上问题某个整点活动结束后大量用户同时下单延迟消息在30分钟后的同一秒集中到期消费者瞬间处理不过来消息堆积了几万条处理延迟越来越久。后续我把消费者的prefetch值调小同时把订单关闭这样轻量级的操作单独拆到一个队列里处理保证了关单任务不和其他消息互相影响。6.3 给延迟消息加业务标识方便链路追踪生产环境的延迟消息链路从发消息到消费可能要跨越30分钟甚至更久。中间出现问题时如果没有唯一的业务标识很难快速定位到底哪条消息出了问题。因此我在消息体里除了业务标识字段还会加一个messageId用一个全局序列生成器生成。日志输出时同时打印messageId和业务字段。排查问题时可以通过RabbitMQ管理界面搜索队列中的消息内容也可以到日志系统按messageId检索完整链路。6.4 测试环境如何模拟长延迟30分钟的延迟消息在联调环境里总不能真的等30分钟。我的做法是把延迟时间参数化通过配置中心控制。生产环境延迟时间配置30分钟测试环境配置5秒代码里只需要读取一个配置项。这样既不用修改代码又能快速联调。还有一个技巧引入一个Mock交换机测试环境下生产者直接把消息发到普通交换机跳过延迟环节。这样做的好处是测试环境可以即时消费消息不需要等待延迟时间方便快速验证消费者逻辑。但要注意这只是测试手段不要带到生产环境。6.5 监控指标要盯哪些延迟消息相关的监控指标我主要盯三个。一是交换机里当前滞留的消息数量这个指标在RabbitMQ管理界面的Exchange页可以看到。二是消费者消费消息的延迟峰值通过消息里的时间戳和消费时间对比得出如果延迟越来越久说明消费者处理能力跟不上。三是延迟消息的丢失率这个需要依赖业务层的补偿机制统计没有补偿机制的话就无法感知丢失。这三条指标配合上一套简单的告警基本能保证延迟消息链路在生产环境不出大问题。我见过很多项目延迟消息上线后无人维护等到线上订单超时没被关闭、用户投诉了才发现消息早就丢了那时候再去排查已经很被动。6.6 延迟消息的边界什么需求不适合用它延迟消息能解决一批问题但不是银弹。我举两个不适合的例子。第一个是秒杀系统的库存释放。秒杀场景需要极高精度的超时释放业务量巨大且瞬间并发集中延迟消息在毫秒级场景下的表现不如直接用内存定时器或专业的调度框架稳定。第二个是定时任务型的周期任务比如每天凌晨清理数据这种任务用分布式调度框架更直观没必要为了用延迟消息而用。我在项目里会做一个简单的判断如果这个“延迟”是单次的、事件驱动的、延迟时间不均匀的优先考虑延迟消息如果延迟是周期性的、日常任务型的、固定时间执行的优先考虑定时任务调度。两者相辅相成不是替代关系。做了几个项目下来我的真实感受是RabbitMQ延迟消息真正降低的是业务系统的结构复杂度。不用延迟消息的时候订单超时靠轮询提醒服务靠轮询重试机制也靠轮询整个系统遍布定时器维护起来相当痛苦。引入延迟消息后所有“未来某个时刻需要做点什么”的需求都收敛到一条消息和一个消费者上思路清晰了出问题的概率也小很多。如果你正好在处理类似的业务场景建议先花一天时间把插件方案通一遍再结合我这篇的排查经验去观察线上表现应该能比直接复制网上的代码段走得更稳。