(ns clj-webapp.stm-test
(:require [clojure.test :refer :all]))
(def attempts (atom 0))
(def transfers (agent 0))
(defn transfer [from to amount]
(dosync
(swap! attempts inc) ;; 会产生副作用
(send transfers inc)
(alter from - amount)
(alter to + amount)
)
)
(def checking (ref 10000))
(def savings (ref 20000))
(defn stress-thread [from to iterations amount]
(Thread. #(dotimes [_ iterations] (transfer from to amount)))
)
(deftest test-transaction
(testing "test transaction"
(println "Before: Checking =" @checking " Savings =" @savings)
(let [t1 (stress-thread checking savings 100 100)
t2 (stress-thread savings checking 200 100)]
(.start t1)
(.start t2)
(.join t1)
(.join t2)
(await transfers)
)
(is (= 300 @transfers))
(is (> @attempts @transfers))
(println "Attempts: " @attempts )
(println "Transfers:" @transfers )
(println "After: Checking =" @checking " Savings =" @savings)
)
)
全部源码见参clj-webapp/test/clj_webapp/stm_test.clj at main · bruceblink/clj-webapp
执行这段代码,这是一种可能的执行结果:
Before: Checking = 10000 Savings = 20000
Attempts: 541
Transfers: 300
After: Checking = 20000 Savings = 10000
这段测试代码,每次执行Attempts的值都是大于300,Transfers的值都是300。这里包含两个问题:
出现这几个问题的原因是Clojure STM(Software Transactional Memory)的“重试机制”导致的结果。
虽然你只运行了 100 + 200 = 300 次逻辑转账,但 transfers 为 300 是个假象,让我们仔细看看为什么。
关键部分:
(defn transfer1 [from to amount]
(dosync
(swap! attempts inc) ;; ⚠️ 原子副作用
(send transfers inc)
(alter from - amount)
(alter to + amount))
)
看起来我们希望每次事务执行都:
attempts 计数 +1transfers 异步发送 +1在 Clojure 中,dosync 是 事务性内存块。