数据库基本知识点梳理系列 - 锁
数据库的锁是用于保证数据库事务在并发的情况下依旧能够保证数据的一致性的. 所以深入理解锁的原理, 能够帮助我们更好地理解事务隔离级别的原理, 以及在实际的业务场景中, 有把握的使用隔离级别保障系统的效率和稳定.
锁的分级
表级锁 | 快, 开销小 | 不会出现死锁 | 锁定粒度很大, 因此发生加锁的冲突概率最高 | 并发度最低 |
行级锁 | 开销大, 加锁慢 | 会出现死锁 | 锁定粒度最小, 发生加锁冲突概率最低 | 并发度最高 |
页面锁 | 开销和加速速度介于上面两者间 | 会出现死锁 | 粒度介于上面两者之间 | 并发度介于两者之间 |
行级锁
共享锁 | (S) | 读锁 。可以并发读取数据,但不能修改数据。也就是说当数据资源上存在共享锁的时候,所有的事务都不能对这个资源进行修改,直到数据被所有资源读取完成,共享锁释放。 |
排它锁 | (X) | 独占锁、写锁 。就是如果你对数据资源进行增删改(DML)操作时,不允许其它任何事务操作这块资源,直到排它锁被释放,防止同时对同一资源进行多重操作。当资源上已经有共享锁或者排他锁, 则无法对这个资源添加额外的排他锁. 即排他锁与共享锁不兼容. 排他锁与自己也不兼容. |
更新锁 | (U) | 当事务发现资源上既没有更新锁也没有排他锁时, 可以对资源添加更新锁, 也就是说, 更新锁与自己不兼容, 与排他锁也不兼容, 但是与共享锁兼容. 如果资源上已经有了共享锁, 那么在这种情况下, 更新锁会对该资源申请另外一个共享锁. 当事务准备修改资源时, 如果此时资源上没有其他的共享锁, 可以申请排他锁对数据进行修改. 所以对于更新锁我们可以这样理解. 一个资源只能有一个更新锁. 当更新锁发现资源上有其他共享锁时, 更新锁相当于共享锁. 如果事务准备修改数据, 但是资源上有其他共享锁, 则无法修改, 等其他共享锁释放后, 此时更新锁会变成排他锁, 用于修改资源. |
表级锁
下面介绍表级锁, 我们先看看都有哪些可以在表级别加的锁, 再对新出现的锁进行介绍:
共享锁 | (S) | 同行级锁 |
排他锁 | (X) | 同行级锁 |
意向锁 | (IS)(IX)(SIX) | 在意向锁出现之前, 一般是通过共享锁和排他锁对表和行上锁. 所以当我们为一个表加锁时, 一方面需要检查申请的锁与该表原有的表锁是否兼容, 另一方面, 还要检查该锁是否与表内的每一行的行锁兼容. 比如事务A要在一个表上加S锁, 如果表中某一行已经被事务B加了X锁, 那么事务A对表加S锁的申请就会被阻塞. 如果表中的数据量过多, 逐行检查行锁的开销将会很大, 系统的性能也会受到影响.为了解决这个问题, 数据库开发者想出, 是否可以在表级别加入新的锁类型,来表示其表内行的加锁情况? 这就诞生了意向锁.比如事务A准备对表中某一行加锁时, 首先要对数据所在的表加一个意向锁. 在此之后, 事务B准备对表的某一行加锁, 但是发现该表已经有了意向锁, 此时事务B需要根据现有的意向锁, 决定是否去逐行检查行锁, 进而节省系统的性能. 所以意向锁一般是基本锁(写锁, 读锁)的数据的上级数据的锁: 比如准备为数据A加X锁之前, 需要对数据A的上级表A加IX锁[IX:意向排他锁 ]). IS锁同理. 基本锁: S, X, 与意向锁: IS, IX 自由组合诞生进阶锁: S+IS,S+IX,X+IS,X+IX, 但稍加分析不难看出,实际上只有 S+IX 有新的意义,其它三种组合都没有使锁的强度得到提高, SIX的名字叫做 共享意向排它锁 , 举个例子: 事务A准备对表A加 SIX 锁, 这表示它准备读整个表(所以要对表加S锁), 同时也会更新个别行数据(所以要加IX锁). |
锁之间的兼容性总结
意向锁提升了数据库的并发性. 了解一下不同的锁之间的兼容性, 有助于更好地理解为什么你的项目里对并发事务的阻塞问题的原因:
IS | Y | Y | Y | Y | Y | N |
S | Y | Y | Y | N | N | N |
U | Y | Y | N | N | N | N |
IX | Y | N | N | Y | N | N |
SIX | Y | N | N | N | N | N |
X | N | N | N | N | N | N |
做个总结就是: 锁的模式和兼容性是SQL Server预先定义好的,没有任何参数或配置能够去修改它们。但是可以通过隔离级别来控制申请锁和释放锁的时机,如果应用申请的锁粒度都比较小,产生阻塞的几率就会比较小。如果一个连接会经常申请页面级、表级,甚至是数据库一级的锁资源,程序产生阻塞的可能性就会很大。
死锁
死锁不是一种锁, 是并发事务由于争夺锁时发生的一种现象. 死锁是系统性能的大杀器. 为了保证系统的性能和并发性, 我们不得不面临死锁.
首先举一个例子说明死锁如何产生:
- 第一个事务(称为A):先更新lives表 --->>停顿5秒---->>更新earth表
- 第二个事务(称为B):先更新earth表--->>停顿5秒---->>更新lives表
- 先执行事务A----5秒之内---执行事务B,此时出现死锁现象。
过程是这样子的:
- A更新lives表,请求lives的排他锁,成功。
- B更新earth表,请求earth的排他锁,成功。
- 5秒过后
- A更新earth,请求earth的排它锁,由于B占用着earth的排它锁,等待。
- B更新lives,请求lives的排它锁,由于A占用着lives的排它锁,等待。
避免死锁的一般经验总结
- 按照同一顺序访问数据库资源,上述例子就不会发生死锁
- 保持是事务的简短,尽量不要让一个事务处理过于复杂的读写操作。事务过于复杂,占用资源会增多,处理时间增长,容易与其它事务冲突,提升死锁概率。
- 尽量不要在事务中要求用户响应,比如修改新增数据之后在完成整个事务的提交,这样延长事务占用资源的时间,也会提升死锁概率。
- 尽量减少数据库的并发量。
- 尽可能使用分区表,分区视图,把数据放置在不同的磁盘和文件组中,分散访问保存在不同分区的数据,减少因为表中放置锁而造成的其它事务长时间等待。
- 避免占用时间很长并且关系表复杂的数据操作。
- 使用较低的隔离级别,使用较低的隔离级别比使用较高的隔离级别持有共享锁的时间更短。这样就减少了锁争用。