锁是数据库区别于文件系统的一个关键特性。锁机制用于管理共享资源的并发访问。InnoDB除了在表上面进行上锁之外,在其他层面也会进行上锁,如操作缓冲池当中的LRU列表,删除、添加和移动都需要有锁的介入。本文只讨论在InnoDB中的锁。
一、InnoDB存储引擎中的锁
1.1 锁的类型
1)共享锁(s lock):允许多个事务读一行数据。
2)排它锁(x lock):允许一个事务修改或删除数据。
锁的兼容和锁的不兼容
X | S | |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
例:
T1获取了行r的共享锁,T2仍然可以获取r的共享锁,这称为锁的兼容。
如果此时T3想要获取r的排他锁,则需要等到T1和T2释放r的共享锁之后,这称为锁的不兼容。
InnoBD允许行级锁和表级锁同时存在,为了支持在不同粒度上执行加锁操作,支持了额外的锁方式,称之为意向锁(Intention Lock)。InnoDB设计的意向锁比较简单,因为其支持行级锁,所以意向锁仅设计在表锁上。其主要目的是为了揭示下一行将要被请求的锁类型。
1)意向共享锁(IS Lock)想要获得一张表中某几行的共享锁。
2)意向排它锁(IX Lock)想要获得一张表中某几行的排它锁。
意向锁不会阻塞除全表扫描以外的请求。
在information_schema当中有以下三张表可以让我们分析锁的情况:INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS。
INNODB_TRX:
首先查看INNODB_TRX中的主要段含义:
字段名 | 说明 |
---|---|
trx_id | 唯一事务id |
trx_state | 事务状态 |
trx_started | 事务开始时间 |
trx_requested_lock_id | 等待事务的锁id,如trx_wait为lock_wait的时候;如果不是trx_wait,则是null |
trx_wait_started | 事务等待开始时间 |
trx_weight | 权重,该值反映修改和锁住的行数。当发生死锁需要回滚时,会回滚该值最小的记录 |
trx_mysql_thread_id | mysql中的线程id |
trx_query | 事务运行的sql语句 |
INNODB_LOCKS:
首先查看INNODB_LOCKS中的主要段含义:
|字段名|说明|
|lock_id|锁的id|
|lock_trx_id|事务的id|
|lock_mode|锁模式|
|lock_type|锁的类型,表锁和行锁|
|lock_table|要加锁的表|
|lock_index|锁住的索引|
|lock_space|锁对象的space id|
|lock_page|事务锁定页的数量。表锁该值为null|
|lock_rec|事务锁定记录的数量,表锁该值为null|
|lock_data|事务锁定记录的主键值,表锁该值为null|
INNODB_LOCK_WAITS:
首先查看INNODB_LOCK_WAITS中的主要段含义:
字段名 | 说明 |
---|---|
requesting_trx_id | 申请锁资源的事务id |
requested_lock_id | 申请锁的id |
blocking_trx_id | 阻塞的事务id |
blocking_lock_id | 阻塞锁的id |
1.2 一致性非锁定读
一致性非锁定读是指InnoDB通过多版本并发控制MVCC(Multi-Version Concurrency Control)来读取当前执行时间数据库中行的数据。
如果一行或多行数据正在进行update或者delete操作,这时候另外的事务去读取这些数据并不会等待其释放锁,可以直接进行读取。读取的是其快照数据。
之所以称之为一致性非锁定读,因为不需要等到行上面的X锁进行释放即可进行读取。如下图所示:
快照数据是指行之前版本的数据,通过undo log实现。
如上图所示,一个行的版本可能有多个,一般称之为多版本技术,由此带来的并发控制称之为多版本并发控制(MVCC)。
在事务隔离级别read commited(oracle默认)和repeatable read(innoDB的默认隔离级别)中,使用非锁定一致性读,然而其对于快照读的定义却不相同。
隔离级别 | 读取版本 |
---|---|
rc | 读取最新版本数据 |
rr | 读取最开始版本数据 |
下面看个例子:
时间 | 会话1 | 会话2 |
---|---|---|
1 | begin | |
2 | select * from student where id = 1 | |
3 | begin | |
4 | update student set id =3 where id = 1 | |
5 | select * from student where id = 1 | |
6 | commit | |
7 | select * from student where id = 1 | |
8 | commit |
如上表格所示:
在rr级别下,会话1读取到的结果分别是:1,1,1
在rc级别下,会话1读取到的结果分别是:1,1,empty
1.3 一致性锁定读
在某些特定情况下,用户需要显示的对数据库读取进行加锁操作以保证数据的一致性。InnoDB对select支持两种一致性锁定读:
- select * from table where id = X for update 对数据加X锁,其他事物不能对该行数据加任何锁。
- select * from table where id = X lock in share model 对数据加S锁,其他事务可以对其加S锁,但是加X锁会被阻塞。
以上加锁操作必须保证在一个事务当中,一旦事务提交后,锁即被释放掉。所以使用时不需使用begin、start transaction、set autocommit = 0。
1.4 自增长和锁
自增长是一种常见的使用方式,也是数据主键的首选。InnoDB对于每一个含有自增长属性的表都维护一个自增长计数器。会依据该计数器的值自动加1赋予自增长列。这种方式称作Auto Inc Locking,这是一种特殊的表锁机制,而这种方式存在一定的性能问题。
在mysql 5.1.22开始,提供了一种轻量级互斥量的自增长方式,这种机制大大提高了自增长值插入的性能。并且提供一个innodb_autoinc_lock_mode来控制自增长,默认为1。另外还有0和2两种方式。
0:并发性能不好,在5.1.22版本前使用。
1:默认值。
2:性能最高,并发插入会带来一定的问题,导致不连续。
二、锁的算法
2.1 三种行锁的算法:
1)Record Lock:单条记录锁。
2)Gap Lock:区间锁或间隙锁,锁定一个范围但不包含本身。
3)Next-key Lock:临键,Record Lock + Gap Lock,锁定范围区间的同时并且锁定记录本身。
如下索引值有1,5,9,11,name在三种锁的不同区间如下所示:
Next-key Lock算法是结合了record lock 和gap lock,其出现的原因是为了解决幻读(snapshot read),利用这种技术,锁定的不再是单个值,而是一个范围。
如果查询的索引含有唯一索引时,Next-key Lock将会降级为Record lock,仅锁住索引本身。
如果查询的索引是辅助索引,使用Next-key Lock进行加锁。按照上图给出下面一个sql,key是辅助索引,本例子不写唯一索引了:
select * from table where key = 5 for update;添加了X锁
那么会对5这个值增加(1,5]的前区间,还会使用gap lock对其增加一个后区间(5,9),因此,运行一下sql都会被阻塞:
select * from table where key = 6 lock in share mode;无法对X锁添加S锁
insert into table select 2;2在锁定区间,被阻塞
insert into table select 7;7在锁定区间,被阻塞
Gap Lock的作用是为了解决幻读问题,只在rr级别存在,如果想要关闭可以修改默认隔离界别为rc,但是这违反了隔离性。
在默认的隔离级别repeatable read下,Innodb使用next-key lock来解决幻读问题。幻读问题是指在同一事务的两次查询中,可能得到不同的结果,即第二次可能查询到之前不存在的行。