MySQL的自增id定义初始值,然后不断增加步长。虽然自然数没有上限,但是代表这个数的字节长度是有定义的,计算机存储是有上限的。比如unsignedint是4个字节,上限是2^32-1,自增id用完了怎么办?表定义自增id表中定义的自增值达到上限后的逻辑是:申请下一个id时,获取的值保持不变。mysql>createtablet(idintunsignedauto_incrementprimarykey)auto_increment=4294967295;QueryOK,0rowsaffected(0.01sec)mysql>insertintotvalues(null);查询成功,1rowaffected(0.00sec)mysql>show创建表t;+--------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+|表|创建表|+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+|吨|CREATETABLE`t`(`id`intunsignedNOTNULLAUTO_INCREMENT,PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=4294967295DEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_general_ci|+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+1行在集合中(0.00秒)//成功插入一行4294967295mysql>insertintotvalues(null);ERROR1062(23000):Duplicateentry'4294967295'forkey't.PRIMARY'第一次插入成功后,表的AUTO_INCREMENT仍然是4294967295,导致第一次插入得到相同的自增id值,然后尝试执行插入语句。主键冲突2^32-1(4294967295)不是特别大的数字,频繁插入和删除数据的表可能会被耗尽。创建表时,需要考虑你的表是否可能达到上限。如果是这样,您应该创建一个8字节的无符号bigint。InnoDB系统自动递增row_id。如果你创建的InnoDB表没有指定主键,InnoDB会自动创建一个不可见的,6字节的row_id。InnoDB为所有没有主键的InnoDB表维护一个全局的dict_sys->row_id值。每插入一行数据,就将当前的dict_sys->row_id作为待插入数据的row_id,然后dict_sys->row_id加1。代码实现时,row_id为无符号长整型(bigintunsigned)长度为8个字节。但是InnoDB在设计的时候,只为row_id保留了6个字节,这样只有最后6个字节写入数据表,所以row_id可以写入数据表中的value,有两个特点:value表中写入row_id的范围是0到2^48-1当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的第一行,因为valuerow_id=0。再插入a=3,因为row_id=1,覆盖了上一行a=1,因为a=1的row_id也为1,所以应该在InnoDB表中主动创建一个自增主键:当表自增id达到上限时,创建一个主键插入数据时会报冲突错误。毕竟,覆盖数据意味着数据丢失,影响数据可靠性;报主键冲突,插入失败,影响可用性。一般可靠性优于可用性。xidredolog和binlog有一个共同的字段Xid,用来对应事务。MySQL内部如何生成Xid?MySQL内部维护了一个全局变量global_query_id,每执行一条语句就赋值给query_id,然后对该变量+1:如果当前语句是事务执行的第一条语句,MySQL也会将query_id赋给query_id在同一时间。事务的Xid:global_query_id是纯内存变量,重启后清除。所以同一个DBinstance,不同事务的Xid可能是一样的。但是MySQL重启后会重新生成一个新的binlog文件,这样就保证了同一个binlog文件中的Xid是唯一的。虽然重启MySQL不会导致两个相同的Xid出现在同一个binlog中,但是如果global_query_id达到上限,它会继续从0开始计数,理论上同一个Xid还是会出现在同一个binlog中。因为global_query_id8字节,上限为2^64-1。要做到这一点,需要满足:执行一个事务,假设Xid为A,然后执行2^64条查询语句,对于global_query_id来说太大了回到A2^64,这种可能性只存在于理论上。开始另一笔交易。这笔交易的Xid也是AInnodbtrx_idXid,由server层维护。InnoDB在内部使用Xid。为了将InnoDB事务与服务器相关联,InnoDB自己的trx_id是额外维护的一个事务标识(transactionid)。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_idS2的执行记录:mysql>useinformation_schema;读取表信息完成表名和列名你可以关闭这个功能,使用-ADatabasechangedmysql>selecttrx_id,trx_mysql_thread_idfrominnodb_trx;+---------------+--------------------+|trx_id|trx_mysql_thread_id|+---------------+--------------------+|421972504382792|70|+--------------------+--------------------+1组中的行(0.00秒)mysql>从innodb_trx中选择trx_id,trx_mysql_thread_id;+-------+--------------------+|trx_id|trx_mysql_thread_id|+--------+-------------------+|1355623|70|+--------+----------------------+1rowinset(0.01sec)S2检查这两个字段innodb_trx表,第二个字段trx_mysql_thread_id是线程id。显示threadid是为了说明这两个查询看到的事务对应的threadid都是5,也就是S1所在的线程。t2时显示的trx_id是一个很大的数字;t4时显示的trx_id是1289,看起来是一个比较正常的数字。为什么?在t1时刻,S1还没有被更新,是一个只读事务。对于只读事务,InnoDB不分配trx_id:当t1时,trx_id的值为0。而这个大的数字恰恰表明InnoDB在t3时刻S1执行insert之前并没有真正分配trx_id。因此,在t4时刻,S2发现trx_id的值为1289,除了明显的修改语句外,如果在select语句后添加forupdate,则不是只读事务。update和delete语句除了事务本身外,还涉及标记和删除旧数据,即将数据放入purgequeue,以便后续进行物理删除。这个操作也会增加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,所以sessionA开始的事务TA的低水位为2^48-1。t2时刻:sessionB执行第一次更新的事务idstatement=2^48-1且第二个事务id为0,执行本次更新后生成的数据版本上trx_id=0。t3时刻:sessionA执行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并继续增加。但是您不会在showprocesslist中看到两个相同的thread_id。因为MySQL使用唯一数组给新线程赋值thread_id逻辑:综上所述,每个自增id都有自己的应用场景,达到上限后的表现也不一样:表的自增id后达到上限,将再次申请。value的值不会变化,会导致插入数据时出现主键冲突错误。row_id达到上限后,会回到0,然后再增加。如果出现相同的row_id,后面写入的数据会覆盖前面的数据。Xid只需要不同即可。binlog文件中可以出现重复值。虽然理论上会有重复值,但概率极小,可以忽略不计。InnoDB的max_trx_id增量值会在每次重启MySQL时保存,所以我们文章中提到的脏读例子是必然出现的bug。我们有大把的时间
