InnoDB存储引擎与MylSAM存储引擎一个差别就是InnoDB支持表锁和行锁,MylSAM存储引擎只实现了表锁。
InnoDB存储引擎实现了两种标准的行级锁:
- 共享锁(S LOCK),允许事务读一行数据。
- 排他锁(X LOCK),允许事务删除或者更新一行数据。
存在一行数据r,如果事务T1获取了数据行r的S锁,此时事务T2可以立即获取数据行r的S锁,因为读取并没有更改数据的内容,因此S锁之间是可以兼容的。如果此时事务T3想要获取数据行r的X锁,则必须等待事务T1,T2释放行r上的S锁,这种情况说明S锁与X锁不能在同一行数据上共存,因此S锁与X锁不兼容。
S | X | |
---|---|---|
S | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 |
与此同时,InnoDB存储引擎支持多粒度锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在,为了支持多粒度锁定,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁。意向锁是将锁定的对象分为多个层次。我们可以把上锁的记录看成一个倒立的金字塔,上方代表粗粒度锁,最下方看作细粒度锁,当我们需要对记录加细粒度锁时,我们需要提前获取该记录的粗粒度锁。
举个例子,一条记录存在于某个数据库中的某张表中,此时数据库锁–>表锁–>行级锁(锁粒度由粗到细),所以我们需要先获取数据库,表上的意向锁之后,才能获取记录上的行锁。
InnoDB存储引擎实现意向锁的方式比较简单,其意向锁即为表级的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型,支持两种意向锁:
- 意向共享锁(IS LOCK):事务将要获取一张表中的某几行的共享锁。
- 意向排他锁(IX LOCK):事务将要获取一张表中的某几行的排他锁。
S锁,X锁,IS锁,IX锁的兼容性:
IS | IX | S | X | |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
举个例子,在对记录r加X锁之前,记录r在表1中,发现有事务对表1加了S锁,因此表1上已存在S锁,之后事务想要对记录r加IX锁时,由于S锁与IX不兼容,所以该事务需要等待表锁操作的完成。
数据库为我们在INFORMATION_SCHEMA架构下添加了INNODB_TRX,INNODB_LOCKS,INNODB_LOCK_WAITS,通过这三张表,可以监控当前事务并分析可能存在的锁问题。
一致性非锁定读
非锁定读意味着,当我们读取行记录时,不需要等待行数据上的X锁释放。
例如,当我们读取的行正在执行update操作或者delete操作时,我们不需要等待行上的X锁释放,相反的,我们可以读取该行数据的一个快照版本。如图所示,某行数据存在多个快照版本。这种通过读取快照数据的方式极大的提高了数据库的并发,这种方式称为多版本并发控制(Multi Version Concurrency Control,MVCC)
在事务隔离级别为REPEATABLE READ和READ COMMITTED时,InnoDB存储引擎使用的是非锁定的一致性读,然而在不同的隔离级别时,读取的快照版本却不一样。
- READ COMMITTED:总是读取最新的快照版本。
- REPEATABLE READ:总是读取事务开始时的行数据版本。
查看目前的隔离级别:
mysql> show global variables like "%isol%";
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set, 1 warning (0.00 sec)
此时我们开启会话A,读取id=1的行内容。
#sessionA
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from parent where id=1;
+----+
| id |
+----+
| 1 |
+----+
1 row in set (0.00 sec)
我们另开一个会话B,对id=1的记录进行修改。
#sessionB
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update parent set id=3 where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
此时并没有提交记录,由于是更新操作,相当于对该记录加了X锁,这时如果
在会话A中对该记录的再次读取,按照之前所说的,X锁排斥任何形式的锁,但是在InnoDB存储引擎的特性下,REPEATABLE READ和READ COMMITTED的隔离级别下会使用一致性的非锁定读,因此,我们在会话A中读取该记录是可行的。
#session A
mysql> select * from parent where id=1;
+----+
| id |
+----+
| 1 |
+----+
1 row in set (0.00 sec)
因为该数据只被会话B更改过一次,所以也就只存在一个快照版本。此时提交会话B中的内容。
#sessionB
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
此时不同的隔离级别读取的数据就体现出差异了,如上所述REPEATABLE READ总是读取事务开始时的行数据版本也就是id=1,而READ COMMITTED总是读取最新的快照版本也就是id=3。接下来证实一下。目前的隔离级别时REPEATABLE READ。预期id=1;
#session A
mysql> select * from parent where id=1;
+----+
| id |
+----+
| 1 |
+----+
1 row in set (0.00 sec)
当在READ COMMITTED级别时,总是读取最新的快照。因此有如下结果:
#sessionA
mysql> select * from parent where id=1;
Empty set (0.00 sec)
执行过程如下:
一致性锁定读:
在innoDB存储引擎下,对于读操作,默认使用了一致性非锁定读,即读取快照版本,在一些情况下,需要对读取操作加锁来保证数据的一致性,对于select操作,加锁方式如下:
- select … for update;
- select … lock in share mode;
第一种方式代表对行记录加X锁,第二种方式代表对行记录加S锁,其他事务可以对加了S锁的数据加S锁,如果对其加X锁,则需要阻塞。
总结一下:
InnoDB存储引擎实现了标准的行级锁:共享锁S,排他锁X。只有S锁与S锁可以共存,其他的组合都排斥,需要等到原有的锁释放。
InnoDB存储引擎支持多粒度锁,从数据分布来看,可以分为数据库–>表–>数据页—>行记录,锁粒度由粗到细进行加锁。
InnoDB存储引擎支持意向锁,分为意向共享锁,意向排他锁。意向锁为表级锁。
InnoDB存储引擎在READ COMMITTED和REPEATABLE READ隔离级别时采用一致性非锁定读,通过读取数据快照的形式读取数据。区别在于:
READ COMMITTED:总是读取最新的行记录快照。
REPEATABLE READ:总是读取事务开始时的行记录。
InnoDB存储引擎对读操作也提供了加S锁和X锁,执行语句分别为:
select … for update 加X锁。
select … lock in share mode 加S锁。
常用命令:
show global variables like "%isolation%"; //查看隔离级别
set global transaction_isolation ='read-committed'; //修改隔离级别