乐观锁 订阅
乐观锁机制采取了更加宽松的加锁机制。相对悲观锁而言,乐观锁更倾向于开发运用。 展开全文
乐观锁机制采取了更加宽松的加锁机制。相对悲观锁而言,乐观锁更倾向于开发运用。
信息
应    用
金融行业
外文名
Optimistic locking
介    绍
记录机制
中文名
乐观锁
乐观锁乐观锁介绍
乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号等于数据库表当前版本号,则予以更新,否则认为是过期数据。
收起全文
精华内容
下载资源
问答
  • MyBatis乐观锁插件3.0,全面升级,更加复杂,更加强大 旧版本MyBatis乐观锁插件1.x请移步至wiki文档: MyBatis乐观储物柜插件 文档: 开始... <groupId>com.github.dreamroute</groupId> <artifactId>locker-...
  • 前言 ...如果是Django2.0以下的版本,需要去修改到这个隔离级别,不然乐观锁操作时无法读取已经被修改的数据 RepeatableRead(可重读) 这是这是Mysql默认的隔离级别,可以到mysql的配置文件中去修改;
  • 主要介绍了thinkPHP框架乐观锁和悲观锁,结合实例形式分析了框架乐观锁和悲观锁的原理及thinkPHP相关实现技巧,需要的朋友可以参考下
  • 主要介绍了java乐观锁原理与实现,结合具体案例形式分析了乐观锁的原理及java使用乐观锁实现自动派单功能的相关操作技巧,需要的朋友可以参考下
  • 以下是对Sql server锁,独占锁,共享锁,更新锁,乐观锁,悲观锁进行了详细的介绍,需要的朋友可以过来参考下
  • 主要介绍了SpringBoot整合MyBatis实现乐观锁和悲观锁的示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
  • 主要介绍了浅谈mybatis 乐观锁实现,解决并发问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
  • 封锁的类型以及粒度,两段锁协议,隐式和显式锁定 封锁类型有两种:读写锁和意向锁 读写锁分类读锁(s锁)和写锁(x锁)。...乐观锁和悲观锁都是为了事务的并发控制。 乐观锁 悲观锁 目的 事务
  • 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低(不是没有, 所以还要加锁, 区别于不加锁的乐观读),每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有...
  • 不用队列,队列太占用内存,所以用redis 的watch 就是乐观锁的原理,来做的秒杀 抢购
  • 项目中的常用的锁(乐观锁、悲观锁、基于Redis的分布式锁) 乐观锁 一般通过使用sql来做如添加版本之类的字段来做控制 例:select * from table where version = ?。保证都是一个线程在做更新。 悲观锁 锁住操作的...
  • 本文实例讲述了mysql 悲观锁与乐观锁。分享给大家供大家参考,具体如下: 悲观锁与乐观锁是人们定义出来的概念,你可以理解为一种思想,是处理并发资源的常用手段。 不要把他们与mysql中提供的锁机制(表锁,行锁,...
  • 数据库通过以及协议来进行并发控制,解决并发事务带来的问题,本篇博文主要是解析数据库的协议和Mysql的默认存储引擎InnoDB的机制。 如果对事务隔离级别以及并发事务带来的问题不熟悉可以翻阅我的另外一篇...
  • 主要介绍了Java并发问题之乐观锁与悲观锁,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 主要给大家介绍了关于Mysql悲观锁和乐观锁使用的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Mysql具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
  • 主要介绍了MySQL中悲观锁与乐观锁的相关资料,帮助大家更好的理解和学习MySQL数据库,感兴趣的朋友可以了解下
  • 主要介绍了Hibernate实现悲观锁和乐观锁的有关内容,涉及hibernate的隔离机制,以及实现悲观锁和乐观锁的代码实现,需要的朋友可以了解下。
  • 主要介绍了深入理解Yii2.0乐观锁与悲观锁的原理与使用,具有一定的参考价值,感兴趣的小伙伴们可以参考一下。
  • 浅谈Mybatis乐观锁插件

    2020-08-28 13:40:40
    主要介绍了浅谈Mybatis乐观锁插件,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 的定义:   数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。 表级:开销小,加锁快;不会出现死锁;锁定粒度大,发生冲突的概率最高,...
  • MyBatisPlus插件扩展_OptimisticLockerInterceptor乐观锁插件的使用示例代码
  • 我今天就简单聊一下乐观锁和悲观锁,他们对应的实现 CAS ,Synchronized,ReentrantLock 正文 一个120斤一身黑的小伙子走了进来,看到他微微发福的面容,看来是最近疫情伙食好运动少的结果,他难道就是今天的面试官...

    前言

    关于线程安全一提到可能就是加锁,在面试中也是面试官百问不厌的考察点,往往能看出面试者的基本功和是否对线程安全有自己的思考。

    那锁本身是怎么去实现的呢?又有哪些加锁的方式呢?

    我今天就简单聊一下乐观锁和悲观锁,他们对应的实现 CAS ,Synchronized,ReentrantLock

    正文

    一个120斤一身黑的小伙子走了进来,看到他微微发福的面容,看来是最近疫情伙食好运动少的结果,他难道就是今天的面试官渣渣丙?

    等等难道是他?前几天刷B站看到的不会是他吧!!!

    是的我已经开始把面试系列做成视频了,以后会有各种级别的面试,从大学生到阿里P7+的面试,还有阿里,拼多多,美团,字节风格的面试我也都约好人了,就差时间了,大家可以去B站搜:三太子敖丙 观看

    我也不多跟你BB了,我们直接开始好不好,你能跟我聊一下CAS么?

    CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。

    CAS 是怎么实现线程安全的?

    线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。

    举个栗子:现在一个线程要修改数据库的name,修改前我会先去数据库查name的值,发现name=“帅丙”,拿到值了,我们准备修改成name=“三歪”,在修改之前我们判断一下,原来的name是不是等于“帅丙”,如果被其他线程修改就会发现name不等于“帅丙”,我们就不进行操作,如果原来的值还是帅丙,我们就把name修改为“三歪”,至此,一个流程就结束了。

    有点懵?理一下停下来理一下思路。

    Tip:比较+更新 整体是一个原子操作,当然这个流程还是有问题的,我下面会提到。

    他是乐观锁的一种实现,就是说认为数据总是不会被更改,我是乐观的仔,每次我都觉得你不会渣我,差不多是这个意思。

    你这个栗子不错,他存在什么问题呢?

    有,当然是有问题的,我也刚好想提到。

    你们看图发现没,要是结果一直就一直循环了,CUP开销是个问题,还有ABA问题和只能保证一个共享变量原子操作的问题。

    你能分别介绍一下么?

    好的,我先介绍一下ABA这个问题,直接口述可能有点抽象,我画图解释一下:

    看到问题所在没,我说一下顺序:

    1. 线程1读取了数据A
    2. 线程2读取了数据A
    3. 线程2通过CAS比较,发现值是A没错,可以把数据A改成数据B
    4. 线程3读取了数据B
    5. 线程3通过CAS比较,发现数据是B没错,可以把数据B改成了数据A
    6. 线程1通过CAS比较,发现数据还是A没变,就写成了自己要改的值

    懂了么,我尽可能的幼儿园化了,在这个过程中任何线程都没做错什么,但是值被改变了,线程1却没有办法发现,其实这样的情况出现对结果本身是没有什么影响的,但是我们还是要防范,怎么防范我下面会提到。

    循环时间长开销大的问题

    是因为CAS操作长时间不成功的话,会导致一直自旋,相当于死循环了,CPU的压力会很大。

    只能保证一个共享变量的原子操作

    CAS操作单个共享变量的时候可以保证原子的操作,多个变量就不行了,JDK 5之后 AtomicReference可以用来保证对象之间的原子性,就可以把多个对象放入CAS中操作。

    我还记得你之前说在JUC包下的原子类也是通过这个实现的,能举个栗子么?

    那我就拿AtomicInteger举例,他的自增函数incrementAndGet()就是这样实现的,其中就有大量循环判断的过程,直到符合条件才成功。

    大概意思就是循环判断给定偏移量是否等于内存中的偏移量,直到成功才退出,看到do while的循环没。

    乐观锁在项目开发中的实践,有么?

    有的就比如我们在很多订单表,流水表,为了防止并发问题,就会加入CAS的校验过程,保证了线程的安全,但是看场景使用,并不是适用所有场景,他的优点缺点都很明显。

    那开发过程中ABA你们是怎么保证的?

    加标志位,例如搞个自增的字段,操作一次就自增加一,或者搞个时间戳,比较时间戳的值。

    举个栗子:现在我们去要求操作数据库,根据CAS的原则我们本来只需要查询原本的值就好了,现在我们一同查出他的标志位版本字段vision。

    之前不能防止ABA的正常修改:

    update table set value = newValue where value = #{oldValue}
    //oldValue就是我们执行前查询出来的值 

    带版本号能防止ABA的修改:

    update table set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision} 
    // 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样

    除了版本号,像什么时间戳,还有JUC工具包里面也提供了这样的类,想要扩展的小伙伴可以去了解一下。

    聊一下悲观锁?

    悲观锁从宏观的角度讲就是,他是个渣男,你认为他每次都会渣你,所以你每次都提防着他。

    我们先聊下JVM层面的synchronized:

    synchronized加锁,synchronized 是最常用的线程同步手段之一,上面提到的CAS是乐观锁的实现,synchronized就是悲观锁了。

    它是如何保证同一时刻只有一个线程可以进入临界区呢?

    synchronized,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。

    我分别从他对对象、方法和代码块三方面加锁,去介绍他怎么保证线程安全的:

    • synchronized 对对象进行加锁,在 JVM 中,对象在内存中分为三块区域:对象头(Header)、实例数据(Instance
      Data)和对齐填充(Padding)。

    • 对象头:我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

      • Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
      • Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

      你可以看到在对象头中保存了锁标志位和指向 monitor 对象的起始地址,如下图所示,右侧就是对象对应的 Monitor 对象。

      当 Monitor 被某个线程持有后,就会处于锁定状态,如图中的 Owner 部分,会指向持有 Monitor 对象的线程。

      另外 Monitor 中还有两个队列分别是EntryList和WaitList,主要是用来存放进入及等待获取锁的线程。

      如果线程进入,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行。

    在对象级使用锁通常是一种比较粗糙的方法,为什么要将整个对象都上锁,而不允许其他线程短暂地使用对象中其他同步方法来访问共享资源?

    如果一个对象拥有多个资源,就不需要只为了让一个线程使用其中一部分资源,就将所有线程都锁在外面。

    由于每个对象都有锁,可以如下所示使用虚拟对象来上锁:

    java class FineGrainLock{  MyMemberClassx,y;  Object xlock = new Object(), ylock = newObject();  public void foo(){   synchronized(xlock){   //accessxhere    }   //dosomethinghere-butdon'tusesharedresources    synchronized(ylock){    //accessyhere    }  } public void bar(){    synchronized(this){    //accessbothxandyhere  }  //dosomethinghere-butdon'tusesharedresources } }

    • synchronized 应用在方法上时,在字节码中是通过方法的 ACC_SYNCHRONIZED 标志来实现的。

      我反编译了一小段代码,我们可以看一下我加锁了一个方法,在字节码长啥样,flags字段瞩目:

      javascript synchronized void test(); descriptor: ()V flags: ACC_SYNCHRONIZED Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this Ljvm/ClassCompile;

    反正其他线程进这个方法就看看是否有这个标志位,有就代表有别的仔拥有了他,你就别碰了。

    • synchronized 应用在同步块上时,在字节码中是通过 monitorenter 和 monitorexit 实现的。

      每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。

      步骤如下:

      1. 每个monitor维护着一个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,当一个线程获得monitor(执行monitorenter)后,该计数器自增变为 1 。
        • 当同一个线程再次获得该monitor的时候,计数器再次自增;
      • 当不同线程想要获得该monitor的时候,就会被阻塞。
      1. 当同一个线程释放 monitor(执行monitorexit指令)的时候,计数器再自减。

        当计数器为0的时候,monitor将被释放,其他线程便可以获得monitor。

        同样看一下反编译后的一段锁定代码块的结果:

        public void syncTask();
          descriptor: ()V
          flags: ACC_PUBLIC
          Code:
            stack=3, locals=3, args_size=1
               0: aload_0
               1: dup
               2: astore_1
               3: monitorenter  //注意此处,进入同步方法
               4: aload_0
               5: dup
               6: getfield      #2             // Field i:I
               9: iconst_1
              10: iadd
              11: putfield      #2            // Field i:I
              14: aload_1
              15: monitorexit   //注意此处,退出同步方法
              16: goto          24
              19: astore_2
              20: aload_1
              21: monitorexit //注意此处,退出同步方法
              22: aload_2
            23: athrow
              24return
            Exception table:
            //省略其他字节码.......

      小结:

      同步方法和同步代码块底层都是通过monitor来实现同步的。

      两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现,同步代码块是通过monitorenter和monitorexit来实现。

      我们知道了每个对象都与一个monitor相关联,而monitor可以被线程拥有或释放。

      🐂,小伙子我只能说,你确实有点东西,以前我们一直锁synchronized是重量级的锁,为啥现在都不提了?

      在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。

      但是,随着 Java SE 1.6 对 synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。

      针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。

      Tip:本来锁升级的过程我是搞了个贼详细贼复杂的图,但是我发现不便于理解,我就幼儿园化了,所以就有了个简单版本的,先看下复杂版本的:

      幼儿园版本:

      看到这你如果还想白嫖,我劝你善良,万水千山总是情,不要白嫖行不行?点个赞再走哈哈。

      对了锁只能升级,不能降级。

      还有其他的同步手段么?

      ReentrantLock但是在介绍这玩意之前,我觉得我有必要先介绍AQS(AbstractQueuedSynchronizer)。

      AQS:也就是队列同步器,这是实现 ReentrantLock 的基础。

      AQS 有一个 state 标记位,值为1 时表示有线程占用,其他线程需要进入到同步队列等待,同步队列是一个双向链表。

      当获得锁的线程需要等待某个条件时,会进入 condition 的等待队列,等待队列可以有多个。

      当 condition 条件满足时,线程会从等待队列重新进入同步队列进行获取锁的竞争。

      ReentrantLock 就是基于 AQS 实现的,如下图所示,ReentrantLock 内部有公平锁和非公平锁两种实现,差别就在于新来的线程是否比已经在同步队列中的等待线程更早获得锁。

      和 ReentrantLock 实现方式类似,Semaphore 也是基于 AQS 的,差别在于 ReentrantLock 是独占锁,Semaphore 是共享锁。

      从图中可以看到,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。

      它有公平锁FairSync和非公平锁NonfairSync两个子类。

      ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。

      相关资料

      Tip:本来这一栏有很多我准备的资料的,但是都是外链,或者不合适的分享方式,博客的运营小姐姐提醒了我,所以大家去公众号回复【资料】好了。

      技术总结

      锁其实有很多,我这里只是简单的介绍了一下乐观锁和悲观锁,后面还会有,自旋锁,自适应自旋,公平锁,非公平锁,可重入(文中提到的都是可重入),不可重入锁,共享锁,排他锁等。

      多去了解他们的用法,多去深究他们的原理以及实现,我后面会持续更新多线程方面的知识点。

      Tip:这是我新的技术总结方式,以后会慢慢完善,如果被博客平台二压公众号回复【多线程】获取。

      参考:《Java高并发编程》、《美团技术团队锁的思考》、《拉钩张雷java32个考点》

      絮叨

      我不断的尝试新的文章风格,我也把絮叨环节放到了最后就是给大家一个好的阅读体验,有建议随时提哟,新的技术总结方式如何?

      还记得我帮公司内推么,我收到300封简历,但是我放到系统的只有13份,不是我想吐槽大家,是真的得用心点啊,叫我内推发个邮件,简历都不发什么鬼,简历总共才100个字又是什么鬼。。。

      周末出一期视频说一下简历的问题,真的为你们春招担心啊仔。。。。

      絮叨

      另外,敖丙把自己的面试文章整理成了一本电子书,共 1630页!目录如下

      现在免费送给大家,在我的公众号三太子敖丙回复 【888】 即可获取。

      我是敖丙,一个在互联网苟且偷生的程序员。

      你知道的越多,你不知道的越多人才们的 【三连】 就是丙丙创作的最大动力,我们下期见!

      注:如果本篇博客有任何错误和建议,欢迎人才们留言!


      文章持续更新,可以微信搜索「 三太子敖丙 」第一时间阅读,回复【资料】有我准备的一线大厂面试资料和简历模板,本文 GitHub https://github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。

    展开全文
  • 悲观锁与乐观锁的实现(详情图解)

    万次阅读 多人点赞 2020-05-27 10:26:59
    在了解悲观锁和乐观锁之前,我们先了解一下什么是锁,为什么要用到锁? 技术来源于生活,锁不仅在程序中存在,在现实中我们也随处可见,例如我们上下班打卡的指纹锁,保险柜上的密码锁,以及我们我们登录的用户名...

    一、前言

    • 在了解悲观锁和乐观锁之前,我们先了解一下什么是锁,为什么要用到锁?

    • 技术来源于生活,锁不仅在程序中存在,在现实中我们也随处可见,例如我们上下班打卡的指纹锁,保险柜上的密码锁,以及我们我们登录的用户名和密码也是一种锁,生活中用到锁可以保护我们人身安全(指纹锁)、财产安全(保险柜密码锁)、信息安全(用户名密码锁),让我们更放心的去使用和生活,因为有锁,我们不用去担心个人的财产和信息泄露。

    • 而程序中的锁,则是用来保证我们数据安全的机制和手段,例如当我们有多个线程去访问修改共享变量的时候,我们可以给修改操作加锁(syncronized)。当多个用户修改表中同一数据时,我们可以给该行数据上锁(行锁)。因此,当程序中可能出现并发的情况时,我们就需要通过一定的手段来保证在并发情况下数据的准确性,通过这种手段保证了当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的

    • 没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题,如下图所示:
      在这里插入图片描述
      由于并发操作,如果没有加锁进行并发控制,数据库的最终的一条数据可能为3也有可能为5,导致数值不准确

    二、悲观锁和乐观锁

    首先我们需要清楚的一点就是无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。

    2.1、悲观锁

    悲观锁(Pessimistic Lock): 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

    但是在效率方面,处理加锁的机制会产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,如果已经锁定了一个线程A,其他线程就必须等待该线程A处理完才可以处理

    数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁),以及syncronized实现的锁均为悲观锁

    在这里插入图片描述
    悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证,
    在这里插入图片描述

    2.2、乐观锁

    乐观锁(Optimistic Lock): 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作),乐观锁适用于多读的应用类型,这样可以提高吞吐量

    相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本(version)或者是时间戳来实现,不过使用版本记录是最常用的。

    在这里插入图片描述
    乐观控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。

    三、锁的实现

    悲观锁阻塞事务、乐观锁回滚重试:它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

    3.1 悲观锁的实现方式

    场景:

    有用户A和用户B,在同一家店铺去购买同一个商品,但是商品的可购买数量只有一个

    下面是这个店铺的商品表t_goods结构和表中的数据:
    在这里插入图片描述
    在不加锁的情况下,如果用户A和用户B同时下单,就会报错。

    悲观锁的实现,往往依靠数据库提供的锁机制,在数据库中,我们如何用悲观锁去解决这个事情呢?

    1. 加入当用户A对下单购买商品(臭豆腐)的时候,先去尝试对该数据(臭豆腐)加上悲观锁
    2. 加锁失败:说明商品(臭豆腐)正在被其他事务进行修改,当前查询需要等待或者抛出异常,具体返回的方式需要由开发者根据具体情况去定义
    3. 加锁成功:对商品(臭豆腐)进行修改,也就是只有用户A能买,用户B想买(臭豆腐)就必须一直等待。当用户A买好后,用户B再想去买(臭豆腐)的时候会发现数量已经为0,那么B看到后就会放弃购买
    4. 在此期间如果有其他对该数据(臭豆腐)做修改或加锁的操作,都会等待我们解锁后或者直接抛出异常

    在这里插入图片描述

    那么如何加上悲观锁呢?我们可以通过以下语句给id=2的这行数据加上悲观锁,首先关闭MySQL数据库的自动提交属性。因为MySQL默认使用autocommit模式,也就是说,当我们执行一个更新操作后,MySQL会立刻将结果进行提交,(sql语句:set autocommit=0)

    悲观锁加锁sql语句: select num from t_goods where id = 2 for update

    我们通过开启mysql的两个会话,也就是两个命令行来演示:

    事务A:
    我们可以看到数据是立刻马上就可以查询出来,num=1
    在这里插入图片描述
    事务B:
    我们是可以看到,事务B会一直等待事务A释放锁。如果事务A长期不释放锁,那么最终事务B将会报错,报错如下:Lock wait timeout exceeded; try restarting transaction,表示语句已被锁住
    在这里插入图片描述
    现在我们让事务A执行命令去修改数据,让臭豆腐的数量减一,然后查看修改后的数据,最后commit,结束事务

    在这里插入图片描述

    我们可以看到当我们事务A执行完成之后,臭豆腐的库存只有0个了,这个时候我们用户B再来购买这个臭豆腐的时候就会发现,最后一个臭豆腐已经被用户A购买完了,那么用户B只能放弃购买臭豆腐了。
    在这里插入图片描述
    通过悲观锁我们可以解决因为商品库存不足,导致的商品超出库存的售卖。

    3.1 乐观锁的实现方式

    对于上面的应用场景,我们应该怎么用乐观锁去解决呢?在上面的乐观锁中,我们有提到使用版本号(version)来解决,所以我们需要在t_goods加上版本号,调整后的sql表结构如下:
    在这里插入图片描述
    具体操作步骤如下:
    1、首先用户A和用户B同时将臭豆腐(id=2)的数据查出来
    2、然后用户A先买,用户A将(id=1和version=0)作为条件进行数据更新,将数量-1,并且将版本号+1。此时版本号变为1。用户A此时就完成了商品的购买
    3、 用户B开始买,用户B也将(id=1和version=0)作为条件进行数据更新
    4、更新完后,发现更新的数据行数为0,此时就说明已经有人改动过数据,此时就应该提示用户B重新查看最新数据购买

    在这里插入图片描述

    1、首先我们开启两个会话窗口,输入查询语句:select num from t_goods where id = 2
    事务A:
    在这里插入图片描述

    事务B:
    在这里插入图片描述

    这个时候事务A和事务B同时获取相同的数据

    2、此时事务A进行更新数据的操作,然后在查询更新后的数据
    在这里插入图片描述
    这个时候我们可以看到事务A更新成功,并且库存-1 版本号+1成功

    2、此时事务B进行更新数据的操作,然后在查询更新后的数据
    在这里插入图片描述
    可以看到最终修改的时候失败,数据没有改变。此时就需要我们告知用户B重新处理

    3.1.1 CAS

    说到乐观锁,就必须提到一个概念:CAS
    什么是CAS呢?Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。
    1、比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)。
    2、设置:如果是,将A更新为B,结束。[1]如果不是,则什么都不做。
    上面的两步操作是原子性的,可以简单地理解为瞬间完成,在CPU看来就是一步操作。
    有了CAS,就可以实现一个乐观锁,允许多个线程同时读取(因为根本没有加锁操作),但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试。 CAS利用CPU指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。

    Java中真正的CAS操作调用的native方法
    因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已,但是CAS有一个问题那就是会产生ABA问题,什么是ABA问题,以及如何解决呢?

    ABA 问题:
    如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

    ABA 问题解决:
    我们需要加上一个版本号(Version),在每次提交的时候将版本号+1操作,那么下个线程去提交修改的时候,会带上版本号去判断,如果版本修改了,那么线程重试或者提示错误信息~

    四、如何选择

    悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。

    但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

    注意点:

    1、乐观锁并未真正加锁,所以效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。

    2、悲观锁依赖数据库锁,效率低。更新失败的概率比较低。

    五、总结

    这篇文章讲解了悲观锁与乐观锁的区别,以及实现场景,不管是悲观锁还是乐观锁都是人们定义出来的概念,是一种思想,如何有有疑问或者问题的小伙伴可以在下面进行留言,小农看到了会第一时间回复大家,谢谢,大家加油~

    展开全文
  • 乐观锁源码
  • 这个时候,你进行乐观锁并发控制的时候,可能并不是想要用es内部的_version来进行控制,而是用你自己维护的那个version来进行控制。 (2)区别 语法区别: es内部version: ?version=1 external version:?v
  • Gorm乐观锁 这是一个基于的乐观锁插件。 快速开始 func BenchmarkUpdateWithOptimistic ( b * testing. B ) { dsn := "root:abc123@/test?charset=utf8&parseTime=True&loc=Local" db , err := gorm . Open ( ...
  • 36谈谈MySQL支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景?

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 99,091
精华内容 39,636
关键字:

乐观锁