[数据库锁机制] 深刻理解乐观锁、悲观锁以及CAS乐观锁的实现机制原理分析

前言:

  • 在并发访问状况下,可能会出现脏读、不可重复读和幻读等读现象,为了应对这些问题,主流数据库都提供了锁机制,并引入了事务隔离级别的概念。数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。
  • 乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。不管是悲观锁仍是乐观锁,都是人们定义出来的概念,能够认为是一种思想。其实不只仅是关系型数据库系统中有乐观锁和悲观锁的概念,像memcache、hibernate、tair等都有相似的概念。
  • 本文中也将深刻分析一下乐观锁的实现机制,介绍什么是CAS、CAS的应用以及CAS存在的问题等。

并发控制html

在计算机科学,特别是程序设计、操做系统、多处理机和数据库等领域,并发控制(Concurrency control)是确保及时纠正由并发操做致使的错误的一种机制。java

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。下面举例说明并发操做带来的数据不一致性问题:mysql

现有两处火车票售票点,同时读取某一趟列车车票数据库中车票余额为 X。两处售票点同时卖出一张车票,同时修改余额为 X -1写回数据库,这样就形成了实际卖出两张火车票而数据库中的记录却只少了一张。 产生这种状况的缘由是由于两个事务读入同一数据并同时修改,其中一个事务提交的结果破坏了另外一个事务提交的结果,致使其数据的修改被丢失,破坏了事务的隔离性。并发控制要解决的就是这类问题。算法

封锁、时间戳、乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。sql

1、数据库的锁

数据库

当并发事务同时访问一个资源时,有可能致使数据不一致,所以须要一种机制来将数据访问顺序化,以保证数据库数据的一致性。锁就是其中的一种机制。缓存

在计算机科学中,锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的知足。安全

锁的分类(oracle)数据结构

1、按操做划分,可分为DML锁DDL锁多线程

2、按锁的粒度划分,可分为表级锁行级锁页级锁(mysql)

3、按锁级别划分,可分为共享锁排他锁

4、按加锁方式划分,可分为自动锁显示锁

5、按使用方式划分,可分为乐观锁悲观锁

DML锁(data locks,数据锁),用于保护数据的完整性,其中包括行级锁(Row Locks (TX锁))、表级锁(table lock(TM锁))。

DDL锁(dictionary locks,数据字典锁),用于保护数据库对象的结构,如表、索引等的结构定义。其中包排他DDL锁(Exclusive DDL lock)、共享DDL锁(Share DDL lock)、可中断解析锁(Breakable parse locks)

1.1 锁机制

经常使用的锁机制有两种:

一、悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操做。悲观锁的实现,每每依靠底层提供的锁机制;悲观锁会致使其它全部须要锁的线程挂起,等待持有锁的线程释放锁。

二、乐观锁:假设不会发生并发冲突,每次不加锁而是假设没有冲突而去完成某项操做,只在提交操做时检查是否违反数据完整性。若是由于冲突失败就重试,直到成功为止。乐观锁大可能是基于数据版本记录机制实现。为数据增长一个版本标识,好比在基于数据库表的版本解决方案中,通常是经过为数据库表增长一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,以后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,若是提交的数据版本号大于数据库表当前版本号,则予以更新,不然认为是过时数据。 

乐观锁的缺点是不能解决部分脏读的问题,例如ABA问题(下面会讲到)。

在实际生产环境里边,若是并发量不大且不容许脏读,可使用悲观锁解决并发问题;但若是系统的并发很是大的话,悲观锁定会带来很是大的性能问题,因此咱们就要选择乐观锁定的方法。

2、悲观锁与乐观锁详解

2.1 悲观锁

在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它能够阻止一个事务以影响其余用户的方式来修改数据。若是一个事务执行的操做都某行数据应用了锁,那只有当这个事务把锁释放,其余事务才可以执行与该锁冲突的操做。
悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其余事务,以及来自外部系统的事务处理)修改持保守态度(悲观),所以,在整个数据处理过程当中,将数据处于锁定状态。 悲观锁的实现,每每依靠数据库提供的锁机制 (也只有数据库层提供的锁机制才能真正保证数据访问的排他性,不然,即便在本系统中实现了加锁机制,也没法保证外部系统不会修改数据)

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

在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。

若是加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际须要决定。

若是成功加锁,那么就能够对记录作修改,事务完成后就会解锁了。

其间若是有其余对该记录作修改或加排他锁的操做,都会等待咱们解锁或直接抛出异常。

MySQL InnoDB中使用悲观锁:

要使用悲观锁,咱们必须关闭mysql数据库的自动提交属性,由于MySQL默认使用autocommit模式,也就是说,当你执行一个更新操做后,MySQL会马上将结果进行提交。set autocommit=0;

//0.开始事务
begin;/begin work;/start transaction; (三者选一就能够)
//1.查询出商品信息
select status from t_goods where id=1 for update;
//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_goods set status=2;
//4.提交事务
commit;/commit work;

 

上面的查询语句中,咱们使用了select…for update的方式,这样就经过开启排他锁的方式实现了悲观锁。此时在t_goods表中,id为1的 那条数据就被咱们锁定了,其它的事务必须等本次事务提交以后才能执行。这样咱们能够保证当前的数据不会被其它事务修改。

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

优势与不足

悲观并发控制其实是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。可是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增长产生死锁的机会;另外,在只读型事务处理中因为不会产生冲突,也不必使用锁,这样作只能增长系统负载;还有会下降了并行性,一个事务若是锁定了某行数据,其余事务就必须等待该事务处理完才能够处理那行数

2.2 乐观锁

在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务可以在不产生锁的状况下处理各自影响的那部分数据。在提交数据更新以前,每一个事务会先检查在该事务读取数据后,有没有其余事务又修改了该数据。若是其余事务有更新的话,正在提交的事务会进行回滚。乐观事务控制最先是由孔祥重(H.T.Kung)教授提出。

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据通常状况下不会形成冲突,因此在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,若是发现冲突了,则让返回用户错误的信息,让用户决定如何去作。

相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。通常的实现乐观锁的方式就是记录数据版本。

数据版本,为数据增长的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当咱们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,若是数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,不然认为是过时数据。

实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间戳。

使用版本号实现乐观锁

使用版本号时,能够在数据初始化时指定一个版本号,每次对数据的更新操做都对版本号执行+1操做。并判断当前版本号是否是该数据的最新的版本号。

1.查询出商品信息
select (status,status,version) from t_goods where id=#{id}
2.根据商品信息生成订单
3.修改商品status为2
update t_goods 
set status=2,version=version+1
where id=#{id} and version=#{version};

优势与不足

乐观并发控制相信事务之间的数据竞争(data race)的几率是比较小的,所以尽量直接作下去,直到提交的时候才去锁定,因此不会产生任何锁和死锁。但若是直接简单这么作,仍是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,通过修改之后写回数据库,这时就遇到了问题。

3、CAS详解

在说CAS以前,咱们不得不提一下Java的线程安全问题。

线程安全:

众所周知,Java是多线程的。可是,Java对多线程的支持实际上是一把双刃剑。一旦涉及到多个线程操做共享资源的状况时,处理很差就可能产生线程安全问题。线程安全性多是很是复杂的,在没有充足的同步的状况下,多个线程中的操做执行顺序是不可预测的。

Java里面进行多线程通讯的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性。加上复合操做的原子性,咱们能够认为Java的线程安全性问题主要关注点有3个:可见性、有序性和原子性。

Java内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题。这里再也不详细介绍JMM及锁的其余相关知识。可是咱们要讨论一个问题,那就是锁究竟是不是有利无弊的?

3.1 锁存在的问题

Java在JDK1.5以前都是靠synchronized关键字保证同步的,这种经过使用一致的锁定协议来协调对共享状态的访问,能够确保不管哪一个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,因此能够说synchronized是悲观锁。

悲观锁机制存在如下问题:

1) 在多线程竞争下,加锁、释放锁会致使比较多的上下文切换和调度延时,引发性能问题。

2) 一个线程持有锁会致使其它全部须要此锁的线程挂起。

3) 若是一个优先级高的线程等待一个优先级低的线程释放锁会致使优先级倒置,引发性能风险。

而另外一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。

与锁相比,volatile变量是一个更轻量级的同步机制,由于在使用这些变量时不会发生上下文切换和线程调度等操做,可是volatile不能解决原子性问题,所以当一个变量依赖旧值时就不能使用volatile变量。所以对于同步最终仍是要回到锁机制上来。

乐观锁

乐观锁( Optimistic Locking)实际上是一种思想。相对悲观锁而言,乐观锁假设认为数据通常状况下不会形成冲突,因此在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,若是发现冲突了,则让返回用户错误的信息,让用户决定如何去作。

上面提到的乐观锁的概念中其实已经阐述了他的具体实现细节:

主要就是两个步骤:冲突检测数据更新

其实现方式有一种比较典型的就是Compare and Swap(CAS)。

3.2 CAS

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知此次竞争中失败,并能够再次尝试。

CAS 操做包含三个操做数 —— 内存位置(V)、预期原值(A)和新值(B)。若是内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。不然,处理器不作任何操做。不管哪一种状况,它都会在 CAS 指令以前返回该位置的值。(在 CAS 的一些特殊状况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;若是包含该值,则将 B 放到这个位置;不然,不要更改该位置,只告诉我这个位置如今的值便可。”这其实和乐观锁的冲突检查+数据更新的原理是同样的。

这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。

3.3 Java对CAS的支持

JDK 5以前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。j在JDK1.5 中新增java.util.concurrent(J.U.C)就是创建在CAS之上的。相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。因此J.U.C在性能上有了很大的提高。

现代的CPU提供了特殊的指令,容许算法执行读-修改-写操做,而无需惧怕其余线程同时修改变量,由于若是其余线程修改变量,那么CAS会检测它(并失败),算法能够对该操做从新计算。而 compareAndSet() 就用这些代替了锁定。

咱们以java.util.concurrent中的AtomicInteger为例,看一下在没有锁的状况下是如何保证线程安全的。主要理解getAndIncrement方法,该方法的做用至关于 ++i 操做。

public class AtomicInteger extends Number implements java.io.Serializable {
    
    private volatile int value;
    
    public final int get() {
        return value;
    }
    
    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }
    
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

 

字段value须要借助volatile原语,保证线程间的数据是可见的(共享的)。这样在获取变量的值的时候才能直接读取。而后来看看++i是怎么作到的。getAndIncrement采用了CAS操做,每次从内存中读取数据而后将此数据和+1后的结果进行CAS操做,若是成功就返回结果,不然重试直到成功为止。而compareAndSet利用JNI来完成CPU指令的操做。

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
 }

 

总体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操做都是利用相似的特性完成的。

而整个J.U.C都是创建在CAS之上的,所以对于synchronized阻塞算法,J.U.C在性能上有了很大的提高。

3.4 CAS会致使“ABA问题”:

ABA问题:

aba其实是乐观锁没法解决脏数据读取的一种体现。CAS算法实现一个重要前提须要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会致使数据的变化。

好比说一个线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,而且two进行了一些操做变成了B,而后two又将V位置的数据变成A,这时候线程one进行CAS操做发现内存中仍然是A,而后one操做成功。尽管线程one的CAS操做成功,可是不表明这个过程就是没有问题的。

部分乐观锁的实现是经过版本号(version)的方式来解决ABA问题,乐观锁每次在执行数据的修改操做时,都会带上一个版本号,一旦版本号和数据的版本号一致就能够执行修改操做并对版本号执行+1操做,不然就执行失败。由于每次操做的版本号都会随之增长,因此不会出现ABA问题,由于版本号只会增长不会减小。

 若是链表的头在变化了两次后恢复了原值,可是不表明链表就没有变化。所以AtomicStampedReference/AtomicMarkableReference就颇有用了。

AtomicMarkableReference 类描述的一个<Object,Boolean>的对,能够原子的修改Object或者Boolean的值,这种数据结构在一些缓存或者状态描述中比较有用。这种结构在单个或者同时修改Object/Boolean的时候可以有效的提升吞吐量。 


AtomicStampedReference 类维护带有整数“标志”的对象引用,能够用原子方式对其进行更新。对比AtomicMarkableReference 类的<Object,Boolean>,AtomicStampedReference 维护的是一种相似<Object,int>的数据结构,其实就是对对象(引用)的一个并发计数(标记版本戳stamp)。可是与AtomicInteger 不一样的是,此数据结构能够携带一个对象引用(Object),而且可以对此对象和计数同时进行原子操做。

REFERENCE:

整理自如下博客:

1.  http://www.hollischuang.com/archives/934

2.  http://www.hollischuang.com/archives/1537

3.  http://www.cnblogs.com/Mainz/p/3546347.html

4.  http://www.digpage.com/lock.html

5.  https://chenzhou123520.iteye.com/blog/1863407

6.  https://chenzhou123520.iteye.com/blog/1860954

相关文章
相关标签/搜索