Quarkus多数据源场景下Panache与Mutiny并发调用Uni触发BlockingOperationNotAllowedException问题排查
让我们先拆解一下你遇到的问题核心:为什么并行启动三个Uni会触发BlockingOperationNotAllowedException,而先串行第一个再并行另外两个就正常?
根本原因:线程上下文与阻塞操作的限制
Quarkus的事件循环线程(IO线程)是专门处理非阻塞IO任务的,它严格禁止执行阻塞操作(比如imperative Panache的查询、JDBC同步调用),否则就会抛出BlockingOperationNotAllowedException。而工作线程池(Worker Pool)则是专门用来处理阻塞任务的,在这里执行同步操作是安全的。
我们来看你的代码细节:
DbPropertyMapper的方法差异get()和getInt():直接在调用线程执行阻塞的Panache查询(DbPropertyDictionnary.get(key)),没有切换线程。如果调用线程是事件循环线程,立刻就会触发异常。getUnblocking():通过runSubscriptionOn(Infrastructure.getDefaultWorkerPool())把查询逻辑切换到了工作线程,所以不会有问题。
willFailed()的问题所在
当你用Uni.join().all(...)同时启动三个Uni时,它们的初始订阅线程是当前的请求处理线程(大概率是事件循环线程):firstRequest()里用了getUnblocking(),会自动切换到工作线程,没问题;- 但
secondRequest()用的是get(),thirdRequest()用的getInt()内部也是调用get(),这两个方法都会直接在事件循环线程上执行阻塞的Panache查询,瞬间触发异常。
willWork()为什么能正常运行
你先调用firstRequest(idList),它内部的getUnblocking()会把整个Uni的执行上下文切换到工作线程。当这个Uni完成后,onItem().transformToUni的逻辑会继承上游的线程上下文(也就是工作线程)。此时再并行启动secondRequest()和thirdRequest(),它们的订阅线程就是工作线程——而工作线程允许执行阻塞操作,所以不会抛出异常。
解决方案:统一处理阻塞操作的线程切换
最彻底的解决方法是让所有涉及阻塞Panache调用的方法都切换到工作线程,修改DbPropertyMapper的get()和getInt()方法:
public class DbPropertyMapper { private DbPropertyMapper() {} public static Uni<String> get(String key) { // 新增线程切换逻辑,和getUnblocking保持一致 return Uni.createFrom().item(() -> DbPropertyDictionnary.get(key).getValue()) .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); } public static Uni<Integer> getInt(String key) { return get(key).onItem().transform(Integer::parseInt); } public static Uni<String> getUnblocking(String key) { return Uni.createFrom() .item(() -> DbPropertyDictionnary.get(key).getValue()) .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); } }
这样不管你是并行还是串行调用这三个请求方法,所有阻塞的Panache查询都会在工作线程执行,再也不会触发BlockingOperationNotAllowedException。
额外建议
如果你的主数据源支持Reactive模式,建议迁移到Reactive Panache,它本身就是非阻塞的,不需要手动切换线程,代码会更简洁优雅。不过考虑到你提到用了两个数据源,这个可能需要根据实际情况评估。
内容的提问来源于stack exchange,提问作者Leto




