jdk中的CAS实现乐观锁 vs 数据库乐观锁

记录一次面试经历,有问到乐观锁,然后问了一堆问题,我也没搞懂到底他再问什么,然后猜测了半天,他也提示了一些,总结成如下两点:

1. ABA问题。这个是我猜测了半天提出来的,他一直在说别的线程更新了,该怎么办,我开始理解的都是并发冲突了怎么办?然后我说是常见的做法就是一定的重试或者直接抛异常让用户重试,然后又balabala了一堆,然后才知道说的是ABA问题。

2. 乐观锁应用场景问题。这个也是,说了很多,最终我也是没理解到他的意思,最终他总结了一下:换句话说什么场景下使用乐观锁。我的回答是如果使用悲观锁效率不高,会锁很长时间,而我们期望更高的效率可考虑使用乐观锁。看来这不是他想要的答案,然后又balabala了一堆,看我还是没get到他的点,然后绝对下一个环节。我多问了一句,要不提示一下,我也学习一下。然后他的答案是:并发量小的场景可以用乐观锁,然后冲突后重试就可以成功,当并发量大了不适合用乐观锁。回头想想,这个答案是有点问题的,具体的在最后我会表达下我自己的看法。

所以下面结合java的CAS来记录下个人对乐观锁的理解。先说结论:我觉得乐观锁是一个思路。在不同的地方说的自旋锁、轻量级锁、CAS、免锁机制等等,我觉得都是这种思想的体现,只是说在实现上有些区别,然后给这种对应的实现搞了个名字而已。比如msyql的乐观锁是不是就是CAS。

CAS

现代操作系统都是分时复用的,即多个进程/线程会分时复用一个cpu来执行cpu指令,操作系统负责调度使用cpu的时间片。不管哪个操作系统、调度算法如何,调度的基本单位都是cpu时间片,换句话说,当一个进程/线程获得cpu使用权,那么这个进程/线程至少使用一个cpu时间片,而cpu会保证一个cpu指令一定会在一个时间片内执行完成,即cpu执行指令是原子的,不会中途打断将cpu分配给其他进程/线程。

现在的编程语言,一个条语句经过层层编译,最终都是翻译成一条条cpu指令的(ps:即使是汇编语言,一条汇编指令也是可能最终翻译成多条cpu指令的),对于java来说,绝大部分的语句最终翻译成机器指令的时候,一条语句都会被翻译成多条cpu指令,那么执行一条java语句,可能中途会被打断,经过多次时间片的调度切换才能完成,那么这条语句不是原子的,就可能存在并发安全问题,最典型的就是++操作。但是java中的赋值语句是原子的。

ps:很多地方会特意强调除了long/double以外的基本类型及引用的复制语句是原子的,而volatile的long和double赋值语句才是原子的。这种情况主要是指在32位的jvm上,因为在32位的jvm上,栈帧是划分成很多32字节的slot,read、load、store、write将数据加载到工作内存/写入主存都是原子的,但是一个long是64位的,jvm规范中允许在实现java虚拟可以将long/double这种64位的数据可以不保证read、load、store、write这4个操作的原子性,也就是说多线程对long/double的操作是不安全的,但jvm规范仅仅是允许,但是强烈建议将其实现成原子操作。所以一个是这种情况非常罕见才会出现,第二我们常用的jvm虚拟机是实现了long/double的原子操作(这个是揣测的,没有任何证实)

对于java来说,如果我们想要实现对一个共享变量的操作是线程安全的,就需要使用锁来防护所有访问这个共享变量的代码块。当然java也提供了一些能保证线程安全的免锁数据类型:AtomicXXX

这些类型实现免锁的基本原理就是利用CAS(compare and set)。

感性认识一下CAS,望文生义也是先比较后设置值,这给人的第一映像就不是原子的,要先比较然后设置值。那要保证这两个操作的线程安全,怎么办?加锁,让比较和设置值使用锁防护可保证线程安全。但是这不是使用了锁么?免锁在哪儿呢?

所以说要是的CAS有意义,保证CAS操作是原子的,需要cpu指令级别的支持,保证compare和set是原子的。好在现代操作系统都已经支持了cas指令了,也就是对一个变量的操作可以通过cas指令来完成,那就一定是原子的。

对于java语言来说,将底层的所有操作都封装起来了,我们基本上没办法通过java的语法去操作内存等,现在也没有哪个java语句编译后会是一条cas指令,因为java认为对这些的访问都是不安全的,容易造成问题,所以没有对外提供直接访问内存等底层资源的方式。但是java使用了一个Unsafe类,它里面封装了一些对底层资源的访问,包括直接访问内存、使用cas指令等,但是这个类只允许jdk访问,其他地方使用Unsafe会报错。AtomicXXX就是基于Unsafe封装的CAS来实现免锁的线程安全的数据类型的。

AtomicLong

AtomicLong的几个重要成员变量:

private static final Unsafe unsafe = Unsafe.getUnsafe();

private volatile long value;

private static final long valueOffset;
  • unsafe:AtomicLong的免锁是利用Unsafe封装的cas指令来实现的,所以需要使用到unsafe的方法。
  • value:这个就是具体的值。
  • valueOffset:在java语言层面,我们要访问一个对象的变量,只能通过这个变量的引用。但本质上,这个变量都是存到一段内存的,我们直接访问这段内存,效果其实是一样的,只是java没有提供这样的预发。但是在Unsafe的封装了地缝访问内存的方法,可以通过内存偏移量直接访问一个对象的属性。而Unsafe的cas的实现就是直接去访问内存的,所以在使用CAS的时候传给Unsafe的方法是对象属性的内存偏移量,而不是字段名。对于java对象来说,一旦初始化分配内存后,对象中的某个属性的内存地址就是固定的,所以,这里将AtomicLong的value字段的内存偏移量给记录下来,方便CAS的使用。这个值是在分配内存的时候通过unsafe.objectFieldOffset()来获得的
 static {
        try {
            valueOffset = unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

下面以一个利用CAS操作保证原子性的操作为例来看AtomicLong是如何利用CAS实现免锁线程安全的:AtomicLong#getAndSet(long newValue),

// AtomicLong一个利用CAS更新值的方法:将Atomic的值更新成newValue,返回老的值
 public final long getAndSet(long newValue) {
        // 这里调用的是Unsafe的CAS方法
        return unsafe.getAndSetLong(this, valueOffset, newValue);
}

这个方法及时直接调用的Unsafe封装的同方法:

    public final long getAndSetLong(Object obj, long valueOffset, long newValue) {
        long oldValue;
        do {
            //通过对象及偏移量获得属性的值。getLongVolatile()是个native防范
            oldValue = this.getLongVolatile(obj, valueOffset);
            
           // cas来更新值。compareAndSwapLong()方法是个native方法,封装的就是cAS指令
        } while(!this.compareAndSwapLong(obj, valueOffset, oldValue, newValue));// cas不成功就在无限重试

        return oldValue;
    }

使用的两个Unsafe的两个native方法:

public native long getLongVolatile(Object obj, long valueOffset);    

public final native boolean compareAndSwapLong(Object obj, long valueOffset, long except, long newValue);

看懂了这个方法,再去看其他的方法,就非常容易了,因为AtomicLong这个类型实现本身也不复杂,主要就是利用了Unsafe的封装。

by the way,AtomicLong的set()方法,可以发现,没有使用Unsafe的CAS的,不会有问题?是没问题的,原因就是java的基本类型的复制指令是原子的

AtomicLong的问题:

就是这里如果CAS失败,这里在无限重试,直到成功为止。极端一点并发特别高,每次CAS失败,那么一个线程就会一致再这疯狂的重试,然后更多的线程进来重试,那么cpu会被瞬间打爆的。

所以说,AtomicLong这种类型,只是适合并发冲突不高的场景。ps:如果是并发冲突不高,为啥要用呢,直接synchronize关键字不也一样么,在jdk6优化以后,并发不高的情况下,其实synchronize也是免锁的(偏向锁、轻量锁、自旋锁这些和CAS差不多,并不会使用操作系统的重量级锁,不会有内核态的切换等耗时操作),理论上不一定比CAS慢,只是说要自己去显示控制线程同步。

ps:所有的Atomicxxx的实现和AtomicLong其实都是一样的。

LongAdder

个人觉得LongAdder的源码比AtomicLong更值得学习,这里面的设计思想非常精巧,在jdk8中,ConcurrentHashMap也放弃了jdk7中分段锁的实现,转而采用了LongAdder的思想重新实现了。

在AtomicLong的实现中,如果CAS失败,就无限的重试,直到成功为止,但是在LongAdder是想办法尽量减少冲突。

产生冲突的本质原因是多个线程共享一个变量的场景,为了保证对这个变量操作的线程安全,那就必须采取措施,不让多个线程同时操作这个共享变量。一种方式是加锁,这里不多说了。一种方式是如果一个线程尝试去操作的时候发现有其他线程正在操作,那我就放弃本次操作,过会再来,这就是AtomicLong的思路;那LongAdder的思路就是将这个共享变量打散,变成很多碎片,当遇到多个线程同时更改这个变量的时候,给每个线程分配一个碎片,各自更改一个碎片,然后在读取这个变量的时候,将所有的碎片整合到一起,对外部看来,这就是一个变量。

将一个变量分成了n多部分:一个base和m个cell,其中base和cell背后其实都是一个long型的变量。当更新LongAdder数据的时候,首先尝试用CAS去更新base,如果没有冲突就会更新成功,就结束了;如果有冲突,就随机选择一个cell,然后用CAS将数据更新到这个cell上;如果也冲突,那就重新换个cell重试;如果还冲突就将cell的个数扩充2倍,然后再重新选一个cell来cas将数据更新到这个cell上。

通过这样的方式,将冲突分摊到了很多个cell上,理论上大大的会减少冲突的。过程如下(以LongAdder#add()方法为例):

这个方法的代码就不源码了,搞清楚它这里的的基本思想,看懂源码就比较容易了。不过这里贴一下读取LongAdder中实际的数据时候的代码,也不用多解释,看懂上面的过程,自然就懂:

ps:读取具体数据都是调用了这个方法的。

    public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

LongAdder的问题:

  1. 实时性。这里是在读取的时候去将各个cell以及base中维护的值累加起来的,所以有一定的延迟。如果对实时性要求非常高,慎用LongAdder
  2. LongAdder利用这种方式只是极大的减少了冲突。如果再极端点,冲突特别的大,那么LongAdder也在无限的重试,并且还会不断的去扩容cells数组。扩容数组是个比较耗时的过程。ps:看源码里其实在扩容之前还是有些重试的,不是每次冲突都去扩容。

在LongAdder中,除了将一个变量打散来降低冲突概率的思路很有意思,在初始化cells数组、cells数组扩容、新建一个Cell添加到cells数组中,为了保证对数组的操作的安全性,自己实现了一个锁机制:cellsBusy。

对这个cellsBusy的理解可以类比一下AQS的state字段,只是说这里的cellsBusy实现的锁和AQS不一样,它不是一个通用的,是专门正对这种场景实现的,所以它仅仅是个cells数组是否被其他线程正在修改的一个标志,不考虑重入等问题。

cellsBusy=0表示锁空闲,即当前没有线程在操作cells数组;cellsBusy=1表示锁被其他线程占用,其他线程在操作cells数组(初始化、扩容、添加一个新的Cell到数组中)

  • 加锁:cas将cellsBusy设置为1,cas成功就是获得锁;cas失败就是锁争抢失败
  • 释放锁:直接将cellsBusy=0。ps:为啥加锁的时候要用CAS,不是直接cellsBusy=1,而释放锁的时候就直接赋值而不用CAS了呢?

CAS的问题:

不管是AtomicLong还是LongAdder,都存在一个问题:ABA,即在CAS的过程中,如果其他线程动作特别快,将值原本为a的值变为了b、然后迅速的又变成了a,在CAS看来,值没有变化,所以会更新成功的。再绝大多数场景下,这种ABA是不会有什么问题的。要解决这个问题,可以采用给数据加上版本,CAS的判断是判断版本而不是数据本身,而版本会随着每次数据变更而变化。

乐观锁

回顾下上面分析的java中的CAS的场景:是一个jvm进程中,多个线程共享jvm进程的一个变量的时候,要保证多线程访问这个贡献变量的线程安全,就必须采取线程同步措施。一种是加锁,一种就是上述的利用CAS实现的免锁线程同步机制。

总结一下:这是单进程中,多线程访问内存中贡献变量的场景。

将场景扩充一下:到了分布式多进程并发的场景。那么cpu指令级别的CAS还能用么?肯定是不行的,因为多个进程可能都不运行在一个机器上,甚至不同的城市。java的synchronize锁、基于AQS实现的锁、基于CAS实现的原子类型都不能使用了,因为这些都是一个进程内的线程同步机制。不过同样可以借助这些线程同步机制,来实现分布式环境下的多进程同步。比如synchronize锁/AQS锁对应的就是分布式锁,而对于CAS就是对应的乐观锁。分布式锁这里不多介绍,主要说一下使用mysql数据库实现乐观锁。

下面以更新库存为例来说明几种保证更新库存不出问题的方式。

悲观锁实现

典型的悲观锁方式:在事务开始,就先锁定对应商品的库存,这样msql的锁会去保证只有一个线程会来修改制定商品的库存,如果有其他线程,将会被阻塞,直到原来线程释放掉X锁,然后这个线程再去执行查询库存-->判断库存-->修改库存的逻辑。从而保证库存修改的正确性,不出现超卖。


public void deductStore(long skuId, int storeCount){
    start transaction;

    select sku_id,store from pd_sku_store where sku_id=#skuId# for update;

    // 业务逻辑:判断库存是否冲突,如果冲突再扣减库存;如果不足就直接返回

    int affectLines = update pd_sku_store set store=store-#storeCount# where         
     sku_id=#skuId# 

    if(affectLines<=0){
        throw new RuntimeExcption("库存更新失败");
    }

    commit;
}

使用事务+悲观锁实现库存更新的几个要点:

1. 当前读:第一个地方的读必须是当前读,不能是快照读。保证每个线程进来读取的一定是最新数据(即只要别人提交的数据,这里一定要读取到已经提交的最新数据)。这个for update能保证是当前读。

2. 锁:一定是要在读数据的时候就加X锁。不能是读锁(lock in share mode),因为如果加的是读锁,虽然可以保证读是当前读,但是因为读锁不排读,所以可能两个线程同时执行第一个读,然后业务逻辑判断都会通过,就可能导致超卖。

3. 如果你的mysql是主从架构,一定要保证第一开始的读是在主库上。这一点一般的主从架构中,如果你使用了本地事务,会直接走到主库的。

4. 在最后的update语句上加上业务规则判断是个好习惯: update pd_sku_store set store=store-#storeCount# where sku_id=#skuId#  and store>=#storeCount#。这样即使第二点的锁使用了读锁,这里是不是也多点保证。

悲观锁的问题:

1. 数据加锁时间比较长。从事务开始,查询的时候就会对数据加锁,直到事务提交锁才会释放。如果中间的业务逻辑复杂点,那这个加锁就会特别长,那这个事务也就成了大事务。加锁时间长会影响并发,且相对更加容易出现死锁等情况。而对于大事务会引发一些列的问题:比如undolog堆积,就会导致一致性读的性能会受影响(简单理解一致性读是根据unlog推算出来的,而不是真的一条数据冗余了很多版本);大事务会导致主从延迟增加;大事务导致数据库连接一直被占用,得不到释放(这种在并发大的情况下就会出现too many connection的异常)等

ps: update pd_sku_store set store=store-#storeCount# where sku_id=#skuId#  and store>=#storeCount#不要不久行了,mysql会保证一个sql执行的原子性的。这个确实没错,这里只是这个用这个简单场景举例说明,如果说中间的业务逻辑比较复杂,甚至说这里的业务逻辑需要张表的数据,比如需要判断sku是上架状态且库存充足,才能去扣减库存,那么一个sql是不是就搞不定了,就需要使用这里的事务防护了。另外,即使是一条sql,mysql是不是也是类似的过程:开启事务-->根据条件查询数据-->更新。只不过store>=#storeCount#作为查询条件罢了,库存不足就查询不出数据,也就不会更新而已。

乐观锁的实现

我们从几个角度(几个方面)来理解乐观锁和悲观锁(这些角度是自我总结,不一定那么准确)

大家非常习惯的角度:从对待并发冲突可能性的态度上:

  • 悲观锁的出发点是:冲突一定会发生,所以我先给加上锁。不管是否会出现冲突,操作前我都加上锁
  • 乐观锁的出发点:冲突不一定发生,所有操作的时候我不加锁,而是等到我实际操作的时候如果发现冲突了,我再来处理冲突就好了。

从实现形式上来看:

  • 悲观锁:一般都是使用的重量级锁。是利用了操作系统的mutex来实现,所以加锁/释放锁等都需要从用户态切换到内核态,代价相对较大。
  • 乐观锁:一般都都是在用户态,业务侧自己实现,不依赖于操作系统的重量级锁,这样就比较轻量,减少开销。从这个角度java的synchronize的偏向锁、轻量级锁、自旋锁、CAS都可以归属到这一类。

还有一种就是从系统分层上来看的,这个我也不太好准确描述(以mysql为例)

  • 悲观锁:业务层强基于mysql事务+mysql锁来实现
  • 乐观锁:业务层不强依赖于mysql事务以及锁来实现。

为什么之类强调不强依赖呢?1. 对于mysql来说,所有的sql执行一定是在事务环境中,不过是单条sql的事务开启/提交被mysql封装掉了,我们不感知而已。2. 对于mysql来说,mysql提供了很多的各种粒度的锁,只是很多场景在业务侧我们对这些锁不感知,比如MDL(meta data lock)。所以这里来的却别在于是从业务层来讲,是否强依赖于事务和锁。

乐观锁的基本思路:其实就是CAS的思想,都是在更新的时候去和原来的值比较,一致就更新;不一致就表示有冲突,更新失败。只是支持CAS这个过程的原子性是不一样。

public void deductStore(long skuId, int storeCount){
    // 查原始数据
    int originStore = select store from pd_sku_store where sku_id=#skuId#;

    // 作业务校验
    if(originStore < storeCount){
        throw new RuntimeException("库存不足");
    }

    // store=#originStore#表示如果库存更改过,本次就不允许更新
    int affectLines = update pd_sku_store set store=store-#storeCount# where sku_id=#skuID# and store=#originStore#

    if(affectLines<=0){
        throw new RuntimeException("价格更新失败");
    }
}

ps:再次说明一下,这里只是个例子,不纠结这个简单场景优化问题:比如这里一条语句搞定update pd_sku_store set store=store-#storeCount# where sku_id=#skuID# and store>=#storeCount#,这里只是借这个例子讨论一下通用思路,而实际业务实现的时候可以根据不同的业务场景,有所变化。

和java的CAS比较:

1. java的CAS为了保证CAS的原子性,所以需要cpu指令的支持。

2. 乐观锁为了CAS的原子性保证,是利用了mysql执行一条sql语句是原子性的。

如上这种实现的问题:

1. ABA问题。比如在线程A查出originStore=100 ,然后线程B将originStore改成了80,然后又有一个线程将originStore改成了100,然后线程A在最后执行update的时候发现originStore还是是100,"没有变化",所以更新成功。但实际上库存是改变过的。

2. 如果有冲突就整体失败了。这个很明显了,只要库存在线程A查出来后到更新前有任何变化,更新就会失败。

为了解决ABA的问题,加入版本;为了加入只要有冲突就报错失败的问题,加入重试。

public void deductStore(long skuId, int storeCount){
    int affectLines=0;
    while(affectLines <= 0){
        affectLines = doDecutStore(skuId, storeCount);
    }
}

private int doDecutStore(long skuId, int storeCount){
    // 查原始数据以及版本。
    int originVersion/originStore = select version,store from pd_sku_store where sku_id=#skuId#;

    // 作业务校验
    if(originStore < storeCount){
        throw new RuntimeException("库存不足");
    }

    return affectLines = update pd_sku_store set store=store-#storeCount# where sku_id=#skuID# and version=#originVersion#
    //and store=#originVersion# 这里根据不同的业务约束,加上前置条件更新,我觉得是个好习惯
}

这种方式可以解决ABA的问题以及只要有冲突就失败的问题,但是需要注意:

  1. 这个version的更新时机,这个其实就要看我们到底想要用乐观锁防护什么?如果仅仅是为了防护库存的更新,那么我们的版本就应该是storeVersion,当且仅当库存变更的时候才会去变更storeVersion。否则如果是个通用的version,任何信息的变更去变更version,如果在查询和更新这个间隙建,哪怕是更改了个名称,也会导致库存更新失败,但实际上跟库存没有任何关系。
  2. 关于重试:如上例子是个无限重试,这个和java的AtomicXXX是一样的。但是如果说更新流量比较大,那么冲突较大的情况下,那么可能非常多的重试;然后重试线程堆积越来越多,db的qps飙升,那么最严重的情况就是db被压垮,业务整体跌0。所以在使用乐观锁的时候,一定要注意冲突的处理策略,如果冲突策略是重试,那就一定要评估了。一个是冲突大不大,一个是重试次数。所以这里个人觉得,乐观锁的冲突策略更好的处理方式就是报错,让用户在界面点击重试。
  3. 关于主从:使用乐观锁查询的时候,如果使用了主从架构,那么一定要是去查主库,否则主从延迟会有问题的。

ps:

1. 凡是在业务中使用自动重试的,都要三思。有效的重试一定只是解决抖动的场景,因为偶尔抖动导致失败的重试才是有效的,除此之外用了重试就应该慎重再慎重。

2. 为了减少数据库冲突,可以借鉴LongAdder的思路,将一个sku的库存进行打散,修改库存的时候,可任意去修改一个库存就好了,但是这个实现会比较复杂,坑也会很多,自己实现是比较容易出错的。举个简单的例子:LongAdder其实代表的就是个数字,加和减都可以只是操作某个cell就好,但是如果是库存,加库存的时候加到任意cell没啥问题,但是减就不一定了,需要考虑是否会导致库存为负,这就需要统筹考虑所有的cell+base了。当然也可以参考synchronize对锁的优化思路,搞个锁升级。比如重试两次还是失败,就切换到悲观锁。

3. 以前听有人说,使用乐观锁有没有事务防护,我到现在也没get到使用乐观锁到底哪个地方必须要用事务防护?如果哪位高手看到这个,一定要评论让我学习一下。相反,如果上述例子加上事务,反而是有问题的(和上面例子没有任何区别,仅仅在前后加了事务),这么写一旦有冲突,会导致无限循环

public void deductStore(long skuId, int storeCount){
    start transaction;

    int affectLines=0;
    while(affectLines <= 0){
        affectLines = doDecutStore(skuId, storeCount);// doDecutStore()就是如上例子中的这个方法
    }

    commit;
}

答案就是:在隔离级别=RR下,会有MVCC,在开启事务(准确的说是start transaction/begion方式开启事务当执行第一条sql的时候),mysql会创建一个一致性读视图,后面在事务内的任何读取都是基于这个一致性视图,根本看不到别的事务已经提交的最新数据,那么重试的时候查询的数据一直是老数据,但是在update的时候却是当前读,然后就会失败。所以如果这里加了事务,反而在读取的时候需要当前读:对于select语句,for update和lock in share mode是当前读。

这里简单贴个例子验证一下:

然后模拟乐观锁重试场景:T5时刻查询到的d是事务开始时刻的快照:

最后一点:什么时候用乐观锁?我的答案就是尽量别用,这条规则是写入了某绝对独角兽互联网的军规中的(一锁二查三判四更新)

1. 基于数据库的乐观锁实现,其实坑还是不少。尽管现在对mysql乐观锁的实现网上讨论很多,但是都比较浅,如果自己实现搞不好就整出什么问题。就比如我,其实我一直没有想通为啥别人说在用乐观锁的时候要用事务防护,但是在我看来用事务防护的乐观锁反而有坑?并且我可以场景复现。那么如果真的有什么坑,那让我去实现,是不是写下第一行代码的时候就埋下了雷呢?

2. 另外就是乐观锁冲突的处理策略。有人说在并发不大冲突小的场景才能使用乐观锁,原因就是冲突后的策略是重试。但是这跟乐观锁本身没关系,而是重试这种冲突处理策略的场景,不宜并发高的场景。现在不管是mysql还是别的什么db,都支持行锁,能够支持的TPS其实都还是比较高了,既然都并发不高了,为啥不用相对更不易出多查场景。我猜这个针对的是像那种不支持行锁,一锁就会锁表的那种场景,那这么考虑倒是有些道理了,因为乐观锁理论上能够支撑的并发度要比表锁更高的。

3. 如果一定要找个应用场景,我能想到的就是解决当业务逻辑比较复杂,一条记录锁太长时间了,可考虑用乐观锁。另外就是,mysql的锁的粒度只到行锁,如果我想到字段级别呢?msyql是搞不定的,那这个时候可用乐观锁。比如上述更新库存的例子,其实除了库存以外的字段有变更对库存没有影响,我只是需要锁定库存这一个字段就好了,但是mysql的锁是做不到的,只能锁住对应的一条记录,这个时候可以尝试考虑乐观锁。

ps:对于要不要用乐观锁,各位大佬有没有什么高见?

总结一下两个疑问,求大佬高见:

1. 为什么有人说乐观锁要用事务防护?有啥坑?

2. 什么场景下用乐观锁而不是悲观锁?

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值