gorm中使用乐观锁

news/2024/11/15 17:22:36/文章来源:https://www.cnblogs.com/niuben/p/18328748

乐观锁简介

乐观锁(又称乐观并发控制)是一种常见的数据库并发控制策略。

乐观并发控制多数用于数据竞争(data race)不大、冲突较少的环境中,这种环境中,偶尔回滚事务的成本会低于读取数据时锁定数据的成本,因此可以获得比其他并发控制方法更高的吞吐量。 它的作用是防止并发更新数据库中的数据,从而避免数据的混乱。

实现乐观锁的核心要素

乐观锁由以下几个要素组成:

  1. table 中增加一列,用于记录此行数据的版本号

  2. 更新数据前,先读取当前数据行的版本号

  3. 更新时,对 UPDATE 语句作两处调整:

(1) WHERE 语句中加入版本号的比较条件,确保只有当前版本号与数据库中的版本号一致时才执行更新

WHERE ... and version = [current version]

(2) UPDATE 语句中递增版本号以保证每次更新后版本号都会变化

UPDATE set  ..., version = version + 1
  1. SQL 执行以后需要检查更新行数是否为0,如果为0则说明有更新冲突,需要重试直到成功为止

在GORM中使用乐观锁

GORM是基于plugin架构的,GORM官方团队提供了这个plugin: go-gorm/optimisticlock。有了这个 plugin,在 GORM 中使用乐观锁就非常简单了。

安装这个插件:

go get -u gorm.io/plugin/optimisticlock

要使用乐观锁, 首先需要在 GORM model 中增加一个类型为 optimisticlock.Version 的版本字段:

import ("gorm.io/plugin/optimisticlock"
)
...
type Blog struct {Id      intTitle stringContent string// add version column to support optimistic lock 引入乐观锁版本号Version optimisticlock.Version
}

增加了这个字段以后,GORM 的更新操作就自动支持乐观锁了。

由于增加了版本判断,所以发生更新冲突时,更新行数将是0,这意味着此次更新失败,需要将此错误返回,通知调用方重试。

下面示例代码演示了如何更新Blog的标题字段:

func UpdateTitle(db *gorm.DB, id int, title string) error {blog := &Blog{}// load blog with latest versionif err := db.Take(blog, id).Error; err != nil {return err}blog.Title = title// SQL: UPDATE blogs SET title = ?, version = version + 1 WHERE id = ? and version = ?// 更新用户信息,Gorm 会自动处理乐观锁逻辑result := db.Model(blog).Update("title", blog.Title)if err := result.Error; err != nil {return err    }// version conflict occurred if result.RowsAffected == 0 {return ErrOptimisticLock}return nil
}

冲突处理策略

当乐观锁冲突发生时,开发者需要决定如何处理这种情况。通常,有以下几种策略:

  1. 重试:重新读取数据,并尝试再次执行更新操作。
  2. 回滚:取消当前操作,并通知用户操作失败。
  3. 自定义逻辑:根据业务需求,执行特定的错误处理逻辑。

实际示例:乐观锁在并发场景中的应用

假设我们有一个在线购物系统,用户可以查看商品并将其添加到购物车。在这个场景中,我们希望确保用户在添加商品到购物车时,商品的库存数量是准确的。我们可以通过乐观锁来实现这一目标。

商品模型与库存管理

首先,我们定义一个商品模型,并在其中添加一个版本号字段:

type Product struct {ID        intName      stringPrice     intQuantity  intVersion   optimisticlock.Version // 版本号用于乐观锁
}

当用户尝试添加商品到购物车时,我们首先查询商品的当前库存和版本号,然后尝试更新库存数量。如果库存足够,我们减少库存数量并更新版本号。如果在这个过程中,库存被其他用户更新了,乐观锁会捕获到版本号的变化,并拒绝这次更新。

func AddToCart(db *gorm.DB, productID int, quantity int) (bool, error) {var product Product// 查询商品信息和版本号if err := db.First(&product, "id = ?", productID).Error; err != nil {return false, err}// 检查库存是否足够if product.Quantity < quantity {return false, nil // 库存不足}// 更新库存和版本号if err := db.Model(&product).Update("quantity", product.Quantity-quantity).Error; err != nil {if errors.Is(err, optimisticlock.ErrOptimisticLock) {// 乐观锁冲突,需要重新尝试return false, nil}return false, err}return true, nil
}

在这个示例中,我们通过乐观锁确保了在并发环境下,商品库存的更新是安全的。如果发生冲突,我们可以通知用户重新尝试操作,或者采取其他补救措施。

go-gorm/optimisticlock 影响了哪些方法

由于 go-gorm/optimisticlock 是用 GORM plugin 机制实现的。 所以所有支持plugin的更新和插入方法会受影响,这包括:

  1. Update
  2. Updates
  3. Create

有些方法不支持plugin,因此不会受影响:

  1. UpdateColumn
  2. UpdateColumns

注意: DB.Save与此plugin有冲突

前面列出的方法不包括 DB.Save,因为它在特定场景下不能正常工作。

DB.Save 支持plugin,所以它生成的SQL也会被自动修改。

我们知道 Save 既支持数据插入也支持更新:

  • 当 model 主键为空值时,Save 的行为与 Create 相同,这种情况下没有问题,
  • 当 model 主键不为空时,Save 会更新全部字段。这时会出现bug

下面我们尝试用 DB.Save 来更新全部字段,用这个示例说明问题所在:

func UpdateAll(db *gorm.DB, blog *Blog) error {// save blog // bug: will return primary key duplicate error in case update conflictresult := db.Save(blog)if err := result.Error; err != nil {return err    }// bug: never execute if result.RowsAffected == 0 {return ErrOptimisticLock}return nil
}

当发生更新冲突时,UpdateAll 并没有返回我们期望的 ErrOptimisticLock,而是返回了 duplicate key value violates ... 错误。

这是相同主键重复插入时才会出的错误。

为什么会这样? 答案在 DB.Save 的源码里:

// Save update value in database, if the value doesn't have primary key, will insert it
func (db *DB) Save(value interface{}) (tx *DB) {...tx = tx.callbacks.Update().Execute(tx)if tx.Error == nil && tx.RowsAffected == 0 && !tx.DryRun && !selectedUpdate {result := reflect.New(tx.Statement.Schema.ModelType).Interface()if result := tx.Session(&Session{}).Limit(1).Find(result); result.RowsAffected == 0 {return tx.Create(value)}}...    
}

对照下面 DB.Save 的流程图,会发现问题的根源在 tx.RowsAffected == 0

发生更新冲突时 RowsAffected 将是 0。

而这会导致 DB.Save 再次执行 Insert 操作,此时的主键不是空,所以会出现重复主键的错误。

要避免此问题,需要用 Updates 替换 Save

同时要注意 Updates 默认只更新"非空"字段,需要加上 db.Select("*") 才能更新全部字段。

修正后的方法如下:

func UpdateAll(db *gorm.DB, blog *Blog) error {// make sure update all fieldsresult := db.Select("*").Updates(blog)if err := result.Error; err != nil {return err    }if result.RowsAffected == 0 {return ErrOptimisticLock}return nil
}

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

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

相关文章

lapce源码学习-启动过程

一、记录日志 1.1、panic恐慌(异常)记录跟踪 调用堆栈,如果有源代码信息时,则记录文件路径、行、列、堆栈;否则仅记录堆栈。 window系统,调用win32接口弹出模态框窗口提醒。1.2、链路追踪日志,输出到文件和控制台配置 文件:文件数目、文件前缀/后缀名、文件收集各目标日…

模块3 面向对象编程高级 --- 第十一章:异常处理

第十一章 异常处理 主要知识点1、异常产生的原因2、标准异常类3、Java的异常处理机制4、异常的创建5、异常的抛出6、异常语句的编程 学习目标熟悉异常产生的原因和标准异常类的用法。能够运用异常处理机制编写Java程序,提高安全性; 11.1 异常的分类一旦出现异常,系统将会立刻…

Win10资源管理器文件夹错乱

昨天晚上我不知道怎么搞得,鼠标在资源管理器界面,我手放在触控板上,不知道拖拽了啥,把所有文件搞到桌面文件夹下了。然后焦虑了一天,都打算从还原点还原了,然后今天早上更新了Windows,发现最新的还原点就在今早。。。无奈之下想到了去问问ChatGPT,然后居然它真的给了我…

[Redis]原子性

事务 为了确保连续多个操作的原子性,一个成熟的数据库通常都会有事务支持,Redis也不例外。 Redis的事务使用方法非常简单 不同于关系数据库我们无须理解那么多复杂的事务模型就可以直接使用。不过也正是因为这种简单性它的事务模型很不严格这要求我们不能像使用关系数据库的事…

[米联客-安路飞龙DR1-FPSOC] FPGA基础篇连载-01 软件工具环境搭建

软件版本:Anlogic -TD5.9.1-DR1_ES1.1 操作系统:WIN10 64bit 硬件平台:适用安路(Anlogic)FPGA 实验平台:米联客-MLK-L1-CZ06-DR1M90G开发板 板卡获取平台:https://milianke.tmall.com/ 登录"米联客"FPGA社区 http://www.uisrc.com 视频课程、答疑解惑! 1代码编…

【Python】Django学习1

按黑马程序员的美多商场作方向:https://www.bilibili.com/video/BV1nf4y1k7G3一、应用创建、注册处理、配置 Pycharm 创建Django项目: 自应用注册处理: 二、应用数据初始化 第一步:创建元数据初始化py脚本python manage.py makemigrations初始化的脚本会放在各个自应用的…

catboost库作用与安装

CatBoost是一种机器学习库,‌由俄罗斯的搜索巨头Yandex在2017年开源。‌它属于Boosting族算法的一种,‌与XGBoost、‌LightGBM并称为GBDT的三大主流神器。‌CatBoost的主要作用和创新点在于高效合理地处理类别型特征,‌这是从其名称中的"Cat"(‌分类)‌和"…

xgboost的作用与库的安装

XGBoost是一个非常强大的Boosting算法工具包,‌以其优秀的性能(‌效果与速度)‌在数据科学比赛中长期占据领先地位,‌并且在许多大厂的机器学习方案中也是首选模型。‌ XGBoost在并行计算效率、‌缺失值处理、‌控制过拟合、‌预测泛化能力等方面表现出色。‌它的主要特点和…

设计模式:代理、装饰和适配器模式的区别

适配器模式、代理模式、装饰模式结构对比 讲实话,博主当初学习完整设计模式时,这三种设计模式单独摘哪一种都是十分清晰和明确的,但是随着模式种类的增加,在实际使用的时候竟然会出现恍惚,例如读开源代码时,遇到不以模式命名规范的代码时,一时难以说清具体是使用的这三种…

nand2tetris_hack计算机

构建Hack CPU,然后将CPU与RAM集成在一起,创建一个能够执行Hack机器语言的通用16位计算机终于来到了这一步!! 前文里,我们学习了hack编程语言,大概知道需要实现的hack计算机是什么样子,需要实现哪些功能。同时在更早的时候,我们建造了ALU和RAM组件,加上老师内置的ROM和…

暑假集训csp提高模拟10

赛时 rank 19,T1 0,T2 25 T3 10 T4 100 T3 挂了10pts? 数学专场,套路专场,烧脑专场。 幸亏我还有缓存的李超树博客,最后一个小时就溜了去打数据结构。 数学好难,拜谢数学。 T1 黑暗型高松灯 Company Acquisitions 要用势能分析,鞅的停时定理。由于赛时这个放T1非常逆天,…

Java-002

final关键字 final 关键字是最终的意思,可以修饰(类、方法、变量) 修饰类:该类被称为最终类,特点是不能被继承了。 修饰方法:该方法被称为最终方法,特点是不能被重写了。 修饰变量:该变量只能被赋值一次。 final修饰的变量必须赋值,要么在定义时赋值,要么在构造器中赋…