在上一篇文章“一个非侵入的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. MySQL所有的安装部署方式

    目录 一.前言 二.关于MySQL的安装 三.部署规划 3.1 服务器规划 3.2 数据库目录规划 四.准备工具 五.通用二进制包安装MySQL 5.1 上传MySQL通用二进制安装包到node7的/ ...

  2. springboot系列——重试机制原理和应用,还有比这个讲的更好的吗(附完整源码)

    1. 理解重试机制 2. 总结重试机制使用场景 3. spring-retry重试组件 4. 手写一个基于注解的重试组件 5. 重试机制下会出现的问题 6. 模板方法设计模式实现异步重试机制 如果有, ...

  3. css:选择器(标签、类、ID、通配符)

    1.css概述 主要的使用场景就是美化网页,布局页面 (1)html的局限性 它只关注内容的语义,只能做一些简单的样式,并且非常的臃肿和繁琐 (2)css对网页美化的作用 css是层叠样式表的简称,它 ...

  4. [COCOS2DX-LUA]0-002.在C++中如何接收Lua方法,然后tolua

    用过COCOS2DX-LUA的同学们,大家都知道回调函数这个东西,在Lua中,我们可以把一个方法当成是一个参数传递给另一个方法,然后在方法中进行调用.但是我们经常要写C++代码,然后再转成Lua,那么 ...

  5. 用python编写测试脚本

    def f(n): """ >>>f(1) 1用例 >>>f(2) 2用例 ...... >>>f(n) n用例 & ...

  6. jdk生成keystore、tomcat配置https

    一.首先利用jdk自带工具keytool生成keystore,命令如下:keytool -genkey -alias tomcat -keypass 123456 -keyalg RSA -keysi ...

  7. Nessus静态ip配置及内网扫描

    环境ubuntu虚拟机,以前linux配置ip都是从/etc/network/interfaces这里面更改,现在要在/etc/netplan下面配置. vim /etc/netplan/01-net ...

  8. Java试题错题集01

    1.javac与java命令 两个命令 javac 和 java. javac 后面跟着的是java文件的文件名,例如 HelloWorld.java. 该命令用于将 java 源文件编译为 clas ...

  9. Java实现 LeetCode 662 二叉树最大宽度(递归)

    662. 二叉树最大宽度 给定一个二叉树,编写一个函数来获取这个树的最大宽度.树的宽度是所有层中的最大宽度.这个二叉树与满二叉树(full binary tree)结构相同,但一些节点为空. 每一层的 ...

  10. Java实现蓝桥杯VIP 算法训练 矩阵乘方

    import java.util.Scanner; public class 矩阵乘方 { public static void main(String[] args) { Scanner scann ...