diff --git a/notes/数据库系统原理.md b/notes/数据库系统原理.md index b38cc7e0..4fd25d2a 100644 --- a/notes/数据库系统原理.md +++ b/notes/数据库系统原理.md @@ -288,7 +288,7 @@ C:SELECT * FROM t WHERE id = 2; -- Waiting for table metadata lock A:COMMIT; -- B、C 随后执行完成 ``` -C 只是一条普通的查询,也被阻塞了。这是因为 B 在等待 MDL 写锁,写锁请求的优先级高于读锁,排在 B 后面的读请求都要等待,表上的所有查询看起来就像“锁表”了。MySQL 8.0 中加列默认使用 ALGORITHM=INSTANT,只改元数据、瞬间完成,但同样要先拿到 MDL 写锁。 +C 只是一条普通的查询,也被阻塞了。这是因为 B 在等待 MDL 写锁,写锁请求的优先级高于读锁,排在 B 后面的读请求都要等待,表上的所有查询看起来就像“锁表”了。MySQL 8.0.12 起加列默认使用 ALGORITHM=INSTANT(8.0.29 之前只能加在最后一列),只改元数据、瞬间完成,但同样要先拿到 MDL 写锁。 所以改表前先在 information_schema.innodb_trx 中检查有没有长事务,并给 DDL 设置较短的 lock_wait_timeout(默认是一年)。上例中 B 设置 lock_wait_timeout = 3 后,等待 3 秒报错退出,C 随即执行完成,ALTER 可以稍后重试。 @@ -339,7 +339,7 @@ InnoDB 的实现方式是在显式开始的事务中(BEGIN,或者关闭了 | 可重复读 | 不可能 | 不可能 | 可能 | | 可串行化 | 不可能 | 不可能 | 不可能 | -这是 SQL 标准的规定。MySQL 的 InnoDB 默认使用可重复读,并且用 MVCC 和 Next-Key Locks 在很大程度上避免了幻读,见 [六、Next-Key Locks](#六next-key-locks);Oracle 和 PostgreSQL 默认使用提交读。 +这是 SQL 标准的规定。MySQL 的 InnoDB 默认使用可重复读,并且用 [MVCC](#五多版本并发控制) 和 Next-Key Locks 在很大程度上避免了幻读,见 [六、Next-Key Locks](#六next-key-locks);Oracle 和 PostgreSQL 默认使用提交读。 MySQL 默认使用可重复读与 binlog 有关:提交读不加间隙锁,按语句重放时从库可能得到与主库不同的结果,所以 InnoDB 的提交读只能按行记录 binlog;而 MySQL 5.1.4 及以前,binlog 只有记录语句的 STATEMENT 格式。 @@ -451,11 +451,11 @@ Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。 在可重复读(REPEATABLE READ)隔离级别下,MVCC 使普通的 SELECT(快照读)不会读到其它事务新插入的行,但是 UPDATE、DELETE、SELECT ... FOR UPDATE 等当前读仍然可能出现幻读,Next-Key Locks 锁住查询范围内的间隙,阻止其它事务在范围内插入,从而解决这个问题。 -需要注意的是,这要求一个事务中的读取全部是快照读或者全部是当前读,两者混用时仍然会出现幻读。例如表 acct 原有 id 为 1、2 的两行,balance 都是 100(MySQL 8.0 实测,见[附录 A4](#a4-快照读与当前读混用)): +需要注意的是,这要求一个事务中的读取全部是快照读或者全部是当前读,两者混用时仍然可能出现幻读。例如表 acct 原有 id 为 1、2 的两行,balance 都是 100(MySQL 8.0 实测,见[附录 A4](#a4-快照读与当前读混用)):

-事务 A 的 UPDATE 是当前读,看到了事务 B 新插入并已提交的第 3 行,并且修改了它;这一行最新版本的事务 ID 变成了 A 自己,所以之后的快照读也能看到它。如果 A 一开始就用 SELECT ... FOR UPDATE 加锁读取,B 的插入会被 Next-Key Locks 阻塞,直到 A 结束或者锁等待超时。 +事务 A 的 UPDATE 是当前读,看到了事务 B 新插入并已提交的第 3 行,并且修改了它;这一行最新版本的事务 ID 变成了 A 自己,所以之后的快照读也能看到它。如果 A 只执行 SELECT ... FOR UPDATE、不修改,它会返回第 3 行,但之后的快照读仍只有 1、2 两行,快照读能看到第 3 行是因为 UPDATE 修改了它。如果 A 一开始就用 SELECT ... FOR UPDATE 加锁读取,B 的插入会被 Next-Key Locks 阻塞,直到 A 结束或者锁等待超时。 ### Record Locks @@ -495,6 +495,8 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; 按 c = 11 查询时,c 不是唯一索引,后面还可能有 c = 11 的记录,所以除了 (10, 11] 的 Next-Key Lock,还要锁住到下一条记录 13 之间的间隙;如果 c 是唯一索引并且 c = 11 这条记录存在,就只锁这一条记录;查询不存在的 c = 12 时,仍会在 (11, 13) 上加间隙锁,阻止插入 12。 +需要注意的是,二级索引的记录按(c, 主键)排序,间隙的边界也是这样的二元组,所以插入 c 等于边界值的行会不会被阻塞,要看主键落在哪一侧。上面按 c = 11 加锁后,锁住的范围是 (10, id 1) 到 (13, id 3) 之间:插入 c = 10、id = 5 的行会阻塞,c = 10、id = 0 的行不会;插入 c = 13、id = 0 的行会阻塞,c = 13、id = 5 的行不会(见[附录 A5](#a5-next-key-locks-的加锁范围))。 + 当前读的条件列没有索引时,InnoDB 只能扫描整个聚簇索引,扫描过的每一行都要加锁。例如表 t_nl 有 5 行,d 列没有索引,执行 UPDATE t_nl SET d = d + 1 WHERE d = 30 只修改 1 行:在可重复读下锁住了全部 5 行和最后的间隙,其它事务插入新行、修改 id 为 5 的行都要等待,效果相当于锁住了整张表;在提交读下只锁住 1 行,插入和修改都不受影响(MySQL 8.0 实测,见[附录 A6](#a6-没有索引时的加锁))。所以 UPDATE、DELETE 的条件列要建索引。 ## 七、关系数据库设计理论 @@ -753,6 +755,15 @@ A: SELECT * FROM acct WHERE balance >= 100 -> 1, 2, 3(ba A 先执行 SELECT ... FROM acct WHERE balance >= 100 FOR UPDATE 时,B 的 INSERT 在 innodb_lock_wait_timeout = 1 下报错 ERROR 1205。 +只加锁读、不修改时(运行 3 次结果相同): + +```text +A: SELECT id FROM acct -> 1, 2 +B: INSERT INTO acct VALUES (3, 100) (自动提交) +A: SELECT id FROM acct FOR UPDATE -> 1, 2, 3 +A: SELECT id FROM acct -> 1, 2 +``` + 当前读基于最新版本计算:表 account 中 id = 1 的 balance 为 100,A 为 REPEATABLE READ: ```text @@ -798,6 +809,15 @@ SELECT * FROM t WHERE c BETWEEN 10 AND 20 FOR UPDATE 查询不存在的 c = 12 FOR UPDATE 时,行锁只有 uk_c X,GAP 13, 3,插入 c = 12 阻塞。 ``` +按本节开头重新建表(普通索引 idx_c)并插入初始数据,A 执行 c = 11 FOR UPDATE 后不提交,B 分别插入 c 等于边界值 10、13 而主键不同的行,每次尝试后回滚(innodb_lock_wait_timeout = 1): + +```text +插入 (id = 0, c = 10):成功 +插入 (id = 5, c = 10):阻塞(1205) +插入 (id = 0, c = 13):阻塞(1205) +插入 (id = 5, c = 13):成功 +``` + ### A6 没有索引时的加锁 ```sql