Day909.MySQL 不同的自增 id 达到上限以后的行为 -MySQL实战

news2024/11/25 4:37:41

MySQL 不同的自增 id 达到上限以后的行为

Hi,我是阿昌,今天学习记录的是关于MySQL 不同的自增 id 达到上限以后的行为的内容。

MySQL 里有很多自增的 id,每个自增 id 都是定义了初始值,然后不停地往上加步长。

虽然自然数是没有上限的,但是在计算机里,只要定义了表示这个数的字节长度,那它就有上限。

比如,无符号整型 (unsigned int) 是 4 个字节,上限就是 2^32-1。

既然自增 id 有上限,就有可能被用完。但是,自增 id 用完了会怎么样呢?

一、表定义自增值 id

说到自增 id,第一个想到的应该就是表结构定义里的自增字段,在自增主键为什么不是连续的?中和介绍过的自增主键 id。

表定义的自增值达到上限后的逻辑是:再申请下一个 id 时,得到的值保持不变。

可以通过下面这个语句序列验证一下:

create table t(id int unsigned auto_increment primary key) auto_increment=4294967295;
insert into t values(null);
//成功插入一行 4294967295
show create table t;
/* CREATE TABLE `t` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4294967295;
*/

insert into t values(null);
//Duplicate entry '4294967295' for key 'PRIMARY'

可以看到,第一个 insert 语句插入数据成功后,这个表的 AUTO_INCREMENT 没有改变(还是 4294967295),就导致了第二个 insert 语句又拿到相同的自增 id 值,再试图执行插入语句,报主键冲突错误。2^32-1(4294967295)不是一个特别大的数,对于一个频繁插入删除数据的表来说,是可能会被用完的。

因此在建表的时候你需要考察表是否有可能达到这个上限,如果有可能,就应该创建成 8 个字节的 bigint unsigned


二、InnoDB 系统自增 row_id

如果创建的 InnoDB 表没有指定主键,那么 InnoDB 会给创建一个不可见的,长度为 6 个字节的 row_id。

InnoDB 维护了一个全局的 dict_sys.row_id 值,所有无主键的 InnoDB 表,每插入一行数据,都将当前的 dict_sys.row_id 值作为要插入数据的 row_id,然后把 dict_sys.row_id 的值加 1。

实际上,在代码实现时 row_id 是一个长度为 8 字节的无符号长整型 (bigint unsigned)。但是,InnoDB 在设计时,给 row_id 留的只是 6 个字节的长度,这样写到数据表中时只放了最后 6 个字节,所以 row_id 能写到数据表中的值,就有两个特征:

  1. row_id 写入表中的值范围,是从 0 到 2^48-1;
  2. 当 dict_sys.row_id=2^48时,如果再有插入数据的行为要来申请 row_id,拿到以后再取最后 6 个字节的话就是 0。

也就是说,写入表的 row_id 是从 0 开始到 248-1。达到上限后,下一个值就是 0,然后继续循环

当然,2^48-1 这个值本身已经很大了,但是如果一个 MySQL 实例跑得足够久的话,还是可能达到这个上限的。

在 InnoDB 逻辑里,申请到 row_id=N 后,就将这行数据写入表中;如果表中已经存在 row_id=N 的行,新写入的行就会覆盖原有的行。

要验证这个结论的话,可以通过 gdb 修改系统的自增 row_id 来实现。

注意,用 gdb 改变量这个操作是为了便于复现问题,只能在测试环境使用。

图 1 row_id 用完的验证序列
图 2 row_id 用完的效果验证

可以看到,在用 gdb 将 dict_sys.row_id 设置为 2^48之后,再插入的 a=2 的行会出现在表 t 的第一行,因为这个值的 row_id=0。之后再插入的 a=3 的行,由于 row_id=1,就覆盖了之前 a=1 的行,因为 a=1 这一行的 row_id 也是 1。

从这个角度看,还是应该在 InnoDB 表中主动创建自增主键。

因为,表自增 id 到达上限后,再插入数据时报主键冲突错误,是更能被接受的。毕竟覆盖数据,就意味着数据丢失,影响的是数据可靠性;报主键冲突,是插入失败,影响的是可用性。而一般情况下,可靠性优先于可用性。


三、Xid

在数据库日志和索引相关问题中,介绍 redo log 和 binlog 相配合的时候,提到了它们有一个共同的字段叫作 Xid

它在 MySQL 中是用来对应事务的。那么,Xid 在 MySQL 内部是怎么生成的呢?

MySQL 内部维护了一个全局变量 global_query_id,每次执行语句的时候将它赋值给 Query_id,然后给这个变量加 1。

如果当前语句是这个事务执行的第一条语句,那么 MySQL 还会同时把 Query_id 赋值给这个事务的 Xid。而 global_query_id 是一个纯内存变量,重启之后就清零了

在同一个数据库实例中,不同事务的 Xid 也是有可能相同的。但是 MySQL 重启之后会重新生成新的 binlog 文件,这就保证了,同一个 binlog 文件里,Xid 一定是惟一的

虽然 MySQL 重启不会导致同一个 binlog 里面出现两个相同的 Xid,但是如果 global_query_id 达到上限后,就会继续从 0 开始计数。

从理论上讲,还是就会出现同一个 binlog 里面出现相同 Xid 的场景。因为 global_query_id 定义的长度是 8 个字节,这个自增值的上限是 264-1。

要出现这种情况,必须是下面这样的过程:

  1. 执行一个事务,假设 Xid 是 A;
  2. 接下来执行 2^64次查询语句,让 global_query_id 回到 A;
  3. 再启动一个事务,这个事务的 Xid 也是 A。

不过,2^64这个值太大了,大到你可以认为这个可能性只会存在于理论上。


四、Innodb trx_id

Xid 和 InnoDB 的 trx_id 是两个容易混淆的概念。

Xid 是由 server 层维护的。InnoDB 内部使用 Xid,就是为了能够在 InnoDB 事务和 server 之间做关联。

但是,InnoDB 自己的 trx_id,是另外维护的。

binlog是server层,redolog是innodb。xid是关联binlog和redolog的。
xid是server层维护。 trx_id是innodb记录行数据的版本号,用作事物隔离,是innodb维护

trx_id就是在事务间是否需要隔离中讲事务可见性时,用到的事务 id(transaction id)。

InnoDB 内部维护了一个 max_trx_id 全局变量,每次需要申请一个新的 trx_id 时,就获得 max_trx_id 的当前值,然后并将 max_trx_id 加 1。

InnoDB 数据可见性的核心思想是:每一行数据都记录了更新它的 trx_id,当一个事务读到一行数据的时候,判断这个数据是否可见的方法,就是通过事务的一致性视图与这行数据的 trx_id 做对比。对于正在执行的事务,可以从 information_schema.innodb_trx 表中看到事务的 trx_id。


一个事务现场:

图 3 事务的 trx_id
session B 里,从 innodb_trx 表里查出的这两个字段,第二个字段 trx_mysql_thread_id 就是线程 id。

显示线程 id,是为了说明这两次查询看到的事务对应的线程 id 都是 5,也就是 session A 所在的线程。

可以看到,T2 时刻显示的 trx_id 是一个很大的数;T4 时刻显示的 trx_id 是 1289,看上去是一个比较正常的数字。这是什么原因呢?

实际上,在 T1 时刻,session A 还没有涉及到更新,是一个只读事务。而对于只读事务,InnoDB 并不会分配 trx_id。也就是说:

  1. 在 T1 时刻,trx_id 的值其实就是 0。而这个很大的数,只是显示用的。
  2. 直到 session A 在 T3 时刻执行 insert 语句的时候,InnoDB 才真正分配了 trx_id。所以,T4 时刻,session B 查到的这个 trx_id 的值就是 1289。

需要注意的是,除了显而易见的修改类语句外,如果在 select 语句后面加上 for update,这个事务也不是只读事务。实验的时候发现不止加 1。

这是因为:

  1. update 和 delete 语句除了事务本身,还涉及到标记删除旧数据,也就是要把数据放到 purge 队列里等待后续物理删除,这个操作也会把 max_trx_id+1, 因此在一个事务中至少加 2;
  2. InnoDB 的后台操作,比如表的索引信息统计这类操作,也是会启动内部事务的,因此你可能看到,trx_id 值并不是按照加 1 递增的。

T2 时刻查到的这个很大的数字是怎么来的呢?

其实,这个数字是每次查询的时候由系统临时计算出来的。它的算法是:把当前事务的 trx 变量的指针地址转成整数,再加上 2^48。

使用这个算法,就可以保证以下两点:

  1. 因为同一个只读事务在执行期间,它的指针地址是不会变的,所以不论是在 innodb_trx 还是在 innodb_locks 表里,同一个只读事务查出来的 trx_id 就会是一样的。
  2. 如果有并行的多个只读事务,每个事务的 trx 变量的指针地址肯定不同。这样,不同的并发只读事务,查出来的 trx_id 就是不同的。

那么,为什么还要再加上 2^48呢?

在显示值里面加上 248,目的是要保证只读事务显示的 trx_id 值比较大,正常情况下就会区别于读写事务的 id。但是,trx_id 跟 row_id 的逻辑类似,定义长度也是 8 个字节。

因此,在理论上还是可能出现一个读写事务与一个只读事务显示的 trx_id 相同的情况。不过这个概率很低,并且也没有什么实质危害,可以不管它。


只读事务不分配 trx_id,有什么好处呢?

  • 一个好处是,这样做可以减小事务视图里面活跃事务数组的大小。因为当前正在运行的只读事务,是不影响数据的可见性判断的。所以,在创建事务的一致性视图时,InnoDB 就只需要拷贝读写事务的 trx_id。
  • 另一个好处是,可以减少 trx_id 的申请次数。在 InnoDB 里,即使你只是执行一个普通的 select 语句,在执行过程中,也是要对应一个只读事务的。所以只读事务优化后,普通的查询语句不需要申请 trx_id,就大大减少了并发事务申请 trx_id 的锁冲突。

由于只读事务不分配 trx_id,一个自然而然的结果就是 trx_id 的增加速度变慢了。但是,max_trx_id 会持久化存储,重启也不会重置为 0,那么从理论上讲,只要一个 MySQL 服务跑得足够久,就可能出现 max_trx_id 达到 2^48-1 的上限,然后从 0 开始的情况。


当达到这个状态后,MySQL 就会持续出现一个脏读的 bug,来复现一下这个 bug。

首先我们需要把当前的 max_trx_id 先修改成 248-1。

注意:这个 case 里使用的是可重复读隔离级别

具体的操作流程如下:

图 4 复现脏读

由于已经把系统的 max_trx_id 设置成了 2^48-1,所以在 session A 启动的事务 TA 的低水位就是 2^48-1。

在 T2 时刻,session B 执行第一条 update 语句的事务 id 就是 2^48-1,而第二条 update 语句的事务 id 就是 0 了,这条 update 语句执行后生成的数据版本上的 trx_id 就是 0。

在 T3 时刻,session A 执行 select 语句的时候,判断可见性发现,c=3 这个数据版本的 trx_id,小于事务 TA 的低水位,因此认为这个数据可见。但,这个是脏读。

由于低水位值会持续增加,而事务 id 从 0 开始计数,就导致了系统在这个时刻之后,所有的查询都会出现脏读的。并且,MySQL 重启时 max_trx_id 也不会清 0,也就是说重启 MySQL,这个 bug 仍然存在。

那么,这个 bug 也是只存在于理论上吗?

假设一个 MySQL 实例的 TPS 是每秒 50 万,持续这个压力的话,在 17.8 年后,就会出现这个情况。

如果 TPS 更高,这个年限自然也就更短了。但是,从 MySQL 的真正开始流行到现在,恐怕都还没有实例跑到过这个上限。

不过,这个 bug 是只要 MySQL 实例服务时间够长,就会必然出现的。


五、thread_id

线程 id(thread_id),其实,线程 id 才是 MySQL 中最常见的一种自增 id。

平时在查各种现场的时候,show processlist 里面的第一列,就是 thread_id。

thread_id 的逻辑很好理解:系统保存了一个全局变量 thread_id_counter,每新建一个连接,就将 thread_id_counter 赋值给这个新连接的线程变量。

thread_id_counter 定义的大小是 4 个字节,因此达到 2^32-1 后,它就会重置为 0,然后继续增加。但是,不会在 show processlist 里看到两个相同的 thread_id。

这,是因为 MySQL 设计了一个唯一数组的逻辑,给新线程分配 thread_id 的时候,逻辑代码是这样的:

do { 
	new_id= thread_id_counter++;
} 
while (!thread_ids.insert_unique(new_id).second);

六、总结

MySQL 不同的自增 id 达到上限以后的行为

数据库系统作为一个可能需要 7*24 小时全年无休的服务,考虑这些边界是非常有必要的。

每种自增 id 有各自的应用场景,在达到上限后的表现也不同:

  1. 表的自增 id 达到上限后,再申请时它的值就不会改变,进而导致继续插入数据时报主键冲突的错误。
  2. row_id 达到上限后,则会归 0 再重新递增,如果出现相同的 row_id,后写的数据会覆盖之前的数据。
  3. Xid 只需要不在同一个 binlog 文件中出现重复值即可。虽然理论上会出现重复值,但是概率极小,可以忽略不计。
  4. InnoDB 的 max_trx_id 递增值每次 MySQL 重启都会被保存起来,所以提到的脏读的例子就是一个必现的 bug,好在留给的时间还很充裕。
  5. thread_id 是使用中最常见的,而且也是处理得最好的一个自增 id 逻辑了。

不同的自增 id 有不同的上限值,上限值的大小取决于声明的类型长度。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/392397.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TSP 问题求解的最好方法 LKH

目前可以查到的最好的方法求解TSP问题是 LKH,所以本篇文章介绍如何使用Matlab 调用LKH 参考文档:用matlab调用迄今为止最强悍的求解旅行商(TSP)的算法-LKH算法_wx6333e948c3602的技术博客_51CTO博客 【LKH算法体验】用matlab调用…

游戏玩的多,陪玩你了解的多吗?用Python来采集陪玩数据,看看行情和美照

前言 (。・∀・)ノ゙嗨 大家好 现在应该每个人都玩过游戏吧,有些的上瘾,天天玩停不下来,有些的倒是没啥感觉 有游戏就肯定有陪玩啊,毕竟当朋友忙的时候,自己一个…

天琊超级进程监视器的应用试验(19)

实验目的 1、了解进程概念及其基本原理; 2、掌握天琊超级进程监视器的安装与使用。预备知识 本实验要求实验者具备如下的相关知识。 操作系统的安全配置是整个系统安全审计策略核心,其目的就是从系统根源构筑安全防护体系,通过用户的一…

Linux系统安装Tomcat

(1)登录Tomcat网址 https://tomcat.apache.org/,下载Tomcat安装包(2)登录我们的虚拟机,使用Linux系统中的“rz”命令上传压缩包(注意:如果使用“rz”上传压缩包出现错误时&#xff0…

STM32模拟SPI时序控制双路16位数模转换(16bit DAC)芯片DAC8552电压输出

STM32模拟SPI时序控制双路16位数模转换(16bit DAC)芯片DAC8552电压输出 STM32部分芯片具有12位DAC输出能力,要实现16位及以上DAC输出需要外挂DAC转换ASIC。 DAC8552是双路16位DAC输出芯片,通过SPI三线总线进行配置控制输出。这里…

苹果新卫星专利公布,苹果Find My功能知多少

根据美国商标和专利局(USPTO)公示的清单,苹果公司获得了一项新的卫星专利,可在非地面网络(Non-Terrestrial Networks,NTN)中定位用户设备(iDevice、MacBook 等)。 在专利…

Docker(六)--Docker网络--单机与跨主机(macvlan)容器通信

文章目录一、本地节点通信1.DNS2.joind网络模式3.端口映射4.跨主机容器通信二、跨主机容器通信1.相同网段进行通信2.不同网段进行通信一、本地节点通信 1.DNS 容器之间除了使用ip通信外,还可以使用容器名称通信。docker 1.10开始,内嵌了一个DNS server。…

【算法进阶】回溯(backtracking)基本逻辑,以及常见回溯问题(全排列、解数独、八皇后)

文章目录一、引言二、回溯法基本逻辑三、回溯法代码模板三、回溯法常见问题3.1 组合逻辑代码3.2 子集逻辑代码3.3 子集Ⅱ(未解答)逻辑代码3.4 分割回文串逻辑代码3.5 组合总和Ⅰ逻辑代码3.6 组合总和Ⅱ(未解答)逻辑代码3.7 组合总…

Mycat

Mycat 1.概述 1.Mycat是数据中间件2.中间件:连接软件组件和应用的计算机软件,便于软件和各部件的交互3.数据中间件:连接Java应用程序与数据库的软件2.适用场景 1.Java与数据库紧耦合(直接连接)2.高访问量高并发对数据库压力(集群)3.读写请求数据不一致(读写分离+主从复制)3.…

关于Gooey复选框CheckBox的使用

折腾了我一下午 官网也没发现具体的使用方法 老是报错 索引超出范围 我就很疑惑 百度也没有答案后来我修改成了非必参 加-- 这是不选中操作这是选中操作他说必须要有一个参数 我有啊 没搞懂 后来 我就这样(根据他报错提示来的)果真就没了问题这样也没问题 具体我还是没搞懂 反正…

K_A16_001 基于STM32等单片机驱动HX711称重模块 串口与OLED0.96双显示

K_A16_001 基于STM32等单片机驱动HX711称重模块 串口与OLED0.96双显示一、资源说明二、基本参数参数引脚说明三、驱动说明对应程序:四、部分代码说明1、接线引脚定义1.1、STC89C52RCHX711称重模块1.2、STM32F103C8T6HX711称重模块五、基础知识学习与相关资料下载六、视频效果展…

项目实战典型案例17——环境混用来带的影响

环境混用来带的影响一:背景介绍背景出现的事故二:思路&方案环境混用的危害如何彻底避免环境混用的问题四:总结五:升华一:背景介绍 本篇博客是对对项目开发中出现的环境混用来带的影响进行的总结并进行的改进。目的…

你想知道的OSPF协议知识点都在这里了

1、OSPF协议概述 1)为什么需要动态路由协议? 静态路由是由工程师手动配置和维护的路由条目,命令行简单明确,适用于小型或稳定的网络。静态路由有以下问题:a)无法适应规模较大的网络:随着设备数量增加,配置量…

SpringBoot学习笔记(三)整合Logback日志框架

一、日志框架介绍1、常见日志框架目前我们常见的日志框架为Log4j、Log4j2、Logback这3种,并且现在很多的工具包里面都会自带日志框架,因此我们使用要格外小心日志框架的冲突。2、三种日志框架之间的关系最先有Log4j,然后因为Log4j有很大的性能…

uniapp系列-图文并茂手把手教你hbuilder进行uniapp云端打包 - 安心打包

什么是安心打包 提交App的模块配置信息到云端,在云端打包机生成原生代码包 为什么使用云打包 更安全:打包时不提交应用代码、证书等信息更快速:非首次打包时不用提交云端打包机排队等待,本地直接出包省流量:减少了打…

Linux开发环境配置--正点原子阿尔法开发板

Linux开发环境配置–正点原子阿尔法开发板 文章目录Linux开发环境配置--正点原子阿尔法开发板1.网络环境设置1.1添加网络适配器1.2虚拟网络编辑器设置1.3Ubuntu和Windows网络信息设置Ubuntu网络信息配置方式:1.系统设置->网络->选项2.配置网络文件2源码准备2.…

Vuex 状态管理

文章目录Vuex概述安装单向数据流Vuex核心概念StatemapState 辅助函数扩展运算符GettermapGetters 辅助函数Mutation提交载荷提交载荷对象对象风格提交使用常量替代mutation事件类型Action异步分发Module命名空间Vuex 概述 Vuex 是一个状态管理库,用于管理 Vue.js …

CRM系统是什么?为什么使用它?

CRM系统是什么?为什么使用它?这篇来简单说下,CRM系统是什么?能帮助我们做什么?有什么好处? 01 CRM系统是什么? 我总结了7种关于CRM的概念,任意一个解释得其实都没什么问题&#xff…

【数据结构】核心数据结构之二叉堆的原理及实现

1.大顶堆和小顶堆原理 什么是堆 堆(Heap)是计算机科学中一类特殊的数据结构,通常是一个可以被看作一颗完全二叉树的数组对象。 完全二叉树 只有最下面两层节点的度可以小于2,并且最下层的叶节点集中在靠左连续的边界 只允许最后…

2023FL Studio最新中文版电子音乐、混音和母带制作DAW

水果具有独特的底层逻辑,其开创了编曲“块”的思维。用FL Studio编曲的流程是在把一个样式编辑好,然后将编辑好的样式当做音频块,在播放列表中像“搭积木”一样任意编排,形成一首歌,这种模式非常利于电子音乐编曲。 2…