Go微服务: 乐观锁

2024-06-12 09:04
文章标签 服务 go 乐观

本文主要是介绍Go微服务: 乐观锁,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

概述

  • 乐观锁(Optimistic Lock)是一种并发控制策略,与悲观锁相对,它基于一种“乐观”的假设,即在大多数情况下,数据访问不会产生并发冲突
  • 乐观锁尽量减少锁的使用,从而提高了系统的并发性能,尤其适用于读多写少的场景
  • 乐观锁并不在数据访问前进行加锁,而是在数据更新时才检查数据是否被其他事务修改过
  • 这种机制假设读操作远多于写操作,而且在大部分情况下,数据在读取和更新之间的这段时间内不会被其他事务修改
  • 如果在更新时发现数据已被其他事务修改,则乐观锁通常会抛出异常或返回错误,由应用程序决定如何处理冲突

Mysql实现乐观锁

  • 数据库乐观锁,在数据库层面,乐观锁可以通过在表中添加一个版本字段(如version)或使用时间戳字段来实现
    • 版本号:每次更新时,都会先检查当前版本号与之前读取的版本号是否一致,一致则更新数据并递增版本号
    • 时间戳:使用数据的最后修改时间作为版本控制,更新时比较当前时间戳与读取时的时间戳
  • 现在,假设有2个库存服务都要查询一个id=1的库存数据,之后进行各自的更新
  • 在查询之后,紧接着就要进行更新操作了,更新是最终目的
  • 2个库存服务,在计算机内部总有一个先执行,总有一个后执行,假设库存1先执行了
  • 如果库存1先执行了,那 Version 字段一定被修改了,而库存2服务的修改一定是失败的,version 已经不再为 0
  • 失败之后,有几种处理方法:
    • ①. 直接返回上层
    • ②. 重复执行, 查询, 更新, 重复这个流程, 直到成功
  • 以上两种解决方案取决于业务形态,没有对错
  • 也可以结合两者,比如:在第②种可以变形为尝试N次,不行后, 执行 ①
  • 也就是说,存在数据竞争,总有一个获胜的,获胜的现行,失败的重试
  • 在乐观锁中,数据库并没有加锁,通过version机制,达到数据一致性

乐观锁在GORM中的应用


1 ) 库存扣减函数

func deductStock(db *gorm.DB, productId uint, quantity int) error {var product Productif err := db.First(&product, productId).Error; err != nil {return err}// 减少库存并递增版本号,这里使用了GORM的UpdateColumn方法避免了其他字段被覆盖if product.Quantity < quantity {return fmt.Errorf("insufficient stock for product %d", productId)}// 使用事务处理乐观锁更新,确保操作的原子性tx := db.Begin()if err := tx.Model(&product).UpdateColumns(map[string]interface{}{"Quantity": gorm.Expr("Quantity - ?", quantity),"Version":  gorm.Expr("Version + 1"),}).Where("id = ? AND version = ?", product.ID, product.Version).Error; err != nil {tx.Rollback() // 发生错误时回滚事务return err}if err := tx.Commit().Error; err != nil {return err}return nil
}
  • 注意,在 gorm 中,有零值保护,如果想要操作相关字段,使用 Select
  • 参考:https://gorm.io/zh_CN/docs/update.html#更新选定字段

2 )乐观锁测试库存扣减

func main() {// 假设已经有一条产品记录,ID为1,初始库存为100// 这里仅示例,实际应用中应检查是否插入成功// 并发测试扣减库存for i := 0; i < 10; i++ {go func(i int) {err := deductStock(db, 1, 10)if err != nil {log.Printf("Error in deduction for iteration %d: %v", i, err)} else {log.Printf("Iteration %d: Stock deducted successfully.", i)}}(i)}// 注意:在实际应用中,你可能需要添加适当的同步或等待机制来确保所有goroutines执行完毕
}
  • 为了模拟并发扣减库存的场景,你可以使用goroutines来并行执行deductStock 函数
  • 但请注意在实际应用中应谨慎处理并发逻辑,避免过度并发导致数据库压力过大
  • 这个示例展示了如何使用GORM和MySQL实现乐观锁机制来处理库存扣减,确保了在高并发环境下数据的一致性和完整性
  • 请根据你的具体需求调整数据库连接字符串、库存扣减逻辑和测试代码

工作机制

  • 乐观锁的实现依赖于数据版本控制或时间戳,基本步骤如下:
  • 1 )读取数据:
    • 读取数据时,除了数据本身,还会读取一个表示数据版本的字段(版本号或时间戳)
  • 2 )更新数据
    • 当需要更新数据时,先检查数据版本是否与之前读取时的版本相匹配
    • 如果匹配,说明数据没有被其他事务修改过,可以直接更新数据,并递增版本号或更新时间戳
    • 如果不匹配,说明数据已被其他事务修改,乐观锁通常会抛出并发异常,需要应用程序处理这种情况,如重试操作或提示用户

应用场景

  • 乐观锁特别适合于以下场景
    • 读多写少:在读操作远多于写操作的系统中,乐观锁可以减少锁的竞争,提高系统的并发性能
    • 高并发环境:在需要处理大量并发请求的场景下,乐观锁能够有效减少锁的使用,降低锁带来的性能损耗
    • Web应用:很多Web应用的业务逻辑天然适合乐观锁,如购物车、评论系统等,这些场景下数据冲突相对较少

乐观锁与悲观锁的对比

  • 并发性能
    • 乐观锁在读多写少的场景下性能优于悲观锁,因为它减少了锁的使用
    • 悲观锁假定冲突频繁,优先防止并发修改,牺牲了部分并发性能
  • 资源消耗
    • 乐观锁减少了锁的争用,降低了系统的开销
  • 冲突处理
    • 悲观锁在操作开始时就加锁,防止了冲突;而乐观锁在更新时检测冲突,遇到冲突时需要重试或处理异常
    • 乐观锁假定冲突较少,允许并发访问,但在提交修改时检查数据是否被其他事务修改,通过版本控制或条件检查实现,更适合读多写少的场景
  • 适用场景
    • 乐观锁适合并发读远多于写的场景,而悲观锁更适用于写操作频繁或对数据一致性和实时性要求极高的场景

总结

  • 乐观锁以其轻量级、高性能的特点,在现代软件开发中占据了一席之地,尤其是在处理高并发读操作的场景下表现优异
  • 通过合理设计和应用乐观锁,开发者能够构建出既高效又可靠的并发控制机制
  • 然而,选择乐观锁还是悲观锁,需要根据实际业务场景和性能需求综合考量,有时两者结合使用,互补优劣,可以达到更好的并发控制效果

这篇关于Go微服务: 乐观锁的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1053777

相关文章

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

Go路由注册方法详解

《Go路由注册方法详解》Go语言中,http.NewServeMux()和http.HandleFunc()是两种不同的路由注册方式,前者创建独立的ServeMux实例,适合模块化和分层路由,灵活性高... 目录Go路由注册方法1. 路由注册的方式2. 路由器的独立性3. 灵活性4. 启动服务器的方式5.

TP-Link PDDNS服将于务6月30日正式停运:用户需转向第三方DDNS服务

《TP-LinkPDDNS服将于务6月30日正式停运:用户需转向第三方DDNS服务》近期,路由器制造巨头普联(TP-Link)在用户群体中引发了一系列重要变动,上个月,公司发出了一则通知,明确要求所... 路由器厂商普联(TP-Link)上个月发布公告要求所有用户必须完成实名认证后才能继续使用普联提供的 D

Go语言中三种容器类型的数据结构详解

《Go语言中三种容器类型的数据结构详解》在Go语言中,有三种主要的容器类型用于存储和操作集合数据:本文主要介绍三者的使用与区别,感兴趣的小伙伴可以跟随小编一起学习一下... 目录基本概念1. 数组(Array)2. 切片(Slice)3. 映射(Map)对比总结注意事项基本概念在 Go 语言中,有三种主要

Go Mongox轻松实现MongoDB的时间字段自动填充

《GoMongox轻松实现MongoDB的时间字段自动填充》这篇文章主要为大家详细介绍了Go语言如何使用mongox库,在插入和更新数据时自动填充时间字段,从而提升开发效率并减少重复代码,需要的可以... 目录前言时间字段填充规则Mongox 的安装使用 Mongox 进行插入操作使用 Mongox 进行更

Go语言利用泛型封装常见的Map操作

《Go语言利用泛型封装常见的Map操作》Go语言在1.18版本中引入了泛型,这是Go语言发展的一个重要里程碑,它极大地增强了语言的表达能力和灵活性,本文将通过泛型实现封装常见的Map操作,感... 目录什么是泛型泛型解决了什么问题Go泛型基于泛型的常见Map操作代码合集总结什么是泛型泛型是一种编程范式,允

微服务架构之使用RabbitMQ进行异步处理方式

《微服务架构之使用RabbitMQ进行异步处理方式》本文介绍了RabbitMQ的基本概念、异步调用处理逻辑、RabbitMQ的基本使用方法以及在SpringBoot项目中使用RabbitMQ解决高并发... 目录一.什么是RabbitMQ?二.异步调用处理逻辑:三.RabbitMQ的基本使用1.安装2.架构

Java中使用Java Mail实现邮件服务功能示例

《Java中使用JavaMail实现邮件服务功能示例》:本文主要介绍Java中使用JavaMail实现邮件服务功能的相关资料,文章还提供了一个发送邮件的示例代码,包括创建参数类、邮件类和执行结... 目录前言一、历史背景二编程、pom依赖三、API说明(一)Session (会话)(二)Message编程客

基于Go语言实现一个压测工具

《基于Go语言实现一个压测工具》这篇文章主要为大家详细介绍了基于Go语言实现一个简单的压测工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录整体架构通用数据处理模块Http请求响应数据处理Curl参数解析处理客户端模块Http客户端处理Grpc客户端处理Websocket客户端

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操