精华内容
下载资源
问答
  • 乐观锁与悲观锁

    2021-04-30 11:46:54
    1、悲观锁 1.1 定义 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后...

    1、悲观锁

    1.1 定义

    总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。在Java中,synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁

    手动加悲观锁:

    • 读锁:LOCK tables test_db read,释放锁:UNLOCK TABLES;
    • 写锁:LOCK tables test_db WRITE,释放锁:UNLOCK TABLES;

    读锁与写锁

    • 如果要更新数据,那么加锁的时候就直接加写锁,一个线程持有写锁的时候别的线程无论读还是写都需要等待;
    • 如果是读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1)
    • 虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁。

    1.2 特点

    • 优点:可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高;
    • 缺点:因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高;

    2、乐观锁

    2.1 定义

    总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下,在此期间有没有别人去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS。

    形象化记忆:乐观锁认为对数据的操作不会产生冲突,所以不会加锁,而是在提交更新的时候才会对数据的冲突与否进行检测。如果发现冲突,要么再重试一次,要么切换为悲观的策略。乐观并发控制要解决的是数据库并发场景下的写-写冲突,指用无锁的方式去解决。

    2.2 特点

    • 优点:乐观锁是一种并发类型的锁,其本身并不对数据进行加锁,而是通循环重试CAS进而实现锁的功能其不对数据进行加锁就意味着允许多个线程同时读取(因为根本没有加锁操作)数据,但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试,这种方式大大的提高了数据操作的性能因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。

    • 缺点

    2.3 乐观锁常见的两种实现方式

    乐观锁一般会使用版本号机制或CAS算法实现

    2.3.1 版本号机制

    在这里插入图片描述

    2.3.2 CAS算法

    2.3.2.1 CAS算法的定义

    compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

    • 需要读写的内存值 V
    • 旧的预期值 A
    • 拟写入的新值 B

    当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

    2.3.2.2 举例
    • t1和t2线程同时去修改内存中的同一变量56,他们会把主内存的值完全拷贝一份到自己的工作内存空间,所以t1和t2线程的预期值都为56。
    • 假设t1在与t2线程竞争中,线程t1能去更新变量的值,而其他线程都失败(失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试)。
    • t1线程去更新变量值改为57,然后写到内存中。此时对于t2来说,内存值变为了57,与预期值56不一致,就操作失败了(想改的值不再是原来的值)。
    • 通俗的解释是:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升
    2.3.2.3 CAS算法的缺点
    • ABA 问题及其 解决方案 :

    thread1意图对val=1进行操作变成2,cas(val,1,2)。
    thread1先读取val=1;可是thread1被抢占,让thread2运行。
    thread2修改val=3,又修改回1。thread1继续执行,发现期望值与“原值”(其实被修改过了)相同,完成CAS操作。

    添加额外的标记用来指示是否被修改:从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。


    • 循环时间长,开销大
      自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

    • 只能保证一个共享变量的原子操作
      CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。所以,我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作

    3、两者的比较

    • 1、乐观锁并未真正加锁(不是真正的锁),效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
    • 2、悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
    • 3、悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

    参考:https://zhuanlan.zhihu.com/p/31537871
    参考:https://blog.csdn.net/qq_34337272/article/details/81072874
    参考:https://blog.csdn.net/qq_34337272/article/details/81072874
    参考:https://mp.weixin.qq.com/s/1o-zFPBWYg4bo05MnJqvrQ
    参考:https://mp.weixin.qq.com/s/QKyNL-piwFTfEDZJYSwzXA

    展开全文
  • golang的乐观锁与悲观锁

    千次阅读 2021-09-23 10:15:55
    golang的乐观锁与悲观锁基本概念 基本概念 乐观锁悲观锁是两种思想,用于解决并发场景下的数据竞争问题。 乐观锁乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行...

    golang的乐观锁与悲观锁

    基本概念

    1. 乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。

    2. 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

    3. 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

    package main
    
    import (
    	"fmt"
    	"sync"
    	"sync/atomic"
    	"time"
    )
    
    func IncreValue1() {
    	value1++
    }
    
    func IncreValue2() {
    	atomic.AddInt32(&value2, 1)
    }
    
    func IncreValue3() {
    	lock.Lock()
    	value3++
    	lock.Unlock()
    }
    
    var value1, value2, value3 int32
    var lock sync.Mutex
    
    func main() {
    	//开启1000个线程,并执行自增操作
    	for i := 0; i < 1000; i++ {
    		go IncreValue1()
    		go IncreValue2()
    		go IncreValue3()
    	}
    	//打印结果
    	time.Sleep(1000)
    	fmt.Println("线程不安全:", value1)
    	fmt.Println("乐观锁:", value2)
    	fmt.Println("悲观锁:", value2)
    }
    

    value1的值每次执行都小于1000,因为没有做任何线程安全的操作。
    func AddInt32(addr *int32, delta int32) (new int32)

    第一个参数是指针,是因为该函数需要获得到被操作值在内存中的存放位置(如果不是指针就是传值了),以便施加特殊的CPU指令(原子性是在硬件层面保证的)。

    从另一个角度看,对于一个不能被取址的数值,我们是无法进行原子操作的。

    1. atomic.AddInt64、atomic.AddUint32,atomic.AddUint64,atomic.AddUintptr
    2. 除了支持这些类型外还有另外四种方法CompareAndSwapInt32,LoadInt32,StoreInt32,SwapInt32(比较并交换,载入(拿),存储(放),交换)

    https://www.kancloud.cn/digest/batu-go/153537
    http://ifeve.com/go-concurrency-atomic/

    展开全文
  • 乐观锁与悲观锁详解

    2021-04-17 23:15:46
    LongAddr 的兄弟类如下: LongAdder兄弟类 八、如何选择 在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。 1️⃣响应效率:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不...

    一、并发控制

    当程序中可能出现并发的情况时,就需要保证在并发情况下数据的准确性,以此确保当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。这种手段就叫做并发控制。并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。

    没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。
    在这里插入图片描述

    常说的并发控制,一般都和数据库管理系统(DBMS)有关。在 DBMS 中的并发控制的任务,是确保在多个事务同时存取数据库中同一数据时,不破坏事务的隔离性、一致性和数据库的统一性。

    实现并发控制的主要手段大致可以分为乐观并发控制和悲观并发控制两种。
    首先要明确:无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像 hibernate、tair、memcache 等都有类似的概念。所以,不应该拿乐观锁、悲观锁和其他的数据库锁等进行对比。乐观锁比较适用于读多写少的情况(多读场景),悲观锁比较适用于写多读少的情况(多写场景)。

    二、悲观锁(Pessimistic Lock)

    1️⃣理解
    当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。

    悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
    在这里插入图片描述

    之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:

    传统的关系型数据库使用这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
    Java 里面的同步 synchronized 关键字的实现。
    2️⃣悲观锁主要分为共享锁排他锁

    共享锁【shared locks】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
    排他锁【exclusive locks】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。
    3️⃣说明
    悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。

    三、乐观锁(Optimistic Locking)

    1️⃣理解
    乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。
    在这里插入图片描述

    乐观锁机制采取了更加宽松的加锁机制。乐观锁是相对悲观锁而言,也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。乐观锁的实现:

    CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
    版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会+1。当线程A要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
    2️⃣说明
    乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。

    四、具体实现

    1️⃣悲观锁实现方式
    悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:

    1.在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locks)。
    2.如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
    3.如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
    4.期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。
    拿比较常用的 MySql Innodb 引擎举例,来说明一下在 SQL 中如何使用悲观锁。

    要使用悲观锁,必须关闭 MySQL 数据库的自动提交属性。因为 MySQL 默认使用 autocommit 模式,也就是说,当执行一个更新操作后,MySQL 会立刻将结果进行提交。(sql语句:set autocommit=0)

    以电商下单扣减库存的过程说明一下悲观锁的使用:
    在这里插入图片描述

    以上,在对id = 1的记录修改前,先通过 for update 的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。

    如果以上修改库存的代码发生并发,同一时间只有一个线程可以开启事务并获得id=1的锁,其它的事务必须等本次事务提交之后才能执行。这样可以保证当前的数据不会被其它事务修改。

    上面提到,使用 select…for update 会把数据给锁住,不过需要注意一些锁的级别,MySQL InnoDB 默认行级锁。行级锁都是基于索引的,如果一条 SQL 语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。

    2️⃣乐观锁实现方式乐观锁不需要借助数据库的锁机制。

    主要就是两个步骤:冲突检测和数据更新。比较典型的就是 CAS (Compare and Swap)。

    CAS 即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS 操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值(V)与预期原值(A)相匹配,那么处理器会自动将该位置值更新为新值(B)。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置(V)应该包含值(A)。如果包含该值,则将新值(B)放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。Java 中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个 CAS。java.util.concurrent包下大量的类都使用了这个 Unsafe.java 类的 CAS 操作。

    当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。比如前面的扣减库存问题,通过乐观锁可以实现如下:

    乐观锁使用
    在这里插入图片描述
    在更新之前,先查询一下库存表中当前库存数(quantity),然后在做 update 的时候,以库存数作为一个修改条件。当提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。

    以上更新语句存在一个比较严重的问题,即ABA问题:
    在这里插入图片描述

    1.比如说线程一从数据库中取出库存数 3,这时候线程二也从数据库中取出库存数 3,并且线程二进行了一些操作变成了 2。
    2.然后线程二又将库存数变成 3,这时候线程一进行 CAS 操作发现数据库中仍然是 3,然后线程一操作成功。
    3.尽管线程一的 CAS 操作成功,但是不代表这个过程就是没有问题的。

    一个比较好的解决办法,就是通过一个单独的可以顺序递增的 version 字段。优化如下:
    在这里插入图片描述

    乐观锁每次在执行数据修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题。除了 version 以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。

    以上 SQL 其实还是有一定的问题的,就是一旦遇上高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。所以,还是要想办法减少乐观锁的粒度。一个比较好的建议,就是减小乐观锁力度,最大程度的提升吞吐率,提高并发能力!如下:

    在这里插入图片描述

    以上 SQL 语句中,如果用户下单数为 1,则通过quantity - 1 > 0的方式进行乐观锁控制。在执行过程中,会在一次原子操作中查询一遍 quantity 的值,并将其扣减掉 1。

    高并发环境下锁粒度把控是一门重要的学问。选择一个好的锁,在保证数据安全的情况下,可以大大提升吞吐率,进而提升性能。

    五、理解 CAS 底层

    														流程图
    

    在这里插入图片描述

    假如说有 3 个线程并发的要修改一个 AtomicInteger 的值,底层机制如下:

    1.首先,每个线程都会先获取当前的值,接着走一个原子的 CAS 操作。原子的意思就是这个 CAS 操作一定是自己完整执行完的,不会被别人打断。
    2.然后 CAS 操作里,会比较一下,现在的值是不是刚才获取到的那个值。如果是,说明没人改过这个值,然后设置成累加 1 之后的一个值。
    3.同理,如果有人在执行 CAS 的时候,发现之前获取的值跟当前的值不一样,会导致 CAS 失败。失败之后,进入一个无限循环,再次获取值,接着执行 CAS 操作。

    六、CAS 典型应用

    java.util.concurrent.atomic包下的类大多是使用 CAS 操作来实现的,比如 AtomicInteger、AtomicBoolean、AtomicLong。一般在竞争不是特别激烈的时候,使用该包下的原子操作性能比使用 synchronized 关键字的方式高效的多(查看 getAndSet(),可知如果资源竞争十分激烈的话,这个 for 循环可能会持续很久都不能成功跳出。不过这种情况可能需要考虑降低资源竞争才是)。
    在较多的场景都可能会使用到这些原子类操作。一个典型应用就是计数了,在多线程的情况下需要考虑线程安全问题。

    1️⃣支持计数功能 Demo 实现

    public class Increment {
        private int count = 0;
        public void add() {
            count++;
        }
    }
    

    在并发环境下对 count 进行自增运算是不安全的,为什么不安全以及如何解决这个问题呢?

    2️⃣为什么并发环境下的 count 自增操作不安全?因为 count++ 不是原子操作,而是三个原子操作的组合:

    1.读取内存中的 count 值赋值给局部变量 temp;
    2.执行 temp+1 操作;
    3.将 temp 赋值给 count。
    所以如果两个线程同时执行 count++ 的话,不能保证线程一按顺序执行完上述三步后线程二才开始执行。

    3️⃣并发环境下 count++ 不安全问题的解决方案

    方案①:synchronized 加锁。同一时间只有一个线程能加锁,其他线程需要等待锁,这样就不会出现 count 计数不准确的问题了:

    public class Increment {
        private int count = 0;
        public synchronized void add() {
            count++;
        }
    }
    

    但是引入 synchronized 会造成多个线程排队的问题,相当于让各个线程串行化了,一个接一个的排队、加锁、处理数据、释放锁,下一个再进来。同一时间只有一个线程执行,这样的锁有点“重量级”了。这类似于悲观锁的实现,需要获取这个资源,就给它加锁,别的线程都无法访问该资源,直到操作完后释放对该资源的锁。虽然随着 Java 版本更新,也对 synchronized 做了很多优化,但是处理这种简单的累加操作,仍然显得“太重了”。

    方案②:Atomic 原子类。对于 count++ 的操作,完全可以换一种做法,Java 并发包下面提供了一系列的 Atomic 原子类,比如说 AtomicInteger:

    //import java.util.concurrent.atomic.AtomicInteger;
    public static void main(String[] args) {
        public static AtomicInteger count = new AtomicInteger(0);
        public static void increase() {
            count.incrementAndGet();
        }
    }
    

    多个线程可以并发的执行 AtomicInteger 的 incrementAndGet(),意思就是把 count 的值累加 1,接着返回累加后最新的值。实际上,Atomic 原子类底层用的不是传统意义的锁机制,而是无锁化的 CAS 机制,通过 CAS 机制保证多线程修改一个数值的安全性。

    七、CAS 性能优化

    从流程图可以看出来,大量的线程同时并发修改一个 AtomicInteger,可能有很多线程会不停的自旋,进入一个无限重复的循环中。这些线程不停地获取值,然后发起 CAS 操作,但是发现这个值被别人改过了,于是再次进入下一个循环,获取值,发起 CAS 操作又失败了,再次进入下一个循环。在大量线程高并发更新 AtomicInteger 的时候,这种问题可能会比较明显,导致大量线程空循环,自旋转,性能和效率都不是特别好。那么如何优化呢?

    Java8 有一个新的类,LongAdder,它就是尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能,这个类具体是如何优化性能的呢?如图:
    在这里插入图片描述

    LongAdder
    LongAdder 核心思想就是热点分离,这一点和 ConcurrentHashMap 的设计思想相似。就是将 value 值分离成一个数组,当多线程访问时,通过 hash 算法映射到其中的一个数字进行计数。而最终的结果,就是这些数组的求和累加。这样一来,就减小了锁的粒度。

    LongAddr 的兄弟类如下:
    LongAdder兄弟类
    在这里插入图片描述

    八、如何选择

    在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。
    1️⃣响应效率:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
    2️⃣冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率。冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大。
    3️⃣重试代价:如果重试代价大,建议采用悲观锁。悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
    4️⃣乐观锁如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户从新操作。悲观锁则会等待前一个更新完成。这也是区别。

    随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被应用到生产环境中了,尤其是并发量比较大的业务场景。

    MySQL乐观锁电商库存并发问题应用

    展开全文
  • 下面我们来看看如何合理的使用乐观锁与悲观锁何为悲观锁悲观锁(Pessimistic Lock):就是很悲观,每次去取数据的时候都认为别人会去修改,所以每次在取数据的时候都会给它上锁,这样别人想拿这个数据就会block直到它...

    针对 MySQL的乐观锁与悲观锁的使用,基本都是按照业务场景针对性使用的。针对每个业务场景,对应的使用锁。

    但是两种锁无非都是解决并发所产生的问题。下面我们来看看如何合理的使用乐观锁与悲观锁

    何为悲观锁

    悲观锁(Pessimistic Lock):就是很悲观,每次去取数据的时候都认为别人会去修改,所以每次在取数据的时候都会给它上锁,这样别人想拿这个数据就会block直到它取到锁。比如用在库存增减问题上,利用悲观锁可以有效的防止减库存问题。

    简单来讲,悲观锁就是假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。悲观并发控制实际上是 “先取锁,再访问” 的保守策略,为数据处理的安全提供了保证。

    在效率上,处理加锁的机制会让数据库产生额外的开销,还会有死锁的可能性。降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。、

    悲观锁与乐观锁的区别

    1 优缺点

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

    2 实现方式

    悲观锁的实现方式:悲观锁的实现,依靠数据库提供的锁机制。

    在数据库中,悲观锁的流程如下:

    1 在对数据修改前,尝试增加排他锁。

    2 加锁失败,意味着数据正在被修改,进行等待或者抛出异常。

    3 加锁成功,对数据进行修改,提交事务,锁释放。

    4 如果我们加锁成功,有其他线程对该数据进行操作或者加排他锁的操作,只能等待或者抛出异常。

    乐观锁的实现方式:

    1)version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

    sql实现代码

    update table set n=n+1, version=version+1 where id=#{id} and version=#{version};

    2)CAS(定义见后)操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

    悲观锁与乐观锁的合理使用

    本质上,MySQL的乐观锁与悲观锁主要都是用来解决并发的场景,避免丢失更新问题。

    乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

    悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

    用一个场景与代码来详细的介绍一下如何合理使用,假设有这么一个商品秒杀和抢购的场景:在抢购场景中,一共只有100个商品,在最后一刻,已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致商品超发。也就是:导致了并发用户B也“抢购成功”,多让一个人获得了商品。

    1 用悲观锁的方案:悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

    方案:使用MySQL的事务,锁住操作的行

    include('./mysql.php');

    //生成唯一订单号

    function build_order_no(){

    return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);

    }

    //记录日志

    function insertLog($event,$type=0){

    global $conn;

    $sql="insert into ih_log(event,type)

    values('$event','$type')";

    mysqli_query($conn,$sql);

    }

    //模拟下单操作

    //库存是否大于0

    mysqli_query($conn,"BEGIN"); //开始事务

    //此时这条记录被锁住,其它事务必须等待此次事务提交后才能执行

    $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";

    $rs=mysqli_query($conn,$sql);

    $row=$rs->fetch_assoc();

    if($row['number']>0){

    //生成订单

    $order_sn=build_order_no();

    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)

    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";

    $order_rs=mysqli_query($conn,$sql);

    //库存减少

    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";

    $store_rs=mysqli_query($conn,$sql);

    if($store_rs){

    echo '库存减少成功';

    insertLog('库存减少成功');

    mysqli_query($conn,"COMMIT");//事务提交即解锁

    }else{

    echo '库存减少失败';

    insertLog('库存减少失败');

    }

    }else{

    echo '库存不够';

    insertLog('库存不够');

    mysqli_query($conn,"ROLLBACK");

    }

    上述的方案解决了线程安全的问题,但是,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。

    稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。

    全部请求采用“先进先出”的队列方式来处理,解决了锁的问题。但是新的问题来了,在高并发的场景下请求很多,可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。这个时候,我们就可以用乐观锁来解决相关问题了。

    上面也提到,乐观锁是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。

    我们用Redis中的watch来实现乐观锁,通过这个实现,保证数据的安全。

    $redis = new redis();

    $result = $redis->connect('127.0.0.1', 6379);

    echo $mywatchkey = $redis->get("mywatchkey");

    /*

    //插入抢购数据

    if($mywatchkey>0){

    $redis->watch("mywatchkey");

    //启动一个新的事务。

    $redis->multi();

    $redis->set("mywatchkey",$mywatchkey-1);

    $result = $redis->exec();

    if($result) {

    $redis->hSet("watchkeylist","user_".mt_rand(1,99999),time());

    $watchkeylist = $redis->hGetAll("watchkeylist");

    echo "抢购成功!
    ";

    $re = $mywatchkey - 1;

    echo "剩余数量:".$re."
    ";

    echo "用户列表:

    ";

    print_r($watchkeylist);

    }else{

    echo "手气不好,再抢购!";exit;

    }

    }else{

    // $redis->hSet("watchkeylist","user_".mt_rand(1,99999),"12");

    // $watchkeylist = $redis->hGetAll("watchkeylist");

    echo "fail!
    ";

    echo ".no result
    ";

    echo "用户列表:

    ";

    //var_dump($watchkeylist);

    }*/

    $rob_total = 100; //抢购数量

    if($mywatchkey<=$rob_total){

    $redis->watch("mywatchkey");

    $redis->multi(); //在当前连接上启动一个新的事务。

    //插入抢购数据

    $redis->set("mywatchkey",$mywatchkey+1);

    $rob_result = $redis->exec();

    if($rob_result){

    $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),$mywatchkey);

    $mywatchlist = $redis->hGetAll("watchkeylist");

    echo "抢购成功!
    ";

    echo "剩余数量:".($rob_total-$mywatchkey-1)."
    ";

    echo "用户列表:

    ";

    var_dump($mywatchlist);

    }else{

    $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao');

    echo "手气不好,再抢购!";exit;

    }

    }

    总结

    1 要记住锁机制一定要在事务中才能生效,事务也就要基于MySQL InnoDB 引擎。

    2 访问量不大,不会造成压力时使用悲观锁,面对高并发的情况下,我们应该使用乐观锁。

    3 读取频繁时使用乐观锁,写入频繁时则使用悲观锁。还有一点:乐观锁不能解决脏读的问题。

    以上内容希望帮助到大家,更多PHP大厂PDF面试文档,PHP进阶架构视频资料,PHP精彩好文可以关注公众号:PHP开源社区,或者访问:

    作者:phper

    来源:https://mp.weixin.qq.com/s/yNEQ_77m0D_4p-DVLpuz4w

    展开全文
  • MySQL -乐观锁与悲观锁

    2021-01-23 15:15:43
    乐观锁与悲观锁是数据库的一种思想,和其他的排它锁,共享锁之类的不是一类含义。在并发的情况下,采用乐观锁或者悲观锁可以防止数据问题。 悲观锁 定义 悲观锁乐观锁
  • 何谓悲观锁与乐观锁乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。悲观锁总是假设...
  • 乐观锁与悲观锁概述乐观锁总是假设最好的情况,每次去读数据的时候都认为别人不会修改,所以不会上锁, 但是在更新的时候会判断一下在此期间有没有其他线程更新该数据, 可以使用版本号机制和CAS算法实现。 乐观锁...
  • 数据库中的乐观锁与悲观锁详解

    千次阅读 2021-02-20 15:23:15
    悲观锁 乐观锁 悲观锁实现方式 乐观锁实现方式 如何选择 悲观锁 当我们要对一个数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。 这种借助...
  • 悲观锁,就很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据里边 就用到了很多这种锁机制,比如行锁,表锁等,读锁,写...
  • 悲观锁 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样...乐观锁 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断
  • 浅析乐观锁与悲观锁

    2021-01-27 04:19:00
    悲观锁当我们要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制在修改数据之前锁定,再修改的方式被称为悲观并发控制(PCC)。...
  • 机制乐观锁是一种思想,具体实现是,表中有一个版本字段,第一次读的时候,获取到这个字段。处理完业务逻辑开始更新的时候,需要再次查看该字段的值是否和第一次的一样。如果一样更新,反之拒绝。之所以...
  • 自旋锁与互斥锁 1、初始状态下,锁的状态为未被占有 2、A进程需要访问临界区代码,尝试获取锁。此时发现锁可用,则将锁lock,进入临界区执行临界区代码 3、B进程需要访问临界区代码,尝试获取锁。此时发现锁呢已经被...
  • 五、乐观锁悲观锁的区别。 一、什么是乐观锁。 它假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数 据的冲突与否进行检测,如果发现冲突了,则返回用户错误的信息,让用户决定...
  • (locking)业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算处理中,我们希望针对某个cut-off时间点的数据进行处理,而不希望在结算进行过程中(可能是几秒种,也可能是几个小时),数据...
  • 乐观锁悲观锁都是用于解决并发场景下的数据竞争问题,但是却是两种完全不同的思想。它们的使用非常广泛,也不局限于某种编程语言或数据库。 乐观锁的概念: 乐观锁:指的是在操作数据的时候非常乐观,乐观地认为...
  • 2) mysql事务隔离级别Read Committed(读取提交内容)如果是Django2.0以下的版本,需要去修改到这个隔离级别,不然乐观锁操作时无法读取已经被修改的数据RepeatableRead(可重读)这是这是Mysql默认的隔离级别,可以到...
  • 何谓悲观锁与乐观锁 乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。 悲观...
  • 为什么需要(并发控制) 在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。 典型的冲突有: 1.丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的...
  • 注明: 本文转载自http://www.hollischuang.com/archives/934...乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。...
  • mysql--乐观锁与悲观锁

    2021-10-17 13:48:57
    也不仅仅是mysql几乎所有涉及锁的地方都分为乐观锁悲观锁,比如java的lock和cas、synchronized的锁升级、redis的watch乐观锁和Lua脚本实现悲观锁等等,下面将会详细介绍下mysql的乐观锁与悲观锁 二、悲观锁 悲观...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 64,902
精华内容 25,960
关键字:

乐观锁与悲观锁