一文了解数据库和 Java 的近30种锁

一文了解 数据库 和 Java 近30种锁

本文入门性地介绍数据库(主要是 MySQL + InnoDB 这块) + Java 中近30个不同称呼的锁,事实上“锁”的思想通用性很强,无论是更早的数据库 / MySQL 领域,还是 稍晚 Java / Redis 等领域,会发现锁的思想本源其实是类同的,只是适应了各自不同的场景而已,所以要区分好 MySQL 和 Java 的锁,或者深入理解各类锁,还得具体切入到其场景所要解决的问题和折中优化办法里,这话题太深太广,本文只对各种锁做一个“浅尝辄止”的介绍。
参考文献:《高性能 MySQL》(第3版)、《MySQL 技术内幕 InnoDB存储引擎》(第2版)、部分网帖

数据库相关锁

行锁、页锁、表锁、锁升级/锁粗化、间隙锁、Next-Key锁、lock锁、latch锁

乐观锁、悲观锁

独占锁(写锁)/互斥锁/排他锁、共享锁(读锁)、意向锁、公平锁、非公平锁(抢占式)、分布式锁、一致性非锁定读、一致性锁定

死锁

  1. 行锁
    行锁顾名思义就是锁住某几行记录,有三大类:
    (1)Recrd Lock-单行记录上锁
    (2)Gap Lock-间隙锁,锁定一个范围,但不包含记录本身
    (3)Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身
    注意,InnoDB 的行锁,不是直接锁住记录,而是锁住记录的索引。

  2. 页锁
    粒度介于行锁和表锁之间,锁住一个 Page。

  3. 表锁
    锁住整个表,不同于 InnoDB,MyISAM 存储引擎仅支持表锁不支持行锁。
    当行锁泛滥时,可能会引发锁升级为更粗粒度的表级锁。
    《MySQL 技术内幕 InnoDB存储引擎》(第2版)有段话:“InnoDB 存储引擎不存在锁升级的问题,因为其不是根据每个记录来产生行锁的,相反,其根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式。因此不管一个事务锁住页中一个记录还是多个记录,其开销通常都是一致的。” 这里边的提到“InnoDB 存储引擎不存在锁升级”还需要深入理解下。

  4. 锁升级、锁粗化
    细粒度的锁升级为更粗粒度的锁:行级锁 --> 页锁 --> 表锁。

  5. 间隙锁、Next-Key 锁
    定义如上所述。MySQL 默认的事务隔离级别是:Read Repeatable-可重复读,这会带来幻读或者幻行的问题,可以通过间隙锁来解决,InnoDB 是通过 Next-Key 锁来解决幻行问题的。

  6. latch 与 lock
    以下段落摘录自:[【锁】Latch、lock、 pin的区别][http://blog.itpub.net/26736162/viewspace-2142070/]

(1) Latch是对内存数据结构提供互斥访问的一种机制,而Lock是以不同的模式来套取共享资源对象,各个模式间存在着兼容或排斥;
(2) Latch只作用于内存中,他只能被当前实例访问,而Lock作用于数据库对象;
(3) Latch是瞬间的占用,释放,Lock的释放需要等到事务正确的结束,他占用的时间长短由事务大小决定
(4) Latch是非入队的,而Lock是入队的
(5) Latch不存在死锁,而Lock中存在(死锁在Oracle中是非常少见的)

  1. 乐观锁、悲观锁
    悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。一般多写的场景下用悲观锁就比较合适。

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

    关于乐观锁悲观锁的设计:
    这篇博客写的不错:[乐观锁悲观锁的实现][https://www.jianshu.com/p/4ff4d3f21d1c]
    悲观锁:“一锁二查三更新”,通过常用的select … for update操作来实现悲观锁。具体如下:在对任意记录进行修改前,先尝试为该记录加上排他锁;如果加锁失败,说明该记录正在被修改,那么当前事务可能要进行等待或抛出异常(具体响应方式开发者根据实际决定);如果成功加锁,那么可以对记录做修改,事务完成后就会解锁了,其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或者直接抛出异常。
    乐观锁:CAS机制 --> ABA 问题 --> 版本号机制。
    CAS- Compare And Swap 比较替换算法:典型的无锁算法,先记录变量的值,等到真的要修改变量时,拿此前记录的值跟当前值比较,比较通过则修改成功,否则进入自旋状态等待下一次判断。CAS 天生 A-B-A 问题(其他事务中途修改了变量值从 A 为B ,但在事务 Compare 之间又改回为 A),ABA 问题可以通过增加版本号的机制解决。

  2. 独占锁(写锁)/互斥锁/排他锁、共享锁(读锁)
    写锁是独占 即 排他的。读锁可以做到共享。

  3. 公平锁、非公平锁(抢占式)
    非公平锁是抢占式的,事务上来不排队,而是直接看锁是否空闲,空闲则获取成功,这对于正在排队/自旋的事务就是不公平的。反之,就是公平锁。

  4. 一致性非锁定读 、一致性锁定读
    对于 Read Committed 和 Repeatable Read 两种事务隔离级别,都是一致性非锁定读,但 Read Committ 总是读取被锁定行的最新一份快照数据,而 Repeatable Read 则是读取当前事务开始时的行数据版本(通过 MVCC 机制实现)。

  5. 意向锁
    以下片段摘自[InnoDB 的意向锁有什么作用?][https://www.zhihu.com/question/51513268]:

考虑这个例子:事务A锁住了表中的一行,让这一行只能读,不能写。之后,事务B申请整个表的写锁。如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。数据库要怎么判断这个冲突呢?step1:判断表是否已被其他事务用表锁锁表step2:判断表中的每一行是否已被行锁锁住。注意step2,这样的判断方法效率实在不高,因为需要遍历整个表。于是就有了意向锁。在意向锁存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。在意向锁存在的情况下,上面的判断可以改成step1:不变step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。

  1. 分布式锁
    这篇博客说的很好:[什么是分布式锁?实现分布式锁的三种方式][https://www.cnblogs.com/liuqingzheng/p/11080501.html]
    在分布式架构中,存在变量A 存储在多台应用上被同时修改的情况,导致没有一致性,为此可以分布式锁解决该问题。实现分布式锁可以通过三个方法:基于数据库的互斥锁实现、基于 Redis 实现、基于 Zookeeper 实现。

  2. 死锁
    先温习下死锁形成的四个必要条件:互斥、占有且等待、循环等待、非抢占。所以要破坏死锁只需要破坏其中一个条件就好了。
    在数据库里边,死锁的理解就是:事务之间互相等待对方(AB-BA问题,事务A等待事务B释放锁,同时事务B又等待A释放锁)。
    排查死锁:用锁的信息链表、事务等待链表画成图,若图存在回路【深度优先遍历算法判断回路】则说明死锁。
    解决:按DFS判出回路后,把 undo 量最小的事务给回滚掉。

Java相关锁

重量级锁、锁消除、锁粗化、分段锁、偏向锁、轻量级锁、自旋锁、适用性自旋锁、可重入锁

  1. 先来看 Synchronized 实现机制:
    这两篇博客讲的不错:[深入理解Java并发之synchronized实现原理[https://blog.csdn.net/javazejian/article/details/72828483]
    [深入理解Synchronized实现原理][https://www.jianshu.com/p/46a874d52b71]
    首先, Java 线程是映射到操作系统的原生线程上的,Synchronized 是基于对象头+ monitor 锁实现的,而 monitor 锁又是基于操作系统本身的互斥锁 Mutex Lock 实现的,每次获取锁和释放锁操作都会带来用户态和内核态的切换(开销:线程上下文保存;切换到内核线程时的安全检查;内核线程执行完返回过程有很多额外工作比如检查是否需要调度等),增加性能开销,所以 Synchronized 成为重量级锁。

  2. java1.6之后synchronized的优化
    jdk1.6对锁的实现引入了大量的优化技术来减少锁操作的开销,如:

(1)自旋锁、

(2)适应性自旋锁:动态调整自旋次数、

(3)锁消除:结合方法逃逸【变量只在方法内有效不存在逃逸】分析决定是否要消除掉锁、

(4)锁粗化:针对细粒度的锁,比如锁分段等,有时候都加小锁还不如合并为一个更大范围的锁、

(5)偏向锁:大多数情况下锁不仅不存在多线程竞争而且总是由同一线程获得,为了减少每次获取锁的代价(CAS同步操作等)而引入偏向锁,即如果一个线程获得了锁,那么锁就进入偏向模式,此时 对象头 的 Mark Word 的结构也变为偏向锁的结构,当同一个线程再次请求锁时,无需做任何同步操作,即可获得锁。但对于锁竞争比较激烈的情况,会进行锁升级,但不是直接升级为重量级锁,而是先升级为轻量级锁。

(6)轻量级锁:
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
无实际的锁竞争(比如通过乐观锁CAS-Compare And Swap 实现),只允许短时间的锁竞争(比如通过自旋实现,宁可让线程自旋也不做内核态的切换),如果自旋过多或等待时间太长,自旋带来的损耗反倒比重量级锁更严重,所以会升级为重量级锁。

  1. ReentrantLock-可重入锁(递归锁):
    线程获取了外层的锁之后,可以自动获取到内层的所有锁。
    可重入锁指的是在一个线程中可以多次获取同一把锁,比如:
    一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁,比如递归调用一个加锁的方法;
    主要是用来避免死锁的产生。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值