在MySQL 8.0以前的版本中,因为架构的缘由,MySQL在server层使用统一的frm文件来存储表元数据信息,这个信息可以被不一样的存储引擎识别。而实际上InnoDB自己也存储有元数据信息。
这给ddl带来了必定的挑战,由于这种架构没法作到ddl的原子化,咱们在线上常常可以看到数据目录下遗留的临时文件,或者相似server层和innodb层列个数不一致之类的错误。甚至某些ddl可能还遗留元数据在innodb内,而丢失了frm,致使没法重建表…..(咱们为了解决这个问题,实现了一个叫drop table force的功能,去强制作清理….)html
(如下全部的讨论都假定使用InnoDB存储引擎)mysql
到了8.0版本,咱们知道全部的元数据已经统一用InnoDB来进行管理,这就给实现原子ddl带来了可能,几乎全部的对innodb表,存储过程,触发器,视图或者UDF的操做,都能作到原子化:laravel
- 元数据修改,binlog以及innodb的操做都放在一个事务中 - 增长了一个内部隐藏的系统表`mysql.innodb_ddl_log`,ddl操做被记录到这个表中,注意对该表的操做产生的redo会fsync到磁盘上,而不会考虑innodb_flush_log_at_trx_commit的配置。当崩溃重启时,会根据事务是否提交来决定经过这张表的记录去回滚或者执行ddl操做 - 增长了一个post-ddl的阶段,这也是ddl的最后一个阶段,会去:1. 真正的物理删除或重命名文件; 2. 删除innodb_ddl_log中的记录项; 3.对于一些ddl操做还会去更新其动态元数据信息(存储在`mysql.innodb_dynamic_metadata`,例如corrupt flag, auto_inc值等) - 一个正常运行的ddl结束后,其ddl log也应该被清理,若是这中间崩溃了,重启时会去尝试重放:1.若是已经走到最后一个ddl阶段的(commit以后),就replay ddl log,把ddl完成掉;2. 若是处于某个中间态,则回滚ddl
因为引入了atomic ddl, 有些ddl操做的行为也发生了变化:sql
- DROP TABLE: 在以前的版本中,一个drop table语句中若是要删多个表,好比t1,t2, t2不存在时,t1会被删除。但在8.0中,t1和t2都不会被删除,而是抛出错误。所以要注意5.7->8.0的复制问题 (DROP VIEW, CREATE USER也有相似的问题) - DROP DATABASE: 修改元数据和ddl_log先提交事务,而真正的物理删除数据文件放在最后,所以若是在删除文件时崩溃,重启时会根据ddl_log继续执行drop database
MySQL很贴心的加了一个选项shell
innodb_print_ddl_logs
,打开后咱们能够从错误日志看到对应的ddl log,下面咱们经过这个来看下一些典型ddl的过程数据库
root@(none) 11:12:19>SET GLOBAL innodb_print_ddl_logs = 1; Query OK, 0 rows affected (0.00 sec) root@(none) 11:12:22>SET GLOBAL log_error_verbosity = 3; Query OK, 0 rows affected (0.00 sec)
mysql> CREATE DATABASE test; Query OK, 1 row affected (0.02 sec)
建立数据库语句没有写log_ddl,可能以为这不是高频操做,若是建立database的过程当中失败了,重启后可能须要手动删除目录。数组
mysql> USE test; Database changed mysql> CREATE TABLE t1 (a INT PRIMARY KEY, b INT); Query OK, 0 rows affected (0.06 sec) [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=428, thread_id=7, space_id=76, old_file_path=./test/t1.ibd] [InnoDB] DDL log delete : by id 428 [InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=429, thread_id=7, table_id=1102, new_file_path=test/t1] [InnoDB] DDL log delete : by id 429 [InnoDB] DDL log insert : [DDL record: FREE, id=430, thread_id=7, space_id=76, index_id=190, page_no=4] [InnoDB] DDL log delete : by id 430 [InnoDB] DDL log post ddl : begin for thread id : 7 InnoDB] DDL log post ddl : end for thread id : 7
从日志来看有三类操做,实际上描述了若是操做失败须要进行的三项逆向操做:删除数据文件,释放内存中的数据词典信息,删除索引btree。在建立表以前,这些数据被写入到ddl_log中,在建立完表并commit后,再从ddl log中删除这些记录。服务器
另外上述日志中还有DDL log delete
日志,其实在每次写入ddl log时是单独事务提交的,但在提交以后,会使用当前事务执行一条delete操做,直到操做结束了才会提交。架构
mysql> ALTER TABLE t1 ADD COLUMN c INT; Query OK, 0 rows affected (0.08 sec) Records: 0 Duplicates: 0 Warnings: 0 [InnoDB] DDL log post ddl : begin for thread id : 7 [InnoDB] DDL log post ddl : end for thread id : 7
注意这里执行的是Instant ddl, 这是8.0.13新支持的特性,加列操做能够只修改元数据,所以从ddl log中无需记录数据并发
mysql> ALTER TABLE t1 DROP COLUMN c; Query OK, 0 rows affected (2.77 sec) Records: 0 Duplicates: 0 Warnings: 0 [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=487, thread_id=7, space_id=83, old_file_path=./test/#sql-ib1108-1917598001.ibd] [InnoDB] DDL log delete : by id 487 [InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=488, thread_id=7, table_id=1109, new_file_path=test/#sql-ib1108-1917598001] [InnoDB] DDL log delete : by id 488 [InnoDB] DDL log insert : [DDL record: FREE, id=489, thread_id=7, space_id=83, index_id=200, page_no=4] [InnoDB] DDL log delete : by id 489 [InnoDB] DDL log insert : [DDL record: DROP, id=490, thread_id=7, table_id=1108] [InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=491, thread_id=7, space_id=82, old_file_path=./test/#sql-ib1109-1917598002.ibd, new_file_path=./test/t1.ibd] [InnoDB] DDL log delete : by id 491 [InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=492, thread_id=7, table_id=1108, old_file_path=test/#sql-ib1109-1917598002, new_file_path=test/t1] [InnoDB] DDL log delete : by id 492 [InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=493, thread_id=7, space_id=83, old_file_path=./test/t1.ibd, new_file_path=./test/#sql-ib1108-1917598001.ibd] [InnoDB] DDL log delete : by id 493 [InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=494, thread_id=7, table_id=1109, old_file_path=test/t1, new_file_path=test/#sql-ib1108-1917598001] [InnoDB] DDL log delete : by id 494 [InnoDB] DDL log insert : [DDL record: DROP, id=495, thread_id=7, table_id=1108] [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=496, thread_id=7, space_id=82, old_file_path=./test/#sql-ib1109-1917598002.ibd] [InnoDB] DDL log post ddl : begin for thread id : 7 [InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=496, thread_id=7, space_id=82, old_file_path=./test/#sql-ib1109-1917598002.ibd] [InnoDB] DDL log replay : [DDL record: DROP, id=495, thread_id=7, table_id=1108] [InnoDB] DDL log replay : [DDL record: DROP, id=490, thread_id=7, table_id=1108] [InnoDB] DDL log post ddl : end for thread id : 7
这是个典型的三阶段ddl的过程:分为prepare, perform 以及commit三个阶段:
DROP : table_id=1108
RENAME SPACE: #sql-ib1109-1917598002.ibd文件被rename成t1.ibd
RENAME TABLE: #sql-ib1109-1917598002被rename成t1
RENAME SPACE: t1.ibd 被rename成#sql-ib1108-1917598001.ibd
RENAME TABLE: t1表被rename成#sql-ib1108-1917598001
DROP TABLE: table_id=1108
DELETE SPACE: 删除#sql-ib1109-1917598002.ibd
实际上这一步写的ddl log描述了commit阶段操做的逆向过程:将t1.ibd rename成#sql-ib1109-1917598002, 并将sql-ib1108-1917598001 rename成t1表,最后删除旧表。其中删除旧表的操做这里不执行,而是到post-ddl阶段执行
Post-ddl: 在事务提交后,执行最后的操做:replay ddl log, 删除旧文件,清理mysql.innodb_dynamic_metadata中相关信息
DELETE SPACE:
DROP: table_id=1108
DROP: table_id=1108
mysql> ALTER TABLE t1 ADD KEY(b); Query OK, 0 rows affected (0.14 sec) Records: 0 Duplicates: 0 Warnings: 0 [InnoDB] DDL log insert : [DDL record: FREE, id=431, thread_id=7, space_id=76, index_id=191, page_no=5] [InnoDB] DDL log delete : by id 431 [InnoDB] DDL log post ddl : begin for thread id : 7 [InnoDB] DDL log post ddl : end for thread id : 7
建立索引采用inplace建立的方式,没有临时文件,但若是异常发生的话,依然须要在发生异常时清理临时索引, 所以增长了一条FREE log,用于异常发生时可以删除临时索引。
mysql> TRUNCATE TABLE t1; Query OK, 0 rows affected (0.13 sec) [InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=439, thread_id=7, space_id=77, old_file_path=./test/#sql-ib1103-1917597994.ibd, new_file_path=./test/t1.ibd] [InnoDB] DDL log delete : by id 439 [InnoDB] DDL log insert : [DDL record: DROP, id=440, thread_id=7, table_id=1103] [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=441, thread_id=7, space_id=77, old_file_path=./test/#sql-ib1103-1917597994.ibd] [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=442, thread_id=7, space_id=78, old_file_path=./test/t1.ibd] [InnoDB] DDL log delete : by id 442 [InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=443, thread_id=7, table_id=1104, new_file_path=test/t1] [InnoDB] DDL log delete : by id 443 [InnoDB] DDL log insert : [DDL record: FREE, id=444, thread_id=7, space_id=78, index_id=194, page_no=4] [InnoDB] DDL log delete : by id 444 [InnoDB] DDL log insert : [DDL record: FREE, id=445, thread_id=7, space_id=78, index_id=195, page_no=5] [InnoDB] DDL log delete : by id 445 [InnoDB] DDL log post ddl : begin for thread id : 7 [InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=441, thread_id=7, space_id=77, old_file_path=./test/#sql-ib1103-1917597994.ibd] [InnoDB] DDL log replay : [DDL record: DROP, id=440, thread_id=7, table_id=1103] [InnoDB] DDL log post ddl : end for thread id : 7
Truncate table是个比较有意思的话题,在早期5.6及以前的版本中, 是经过删除旧表建立新表的方式来进行的,5.7以后为了保证原子性,改为了原地truncate文件,同时增长了一个truncate log文件,若是在truncate过程当中崩溃,能够经过这个文件在崩溃恢复时从新truncate。
到了8.0版本,又恢复成了删除旧表,建立新表的方式,与以前不一样的是,8.0版本在崩溃时能够回滚到旧数据,而不是再次执行。以上述为例,主要包括几个步骤:
mysql> RENAME TABLE t1 TO t2; Query OK, 0 rows affected (0.06 sec)
DDL LOG:
[InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=450, thread_id=7, space_id=78, old_file_path=./test/t2.ibd, new_file_path=./test/t1.ibd] [InnoDB] DDL log delete : by id 450 [InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=451, thread_id=7, table_id=1104, old_file_path=test/t2, new_file_path=test/t1] [InnoDB] DDL log delete : by id 451 [InnoDB] DDL log post ddl : begin for thread id : 7 [InnoDB] DDL log post ddl : end for thread id : 7
这个就比较简单了,只须要记录rename space 和rename table的逆操做便可. post-ddl不须要作实际的操做
DROP TABLE t2
[InnoDB] DDL log insert : [DDL record: DROP, id=595, thread_id=7, table_id=1119] [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=596, thread_id=7, space_id=93, old_file_path=./test/t2.ibd] [InnoDB] DDL log post ddl : begin for thread id : 7 [InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=596, thread_id=7, space_id=93, old_file_path=./test/t2.ibd] [InnoDB] DDL log replay : [DDL record: DROP, id=595, thread_id=7, table_id=1119] [InnoDB] DDL log post ddl : end for thread id : 7
先在ddl log中记录下须要删除的数据,再提交后,再最后post-ddl阶段执行真正的删除表对象和文件操做
主要实现代码集中在文件storage/innobase/log/log0ddl.cc中,包含了向log_ddl表中插入记录以及replay的逻辑。
隐藏的innodb_log_ddl表结构以下
def->add_field(0, "id", "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT"); def->add_field(1, "thread_id", "thread_id BIGINT UNSIGNED NOT NULL"); def->add_field(2, "type", "type INT UNSIGNED NOT NULL"); def->add_field(3, "space_id", "space_id INT UNSIGNED"); def->add_field(4, "page_no", "page_no INT UNSIGNED"); def->add_field(5, "index_id", "index_id BIGINT UNSIGNED"); def->add_field(6, "table_id", "table_id BIGINT UNSIGNED"); def->add_field(7, "old_file_path", "old_file_path VARCHAR(512) COLLATE UTF8_BIN"); def->add_field(8, "new_file_path", "new_file_path VARCHAR(512) COLLATE UTF8_BIN"); def->add_index(0, "index_pk", "PRIMARY KEY(id)"); def->add_index(1, "index_k_thread_id", "KEY(thread_id)");
根据不一样的操做类型,能够分为以下几类:
FREE_TREE_LOG
目的是释放索引btree,入口函数:
log_DDL::write_free_tree_log
,在建立索引和删除表时会调用到。
对于drop table中涉及的删索引操做,log ddl的插入操做放到父事务中,一块儿要么提交要么回滚
对于建立索引的case, log ddl就须要单独提交,父事务将记录标记删除,这样后面若是ddl回滚了,也能将残留的index删掉。
DELETE_SPACE_LOG
入口函数:
Log_DDL::write_delete_space_log
用于记录删除tablespace操做,一样分为两种状况:
入口函数:
Log_DDL::write_rename_space_log
用于记录rename操做,例如若是咱们把表t1 rename成t2,在其中就记录了逆向操做t2 rename to t1。
在函数Fil_shard::space_rename()
中,老是先写ddl log, 再作真正的rename操做. 写日志的过程一样是独立事务提交,父事务作未提交的删除操做
DROP_LOG
入口函数: Log_DDL::write_drop_log
用于记录删除表对象操做,这里不涉及文件层操做,写ddl log在父事务中执行
RENAME_TABLE_LOG
入口函数:
Log_DDL::write_rename_table_log
用于记录rename table对象的逆操做,和rename space相似,也是独立事务提交ddl log, 父事务标记删除
REMOVE_CACHE_LOG
入口函数:
Log_DDL::write_remove_cache_log
用于处理内存表对象的清理,独立事务提交,父事务标记删除
ALTER_ENCRYPT_TABLESPACE_LOG
入口函数:
Log_DDL::write_alter_encrypt_space_log
用于记录对tablespace加密属性的修改,独立事务提交. 在写完ddl log后修改tablespace page0 中的加密标记
综上,在ddl的过程当中可能会提交屡次事务,大概分为三类:
post_ddl
如上所述,有些ddl log是随着父事务一块儿提交的,有些则在post-ddl阶段再执行, post_ddl发生在父事提交或回滚以后: 若事务回滚,根据ddl log作逆操做,若事务提交,在post-ddl阶段作最后真正不可逆操做(例如删除文件)
入口函数: Log_DDL::post_ddl -->Log_DDL::replay_by_thread_id
根据执行ddl的线程thread id经过innodb_log_ddl表上的二级索引,找到log id,再到汇集索引上找到其对应的记录项,而后再replay这些操做,完成ddl后,清理对应记录
在崩溃恢复结束后,会调用ha_post_recover
接口函数,进而调用innodb内的函数Log_DDL::recover()
, 一样的replay其中的记录,并在结束后删除记录。但ALTER_ENCRYPT_TABLESPACE_LOG类型并非在这一步删除,而是加入到一个数组ts_encrypt_ddl_records中,在以后调用resume_alter_encrypt_tablespace
来恢复操做。
参考文档
好了各位,以上就是这篇文章的所有内容了,能看到这里的人呀,都是人才。以前说过,PHP方面的技术点不少,也是由于太多了,实在是写不过来,写过来了你们也不会看的太多,因此我这里把它整理成了PDF和文档,若是有须要的能够
更多学习内容能够访问【对标大厂】精品PHP架构师教程目录大全,只要你能看完保证薪资上升一个台阶(持续更新)
以上内容但愿帮助到你们,不少PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提高,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货须要的能够免费分享给你们,须要的能够加入个人PHP技术交流群953224940