首页 > 其他分享 >gorm中使用乐观锁

gorm中使用乐观锁

时间:2024-07-28 19:28:47浏览次数:7  
标签:return err 版本号 更新 乐观 result 使用 Save gorm

乐观锁简介

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

乐观并发控制多数用于数据竞争(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      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
}

冲突处理策略

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

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

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

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

商品模型与库存管理

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

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的更新和插入方法会受影响,这包括:

  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 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
}

标签:return,err,版本号,更新,乐观,result,使用,Save,gorm
From: https://www.cnblogs.com/niuben/p/18328748

相关文章

  • 详细了解Servlet中过滤器和监听器的使用
    目录一、过滤器1.1概念1.2过滤器的实现1.3过滤器中方法的介绍1.4过滤器的执行过程二、监听器2.1概念2.2 监听器的实现2.3不同监听器接口的介绍1. ServletContextListener接口2. HttpSessionListener接口3. HttpSessionAttributeListener接口一、过滤......
  • OAuth2 + Gateway统一认证一步步实现(公司项目能直接使用),密码模式&授权码模式
    文章目录认证的具体实现环境的搭建基础版授权服务搭建引入依赖创建数据表yml配置配置SpringSecurity定义认证授权的配置类授权服务器存储客户端信息修改授权服务配置,支持密码模式基础版授权服务测试授权码模式测试密码模式测试**测试校验token接口**整合JWT使用jwt基......
  • 一篇学会JDBC的使用。
    目录一、基础篇1.1概念1.2快速入门1.3核心API讲解1.4基于PreparedStatement实现CRUD(创建,读取,更新,删除四种操作)1.4.1查询1.4.1增,删,改二、进阶篇2.1jdbc扩展2.2主键回显2.3连接池(Druid为例)2.4properties集合三、高级篇3.1jdbc工具类封装3.2Dao层......
  • 树莓派3b+使用官方屏幕后倒置问题——屏幕倒置
    树莓派3b+的屏幕本身就是倒置的,因此为了使树莓派在官方屏幕下能显示正常的屏幕画面因此需要通过设置把树莓派的官方屏幕的输出倒置一下,这样树莓派的官方屏幕的输出就是正常的了。解决方法:(源自:https://blog.csdn.net/t13506920069/article/details/121359178)官方正版屏幕旋转......
  • 最高法-发包人承包人对未竣工验收合格工程均同意先实际使用的,不属于司法解释规定的发
    (2023)最高法民申1043号  某甲有限公司、某乙有限公司建设工程施工合同纠纷民事申请再审审查民事裁定书申请人主张:某甲公司申请再审称,根据《最高人民法院关于审理建设工程施工合同纠纷案件适用法律问题的解释》(法释[2004]14号)第十三条的规定:“建设工程未经竣工验收,发包人擅......
  • 使用FreeRTOS官方移植,移植到STM32F1平台中
    本教程基本参照[野火]《FreeRTOS内核实现与应用开发实战指南》,本人相当推荐此教程,尤其是第一部分从0开始写内核,虽然比较晦涩,但是学完之后对FreeRTOS的运行原理还有框架的认识会有一个很大的提高。首先获取FreeRTOS的源码我们从官网下载9.0版本的压缩包解压后Plus中包含......
  • Postman中的代理艺术:配置与使用指南
    Postman中的代理艺术:配置与使用指南在API开发和测试过程中,代理服务器常用于捕获、检查、修改请求和响应。Postman作为一个流行的API开发工具,内置了代理服务器功能,使得测试人员可以方便地查看和修改通过代理的流量。本文将详细介绍如何在Postman中配置和使用代理服务器。代......
  • Jenkins如何使用CrumbIssuer防御CSRF攻击
    1、CSRF(跨站请求伪造)概述在讲解Jenkins的跨站请求伪造(CSRF)保护机制之前,让我们首先对CSRF这一安全威胁及其重要性进行简明扼要的概述。1.1  CSRF(跨站请求伪造)的原理CSRF(即跨站请求伪造)是指利用受害者尚未失效的身份认证信息、(cookie、会话等),诱骗其点击恶意链接或者访......
  • Spring Boot 使用Apollo动态调整日志级别
    摘要:在SpringBoot项目中,借助Apollo动态修改配置的能力,结合Logback修改日志级别打印执行的SQL脚本。综述  在生产环境偶现测试环境未发现的SQL查询BUG,但由于线上关闭debug和trace级别日志导致缺少执行SQL、异常堆栈等日志信息,没有办法火速定位问题根源。面对这样的线上问题,通......
  • STM32第二十三课:GUI-Guider安装使用及项目移植(7.11版本)
    目录目标一、GUI-Guider获取与安装1.GUI-Guider获取2.安装二、使用步骤1.创建一个新项目2.快速使用三、项目移植注意事项目标1.安装GUI-guider。2.学会使用GUI-guider设计界面。3.学会将GUI-guider设计后的界面移植到项目工程中。一、GUI-Guider获取与安装 ......