很多人知道相对于Mysql的其他存储引擎,Innodb有一个明显的特点,那就是支持行级锁,下面就让我们了解一下Innodb的行级锁吧。
行级锁主要有三种算法:
Record Lock:单个行记录上的锁。
Gap Lock:间隙锁。锁定一个范围,但不包含记录本身
Next-Key Lock:锁定一个范围,并且包含记录本身。
在InooDB中对于行的查询都是采用Next-Key Lock这种锁定算法,该锁定算法的使用存在三种情况:
下面我们就该三种情况进行解析:
首先构建数据:
DROP TABLE IF EXIST t;
CREATE TABLE t(id INT PRIMARY KEY);
INSERT INTO t SELECT 1;
INSERT INTO t SELECT 2;
INSERT INTO t SELECT 5;
进行实验,建议在开始前将等待锁的超时时间设置到最小SET GLOBAL innodb_lock_wait_timeout=1;
:
线程A | 线程B |
---|---|
BEGIN; | |
SELECT * FROM t WHERE id = 5 FOR UPDATE; // 对目标语句加排外X锁。 | |
BEGIN; | |
INSERT INTO t SELECT 4; //成功 | |
INSERT INTO t SELECT 6; // 成功 | |
INSERT INTO t SELECT 5; // 抛出异常Lock wait timeout exceeded; try restarting transaction | |
COMMIT; | |
COMMIT; |
如果线程A查询的行不存在,则不会进行加锁操作。
需要注意的一点是,锁的超时并不会导致当前事务的回滚,提交之后操作成功的插入命令仍然会生效。若希望超时时回滚事务,可以在mysql的配置文件中设置innodb_rollback_on_timeout=on;
。另外,由于在Innodb中,对数据加锁实际上是在索引上加锁,因此不要试图在存在锁时修改表索引。
首先构建数据:
DROP TABLE IF EXIST t;
CREATE TABLE `t` (
`id` int(11) NOT NULL,
KEY `id` (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO t SELECT 1;
INSERT INTO t SELECT 3;
INSERT INTO t SELECT 6;
进行实验:
线程A | 线程B |
---|---|
BEGIN; | |
SELECT * FROM t WHERE id = 3 FOR UPDATE; | BEGIN; |
INSERT INTO t SELECT 2; // 抛出异常 | |
INSERT INTO t SELECT 4; // 抛出异常 | |
INSERT INTO t SELECT 1; // 抛出异常 | |
INSERT INTO t SELECT 6; // 成功 | |
INSERT INTO t SELECT 0; // 成功 | |
COMMIT; | |
COMMIT; |
Next-Key Lock锁定算法是为了解决Phantom Problem(幻像问题),采用该算法的锁定技术称为Next-Key Locking,该锁定技术锁定的不是单个值,而是一个范围。文末解释Next-Key Lock锁定算法为什么能够解决幻像问题。
在上述例子中,针对上锁的值3而言,Next-Key Lock需要锁定的是区间(1, 3],同样是为了解决Phantom Problem,需要对辅助索引下一个键值使用gap Lock,锁定区间(4, 6)。而上述实验中插入值1也抛出异常的原因可以视为将值1插入到了(1, 3]区间中:
用户可以通过一下两种方式显式关闭锁:
将事务的隔离级别设置为READ COMMITTED。set session transaction isolation level read committed
将参数innodb_locks_unsafe_for_binlog设置为1。sel session innodb_locks_unsafe_for_binlog = 1
首先构建数据:
DROP TABLE IF EXIST t;
CREATE TABLE `t` (
`id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO t SELECT 1;
INSERT INTO t SELECT 3;
INSERT INTO t SELECT 6;
进行实验:
线程A | 线程B |
---|---|
BEGIN; | |
SELECT * FROM t WHERE id = 3 FOR UPDATE; | BEGIN; |
INSERT INTO t SELECT 2; // 抛出异常 | |
INSERT INTO t SELECT 4; // 抛出异常 | |
INSERT INTO t SELECT 1; // 抛出异常 | |
INSERT INTO t SELECT 6; // 抛出异常 | |
INSERT INTO t SELECT 0; // 抛出异常 | |
COMMIT; | |
COMMIT; |
以上就是对于行级锁而言的三种情况。由此可知InnoDB实现行级锁的前提是使用索引,因此在存在事务且并发量较大的系统中,对SQL语句与索引的设计应该将行级锁考虑进去。
上文有提到InnoDB利用Next-Key Lock来解决Phantom Problem,而幻像问题其实就是幻读,是指某个事务两次读取之间,其他事务添加了一行数据,而导致两次查询数据不一致的问题。为了解决幻读,一般都需要进行全表锁定,采用最高的隔离级别——序列化。
但Mysql通过Next-key locking解决了该问题, Next-key locking其实是 gap lock 和 record lock 的统称,解决幻读问题是用 gap lock 来解决的。
下面举个例子:
构造数据:
DROP TABLE IF EXIST t;
CREATE TABLE `t` (
`id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO t SELECT 1;
INSERT INTO t SELECT 3;
INSERT INTO t SELECT 6;
实验步骤:
线程A | 线程B |
---|---|
SET SESSION transaction isolation level read committed; | |
BEGIN; | |
SELECT * FROM t WHERE id >3 FOR UPDATE; // 输出为5 | BEGIN; |
INSERT INTO t SELECT 6; // 成功 | |
COMMIT; | |
SELECT * FROM t WHERE id >3 FOR UPDATE; // 输出为5 6 |
在RC的隔离级别中会出现两次查询数据不一致的问题,而Mysql的Next-key locking通过将[3, +∞]加上X排他锁解决了幻读问题。
不过到这里大家也会发现,该例子举的是范围查找,那么上面的精准查找为什么又要采用Next-key locking呢?因为Next-key locking是通过直接在底层 B+ 树上锁住区间的方式来实现的,并且排他锁只能锁住指定的行,防止其被更新,对非唯一键的插入就没办法了,因此对于上述例子如果此时再插入一条 id=3 的记录,那就出现幻读了。通过将加锁点最近两端的值进行锁定(即(1, 6]),以此来避免幻读。
最后留下一个问题,为什么对主键进行锁定能够降级成为Record Lock,而唯一索引却不能?