软件事务内存STM #

一、STM简介 #

软件事务内存(Software Transactional Memory,STM)是Clojure并发模型的核心。STM提供了一种无需锁的并发控制机制,通过事务保证状态一致性。

1.1 为什么需要STM #

传统并发问题:

clojure
(def balance (atom 1000))

(defn withdraw [amount]
  (let [current @balance]
    (Thread/sleep 10)
    (reset! balance (- current amount))))

多个线程同时执行可能导致竞态条件。

1.2 STM解决方案 #

clojure
(def balance (ref 1000))

(defn withdraw [amount]
  (dosync
    (alter balance - amount)))

STM自动处理并发冲突。

二、事务特性 #

2.1 原子性(Atomicity) #

事务要么全部成功,要么全部回滚:

clojure
(def a (ref 0))
(def b (ref 0))

(try
  (dosync
    (alter a inc)
    (throw (ex-info "Error!" {}))
    (alter b inc))
  (catch Exception e
    (println "Caught error")))

[@a @b]

2.2 一致性(Consistency) #

事务保持数据一致性:

clojure
(def accounts
  {:checking (ref 1000)
   :savings (ref 500)})

(defn transfer [from to amount]
  (dosync
    (when (>= @from amount)
      (alter from - amount)
      (alter to + amount))))

(transfer (:checking accounts) (:savings accounts) 200)

[(:checking @accounts) (:savings @accounts)]

2.3 隔离性(Isolation) #

并发事务相互隔离:

clojure
(def x (ref 0))
(def y (ref 0))

(defn update-both []
  (dosync
    (alter x inc)
    (Thread/sleep 100)
    (alter y inc)))

(defn read-both []
  (dosync [@x @y]))

(future (update-both))
(Thread/sleep 50)
(read-both)

三、MVCC机制 #

3.1 多版本并发控制 #

Clojure STM使用MVCC(Multi-Version Concurrency Control):

clojure
(def counter (ref 0))

(dosync
  (let [v1 @counter]
    (Thread/sleep 100)
    (let [v2 @counter]
      [v1 v2])))

每个事务看到一致的数据快照。

3.2 快照隔离 #

clojure
(def a (ref 0))
(def b (ref 0))

(defn snapshot-read []
  (dosync
    (let [va @a
          vb @b]
      [va vb])))

(defn update-both []
  (dosync
    (alter a inc)
    (alter b inc)))

事务看到开始时的快照。

3.3 写入冲突检测 #

clojure
(def counter (ref 0))

(defn slow-update []
  (dosync
    (let [current @counter]
      (Thread/sleep 100)
      (ref-set counter (inc current)))))

(def futures
  (doall
    (for [_ (range 5)]
      (future (slow-update)))))

(doseq [f futures] @f)

@counter

冲突时事务自动重试。

四、事务操作 #

4.1 alter #

顺序更新,冲突时重试:

clojure
(def counter (ref 0))

(dosync
  (alter counter inc))

4.2 commute #

可交换更新,提交时重新计算:

clojure
(def counter (ref 0))

(dosync
  (commute counter inc))

commute 适合可交换操作,减少重试。

4.3 ref-set #

直接设置值:

clojure
(def state (ref :idle))

(dosync
  (ref-set state :running))

4.4 ensure #

保护读取,防止写入:

clojure
(def a (ref 0))
(def b (ref 0))

(dosync
  (ensure a)
  (alter b + @a))

五、事务嵌套 #

5.1 嵌套事务合并 #

clojure
(def x (ref 0))

(defn inner []
  (dosync
    (alter x + 10)))

(defn outer []
  (dosync
    (alter x inc)
    (inner)))

(outer)

@x

嵌套事务合并为一个。

5.2 事务传播 #

clojure
(def a (ref 0))
(def b (ref 0))

(defn update-a []
  (dosync
    (alter a inc)))

(defn update-b []
  (dosync
    (alter b inc)))

(defn update-both []
  (dosync
    (update-a)
    (update-b)))

(update-both)

[@a @b]

六、事务隔离级别 #

6.1 快照隔离 #

Clojure STM默认使用快照隔离:

clojure
(def x (ref 0))
(def y (ref 0))

(defn read-consistent []
  (dosync
    (let [vx @x
          vy @y]
      (when (not= vx vy)
        (println "Inconsistent!" vx vy)))))

(defn update-both []
  (dosync
    (alter x inc)
    (Thread/sleep 50)
    (alter y inc)))

6.2 读写冲突 #

clojure
(def counter (ref 0))

(defn read-modify-write []
  (dosync
    (let [current @counter]
      (Thread/sleep 10)
      (ref-set counter (inc current)))))

七、性能优化 #

7.1 减少事务时间 #

clojure
(defn slow-transaction []
  (dosync
    (Thread/sleep 100)
    (alter some-ref inc)))

(defn fast-transaction []
  (let [value (compute-expensive-value)]
    (dosync
      (alter some-ref + value))))

7.2 使用commute #

clojure
(def counter (ref 0))

(dosync
  (alter counter inc))

(dosync
  (commute counter inc))

7.3 减少Ref数量 #

clojure
(def state (ref {:a 0 :b 0 :c 0}))

(dosync
  (alter state update :a inc)
  (alter state update :b inc))

(def a (ref 0))
(def b (ref 0))

(dosync
  (alter a inc)
  (alter b inc))

八、实践示例 #

8.1 银行转账系统 #

clojure
(defrecord Account [id owner balance])

(defn make-account [id owner initial-balance]
  (ref (->Account id owner initial-balance)))

(defn balance [account]
  (:balance @account))

(defn deposit [account amount]
  (dosync
    (alter account update :balance + amount)))

(defn withdraw [account amount]
  (dosync
    (let [current (:balance @account)]
      (if (>= current amount)
        (do
          (alter account update :balance - amount)
          {:success true})
        {:success false :error "Insufficient funds"}))))

(defn transfer [from to amount]
  (dosync
    (let [result (withdraw from amount)]
      (if (:success result)
        (do
          (deposit to amount)
          {:success true})
        result))))

(def acc1 (make-account 1 "Alice" 1000))
(def acc2 (make-account 2 "Bob" 500))

(transfer acc1 acc2 200)

[(balance acc1) (balance acc2)]

8.2 库存管理 #

clojure
(def inventory
  (ref {:apple 100 :banana 50 :orange 75}))

(defn check-stock [item]
  (get @inventory item 0))

(defn reserve-items [items]
  (dosync
    (let [current @inventory
          checks (for [[item qty] items]
                   [item qty (get current item 0)])]
      (if (every? (fn [[_ qty available]]
                    (>= available qty))
                  checks)
        (do
          (doseq [[item qty] items]
            (alter inventory update item - qty))
          {:success true})
        {:success false :error "Insufficient stock"}))))

(reserve-items {:apple 10 :banana 5})
@inventory

8.3 拍卖系统 #

clojure
(def auction
  (ref {:highest-bid 0
        :highest-bidder nil
        :bids []}))

(defn place-bid [bidder amount]
  (dosync
    (let [current @auction]
      (if (> amount (:highest-bid current))
        (do
          (alter auction
                 -> (assoc :highest-bid amount)
                 (assoc :highest-bidder bidder)
                 (update :bids conj {:bidder bidder :amount amount}))
          {:success true :message "Bid accepted"})
        {:success false :message "Bid too low"}))))

(defn get-status []
  @auction)

(place-bid "Alice" 100)
(place-bid "Bob" 150)
(place-bid "Charlie" 120)

(get-status)

九、STM最佳实践 #

9.1 事务应该简短 #

clojure
(defn good-practice []
  (let [data (prepare-data)]
    (dosync
      (alter ref assoc :key data))))

(defn bad-practice []
  (dosync
    (let [data (expensive-computation)]
      (alter ref assoc :key data))))

9.2 避免IO操作 #

clojure
(defn bad-io []
  (dosync
    (write-to-disk @ref)
    (alter ref inc)))

(defn good-io []
  (let [data (dosync @ref)]
    (write-to-disk data)))

9.3 选择合适的引用类型 #

场景 类型
简单独立状态 Atom
协调多变量 Ref
异步更新 Agent

十、总结 #

STM核心概念:

概念 说明
MVCC 多版本并发控制
快照隔离 事务看到一致快照
原子性 全部成功或全部回滚
冲突检测 自动重试机制

关键点:

  1. STM提供无锁并发控制
  2. 使用MVCC实现快照隔离
  3. 事务应该简短,避免IO
  4. commute 减少冲突重试
  5. 选择合适的引用类型

并发编程系列完成!接下来学习Java互操作!

最后更新:2026-03-27