面试题:线上MySQL的自增id用尽怎么办?

文章目录

  • 前言
  • 表定义自增值id
  • InnoDB系统自增row_id
    • Xid
    • Innodb trx_id
  • InnoDB数据可见性的核心思想
  • 为什么要加248?
  • 为何只读事务不分配trx_id?
    • thread_id
  • 总结


前言

MySQL的自增id都定义了初始值,然后不断加步长。虽然自然数没有上限,但定义了表示这个数的字节长度,计算机存储就有上限。比如,无符号整型(unsigned int)是4个字节,上限就是2^32 - 1。那自增id用完,会怎么样?


表定义自增值id

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

mysql> create table t(id int unsigned auto_increment primary key) auto_increment=4294967295;
Query OK, 0 rows affected (0.01 sec)mysql> insert into t values(null);
Query OK, 1 row affected (0.00 sec)mysql> show create table t;
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                      |
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| t     | CREATE TABLE `t` (`id` int unsigned NOT NULL AUTO_INCREMENT,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4294967295 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci |
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)//成功插入一行 4294967295
mysql> insert into t values(null);
ERROR 1062 (23000): Duplicate entry '4294967295' for key 't.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~2^48 - 1。达到上限后,下个值就是0,然后继续循环。

2^48 - 1已经很大,但若一个MySQL实例活得久,还是可能达到上限。

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

验证该结论:通过gdb修改系统的自增row_id。用gdb是为了便于复现问题,只能在测试环境使用。

  • row_id用完的验证序列
    在这里插入图片描述

  • 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,用来对应事务。Xid在MySQL内部是如何生成的呢?

MySQL内部维护了一个全局变量global_query_id

图片

每次执行语句时,将它赋值给query_id,然后给该变量+1:

图片

若当前语句是该事务执行的第一条语句,则MySQL还会同时把query_id赋值给该事务的Xid:

图片

而global_query_id是一个纯内存变量,重启之后就清零了。所以同一DB实例,不同事务的Xid可能相同。

但MySQL重启之后会重新生成新binlog文件,这就保证同一个binlog文件里的Xid唯一。

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

因为global_query_id8字节,上限2^64 - 1。要出现这种情况,需满足:

  1. 执行一个事务,假设Xid是A
  2. 接下来执行2^64次查询语句,让global_query_id回到A
    2^64太大了,这种可能只存在于理论中。
  3. 再启动一个事务,这个事务的Xid也是A

Innodb trx_id

Xid由server层维护
InnoDB内部使用Xid,为了关联InnoDB事务和server

但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。

看如下案例:事务的trx_id

图片

S2 的执行记录:

mysql> use information_schema;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -ADatabase changedmysql> select trx_id, trx_mysql_thread_id from innodb_trx;
+-----------------+---------------------+
| trx_id          | trx_mysql_thread_id |
+-----------------+---------------------+
| 421972504382792 |                  70 |
+-----------------+---------------------+
1 row in set (0.00 sec)mysql> select trx_id, trx_mysql_thread_id from innodb_trx;
+---------+---------------------+
| trx_id  | trx_mysql_thread_id |
+---------+---------------------+
| 1355623 |                  70 |
+---------+---------------------+
1 row in set (0.01 sec)

S2从innodb_trx表里查出的这两个字段,第二个字段trx_mysql_thread_id就是线程id。显示线程id,是为说明这两次查询看到的事务对应的线程id都是5,即S1所在线程。

t2时显示的trx_id是一个很大的数;t4时刻显示的trx_id是1289,看上去是一个比较正常的数字。这是为啥?

t1时,S1还未涉及更新,是一个只读事务。对于只读事务,InnoDB并不会分配trx_id:

  • t1时,trx_id的值就是0。而这个很大的数,只是显示用
  • 直到S1在t3时执行insert,InnoDB才真正分配trx_id。所以t4时,S2查到该trx_id的值就是1289。

除了明显的修改类语句,若在select 语句后面加上for update,也不是只读事务。

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

t2时查到的很大数字是怎么来的?

每次查询时,由系统临时计算:

当前事务的trx变量的指针地址转成整数,再加上248

这样可以保证:

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

为什么要加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:

首先把当前的max_trx_id先修改成2^48 - 1。这里是可重复读。

  • 复现脏读

图片
图片

  • 因为系统的max_trx_id被设置成2^48 - 1,所以在session A启动的事务TA的低水位就是2^48 - 1。

t2时:

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

t3时:

session A执行select的可见性判断:c=3这个数据版本的trx_id(0),小于事务TA的低水位(2^48 - 1),所以认为该数据可见。

但这是脏读。

由于低水位值会持续增加,而事务id从0开始计数,导致系统在该时刻后,所有查询都会出现脏读。

并且MySQL重启时max_trx_id也不会清0,即重启MySQL,这个bug仍然存在。那这bug也是只存在于理论上吗?

假设一个MySQL实例的TPS是50w,持续这样,17.8年后就会出现该情况。但从MySQL真正开始流行到现在,恐怕都还没有实例跑到过这个上限。不过,只要MySQL实例服务时间够长,就必然会出现该bug。

这也可以加深对低水位和数据可见性的理解

thread_id

系统保存了一个全局变量thread_id_counter

图片

每新建一个连接,就将thread_id_counter赋值给这个新连接的线程变量new_id。

thread_id_counter定义为4个字节,因此达到2^32 - 1,就会重置为0,继续增加。

图片

但不会在show processlist看到两个相同的thread_id。因为MySQL使用了一个唯一数组
在这里插入图片描述

给新线程分配thread_id时的逻辑:

图片


总结

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

表的自增id达到上限后,再申请时它的值就不会改变,进而导致继续插入数据时报主键冲突错误

row_id达到上限后,则会归0再重新递增,如果出现相同的row_id,后写的数据会覆盖之前的数据

Xid只需要不在同一个binlog文件中出现重复值即可。虽然理论上会出现重复值,但是概率极小,可以忽略不计

InnoDB的max_trx_id 递增值每次MySQL重启都会被保存起来,所以我们文章中提到的脏读的例子就是一个必现的bug,好在留给我们的时间还很充裕。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

竞赛 深度学习疲劳检测 驾驶行为检测 - python opencv cnn

文章目录 0 前言1 课题背景2 相关技术2.1 Dlib人脸识别库2.2 疲劳检测算法2.3 YOLOV5算法 3 效果展示3.1 眨眼3.2 打哈欠3.3 使用手机检测3.4 抽烟检测3.5 喝水检测 4 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 🚩 **基于深度学习加…

Web服务器实战

网站需求 1.基于域名www.openlab.com可以访问网站内容为 welcome to openlab!!! 2.给该公司创建三个网站目录分别显示学生信息,教学资料和缴费网站,基于www.openlab.com/student 网站访问学生信息,www.openlab.com/data网站访问教学资料 www…

Spring Boot创建多模块项目

创建一个普通的Spring Boot项目, 然后只留下 pom.xml 剩下的都删掉 删除多余标签 标识当前为父模块 创建子模块 删除子模块中多余标签 声明父模块 在父模块中声明子模块

【MongoDB-Redis-MySQL-Elasticsearch-Kibana-RabbitMQ-MinIO】Java全栈开发软件一网打尽

“Java全栈开发一网打尽:在Windows环境下探索技术世界的奇妙之旅” 前言 全栈开发是一项复杂而令人兴奋的任务,涵盖了从前端到后端、数据库到可视化层、消息队列到文件存储的广泛领域。本文将带您深入探讨在Windows环境下进行全栈开发的过程&#xff0…

CRM中的销售机会管理是什么?三个步骤帮你创建销售渠道

企业销售业务中,有个名词叫做“机会管理”,有效的机会管理可以帮助销售人员准确地抓住潜在客户群体,并将其转化为真正的客户、持续带来收入。CRM客户管理系统也是销售机会管理的一个重要工具,帮助销售人员与正确的人建立起关系&am…

Spring基础(2):放弃XML,走向注解

上一篇并没有实际地带大家去看源码,而是介绍了两个概念: BeanDefinitionBeanPostProcessor 当然,我介绍得非常笼统,不论是BeanDefinition还是BeanPostProcessor其实都有着较为复杂的继承体系,种类也很多。作为Spring…

王道考研--》顺序表课后习题C语言代码实现(冲刺)

考研是许多计算机科学专业学生追求高学历、寻求更好就业前景的途径。在考研过程中,数据结构是一个非常重要的科目,而代码实现题更是其中的难点之一。在这篇文章中,我们将探讨如何通过实现数据结构代码问题来提升考研成绩。无论您是否有编程经…

pytoch安装指定版本教程pytorch1.3安装笔记

一、先生成一个环境 如果电脑里安装了其他的torch版本,另外生成一个环境可以防止原先torch版本被替换掉。 打开conda的终端窗口输入以下命令就可以生成一个名为torch_1.3的环境: conda create -n torch_1.3 python3.6 输入以下命令进入到torch_1.3的…

vite基础学习笔记:13.Dialog 对话框 (用户注册与登录)

说明:自学做的笔记和记录,如有错误请指正 1. Dialog 对话框组件 目标效果:点击“登录/注册”,弹框 (1)创建全局组件,在官网中查询代码粘贴 (2) 注册和使用全局组件 &a…

NAT协议

目录 NAT 前言 NAT地址转换表 NAT分类 前言 静态NAT 192.168.1.2访问200.1.1.2执行过程 动态NAT 192.168.1.2访问200.1.1.2执行过程 NAPT 192.168.1.2的5000端口访问200.1.1.2的80端口执行过程 基本命令 配置动态NAPT转换 定义内外网接口 配置NAPT 静态NAPT配置…

智能井盖传感器实时批发价格

城市之中高楼大厦林立,越来越多的人群涌入一线城市或二线城市。同时即便是县城之中接连不断的高楼大厦拔地而起,住宅小区的面积在不断拓宽。随着这一系列情况的出现,首先要完善的是每一个地区的城市道路设施建设。无论小区还是在城市路面之中…

ubuntu 安装 zsh、ohmyzsh并配置必要插件

下述记录是完成全部操作后回忆记录得来,或有不准确。我只记录安装中确实用到的指令,参考资料中有扩展内容,记录如下: ubuntu使用zsh终端并安装nerd font字体——nerd font字体不太好安装,使用fonts-powerline替代。 Ub…

「Verilog学习笔记」使用子模块实现三输入数的大小比较

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点,刷题网站用的是牛客网 分析 题目要求编写子模块实现两个输入数的大小比较并输出较小值,可以使用if-else语句实现。同时要求在主模块中实现三个输入数值的大小比较,假设三个…

RDS for Mysql 到云数据库GaussDB

前言 该实验旨在指导用户使用DRS将RDS MySQL上的数据迁移到 GaussDB中。 本实验涉及数据复制服务DRS(Data Replication Service)、关系型数据库服务RDS(Relational Database Service)、GaussDB、数据管理服务DAS(Data…

LeetCode.6 N字形变换

一开始想的是真的创建一个数组 去按照题目所给的要求填入数据 最后输出不为空的数组项 但是不仅时间复杂度高 而且错误频繁出现 最终也没有提交成功 查阅题解后发现数组并不重要 假设我们忽略掉数组中的那些空白项 最终输出的结果就是numRows行的字符串的拼接 string conver…

MobaXterm配置SSHTunnel

本地与远程服务器之间存在防火墙,防火墙只允许SSH端口通过,为访问远程服务器,我们可以借助MobaXterm来与SSH服务器建立隧道,使得防火墙外的用户能够访问远程服务器 配置 打开SSHTunnel 新建SSH tunnel 点击开启就生效了&…

webgoat-Request Forgeries 请求伪造

(A8:2013) Request Forgeries Cross-Site Request Forgeries 跨站请求伪造,又称一键攻击或会话骑乘,简称CSRF (有时发音为 sea-surf)或 XSRF,是一种恶意利用网站,其中传输未经授权的命令 来自网站信任的用…

能源监测管理系统有哪些作用与效果?

随着全球能源的不断增加,能源的有限性与环境问题日益严重,用能管理企业需要一种高效的方法来管理能源与利用能源,因此能源监测管理系统成为了一种不可或缺的工具。 能源监测管理系统的重要性 1、实现节能减排的目标 通过系统,可…

apollo docker搭建

1 mysql搭建 先需要一个mysql,mysql我使用的是5.7,搭建过程忽略 2 数据导入 我们需要从github上下载apolloportaldb.sql, apolloconfigdb.sql 2个sql 随后cp apolloconfigdb.sql apolloconfigdbUat.sql cp apolloconfigdb.sql apolloconfigdbDev.sq…

实现dialog在页面随意拖拽

实现dialog在页面随意拖拽 1.先建一个文件如图所示: 文件名:dialog-directive.js 文件内容: import Vue from vue // v-dialogDrag: 弹窗拖拽Vue.directive(dialogDrag, {bind(el, binding, vnode, oldVnode) {// 获取拖拽内容的头部const dialogHeade…