Spring Boot 重新部署后 Redis 队列 Worker 持续报错怎么办?
结论
日志显示,EmailQueueWorker 仍在使用已经销毁的 LettuceConnectionFactory。应先检查重新部署时旧 Worker 是否退出,确保消费任务停止后再销毁它使用的 Redis 连接工厂。
仅凭现有堆栈,无法确定重新部署 OTP 服务为什么会影响邮件 Worker。即使两个应用使用同一个 Redis、部署在同一个 Tomcat 上,也不一定共用同一个 Java 连接工厂对象。还需确认 Worker 实际运行在哪个应用上下文中,以及连接工厂由谁创建和关闭。
日志说明了什么
原始场景中,OTP 服务使用 Redis,另一个应用使用 Redis 队列发送邮件。重新部署 OTP 服务后,邮件 Worker 开始重复记录异常。关键调用链是:
EmailQueueWorker.lambda$0(EmailQueueWorker.java:142)
DefaultListOperations.rightPop(...)
RedisTemplate.execute(...)
LettuceConnectionFactory.getConnection(...)
IllegalStateException: LettuceConnectionFactory was destroyed and cannot be used anymore
Worker 调用 rightPop 获取队列消息时,尝试通过 RedisTemplate 获取连接,但它引用的连接工厂已经被销毁。
这条异常反映的是应用内连接工厂的生命周期状态,不能直接据此判断 Redis 服务器宕机或网络故障。如果消费代码捕获异常后立即进入下一轮循环,就会反复访问同一个失效对象,迅速产生大量日志。由于没有提供 Worker 源码,需要检查 EmailQueueWorker.java:142 附近的循环和异常处理才能确认这一点。
先确认 Worker 属于哪个应用
可以按以下顺序检查:
- 查找
EmailQueueWorker、new Thread、Executors.newFixedThreadPool、Executors.newSingleThreadExecutor,以及提交消费任务的代码,确认 Worker 的创建位置,检查是否有手动创建但未关闭的线程池。 - 检查 OTP 应用是否也加载了邮件 Worker。如果邮件组件位于公共依赖中,组件扫描或自动配置可能使它同时出现在两个应用里。确认存在这种情况后,再调整扫描范围、配置条件或依赖关系。
- 核对关闭与报错的时间。在 Worker 启动、停止和应用上下文关闭时,记录应用标识、上下文 ID、线程名和连接工厂实例标识,判断报错来自旧部署还是新部署。
- 检查静态引用和共享资源。查看是否将
RedisTemplate、连接工厂或线程池保存在static字段、全局注册表或 Tomcat 共享类加载器加载的组件中,导致旧对象在重新部署后仍被引用。
日志中的 pool-4-thread-1 可以帮助定位线程,但单凭这个名称无法判断它属于哪个应用。
让消费循环能够正常退出
消费循环需要有停止标志、可控的等待时间和异常后的退避处理。下面的代码展示了需要合并到现有 Worker 中的循环结构,邮件处理部分应接入现有业务逻辑。
private volatile boolean running = true;
private void consumeLoop() {
while (running && !Thread.currentThread().isInterrupted()) {
try {
String message = redisTemplate.opsForList()
.rightPop(queueKey, 2, TimeUnit.SECONDS);
if (message != null) {
// 在这里调用现有邮件处理逻辑。
}
} catch (RuntimeException ex) {
if (!running || Thread.currentThread().isInterrupted()) {
break;
}
log.error("Email queue worker error", ex);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
break;
}
}
}
}
这里假设 redisTemplate 的值类型为 String,并使用支持该重载的 Spring Data Redis API。2 秒的弹出等待和 1 秒的重试间隔只是示例配置,应根据实际关闭时间要求和连接超时调整。
有限等待让循环定期检查停止标志,但网络异常时的实际返回时间还受客户端超时配置影响。增加重试间隔可以缓解日志刷屏,但连接工厂已经销毁时,继续重试同一个工厂无法使它恢复。
在销毁连接工厂之前停止 Worker
如果线程池由当前 Worker 独占,可以在 Worker 的销毁回调中停止消费并等待线程退出。例如,让现有类实现 Spring 的 DisposableBean,加入以下片段:
@Override
public void destroy() {
running = false;
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
log.warn("Email queue worker did not stop in time");
}
}
} catch (InterruptedException ex) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
这里的 executor 指运行该消费循环的专用 ExecutorService。如果多个组件共享线程池,应取消当前 Worker 的任务,由线程池的拥有者负责整体关闭。
还要确保 Spring 正确识别 Bean 之间的依赖。对于同一应用上下文中的 Bean,如果连接工厂 Bean 的名称确实为 redisConnectionFactory,可以显式声明:
@Component
@DependsOn("redisConnectionFactory")
public class EmailQueueWorker implements DisposableBean {
// 保留现有依赖注入、任务启动和业务处理代码。
// 合并消费循环及 destroy() 实现。
}
对于这些单例 Bean,显式依赖关系可用于保证 Worker 的销毁回调先于连接工厂销毁。Bean 名称应以项目实际配置为准;这种声明也不能协调两个独立应用上下文之间的资源关闭。
shutdownNow() 通过中断请求任务退出,不保证线程已经停止。如果邮件发送或其他处理逻辑长时间阻塞且不响应中断,仍需为这些操作配置超时,并结合实际 Spring 版本选择合适的生命周期协调方式,例如 SmartLifecycle。
修复后如何验证
连续重新部署几次 OTP 服务,检查以下结果:
- 旧 Worker 收到停止信号后退出,旧线程不再残留。
- 新部署中的 Worker 使用新创建且有效的连接工厂。
- 如果邮件应用独立部署,确认它是否真的发生了上下文关闭或 Worker 重建。
- Redis 暂时不可用时,Worker 的重试频率受控;应用关闭时能够结束消费。
关闭期间还要明确在途消息的处理策略。rightPop 会直接将消息移出列表,如果弹出后线程退出而邮件尚未发送,消息可能丢失。当前日志不足以判断现有系统是否已有确认或重试机制,因此调整关闭流程时,应一并核对这一环节。
备注:内容仅供参考。
