之前我见过有的业务设计依赖于自增主键的连续性,也就是说,这个设计假设自增主键是连续的。但实际上,这样的假设是错的,因为自增主键不能保证连续递增。
今天这篇文章,我们就来说说这个问题,看看什么情况下自增主键会出现 “空洞”?
为了便于说明,我们创建一个表t,其中id是自增主键字段、c是唯一索引。
自增值保存在哪儿?
在这个空表t里面执行
insert into t values(null, 1, 1)
;插入一行数据,再执行show create table
命令,就可以看到如下图所示的结果:AUTO_INCREMENT
值可以看到,表定义里面出现了一个
AUTO_INCREMENT=2
,表示下一次插入数据时,如果需要自动生成自增值,会生成id=2
。其实,这个输出结果容易引起这样的误解:自增值是保存在表结构定义里的。实际上,表的结构定义存放在后缀名为.frm的文件中,但是并不会保存自增值。
不同的引擎对于自增值的保存策略不同。
MyISAM
引擎的自增值保存在数据文件中。
InnoDB
引擎的自增值,其实是保存在了内存里,并且到了MySQL 8.0
版本后,才有了“自增值持久化”的能力,也就是才实现了“如果发生重启,表的自增值可以恢复为MySQL
重启前的值”,具体情况是:- 在MySQL 5.7及之前的版本,自增值保存在内存里,并没有持久化。
- 在MySQL 8.0版本,将自增值的变更记录在了
redo log
中,重启的时候依靠redo log
恢复重启之前的值。
每次重启后,第一次打开表的时候,都会去找自增值的最大值
max(id)
,然后将max(id)+1
作为这个表当前的自增值。举例来说,如果一个表当前数据行里最大的id是10,
AUTO_INCREMENT=11
。这时候,我们删除id=10的行,AUTO_INCREMET
还是11。但如果马上重启实例,重启后这个表的AUTO_INCREMENT
就会变成10。也就是说,MySQL重启可能会修改一个表的AUTO_INCREMENT
的值。理解了MySQL对自增值的保存策略以后,我们再看看自增值修改机制。
自增值修改机制
在MySQL里面,如果字段id被定义为
AUTO_INCREMENT
,在插入一行数据的时候,自增值的行为如下:- 如果插入数据时id字段指定为0、null 或未指定值,那么就把这个表当前的
AUTO_INCREMENT
值填到自增字段;
- 如果插入数据时id字段指定了具体的值,就直接使用语句里指定的值。
根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设,某次要插入的值是X,当前的自增值是Y。
- 如果X<Y,那么这个表的自增值不变;
- 如果X≥Y,就需要把当前自增值修改为新的自增值。
新的自增值生成算法是:从
auto_increment_offset
开始,以auto_increment_increment
为步长,持续叠加,直到找到第一个大于X的值,作为新的自增值。其中,
auto_increment_offset
和 auto_increment_increment
是两个系统参数,分别用来表示自增的初始值和步长,默认值都是1。备注:在一些场景下,使用的就不全是默认值。比如,双M的主备结构里要求双写的时候,我们就可能会设置成auto_increment_increment=2
,让一个库的自增id都是奇数,另一个库的自增id都是偶数,避免两个库生成的主键发生冲突。
当
auto_increment_offset
和auto_increment_increment
都是1的时候,新的自增值生成逻辑很简单,就是:- 如果准备插入的值>=当前自增值,新的自增值就是“准备插入的值+1”;
- 否则,自增值不变。
这就引入了我们文章开头提到的问题,在这两个参数都设置为1的时候,自增主键id却不能保证是连续的,这是什么原因呢?
自增值的修改时机
要回答这个问题,我们就要看一下自增值的修改时机。
唯一索引导致自增id不连续
假设,表t里面已经有了(1,1,1)这条记录,这时我再执行一条插入数据命令:
这个语句的执行流程就是:
- 执行器调用
InnoDB
引擎接口写入一行,传入的这一行的值是(0,1,1)
;
InnoDB
发现用户没有指定自增id
的值,获取表t当前的自增值2;
- 将传入的行的值改成
(2,1,1)
;
- 将表的自增值改成3;
- 继续执行插入数据操作,由于已经存在
c=1
的记录,所以报Duplicate key error
,语句返回。
对应的执行流程图如下:
insert(null, 1,1)
唯一键冲突可以看到,这个表的自增值改成3,是在真正执行插入数据的操作之前。这个语句真正执行的时候,因为碰到唯一键c冲突,所以
id=2
这一行并没有插入成功,但也没有将自增值再改回去。所以,在这之后,再插入新的数据行时,拿到的自增id就是3。也就是说,出现了自增主键不连续的情况。
如下图所示就是完整的演示结果。
可以看到,这个操作序列复现了一个自增主键id不连续的现场(没有
id=2
的行)。可见,唯一键冲突是导致自增主键id不连续的第一种原因。事务回滚导致的id不连续
同样地,事务回滚也会产生类似的现象,这就是第二种原因。
下面这个语句序列就可以构造不连续的自增id,你可以自己验证一下。
你可能会问,为什么在出现唯一键冲突或者回滚的时候,MySQL没有把表t的自增值改回去呢?如果把表t的当前自增值从3改回2,再插入新数据的时候,不就可以生成id=2的一行数据了吗?
自增值不能回滚原因
其实,MySQL这么设计是为了提升性能。接下来,我就跟你分析一下这个设计思路,看看自增值为什么不能回退。
假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增id,肯定要加锁,然后顺序申请。
- 假设事务A申请到了
id=2
, 事务B申请到id=3
,那么这时候表t的自增值是4,之后继续执行。
- 事务B正确提交了,但事务A出现了唯一键冲突。
- 如果允许事务A把自增id回退,也就是把表t的当前自增值改回2,那么就会出现这样的情况:表里面已经有
id=3
的行,而当前的自增id值是2。
- 接下来,继续执行的其他事务就会申请到
id=2
,然后再申请到id=3
。这时,就会出现插入语句报错“主键冲突”。
而为了解决这个主键冲突,有两种方法:
- 每次申请id之前,先判断表里面是否已经存在这个id。如果存在,就跳过这个id。但是,这个方法的成本很高。因为,本来申请id是一个很快的操作,现在还要再去主键索引树上判断id是否存在。
- 把自增id的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。
可见,这两个方法都会导致性能问题。造成这些麻烦的罪魁祸首,就是我们假设的这个“允许自增id回退”的前提导致的。
因此,InnoDB放弃了这个设计,语句执行失败也不回退自增id。也正是因为这样,所以才只保证了自增id是递增的,但不保证是连续的。
自增锁的优化
可以看到,自增id锁并不是一个事务锁,而是每次申请完就马上释放,以便允许别的事务再申请。其实,在
MySQL 5.1
版本之前,并不是这样的。接下来,我会先给你介绍下自增锁设计的历史,这样有助于你分析接下来的一个问题。
在
MySQL 5.0
版本的时候,自增锁的范围是语句级别。也就是说,如果一个语句申请了一个表自增锁,这个锁会等语句执行结束以后才释放。显然,这样设计会影响并发度。MySQL 5.1.22
版本引入了一个新策略,新增参数innodb_autoinc_lock_mode
,默认值是1。- 这个参数的值被设置为0时,表示采用之前MySQL 5.0版本的策略,即语句执行结束后才释放锁;
- 这个参数的值被设置为1时:
- 普通insert语句,自增锁在申请之后就马上释放;
- 类似
insert … select
这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;
- 这个参数的值被设置为2时,所有的申请自增主键的动作都是申请后就释放锁。
你一定有两个疑问:为什么默认设置下,
insert … select
要使用语句级的锁?为什么这个参数的默认值不是2?答案是,这么设计还是为了数据的一致性。
批量更新导致id不连续
我们一起来看一下这个场景:
在这个例子里,我往表t1中插入了4行数据,然后创建了一个相同结构的表t2,然后两个session同时执行向表t2中插入数据的操作。
你可以设想一下,如果session B是申请了自增值以后马上就释放自增锁,那么就可能出现这样的情况:
- session B先插入了两个记录,(1,1,1)、(2,2,2);
- 然后,session A来申请自增id得到id=3,插入了(3,5,5);
- 之后,session B继续执行,插入两条记录(4,3,3)、 (5,4,4)。
你可能会说,这也没关系吧,毕竟session B的语义本身就没有要求表t2的所有行的数据都跟session A相同。
是的,从数据逻辑上看是对的。但是,如果我们现在的
binlog_format=statement
,你可以设想下,binlog会怎么记录呢?由于两个session是同时执行插入数据命令的,所以binlog里面对表t2的更新日志只有两种情况:要么先记session A的,要么先记session B的。
但不论是哪一种,这个binlog拿去从库执行,或者用来恢复临时实例,备库和临时实例里面,session B这个语句执行出来,生成的结果里面,id都是连续的。这时,这个库就发生了数据不一致。
你可以分析一下,出现这个问题的原因是什么?
其实,这是因为原库session B的insert语句,生成的id不连续。这个不连续的id,用statement格式的binlog来串行执行,是执行不出来的。
批量更新自增锁优化
而要解决这个问题,有两种思路:
- 一种思路是,让原库的批量插入数据语句,固定生成连续的id值。所以,自增锁直到语句执行结束才释放,就是为了达到这个目的。
- 另一种思路是,在binlog里面把插入数据的操作都如实记录进来,到备库执行的时候,不再依赖于自增主键去生成。这种情况,其实就是
innodb_autoinc_lock_mode
设置为2,同时binlog_format
设置为row。
因此,在生产上,尤其是有
insert … select
这种批量插入数据的场景时,从并发插入数据性能的角度考虑,我建议你这样设置:innodb_autoinc_lock_mode=2
,并且 binlog_format=row
.这样做,既能提升并发性,又不会出现数据一致性问题。需要注意的是,我这里说的批量插入数据,包含的语句类型是
insert … select
、replace … select
和load data
语句。但是,在普通的insert语句里面包含多个value值的情况下,即使
innodb_autoinc_lock_mode
设置为1,也不会等语句执行完成才释放锁。因为这类语句在申请自增id的时候,是可以精确计算出需要多少个id的,然后一次性申请,申请完成后锁就可以释放了。也就是说,批量插入数据的语句,之所以需要这么设置,是因为“不知道要预先申请多少个id”。
既然预先不知道要申请多少个自增id,那么一种直接的想法就是需要一个时申请一个。
但如果一个
select … insert
语句要插入10万行数据,按照这个逻辑的话就要申请10万次。显然,这种申请自增id的策略,在大批量插入数据的情况下,不但速度慢,还会影响并发插入的性能。因此,对于批量插入数据的语句,MySQL有一个批量申请自增id的策略:
- 语句执行过程中,第一次申请自增id,会分配1个;
- 1个用完以后,这个语句第二次申请自增id,会分配2个;
- 2个用完以后,还是这个语句,第三次申请自增id,会分配4个;
- 依此类推,同一个语句去申请自增id,每次申请到的自增id个数都是上一次的两倍。
举个例子,我们一起看看下面的这个语句序列:
insert…select
,实际上往表t2中插入了4行数据。但是,这四行数据是分三次申请的自增id,第一次申请到了id=1
,第二次被分配了id=2
和id=3
, 第三次被分配到id=4
到id=7
。由于这条语句实际只用上了4个id,所以
id=5
到id=7
就被浪费掉了。之后,再执行insert into t2 values(null, 5,5)
,实际上插入的数据就是(8,5,5)。这是主键id出现自增id不连续的第三种原因。
今天,我们从“自增主键为什么会出现不连续的值”这个问题开始,首先讨论了自增值的存储。
在MyISAM引擎里面,自增值是被写在数据文件上的。而在InnoDB中,自增值是被记录在内存的。MySQL直到8.0版本,才给InnoDB表的自增值加上了持久化的能力,确保重启前后一个表的自增值不变。
然后,我和你分享了在一个语句执行过程中,自增值改变的时机,分析了为什么MySQL在事务回滚的时候不能回收自增id。
MySQL 5.1.22
版本开始引入的参数innodb_autoinc_lock_mode
,控制了自增值申请时的锁范围。从并发性能的角度考虑,我建议你将其设置为2,同时将binlog_format
设置为row。我在前面的文章中其实多次提到,binlog_format
设置为row,是很有必要的。今天的例子给这个结论多了一个理由。MySQL对自增主键锁做了优化,尽量在申请到自增id以后,就释放自增锁。因此,insert语句是一个很轻量的操作。不过,这个结论对于“普通的insert语句”才有效。也就是说,还有些insert语句是属于“特殊情况”的,在执行过程中需要给其他资源加锁,或者无法在申请到自增id以后就立马释放自增锁。
那么,今天这篇文章,我们就一起来聊聊这个话题。
自增id用完怎么办?
MySQL里有很多自增的id,每个自增id都是定义了初始值,然后不停地往上加步长。虽然自然数是没有上限的,但是在计算机里,只要定义了表示这个数的字节长度,那它就有上限。比如,无符号整型(unsigned int)是4个字节,上限就是2的32次-1。
既然自增id有上限,就有可能被用完。但是,自增id用完了会怎么样呢?
今天这篇文章,我们就来看看MySQL里面的几种自增id,一起分析一下它们的值达到上限以后,会出现什么情况。
表定义自增值id
说到自增id,你第一个想到的应该就是表结构定义里的自增字段,自增主键id。
表定义的自增值达到上限后的逻辑是:再申请下一个id时,得到的值保持不变。
我们可以通过下面这个语句序列验证一下:
可以看到,第一个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
能写到数据表中的值,就有两个特征:row_id
写入表中的值范围,是从0到2的48次-1;
- 当
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改变量这个操作是为了便于我们复现问题,只能在测试环境使用。row_id
用完的验证序列row_id
用完的效果验证可以看到,在我用gdb将
dict_sys.row_id
设置为2的48次-1
之后,再插入的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个字节,这个自增值的上限是2的64次-1
。要出现这种情况,必须是下面这样的过程:- 执行一个事务,假设Xid是A;
- 接下来执行2的64次查询语句,让
global_query_id
回到A;
- 再启动一个事务,这个事务的Xid也是A。
不过,2的64次这个值太大了,大到你可以认为这个可能性只会存在于理论上。
Innodb trx_id
Xid和InnoDB的
trx_id
是两个容易混淆的概念。Xid是由server层维护的。InnoDB内部使用Xid,就是为了能够在InnoDB事务和server之间做关联。但是,InnoDB自己的trx_id,是另外维护的。
其实,你应该非常熟悉这个
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
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
。也就是说:- 在T1时刻,
trx_id
的值其实就是0。而这个很大的数,只是显示用的。一会儿我会再和你说说这个数据的生成逻辑。
- 直到session A 在T3时刻执行insert语句的时候,InnoDB才真正分配了
trx_id
。所以,T4时刻,session B查到的这个trx_id
的值就是1289。
需要注意的是,除了显而易见的修改类语句外,如果在select 语句后面加上for update,这个事务也不是只读事务。
在上一篇文章的评论区,有同学提出,实验的时候发现不止加1。这是因为:
- update 和 delete语句除了事务本身,还涉及到标记删除旧数据,也就是要把数据放到
purge
队列里等待后续物理删除,这个操作也会把max_trx_id
+1, 因此在一个事务中至少加2;
- InnoDB的后台操作,比如表的索引信息统计这类操作,也是会启动内部事务的,因此你可能看到,
trx_id
值并不是按照加1递增的。
那么,T2时刻查到的这个很大的数字是怎么来的呢?
其实,这个数字是每次查询的时候由系统临时计算出来的。它的算法是:把当前事务的trx变量的指针地址转成整数,再加上2的48次。使用这个算法,就可以保证以下两点:
- 因为同一个只读事务在执行期间,它的指针地址是不会变的,所以不论是在
innodb_trx
还是在innodb_locks
表里,同一个只读事务查出来的trx_id
就会是一样的。
- 如果有并行的多个只读事务,每个事务的trx变量的指针地址肯定不同。这样,不同的并发只读事务,查出来的
trx_id
就是不同的。
那么,为什么还要再加上2的48次呢?
在显示值里面加上2的48次,目的是要保证只读事务显示的
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
达到248-1的上限,然后从0开始的情况。当达到这个状态后,MySQL就会持续出现一个脏读的bug,我们来复现一下这个bug。
首先我们需要把当前的
max_trx_id
先修改成248-1。注意:这个case里使用的是可重复读隔离级别。具体的操作流程如下:由于我们已经把系统的
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实例服务时间够长,就会必然出现的。
当然,这个例子更现实的意义是,可以加深我们对低水位和数据可见性的理解。你也可以借此机会再回顾下第8篇文章《事务到底是隔离的还是不隔离的?》中的相关内容。
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
的时候,逻辑代码是这样的:这个代码逻辑简单而且实现优雅,相信你一看就能明白。
今天这篇文章,我给你介绍了MySQL不同的自增id达到上限以后的行为。数据库系统作为一个可能需要7*24小时全年无休的服务,考虑这些边界是非常有必要的。
每种自增id有各自的应用场景,在达到上限后的表现也不同:
- 表的自增id达到上限后,再申请时它的值就不会改变,进而导致继续插入数据时报主键冲突的错误。
- 00达到上限后,则会归0再重新递增,如果出现相同的
row_id
,后写的数据会覆盖之前的数据。
- Xid只需要不在同一个binlog文件中出现重复值即可。虽然理论上会出现重复值,但是概率极小,可以忽略不计。
- InnoDB的
max_trx_id
递增值每次MySQL重启都会被保存起来,所以我们文章中提到的脏读的例子就是一个必现的bug,好在留给我们的时间还很充裕。
- thread_id是我们使用中最常见的,而且也是处理得最好的一个自增id逻辑了。
当然,在MySQL里还有别的自增id,比如
table_id
、binlog文件序号等,就留给你去验证和探索了。不同的自增id有不同的上限值,上限值的大小取决于声明的类型长度