背景
MYSQL的自增列在实际生产中应用的非常广泛,相信各位所在的公司or团队,MYSQL开发规范中一定会有要求尽量使用自增列去充当表的主键,为什么DBA会有这样的要求,各位在使用MYSQL自增列时遇到过哪些问题?这些问题是由什么原因造成的呢?本文由浅入深,带领大家彻底弄懂MYSQL的自增机制。
基础扫盲
t
(id
int(11) NOT NULL AUTO_INCREMENT,name
varchar(10) COLLATE utf8mb4_bin DEFAULT NULL,id
)[root@localhost][test1]> insert into t(name) values (‘test’);
Query OK, 1 row affected (0.01 sec)
[root@localhost][test1]> insert into t(name) values (‘test’);
ERROR 1062 (23000): Duplicate entry ‘2147483647’ for key ‘PRIMARY’
可以看到,第一个插入没问题,因为自增列的值为2147483647,这是达到了上限,还没有超过,第二行数据插入时,则报出主键重复,在达到上限后,无法再分配新的更大的自增值,也没有从1开始从头分配,在这里表的auto_increment值会一直是2147483647。
对于写入量大,且经常删除数据的表,自增id设为int类型还是偏小的,所以我们为了避免出现自增id涨满的情况,这边统一建议自增id的类型设为unsigned bingint,这样基本可以保障表的自增id是永远够用的。
自增id的优势
这里内容其实比较多,innodb是索引组织表,所以涉及到索引的知识,但这不是本文的重点,我简单列出一个索引结构图来说明吧:
快速回顾索引知识:
其实能够理解索引的结构及索引写入插入、更新的原理,则自然就明白为何建议使用自增id。这里我直接列出使用自增id 当主键的好处吧:
顺序写入,避免了叶的分裂,数据写入效率好
缩小了表的体积,特别是相比于UUID当主键,甚至组合字段当主键时,效果更明显
查询效率好,原因就是我上面说到索引知识的第二点。
某些情况下,我们可以利用自增id来统计大表的大致行数。
在数据归档or垃圾数据清理时,也可方便的利用这个id去操作,效率高。
自增id的问题
容易出现不连续的id
有的同志会发现,自己的表中id值存在空洞,如类似于1、2、3、8、9、10这样,有的适合有想依赖于自增id的连续性来实现业务逻辑,所以会想方设法去修改id让其变的连续,其实,这是没有必要的,这一块的业务逻辑交由MySQL实现是很不理智的,表的记录小还好,要是表的数据量很大,修改起来就糟糕了。那么,为什么自增id会容易出现空洞呢?
自增id的修改机制如下:
在MySQL里面,如果字段id被定义为AUTO_INCREMENT,在插入一行数据的时候,自增值的行为如下:
如果插入数据时id字段指定为0、null 或未指定值,那么就把这个表当前的
AUTO_INCREMENT值填到自增字段;
如果插入数据时id字段指定了具体的值,就直接使用语句里指定的值。
根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设,某次要插入的值是X,当前的自增值是Y。
如果X
如果X≥Y,就需要把当前自增值修改为新的自增值。
新的自增值生成算法是:从auto_increment_offset开始,以auto_increment_increment为步长,持续叠加,直到找到第一个大于X的值,作为新的自增值。
Insert、update、delete操作会让id不连续。
Delete、update:删除中间数据,会造成空动,而修改自增id值,也会造成空洞(这个很少)。
Insert:插入报错(唯一键冲突与事务回滚),会造成空洞,因为这时候自增id已经分配出去了,新的自增值已经生成,如下面例子:
[root@localhost][test1]> select * from t;
±—±-----+
| id | name |
±—±-----+
| 1 | aaa |
| 2 | aaa |
| 3 | aaa |
| 4 | aaa |
±—±-----+
4 rows in set (0.00 sec)
[root@localhost][test1]> select Auto_increment from information_schema.tables where table_name=‘t’;
±---------------+
| Auto_increment |
±---------------+
| 5 |
±---------------+
1 row in set (0.00 sec)
[root@localhost][test1]> begin;
Query OK, 0 rows affected (0.00 sec)
[root@localhost][test1]> insert into t(name) values(‘aaa’);
Query OK, 1 row affected (0.00 sec)
[root@localhost][test1]> select * from t;
±—±-----+
| id | name |
±—±-----+
| 1 | aaa |
| 2 | aaa |
| 3 | aaa |
| 4 | aaa |
| 5 | aaa |
±—±-----+
5 rows in set (0.00 sec)
[root@localhost][test1]> select Auto_increment from information_schema.tables where table_name=‘t’;
±---------------+
| Auto_increment |
±---------------+
| 6 |
±---------------+
1 row in set (0.00 sec)
[root@localhost][test1]> rollback;
Query OK, 0 rows affected (0.00 sec)
[root@localhost][test1]> select Auto_increment from information_schema.tables where table_name=‘t’;
±---------------+
| Auto_increment |
±---------------+
| 6 |
±---------------+
1 row in set (0.01 sec)
[root@localhost][test1]> select * from t;
±—±-----+
| id | name |
±—±-----+
| 1 | aaa |
| 2 | aaa |
| 3 | aaa |
| 4 | aaa |
±—±-----+
4 rows in set (0.00 sec)
可以看到,虽然事务回滚了,但自增id已经回不到从前啦,唯一键冲突也是这样的,这里就不做测试了。
在批量插入时(insert select等),也存在空洞的问题。看下面实验:
[root@localhost][test1]> select * from t;
±—±-----+
| id | name |
±—±-----+
| 1 | aaa |
| 2 | aaa |
| 3 | aaa |
| 4 | aaa |
±—±-----+
4 rows in set (0.00 sec)
[root@localhost][test1]> select Auto_increment from information_schema.tables where table_name=‘t’;
±---------------+
| Auto_increment |
±---------------+
| 5 |
±---------------+
1 row in set (0.00 sec)
[root@localhost][test1]> insert into t(name) select name from t;
Query OK, 4 rows affected (0.04 sec)
Records: 4 Duplicates: 0 Warnings: 0
[root@localhost][test1]> select * from t;
±—±-----+
| id | name |
±—±-----+
| 1 | aaa |
| 2 | aaa |
| 3 | aaa |
| 4 | aaa |
| 5 | aaa |
| 6 | aaa |
| 7 | aaa |
| 8 | aaa |
±—±-----+
8 rows in set (0.00 sec)
[root@localhost][test1]> select Auto_increment from information_schema.tables where table_name=‘t’;
±---------------+
| Auto_increment |
±---------------+
| 12 |
±---------------+
1 row in set (0.00 sec)
可以看到,批量插入,导致下一个id值不为9了,再插入数据,即产生了空洞,这里是由mysql申请自增值的机制所造成的,MySQL在批量插入时,若一个值申请一个id,效率太慢,影响了批量插入的速度,故mysql采用下面的策略批量申请id。
验证一下,再来批量插入,自增id的值会变成多少呢?
[root@localhost][test1]> insert into t(name) select name from t;
Query OK, 8 rows affected (0.02 sec)
Records: 8 Duplicates: 0 Warnings: 0
[root@localhost][test1]> select Auto_increment from information_schema.tables where table_name=‘t’;
±---------------+
| Auto_increment |
±---------------+
| 27 |
±---------------+
1 row in set (0.00 sec)
[root@localhost][test1]> select * from t;
±—±-----+
| id | name |
±—±-----+
| 1 | aaa |
| 2 | aaa |
| 3 | aaa |
| 4 | aaa |
| 5 | aaa |
| 6 | aaa |
| 7 | aaa |
| 8 | aaa |
| 12 | aaa |
| 13 | aaa |
| 14 | aaa |
| 15 | aaa |
| 16 | aaa |
| 17 | aaa |
| 18 | aaa |
| 19 | aaa |
±—±-----+
16 rows in set (0.00 sec)
自增锁
在对自增列进行操作时,存在着自增锁,mysql的innodb_autoinc_lock_mode参数控制着自增锁的上锁机制。该参数有0、1、2三种模式:
0:语句执行结束后释放自增锁,MySQL5.0时采用这种模式,并发度较低。
1:mysql的默认设置。普通的insert语句申请后立马释放,insert select、replace insert、load data等批量插入语句要等语句执行结束后才释放,并发读得到提升
2:所有的语句都是申请后立马释放,并发度大大提升!但是在binlog为statement格式时,主从数据会发生不一致。这一块网上有很多例子,我不做介绍了。
大家生产上该参数设为2,然后binlog设为row格式就行
总结
在彻底了解了MYSQL的自增机制以后,在实际生产中就能灵活避坑,这里建议不要用自增id值去当表的行数,当需要对大表准确统计行数时,可以去count(*)从库,如果业务很依赖大表的准确行数,直接弄个中间表来统计,或者考虑要不要用mysql的innodb来存储数据,这个是需要自己去权衡。另外对于要求很高的写入性能,但写入量又比较大的业务,自增id的使用依然存在热点写入的问题,存在性能瓶颈,这时候可通过分库分表来解决。