文章总共分为五个部分:html
大而全版(五合一):InnoDB的锁机制浅析(All in One)mysql
这一章节,咱们经过幻读,逐步展开对InnoDB锁的探究。sql
解释了不一样概念的锁的做用域,咱们来看一下幻读究竟是什么。幻读在RR条件下是不会出现的。由于RR是Repeatable Read,它是一种事务的隔离级别,直译过来也就是“在同一个事务中,一样的查询语句的读取是可重复”,也就是说他不会读到”幻影行”(其余事务已经提交的变动),它读到的只能是重复的(不管在第一次查询以后其余事务作了什么操做,第二次查询结果与第一次相同)。数据库
上面的例子都是使用for update
,这种读取操做叫作当前读,对于普通的select
语句均为快照读。并发
当前读,又叫加锁读,或者 阻塞读。这种读取操做再也不是读取快照,而是读取最新版本而且加锁。
快照读不会添加任何锁。ui
官方文档对于幻读的定义是这样的:翻译
原文:The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
手动无脑翻译:所谓的幻影行问题是指,在同一个事务中,一样的查询语句执行屡次,获得了不一样的结果,这就是幻读。例如,若是同一个SELECT
语句执行了两次,第二次执行的时候比第一次执行时多出一行,则该行就是所谓的幻影行。设计
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times.
,这句话看起来应该是不可重复读的定义,一样的查询获得了不一样的结果(两次结果不是重复的),可是后面的举例给出了幻读真正的定义,第二次比第一次多出了一行。也就是说,幻读的出现有这样一个前提,第二次查询前其余事务提交了一个INSERT
插入语句。而不可重复读出现的前提是第二次查询前其余事务提交了UPDATE
或者DELETE
操做。code
mysql的快照读,使得在RR的隔离级别上在next-Key的做用区间内,制造了一个快照副本,这个副本是隔离的,不管副本对应的区间里的数据被其余事务如何修改,在当前事务中,取到的数据永远是副本中的数据。
RR级别下之因此能够读到以前版本的数据,是因为数据库的MVCC(Multi-Version Concurrency Control,多版本并发控制)。参见InnoDB Multi-Versioninghtm
有些文章中提到“RR也不能彻底避免幻读”,实际上官方文档实际要表达的意义是“在同一个事务内,屡次连续查询的结果是同样的,不会因其余事务的修改而致使不一样的查询结果”,这里先给出实验结论:
1.当前事务若是未发生更新操做(增删改),快照版本会保持不变,屡次查询读取的副本是同一个。
2.当前事务若是发生更新(增删改),再次查询时,会刷新快照版本。
示例的基础是一个只有两列的数据库表。
mysql> CREATE TABLE test ( id int(11) NOT NULL, code int(11) NOT NULL, PRIMARY KEY(id), KEY (code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; mysql> INSERT INTO test(id,code) values(1,1),(10,10);
RC状况下会出现幻读。
首先设置隔离级别为RC,SET SESSION tx_isolation='READ-COMMITTED';
事务一 | 事务二 |
---|---|
mysql> SET SESSION tx_isolation='READ-COMMITTED'; mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(9,9); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 9 | 9 | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
RC(Read Commit)隔离级别能够避免脏读,事务内没法获取其余事务未提交的变动,可是因为可以读到已经提交的事务,所以会出现幻读和不重复读。
也就是说,RC的快照读是读取最新版本数据,而RR的快照读是读取被next-key锁做用区域的副本
咱们先来模拟一下RR隔离级别下没有出现幻读的状况:
开启第一个事务并执行一次快照查询。
事务一 | 事务二 |
---|---|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(9,9); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
这两个事务的执行,有两个问题:
1.为何以前的例子中,在第二个事务的INSERT
被阻塞了,而此次却执行成功了。
这是由于原来的语句中带有for update
,这种读取是当前读,会加锁。而本次第一个事务中的SELECT
仅仅是快照读,没有加任何锁。因此不会阻塞其余的插入。
2.数据库中的数据已经改变,为何会读不到?
这个就是以前提到的next-key lock锁定的副本。RC及如下级别才会读到已经提交的事务。更多的业务逻辑是但愿在某段时间内或者某个特定的逻辑区间中,先后查询到的数据是一致的,当前事务是和其余事务隔离的。这也是数据库在设计实现时遵循的ACID原则。
再给出RR条件下出现幻读的情形,这种情形不须要两个事务,一个事务就已经能够说明,
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where id>8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.00 sec) mysql> update test set code=9 where id=10; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> select * from test where id>8; +----+------+ | id | code | +----+------+ | 10 | 9 | +----+------+ 1 row in set (0.00 sec)
至于RR隔离级别下到底会不会出现幻读,就须要看幻读的定义中的查询究竟是连续的查询仍是不连续的查询。若是认为RR级别下可能会出现幻读,那该级别下也会出现不重复读。
RR隔离级别下,虽然不会出现幻读,可是会所以产生其余的问题。
前提:当前数据表中只存在(1,1),(5,5),(10,10)三组数据。
若是数据库隔离级别不是默认,能够执行SET SESSION tx_isolation='REPEATABLE-READ';
(该语句不是全局设置)更新为RR。
而后执行下列操做:
事务一 | 事务二 | 备注 |
---|---|---|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
开启事务一,并查询code>8 的记录,只有一条(10,10) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(11,11); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) |
开启第二个事务,插入(11,11)并提交 | |
mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
事务一再查询一次,因为RR级别并无读到更新 | |
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(11,11); ERROR 1062 (23000): Duplicate entry '11' for key 'PRIMARY' |
事务一明明没有查到,却插入不了 |
除了上述这类问题外,RR还会有丢失更新的问题。
以下表给出的操做:
事务一 | 事务二 | 备注 |
---|---|---|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
开启事务一,并查询code>8 的记录,只有一条(10,10) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> update test set id=12,code=12 where id=10; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.00 sec) |
开启第二个事务,将(10,10)改成(12,12)并提交,注意这里matched是1,changed也是1 | |
mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
事务一再次查询code>8 的记录,仍然只有一条(10,10) |
|
mysql> update test set id=9,code=9 where id=10; Query OK, 0 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0 |
这里查询到0条,更新了0条 |
这个例子里,事务一的更新是无效的,尽管在这个事务里程序认为还存在(10,10)记录。
事务一中更新以前的SELECT
操做是快照读,因此读到了快照里的(10,10),而UPDATE
中的WHERE
子句是当前读,取得是最新版本的数据,因此matched: 0 Changed: 0
。
若是上述例子中的操做是对同一条记录作修改,就会引发更新丢失。例如,事务一和二同时开启,事务一先执行update test set code=100 where id=10;
,事务二再执行update test set code=200 where id=10;
,事务一的更新就会被覆盖。
这就是经典的丢失更新问题,英文叫
Lost Update
,又叫提交覆盖,由于是最后执行更新的事务提交致使的覆盖。还有一种更新丢失叫作回滚覆盖,即一个事务的回滚把另外一个事务提交的数据给回滚覆盖了,可是目前市面上全部的数据库都不支持这种stupid的操做,所以再也不详述。
这种状况下,引入咱们常见的两种方式来解决该问题
UPDATE
的WHERE
子句中加入版本号信息来肯定修改是否生效UPDATE
执行前,SELECT
后面加上FOR UPDATE
来给记录加锁,保证记录在UPDATE
前不被修改。SELECT ... FOR UPDATE
是加上了X锁,也能够经过SELECT ... LOCK IN SHARE MODE
加上S锁,来防止其余事务对该行的修改。不管是乐观锁仍是悲观锁,使用的思想都是一致的,那就是当前读。乐观锁利用当前读
判断是不是最新版本,悲观锁利用当前读
锁定行。 可是使用乐观锁时仍然须要很是谨慎,由于RR是可重复读的,必定不能在UPDATE以前先把版本号使用快照读获取出来。