乐观锁、悲观锁、分布式锁的总结

乐观锁、悲观锁、分布式锁

面试必备-行锁、表锁 - 乐观锁、悲观锁的区别和联系(史上最全)_yxg520s的博客-CSDN博客_行锁和表锁是悲观锁吗

乐观锁、悲观锁、读写锁、互斥锁之间的关系_Lerix的博客-CSDN博客_悲观锁和互斥锁区别

面试官灵魂4连问:乐观锁与悲观锁的概念、实现方式、场景、优缺点? - 知乎 (zhihu.com)

什么是分布式锁?实现分布式锁的三种方式 - 刘清政 - 博客园 (cnblogs.com)

[分布式锁看这篇就够了 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/42056183#:~:text=什么是分布式锁?,当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。 与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。)

相关名词

  • |–表级锁(锁定整个表)
  • |–页级锁(锁定一页)
  • |–行级锁(锁定一行)
  • |–共享锁(S锁,MyISAM 叫做读锁)
  • |–排他锁(X锁,MyISAM 叫做写锁)
  • |–意向共享锁(IS锁,事务打算给数据行加共享锁,但是在这之前必须获得意向共享锁,innodb引擎会自动给符合条件的事务加IS锁)
  • |–意向排他锁(IX锁,事务打算给数据行加排他锁,但是在这之前必须获得意向排他锁,innodb引擎会自动给符合条件的事务加IX锁)
  • |–悲观锁(抽象性,不真实存在这个锁,是一个概念,需要自己编程去实现)
  • |–乐观锁(抽象性,不真实存在这个锁,是一个概念,需要自己编程去实现)
  • |-分布式锁(针对分布式环境下的锁,抽象性,需要通过各种手段实现)

锁类别

不同的应用场景对锁的要求各不相同,我们先来看下锁都有哪些类别,这些锁之间有什么区别。

  • 悲观锁(synchronize)
    • Java 中的重量级锁 synchronize----对代码上锁
    • 数据库行锁-----对数据上锁
  • 乐观锁
    • Java 中的轻量级锁 volatile 和 CAS-----对代码上锁
    • 数据库版本号------对数据上锁
  • 分布式锁
    • 基于数据库实现分布式锁;
    • 基于缓存(Redis等)实现分布式锁;
    • 基于Zookeeper实现分布式锁;

共享锁/排他锁

1)共享锁(Shared Lock)

也称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是都只能读不能修改,因为当多个事务对同一行数据执行修改操作时,会产生死锁现象。

2)排他锁(Exclusive Lock)

也称X锁,获取排他锁的事务可以对数据行读取和修改;如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的共享锁和排他锁,

对于共享锁大家可能很好理解,就是多个事务只能读数据不能改数据,对于排他锁大家的理解可能就有些差别,这里有一个重点就是~~:排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,~~其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在该行上上加其他的锁—(导致不能进行其他的操作)。

共享锁和排他锁是在数据库中实实在在存在的锁!!

Mysql InnoDB引擎默认的修改数据语句:

  • update,delete,insert都会自动给涉及到的数据加上排他锁
  • select语句默认不会加任何锁类型
  • 如果加排他锁可以使用select …for update语句,
  • 加共享锁可以使用select … lock in share mode语句。
  • 所以加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。

申请排他锁的前提是,没有线程对该结果集的任何行数据使用排它锁或者共享锁,否则申请会受到阻塞。

乐观锁/悲观锁

注意:乐观锁和悲观锁是抽象性的概念,不是实际存在的一种锁,而是通过实际存在的锁或其他机制来实现乐观锁、悲观锁的思想!!

对于乐观锁和悲观锁的区别及应用,要牢记一句话:读取频繁使用乐观锁,写入频繁使用悲观锁

乐观锁和悲观锁是同一维度的概念,都是从数据访问的角度来说。所以经常出现在数据库相关问题中,即当数据同时被多个对象访问了,应该持什么态度来对数据进行保护。这两种锁,是两种思想。它们的使用是非常广泛的,不局限于某种编程语言或数据库。

1)乐观锁

乐观锁认为,数据被访问,对方不大可能要修改这个数据。所以在此思想的引导下,

  • 一个对象读数据时,不会上锁不会排斥其他的访问(包括读和写)

  • 但是在更新(写)的时候会判断其他线程在这之前有没有对数据进行修改。若有人动过,则取消 重读再重试。

  • 一般会使用版本号机制或CAS操作实现。

2)悲观锁

悲观锁认为,数据被访问,对方很可能要修改这个数据。所以在此思想的引导下,

  • 数据被访问时,不管是读还是写,步步加锁,严格排斥其他对象的访问
  • 可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁

3)乐观锁和悲观锁的实现方式

悲观锁:--------步步加锁

悲观锁的实现方式是加锁:

  • 加锁既可以是对代码块加锁(如Java的synchronized关键字)
  • 也可以是对数据加锁(如MySQL中的排它锁),如行锁、读锁和写锁等。

排他性,只有当前进行加锁的用户,才可以对被锁的数据进行操作,账务中悲观锁的使用相对较多,其他系统使用较少,因为悲观锁影响性能

乐观锁:----------可以理解为并不加锁,只是更新时需要进行判断
  • 数据库版本号方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数

    当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

核心SQL代码:

update table set x=x+1, version=version+1 where id=#{id} and version=#{version};  
  • CAS操作方式:即compare and swap,涉及到三个操作数:数据所在的内存值,预期值,新值

    当需要更新时,判断当前内存值与进行比较的预期值是否相等,若相等,则用新值更新;若失败则重试,一般情况下是一个自旋操作,即不断的重试。

CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?

答案是:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。例如在JAVA中,AtomicInteger是java.util.concurrent.atomic包提供的原子类,利用CPU提供的CAS操作来保证原子性;

CAS(Compare and Swap):

CAS是由CPU硬件实现,所以执行相当快。

CAS有三个操作参数:内存值,期望值,要修改的新值

当期望值和内存当中的值进行比较不相等的时候,表示内存中的值已经被别线程改动过,这时候失败返回;当相等的时候,将内存中的值改为新的值,并返回成功。

CAS这种实现方式有什么缺点吗?

1. ABA问题 假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:

  • (1)线程1读取内存中数据为A;
  • (2)线程2将该数据修改为B;
  • (3)线程2将该数据修改为A;
  • (4)线程1对数据进行CAS操作

在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了。这就是ABA问题。

在AtomicInteger的例子中,ABA似乎没有什么危害。

但是在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化。

对于ABA问题,比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1;

在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。

Java中的AtomicStampedReference类便是使用版本号来解决ABA问题的。

2.高竞争下的开销问题 在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。

针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。

当然,更重要的是避免在高竞争环境下使用乐观锁。

3.功能限制 CAS的功能是比较受限的,例如CAS只能保证单个变量(或者说单个内存值)操作的原子性,这意味着:

(1)原子性不一定能保证线程安全,例如在Java中需要与volatile配合来保证线程安全;

(2)当涉及到多个变量(内存值)时,CAS也无能为力。

除此之外,CAS的实现需要硬件层面处理器的支持,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。

4)乐观锁和悲观锁优缺点和适用场景

乐观锁和悲观锁并没有优劣之分,它们有各自适合的场景;下面从两个方面进行说明。

  • 功能限制 与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。

    • 比如CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理
    • 再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。
  • 竞争激烈程度 如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:

    • 当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源
    • 当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源

分布式锁

  • 当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
  • 与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。(我觉得分布式情况下之所以问题变得复杂,主要就是需要考虑到网络的延时和不可靠。。。一个大坑)
  • 分布式锁还是可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存如 Redis、Memcache。至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。

1)分布式锁应该具备哪些条件

在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:

1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

2)分布式锁的三种实现方式

目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要**保证“最终一致性”**,只要这个最终时间是在用户可以接受的范围内即可。

在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。分布式锁的三种实现方式有:

  1. 基于数据库实现分布式锁;
  2. 基于缓存(Redis等)实现分布式锁;
  3. 基于Zookeeper实现分布式锁;

3)基于数据库实现分布式锁

具体实现方式有多种,如:

  • 基于表主键唯一做分布式锁
    利用主键唯一的特性,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,当方法执行完毕之后,想要释放锁的话,删除这条数据库记录即可。
  • 基于表字段版本号做分布式锁
    这个策略源于 mysql 的 mvcc 机制,使用这个策略其实本身没有什么问题,唯一的问题就是对数据表侵入较大,我们要为每个表设计一个版本号字段,然后写一条判断 sql 每次进行判断,增加了数据库操作的次数,在高并发的要求下,对数据库连接的开销也是无法忍受的。
  • 基于数据库排他锁做分布式锁
    在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁 (注意: InnoDB 引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给要执行的方法字段名添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。

优点:简单,易于理解

缺点:会有各种各样的问题(操作数据库需要一定的开销,使用数据库的行级锁并不一定靠谱,性能不靠谱)

4)基于Redis实现分布式锁

redis加锁的命令setnx,设置锁的过期时间是expire,解锁的命令是del,但是2.6.12之前的版本中,加锁和设置锁过期命令是两个操作,不具备原子性。如果setnx设置完key-value之后,还没有来得及使用expire来设置过期时间,当前线程挂掉了或者线程阻塞,会导致当前线程设置的key一直有效,后续的线程无法正常使用setnx获取锁,导致死锁。针对这个问题,redis2.6.12以上的版本增加了可选的参数,可以在加锁的同时设置key的过期时间,保证了加锁和过期操作原子性的。

5)基于ZooKeeper实现分布式锁:

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁

优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。

缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

你可能感兴趣的:(大数据,分布式,分布式,java,面试)