在上一篇文章“一个非侵入的Go事务管理库——如何使用”中,我讲述了如何使用事务库。有些读者可能读过"清晰架构(Clean Architecture)的Go微服务: 事物管理" ,其中描述了事务管理系统的旧版本。那篇文章和本文之间会有一些重叠。因为大多数人可能还没有读过那篇文章或者即使读了也忘记了它的内容。因此为了照顾多数读者,本文还是从头开始(假设你没有读过前文)。如果你读过,那你可以直接跳过熟悉的部分。

好的事务库对于使用它的应用程序是透明的。在Go的“sql”库中,有两种类型的数据库链接,“sql.DB”和“sql.Tx”。当你不需要事务支持时,使用“sql.DB”;否则使用“sql.Tx”。为了让这两种不同场景共享相同的持久层代码,我们需要对数据库链接进行一个封装来同时支持这两种场景。我从"db transaction in golang" 里得到了这个想法。

数据库层的接口

数据库层是事务管理库中处理数据库访问的最低层。应用程序不需要修改该层,只有事务管理库需要这样做。

数据库访问封装

下面是可同时支持事务和非事务操作的共享数据库访问接口, 它在“gdbc.go”中定义。

// SqlGdbc (SQL Go database connection) is a wrapper for SQL database handler ( can be *sql.DB or *sql.Tx)
// It should be able to work with all SQL data that follows SQL standard.
type SqlGdbc interface {
Exec(query string, args ...interface{}) (sql.Result, error)
Prepare(query string) (*sql.Stmt, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
// If need transaction support, add this interface
Transactioner
} // Transactioner is the transaction interface for database handler
// It should only be applicable to SQL database
type Transactioner interface {
// Rollback a transaction
Rollback() error
// Commit a transaction
Commit() error
// TxEnd commits a transaction if no errors, otherwise rollback
// txFunc is the operations wrapped in a transaction
TxEnd(txFunc func() error) error }

它有两部分。一个是数据库接口,它包含了常规的数据库操作,如查询表、更新表记录。另一个事务接口,它包含里支持事务所需要的函数,如“提交”和“回滚”。“SqlGdbc”接口是两者的结合。该接口将用于连接数据库。

数据库访问接口的实现

下面是数据库访问接口的代码实现。它在“sqlConnWrapper.go”文件中。它定义了两个结构体,“SqlDBTx”是对“sql.DB”的封装,将被非事务函数使用。“SqlConnTx”是对“sql.Tx”的封装,将被事务函数使用。

// SqlDBTx is the concrete implementation of sqlGdbc by using *sql.DB
type SqlDBTx struct {
DB *sql.DB
} // SqlConnTx is the concrete implementation of sqlGdbc by using *sql.Tx
type SqlConnTx struct {
DB *sql.Tx
} func (sdt *SqlDBTx) Exec(query string, args ...interface{}) (sql.Result, error) {
return sdt.DB.Exec(query, args...)
} func (sdt *SqlDBTx) Prepare(query string) (*sql.Stmt, error) {
return sdt.DB.Prepare(query)
} func (sdt *SqlDBTx) Query(query string, args ...interface{}) (*sql.Rows, error) {
return sdt.DB.Query(query, args...)
} func (sdt *SqlDBTx) QueryRow(query string, args ...interface{}) *sql.Row {
return sdt.DB.QueryRow(query, args...)
} func (sdb *SqlConnTx) Exec(query string, args ...interface{}) (sql.Result, error) {
return sdb.DB.Exec(query, args...)
} func (sdb *SqlConnTx) Prepare(query string) (*sql.Stmt, error) {
return sdb.DB.Prepare(query)
} func (sdb *SqlConnTx) Query(query string, args ...interface{}) (*sql.Rows, error) {
return sdb.DB.Query(query, args...)
} func (sdb *SqlConnTx) QueryRow(query string, args ...interface{}) *sql.Row {
return sdb.DB.QueryRow(query, args...)
}

事务接口的实现

下面是“Transactioner”接口的代码实现,它在文件 "txConn.go"中。我从"database/sql Tx — detecting Commit or Rollback"中得到这个想法。

因为“SqlDBTx”不支持事务,所以它的所有函数都返回“nil"。

// DB doesn't rollback, do nothing here
func (cdt *SqlDBTx) Rollback() error {
return nil
} //DB doesnt commit, do nothing here
func (cdt *SqlDBTx) Commit() error {
return nil
} // DB doesnt rollback, do nothing here
func (cdt *SqlDBTx) TxEnd(txFunc func() error) error {
return nil
} func (sct *SqlConnTx) TxEnd(txFunc func() error) error {
var err error
tx := sct.DB defer func() {
if p := recover(); p != nil {
log.Println("found p and rollback:", p)
tx.Rollback()
panic(p) // re-throw panic after Rollback
} else if err != nil {
log.Println("found error and rollback:", err)
tx.Rollback() // err is non-nil; don't change it
} else {
log.Println("commit:")
err = tx.Commit() // if Commit returns error update err with commit err
}
}()
err = txFunc()
return err
} func (sct *SqlConnTx) Rollback() error {
return sct.DB.Rollback()
} func (sct *SqlConnTx) Commit() error {
return sct.DB.Commit()
}

持久层的接口

在数据库层之上是持久层,应用程序使用持久层来访问数据库表中的记录。你需要定义一个函数在本层中实现对事务的支持。下面是持久层的事务接口,它位于“txDataService.go”文件中。

// TxDataInterface represents operations needed for transaction support.
type TxDataInterface interface {
// EnableTx is called at the end of a transaction and based on whether there is an error, it commits or rollback the
// transaction.
// txFunc is the business function wrapped in a transaction
EnableTx(txFunc func() error) error
}

以下是它的实现代码。它只是调用下层数据库中的函数“TxEnd()”,该函数已在数据库层实现。下面的代码不是事务库的代码(它是本文中惟一的不是事务库中的代码),你需要在应用程序中实现它。

func (uds *UserDataSql) EnableTx(txFunc func() error) error {
return uds.DB.TxEnd(txFunc)
}

获取数据库链接的代码

除了我们上面描述的调用接口之外,在应用程序中你还需要先获得数据库链接。事务库中有两个函数可以完成这个任务。

返回"SqlGdbc"接口的函数

函数"Build()"(在"factory.go"中)将返回"SqlGdbc"接口。根据传入的参数,它讲返回满足"SqlGdbc"接口的结构,如果需要事务支持就是“SqlConnTx”,不需要就是“SqlDBTx”。如果你不需要在应用程序中直接使用数据库链接,那么调用它是最好的。

// Build returns the SqlGdbc interface. This is the interface that you can use directly in your persistence layer
// If you don't need to cache sql.DB connection, you can call this function because you won't be able to get the sql.DB
// in SqlGdbc interface (if you need to do it, call BuildSqlDB()
func Build(dsc *config.DatabaseConfig) (gdbc.SqlGdbc, error) {
db, err := sql.Open(dsc.DriverName, dsc.DataSourceName)
if err != nil {
return nil, errors.Wrap(err, "")
}
// check the connection
err = db.Ping()
if err != nil {
return nil, errors.Wrap(err, "")
}
dt, err := buildGdbc(db, dsc)
if err != nil {
return nil, err
}
return dt, nil
} func buildGdbc(sdb *sql.DB,dsc *config.DatabaseConfig) (gdbc.SqlGdbc, error){
var sdt gdbc.SqlGdbc
if dsc.Tx {
tx, err := sdb.Begin()
if err != nil {
return nil, err
}
sdt = &gdbc.SqlConnTx{DB: tx}
log.Println("buildGdbc(), create TX:")
} else {
sdt = &gdbc.SqlDBTx{sdb}
log.Println("buildGdbc(), create DB:")
}
return sdt, nil
}

返回数据库链接的函数

函数"BuildSqlDB()"(在"factory.go"中)将返回"sql.DB"。它会忽略传入的事务标识参数。应用程序在调用这个函数获得数据库链接后,还需要根据事务标识自己生成“SqlConnTx”或“SqlDBTx”。如果你需要在应用程序里缓存"sql.DB",那么你必须调用这个函数。

// BuildSqlDB returns the sql.DB. The calling function need to generate corresponding gdbc.SqlGdbc struct based on
// sql.DB in order to use it in your persistence layer
// If you need to cache sql.DB connection, you need to call this function
func BuildSqlDB(dsc *config.DatabaseConfig) (*sql.DB, error) {
db, err := sql.Open(dsc.DriverName, dsc.DataSourceName)
if err != nil {
return nil, errors.Wrap(err, "")
}
// check the connection
err = db.Ping()
if err != nil {
return nil, errors.Wrap(err, "")
}
return db, nil }

局限性

首先,它只支持SQL数据库的事务。如果你有一个NoSql数据库,那么它不支持(大多数NoSql数据库不支持事务)。

其次,如果你的事务跨越数据库(例如在不同的微服务之间),那么它将无法工作。常用的做法是使用“Saga Pattern”。你可以为事务中的每个操作编写一个补偿操作,并在回滚阶段逐个执行补偿操作。在应用程序中添加“Saga”解决方案并不困难。你可能会问,为什么不把“Saga”加到事务库中呢? 这是一个有趣的问题。我觉得还是单独为“Saga”建一个库比较合适。

第三,它不支持嵌套事务(Nested Transaction),因此你需要手动确保在代码中没有嵌套事务。如果代码库不是太复杂,这很容易做到。如果你有一个非常复杂的代码库,其中有很多事务和非事务代码混在一起,那么你需要一个支持嵌套事务的解决方案。我没有花时间研究如何添加嵌套事务,但它应该有一定的工作量。如果你对此感兴趣,可以从"database/sql: nested transaction or save point support"开始。到目前为止,对于大多数场景,当前的解决方案可能是在代价不大的情况下的最佳方案。

如何扩展库的功能

“SqlGdbc”接口没有列出“sql”包中的所有函数,只列出我的应用程序中需要的函数。你可以轻松地扩展该接口以包含其他函数。

例如,如果需要将全链路跟踪(详情请见"Go微服务全链路跟踪详解")扩展到数据库中,则可能需要在上下文中传递到数据库函数中。“sql”库已经支持具有上下文的数据库函数。你只需要找到它们并将它们添加到"SqlGdbc"接口中,然后在"sqlConnWrapper "中实现它们。然后在持久层中,需要使用上下文作为参数调用函数。

源码:

完整源码: "jfeng45/gtransaction"

索引:

1 "一个非侵入的Go事务管理库——如何使用"

2 "清晰架构(Clean Architecture)的Go微服务: 事物管理"

3 "db transaction in golang"

4 "database/sql Tx — detecting Commit or Rollback"

5 "Applying the Saga Pattern - GOTO Conference"

6 "database/sql: nested transaction or save point support"

7 "Go微服务全链路跟踪详解"

一个非侵入的Go事务管理库——工作原理的更多相关文章

  1. 一个非侵入的Go事务管理库——如何使用

    在文章"清晰架构(Clean Architecture)的Go微服务: 事物管理"中,我谈到了如何在清晰架构中实现非侵入的事务管理. 它允许你把事务代码与业务逻辑代码分开,并且让你 ...

  2. Vuex 状态管理的工作原理

    Vuex 状态管理的工作原理 为什么要使用 Vuex 当我们使用 Vue.js 来开发一个单页应用时,经常会遇到一些组件间共享的数据或状态,或是需要通过 props 深层传递的一些数据.在应用规模较小 ...

  3. linux 文件系统的管理 (硬盘) 工作原理

    一.系统在初始化时如何识别硬盘 1.系统初始时根据MBR的信息来识别硬盘,其中包括了一些执行文件就来载入系统,这些执行文件就是MBR里前面446bytes里的boot loader 程式,而后面的16 ...

  4. Spring事务管理详解_基本原理_事务管理方式

    1. 事务的基本原理 Spring事务的本质其实就是数据库对事务的支持,使用JDBC的事务管理机制,就是利用java.sql.Connection对象完成对事务的提交,那在没有Spring帮我们管理事 ...

  5. Spring003--Spring事务管理(mooc)

    Spring事务管理 一.事务回顾 1.1.什么是事务 事务指的是逻辑上的一组操作,这组操作要么全部成功,要么全部失败. 异常情况发生,需要保证:[1]张三将钱转出,李四收到钱.[2]张三钱未成功转出 ...

  6. SSM框架——以注解形式实现事务管理

    上一篇博文<SSM三大框架整合详细教程>详细说了如何整合Spring.SpringMVC和MyBatis这三大框架.但是没有说到如何配置mybatis的事务管理,在编写业务的过程中,会需要 ...

  7. Spring学习笔记五:Spring进行事务管理

    转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/6776256.html  事务管理主要负责对持久化方法进行统一的提交或回滚,Spring进行事务管理即我们无需在 ...

  8. 【spring源码学习】spring的事务管理的源码解析

    [一]spring事务管理(1)spring的事务管理,是基于aop动态代理实现的.对目标对象生成代理对象,加入事务管理的核心拦截器==>org.springframework.transact ...

  9. Java非侵入式API接口即文档工具apigcc

    一个非侵入的api编译.收集.Rest文档生成工具.工具通过分析代码和注释,获取文档信息,生成RestDoc文档 前言 程序员一直以来都有一个烦恼,只想写代码,不想写文档.代码就表达了我的思想和灵魂. ...

随机推荐

  1. 【Python】自己写日志功能

    Python有自带的logging模块,用于日志记录,功能很强大,但不好用,使用挺麻烦的,而且发现了几个bug,调用了一个logger.warning()一次,结果日志文件中出现了n行记录,且逐渐变成 ...

  2. CVE-2019-7238 poc

    from requests.packages.urllib3.exceptions import InsecureRequestWarning import urllib3 import reques ...

  3. PAT 1036 Boys vs Girls (25分) 比大小而已

    题目 This time you are asked to tell the difference between the lowest grade of all the male students ...

  4. Java实现 LeetCode 442 数组中重复的数据

    442. 数组中重复的数据 给定一个整数数组 a,其中1 ≤ a[i] ≤ n (n为数组长度), 其中有些元素出现两次而其他元素出现一次. 找到所有出现两次的元素. 你可以不用到任何额外空间并在O( ...

  5. Java实现 LeetCode 145 二叉树的后序遍历

    145. 二叉树的后序遍历 给定一个二叉树,返回它的 后序 遍历. 示例: 输入: [1,null,2,3] 1 \ 2 / 3 输出: [3,2,1] 进阶: 递归算法很简单,你可以通过迭代算法完成 ...

  6. Java实现 LeetCode 137 只出现一次的数字 II(二)

    137. 只出现一次的数字 II 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次.找出那个只出现了一次的元素. 说明: 你的算法应该具有线性时间复杂度. 你可以不使用额外空 ...

  7. java实现三进制转十进制

    ** 三进制转十进制** 不同进制的数值间的转换是软件开发中很可能 会遇到的常规问题.下面的代码演示了如何把键盘输入的3 进制数字转换为十进制.试完善之. BufferedReader br = ne ...

  8. java实现欧拉与鸡蛋

    ** 欧拉与鸡蛋** 大数学家欧拉在集市上遇到了本村的两个农妇,每人跨着个空篮子.她们和欧拉打招呼说两人刚刚卖完了所有的鸡蛋. 欧拉随便问:"卖了多少鸡蛋呢?" 不料一个说:&qu ...

  9. 【Jquery】根据元素个数给予宽度

    方法一: $(document).ready(function(){ $(".xn_mc_solu_2_ul").css("width", $(".x ...

  10. Jmeter让压测随时做起来(转载)

    为什么要压测 这个问题问的其实挺没有必要的,做开发的同学应该都很清楚,压测的必要性,压力测试主要目的就是让我们在上线前能够了解到我们系统的承载能力,和当前.未来系统压力的提升情况,能够评估出当前系统的 ...