Golang数据库事务实践

事务是很多业务的基础,本文介绍了如何在Golang里实现数据库事务操作,并以一个用户注册场景给出了完整实现。原文: Transactions in Go application

alt

Go 是一种年轻而强大的语言,专为编写小型、简单的服务而创建。但随着时间推移,越来越多复杂应用和系统也在采用 Go 进行开发,这就出现了一些问题:如何处理事务?

为了深入探讨这个问题,我们假设一个简单的业务场景:用户注册

作为一个系统,我希望在注册时创建用户和个人资料

RDBMS/DBMS 的现代 Go 库不像 C# 和 Java 的 Hibernate、Entity Framework 那样强大,因此我们必须自己处理。为了实现用户注册业务场景,我们将创建并评估几种处理事务的方法。

由于每种事务处理方法都必须与 sql.DB 和 sql.Tx 配合使用,因此需要引入接口来封装对数据库的访问。

生成的应用有两个域实体和一个用于访问数据库的 DB 低级接口。

package model

type User struct {
 Email string
}

type Profile struct {
 Name string
}
package transaction

type DB interface {
 QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
 QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
 ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
 PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
}

准备工作完成后,就可以采用如下两种方法。

1. 事务感知上下文

工作原理:transaction.Manager启动事务并将其放入上下文。当存储库执行查询时,助手会检查上下文中是否有事务,并使用创建的事务来执行查询,或者如果上下文为空,则不使用事务来执行查询。

为了启动事务,我们需要实体:Manager

package transaction

type Manager interface {
 Run(
  ctx context.Context,
  callback func(ctx context.Context) error,
 ) error
}

transaction.Manager 实现

package transaction

import (
 "context"
 "database/sql"
 "github.com/pkg/errors"
 "go.uber.org/multierr"
)

type txKey string

var ctxWithTx = txKey("tx")

type SQLTransactionManager struct {
 db *sql.DB
}

func NewManager(db *sql.DB) *SQLTransactionManager {
 return &SQLTransactionManager{db: db}
}

func (m *SQLTransactionManager) Run(
 ctx context.Context,
 callback func(ctx context.Context)
 error,

) (rErr error) {
 tx, err := m.db.BeginTx(ctx, &sql.TxOptions{})
 if err != nil {
  return errors.WithStack(err)
 }

 defer func() {
  if rErr != nil {
   rErr = multierr.Combine(rErr, errors.WithStack(tx.Rollback()))
  }
 }()

 defer func() {
  if rec := recover(); rec != nil {
   if e, ok := rec.(error); ok {
    rErr = e
   } else {
    rErr = errors.Errorf("%s", rec)
   }
  }
 }()

 if err = callback(putTxToContext(ctx, tx)); err != nil {
  return err
 }

 return errors.WithStack(tx.Commit())
}

func ExtractTxFromContext(ctx context.Context) (*sql.Tx, bool) {
 tx := ctx.Value(ctxWithTx)

 if t, ok := tx.(*sql.Tx); ok {
  return t, true
 }

 return nilfalse
}

func putTxToContext(ctx context.Context, tx *sql.Tx) context.Context {
 return context.WithValue(ctx, ctxWithTx, tx)
}

DB实现

package storage

import (
 "brand/transaction/example1/transaction"
 "context"
 "database/sql"
)

type DB struct {
 db *sql.DB
}

func NewDB(db *sql.DB) *DB {
 return &DB{db: db}
}

func (d *DB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
 tx, ok := transaction.ExtractTxFromContext(ctx)
 if !ok {
  return d.db.QueryRowContext(ctx, query, args...)
 }

 return tx.QueryRowContext(ctx, query, args...)
}

func (d *DB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
 tx, ok := transaction.ExtractTxFromContext(ctx)
 if !ok {
  return d.db.QueryContext(ctx, query, args...)
 }

 return tx.QueryContext(ctx, query, args...)
}

func (d *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
 tx, ok := transaction.ExtractTxFromContext(ctx)
 if !ok {
  return d.db.ExecContext(ctx, query, args...)
 }

 return tx.ExecContext(ctx, query, args...)
}

func (d *DB) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
 tx, ok := transaction.ExtractTxFromContext(ctx)
 if !ok {
  return d.db.PrepareContext(ctx, query)
 }

 return tx.PrepareContext(ctx, query)
}

RegistrationService 负责用户注册业务场景

package service

import (
 "brand/transaction/example1/model"
 "brand/transaction/example1/transaction"
 "context"
)

type UserRepository interface {
 Create(ctx context.Context, user *model.User) error
}

type ProfileRepository interface {
 Create(ctx context.Context, user *model.Profile) error
}

type RegistrationData struct {
 Email string
 Name  string
}

type RegistrationService struct {
 transactionManager transaction.Manager
 userRepository     UserRepository
 profileRepository  ProfileRepository
}

func NewRegistrationService(
 transactionManager transaction.Manager,
 userRepository UserRepository,
 profileRepository ProfileRepository,
)
 *RegistrationService
 {
 return &RegistrationService{
  transactionManager: transactionManager,
  userRepository:     userRepository,
  profileRepository:  profileRepository,
 }
}

func (s *RegistrationService) Register(ctx context.Context, data RegistrationData) error {
 return s.transactionManager.Run(ctx, func(ctx context.Context) error {
  if err := s.userRepository.Create(ctx, &model.User{
   Email: data.Email,
  }); err != nil {
   return err
  }

  if err := s.profileRepository.Create(ctx, &model.Profile{
   Name: data.Name,
  }); err != nil {
   return err
  }

  return nil
 })
}

UserProfileRepository的实现

package storage

import (
 "brand/transaction"
 "brand/transaction/example1/model"
 "context"
)

type ProfileRepository struct {
 db transaction.DB
}

func NewProfileRepository(db transaction.DB) *ProfileRepository {
 return &ProfileRepository{db: db}
}

func (r *ProfileRepository) Create(ctx context.Context, profile *model.Profile) error {
 _, err := r.db.ExecContext(ctx, "INSERT ...", profile.Name)

 return err
}
package storage

import (
 "brand/transaction"
 "brand/transaction/example1/model"
 "context"
)

type UserRepository struct {
 db transaction.DB
}

func NewUserRepository(db transaction.DB) *UserRepository {
 return &UserRepository{db: db}
}

func (r *UserRepository) Create(ctx context.Context, user *model.User) error {
 _, err := r.db.ExecContext(ctx, "INSERT ...", user.Email)

 return err
}

优点

  • 简单:存储库会自动使用由 TransactionManager 启动的事务
  • 与存储无关:客户端代码对存储类型一无所知

缺点

  • 不符合Go的使用习惯
  • 控制较少:无法防止在事务中启动事务,可能会产生意想不到的副作用,代码审查时必须考虑到这一点
2. 事务感知存储库

工作原理:事务管理器启动事务并将事务放入回调,存储库工厂方法使用事务创建自己。

为了启动事务,我们需要实体:Manager

type Manager interface {
 Run(
  ctx context.Context,
  callback func(ctx context.Context, tx *sql.Tx) error,
 ) error
}

transaction.Manager 实现

package transaction

import (
 "context"
 "database/sql"
 "github.com/pkg/errors"
 "go.uber.org/multierr"
)

type txKey string

var ctxWithTx = txKey("tx")

type SQLTransactionManager struct {
 db *sql.DB
}

func NewManager(db *sql.DB) *SQLTransactionManager {
 return &SQLTransactionManager{db: db}
}

func (m *SQLTransactionManager) Run(
 ctx context.Context,
 callback func(ctx context.Context, tx *sql.Tx)
 error,

) (rErr error) {
 tx, err := m.db.BeginTx(ctx, &sql.TxOptions{})
 if err != nil {
  return errors.WithStack(err)
 }

 defer func() {
  if rErr != nil {
   rErr = multierr.Combine(rErr, errors.WithStack(tx.Rollback()))
  }
 }()

 defer func() {
  if rec := recover(); rec != nil {
   if e, ok := rec.(error); ok {
    rErr = e
   } else {
    rErr = errors.Errorf("%s", rec)
   }
  }
 }()

 if err = callback(ctx, tx); err != nil {
  return err
 }

 return errors.WithStack(tx.Commit())
}

DB实现

package storage

import (
 "context"
 "database/sql"
)

type DB struct {
 db *sql.DB
}

func NewDB(db *sql.DB) *DB {
 return &DB{db: db}
}

func (d *DB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
 return d.db.QueryRowContext(ctx, query, args...)
}

func (d *DB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
 return d.db.QueryContext(ctx, query, args...)
}

func (d *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
 return d.db.ExecContext(ctx, query, args...)
}

func (d *DB) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
 return d.db.PrepareContext(ctx, query)
}

RegistrationService 负责用户注册业务场景

有两种方法可以创建带有事务的存储库:

  • 存储库带有结构方法 WithTransaction(示例中使用了该方法)
  • 存储库工厂 userRepositoryFactory.CreateFromTransaction(tx)
package service

import (
 "brand/transaction/example2/model"
 "brand/transaction/example2/transaction"
 "context"
 "database/sql"
)

type UserRepository interface {
 Create(ctx context.Context, user *model.User) error
 WithTransaction(tx *sql.Tx) UserRepository
}

type ProfileRepository interface {
 Create(ctx context.Context, user *model.Profile) error
 WithTransaction(tx *sql.Tx) ProfileRepository
}

type RegistrationData struct {
 Email string
 Name  string
}

type RegistrationService struct {
 transactionManager transaction.Manager
 userRepository     UserRepository
 profileRepository  ProfileRepository
}

func NewRegistrationService(
 transactionManager transaction.Manager,
 userRepository UserRepository,
 profileRepository ProfileRepository,
)
 *RegistrationService
 {
 return &RegistrationService{
  transactionManager: transactionManager,
  userRepository:     userRepository,
  profileRepository:  profileRepository,
 }
}

func (s *RegistrationService) Register(ctx context.Context, data RegistrationData) error {
 return s.transactionManager.Run(ctx, func(ctx context.Context, tx *sql.Tx) error {
  userRepository := s.userRepository.WithTransaction(tx)
  profileRepository := s.profileRepository.WithTransaction(tx)

  if err := userRepository.Create(ctx, &model.User{
   Email: data.Email,
  }); err != nil {
   return err
  }

  if err := profileRepository.Create(ctx, &model.Profile{
   Name: data.Name,
  }); err != nil {
   return err
  }

  return nil
 })
}

UserProfileRepository的实现

package storage

import (
 "brand/transaction"
 "brand/transaction/example2/model"
 "brand/transaction/example2/service"
 "context"
 "database/sql"
)

type ProfileRepository struct {
 db transaction.DB
}

func NewProfileRepository(db transaction.DB) *ProfileRepository {
 return &ProfileRepository{db: db}
}

func (r *ProfileRepository) Create(ctx context.Context, profile *model.Profile) error {
 _, err := r.db.ExecContext(ctx, "INSERT ...", profile.Name)

 return err
}

func (r *ProfileRepository) WithTransaction(tx *sql.Tx) service.ProfileRepository {
 return NewProfileRepository(tx)
}
package storage

import (
 "brand/transaction"
 "brand/transaction/example2/model"
 "brand/transaction/example2/service"
 "context"
 "database/sql"
)

type UserRepository struct {
 db transaction.DB
}

func NewUserRepository(db transaction.DB) *UserRepository {
 return &UserRepository{db: db}
}

func (r *UserRepository) Create(ctx context.Context, user *model.User) error {
 _, err := r.db.ExecContext(ctx, "INSERT ...", user.Email)

 return err
}

func (r *UserRepository) WithTransaction(tx *sql.Tx) service.UserRepository {
 return NewUserRepository(tx)
}

优点

  • 更明确:在注册服务内部创建事务,可避免副作用

缺点

  • 客户端代码知道存储类型
  • 客户端代码负责创建新的存储库

我相信任何一种方法都能使代码更易读、更简单,但建议使用第一种方法,从而可以隐藏存储细节,使我们能够在一个项目中使用多个存储,而无需考虑实现和存储细节。


你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/788042.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redis数据类型介绍

字符串string: 字符串类型是Redis中最为基础的数据存储类型,是一个由字节组成的序列,他在Redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据,如JPEG图像数据货Json对象描述信息等,是标准的key…

分享几个可以免费使用的GPT网站吧

1. ChatGAI ChatGAI是一个界面简洁的AI平台,提供App和网页版,每日均有免费使用机会。 2. ChatGPT 本网站向大家开放了ChatGPT 3.5和4.0版本的免费体验,特别适合新用户。每天都有免费次数,响应迅速,注册便捷&#xff0…

Java中Filter和Interceptor的区别

概述 本文阐述Java中Filter和Interceptor的区别。 执行顺序不同 FIlter->Servlet->Interceptor->Controller 配置方式不同 FIlter在web.xml中配置 Interceptor在spring中的配置文件中、使用注解 是否依赖servlet Filter依赖servlet,而Interceptor不…

java中split(“.“)失效问题

来源:比较版本号_牛客题霸_牛客网 在写到这道算法题的时候,发现一个问题, String[] leftversion1.split("."); 返回结果为空,经过查阅得知,是split中的正则表达式里的问题,这个 . 代表的意思是…

深入理解数据结构第二弹——二叉树(2)——堆排序及其时间复杂度

看这篇前请先把我上一篇了解一下:深入理解数据结构第一弹——二叉树(1)——堆-CSDN博客 前言: 相信很多学习数据结构的人,都会遇到一种情况,就是明明最一开始学习就学习了时间复杂度,但是在后期…

12313124

c语言中的小小白-CSDN博客c语言中的小小白关注算法,c,c语言,贪心算法,链表,mysql,动态规划,后端,线性回归,数据结构,排序算法领域.https://blog.csdn.net/bhbcdxb123?spm1001.2014.3001.5343 给大家分享一句我很喜欢我话: 知不足而奋进,望远山而前行&am…

【与C++的邂逅】---- 函数重载与引用

关注小庄 顿顿解馋(▿) 喜欢的小伙伴可以多多支持小庄的文章哦 📒 数据结构 📒 C 引言 : 上一篇博客我们了解了C入门语法的一部分,今天我们来了解函数重载,引用的技术,请放心食用 ~ 文章目录 一. 🏠 函数重…

【JavaWeb】Day31.SpringBootWeb请求响应——分层解耦(二)

3.IOC&DI 3.1 IOC&DI入门 完成Controller层、Service层、Dao层的代码解耦 思路: 1. 删除Controller层、Service层中new对象的代码 2. Service层及Dao层的实现类,交给IOC容器管理 3. 为Controller及Service注入运行时依赖的对象 Controller程序…

隐私计算实训营第六讲-隐语PIR介绍及开发实践

隐私计算实训营第六讲-隐语PIR介绍及开发实践 文章目录 隐私计算实训营第六讲-隐语PIR介绍及开发实践1.隐语实现PIR总体介绍1.1按服务器数量分类1.2按查询类型分类 2. Index PIR - SealPIR3. Keyword PIR - Labeled PSI4.隐语PIR功能分层5.隐语PIR后续计划PIR协议开发PIR调用框…

JavaScript 数组元素交互最优解

利用 ES6 解构赋值: let arr [1, 2, 3, 4, 5];// 交互下标 1,4 元素的值 [arr[1], arr[4]] [arr[4], arr[1]];// 输出: [1, 5, 3, 4, 2] console.log(arr);浏览器控制台效果:

Node.js-知识点学习总结归纳

Node.js-知识点学习总结归纳 安装nodenode运行方式通过Node.js直接运行js文件(也就不用通过网页html了)绝对路径调用:相对路径调用:直接运行js命令: Vscode控制台使用node运行js文件 安装node 这个就不用讲了吧,网上搜…

【Python全栈】Python实现交通信号灯

文章目录 一、实现过程1. 导入必要的库和初始化Pygame2. 定义颜色和屏幕尺寸3. 创建交通信号灯、行人和车辆类4. 定义绘制函数draw5. 实例化交通信号灯、行人和车辆对象6. 创建并启动线程7. 游戏循环 二、代码 一、实现过程 1. 导入必要的库和初始化Pygame 导入Pygame库并初始…

硬件知识:点亮led

怎么做 1、看原理图,去顶控制LED的引脚 1.1led 1.2gpio 1、使能gpio模块,设置Power/clock control,向gpio模块提供电源和时钟 2.设置引脚模式为gpio模式 3.设置方向output/input4、读取引脚,获得当前电频到底是低电平/高电平;这…

ES集群部署的核心步骤

文章目录 引言I ES集群部署前期工作II 部署ES2.1 配置安全组2.2 创建ES用户和组2.3 下载安装ES2.4 修改内存相关配置III es集群添加用户安全认证功能3.1 生成 elastic-certificates.p123.2 创建 Elasticsearch 集群密码3.2 设置kibana的 elasticsearch帐号角色和密码3.3 logsta…

maya 遍历所有子节点

目录 MEL遍历所有子节点 python遍历所有子节点 MEL遍历所有子节点 // 定义一个递归函数来遍历所有子节点 proc traverseAllChildren(string $node) {// 获取直接子节点string $children[] listRelatives -children $node;// 遍历所有直接子节点for ($child in $children) {…

社会公益第三方评估

本文由群狼调研(公众项目第三方评估)出品,欢迎转载,请注明出处。社会公益第三方评估是由独立的组织或机构进行的对社会公益项目的评估和监督。这些第三方评估机构通常是独立于项目实施者和受益者的中立机构,旨在提供客…

LeetCode热题Hot100 - 正则表达式匹配

一刷~ 给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 . 和 * 的正则表达式匹配。 . 匹配任意单个字符* 匹配零个或多个前面的那一个元素 所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。 有几个用例超时,容我三思…

数据库:Redis数据库

一、非关系型数据库 1.什么是非关系型数据库 非关系型数据库(Non-relational Database)又称NoSQL数据库是一种不同于传统关系型数据库管理系统(RDBMS)的数据存储解决方案。NoSQL这个术语最初意味着"Not Only SQL"&…

静态资源不加载是否会影响性能吗

静态资源不加载或加载缓慢确实可能会影响网站或应用的性能。静态资源,如CSS、JavaScript、图片等,是网页的重要组成部分,它们负责网页的样式、交互和视觉效果。 如果静态资源不加载或加载失败,用户可能会看到一个没有样式或交互的…

1999-2022年上市公司员工人数数据

1999-2022年上市公司员工人数数据 1、时间:1999-2022年 2、指标:证券代码、时间、员工人数 3、来源:整理自csmar 4、范围:上市公司 5、指标解释: 上市公司员工人数是衡量公司规模和发展状的重要指标。该数据直接…