(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。这里包含两个问题:

  1. 为什么Transfers是300?
  2. 为什么Attempts大于Transfers?

出现这几个问题的原因是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))
 )

看起来我们希望每次事务执行都:


🧠 二、STM 的事务回滚与重试机制

在 Clojure 中,dosync 是 事务性内存块。