MySQL · 8.0.0新特性 · 持久化自增列值

简介:

Worklog: WL#6204

这是MySQL8.0修复的上古bug之一,在2003年由Percona的CEO(当时应该还没Percona吧)提出的bug#199,光看这bug号就扑面而来一股上古时代的沧桑气息。

问题的本质在于InnoDB初始化AUTO_INCREMENT的方式,在每次重启时,总是算出表上最大的自增值作为最大值,下一次分配从该值开始。这意味着如果在btree右侧叶节点大量删除记录,重启后,自增值可能被重用。这在很多场景下可能导致问题,包括但不限于:主备切换、历史数据迁移等场景。在bug#199下面一大堆的回复里,可以看到大量的同行抱怨。

很早阿里的MySQL版本就解决了这个问题,主要思路是取btree根page的一个未用的长整数字段(page header的PAGE_MAX_TRX_ID),然后将当前表上的auto-increment的值持久化到其中 (还好目前innodb还不支持多个自增列),由于一般表的root页都是驻留在内存的,纯内存操作对性能带来的影响几乎可以忽略。

官方的修复就比较优雅了,不改变任何现有的存储,而是通过redo log来进行恢复。该补丁基于WL#7816的框架实现的,要想搞懂这个补丁,得先看看WL#7816做了哪些改动

根据Worklog的描述,当InnoDB发现某个索引损坏时,它会设置flag其标记成corruption状态, 并持久化到内部数据词典及持久化存储中。但是新的全局数据词典(data directory,简称DD)置于存储引擎上层,而从底层引擎去更新数据词典可能会导致死锁。而将corruption信息层层传递到上层,看起来也比较诡异.

为了解决这个问题,InnoDB使用一个引擎私有的系统表+特殊redo log的方式,在引擎内部自己解决corruption标记持久化的问题。其大概思路为:

  1. 当发现索引损坏时,写入一条redo log,但不更新数据词典
  2. 引入一个innodb引擎私有的系统表,称为DD Buffer Table,每次checkpoint之前会将索引corruption bit存入其中。
  3. 在崩溃恢复时,同时从redo log和DD Buffer Table中读取索引 corruption bit, 合并结果,并标记内存中的表和索引对象。

在该worklog中解决的是corruption bit的持久化问题,但实现的框架也适用于其他目的,例如update_time, auto_inc, count(*)等,因此对代码做了通用性的抽象。

初始化Persister

目前Persister的类型仅有两种,一个用于corruption bit的持久化,一个用于自增列的持久化,对应的类为:

Persister:
    |-- CorruptedIndexPersister
    |-- AutoIncPersister

Persister对应全局对象dict_persist_t::persisters,可以通过类型persistent_type_t来找到对应的Persister,目前仅有PM_INDEX_CORRUPTED及PM_TABLE_AUTO_INC,但从注释来看,未来肯定会做更多的扩展

Persister在启动时调用函数dict_persist_init进行初始化。

新的系统表

新的系统表名为SYS_TABLE_INFO_BUFFER,对应管理类为DDTableBuffer,指针存储在dict_persist->table_buffer中。

Table id为DICT_TBL_BUFFER_ID,值为0xFFFFFFFFFF000000ULL, ROOT PAGE是ibdata的第8个page(FSP_TBL_BUFFER_TREE_ROOT_PAGE_NO)

系统表包含两个列:TABLE_ID及BLOB类型的METADATA(ref DDTableBuffer::init),METADATA列包含了所有需要持久化的元数据。

更新Metata

当发现索引损坏时,调用dict_set_corrupted标记索引损坏,并进行日志写入(Persister::write_log):

  • 写入的内容包含space id 和index id
  • 日志格式为:
| 1byte: type = MLOG_TABLE_DYNAMIC_META
| Table ID
| 1byte: SUB-TYPE: PM_INDEX_CORRUPTED
| 1byte: Num: Number of corrupted indexs
| 4bytes: space id
| 8bytes: index id

## 这个结构有点奇怪,理论上同一个表的索引应该存在于同一个space中,这里只需要记录一个space id就可以了

写完这条日志后,会进行一次log flush,将日志持久化到磁盘。由于index corruption属于低概率事件,不会引起性能问题。

然后再设置表的状态为脏 (dict_table_mark_dirty),这里为表定义了三种状态:

dict_table_t::dirty_status

METADATA_CLEAN: 在DDTableBuffer表中没有任何缓存数据
METADATA_BUFFERED: 在DDTableBuffer系统表中存在至少一行数据,未来需要回写到DD中
METADATA_DIRTY: 一些持久化元数据在内存中被修改,需要回写到DDTableBuffer中

如果当前表状态为METADATA_CLEAN,则需要将对象加到全局链表dict_persist_t::dirty_dict_tables中,这个链表用于维护状态为METADATA_DIRTY或者METADATA_BUFFERED的表对象

dirty_status在调用dict_table_mark_dirty后被设置成METADATA_DIRTY,并确保在dict_persist_t::dirty_dict_tables链表上

而对于AUTOINC列的持久化发生在插入或者更新时,注意对于临时表无需做持久化。

在插入聚集索引记录前(row_ins_clust_index_entry_low), 会先从entry中把counter拿出来,并记入日志

在更新记录时(row_upd_clust_rec),如果表上有autoinc列并且被更新成更大的值(row_upd_check_autoinc_counter),也会去尝试记录写日志。

持久化AUTOINC的日志写入函数为AutoIncLogMtr::log,当新的counter大于已经持久化的dict_table_t::autoinc_persisted时,将autoinc_persisted更新为新的counter,并将表的diry_status置为dirty(如果需要的话)

记录的日志格式为

| 1byte: type = MLOG_TABLE_DYNAMIC_META
| Table ID
| 1byte: Sub-type: PM_TABLE_AUTO_INC
| Autoinc Counter

注意这里在写入日志后,出于性能考虑并没有做flush log操作,因此如果crash了,已分配的autoinc不能保证不被重用,但从用户的角度来看(事务级别),autoinc是不会重用的。

回写DDTableBuffer

有几种情况会将内存修改回写到DDTableBuffer中:

  1. 在做checkpoint(log_checkpoint)之前,所有在dirty_dict_tables链表上的表对象,对应persist metadata都需要回写到DDTableBuffer中(dict_persist_to_dd_table_buffer)
  2. 从内存中驱逐一个表对象时(dict_table_remove_from_cache_low),如果需要的话也会去尝试回写。

  3. 在对包含自增列的表做DDL后,需要持久化counter,在如下函数中,会调用dict_table_set_and_persist_autoinc:
ha_innobase::commit_inplace_alter_table
create_table_info_t::initialize_autoinc()
// for example: alter table..auto_increment = ??
row_rename_table_for_mysql;
// rename from temporary table to normal table

回写的过程也比较简单(dict_table_persist_to_dd_table_buffer_low):

  1. 通过表对象初始化需要回写的Metadata数据: corrupt index及autoinc值(dict_init_dynamic_metadata)
  2. 构建记录值,插入DDTableBuffer系统表(DDTableBuffer::replace(), 如果记录存在的话,则进行悲观更新操作
  3. 表对象的diry_status修改成 METADATA_BUFFERED,表示有buffer的元数据

Recovery and Startup

在崩溃恢复时,当解析到日志MLOG_TABLE_DYNAMIC_META时(MetadataRecover::parseMetadataLog),会进行解析并将解析得到的数据存储到集合中(MetadataRecover::m_tables),如果存在相同table-id的项,就进行替换,确保总是最新的。

在完成recovery后,搜集到的meta信息暂时存储到srv_dict_metadata中, 随后进行apply(srv_dict_recover_on_restart), apply的过程也比较简单,载入表对象,然后对表对象进行更新(MetadataRecover::apply),例如对于autoinc列,就总是选择更大的那个值。

最后

详细参阅代码 commit dcb8792b371601dc5fc4e9f42fb9c479532fc7c2

这个bug已经挂了相当长的时间,不排除把这个bug当作InnoDB的“特性”的同学,一定要注意到这个改动...

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
相关文章
|
3月前
|
SQL 监控 关系型数据库
MySQL事务处理:ACID特性与实战应用
本文深入解析了MySQL事务处理机制及ACID特性,通过银行转账、批量操作等实际案例展示了事务的应用技巧,并提供了性能优化方案。内容涵盖事务操作、一致性保障、并发控制、持久性机制、分布式事务及最佳实践,助力开发者构建高可靠数据库系统。
|
3月前
|
存储 关系型数据库 MySQL
介绍MySQL的InnoDB引擎特性
总结而言 , Inno DB 引搞 是 MySQL 中 高 性 能 , 高 可靠 的 存 储选项 , 宽泛 应用于要求强 复杂交易处理场景 。
157 15
|
3月前
|
关系型数据库 MySQL 数据库
MySql事务以及事务的四大特性
事务是数据库操作的基本单元,具有ACID四大特性:原子性、一致性、隔离性、持久性。它确保数据的正确性与完整性。并发事务可能引发脏读、不可重复读、幻读等问题,数据库通过不同隔离级别(如读未提交、读已提交、可重复读、串行化)加以解决。MySQL默认使用可重复读级别。高隔离级别虽能更好处理并发问题,但会降低性能。
180 0
|
5月前
|
SQL 存储 缓存
MySQL 如何高效可靠处理持久化数据
本文详细解析了 MySQL 的 SQL 执行流程、crash-safe 机制及性能优化策略。内容涵盖连接器、分析器、优化器、执行器与存储引擎的工作原理,深入探讨 redolog 与 binlog 的两阶段提交机制,并分析日志策略、组提交、脏页刷盘等关键性能优化手段,帮助提升数据库稳定性与执行效率。
159 0
|
NoSQL 关系型数据库 MySQL
2024Mysql And Redis基础与进阶操作系列(4-2)作者——LJS[含MySQL非空、唯一性、PRIMARY KEY、自增列/自增约束举例说明等详解步骤及常见报错问题对应的解决方法]
24MySQL非空、唯一性、PRIMARY KEY、自增列/自增约束举例说明等详解步骤及常见报错问题对应的解决方法(4-2) 学不会你来砍我!!!
|
12月前
|
SQL 安全 关系型数据库
【MySQL基础篇】事务(事务操作、事务四大特性、并发事务问题、事务隔离级别)
事务是MySQL中一组不可分割的操作集合,确保所有操作要么全部成功,要么全部失败。本文利用SQL演示并总结了事务操作、事务四大特性、并发事务问题、事务隔离级别。
5021 56
【MySQL基础篇】事务(事务操作、事务四大特性、并发事务问题、事务隔离级别)
|
9月前
|
人工智能 关系型数据库 MySQL
解决MySQL自增id用尽的问题
本文介绍了解决文章点击记录表(`article_click_record`)数据量激增问题的方案。由于用户量大,每天新增约400万条记录,导致表id接近溢出(2,100,000,000),且占用空间超320G。解决方案包括:1) 新建`article_click_record_new`表,将id类型改为BIGINT以避免溢出;2) 过渡阶段同时写入新旧表,待旧表id溢出后切换至新表;3) 定时清理过期数据或转移旧表内容。实现方式涉及修改相关接口和服务逻辑,确保业务平稳过渡。
208 5
|
12月前
|
存储 关系型数据库 MySQL
MySQL主键谁与争锋:MySQL为何钟爱自增主键ID+UUID?
本文深入探讨了在MySQL中使用自增类型主键的优势与局限性。自增主键通过保证数据的有序性和减少索引维护成本,提升了查询和插入性能,简化了数据库管理和维护,并提高了数据一致性。然而,在某些业务场景下,如跨表唯一性需求或分布式系统中,自增主键可能无法满足要求,且存在主键值易预测的安全风险。因此,选择主键类型时需综合考虑业务需求和应用场景。
422 2
|
关系型数据库 MySQL
mysql事务特性
原子性:一个事务内的操作统一成功或失败 一致性:事务前后的数据总量不变 隔离性:事务与事务之间相互不影响 持久性:事务一旦提交发生的改变不可逆
|
监控 关系型数据库 MySQL
MySQL自增ID耗尽应对策略:技术解决方案全解析
在数据库管理中,MySQL的自增ID(AUTO_INCREMENT)属性为表中的每一行提供了一个唯一的标识符。然而,当自增ID达到其最大值时,如何处理这一情况成为了数据库管理员和开发者必须面对的问题。本文将探讨MySQL自增ID耗尽的原因、影响以及有效的应对策略。
601 3

相关产品

  • 云数据库 RDS MySQL 版
  • 推荐镜像

    更多