December 28, 2024
By: 李梦杰

Clojure Luminus 嵌套事务

  1. 嵌套事务
  2. clojure数据库操作相关库
  3. 嵌套事务处理机制和方案
  4. 示例代码测试及说明

事务一般是针对数据库操作而言的. 事务一般用在管理同时出现的多处数据库增删改操作,以保证数据一致性. 嵌套事务,顾名思义就是事务出现了嵌套使用的情况. 本文首先说明了嵌套事务的各类情形,接着简单介绍了Clojure Luminus数据库相关组件,基于组件给出了嵌套事务处理的方案,并在最后附上了测试用代码.

嵌套事务

  1. 嵌套事务常见情景:嵌套事务的位置
情景一:嵌套事务在末尾
  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. 一般嵌套事务出现在函数调用中 如,在上述"情景一"种,事务1在函数A中,事务2在函数B中, 函数A内部调用了函数B

clojure数据库操作相关库

  1. conman
    1. 地址https://github.com/luminus-framework/conman
      1. Luminus 数据库连接管理和 SQL 查询生成库
      2. 该库使用 HikariCP 库提供池连接。
      3. 查询是使用 HugSQL 生成的,并使用连接感知函数包装。
  2. next-jdbc: 真正实现事务控制
    1. conman库底层使用jdbc库
    2. https://github.com/seancorfield/next-jdbc
      1. 下一代 clojure.java.jdbc :一种轻量级级 Clojure 包装器,用于基于 JDBC 的数据库访问。

嵌套事务处理机制和方案

  1. 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 "手动异常" {})))
  1. 对于嵌套事务,子事务提交时,把之前所有的记录都提交了,但父(外层)事务还没结束,还会继续记录,直到下一次提交.
    1. 子事务的提交引发了父事务中部分已执行操作的提交;——期待子事务执行完成后,等待父事务一起提交
    2. 子事务的回滚引发了父事务的已执行操作的回滚;——符合需求
    3. 当前只有该模式的嵌套事务才满足预期:
T1(事务1)
    D1 (操作1)
  D2
  T2(事务2)——事务2的结束也意味着事务1的结束,同时回滚,同时提交
  1. next-jdbc-嵌套事务处理
    1. 事务处理说明:https://github.com/seancorfield/next-jdbc/blob/develop/doc/transactions.md
    2. 嵌套事务处理说明:https://github.com/seancorfield/next-jdbc/blob/develop/doc/transactions.md#nesting-transactions
      1. 允许嵌套事务,默认行为通常是有缺陷的,如上述只使用(conman/with-transaction [db])的例子
      2. next.jdbc 提供了一种通过公共动态 Var 控制行为的方法:next.jdbc.transaction/nested-tx
        1. :allow 允许嵌套调用,但使它们重叠, 不区分子事务还是父事务. 如,子事务的回滚,也是事务的回滚,会引发了所有已执行操作的回滚,无论操作是子事务中的,还是父事务中的. 如,上述示例中的嵌套调用场景,默认就是该配置,可自行揣摩
        2. :ignore 提供与clojure.java.jdbc相同的行为,其中嵌套调用基本上被忽略,只有最外层的事务生效。相当于所有数据库操作交由最外层事务统一管理
          1. 设置:ignore后单独调用(非嵌套事务)仍旧有事务效果
        3. :prohibit 将导致任何启动嵌套事务的尝试都引发异常,这可能是检测上述潜在错误行为的有用方法(对于 :allow OR :ignore )。
      3. 忽略嵌套事务示例
(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 "手动异常" {})))
  1. 整体嵌套事务处理方案
    1. 未找到全局设置嵌套事务(binding [nested-tx :ignore])的方案
      1. *nested-tx*类型为defonce ^:dynamic
        1. defonce定义的变量不能直接重新赋值,需要使用binding线程内局部修改
    2. 通用db_utils.clj中的函数增加事务时,嵌套事务(binding [nested-tx :ignore])
      1. 一个函数中同时出现多条增删改的数据库操作需要使用事务
    3. 其他存在事务嵌套的函数调用,要在子函数的事务里设置(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 "手动异常" {})))))
    ```
Tags: transaction clojure