一个非侵入的Go事务管理库——工作原理
在上一篇文章“一个非侵入的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"
索引:
2 "清晰架构(Clean Architecture)的Go微服务: 事物管理"
4 "database/sql Tx — detecting Commit or Rollback"
5 "Applying the Saga Pattern - GOTO Conference"
6 "database/sql: nested transaction or save point support"
一个非侵入的Go事务管理库——工作原理的更多相关文章
- 一个非侵入的Go事务管理库——如何使用
在文章"清晰架构(Clean Architecture)的Go微服务: 事物管理"中,我谈到了如何在清晰架构中实现非侵入的事务管理. 它允许你把事务代码与业务逻辑代码分开,并且让你 ...
- Vuex 状态管理的工作原理
Vuex 状态管理的工作原理 为什么要使用 Vuex 当我们使用 Vue.js 来开发一个单页应用时,经常会遇到一些组件间共享的数据或状态,或是需要通过 props 深层传递的一些数据.在应用规模较小 ...
- linux 文件系统的管理 (硬盘) 工作原理
一.系统在初始化时如何识别硬盘 1.系统初始时根据MBR的信息来识别硬盘,其中包括了一些执行文件就来载入系统,这些执行文件就是MBR里前面446bytes里的boot loader 程式,而后面的16 ...
- Spring事务管理详解_基本原理_事务管理方式
1. 事务的基本原理 Spring事务的本质其实就是数据库对事务的支持,使用JDBC的事务管理机制,就是利用java.sql.Connection对象完成对事务的提交,那在没有Spring帮我们管理事 ...
- Spring003--Spring事务管理(mooc)
Spring事务管理 一.事务回顾 1.1.什么是事务 事务指的是逻辑上的一组操作,这组操作要么全部成功,要么全部失败. 异常情况发生,需要保证:[1]张三将钱转出,李四收到钱.[2]张三钱未成功转出 ...
- SSM框架——以注解形式实现事务管理
上一篇博文<SSM三大框架整合详细教程>详细说了如何整合Spring.SpringMVC和MyBatis这三大框架.但是没有说到如何配置mybatis的事务管理,在编写业务的过程中,会需要 ...
- Spring学习笔记五:Spring进行事务管理
转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/6776256.html 事务管理主要负责对持久化方法进行统一的提交或回滚,Spring进行事务管理即我们无需在 ...
- 【spring源码学习】spring的事务管理的源码解析
[一]spring事务管理(1)spring的事务管理,是基于aop动态代理实现的.对目标对象生成代理对象,加入事务管理的核心拦截器==>org.springframework.transact ...
- Java非侵入式API接口即文档工具apigcc
一个非侵入的api编译.收集.Rest文档生成工具.工具通过分析代码和注释,获取文档信息,生成RestDoc文档 前言 程序员一直以来都有一个烦恼,只想写代码,不想写文档.代码就表达了我的思想和灵魂. ...
随机推荐
- [Node.js]001.安装与环境配置
安装与环境配置 第一步:下载安装文件 第二步:安装nodejs 第三步:npm安装 第四步:安装相关环境 第五步:安装CoffeeScript 第六步:CoffeeScript测试实例 第一步:下载安 ...
- [工具推荐]005.Axure RP Pro 7.0模拟C#TAB控件
有一次,主管安排我写一个项目的原型,但是项目中涉及到了Tab控件,在Axure中的控件中找了一番,没有找着Tab控件.那么我们只能换种法子来实现它了,我们用到了Dynamic Panel来模拟. 1. ...
- Parrot os KDE还是MATE版本
在经历了KDE桌面痛苦折磨后,准备转投MATE的怀抱,不得不说Parrot KDE的ram的占有和windows 10差不多,大量的图形化处理,让我本来不多的内存更加血上加霜. 所以,关于版本的推荐, ...
- Android简单应用程序破解——runtime.apk
对于<Debugging Android Application>一文中最后附上的练习,我采用了另一种静态方法绕开原有的逻辑去破解.主要的过程如下: 利用apktool将练习的runtim ...
- Python--numpy中的tile()函数
首先是官方给的定以(我是用的VsCode,鼠标放置在tile上出现的),建议直接看后面的示例. def tile(A, reps) Construct an array by repeating ...
- 【HIVE高级笔试必备题型】(组内topN、相邻行的值比较问题)求语文大于数学_/_求文科大于理科成绩的学生
Hive SQL练习之成绩分析 数据:[id, 学号,班级,科目,成绩] 1,1,1,yuwen,80 2,1,1,shuxue,85 3,2,1,yuwen,75 4,2,1,shuxue,70 5 ...
- Java实现 LeetCode 423 从英文中重建数字
423. 从英文中重建数字 给定一个非空字符串,其中包含字母顺序打乱的英文单词表示的数字0-9.按升序输出原始的数字. 注意: 输入只包含小写英文字母. 输入保证合法并可以转换为原始的数字,这意味着像 ...
- Java实现 蓝桥杯VIP 算法提高 密码锁
算法提高 题目 2 密码锁 时间限制:1.0s 内存限制:1.0GB 问题描述 你获得了一个据说是古代玛雅人制作的箱子.你非常想打开箱子看看里面有什么东西,但是不幸的是,正如所有故事里一样,神秘的箱子 ...
- Java实现 黑洞数
任意一个5位数,比如:34256,把它的各位数字打乱,重新排列,可以得到一个最大的数:65432,一个最小的数23456.求这两个数字的差,得:41976,把这个数字再次重复上述过程(如果不足5位,则 ...
- 对LinkedList源码的一些个人理解
由于转行的原因,最近打算开始好好学习,昨天看到了部分的LinkedList源码,并且看了一点数据结构的视频,现总结部分自己的心得体会,以供后期给现在的自己拍砖~ 双向链表每一个元素都有数据本身加指向前 ...