本文介绍一种基于 core.async 的轻量级并发方案,解决传统线程池在递归 jdbc 查询中因线程饥饿导致死锁的问题,通过分离 io 阻塞与逻辑编排,兼顾连接池利用率与可伸缩性。
本文介绍一种基于 core.async 的轻量级并发方案,解决传统线程池在递归 jdbc 查询中因线程饥饿导致死锁的问题,通过分离 io 阻塞与逻辑编排,兼顾连接池利用率与可伸缩性。
在 Clojure 中对数据库执行深度嵌套或递归查询(如树形结构展开)时,若直接复用固定大小的 JDBC 连接池作为通用线程池,极易引发线程饥饿型死锁:每个递归层级都需占用一个线程等待数据库响应,而深层调用持续抢占线程资源,导致顶层查询无法获取连接,整个调用链停滞。
根本症结在于混淆了两类任务的执行模型:
- IO 阻塞任务(如 jdbc/query):必须独占线程,适合交由专用、可配置的线程池(如 Executors/newCachedThreadPool)承载;
- 组合协调任务(如聚合子节点、构建树形结构):本质是轻量计算+等待,无需长期占用 OS 线程,应使用 core.async 的 go 块配合通道(channel)实现“协程式”调度。
✅ 正确做法是「分层解耦」:
- IO 层:所有 JDBC 调用封装为返回 chan 的函数,内部用 thread(非 go!)提交至专用 IO 线程池,避免阻塞 go 调度器;
- 编排层:主递归逻辑使用 go 块,通过
以下是关键代码模式示例:
(require '[clojure.core.async :as a])
;; 专用 IO 线程池(建议 size ≥ 连接池大小,如 20~30)
(def io-pool (java.util.concurrent.Executors/newCachedThreadPool))
;; ✅ 安全封装 JDBC 调用:阻塞操作交给 thread,返回通道
(defn resolve-eids [ctx form]
(a/thread
(let [[where-view eids] (jdbc/query (:conn-pool ctx) ...)]
[where-view eids])))
(defn obj-node [ctx eid]
(a/thread
(jdbc/query (:conn-pool ctx) ["SELECT * FROM attrs WHERE id = ?" eid])))
(defn query-one [ctx form]
(a/thread
(query ctx form))) ; 递归调用仍返回 chan
;; ✅ 编排层:纯 go 块,无阻塞,支持任意深度递归
(defn- query [ctx form]
(a/go
(let [[where-view eids] (a/ (make-node where-view)
(add-children (a/<p>⚠️ <strong>关键注意事项</strong>:</p>
- 绝对避免在 go 块内执行 jdbc/query 或其他阻塞 IO —— 这会耗尽 go 调度器线程(默认仅 8 个),重蹈线程池饥饿覆辙;
- a/thread 是 core.async 提供的安全包装,它自动将 thread 执行结果放入通道,比手动 future + chan 更简洁;
- 若需精细控制 IO 并发度(如限制同时最多 10 个查询),可用 a/pipe + a/take! 或 a/merge 配合限流通道;
- 对于超深递归,可考虑添加 :max-depth 参数或改用显式栈(loop/recur + vector)防止栈溢出,但 go 块本身无栈深度限制。
总结:用 thread 承载阻塞,用 go 编排流程,是 Clojure 中处理 IO 密集型递归任务的黄金法则。它既尊重了 JDBC 的阻塞性质,又充分发挥了 CSP 模型的轻量优势,让 20 连接的池真正跑满,而非被逻辑等待卡死。










