December 28, 2024
By: 李梦杰
Clojure Luminus 嵌套事务
事务一般是针对数据库操作而言的. 事务一般用在管理同时出现的多处数据库增删改操作,以保证数据一致性. 嵌套事务,顾名思义就是事务出现了嵌套使用的情况. 本文首先说明了嵌套事务的各类情形,接着简单介绍了Clojure Luminus数据库相关组件,基于组件给出了嵌套事务处理的方案,并在最后附上了测试用代码.
嵌套事务
- 嵌套事务常见情景:嵌套事务的位置
情景一:嵌套事务在末尾
T1(事务1)
D1 (操作1)
D2
T2(事务2)
D2
D3
情景二:嵌套事务在中间
T1(事务1)
D1 (操作1)
T2(事务2)
D2
D3
D2
情景三:嵌套事务在首位
T1(事务1)
T2(事务2)
D2
D3
D1 (操作1)
D2
- 一般嵌套事务出现在函数调用中 如,在上述"情景一"种,事务1在函数A中,事务2在函数B中, 函数A内部调用了函数B
clojure数据库操作相关库
- conman
- 地址https://github.com/luminus-framework/conman
- Luminus 数据库连接管理和 SQL 查询生成库
- 该库使用 HikariCP 库提供池连接。
- 查询是使用 HugSQL 生成的,并使用连接感知函数包装。
- 地址https://github.com/luminus-framework/conman
- next-jdbc: 真正实现事务控制
- conman库底层使用jdbc库
- https://github.com/seancorfield/next-jdbc
- 下一代 clojure.java.jdbc :一种轻量级级 Clojure 包装器,用于基于 JDBC 的数据库访问。
嵌套事务处理机制和方案
- conman-普通事务处理
(ns clj-backend.db.core)
(defstate ^:dynamic *db*
:start (jdbc/get-connection @used-database-url)
:stop (.close *db*))
(ns clj-backend.common.db-utils)
[clj-backend.db.core :refer [*db*]]
[conman.core :as conman]
;; 加了事务,正常回滚
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell1"}})
;; 触发异常
(throw (ex-info "手动异常" {})))
;; 事务嵌套,3.5_1(正常插入,未回滚),3.5_2(正常插入,未回滚)
;; 3.5_3(正常插入,回滚),3.5_5(正常插入,回滚)
;; ——嵌套事务场景2,期待都能回滚
;; 插入成功
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.5_1"}})
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.5_2"}}))
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.5_3"}})
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.5_4"}})
(throw (ex-info "手动异常" {})))
- 对于嵌套事务,子事务提交时,把之前所有的记录都提交了,但父(外层)事务还没结束,还会继续记录,直到下一次提交.
- 子事务的提交引发了父事务中部分已执行操作的提交;——期待子事务执行完成后,等待父事务一起提交
- 子事务的回滚引发了父事务的已执行操作的回滚;——符合需求
- 当前只有该模式的嵌套事务才满足预期:
T1(事务1)
D1 (操作1)
D2
T2(事务2)——事务2的结束也意味着事务1的结束,同时回滚,同时提交
- next-jdbc-嵌套事务处理
- 事务处理说明:https://github.com/seancorfield/next-jdbc/blob/develop/doc/transactions.md
- 嵌套事务处理说明:https://github.com/seancorfield/next-jdbc/blob/develop/doc/transactions.md#nesting-transactions
- 允许嵌套事务,默认行为通常是有缺陷的,如上述只使用(conman/with-transaction [db])的例子
- next.jdbc 提供了一种通过公共动态 Var 控制行为的方法:next.jdbc.transaction/nested-tx
- :allow 允许嵌套调用,但使它们重叠, 不区分子事务还是父事务. 如,子事务的回滚,也是事务的回滚,会引发了所有已执行操作的回滚,无论操作是子事务中的,还是父事务中的. 如,上述示例中的嵌套调用场景,默认就是该配置,可自行揣摩
- :ignore 提供与clojure.java.jdbc相同的行为,其中嵌套调用基本上被忽略,只有最外层的事务生效。相当于所有数据库操作交由最外层事务统一管理
- 设置:ignore后单独调用(非嵌套事务)仍旧有事务效果
- :prohibit 将导致任何启动嵌套事务的尝试都引发异常,这可能是检测上述潜在错误行为的有用方法(对于 :allow OR :ignore )。
- 忽略嵌套事务示例
(ns clj-backend.common.db-utils)
[clj-backend.db.core :refer [*db*]]
[conman.core :as conman]
[next.jdbc.transaction :refer [*nested-tx*]]
;; 全部回滚
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.4_1"}})
(binding [*nested-tx* :ignore]
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.4_2"}})))
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.4_3"}})
(throw (ex-info "手动异常" {})))
- 整体嵌套事务处理方案
- 未找到全局设置嵌套事务(binding [nested-tx :ignore])的方案
- *nested-tx*类型为defonce ^:dynamic
- defonce定义的变量不能直接重新赋值,需要使用binding线程内局部修改
- *nested-tx*类型为defonce ^:dynamic
- 通用db_utils.clj中的函数增加事务时,嵌套事务(binding [nested-tx :ignore])
- 一个函数中同时出现多条增删改的数据库操作需要使用事务
- 其他存在事务嵌套的函数调用,要在子函数的事务里设置(binding [nested-tx :ignore])
- 未找到全局设置嵌套事务(binding [nested-tx :ignore])的方案
示例代码测试及说明
(defn tran-test
"事务测试
1. 事务是否正常.
单个insert也需要加事务吗?
特殊情况下需要,否则不会回滚;
更多是:一般要求数据一致性的场景.组合增删改的场景
2. 两个并列事务
3. 事务嵌套测试
注意: 事务包含太多查询,会有更多等待获取锁的操作,增加事务占用锁的时间
嵌套事务:next.jdbc.transaction/*nested-tx*"
[index]
(condp = index
;; 正常插入
0 (db/insert! {:table-name "cell"
:inserter {:cell_name "cell1"}})
;; 不加事务,不会回滚
0.1 (do
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell0.1"}})
(throw (ex-info "手动异常" {})))
1 (conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell1"}})
(throw (ex-info "手动异常" {})))
;; 两个并列事务,其中一个正常插入,其中一个触发回滚
2 (do
;; 插入成功
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell2_1"}}))
;; 插入回滚
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell2_2"}})
(throw (ex-info "手动异常" {}))))
;; 事务嵌套,内部报错(正常回滚),外部正常(正常回滚)
;;; 不可写成事务嵌套,会出现回滚不一致的情况
3.1 (do
;; 插入成功
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3_1"}})
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3_2"}})
(throw (ex-info "手动异常" {})))))
;; 事务嵌套,外部执行(正常回滚),内部未执行,中部报错
3.2 (do
;; 插入成功
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell4_1"}})
(throw (ex-info "手动异常" {}))
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell4_2"}}))))
;; 事务嵌套,外部执行(未回滚),内部执行(未回滚),外部最后报错
;; 未回滚也可以,保证了,数据的一致性.
;;;; 两个事务都被触发
3.3 ;; 插入成功
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.3_1"}})
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.3_2"}})) ;; ?? 事务提交时,把之前所有的记录都提交了,但事务还有一个,还会继续记录,知道下一次提交.
(throw (ex-info "手动异常" {}))))
;; 事务嵌套,3.4_1(正常插入,未回滚),3.4_2(正常插入,未回滚),3.4_3(正常插入,回滚)
3.4 ;; 插入成功
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.4_1"}})
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.4_2"}}))
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.4_3"}})
(throw (ex-info "手动异常" {})))
;; 事务嵌套,3.5_1(正常插入,未回滚),3.5_2(正常插入,未回滚)
;; 3.5_3(正常插入,回滚),3.5_5(正常插入,回滚)
3.5 ;; 插入成功
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.5_1"}})
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.5_2"}}))
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.5_3"}})
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.5_4"}})
(throw (ex-info "手动异常" {})))
;; 事务嵌套,3.6_1(正常插入,未回滚),3.6_2(正常插入,未回滚)
;; 3.6_3(正常插入,未回滚),3.6_5(正常插入,未回滚)
3.6 ;; 插入成功
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.6_1"}})
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.6_2"}}))
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.6_3"}})
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.6_4"}}))
(throw (ex-info "手动异常" {})))
;; 事务嵌套,3.7_1(正常插入,未回滚),3.7_2(正常插入,未回滚),3.7_3(未执行)
;; 3.7_4(未执行,回滚)
3.7 ;; 插入成功
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.7_1"}})
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.7_2"}}))
(throw (ex-info "手动异常" {}))
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.7_3"}})
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell3.7_4"}}))))
;; 事务嵌套,6.1.1已经提交;6.1.2,6.1.3正常回滚
6.1 ;; 插入成功
(conman/with-transaction [*db*]
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell6.1_1"}}))
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell6.1_2"}})
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell6.1_3"}})
(throw (ex-info "手动异常" {}))))
;; 事务嵌套,6.2.1,6.2.2已经提交;6.2.3正常回滚
6.2 (do
;; 插入成功
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell6.2_1"}})
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell6.2_2"}}))
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell6.2_3"}})
(throw (ex-info "手动异常" {}))))
;; 事务嵌套,6.3.1,6.3.2,6.3.3已经提交;不回滚
6.3 ;; 插入成功
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell6.3_1"}})
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell6.3_2"}})
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell6.3_3"}}))
(throw (ex-info "手动异常" {})))
;; 事务嵌套,6.4.1,6.4.2,6.4.3都回滚
6.4 ;; 插入成功
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell6.4_1"}})
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell6.4_2"}})
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell6.4_3"}})
(throw (ex-info "手动异常" {})))))
;; 事务next.jdbc.transaction/*nested-tx*
7.1 ;; 插入成功
(binding [*nested-tx* :prohibit]
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.1_1"}})))
7.2 ;; 插入失败(全部回滚),Unhandled java.lang.IllegalStateException
;; Nested transactions are prohibited
(binding [*nested-tx* :prohibit]
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.2_1"}})
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.2_2"}}))
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.2_3"}})
(throw (ex-info "手动异常" {}))))
7.3 ;; 全部回滚
;; 方案, 函数内部的事务,都手动维护为:ignore. 所有可能被嵌套调用的事务都设置为ignore
;; 保存点:我们不需要设置保存点,或者我们的每个操作就是一个保存点
;; 所有的视图都可设置为:ignore,但是没有找到全局设置的方案
;; 一个函数中有多个数据库增删改操作时才走事务
(binding [*nested-tx* :ignore]
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.3_1"}})
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.3_2"}}))
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.3_3"}})
(throw (ex-info "手动异常" {}))))
7.4 ;; 全部回滚
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.4_1"}})
(binding [*nested-tx* :ignore]
(conman/with-transaction [*db*]
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.4_2"}})))
(db/insert! {:table-name "cell"
:inserter {:cell_name "cell7.4_3"}})
(throw (ex-info "手动异常" {})))))
```