面试必备-行锁、表锁 - 乐观锁、悲观锁的区别和联系(史上最全)_yxg520s的博客-CSDN博客_行锁和表锁是悲观锁吗
乐观锁、悲观锁、读写锁、互斥锁之间的关系_Lerix的博客-CSDN博客_悲观锁和互斥锁区别
面试官灵魂4连问:乐观锁与悲观锁的概念、实现方式、场景、优缺点? - 知乎 (zhihu.com)
什么是分布式锁?实现分布式锁的三种方式 - 刘清政 - 博客园 (cnblogs.com)
[分布式锁看这篇就够了 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/42056183#:~:text=什么是分布式锁?,当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。 与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。)
不同的应用场景对锁的要求各不相同,我们先来看下锁都有哪些类别,这些锁之间有什么区别。
也称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是都只能读不能修改,因为当多个事务对同一行数据执行修改操作时,会产生死锁现象。
也称X锁,获取排他锁的事务可以对数据行读取和修改;如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的共享锁和排他锁,
对于共享锁大家可能很好理解,就是多个事务只能读数据不能改数据,对于排他锁大家的理解可能就有些差别,这里有一个重点就是~~:排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,~~其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在该行上上加其他的锁—(导致不能进行其他的操作)。
共享锁和排他锁是在数据库中实实在在存在的锁!!
Mysql InnoDB引擎默认的修改数据语句:
update,delete,insert
都会自动给涉及到的数据加上排他锁select
语句默认不会加任何锁类型select …for update
语句,select … lock in share mode
语句。select …from…
查询数据,因为普通查询没有任何锁机制。申请排他锁的前提是,没有线程对该结果集的任何行数据使用排它锁或者共享锁,否则申请会受到阻塞。
注意:乐观锁和悲观锁是抽象性的概念,不是实际存在的一种锁,而是通过实际存在的锁或其他机制来实现乐观锁、悲观锁的思想!!
对于乐观锁和悲观锁的区别及应用,要牢记一句话:读取频繁使用乐观锁,写入频繁使用悲观锁
乐观锁和悲观锁是同一维度的概念,都是从数据访问的角度来说。所以经常出现在数据库相关问题中,即当数据同时被多个对象访问了,应该持什么态度来对数据进行保护。这两种锁,是两种思想。它们的使用是非常广泛的,不局限于某种编程语言或数据库。
乐观锁认为,数据被访问,对方不大可能要修改这个数据。所以在此思想的引导下,
一个对象读数据时,不会上锁,不会排斥其他的访问(包括读和写),
但是在更新(写)的时候会判断其他线程在这之前有没有对数据进行修改。若有人动过,则取消 重读再重试。
一般会使用版本号机制或CAS操作实现。
悲观锁认为,数据被访问,对方很可能要修改这个数据。所以在此思想的引导下,
悲观锁的实现方式是加锁:
排他性,只有当前进行加锁的用户,才可以对被锁的数据进行操作,账务中悲观锁的使用相对较多,其他系统使用较少,因为悲观锁影响性能。
数据库版本号方式:一般是在数据表中加上一个数据版本号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,两个线程按照顺序进行以下操作:
在第(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包下的原子类使用,灵活性受到限制。
乐观锁和悲观锁并没有优劣之分,它们有各自适合的场景;下面从两个方面进行说明。
功能限制 与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。
竞争激烈程度 如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:
在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要**保证“最终一致性”**,只要这个最终时间是在用户可以接受的范围内即可。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。分布式锁的三种实现方式有:
具体实现方式有多种,如:
优点:简单,易于理解
缺点:会有各种各样的问题(操作数据库需要一定的开销,使用数据库的行级锁并不一定靠谱,性能不靠谱)
redis加锁的命令setnx,设置锁的过期时间是expire,解锁的命令是del,但是2.6.12之前的版本中,加锁和设置锁过期命令是两个操作,不具备原子性。如果setnx设置完key-value之后,还没有来得及使用expire来设置过期时间,当前线程挂掉了或者线程阻塞,会导致当前线程设置的key一直有效,后续的线程无法正常使用setnx获取锁,导致死锁。针对这个问题,redis2.6.12以上的版本增加了可选的参数,可以在加锁的同时设置key的过期时间,保证了加锁和过期操作原子性的。
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。