WebFlux 中数据残留源于 doOnError 未配合错误恢复操作符导致副作用悬空,需通过状态比对、事务验证、钩子监控及断连测试综合排查。

定位 WebFlux 中因未显式处理 doOnError 导致的数据残留,关键不是“找错误日志”,而是“查数据状态与流生命周期是否错配”。这类问题往往不抛异常、不报错,但业务数据写入了却不回滚、缓存没清理、下游通知发了一半——表面成功,实则残缺。
从数据写入点反向追踪订阅生命周期
WebFlux 中的 doOnError 是副作用操作,它本身不终止或改变流;若你只用它打日志,而没配合 onErrorResume、onErrorContinue 或事务性兜底,那上游已发生的写入(比如 Redis 存值、DB 插记录)就彻底“悬空”了。
- 检查所有含副作用的链式操作:如
repository.save(...).doOnSuccess(...)、redisTemplate.opsForValue().set(...).doOnTerminate(...),确认它们是否包裹在具备错误恢复能力的操作符内 - 重点排查
doOnError出现的位置是否在flatMap/concatMap内部——这些操作符会创建新子流,子流的错误若未被捕获,父流可能照常完成,导致“部分成功”假象 - 用
log()操作符临时增强调试:在关键节点插入.log("save-step", Level.INFO, SignalType.ON_ERROR),观察错误信号是否被真正消费,还是最终落入onErrorDropped
验证数据库/缓存等外部系统是否存在“半写入”状态
数据残留的本质是“有始无终”:写入动作执行了,但事务未提交、回滚未触发、补偿逻辑未启动。需主动比对预期与实际状态。
- 对每个涉及持久化的响应式调用,人工构造一个失败场景(例如 mock repository 返回
Mono.error(new RuntimeException())),观察 DB 表/Redis key 是否残留脏数据 - 检查 R2DBC 事务是否正确使用:仅靠
transactionalOperator.execute(...)不够,必须确保整个 Mono/Flux 链都在同一事务上下文中,并且错误发生时能触发transactionalOperator.rollback(...) - 若用了自定义缓存更新逻辑(如先查 DB 再 set Redis),确认
doOnError里是否调用了cache.evict(...)——否则缓存将长期与 DB 不一致
启用 Reactor 调试钩子捕获静默丢弃的错误
很多数据残留源于错误被框架静默吞掉,尤其是连接中断、下游超时等场景下触发的 onErrorDropped。这类错误默认不打印堆栈,但可通过全局钩子暴露出来。
- 在应用启动时添加:
Hooks.onOperatorDebug();(开发环境)或Hooks.onErrorDropped(e -> logger.warn("Silent error dropped", e));(生产环境) - 配合 Logback 配置,在
logback-spring.xml中增加 reactor 相关包的日志级别:<logger name="reactor.core.publisher.Operators" level="WARN"/> - 一旦发现
onErrorDropped日志,立即回溯该错误来源的 Mono/Flux 链,重点检查其中是否遗漏了onErrorResume或onErrorContinue,而不是只依赖doOnError
用集成测试模拟客户端断连并观测副作用行为
真实世界中,doOnError 最常失效的场景就是 HTTP 499(客户端关闭连接)。此时 Netty 取消订阅,但你的 doOnError 可能根本没机会执行——因为错误发生在 I/O 层,未传播到业务流中。
- 编写 WebTestClient 测试,用
.exchange()发起请求后立即调用response.releaseBody()模拟断连 - 在待测服务中,对关键副作用操作(如发消息、改状态)加计数器或内存标记(如
AtomicBoolean saved = new AtomicBoolean()),并在测试后断言其是否被误触发 - 观察日志中是否有 “Connection reset by peer” 类错误,若有且无对应业务回滚动作,说明
doOnError位置不对或未覆盖该错误类型


















