Oracle add column的特征

1. 同一事务会看到不同的版本的DD

  1. SQL_1> select * from test;
  2. A
  3. ----------
  4. 1
  5. SQL_2> alter table test add b number;
  6. Table altered.
  7. SQL_1> select * from test;
  8. A B
  9. ---------- ----------
  10. 1

Oracle 不保证事务访问到的DD(data difination)的一致性,而在MySQL中,由于MDL的保护,普通事务是不允许跨DDL的,因此任意普通事务只能看到单一版本的DD。

2. DDL会被写事务堵塞,但后续新的写事务不会被DDL阻塞

  1. SQL_1> insert into test values(1,2);
  2. 1 row created.
  3. SQL_2> alter table test add c number not null;
  4. SQL_3> insert into test values(1,2);
  5. 1 row created.
  6. SQL_3> commit;
  7. Commit complete.

Oracle中,DDL会被写事务阻塞,但此时后续新的写事务不会被该DDL阻塞。而在MySQL中,由于MDL-X锁具有较高的优先级,因此一旦DDL被未提交的事务阻塞,后续该表上的所有新事务都将被阻塞以防止MDL-X锁饥饿。

3. 被阻塞的DDL不会被后续新事务阻塞

  1. SQL_1> insert into test values(1,2);
  2. 1 row created.
  3. SQL_2> alter table test add c number not null;
  4. SQL_3> insert into test values(1,2);
  5. 1 row created.
  6. SQL_1> rollback;
  7. Rollback complete.
  8. Table altered.
  9. SQL_3> commit;
  10. Commit complete.
  11. SQL_1> select * from test;
  12. A B C
  13. ---------- ---------- ----------
  14. 1 2
  15. SQL_1> desc test;
  16. Name Null? Type
  17. ----------------------------------------- -------- ----------------------------
  18. A NUMBER
  19. B NUMBER
  20. C NOT NULL NUMBER

当DDL被事务1阻塞后,新进的事务3正常执行,当事务1回滚后DDL将执行完成,而不会被未提交的事务3阻塞。

但值得注意的是,在该场景下似乎出现了DD和数据不一致的情形,列C有NOT NULL NUMBER约束,但是查询出来的数据列C没有值(该行由事务3写入)。但对该问题我们未做深入探究。

从以上几个特征可以看出,Oracle的add column的约束比MySQL要弱,它不保证DD的事务特性。与DML事务的并发能力比MySQL要强很多,也不会出现MySQL常见的锁表问题。

PolarDB Nonblock add column的能力

1. Nonblock add column不会被未提交的事务阻塞

我们在一个会话中开启一个新的事务,在表t1中插入数据,但不提交。

image-20211223150656311

开启一个新的会话,我们首先查询数据,在RC的隔离级别下,未提交事务写入的数据不可见。然后做add column操作,该操作可以立即完成,而不会被未提交的事务阻塞。

image-20211223150051835

2. 保持DD的事务一致性,跨DDL的事务只能访问到一个版本的DD

回到第一个会话,查看表结构并继续插入数据,此时由于该事务开始时间在add column之前,所以新增的列对其不可见。提交该事务后重新查询,我们将看到最新的表结构。

image-20211223150504858

切换到第二个会话,当第一个会话的事务提交之后,查询将看到最新的已提交数据。

image-20211223150811350

PolarDB Nonblock DDL的基本原理

1. DD多版本技术

说到多版本技术,最为人熟知的就是数据库的MVCC。Innodb MVCC的基本原理是允许每一行数据保持多个历史版本,这些版本通过undo链组织,每个事务在读取数据时都持有一个read view,通过read view判断该事务应该读取哪个版本的数据。通过MVCC,Innodb解决了数据读写相互阻塞的问题。但是,当我们把眼光投向DDL的时候,我们就会发现DD的读写仍然是相互阻塞的:DDL本质上是DD的写操作,而DML/DQL则是DD的读操作。因此为了实现DDL的Nonblock,我们需要在DD中引入MVCC技术,为每一个DD的元素赋予版本信息,允许同时存在DD元素的多个版本,同时为每一个事务绑定一个DD的read view。在访问DD时通过DD的read view判断DD元素的可见性并读取相应的版本。

2. DD元素的多版本存储

在MySQL8.0,所有表的DD信息都存储于Innodb中,以表的形式组织。整个DD的存储体系如下所示

image-20211227112701734

Server层和引擎层的DD信息都来于storage,这个storage就是InnoDB。在DEBUG模式下编译代码,并调用如下SQL语句,我们能看到所有用于存储DD的Innodb表。

  1. set session debug='+d,skip_dd_table_access_check';
  2. select name from mysql.tables where hidden='System' and type='BASE TABLE';
  3. mysql> select name from mysql.tables where hidden='System' and type='BASE TABLE';
  4. +------------------------------+
  5. | name |
  6. +------------------------------+
  7. | dd_properties |
  8. | innodb_dynamic_metadata |
  9. | innodb_ddl_log |
  10. | catalogs |
  11. | character_sets |
  12. | collations |
  13. | column_statistics |
  14. | column_type_elements |
  15. | columns |
  16. | events |
  17. | foreign_key_column_usage |
  18. | foreign_keys |
  19. | index_column_usage |
  20. | index_partitions |
  21. | index_stats |
  22. | indexes |
  23. | parameter_type_elements |
  24. | parameters |
  25. | resource_groups |
  26. | routines |
  27. | schemata |
  28. | st_spatial_reference_systems |
  29. | table_partition_values |
  30. | table_partitions |
  31. | table_stats |
  32. | tables |
  33. | tablespace_files |
  34. | tablespaces |
  35. | triggers |
  36. | view_routine_usage |
  37. | view_table_usage |
  38. +------------------------------+
  39. 31 rows in set (0.02 sec)

从这些表名,我们可以看到,MySQL将DD的信息分门别类的存储在不同的表中,例如columns表存储列的定义信息,indexes表存储索引的定义信息,这些DD的不同子项就是我们所说的DD元素,DD的多版本就是DD中这些元素的多版本。实际上在内部我们曾讨论过多版本信息的粒度问题。

  1. 粗粒度的实现整个表的多版本存储,DD中逻辑上存在table_v1,table_v2等同一个表的不同版本信息。
  2. 细粒度的实现特定元素的多版本存储,对表而言仅有一个版本,但是列/索引等DD元素存储多版本信息。以列信息为例,在该方案下表中的列具有如下图所示的逻辑结构。

image-20211227132640934

虽然细粒度的方案实现起来细节更多,但考虑的工程上的可迭代性,我们仍然选择了方案2。依据该方案的思路,我们率先实现了列信息的多版本:每个表有一个单调递增的唯一版本号,每个列在创建/修改时会从该版本号中分配一个。

3. 事务的DD ReadView

Innodb事务中存储了m_low_limit_id,m_up_limit_id,以及活跃事务列表m_ids等核心信息,用来选择数据的确切版本。因为我们不支持DDL本身的并行,也就是说任意时刻某个表上活跃的DD事务一个,所以DD的read view只需要记录目标表上单调递增的版本号就可以了,因此一个事务的DD ReadView有如下逻辑结构。为了保持MySQL DD语意的一致性,对DML/DQL事务来说,DD的读取总是RR的,当事务第一次访问到某个表时,DD ReadView中就记录了相应的版本,后续直到事务提交,该事务都只能看到该版本的DD信息(对Oracle来说,DD的读取可以认为是RC的,DDL事务提交后,非DDL事务可以立即看到最新的DD)。

image-20211227135401706

4. 一次DDL和DML的并发过程

我们在下文抽象出DDL和DML的事务并发过程,该过程的一个实际例子在前文已有展示。

  1. 1. DML transaction begin;
  2. 1.1 open table t1;
  3. 1.2 build DD ReadView of table t1 (t1:v1)
  4. 1.3 use DD ReadView (t1:v1) to operate Data
  5. 2. DDL transaction begin;
  6. 2.1 operate data
  7. 2.2 operate DD
  8. 2.3 update table version to v2
  9. 2.4 commit
  10. 1.4 operate data with DD ReadView(t1:v1)
  11. 1.5 commit and release DD ReadView
  12. 3. DML transaction begin;
  13. 3.1 open table t1;
  14. 3.2 build DD ReadView of table t1 (t1:v2)
  15. ...

从上述流程中,我们可以看到,非DDL事务在open table时将构建DD ReadView并绑定,随后DDL的执行将不被未提交的事务阻塞,DDL提交后,事务利用ReadView仍然能保证随后看到同一个版本的DD。

Nonblock DDL的后续演进

1. Scope的扩大

PolarDB Nonblock add column已经在内部测试阶段,后续我们将支持更多DD元素的多版本,支持更多的DDL类型,目前在开发计划之内的DDL类型包括modify column type,add index。此外Converting table character set仍在调研阶段,modify column type和Converting a character set是目前为数不多的几个仅支持copy算法的常用DDL,解决这类DDL的并发问题是有非常大的意义的。

2. 限制的放宽

目前Nonblock DDL仅在普通表上得到了支持,在分区表或者含有Trigger/FTS/Foreign key等的表上还无法使用,这些类型的表在MySQL内部实现中关联了多个内部数据结构,具有更复杂的DD操作流程,后续我们将对这些特殊的的表进行适配和支持。