前言
通常情况下,当访问某张表或者记录的时候,操作者必须先获取锁。
需要写锁还是读锁取决于执行的操作,获取写锁的优先级高于读锁。
但是当读取者已经拿到读锁在查询时,此时有写操作请求获取写锁,由于查询开始后不能中断,因此允许读取者完成操作,此时写锁请求需要等待。
锁类型
- 共享锁(读锁,S锁)
- 会阻塞其他事务修改表数据
- 共享锁,允许读取者同时获取相同的锁,在读取过程中不允许其他请求获得写锁
- 排他锁(写锁,X锁)
- 会阻塞其他事务读和写
- 独占的排他锁,为了避免同时写入导致数据错乱,或者读取到变化中的数据,在某一个写入操作获取到写锁后会阻塞其他请求获取读锁或者写锁
- 意向锁(Intention Locks)
- 意向锁的作用是为了解决表锁和行锁共存时,快速判断是否能获取到锁
- 假设事务A锁定了表中的一行,事务B想获取表锁,为了避免事务B表锁和事务A行锁冲突,数据库要通过以下两步进行判断
- step1:判断表是否已被其他事务用表锁锁表, step2:判断表中的每一行是否已被行锁锁住
- step2通过遍历判断,效率太低所以就有了意向锁
- 分为意向共享锁(IS)和意向排他锁(IX)
- innodb事务在获取某行的共享锁或者排它锁之前,会先尝试获取IS或者IX,目的是加速判断是否可以获取到锁,即可以先进行一个初步判断
- 意向锁是表级锁
- 兼容性汇总如下
| X | IX | S | IS | |
|---|---|---|---|---|
| X | 冲突 | 冲突 | 冲突 | 冲突 |
| IX | 冲突 | 兼容 | 冲突 | 兼容 |
| S | 冲突 | 冲突 | 兼容 | 兼容 |
| IS | 冲突 | 兼容 | 兼容 | 兼容 |
- 记录锁(Record Locks)
- 记录锁是索引记录上的锁,如果一个表没有定义索引,那么就会去锁定隐式的“聚集索引”
- 间隙锁(Gap Locks)
- 间隙锁是一个在索引记录之间的间隙上的锁,一个间隙可能跨越单个索引值、多个索引值,甚至为空。
- 当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁
- 目的是防止幻读和满足其恢复和复制的需要
- 临键锁(Next-key Locks)
- Next-Key Locks是行锁与间隙锁的组合。
- 当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上记录锁(Record Lock),然后再对索引记录两边的间隙加上间隙锁(Gap Lock)。
- 插入意向锁(Insert Intention Locks)
- 插入意向锁是在数据行插入之前通过插入操作设置的间隙锁定类型。
- 如果多个事务插入到相同的索引间隙中,如果它们不在间隙中的相同位置插入,则无需等待其他事务。例如:在4和7的索引间隙之间两个事务分别插入5和6,则两个事务不会发冲突阻塞。
- 自增锁
- 自增锁是事务插入到有自增列的表中而获得的一种特殊的表级锁。如果一个事务正在向表中插入值,那么任何其他事务都必须等待,保证第一个事务插入的行是连续的自增值。
锁粒度
- 表级
- 优点
- 实现简单,开销小,加锁快,不会出现死锁
- 缺点
- 锁定粒度大,发生锁冲突的概率最高,并发度最低
- 优点
- 页级(MySQL特有)
- 开销和加锁时间界于表锁和行锁之间;
- 会出现死锁;
- 锁定粒度界于表锁和行锁之间,并发度一般
- 行锁
- 优点
- 锁定粒度最小,发生锁冲突的概率最低,并发度也最高
- 缺点
- 实现复杂,开销大,加锁慢,会出现死锁
- 优点
锁的实现方式
- InnoDB行锁是通过给索引加锁来实现的,如果不使用索引查询,InnoDB会通过隐藏的聚簇索引来对记录进行加锁(全表扫描,也就是表锁)。
- 但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会放锁,最终持有的,是满足条件的记录上的锁。但是不满足条件的记录上的加锁/放锁动作是不会省略的。所以在没有索引时,不满足条件的数据行会有加锁又放锁的耗时过程。
- 索引分为主键索引和非主键索引两种。如果一条sql语句操作了主键索引,MySQL就会锁定对应主键索引;如果一条语句操作了非主键索引,MySQL会先锁定非主键索引,再锁定对应的主键索引。
INNODB行锁注意事项
- 由于InnoDB行锁是针对索引加的锁,不是针对记录加的锁,所以即使是访问不同行的记录,如果使用了相同的索引键,也是会出现锁冲突的
- 即便条件中使用了索引字段,如果MYSQL优化器认为全表扫描效率更高,那么他会使用表锁,放弃行锁
死锁
-
什么是死锁
- 当两个事务分别锁定了两个单独的对象,这时每一个事务都要求在另一个事务锁定的对象上获得一个锁,因此每一个事务都必须等待另一个事务释放占有的锁。这就发生了死锁。
-
解决办法
- 理论上预防死锁的发生就是要破坏产生死锁的条件
- 一次封锁法
- 一次封锁法要求每个事务必须一次将所有要使用的数据全部加锁,否则就不能继续执行
- 存在的问题
- 一次将以后要用到的全部数据加锁,加大封锁范围,降低系统的并发度
- 数据库中数据是不断变化的,原来不要求封锁的数据,在执行过程中可能会变成封锁对象,所以很难事先精确确定每个事务要封锁的数据对象,为此只能扩大封锁范围,将事务在执行过程中可能要封锁的数据对象全部加锁,这就更降低了并发度
- 顺序封锁法
- 预先对数据对象规定一个封锁顺序,所有事务都按这个顺序实行封锁。
- 如:在B树结构的索引中,规定封锁的顺序必须从根结点开始,然后是下一级的子结点,逐级封锁
-
死锁的诊断与解除
- 诊所办法
- 超时法
- 指的是如果一个事务的等待时间超过了规定的时限,就认为发生死锁
- 缺点
- 有可能误判死锁,事务因为其他原因使等待时机超过时限
- 时限若设置得太长,死锁发生后不能及时发现
- 等待图法
- 指的是用事务等待图动态反应所有事务的等待情况
- 事务等待图是一个有向图G=(T,U),其中T为结点的集合,每个结点表示正在运行的事务。U为边的集合,每条边表示事务等待的情况。若T1等待T2,则T1、T2之间划一条有向边,从T1指向T2。事务等待图动态地反映了所有事务的等待情况。并发控制子系统周期性地检测事务等待图,如果发现图中存在回路,则表示系统中出现了死锁
- 指的是用事务等待图动态反应所有事务的等待情况
- 超时法
- 解除办法
- 通常是选择一个处理死锁代价最小的事务,将其撤销,释放此事务持有的所有的锁,使其他事务能继续运行下去。(而且要对撤销的事务所执行的数据修改操作进行恢复)
- 应用
- 选择一个合理的超时时间来自动释放死锁
- 诊所办法
-
应用中尽量减少死锁的方法
- 对同样的表进行操作时,尽量使用相同的访问顺序
- 如果不同的程序会并发存取多个表,应尽量约定以相同的顺序为访问表,这样可以大大降低产生死锁的机会。
- 在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低死锁的可
- 在用一个事务中,尽可能依次锁定需要的资源
- 在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应该先申请共享锁,更新时再申请排他锁
- 在业务允许的前提下,使用较低的隔离级别
- 在REPEATEABLE-READ隔离级别下,如果两个线程同时对相同条件记录用SELECT...ROR UPDATE加排他锁,在没有符合该记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可以避免问题
- 如果业务允许,并且又非常容易发生死锁,可以尝试升级锁粒度,改为表锁来避免死锁
- 对同样的表进行操作时,尽量使用相同的访问顺序