gorm中使用乐观锁
乐观锁简介
乐观锁(又称乐观并发控制)是一种常见的数据库并发控制策略。
乐观并发控制多数用于数据竞争(data race)不大、冲突较少的环境中,这种环境中,偶尔回滚事务的成本会低于读取数据时锁定数据的成本,因此可以获得比其他并发控制方法更高的吞吐量。 它的作用是防止并发更新数据库中的数据,从而避免数据的混乱。
实现乐观锁的核心要素
乐观锁由以下几个要素组成:
在
table
中增加一列,用于记录此行数据的版本号更新数据前,先读取当前数据行的版本号
更新时,对
UPDATE
语句作两处调整:
(1) WHERE
语句中加入版本号的比较条件,确保只有当前版本号与数据库中的版本号一致时才执行更新
WHERE ... and version = [current version]
(2) UPDATE
语句中递增版本号以保证每次更新后版本号都会变化
UPDATE set ..., version = version + 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 int
Title string
Content 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 version
if 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
}
冲突处理策略
当乐观锁冲突发生时,开发者需要决定如何处理这种情况。通常,有以下几种策略:
- 重试:重新读取数据,并尝试再次执行更新操作。
- 回滚:取消当前操作,并通知用户操作失败。
- 自定义逻辑:根据业务需求,执行特定的错误处理逻辑。
实际示例:乐观锁在并发场景中的应用
假设我们有一个在线购物系统,用户可以查看商品并将其添加到购物车。在这个场景中,我们希望确保用户在添加商品到购物车时,商品的库存数量是准确的。我们可以通过乐观锁来实现这一目标。
商品模型与库存管理
首先,我们定义一个商品模型,并在其中添加一个版本号字段:
type Product struct {
ID int
Name string
Price int
Quantity int
Version 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的更新和插入方法会受影响,这包括:
- Update
- Updates
- Create
有些方法不支持plugin,因此不会受影响:
- UpdateColumn
- 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 conflict
result := 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 fields
result := db.Select("*").Updates(blog)
if err := result.Error; err != nil {
return err
}
if result.RowsAffected == 0 {
return ErrOptimisticLock
}
return nil
}
gorm中使用乐观锁的更多相关文章
- mysql中的乐观锁和悲观锁
mysql中的乐观锁和悲观锁的简介以及如何简单运用. 关于mysql中的乐观锁和悲观锁面试的时候被问到的概率还是比较大的. mysql的悲观锁: 其实理解起来非常简单,当数据被外界修改持保守态度,包括 ...
- Java中的乐观锁
1.前言 之前好几次看到有人在面经中提到了乐观锁与悲观锁,但是一本<Java Concurrency In Practice>快看完了都没有见到过这两种锁,今天终于在第15章发现了它们的踪 ...
- MYSQL中的乐观锁实现(MVCC)简析
https://segmentfault.com/a/1190000009374567#articleHeader2 什么是MVCC MVCC即Multi-Version Concurrency Co ...
- 老司机带大家领略MySQL中的乐观锁和悲观锁
原文地址:https://cloud.tencent.com/developer/news/227982 为什么需要锁 在并发环境下,如果多个客户端访问同一条数据,此时就会产生数据不一致的问题,如何解 ...
- B8 Concurrent JDK中的乐观锁与原子类
[概述] 乐观锁采用的是一种无锁的思想,总是假设最好的情况,认为一个事务在读取数据的时候,不会有别的事务对数据进行修改,只需要在修改数据的时候判断原数据数据是否已经被修改了.JDK 中 java.ut ...
- MySQL/InnoDB中,乐观锁、悲观锁、共享锁、排它锁、行锁、表锁、死锁概念的理解
文章出处:https://www.souyunku.com/2018/07/30/mysql/?utm_source=tuicool&utm_medium=referral MySQL/Inn ...
- 利用MySQL中的乐观锁和悲观锁实现分布式锁
背景 对于一些并发量不是很高的场景,使用MySQL的乐观锁实现会比较精简且巧妙. 下面就一个小例子,针对不加锁.乐观锁以及悲观锁这三种方式来实现. 主要是一个用户表,它有一个年龄的字段,然后并发地对其 ...
- 《ASP.NET MVC4 WEB编程》学习笔记------乐观锁和悲观锁
摘要:对数据库的并发访问一直是应用程序开发者需要面对的问题之一,一个好的解决方案不仅可以提供高的可靠性还能给应用程序的性能带来提升.下面我们来看一下Couchbase产品市场经理Don Pinto结合 ...
- oracle的乐观锁和悲观锁
一.问题引出 1. 假设当当网上用户下单买了本书,这时数据库中有条订单号为001的订单,其中有个status字段是’有效’,表示该订单是有效的: 2. 后台管理人员查询到这条001的订单,并且看到状态 ...
- Java-悲观锁和乐观锁
Java中的乐观锁与悲观锁: 1. Java中典型的synchronized就是一种悲观锁,也就是独占锁,不过JDK1.6之后对synchronized已经做了许多优化,也不能说是完全的悲观锁了: 2 ...
随机推荐
- Linux系统手动安装Firefox浏览器
大多数Linux发行版都以Firefox作为默认的浏览器,并可以轻松地从软件库中安装.例如:Debian/Ubuntu: sudo apt-get install firefoxFedora: sud ...
- JDBC核心6步
1JDBC简介 java DataBase Connectivity,又称java数据库连接 是独立于任何数据库管理系统的api java提供接口规范,由各个数据库厂商提供接口的实现,厂商提供的实现封 ...
- Qt开发经验小技巧236-240
关于在头文件中定义函数使用static关键字的血的教训. 有时候我们需要将一些常用函数写在一个文件中供很多地方调用,如果写的是 int doxxx{} 这种,在你多个地方引用的时候,肯定会编译报错提示 ...
- Soulmate
理想之所以是理想,也就是因为它只能存在于脑海中,天上月是天上月,水中花是水中花.但我们仍可以怀揣着对乌托邦的向往,所以,我对理想中的对象设想如下: 原来形容一个女子的眉眼,我总喜欢说眉眼如黛,眉如远山 ...
- WIN10删除文件时提示“找不到该项目,该项目不在......中,请确认该项目的位置,然后重试”的解决办法
问题描述: 最近有部分WIN10用户在删除文件时提示"找不到该项目,该项目不在......中,请确认该项目的位置,然后重试". 解决办法: 1.首先新建一个TXT文档(为了方便使用 ...
- [转]CLion 2019去掉灰色参数提示(parameters hints)
众所周知,clion是一个很好用的c plus plus IDE,刚装好的clion默认的设置多少有一些不符合口味的地方,在查看代码或者敲代码的时候看到如下这样的灰色提示,我是有点受不了的: 之前用的 ...
- 基于STC8G1K08的CH549单键进入USB下载模式实验
一.实验原因 CH552或CH549进入USB下载,通常需要两个按键,一个控制电源的通断,一个通过串联电阻(一头接VCC或V33)冷启动时抬高UDP电平.时序上是这样的:断电--按下接UDP的轻触开关 ...
- 前端学习openLayers配合vue3(获取矢量图的个数,省份的个数)
矢量图层绘制了一个中国地图,我们获取一下矢量图层的个数 关键代码 map .getLayers()//获取所有图层 .item(1)//获取矢量图层 .getSource() .on("ch ...
- 一篇解决编译原理大作业,基于Flex、Bison设计编译器(含语法分析树和符号表)
1.工具简单介绍 Flex 和 Bison 是编译器开发中常用的两个工具,分别用于生成词法分析器和语法分析器.它们通常一起使用,共同完成源代码的词法分析和语法分析工作. Flex: Flex通过读取一 ...
- MySQL---索引-性能-配置参数优化
一般来说,要保证数据库的效率,要做好以下四个方面的工作:数 据库设计.sql语句优化.数据库参数配置.恰当的硬件资源和操作系统,这个顺序也表现了这四个工作对性能影响的大小.下面我们逐个阐明: 1.设计 ...