锁机制用于管理对共享资源的并发访问。InnodDB存储引擎中,会在数据库内部的多个地方使用锁,从而允许多种不同资源的并发访问。
比如:操作缓冲池的LRU列表,删除、添加、移动LRU列表的元素,为了保证一致性,必须有锁的介入。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。
InnoDB存储引擎锁的实现提供一致性的非锁定读、行级锁支持。行级锁没有额外的开销,并可以同时得到并发性和一致性。
锁的类型
行级锁
InnoDB存储引擎实现了两种如下的行级锁:
- 共享锁(S Lock),允许事务读一行数据
- 排他锁(X Lock),允许事务删除或更新一行数据
设T为事务,如果一个T1事务得到了行r上的一个共享锁,此时T2在去申请共享锁则会立即成功,并不会阻塞,即共享锁之间是兼容的。此时再来一个T3事务,想要修改行r的数据,则需要等待T1、T2事务都释放共享锁才能成功。这其实就是我们熟知的读写锁。
我们也知道,数据库由多个表组成,表又由多个行组成,同样的,InnoDB存储引擎在表、行上分别也支持不同粒度的锁。即可以对表加锁1的同时,对其中的某行加锁2.
意向锁
InnoDB支持一种额外的锁方式,用来实现不同粒度的加锁。其将锁定的对象又分为多个粒度(数据库、表、页),对细粒度的对象上锁,需要先对粗粒度的对象上锁(预加小锁,先上大锁)。
如图,如果需要对底层某条记录上锁,分别需要对数据库A、表x、页y上意向锁IX,最后再对记录上X锁。
- 意向共享锁(IS Lock),事务想要获得一张表中某几行记录的共享锁
- 意向排他锁(IX Lock),事务想要获得一张表中某几行记录的排他锁
这里的意向排他锁同上面的排他锁性质并不一样,如果T1对表1加了IX锁,然后T2再对表1加IX锁,也是能成功的。真正的锁申请被排他性锁阻塞,只会发生在行级锁上。
命令SHOW ENGINE INNODB STATUS可以用来查看当前的锁正阻塞在哪里的数据上。
一致性非锁定读
InnoDB存储引擎通过多行版本控制的方案,来读取当前执行时间数据库中行的数据,称为一致性的非锁定读。
如果读取的行正在被delete或者update,此时读取操作并不会因此去等待行数的排他锁释放,而是去读取行的一个快照数据(修改前的数据的备份);
快照数据是通过undo段来读取的,undo段是事务用来回滚数据而做的某种备份。这里正好用来提供非锁定读。
值得一提的是,仅在事务隔离级别为REAN COMMITTED和REPEATABLE READ(InnoDB默认隔离级别)下,InnoDB存储引擎会使用一致性非锁定读。
在REAN COMMITTED事务隔离级别下,一致性非锁定读会读取被锁定行的最新的一份快照数据,而在REPEATABLE READ事务隔离级别下,一致性非锁定读会读取事务开始时的行数据版本。即仅在REPEATABLE READ隔离级别及以上,同个事务内对同行数据的读取才会始终是一致的(也就是重复读的字面意思喽)。
一致性锁定读
某些情况下,用户需要显示地对数据库读取操作加排他锁,以保证业务逻辑上的一致性,这就要求数据库要支持显示加锁的语句,即使是只读操作。
InnoDB存储引擎支持两种一致性的锁定读操作:
- selset ... for update
- select ... lock in share mode
selset ... for update对读取的行记录加一个X锁,其它事务对该行加的任何锁都会被阻塞。select ... lock in share mode对读取的记录加一个S锁,其它事务可以对同一行数据加S锁,但如果是X锁,则会阻塞。值得注意的是,这里的锁是在事务提交后释放的。
锁的算法
- Record Lock:单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
- Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,包含记录本身(为解决幻读问题而被设计的)
InnoDB对于行的查询都是采用的Next-Key Lock算法。加入一个索引有10、13、20这三个值,那么该索引可能被Next-Key Lock锁住的区间(左开右闭)有:
(-∞,10]
(10,13]
(13,20]
(20,+∞)
如果事务T1已经通过Next-Key锁定了范围:
(10,13]、(13,20]
此时再插入记录15,则锁定的范围会变成:
(10,13]、(13,15]、(15,20]
当查询的索引含有唯一属性时,InnoDB存储引擎又会对其优化,将其降级为Record Lock,仅锁住索引本身。
举个栗子,如下创建一个表:
create table z(a int, b int, primary key(a), key(b));
insert into z select 1,2;
insert into z select 3,4;
insert into z select 5,6;
表z的列b是辅助索引,即列表b的值没有唯一属性,若在会话A中执行下面的语句:
selsct * from z where b = 4 for update
很明显,这是SQL语句通过辅助索引列b进行查询,因此对该列使用Next-Key算法加锁,另外因为还有个聚集索引,需要对其加锁Record Lock(具有唯一属性的加Record Lock);
故列a上的锁有:5
列b上的锁有:(2,4], (4,5]
此时,如果在新会话B中执行如下SQL,都会被阻塞:
select * from z where a = 5 lock in share mode;
insert into z select 4,3;
insert into z select 6,5;
第一个语句阻塞是因为列a上已经有一个X锁了,第二、三条语句阻塞,则是因为3、5分别在列b的两个Next-Lock锁的范围内。
而如果在新的会话C中执行如下的SQL语句,则不会阻塞:
insert into z select 4,2;
insert into z select 6,7;
从这个例子也可以看出,Next-Key的作用是为了阻止多个事务将记录插到同一个范围内,而这会导致幻读(phantom problem:是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能返回之前不存在的行)问题的产生。例如在上面的例子中,会话A已经锁定了b=3的记录,若此时没有Next-Key Lock锁住(2,4],(4,6]范围,那么用户可以在b=4行的下一个位置,再插入一条同样列b=4的记录,那么会话A中的用户再次执行同样查询时将会返回两条b=3的记录了,这将导致幻读问题的产生,也就意味着违反了事务的隔离性。
上述的Next-Key Lock算法是在REPEATABLE READ隔离级别下使用的,若将事务隔离级别设置为READ COMMITED,则会自动关闭Next-Key算法,转而使用Record Lock,因为这种隔离级别下,不存在幻读问题。
锁问题
脏读
READ UNCOMMITED事务隔离级别下,在会话A中,在事务没有提交的前提下,会话B中两次select操作取得了不同的结果,并且会话A的事务并没有提交,即产生了脏读。
生产环境中一般都是默认REPEATABLE READ事务隔离级别,所以一般没有脏读现象。脏读隔离在一些比较特殊的情况下才会用到,比如sql备份环境中的slave结点,且该节点的查询并不需要特别精确的返回值,此时设置为READ UNCOMMITED隔离级别,有助于性能提升。
不可重复读
在READ COMMITED隔离级别下,存在不可重复读的问题,不过一般该问题都可以接受,因为读到的是事务已经提交的数据,本身并不会有很大的问题。如前一节所述,InnoDB存储引擎在READ REPEATABLE隔离级别中,采用Next-Key Lock算法避免不可重复读问题。