MySQL -- 自增ID耗尽
显示定义ID
表定义的自增值ID达到上限后,在申请下一个ID时,得到的值保持不变
1 | -- (2^32-1) = 4,294,967,295 |
InnoDB row_id
- 如果创建的InnoDB表没有指定主键,那么InnoDB会创建一个不可见的,长度为6 Bytes的
row_id
- InnoDB维护一个全局的
dict_sys.row_id
值,所有无主键的InnoDB表,每插入一行数据- 都将当前的
dict_sys.row_id
值作为要插入数据的row_id
,然后把dict_sys.row_id
的值+1
- 都将当前的
- 代码实现上,row_id是一个
8 Bytes
的BIGINT UNSIGNED
- 但InnoDB设计时,给
row_id
只保留了6 Bytes
的空间,写到数据表时只会存放最后的6 Bytes
row_id
的取值范围:0 ~ 2^48-1
- 达到上限后,下一个值就是0
- 但InnoDB设计时,给
- 在InnoDB里面,申请到
row_id=N
后,就将这行数据写入表中- 如果表中已经有
row_id=N
的行,新写入的行就会覆盖原有的行
- 如果表中已经有
- 推荐显示创建自增主键
- 表自增ID达到上限后,再插入数据时会报主键冲突的错误,影响的是_可用性_
- 而覆盖数据,意味着数据丢失,影响的是_可靠性_
- 一般来说,_可靠性优于可用性_
XID
redolog
和binlog
相配合的时候,有一个共同的字段XID
,_对应一个事务_- 生成逻辑
- MySQL内部维护一个全局变量
global_query_id
- 每次执行语句的时候将
global_query_id
赋值给Query_id
,然后global_query_id
+1 - 如果当前语句是这个事务执行的第一条语句,把
Query_id
赋值给这个事务的XID
- MySQL内部维护一个全局变量
global_query_id
是一个纯内存变量,重启之后清零- 因此,在同一个数据库实例中,不同事务的
XID
也有可能是相同的 - MySQL重启之后,会重新生成新的
binlog
- 保证:_同一个binlog文件里,XID是唯一的_
global_query_id
达到上限后,就会继续从0开始计数- 因此理论上,同一个
binlog
还是会出现相同的XID
,只是概率极低
- 因此理论上,同一个
- 因此,在同一个数据库实例中,不同事务的
global_query_id
是8 Bytes
,上限为2^64-1
- 执行一个事务,假设
XID
是A - 接下来执行
2^64
次查询语句,让global_query_id
回到A - 再启动一个事务,这个事务的
XID
也是A
- 执行一个事务,假设
InnoDB trx_id
XID
是由Server层维护的- InnoDB内部使用的是
trx_id
,为的是能够在InnoDB事务和Server层之间做关联 - InnoDB内部维护一个
max_trx_id
的全局变量- 每次需要申请一个新的
trx_id
,就获得max_trx_id
的当前值,然后max_trx_id
+1
- 每次需要申请一个新的
- InnoDB数据可见性的核心思想
- 每一行数据都记录了更新它的
trx_id
- 当一个事务读到一行数据的时候,判断数据可见性的方法
- 事务的一致性视图和这行数据的
trx_id
做对比
- 事务的一致性视图和这行数据的
- 每一行数据都记录了更新它的
- 对于正在执行的事务,可以通过
information_schema.innodb_trx
看到事务的trx_id
操作序列
时刻 | session A | session B |
---|---|---|
T1 | BEGIN; SELECT * FROM t LIMIT 1; |
|
T2 | USE information_schema; SELECT trx_id,trx_mysql_thread_id FROM innodb_trx; |
|
T3 | INSERT INTO t VALUES (null); | |
T4 | SELECT trx_id,trx_mysql_thread_id FROM innodb_trx; |
1 | -- T2时刻 |
trx_mysql_thread_id=30
就是线程ID,即session A所在的线程- T1时刻,
trx_id
的值其实为0,而很大的值只是为了显示用的(区别于普通的读写事务) - T2时刻,
trx_id
是一个很大的数字,因为在T1时刻,session A并未涉及更新操作,是一个只读事务- 对于只读事务,InnoDB不会分配
trx_id
- 对于只读事务,InnoDB不会分配
- session A在T3时刻执行
INSERT
语句时,InnoDB才真正分配trx_id
只读事务
- 在上面的T2时刻,很大的
trx_id
是由系统临时计算出来的- 把当前事务的
trx
变量的指针地址转成整数,再加上2^48
- 把当前事务的
- 同一个只读事务在执行期间,它的指针地址是不会变的
- 不论是在
innodb_trx
还是innodb_locks
表里,同一个只读事务查出来的trx_id
都是一样的
- 不论是在
- 如果有多个并行的只读事务,每个事务的trx变量的指针地址肯定是不同的
- 不同的并发只读事务,查出来的trx_id是不同的
- 加上
2^48
的目的:保证只读事务显示的trx_id
值比较大,用于区别普通的读写事务 trx_id
与row_id
的逻辑类似,定义长度为8 Bytes
- 在理论上,可能会出现一个读写事务与一个只读事务显示的trx_id相同的情况
- 但概率极低,并且没有什么实质危害
- 只读事务不分配
trx_id
的好处- 可以减少事务视图里面活跃数组的大小
- 当前正在运行的只读事务,是不影响数据的可见性判断
- 因此,在创建事务的一致性视图时,只需要拷贝读写事务的
trx_id
- 可以减少
trx_id
的申请次数- 在InnoDB里,即使只执行一条普通的
SELECT
语句,在执行过程中,也要对应一个只读事务 - 如果普通查询语句不申请
trx_id
,就可以大大减少并发事务申请trx_id
的锁冲突 - 由于只读事务不分配
trx_id
,trx_id
的增加速度会变慢
- 在InnoDB里,即使只执行一条普通的
- 可以减少事务视图里面活跃数组的大小
max_trx_id
会持久化存储,重启不会重置为0,只有到达2^48-1
的上限后,才会重置为0
thread_id
SHOW PROCESSLIST
的第一列就是thread_id
- 系统保存了一个环境变量
thread_id_counter
- 每新建一个连接,就将
thread_id_counter
赋值给这个新连接的线程变量
- 每新建一个连接,就将
thread_id_counter
定义为4 Bytes
,因此达到2^32-1
后就会重置为0- 但不会在
SHOW PROCESSLIST
里面看到两个相同的thread_id - 因为MySQL设计了一个唯一数组的逻辑,给新线程分配thread_id,逻辑代码如下
- 但不会在
1 | do { |
参考资料
《MySQL实战45讲》
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.