当前位置: 首页 > news >正文

Golang sql 事务如何进行分层

在写代码过程中遇到了需要使用gorm执行sql事务的情况,研究了一下各位大佬的实现方案,结合了自身遇到的问题,特此记录。



代码架构介绍

.
├── apis
├── config
├── internal
│   ├── constant
│   ├── controller
│   ├── logic
│   ├── model
│   ├── repo
│   ├── router
│   └── service
├── library
├── pkg
├── scripts

目录结构其实很简单,主要参考了goframe的架构。internal/logic放逻辑代码,internal/repo是dao层,负责与数据库进行交互。



演变过程

最开始的时候是直接把对事务操作放在repo层中了。比如说有两张表,一张用户消费记录表,一张用户积分表,在用户消费完成后就要增加对应的积分。那么在repo层就会有一个函数负责开启事务,写消费记录表,写积分表,提交事务。在logic层中只需要调用这个函数就可以同时完成这两个操作。


看起来很美好,逻辑分的很清晰,但是这么做其实有个问题。如果logic层中有逻辑只操作消费记录表呢,或者用户消费积分只操作积分表呢,或者又有其他的表需要进行联合操作。这种写法会在repo层不断增加一个又一个函数,导致函数越来越多,因为承载了过多的业务逻辑。


发现这种情况之后,我们想可不可以把业务逻辑从repo层抽离,放在logic层进行处理,repo层只做简单的增删改查。那么repo层就可以只实现几个基础的函数,Create(),Update(),Selete()等,然后在logic层通过调用这几个函数实现复杂的逻辑操作。还是之前那个例子,比如说先消费再增加积分就可以变成对两个函数调用。

	// logic 层func buy() {tx := db.begin()cosume.Create(tx)point.Update(tx)tx.commit()}

分层是明确了,但是又引出了新的问题,就是如果我想使用事务,那么就需要在logic层对事务进行开启和提交,并且logic层还需要保存db的连接(用于开启事务),并且将tx传给repo层进行操作,这不合理。logic应该专注于代码逻辑的处理,而不是如何开启一个数据库事务。
目标很明确,如何让logic层只关注与逻辑的操作,而不是开启事务。上文也说到repo层只做一些简单的增删改查的操作,在repo里实现是不可能了,那么只能采取终极方案,在logic和repo之间增加一层中间层,专门用于开启事务。
这个中间层更像一个管理层,用于管理具体操作数据库的repo类和事务的操作,当需要开启事务时,logic只需要编写好业务逻辑,然后调用管理层的接口。管理层负责开启事务,调用logic层编写好的逻辑,提交/回滚事务即可。
这样logic层既不用处理事务相关的任务,repo层也不需要处理复杂的业务逻辑。



实现方案

总结一下上面所说,我们需要实现一个管理类,在管理类里面提供一个事务接口,这个接口的入参有一个func,这个func就是logic层编写好的各种逻辑,为了将tx传递给repo,我们是将其放在了ctx中。

管理层示例

Internal/repo/transaction.go

这个代码参考的时 gorm.Transaction(),做了简单修改。注释写的很详细了。

// Mysql Mysql数据库
type Mysql struct {db *gorm.DB
}func (m *Mysql) Transaction(ctx context.Context, fc func(txCtx context.Context) error) error {panicked := truevar err error// 拿到 ctx 里面的 db 连接,如果没有则使用默认的 db 连接db := m.getDB(ctx)// ...// 使用这个连接开启事务tx := db.WithContext(ctx).Begin()if tx.Error != nil {return tx.Error}defer func() {// Make sure to rollback when panic, Block error or Commit errorif panicked || err != nil {tx.Rollback()}}()// 将事务的 tx 写入 ctx 中,为了真正操作数据库的函数拿到 txtxCtx := generateContextWithDB(ctx, tx)// 调用 logic 层传入的 funcif err = fc(txCtx); err == nil {panicked = false// 如果没有报错就提交事务return tx.Commit().Error}panicked = false// 把事务内的报错转发出去return err
}

调用示例

/internal/logic/manager.go

func (m *manager) Buy(ctx context.Context) error {// 调用 Transaction 函数,txCtx 则为带有 tx 的 ctxif err := m.mysql.Transaction(ctx, func(txCtx context.Context) {// 这里只需要实现对应的业务即可,无需关注事务的开启和提交/回滚逻辑m.mysql.Consume(txCtx).Create()m.mysql.Point(txCtx).Update()}); err != nil {// do something}return nil
}

从 manager 这个函数的角度来看,整个事务执行的逻辑为,通过 Transaction 第一个参数 ctx 获取要执行事务的 db 连接,开启事务后将 tx 封到 context 中,然后传入 Transaction 第二个参数 func() 的入参中。通过将 txCtx 传入到不同的 repo 类中来实现各个 repo 使用同一个 tx 进行事务的操作。Transaction 函数保证了中间有任何错误都会进行回滚和无错误的提交操作。

Consume()和Point()做的是解析 txCtx 中的 db 连接,然后 new 一个repo 类返回。getDB() 负责返回 ctx 里的 db 接。

func (m *Mysql) Consume(ctx context.Context) ConsumeRepo {return NewConsumeRepo(m.getDB(ctx))
}

整体的代码逻辑就是这样,如果需要支持多个引擎,其实可以考虑抽出一个interface,各个引擎实现这个 interface 的接口即可。



gorm 一些写法

多表联查

我们的表设计的不是很规范,导致表和表之间基本上没有什么关联,外键也没有,所以没有办法使用gorm提供的preload,采取的是手动join的方式。

func (c *ConsumePoint) SelectForUpdate(ctx context.Context) (*model.ConsumeAndPoint, error) {// 接收数据的结构var record model.ConsumeAndPointif err := c.db.WithContext(ctx).// 主表Table("t_consume").// 返回什么Select("t_consume.*, t_point.f_point").// 连接方式Joins("join t_point on t_consume.f_user_id = t_point.f_user_id").// 只查询一条数据First(&record).Error; err != nil {// do something}return &record, nil
}

select for update

先读后更新的数据竞争且应该将加锁操作放到事务中,防止锁被自动释放,其实主要就在于 Set(“gorm:query_option”, “FOR UPDATE”) 这一行。如果采用上面多表联查的方式,设置了 FOR UPDATE 后两张表的内容都会有X锁,直到事务提交。可以用这个特性来搞分布式的竞争更新。

func UpdateUser(db *gorm.DB, id int64) error {tx := db.Begin()defer func() {if r := recover(); r != nil {tx.Rollback()}}()if err := tx.Error; err != nil {return err}user := User{}// 锁住指定 id 的 User 记录if err := tx.Set("gorm:query_option", "FOR UPDATE").First(&user, id).Error; err != nil {tx.Rollback()return err}// 更新操作...// commit事务,释放锁if err := tx.Commit().Error; err != nil {return err}return nil
}

相关文章:

Golang sql 事务如何进行分层

在写代码过程中遇到了需要使用gorm执行sql事务的情况,研究了一下各位大佬的实现方案,结合了自身遇到的问题,特此记录。 代码架构介绍 . ├── apis ├── config ├── internal │ ├── constant │ ├── controller │ ├──…...

linux系统openssh升级

linux系统openssh升级 说明 整个过程不需要卸载原先的openssl包和openssh的rpm包。本文的环境都是系统自带的openssh,没有经历过手动编译安装方式。如果之前有手动编译安装过openssh,请参照本文自行测试是否能成功。 如果严格参照本文操作,保…...

力扣-求关注者的数量

大家好,我是空空star,本篇带大家了解一道简单的力扣sql练习题。 文章目录前言一、题目:1729. 求关注者的数量二、解题1.正确示范①提交SQL运行结果2.正确示范②提交SQL运行结果3.正确示范③提交SQL运行结果4.正确示范④提交SQL运行结果5.正确…...

近红外荧光染料修饰氨基IR 825 NH2,IR 825-Amine,IR-825 NH2

IR 825 NH2,IR 825-NH2,IR825 Amine,IR825-Amine,新吲哚菁绿-氨基,荧光染料修饰氨基产品规格:1.CAS号:N/A2.包装规格:10mg,25mg,50mg,包装灵活&am…...

Android Crash和ANR监控

文章目录一、Crash1.1 概念1.2 类型二、ANR2.1 概念2.2 类型2.2.1 KeyDispatchTimeout(常见)2.2.2 BroadcastTimeout2.2.3 ServiceTimeout2.2.4 ContentProviderTimeout三、测试中如何关注3.1 Crash测试关注方法3.2 ANR测试关注方法四、如何记录与处理4.…...

【02 赖世雄英语语法:复句的语法】

复句的语法复句:把单句 连在一起(标点符号,连词,关系词)1. 连接符号1.1 破折号 — :补充说明,同位语1.2 冒号 : (同位语)1.3 分号 ; ( , 连词)&am…...

北斗导航 | 多参考一致性监测算法(MRCC)(附伪码)—— B值计算

===================================================== github:https://github.com/MichaelBeechan CSDN:https://blog.csdn.net/u011344545 ===================================================== MRCC 用于接收机失效检查与排除。在进行 MRCC 之前,先判断 4 台接收机…...

数字孪生与 UWB 人员定位:双剑合璧的智能物联新时代

人员定位是指利用各种定位技术对人员在特定场所的位置进行准确定位的技术。人员定位技术主要应用于需要实时监控、管理和保障人员安全的场所,如大型厂区、仓库、医院、学校、商场等。人员定位技术的应用范围非常广泛,例如:-在工厂生产线上&am…...

奇点云DataSimba发版全解析:“企业级”版本升级,提供最佳组合

近日,奇点云发布数据云产品商业化版本的全新升级:DataSimba(数据云平台)提供极速版、专业版、旗舰版、红旗版,可靠性、可用性、可服务性再进阶,四大版本满足不同企业选择。 「乐高式DIY」or「最佳组合」&am…...

2-7 SpringCloud快速开发入门: Eureka 注册中心高可用集群搭建

接上一章节Eureka 服务注册中心发现与消费服务,这里讲讲Eureka 注册中心高可用集群搭建 Eureka 注册中心高可用集群搭建 Eureka 注册中心高可用集群就是各个注册中心相互注册 Eureka Server的高可用实际上就是将自己作为服务向其他服务注册中心注册自己&#xff0c…...

STL中的函数对象

STL-函数对象 函数对象概念 重载函数调用操作符的类,其对象常称为函数对象函数对象使用重载的()时,行为类似函数调用,也叫仿函数 本质 函数对象(仿函数)是一个类,不是一个函数—修改算法策略—采用虚拟对象调用 函数对象的使用理…...

linux下libevent的编译安装

1,官网下载最新的,https://libevent.org/2,将文件解压,虚拟机可以右键解压,也可以用命令解压,tar zxvf libevent.tar.gz,进行解压缩。这里压缩包的名称只是举例,实际它还会带上版本号…...

深度学习部署笔记(十): CUDA RunTime API-2.2流的学习

1. 流的定义 流(Stream)是一个基于上下文(Context)的任务管道抽象,是一组由GPU依次执行的CUDA操作序列,其中每个操作可能会使用或产生数据。在一个上下文中可以创建多个流,每个流都拥有自己的任…...

[ROC-RK3568-PC] [Firefly-Android] 10min带你了解I2C的使用

🍇 博主主页: 【Systemcall小酒屋】🍇 博主追寻:热衷于用简单的案例讲述复杂的技术,“假传万卷书,真传一案例”,这是林群院士说过的一句话,另外“成就是最好的老师”,技术…...

工作记录:举步维艰的在线 word 之旅 - tinymce

项目中需要实现 “在线编辑 word 模板” 的功能,我打算使用富文本组件 tinymce ,因为业务需求比较特殊,研究一下 tinymce 是否能实现。 如何在 vue 项目中引用 tinymce,可以看另一篇文章 《在 vue 项目中使用 tinymce》 &#x…...

动态规划编译距离

583. 两个字符串的删除操作方法:dp状态表示:以i-1和j-1为结尾的字符串world1和world2,抵达相同的字符串所需的最少操作数属性:最小值状态计算:world1[i-1]和world2[j-1]相同dp[i][j] dp[i-1][j-1];world1[i-1]和world…...

Netty 教程 – 解码器详解

TCP以流的方式进行数据传输,上层的应用为了对消息进行区分,往往采用如下方式 固定消息长度,累计读取到长度和定长LEN的报文后,就认为读取到了个完整的消息,然后将计数器位置重置在读取下一个报文内容将回车换行符作为…...

Allegro如何自动添加测试点操作指导

Allegro如何自动添加测试点操作指导 在做PCB设计的时候,在一些应用场合下需要给PCB上的网络添加测试点,如下图 测试点除了可以手动逐个添加之外,Allegro还支持自动添加测试点,具体操作如下 点击Manufacture点击Testprep...

【CSS】CSS 背景设置 ③ ( 背景位置-长度值设置 | 背景位置-长度值方位值同时设置 )

文章目录一、背景位置-长度值设置二、背景位置-长度值方位值同时设置三、完整代码示例一、背景位置-长度值设置 长度值设置 效果展示 : 设置背景位置为具体值 10px 50px : 粉色区域是盒子的区域 , 图片背景位于盒子位置 x 轴方向 10 像素 , y 轴方向 50 像素 ; 在水平方向上 ,…...

AbTest —— 不同场景下的应用模式

文章目录不同人群眼中的 AbTestAbTest 不同的功能倚重用户关联性弱,经典场景为 Feed - 部门组织形式大多非垂直业务用户关联性强,经典场景为 垂类/工具类APP;部门组织形式大多为垂直业务康为定律-组织决定产品形态不同应用模式下服务构建开机…...

fast-api 一款快速将spring的bean发布成接口并生产对应swagger文档调试的轻量级工具

fast-api简介背景开发痛点:分析需求实战fast-api快速上手1. 引入依赖2. FastApiMapping标记service对象3. swagger2/knife4j 在线测试进阶使用开启调试模式支持指定类或包目录发布如何关闭fast-api自定义fast-api的前缀写在最后简介 fast-api 一款快速将spring的bean(service)发…...

以公益之名 让人类发现数学之美

目录 1.品牌理念高举高打 2.创新赛制 赋能品牌 3.全球化的品牌传播 9月26日,2022阿里巴巴全球数学竞赛获奖名单公布,4座金杯分别由平均年龄25岁,来自美国麻省理工学院、美国布朗大学、北京大学在读数学博士斩获。77位获奖者中00后超五成引热…...

JUC并发编程之HashMap(jdk1.7版本)-底层源码探究

目录 JUC并发编程之HashMap(jdk1.7版本)-底层源码探究 HashMap底层源码 - jdk1.7 基本概念 -采取层层递进,问答式 存储Key-Value的结构 常量和成员变量 构造方法 put方法 inflateTable方法 hash方法 indexFor方法 addEntry方法 resize方法 createEntry…...

QT Q_OBJECT 和 signals/slots

Q_OBJECT宏展开 #define Q_OBJECT \ public: \QT_WARNING_PUSH \Q_OBJECT_NO_OVERRIDE_WARNING \static const QMetaObject staticMetaObject; \virtual const QMetaObject *metaObject() const; \virtual void *qt_metacast(const char *); \virtual int qt_metacall(QMetaOb…...

APM新添加UAVCAN设备

简介 UAVCAN是一种轻量级协议,旨在通过CAN总线在航空航天和机器人应用中实现可靠通信。要实现通信,最基本需要data_type_ id, signature、数据结构、设备程序初始化。 添加设备数据结构文件(.uavcan格式) 1.在以下路径添加设备数据结构文件,根据设备类…...

【C++】string类基本用法

文章目录string类基本用法1. 为什么要学习string类?1.1 C语言中的字符串2. 标准库中的string类2.1 string类2.2 string类的常用接口说明小试牛刀1. 仅仅反转字母2. 字符串中第一个唯一字符3. 字符串中最后一个单词的长度string类基本用法 1. 为什么要学习string类&…...

KDZD耐电压高压击穿强度测试仪

一、技术参数 01、输入电压: 交流 220 V。 02、输出电压: 交流 0--50KV ; 直流 0—50kv 。 03、电器容量:3KVA。 04、高压分级:0—50KV,(全程可调)。 05、升压速率:0.1KV/s-…...

数组和指针面试题的补充(细的抠jio)

生命是一条艰险的峡谷&#xff0c;只有勇敢的人才能通过。 ——米歇潘 说明&#xff1a;用的vs都是x86的环境&#xff0c;也就是32位平台。 建议&#xff1a;对于难题来说&#xff0c;一定要配合画图来解决问题。 第一题&#xff1a; #include<stdio.h> int…...

Java多线程基础

文章目录Java多线程基础一、什么是进程与线程&#xff1f;二、线程和进程的区别【重点】三、线程的创建方式【重点】1. 继承Thread类2. 实现Runnable接口3. lambda 表达式四、Thread的常见属性线程中断自己定义一个标志位Thread类提供的静态方法线程的状态Java多线程基础 一、…...

爆品分析第5期 | 一条视频带货3700+,这款斋月不锈钢厨具套装火了!

俗话说民以食为天&#xff0c;吃在任何一种文化中都占据重要的位置&#xff0c;要做出一道美味佳肴&#xff0c;除了食材、烹饪者的自身厨艺之外&#xff0c;还少不了一口好锅。新冠疫情以来&#xff0c;全世界范围内的封闭让很多人养成了居家做饭的习惯&#xff0c;不仅为厨具…...

php开发的大型网站有哪些/百度推广有哪些形式

我们都听说过微信小程序&#xff0c;但是你听说过用单片机开发的小程序吗&#xff1f; 世界上没有什么不可以做&#xff0c;只是你没想到。。。。。。见图&#xff0c;所见即所得&#xff1a;图中的手机可不是一般的手机&#xff0c;其内部包含了stm32单片机&#xff0c;我们叫…...

学生做网站怎么收费/商业软文代写

发文词&#xff1a;类似于新刊物的“发刊词”&#xff0c;我们也写两句发文词。笔者前些年关于SAP的文字主要包括“SAP那些事”系列文章中了&#xff0c;这些文章的视角主要是从顾问的角度进行描述的&#xff0c;侧重的也是系统功能和顾问职业的描述。 最近&#xff0c;笔者认…...

河北网站开发/怎么做一个小程序

源码中没有进行单位转换均以字节为单位,如有需要自行转换(1M 1024 字节)...

wordpress怎么添加接口/百度搜索榜排名

模板介绍 精美PPT模板设计&#xff0c;学术论文答辩通用PPT模板。一套论文答辩幻灯片模板&#xff0c;内含黑色,黄色多种配色&#xff0c;精美风格设计&#xff0c;动态播放效果&#xff0c;精美实用。 一份设计精美的PPT模板&#xff0c;可以让你在汇报演讲时脱颖而出。 希…...

如何清空网站数据库/代运营哪家比较可靠

1. matplotlib 图例正常显示中文和正负号 import matplotlib.pyplot as plt plt.rcParams[font.sans-serif][SimHei] #用来正常显示中文标签 plt.rcParams[axes.unicode_minus]False #用来正常显示负号2. 给同一幅图中的不同线加标签 import matplotlib.pyplot as plt import …...

建设部网站 光纤到户/深圳推广系统

localStorage存储的变量可以在同域名的页面里互相访问&#xff0c;在不同域名的网页里是无法访问的&#xff0c;sessionStorage存储的是会话变量&#xff0c;tab关闭或者浏览器关闭之后就会销毁变量。 用http://localhost/1.html和http://www.cyb.com/1.html两个url访问&#…...