阅读 151

InnoDB 存储引擎的锁学习

本文主要是针对《Mysql技术内幕:InnoDB 存储引擎》一书中第六章关于 InnoDB 存储引擎中锁的学习总结。

锁是数据库系统区别于文件系统的一个关键特性,为了支持对共享资源的并发访问,提供数据的完整性和一致性,我们必须为数据加锁,InnoDB 提供了一致性非锁定读、行级锁的支持。

共享锁(S Lock)和排他锁(X Lock)

  • 共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改
  • 排他锁是允许当前事务修改和删除数据,当一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁
  • InnoDB 支持多粒度锁,既支持在表粒度上 S 锁和 X 锁,也支持在行粒度上加 S 锁 和 X 锁
  • 锁的兼容性是指是否可以同时给某个数据加两个锁,S 锁和 X 锁的兼容性如下:
锁类型 S 锁 X 锁
S 锁 兼容 不兼容
X 锁 兼容 不兼容

意向锁

  • 意向锁是一种表级锁
  • 用户要获取某个表的行锁,必须先获取该表的意向锁
  • 意向锁是 InnoDB 自己维护的,用户无法手动操作意向锁
  • 意向锁分为意向共享锁(IS)和意向排他锁(IX)
  • 意向锁和意向锁之间相互兼容,意向锁和行级锁也相互兼容
  • 意向锁和表锁的兼容性如下:
锁类型 IS 锁 IX 锁
表 S 锁 兼容 不兼容
表 X 锁 兼容 不兼容
  • 如果事务想对某个表加锁,那么先会检查是否与该表的意向锁相兼容,如果不兼容则不能加表级锁,而不需要检查是否与该表中行级锁的兼容性
  • 意向锁在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求

一致性非锁定读

  • 一致性非锁定读是基于多版本并发控制(MVCC)技术实现的
  • 多版本并发控制(MVCC) 技术是指一个行记录可能有不止一份快照数据,而 MVCC 是通过事务的 undo 日志实现的
  • 一致性非锁定读是 InnoDB 默认的读取方式,即读取数据时不占用和等待表上的锁
  • 一致性非锁定读只有在隔离级别为 REPEATABLE READ 和 READ COMMITTED 下才会支持
  • 当事务的隔离级别为 REPEATABLE READ 时,同一个事务中的一致性读都是读取的是该事务下第一次查询所建立的快照
  • 当事务隔离级别为 READ COMMITTED 时,同一事务下每次查询都是读的最新一份数据快照,所以可能会违背数据库的隔离性,因为同一份事务里可能读取到两份不同的数据
  • 一致性非锁定读可以看到该时间点之前提交的事务所做的更改并且不会被之后的修改或者未提交事务所影响

一致性锁定读

在数据库的默认隔离级别为 REPEATABLE READ 下,Select 查询默认为一致性非锁定读,如果想要使用一致性锁定读,需要显示给 Select 查询加锁

Select ... FOR UPDATE          - 给读取的行加上 X 锁
Select ... LOCK IN SHARE MODE  - 给读取的行加上 S 锁
复制代码

自增长和锁

  • 因为主键索引是有序的,我们在主键上设置自增属性,可以有效的减少索引页的分裂和数据的移动。
  • 使用关键字 AUTO_INCREMENT 给主键加自增长特性:
CREATE TABLE `test` (
  `id` int(1) NOT NULL AUTO_INCREMENT,
  `name` varchar(8) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12;
复制代码
  • 对于自增长列的索引比较特殊,是用 AUTO-INC Locking 方式先的,是一种表锁机制
  • 自增长的列必须是索引,同时必须是索引的第一个列
  • 为了提高插入性能,锁并不是在一个事务完成时才释放,而是在完成对自增长值的插入 SQL 后立即释放
  • 对于并发插入存在一定性能问题,必须等待前一个自增长插入完成才能继续插入,虽然不是等待整个事务结束
  • 从 MySQL5.1.22 版本开始,InnoDB 存储引擎引擎中提供了一种轻量级互斥量的自增长实现机制,这种机制大大提高了自增长值插入的性能,可以通过参数 innodb_autoinc_lock_mode 对自增长索引模式进行设置:
- 0:这是 MySQL5.1.22 版本之前自增长的实现方式,即通过表锁的 AUTO-INC Locking 方式 
- 1:这是该参数的默认值。对于 simple inserts 该值会用互斥量去对内存中的计数器进行累加的操作,
     对于 bulk inserts 还是使用传统表锁的 AUTO-INC Locking方式
- 2:在这个模式下,对于所有的 insert-like 自增长的产生都是通过互斥量,
     而不是通过 AUTO-INC Locking 的方式,显然这时性能最高的方式。
     然而会带来一定的问题。因为并发插入的存在,在每次插入时,自增长的值可能不是连续的。
复制代码

外键和锁

  • 在InnoDB存储引擎中,对于一个外键列,如果没有显示地对这个列加索引,InnoDB存储引擎会自动对其加一个索引,因为这样可以避免表锁
  • 对于外键值的插入或更新,首先需要检查父表中的记录,即 SELECT 父表
  • 在外键检查对于父表的 SELECT 操作时,不是使用一致性非锁定读的方式,因为这会发生数据不一致的问题,InnoDB 会主动对父表加一个 S 锁

行锁算法

  • InnoDB 三种行锁算法:
- Record Lock(行锁):单个行记录上的锁,锁定单条索引记录。
- Gap Lock(间隙锁):锁定一个范围,但不包括记录本身
- Next-Key Lock(临键锁):是行锁和间隙锁的结合,锁定一个范围,并且锁定记录本身。
复制代码
  • 行锁在 InnoDB 中是基于索引实现的,所以一旦某个加锁操作没有使用索引,那么该锁就会退化为表锁
  • 对于行的等值查询,一般情况下都是用 Next-Key Lock,如果发现查询条件没有设置索引,则退化为表锁,如果发现查询条件是唯一索引,则升级为行锁
  • 在根据非唯一索引进行区间查询时,会使用间隙锁,使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据:
- 此时所有在(1,10)区间内的记录行都会被锁住
SELECT * FROM table WHERE id BETWEN 1 AND 10 FOR UPDATE;
复制代码
  • 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据
  • 假设数据库表中某个非唯一索引存在以下值 10,24,32,45,该表中 age 列潜在的临键锁有:(-∞, 10], (10, 24], (24, 32], (32, 45],(45, +∞]
  • 在根据非唯一索引对记录行进行 UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE 操作时,InnoDB 会获取该记录行的临键锁 ,并同时获取该记录行下一个区间的间隙锁
- 执行以下 SQL 时,InnoDB 会获取 24 对应的临键锁 (10, 24],并获取下一个区间的间隙锁 (24, 32),所以最终在 (10, 32) 区间加上间隙锁
SELECT * FROM table WHERE age = 24 FOR UPDATE;

- 之后如果在其它事务中执行以下命令,则该命令会被阻塞:
INSERT INTO table(age, name) VALUES(26, 'Ezreal');

复制代码
  • Gap Lock 是为了阻止事务将数据插入同一范围内,从而解决幻读现象,也就是两次读不一致情况
  • 用户可以通过以下两种方式关闭间隙锁,这种情况下除了外键约束和唯一性检查以外,其余情况都是用记录锁:
方式一:将事务的隔离级别设置成 READ COMMITTED
方式二:将参数 innodb_locks_unsafe_for_binlog 设置为 1
复制代码

锁问题

  • 脏读:
- 脏读是指在并发情况下,当前事务可以读到其它事务未提交到数据
- 脏读违反了数据库事务的隔离性要求
- InnoDB 中脏读的发生条件是事务的隔离级别为 READ UNCOMMITED
复制代码
  • 不可重复读:
- 不可重复读是指同一个事务内,多次读取同一个数据集合结果不一致现象
- 不可重复读违反了数据库事务的一致性要求
- InnoDB 通过 next-key 算法来避免不可重复读现象
- 为了避免不可重复读现象发生,需要将事务的隔离级别设置为 REPEATABLE READ 以上
复制代码
  • 丢失更新:
- 丢失更新是指一个事务的更新操作会被令一个事务的更新操作覆盖
- 在当前数据库任何隔离级别下,都不会导致数据库丢失更新现象发生
复制代码

阻塞和死锁

  • 阻塞是指一个事务中的锁需要等待另外一个事务的锁释放资源才能继续执行
- 阻塞主要是为了确保事务的并发能够正常有序的执行
- 通过参数 innodb_lock_wait_timeout 来设置事务的超时等待时间,默认是 50 s
- 通过参数 innodb_rollback_on_timeout 来设置事务超时是否进行回滚,默认不进行回滚
- 超时情况下默认不进行回滚,需要业务来捕获超时异常主动进行回滚,否则会导致数据的不一致性发生

复制代码
  • 死锁是指两个及两个以上的事务为了争夺资源而造成的一种相互等待的现象
innoDB 解决死锁的两种方案:
- 事务超时回滚事务,这种方案的缺点是如果超时的事务占用的比重比较大,回滚非常耗时
- 采用 wait-for graphic(等待图) 来进行死锁检查,通过深度优先遍历算法主动检查死锁发生,释放回滚 undo 量最小的事务
复制代码
关注下面的标签,发现更多相似文章
评论