MySQL里有很多自增的id,每个自增id 都是定义了初始值,然后不停地往上加步长。虽然自然数是没有上限的,但是在计算机里,只要定义了表示这个数的字节长度,那它就有上限。
一、AUTO_INCREMENT
CREATE TABLE `dvd_category` (
`dvd_category_id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '分类ID',
`dvd_category_name` varchar(64) DEFAULT NULL COMMENT '分类名称',
`dvd_category_payment` decimal(10,2) NOT NULL COMMENT '每日租赁费用',
`dvd_category_status` int(4) NOT NULL DEFAULT '0' COMMENT '分类状态,0-未启用、1-启用',
`created_date` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建日期',
`last_modified_date` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次修改日期',
PRIMARY KEY (`dvd_category_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4294967295 DEFAULT CHARSET=utf8;
测试库找了一张包含自增主键的表,通过设定AUTO_INCREMENT的值,重新建表。将自增id设置为2^32-1,也即UINT最大值。
INSERT INTO `dvd_db`.`dvd_category`
(`dvd_category_name`, `dvd_category_payment`, `dvd_category_status`)
VALUES ('Movies', '20.00', '1');
插入一条数据,表中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个字节的长度,row_id能写到数据表中的值,有两个特征:
- row_id写入表中的值范围,是从0到2^48-1;
- 当dict_sys.row_id=2^48时,如果再有插入数据的行为来申请row_id,拿到以后再取最后6个字节的为0。
PS:写入表的row_id是从0开始到2^48-1。达到上限后,下一个值就是 0,然后继续循环。
如果一个MySQL实例跑得足够久的话,是有可能达到2^48-1这个上限的。在InnoDB逻辑中,申请到row_id=N后,就将这行数据写入表中;如果表中已经存在row_id=N的行,新写入的行就会覆盖原有的行(通过gdb可以验证)。
CREATE TABLE `t` (
`id` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
gdb -p 21581 -ex 'p dict_sys.row_id=1' --batch
# 插入五条数据,1、2、3、4、5
INSERT INTO t VALUES (1);
SELECT * FROM t;
+------+ | id |
+------+ | 5 |
| 2 |
| 3 |
| 4 |
| 1 |
+------+ 5 rows in set (0.00 sec)
gdb -p 21581 -ex 'p dict_sys.row_id=281474976710655' --batch
# 插入五条数据,10、20、30、40、50
INSERT INTO t VALUES (10);
SELECT * FROM t;
+------+ | id |
+------+ | 20 |
| 30 |
| 40 |
| 50 |
| 4 |
| 5 |
| 10 |
+------+ 7 rows in set (0.00 sec)
如上述结论,之前的数据被新的INSERT覆盖。所以需要在InnoDB表中主动创建自增主键,而不是依靠系统提供的id。
数据库系统作为一个7*24小时全年无休的服务,设计表时是需要考虑数据量以及边界的。
对于分布式的自增id,以后进行总结。
参考:
「极客时间」-《MySQL实战45讲》-《第45讲 | 自增id用完怎么办》 – 2019-02-25 林晓斌