14.3.4 Phantom Rows 幻影行 所谓的幻读为发生在一个事务 当相同的查询产生不同的结果集在不同的时间。 比如,如果一个SELECT被执行2次, 但是第2次返回的记录不是第一次返回的记录, 行是幻行 假设在child 表的id列上有一个索引 ,你需要读取和锁定所有的行 值大于100的, 以便更新选择行的一些列: SELECT * FROM child WHERE id > 100 FOR UPDATE; 查询扫描索引从第一个记录id大于100开始, 让表包含记录90和102. 如果锁设置在index records 在扫描的范围 不堵塞插入 在区间上(在这个例子里,区间是90和102) 另外一个会话可以插入新值到表 id值为101. 如果你执行相同的查询 在相同的session,你会看到新的值id=101(一个幻读) 在查询返回的结果。 如果我们将一组行作为数据项, 新的幻读的child 会违背事务隔离原理, 一个事务应该在事务期间所读取的数据不会改变。 mysql> show variables like '%tx_isolation%'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | REPEATABLE-READ | +---------------+-----------------+ 1 row in set (0.01 sec) CREATE TABLE `child` ( `sn` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) DEFAULT NULL, `info` varchar(40) DEFAULT NULL, PRIMARY KEY (`sn`) ); mysql> show variables like '%tx_isolation%'; +---------------+----------------+ | Variable_name | Value | +---------------+----------------+ | tx_isolation | READ-COMMITTED | +---------------+----------------+ 1 row in set (0.00 sec) Session 1: mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE; Empty set (0.00 sec) Session 2: mysql> select * from child; +----+------+------+ | sn | id | info | +----+------+------+ | 1 | 1 | a1 | | 2 | 99 | a99 | +----+------+------+ 2 rows in set (0.00 sec) mysql> insert into child(id,info) values(101,'a101'); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.01 sec) 继续测试: Session 1: mysql> show variables like '%tx_isolation%'; +---------------+----------------+ | Variable_name | Value | +---------------+----------------+ | tx_isolation | READ-COMMITTED | +---------------+----------------+ 1 row in set (0.00 sec) mysql> SELECT * FROM child; +----+------+------+ | sn | id | info | +----+------+------+ | 1 | 1 | a1 | | 2 | 99 | a99 | | 3 | 101 | a101 | +----+------+------+ 3 rows in set (0.00 sec) mysql> insert into child(id,info) values(110,'a110'); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.01 sec) mysql> select * from child; +----+------+------+ | sn | id | info | +----+------+------+ | 1 | 1 | a1 | | 2 | 99 | a99 | | 3 | 101 | a101 | | 4 | 110 | a110 | +----+------+------+ 4 rows in set (0.00 sec) mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE; +----+------+------+ | sn | id | info | +----+------+------+ | 3 | 101 | a101 | | 4 | 110 | a110 | +----+------+------+ 2 rows in set (0.00 sec) Session 2: mysql> select * from child; +----+------+------+ | sn | id | info | +----+------+------+ | 1 | 1 | a1 | | 2 | 99 | a99 | | 3 | 101 | a101 | | 4 | 110 | a110 | +----+------+------+ 4 rows in set (0.00 sec) mysql> insert into child (id,info) values(105,'a105'); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.01 sec) mysql> select * from child; +----+------+------+ | sn | id | info | +----+------+------+ | 1 | 1 | a1 | | 2 | 99 | a99 | | 3 | 101 | a101 | | 4 | 110 | a110 | | 5 | 105 | a105 | +----+------+------+ 5 rows in set (0.00 sec) 为了防止幻读,InnoDB使用一个算法叫做 next-key locking 由 index-row locking和gap locking 组成。 InnoDB 执行 row-level locking 以这种方式 当它搜索或者扫描表的索引, 它设置共享锁或者排它锁 在它遇到的index records上. 因此, row-level locks 实际上是 index-record locks. 此外, a next-key lock 在一个Index record 也影响了 “gap” before that index record. 也就是说,一个next-key lock 是一个index record lock 加上一个gap lock 在index record 之前的区间。 如果一个会话有一个共享锁或者排它锁在记录R上 在一个index里, 另外的会话不能立即插入到一个新的index record 在一个gap (在记录R之前) 当InnoDB 扫描一个Index, 它也可以lock gap 在最后一条记录后面 在index里, 比如之前的例子,防止任何插入到表 id值大于100的数据, InnoDB设置锁包含一个lock区间 在id=102以后的区间 测试: Session 1: mysql> show variables like '%tx_isolation%'; +---------------+-----------------+ | Variable_name | Value | S+---------------+-----------------+ | tx_isolation | REPEATABLE-READ | +---------------+-----------------+ 1 row in set (0.00 sec) Database changed mysql> select * from child; +----+------+------+ | sn | id | info | +----+------+------+ | 1 | 90 | a90 | | 2 | 102 | a102 | +----+------+------+ 2 rows in set (0.00 sec) mysql> select * from child where id>100 for update; +----+------+------+ | sn | id | info | +----+------+------+ | 2 | 102 | a102 | +----+------+------+ 1 row in set (0.00 sec) mysql> show index from child; +-------+------------+------------+--------------+-------------+-----------+------------- +----------+--------+------+------------+---------+---------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | +-------+------------+------------+--------------+-------------+-----------+------------- +----------+--------+------+------------+---------+---------------+ | child | 0 | PRIMARY | 1 | sn | A | 4 | NULL | NULL | | BTREE | | | | child | 0 | child_idx1 | 1 | id | A | 4 | NULL | NULL | YES | BTREE | | | +-------+------------+------------+--------------+-------------+-----------+------------- +----------+--------+------+------------+---------+---------------+ 2 rows in set (0.00 sec) Session 2: mysql> insert into child(id,info) values(91,'a91'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(90,'a90'); ERROR 1062 (23000): Duplicate entry '90' for key 'child_idx1' mysql> insert into child(id,info) values(89,'a89'); Query OK, 1 row affected (0.00 sec) mysql> rollback; Query OK, 0 rows affected (0.01 sec) mysql> insert into child(id,info) values(88,'a88'); Query OK, 1 row affected (0.00 sec) mysql> rollback; Query OK, 0 rows affected (0.00 sec) mysql> insert into child(id,info) values(92,'a92'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(93,'a93'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(94,'a94'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(95,'a95'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(96,'a96'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(97,'a97'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(98,'a98'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(99,'a99'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(100,'a100'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(101,'a101'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(102,'a102'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(103,'a103'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into child(id,info) values(104,'a104'); x∈[3,4]表示3≤x≤4 因为两端有等号,所以叫闭区间 x∈(3,4)表示3<x<4 因为两端没等号,所以叫开区间 锁了(90,无穷) 你可以使用 next-key locking 来实现一个唯一性检查 在你的应用里: 如果你读取你的数据 在共享模式,没有看到重复的 对于一条记录准备被插入, 然后你可以安全的插入你的记录,知道 next-key lock 设置在你的行的继承者 在读取阻止任何 同时插入一个重复的记录。因此,the next-key locking 让你可以lock 你表中不存在的东西