MySQL 乐观锁&悲观锁

悲观锁(Pessimistic Lock)

悲观锁的特点是先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的“一锁二查三更新”即指的是使用悲观锁。通常来讲在数据库上的悲观锁需要数据库本身提供支持,即通过常用的select … for update操作来实现悲观锁。当数据库执行select for update时会获取被select中的数据行的行锁,因此其他并发执行的select for update如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。

这里需要注意的一点是不同的数据库对select for update的实现和支持都是有所区别的,例如oracle支持select for update no wait,表示如果拿不到锁立刻报错,而不是等待,mysql就没有no wait这个选项。另外mysql还有个问题是select for update语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在mysql中用悲观锁务必要确定走了索引,而不是全表扫描。

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

 

乐观锁(Optimistic Lock)

乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。

乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号,或者时间戳,然后按照如下方式实现:

1. SELECT data AS old_data, version AS old_version FROM …;
2. 根据获取的数据进行业务操作,得到new_data和new_version
3. UPDATE SET data = new_data, version = new_version WHERE version = old_version
if (updated row > 0) {
    // 乐观锁获取成功,操作完成
} else {
    // 乐观锁获取失败,回滚并重试
}

乐观锁是否在事务中其实都是无所谓的,其底层机制是这样:在数据库内部update同一行的时候是不允许并发的,即数据库每次执行一条update语句时会获取被update行的写锁,直到这一行被成功更新后才释放。因此在业务操作进行前获取需要锁的数据的当前版本号,然后实际更新数据时再次对比版本号确认与之前获取的相同,并更新版本号,即可确认这之间没有发生并发的修改。如果更新失败即可认为老版本的数据已经被并发修改掉而不存在了,此时认为获取锁失败,需要回滚整个业务操作并可根据需要重试整个过程。

乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

 

CAS算法

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

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

 

乐观锁的缺点

1. ABA 问题

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

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

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

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

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

 

CAS与synchronized的使用情景

简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)

对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋机率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

 

比较

(没有切身体会,看心情在补充)乐观锁适用于多读的应用类型,这样可以提高吞吐量

 

使用实例:

1. MySQL的事务支持 

MySQL的事务支持不是绑定在MySQL服务器本身,而是与存储引擎相关

  1. MyISAM:不支持事务,用于只读程序提高性能   
  2. InnoDB:支持ACID事务、行级锁、并发   
  3. Berkeley DB:支持事务  


2. 隔离级别

隔离级别决定了一个session中的事务可能对另一个session的影响、并发session对数据库的操作、一个session中所见数据的一致性 
ANSI标准定义了4个隔离级别,MySQL的InnoDB都支持:

  1. READ UNCOMMITTED:最低级别的隔离,通常又称为dirty read,它允许一个事务读取还没commit的数据,这样可能会提高性能,但是dirty read可能不是我们想要的   
  2. READ COMMITTED:在一个事务中只允许已经commit的记录可见,如果session中select还在查询中,另一session此时insert一条记录,则新添加的数据不可见   
  3. REPEATABLE READ:在一个事务开始后,其他session对数据库的修改在本事务中不可见,直到本事务commit或rollback。在一个事务中重复select的结果一样,除非本事务中update数据库。   
  4. SERIALIZABLE:最高级别的隔离,只允许事务串行执行。为了达到此目的,数据库会锁住每行已经读取的记录,其他session不能修改数据直到前一事务结束,事务commit或取消时才释放锁。  

 

可以使用如下语句设置MySQL的session隔离级别:

SET TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}   

 MySQL默认的隔离级别是REPEATABLE READ,在设置隔离级别为READ UNCOMMITTED或SERIALIZABLE时要小心,READ UNCOMMITTED会导致数据完整性的严重问题,而SERIALIZABLE会导致性能问题并增加死锁的机率

3,隔离级别

乐观锁和悲观锁策略: 
悲观锁:在读取数据时锁住那几行,其他对这几行的更新需要等到悲观锁结束时才能继续 
乐观锁:读取数据时不锁,更新时检查是否数据已经被更新过,如果是则取消当前更新 
一般在悲观锁的等待时间过长而不能接受时我们才会选择乐观锁 

 

悲观锁例子

CREATE PROCEDURE tfer_funds     
       (from_account INT, to_account INT,tfer_amount NUMERIC(10,2),     
        OUT status INT, OUT message VARCHAR(30))     
BEGIN     
    DECLARE from_account_balance NUMERIC(10,2);     
    
    START TRANSACTION;     
    
    
    SELECT balance     
      INTO from_account_balance     
      FROM account_balance     
     WHERE account_id=from_account     
       FOR UPDATE;     
    
    IF from_account_balance>=tfer_amount THEN     
    
         UPDATE account_balance     
            SET balance=balance-tfer_amount     
          WHERE account_id=from_account;     
    
         UPDATE account_balance     
            SET balance=balance+tfer_amount     
          WHERE account_id=to_account;     
         COMMIT;     
    
         SET status=0;     
         SET message='OK';     
    ELSE     
         ROLLBACK;     
         SET status=-1;     
         SET message='Insufficient funds';     
    END IF;     
END;    

 

乐观锁例子

CREATE PROCEDURE tfer_funds     
    (from_account INT, to_account INT, tfer_amount NUMERIC(10,2),     
        OUT status INT, OUT message VARCHAR(30) )     
    
BEGIN     
    
    DECLARE from_account_balance    NUMERIC(8,2);     
    DECLARE from_account_balance2   NUMERIC(8,2);     
    DECLARE from_account_timestamp1 TIMESTAMP;     
    DECLARE from_account_timestamp2 TIMESTAMP;     
    
    SELECT account_timestamp,balance     
        INTO from_account_timestamp1,from_account_balance     
            FROM account_balance     
            WHERE account_id=from_account;     
    
    IF (from_account_balance>=tfer_amount) THEN     
    
        -- Here we perform some long running validation that     
        -- might take a few minutes */     
        CALL long_running_validation(from_account);     
    
        START TRANSACTION;     
    
        -- Make sure the account row has not been updated since     
        -- our initial check     
        SELECT account_timestamp, balance     
            INTO from_account_timestamp2,from_account_balance2     
            FROM account_balance     
            WHERE account_id=from_account     
            FOR UPDATE;     
    
        IF (from_account_timestamp1 <> from_account_timestamp2 OR     
            from_account_balance    <> from_account_balance2)  THEN     
            ROLLBACK;     
            SET status=-1;     
            SET message=CONCAT("Transaction cancelled due to concurrent update",     
                " of account"  ,from_account);     
        ELSE     
            UPDATE account_balance     
                SET balance=balance-tfer_amount     
                WHERE account_id=from_account;     
    
            UPDATE account_balance     
                SET balance=balance+tfer_amount     
                WHERE account_id=to_account;     
    
            COMMIT;     
    
            SET status=0;     
            SET message="OK";     
        END IF;     
    
    ELSE     
        ROLLBACK;     
        SET status=-1;     
        SET message="Insufficient funds";     
    END IF;     
END$$    

 

参考文章

面试必备之乐观锁与悲观锁

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章