问题背景
在MySQL 8.0上偶然发现一个比较奇怪的问题,在使用alter table tab auto_increment=N修改表的自增初始值时,information_schema.tables这个表的auto_increment列并没有同步更新;
问题复现
8.0版本
- root@localhost:mysql8006.sock [wjq]>show create table replace_uniq1\G;
- *************************** 1. row ***************************
- Table: replace_uniq1
- Create Table: CREATE TABLE `replace_uniq1` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `content` varchar(10) DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
- 1 row in set (0.00 sec)
- root@localhost:mysql8006.sock [wjq]>select * from replace_uniq1;
- +----+---------+
- | id | content |
- +----+---------+
- | 1 | aa |
- | 2 | b |
- | 3 | c |
- | 4 | d |
- | 5 | ee |
- +----+---------+
- 5 rows in set (0.00 sec)
- root@localhost:mysql8006.sock [wjq]>select auto_increment from information_schema.tables where table_schema='wjq' and table_name='replace_uniq1';
- +----------------+
- | AUTO_INCREMENT |
- +----------------+
- | 6 |
- +----------------+
- 1 row in set (0.00 sec)
- root@localhost:mysql8006.sock [wjq]>alter table replace_uniq1 auto_increment=10;
- Query OK, 0 rows affected (0.02 sec)
- Records: 0 Duplicates: 0 Warnings: 0
- root@localhost:mysql8006.sock [wjq]>select auto_increment from information_schema.tables where table_schema='wjq' and table_name='replace_uniq1';
- +----------------+
- | AUTO_INCREMENT |
- +----------------+
- | 6 |
- +----------------+
- 1 row in set (0.00 sec)
首先想到的是,数据在buffer中没提交?于是重启一下MySQL,auto_increment列仍是没有变化
- root@localhost:mysql8006.sock [wjq]>select auto_increment from information_schema.tables where table_schema='wjq' and table_name='replace_uniq1';
- ERROR 2006 (HY000): MySQL server has gone away
- No connection. Trying to reconnect...
- ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql8006.sock' (2)
- ERROR:
- Can't connect to the server
- root@not_connected:not_connected [wjq]>select auto_increment from information_schema.tables where table_schema='wjq' and table_name='replace_uniq1';
- No connection. Trying to reconnect...
- Connection id: 8
- Current database: wjq
- +----------------+
- | AUTO_INCREMENT |
- +----------------+
- | 6 |
- +----------------+
- 1 row in set (0.00 sec)
auto_increment还是没有更新为10,难道是MySQL8.0把这个数据记录到其他系统表了?在information_schema,sys,mysql等schema里没发现什么。
于是跑去MySQL5.7版本的去测试了一下,发现当用alter table修改了auto_increment,tables表的auto_increment列是会同步更新的。奇怪了,难道是BUG?
5.7版本实例
- root@localhost:mysql3306.sock [wjq]>show create table replace_uniq1\G;
- *************************** 1. row ***************************
- Table: replace_uniq1
- Create Table: CREATE TABLE `replace_uniq1` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `content` varchar(10) DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4
- 1 row in set (0.00 sec)
- root@localhost:mysql3306.sock [wjq]>select * from replace_uniq1;
- +----+---------+
- | id | content |
- +----+---------+
- | 1 | aa |
- | 2 | b |
- | 3 | c |
- | 4 | d |
- | 5 | ee |
- +----+---------+
- 5 rows in set (0.00 sec)
- root@localhost:mysql3306.sock [wjq]>select auto_increment from information_schema.tables where table_schema='wjq' and table_name='replace_uniq1';
- +----------------+
- | auto_increment |
- +----------------+
- | 6 |
- +----------------+
- 1 row in set (0.00 sec)
- root@localhost:mysql3306.sock [wjq]>alter table replace_uniq1 auto_increment=10;
- Query OK, 0 rows affected (0.01 sec)
- Records: 0 Duplicates: 0 Warnings: 0
- root@localhost:mysql3306.sock [wjq]>select auto_increment from information_schema.tables where table_schema='wjq' and table_name='replace_uniq1';
- +----------------+
- | auto_increment |
- +----------------+
- | 10 |
- +----------------+
- 1 row in set (0.00 sec)
- root@localhost:mysql3306.sock [wjq]>select * from replace_uniq1;
- +----+---------+
- | id | content |
- +----+---------+
- | 1 | aa |
- | 2 | b |
- | 3 | c |
- | 4 | d |
- | 5 | ee |
- +----+---------+
- 5 rows in set (0.00 sec)
- root@localhost:mysql3306.sock [wjq]>insert into replace_uniq1(content) values('f');
- Query OK, 1 row affected (0.01 sec)
- root@localhost:mysql3306.sock [wjq]>select * from replace_uniq1;
- +----+---------+
- | id | content |
- +----+---------+
- | 1 | aa |
- | 2 | b |
- | 3 | c |
- | 4 | d |
- | 5 | ee |
- | 10 | f |
- +----+---------+
- 6 rows in set (0.00 sec)
一番折腾之后,发现MySQL 8.0对自增列出了如下的优化:
在5.7及以前,alter table修改的auto_increment值确实是记录到tables表的auto_increment列的,但是有一个缺点是,tables表的引擎是memory,也就是说,如果MySQL重启了,这个表记录的auto_increment值就丢失了。例如表当前的auto_increment值是30,使用alter table将其修改为50,那么tables表的auto_increment列确实是记录显示了50,但如果表没有插入任何数据,并重启了MySQL(不管是正常还是异常重启),下一次运行时,表的auto_increment属性就会回退到30,同时tables表的auto_increment列显示的也是30。
那么既然tables是memory引擎表,重启之后理应没有数据才对,为什么auto_increment列还显示30呢?
原因是MySQL重启后,通过select max(col) from tab for update的方式,来选出当前的最大值并赋值给auto_increment。
那么到了8.0以后,有什么变化呢?
将自增主键的计数器持久化到redo log中。每次计数器发生改变,都会将其写入到redo log中。如果数据库发生重启,InnoDB会根据redo log中的计数器信息来初始化其内存值。为了尽量减小对系统性能的影响,计数器写入到redo log中,并不会马上刷新。
除了redo log,在系统表中也会记录auto_increment的信息,例如mysql.tables表。那么,在插入新行时,递增列应该参考redo log还是系统表呢?
而且从上面的例子可以看到,tables记录根本就不准确。其实答案就一句话,哪个是最新的,就用哪个。
那么回到系统表的问题上,既然要在重启后提供参考,总得靠谱一点吧?看一下8.0之前和之后版本对mysql.tables表定义:
5.7版本
- root@localhost:mysql3306.sock [wjq]>show create table information_schema.tables\G;
- *************************** 1. row ***************************
- Table: TABLES
- Create Table: CREATE TEMPORARY TABLE `TABLES` (
- `TABLE_CATALOG` varchar(512) NOT NULL DEFAULT '',
- `TABLE_SCHEMA` varchar(64) NOT NULL DEFAULT '',
- `TABLE_NAME` varchar(64) NOT NULL DEFAULT '',
- `TABLE_TYPE` varchar(64) NOT NULL DEFAULT '',
- `ENGINE` varchar(64) DEFAULT NULL,
- `VERSION` bigint(21) unsigned DEFAULT NULL,
- `ROW_FORMAT` varchar(10) DEFAULT NULL,
- `TABLE_ROWS` bigint(21) unsigned DEFAULT NULL,
- `AVG_ROW_LENGTH` bigint(21) unsigned DEFAULT NULL,
- `DATA_LENGTH` bigint(21) unsigned DEFAULT NULL,
- `MAX_DATA_LENGTH` bigint(21) unsigned DEFAULT NULL,
- `INDEX_LENGTH` bigint(21) unsigned DEFAULT NULL,
- `DATA_FREE` bigint(21) unsigned DEFAULT NULL,
- `AUTO_INCREMENT` bigint(21) unsigned DEFAULT NULL,
- `CREATE_TIME` datetime DEFAULT NULL,
- `UPDATE_TIME` datetime DEFAULT NULL,
- `CHECK_TIME` datetime DEFAULT NULL,
- `TABLE_COLLATION` varchar(32) DEFAULT NULL,
- `CHECKSUM` bigint(21) unsigned DEFAULT NULL,
- `CREATE_OPTIONS` varchar(255) DEFAULT NULL,
- `TABLE_COMMENT` varchar(2048) NOT NULL DEFAULT ''
- ) ENGINE=MEMORY DEFAULT CHARSET=utf8
- 1 row in set (0.00 sec)
8.0版本
- root@localhost:mysql8006.sock [wjq]>show create table information_schema.tables\G;
- *************************** 1. row ***************************
- View: TABLES
- Create View: CREATE ALGORITHM=UNDEFINED DEFINER=`mysql.infoschema`@`localhost` SQL SECURITY DEFINER VIEW `information_schema`.`TABLES` AS select `cat`.`name` AS `TABLE_CATALOG`,`sch`.`name` AS `TABLE_SCHEMA`,`tbl`.`name` AS `TABLE_NAME`,`tbl`.`type` AS `TABLE_TYPE`,if((`tbl`.`type` = 'BASE TABLE'),`tbl`.`engine`,NULL) AS `ENGINE`,if((`tbl`.`type` = 'VIEW'),NULL,10) AS `VERSION`,`tbl`.`row_format` AS `ROW_FORMAT`,internal_table_rows(`sch`.`name`,`tbl`.`name`,if(isnull(`tbl`.`partition_type`),`tbl`.`engine`,''),`tbl`.`se_private_id`,(`tbl`.`hidden` <> 'Visible'),`ts`.`se_private_data`,coalesce(`stat`.`table_rows`,0),coalesce(cast(`stat`.`cached_time` as unsigned),0)) AS `TABLE_ROWS`,internal_avg_row_length(`sch`.`name`,`tbl`.`name`,if(isnull(`tbl`.`partition_type`),`tbl`.`engine`,''),`tbl`.`se_private_id`,(`tbl`.`hidden` <> 'Visible'),`ts`.`se_private_data`,coalesce(`stat`.`avg_row_length`,0),coalesce(cast(`stat`.`cached_time` as unsigned),0)) AS `AVG_ROW_LENGTH`,internal_data_length(`sch`.`name`,`tbl`.`name`,if(isnull(`tbl`.`partition_type`),`tbl`.`engine`,''),`tbl`.`se_private_id`,(`tbl`.`hidden` <> 'Visible'),`ts`.`se_private_data`,coalesce(`stat`.`data_length`,0),coalesce(cast(`stat`.`cached_time` as unsigned),0)) AS `DATA_LENGTH`,internal_max_data_length(`sch`.`name`,`tbl`.`name`,if(isnull(`tbl`.`partition_type`),`tbl`.`engine`,''),`tbl`.`se_private_id`,(`tbl`.`hidden` <> 'Visible'),`ts`.`se_private_data`,coalesce(`stat`.`max_data_length`,0),coalesce(cast(`stat`.`cached_time` as unsigned),0)) AS `MAX_DATA_LENGTH`,internal_index_length(`sch`.`name`,`tbl`.`name`,if(isnull(`tbl`.`partition_type`),`tbl`.`engine`,''),`tbl`.`se_private_id`,(`tbl`.`hidden` <> 'Visible'),`ts`.`se_private_data`,coalesce(`stat`.`index_length`,0),coalesce(cast(`stat`.`cached_time` as unsigned),0)) AS `INDEX_LENGTH`,internal_data_free(`sch`.`name`,`tbl`.`name`,if(isnull(`tbl`.`partition_type`),`tbl`.`engine`,''),`tbl`.`se_private_id`,(`tbl`.`hidden` <> 'Visible'),`ts`.`se_private_data`,coalesce(`stat`.`data_free`,0),coalesce(cast(`stat`.`cached_time` as unsigned),0)) AS `DATA_FREE`,internal_auto_increment(`sch`.`name`,`tbl`.`name`,if(isnull(`tbl`.`partition_type`),`tbl`.`engine`,''),`tbl`.`se_private_id`,(`tbl`.`hidden` <> 'Visible'),`ts`.`se_private_data`,coalesce(`stat`.`auto_increment`,0),coalesce(cast(`stat`.`cached_time` as unsigned),0),`tbl`.`se_private_data`) AS `AUTO_INCREMENT`,`tbl`.`created` AS `CREATE_TIME`,internal_update_time(`sch`.`name`,`tbl`.`name`,if(isnull(`tbl`.`partition_type`),`tbl`.`engine`,''),`tbl`.`se_private_id`,(`tbl`.`hidden` <> 'Visible'),`ts`.`se_private_data`,coalesce(cast(`stat`.`update_time` as unsigned),0),coalesce(cast(`stat`.`cached_time` as unsigned),0)) AS `UPDATE_TIME`,internal_check_time(`sch`.`name`,`tbl`.`name`,if(isnull(`tbl`.`partition_type`),`tbl`.`engine`,''),`tbl`.`se_private_id`,(`tbl`.`hidden` <> 'Visible'),`ts`.`se_private_data`,coalesce(cast(`stat`.`check_time` as unsigned),0),coalesce(cast(`stat`.`cached_time` as unsigned),0)) AS `CHECK_TIME`,`col`.`name` AS `TABLE_COLLATION`,internal_checksum(`sch`.`name`,`tbl`.`name`,if(isnull(`tbl`.`partition_type`),`tbl`.`engine`,''),`tbl`.`se_private_id`,(`tbl`.`hidden` <> 'Visible'),`ts`.`se_private_data`,coalesce(`stat`.`checksum`,0),coalesce(cast(`stat`.`cached_time` as unsigned),0)) AS `CHECKSUM`,if((`tbl`.`type` = 'VIEW'),NULL,get_dd_create_options(`tbl`.`options`,if((ifnull(`tbl`.`partition_expression`,'NOT_PART_TBL') = 'NOT_PART_TBL'),0,1))) AS `CREATE_OPTIONS`,internal_get_comment_or_error(`sch`.`name`,`tbl`.`name`,`tbl`.`type`,`tbl`.`options`,`tbl`.`comment`) AS `TABLE_COMMENT` from (((((`mysql`.`tables` `tbl` join `mysql`.`schemata` `sch` on((`tbl`.`schema_id` = `sch`.`id`))) join `mysql`.`catalogs` `cat` on((`cat`.`id` = `sch`.`catalog_id`))) left join `mysql`.`collations` `col` on((`tbl`.`collation_id` = `col`.`id`))) left join `mysql`.`tablespaces` `ts` on((`tbl`.`tablespace_id` = `ts`.`id`))) left join `mysql`.`table_stats` `stat` on(((`tbl`.`name` = `stat`.`table_name`) and (`sch`.`name` = `stat`.`schema_name`)))) where (can_access_table(`sch`.`name`,`tbl`.`name`) and is_visible_dd_object(`tbl`.`hidden`))
- character_set_client: utf8
- collation_connection: utf8_general_ci
- 1 row in set (0.00 sec)
可以发现8.0之后,mysql.tables不再是memory表了,在8.0以后变成视图了。再仔细看一下,auto_increment列引用自mysql.table_stats表的auto_increment列。
到这里已经茅塞顿开了,马上执行一下analyze,不出意外mysql.tables表的auto_increment列就变成最新的10了:
- root@localhost:mysql8006.sock [wjq]>analyze table replace_uniq1;
- +-------------------+---------+----------+----------+
- | Table | Op | Msg_type | Msg_text |
- +-------------------+---------+----------+----------+
- | wjq.replace_uniq1 | analyze | status | OK |
- +-------------------+---------+----------+----------+
- 1 row in set (0.01 sec)
- root@localhost:mysql8006.sock [wjq]>select auto_increment from information_schema.tables where table_schema='wjq' and table_name='replace_uniq1';
- +----------------+
- | AUTO_INCREMENT |
- +----------------+
- | 10 |
- +----------------+
- 1 row in set (0.01 sec)
由此可以看出,MySQL 8.0以后,auto_increment方面有了改进的地方,当然不止这里所涉及的,同时在数据字典和系统表方面也做出了一些变化,由表转变为视图,越来越像Oracle了。MySQL自从被Oracle收购以后,真是入乡随俗啊。
参考文章
https://yq.aliyun.com/articles/60885
https://blog.csdn.net/weixin_39004901/article/details/84346093