我知道在Mysql中使用锁或者MVCC可以实现并发控制,比如repeatable-reading。但我不知道 MVCC 如何避免幻读。在其他地方了解到一般是通过MVCC和Gap-Lock来实现的,但是我目前的理解是MVCC不需要锁,即更新和删除都是使用undo-logs来实现的。如果是这样,MVCC 和锁机制如何协同工作?
例如,为了避免幻读,MVCC 是否会在 T1 中的某些行上添加间隙锁?如果是这样,当 T2 发生更新时,MVCC 是如何做的,通常只是附加一个更新撤消日志?还是阻止它?
MySQL(特别是 InnoDB)不支持 REPEATABLE-READ 用于锁定语句。例如UPDATE
,DELETE
或SELECT...FOR UPDATE
。这些语句总是锁定最近提交的行版本,就好像事务隔离级别是 READ-COMMITTED。
您可以观察到这种情况:
mysql> create table mytable (id int primary key, x int);
Query OK, 0 rows affected (0.05 sec)
mysql> insert into mytable values (1, 42);
Query OK, 1 row affected (0.02 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from mytable;
+----+------+
| id | x |
+----+------+
| 1 | 42 |
+----+------+
到目前为止,一切都很好。现在打开第二个窗口并更新值:
mysql> update mytable set x = 84;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
现在回到第一个窗口,由于 REPEATABLE-READ,非锁定读取仍然查看原始值,但锁定读取查看最近提交的版本:
mysql> select * from mytable;
+----+------+
| id | x |
+----+------+
| 1 | 42 |
+----+------+
1 row in set (0.00 sec)
mysql> select * from mytable for update;
+----+------+
| id | x |
+----+------+
| 1 | 84 |
+----+------+
1 row in set (0.00 sec)
mysql> select * from mytable;
+----+------+
| id | x |
+----+------+
| 1 | 42 |
+----+------+
1 row in set (0.00 sec)
您可以根据需要来回多次,并且同一个事务可以返回两个值,具体取决于执行锁定读取与非锁定读取。
这是 InnoDB 的一个奇怪行为,但它允许读取不被阻塞。我使用了其他 MVCC 实现,例如 InterBase/Firebird,它们以不同的方式解决了这个问题。它将阻止读取,直到第二个窗口中的事务提交或回滚。如果它回滚,则锁定读取可以读取原始值。如果其他事务提交,则锁定读取会出错。
InnoDB 对如何实现 MVCC 做出了不同的选择,以避免阻塞读取。但这会导致锁定读取必须查看最新提交的行版本的奇怪行为。
正如这首歌所说,“你不能总是得到你想要的。”