Clojure常驻运行应用最佳实践咨询:定时任务与多线程处理
作为常年写Clojure后台服务的开发者,刚好有不少这类常驻轮询+并发处理任务的实践经验,给你梳理几个核心的最佳实践方向,帮你少踩坑:
一、选对定时调度工具,搞定周期性任务
你的需求是每隔几秒轮询数据库,这里分两种场景选工具:
- 轻量场景首选
chime:这个库专门做简单的周期性任务,代码简洁到离谱,完全不需要复杂配置,适合你的初始需求。示例代码:
(require '[chime.core :as chime] '[java-time.api :as t]) ;; 每隔5秒执行一次任务 (chime/chime-at (t/periodic-seq (t/now) (t/seconds 5)) (fn [current-time] (println "开始轮询数据库:" current-time) ;; 这里放你的数据库查询+处理逻辑 ))
- 复杂调度用
quartzite:如果以后需要更灵活的调度(比如按cron表达式执行、任务持久化),Quartz的Clojure包装库quartzite是生产环境的标准选择,支持任务暂停、恢复、重试等高级功能。
二、数据库操作:必须用连接池+稳定的JDBC库
常驻应用最忌讳每次查询都新建数据库连接,不仅慢还容易把数据库连接耗尽:
- 推荐组合:
next.jdbc+hikari-cp:next.jdbc是Clojure官方推荐的现代JDBC库,hikari-cp是性能顶尖的连接池。示例配置:
(require '[next.jdbc :as jdbc] '[hikari-cp.core :as hikari]) ;; 初始化源数据库连接池 (def source-db-pool (hikari/make-datasource {:jdbc-url "jdbc:mysql://localhost:3306/source_db" :username "your-user" :password "your-pass" :maximum-pool-size 10})) ;; 初始化结果数据库连接池 (def result-db-pool (hikari/make-datasource {:jdbc-url "jdbc:mysql://localhost:3306/result_db" :username "your-user" :password "your-pass" :maximum-pool-size 10})) ;; 查询待处理数据 (defn fetch-pending-data [] (jdbc/execute! source-db-pool ["SELECT * FROM source_table WHERE status = ?" "pending"]))
注意:连接池的maximum-pool-size要根据数据库的最大连接数来设置,别超过数据库的上限。
三、并发处理:别瞎创建线程,用线程池控制资源
你提到要创建新线程处理数据,绝对不能无限制创建——几十上百个线程同时跑很容易把内存吃光。正确的做法是用固定大小的线程池:
- 用JDK线程池控制并发:Clojure可以直接调用Java的线程池API,简单粗暴好用。示例:
;; 创建固定大小的线程池,比如8个线程(根据服务器CPU核心数调整) (def processing-executor (java.util.concurrent.Executors/newFixedThreadPool 8)) (defn transform-data [item] ;; 这里写你的数据处理逻辑,比如计算、转换等 (assoc item :processed true :processed-at (java.util.Date.))) (defn save-processed-result [item] (try (jdbc/execute! result-db-pool ["INSERT INTO result_table (id, content, processed_at) VALUES (?, ?, ?)" (:id item) (:content item) (:processed-at item)]) ;; 处理完可以更新源数据状态,避免重复处理 (jdbc/execute! source-db-pool ["UPDATE source_table SET status = ? WHERE id = ?" "processed" (:id item)]) (catch Exception e (println (str "处理数据ID:" (:id item) "失败,错误:" (.getMessage e))) ;; 可以把失败数据存入错误表,后续重试 (jdbc/execute! source-db-pool ["UPDATE source_table SET status = ? WHERE id = ?" "failed" (:id item)])))) (defn process-data-batch [items] (doseq [item items] ;; 把任务提交到线程池执行 (.submit processing-executor #(save-processed-result (transform-data item)))))
关键提醒:每个任务一定要加异常捕获,不然一个任务报错会把整个线程搞挂,影响后续任务执行。
四、让应用常驻:优雅退出+部署技巧
要让程序一直跑,得处理JVM的退出信号,还要选对部署方式:
- 注册关闭钩子,优雅释放资源:程序退出时(比如收到kill信号),要关闭连接池、线程池,避免资源泄漏:
(.addShutdownHook (Runtime/getRuntime) (Thread. (fn [] (println "程序开始优雅退出...") ;; 关闭数据库连接池 (hikari/close-datasource source-db-pool) (hikari/close-datasource result-db-pool) ;; 关闭线程池 (.shutdown processing-executor) (println "资源已释放,程序退出"))))
- 部署方式:
- 用
lein uberjar或者clj -T:build uber打包成可执行JAR包,然后用java -jar your-app.jar运行,这是生产环境最常用的方式。 - 如果需要自动重启、日志管理,可以把JAR包做成systemd服务(Linux)或者Windows服务,这样程序崩溃后会自动重启,还能统一管理日志。
- 用
五、监控与日志:排查问题的必备神器
常驻应用没日志和监控,出问题了根本找不到原因:
- 日志用
clojure.tools.logging:搭配SLF4J+Logback,配置日志文件滚动,把关键步骤(比如开始轮询、处理成功/失败)都记录下来:
(require '[clojure.tools.logging :as log]) (defn save-processed-result [item] (try (log/info "开始处理数据ID:" (:id item)) (let [processed-item (transform-data item)] (jdbc/execute! result-db-pool ["INSERT INTO result_table (id, content, processed_at) VALUES (?, ?, ?)" (:id processed-item) (:content processed-item) (:processed-at processed-item)]) (jdbc/execute! source-db-pool ["UPDATE source_table SET status = ? WHERE id = ?" "processed" (:id processed-item)]) (log/info "处理数据ID:" (:id item) "成功")) (catch Exception e (log/error e "处理数据ID:" (:id item) "失败"))))
- 监控:可以用JMX暴露线程池状态、数据库连接数等指标,或者用Prometheus+Grafana做可视化监控,及时发现资源耗尽、任务堆积等问题。
内容的提问来源于stack exchange,提问作者newBieDev
相关产品推荐
相关产品推荐

