Clojure中含Ref集合的Atom操作及航班预订逻辑问题排查
解决Clojure多线程航班预订应用的Ref事务处理问题
你的核心问题在于对Clojure的ref和atom配合使用的逻辑偏差——你把航班包装成ref的集合存在atom里,但后续操作既没利用ref的事务特性,还错误地用swap!修改整个序列(assoc根本不适用于序列类型)。我们一步步来修正:
1. 重新设计flights的存储结构
当前flights是ref的序列,查找和修改效率低。我们改成以航班ID为键、对应航班ref为值的map,这样能快速定位目标航班:
; 把atom从空序列改成空map (def flights (atom {})) (defn initialize-flights [initial-flights] (reset! flights (into {} (map (fn [flight] [(:id flight) (ref flight)]) initial-flights))))
2. 修正process-customers的多线程逻辑
不需要用swap!修改整个flights集合——每个航班都是独立的事务性ref,我们直接在事务中修改单个ref即可。用pmap简化多线程处理:
(defn process-customers [customers thread-count] (let [partitions (partitionCustomerInput thread-count customers)] ; 用pmap给每个分区分配线程,doall强制执行所有future (doall (pmap (fn [partition] (doseq [customer partition] (process-customer @flights customer))) partitions))) (reset! finished-processing? true))
3. 修正process-customer及辅助函数
现在process-customer接收的是ref的map,我们需要在find-flight中先deref拿到实际航班数据,再用dosync包裹ref的修改操作:
修正find-flight
(defn- find-flight [flight-refs customer] (let [{:keys [id from to seats budget]} customer flights-and-prices (for [[flight-id flight-ref] flight-refs :let [flight @flight-ref] ; 先解引用拿到航班数据 :when (and (= (:from flight) from) (= (:to flight) to)) :let [lowest-price (lowest-available-price flight seats)] :when (and (some? lowest-price) (<= lowest-price budget))] {:flight-ref flight-ref :price lowest-price :flight-id flight-id})] (first (sort-by :price flights-and-prices))))
修正process-customer(加入事务)
(defn- process-customer [flight-refs customer] (if-let [{:keys [flight-ref price flight-id]} (find-flight flight-refs customer)] (dosync ; 开启事务,保证多线程下数据一致性 (let [current-flight @flight-ref updated-flight (book current-flight price (:seats customer))] (alter flight-ref (constantly updated-flight)) ; 修改ref的值 (log "Customer" (:id customer) "booked" (:seats customer) "seats on flight" flight-id "at $" price " (< budget of $" (:budget customer) ").") updated-flight)) (do (log "Customer" (:id customer) "did not find a flight.") nil)))
4. 修正主函数main
现在flights是map,打印时需要取出所有ref的值:
(defn main [] (initialize-flights input/flights) (time (process-customers input/customers 4)) ; 这里4是示例线程数,可替换为你的N-THREADS (println "Flights:") (print-flights (map (comp deref val) @flights)) ; 解引用所有航班ref并打印 (shutdown-agents)) (main)
关键逻辑说明
ref的事务特性:Clojure的ref必须在dosync事务中修改,这样能自动处理多线程竞态条件——如果两个线程同时修改同一个航班,事务会自动重试,保证数据一致性。- map索引
ref:相比序列,map通过航班ID快速定位ref,避免遍历整个集合,提升处理效率。 - 避免不必要的
swap!:原来的swap!试图修改整个flights集合,实际上我们只需要修改单个航班的ref,更符合Clojure不可变数据模型的设计思想。
内容的提问来源于stack exchange,提问作者Gakuo
相关产品推荐
相关产品推荐

