自增主键是我们在设计数据库表结构时经常使用的主键生成策略,主键的生成可以完全依赖数据库,无需人为干预,在新增数据的时候,我们只需要将主键设置为null,0或者不设置该字段,数据库就会为我们自动生成一个主键值。而且,自增主键是整数型,单调递增,对创建索引也十分友好,
自增主键使用起来方便、简单,但是你真的了解它吗?或者可以尝试回答一下几个问题?
1.自增主键标识符 auto_increment 如何增长?
2.并发环境下,数据库是如何保证主键值的正确性的?
3.可以用最大主键代替count()吗?
1.关键字auto_increment
是用来定义主键是自增的。
定义自增主键:
CREATE TABLE `game` (
`id` int(10) unsigned not null auto_increment comment '主键',
`game_name` varchar(128) not null default '' comment '名称',
primary key (`id`) USING BTREE,
key `ix_status` (`status`) USING BTREE
) ENGINE=InnoDB;
2.auto_increment
用来记录,下一次插入记录主键的值。
通过命令
select auto_increment from information_schema.tables where table_schema='ad' and table_name='game';
可以查看表 game
当前 auto_increment
的值。
3.auto_increment
在建表是设置主键的自增的起始值。
CREATE TABLE `game` (
`id` int(10) unsigned not null auto_increment comment '主键',
`game_name` varchar(128) not null default '' comment '名称',
primary key (`id`) USING BTREE,
key `ix_status` (`status`) USING BTREE
) ENGINE=InnoDB auto_increment = 5;
表示自增主键以5为起始,也就是插入的第一条记录的主键值为5.
也可以通过以下命令,进行设置。
alter table game auto_increment=5;
insert into game (id,game_name,game_id) value(null,'game01',1);
insert into game (id,game_name,game_id) value(0,'game02',2);
insert into game (game_name,game_id) value('game03',3);
select * from game;
结果如下:
5 game05 5
6 game06 6
7 game07 7
注:auto_increment
的起始值为5.
如果指定的主键小于auto_increment时,auto_increment不发生变化
查看当前 auto_increment 的值
select auto_increment from information_schema.tables where table_schema='ad' and table_name='game';
result=8
插入一个主键比当前auto_increment
小的值
insert into game (id,game_name,game_id) value(4,'game04',4);
查看当前game表中的数据
select * from game;
result:
id game_name game_id
4 game04 4
5 game05 5
6 game06 6
7 game07 7
查看当前auto_increment的值:
select auto_increment from information_schema.tables where table_schema='ad' and table_name='game';
result:
8
如果指定的主键大于auto_increment时,auto_increment会发生变化
insert into game (id,game_name,game_id) value(10,'game010',10);
那么会变成多少呢?默认情况下会变成比明确指定的主键值大1。查看当前auto_increment的值:
select auto_increment from information_schema.tables where table_schema='ad' and table_name='game';
result:
11
为什么会是1呢?其实auto_increment的变化,受到两个参数的影响,auto_increment_offset
和auto_increment_increment
,这两个参数分别表示,自增主键起始值和步长。插入数据时,如果没有明确指定主键的话,数据插入后,auto_increment
会变成,插入数据前的auto_increment
+auto_increment_automent
。
如果插入数据时候,指定了一个比当前auto_increment
大的值时,auto_increment
会从当前主键值开始,按照auto_increment_increment
累加,直到找到第一个比指定的主键值大的置。
注:使用update 更改数据行的主键,auto_increment
不会发生变化,如果更改后主键值大于等于当前auto_increment
的话,可能会产生违反主键约束的错误。
select auto_increment from information_schema.tables where table_schema='ad' and table_name='game';
result:
11
更新数据行的主键为11
update game set id = 11 where game_name = 'game05';
查看当前auto_increment的值
select auto_increment from information_schema.tables where table_schema='ad' and table_name='game';
result:
11
插入一条数据
insert into game (game_name,game_id) value('game11',11);
产生报错信息如下:
[SQL]insert into game (game_name,game_id) value('game11',11);
[Err] 1062 - Duplicate entry '11' for key 'PRIMARY'
当多个线程同时插入数据到数据库中,是否存在两个记录有相同的主键值呢?答案是不会的。mysql
提供了一个保证自增主键值正确性的机制–自增锁(一种排他锁)。它让申请自增主键的操作由并行变成串行,保证了每个插入操作都能申请到一个唯一的主键值。而且随着mysql
的版本迭代,自增锁也在不在优化。
在mysql5.0
版本的时候,自增锁是语句级别的,也就是说,自增锁的粒度是一条语句,只有等申请到自增锁的语句执行完,自增锁才能释放。在锁释放前,其申请锁的语句只能阻塞等待。显然这种设计会影响到插入数据的并发度。
在mysql5.1.2
版本引入一个可以对自增锁进行配置的参数。改参数为:innodb_autoinc_lock_mode
,改参数的值可以是0,1,2
。
0:
表示采用mysql5.0
版本的策略,即自增锁需要等到语句执行完才能释放。
1:
对于普通insert
语句,申请到自增锁,获取到主键值后,立即释放自增锁。而非等到整条语句执行完才释放。而对于insert ... select
这种批量到插入操作,自增锁的粒度还是语句级别的。为什么对于insert ... select
自增锁就变成了的语句级别呢,这个其实是出于数据一致性考虑的一种优化。具体原因我们后边在说。
2:
表示所有自增锁的申请,都是获取到主键值后,立即释放。
自增锁使用语句级别锁,会降低获取自增值得效率,但是对于insert ... select
批量插入,还是采用了语句级别锁。上文说了原因:保证数据数据的一致性。
insert...select
是一个批量插入操作,这条语句执行过程中,需要获取多个主键值,如果采用获取到自增锁立即释放的模式,会导致获取的多个主键值不连续。也就是失去了insert…select 的语义了,具体原因如下图:
session1:
向game表中插入两条记录,game
表中的内容为(1,’game1’,1),(2,’game2’,2)。
session2:
创建一个表结构和game相同的表game1
,然后将game
表中的2条记录插入到game1
中。
session1:
向game1
中插入一条记录,game_name
和game_id
分别为"
game3"
和3
。
如果自增锁使用"获取到自增锁立即释放"的模式,插入到game1
中的记录可能有如下几种情况:
1.(1,’game3’,3)
(2,’game1’,1)
(3,’game2’,2)
2.(1,’game1’,1)
(2,’game3’,3)
(3,’game2’,2)
3.(1,’game1’,1)
(2,’game2’,2)
(3,’game3’,3)
以上三种情况都是有可能会产生的,如果出现了1
和2
这两种情况话,那么game
和game1
中的数据是对应不上的。如果game1
表为主从结构的话,主库和从库之间数据同步时,也会出现数据不一致的情况。默认情况下,从库从主库同步数据时,binlog
的格式是statement
,也就是binlog
记录的是主库中执行的sql
语句,对于session1
和session2
同时执行对game1
插入操作,在binlog
中记录的结果要么是:
insert into game1 values(null,‘game3’,3)
insert into game1('game_name',game_id) select game_name,game_id from game
要么是:
insert into game1('game_name',game_id) select game_name,game_id from game
insert into game1 values(null,‘game3’,3)
因为从库执行binlog
是串行执行的,无论binlog
是那种顺序,从库中的数据都不可能是第2
中结果。那么从库中主库的数据也就存在不一致的可能,这个是无法忍受的。
mysql
为了保证数据一致性,针对insert ... select
获取主键锁的模式为语句级别的,这种设计模式是以牺牲性能为代价的。
当insert ... select
语句插入的数据过多时,比如10万
时,那么就需要申请10万
次自增锁,然后在逐个的插入10万
条记录,这种做法不但速度慢,而且影响数据插入的并行度。在这10万
条记录插入的过程中,其他session
的插入语句只能阻塞等待。这严重影响了数据插入的性能。
mysql
针对insert ... select
获取自增锁的方式进行了优化,优化措施是提高这10万
条数据插入时获取自增锁的效率,在原来的方式中,因为每次获取一个可用主键值,所以只能并行插入一条记录。优化后,每次可以获取多个主键值,那么就可以并行插入多个记录。优化后的主键获取方式按照每次获取2的幂次方
个,第一次获取1
个,第二次获取2
个,第三次获取4
个…直到获取足够的主键值。这样理论上插入数据的效率也是呈指数速度增长的。
处了mysql
的优化方案外,我们还可以通过修改binlog
格式为row
,然后将获取主键锁的模式改成2
。在保证主从数据一致的前提下提高插入数据的效率。
可以使用最大主键值,大致估计一下表的数据量,但是这种方式是不准确的,主要原因在于自增主键不连续,而且中间是会产生空洞的。只能说自增主键值大于等于数据量。
在分析自增主键不连续前,我们需要先了解一下,自增值增长的时机。下图是一次数据插入的流程图(默认自增步长为1):
1.game
表初始数据信息。
2.插入一个主键为null
的记录,(null,’game2’,2)
。
3.数据库将game
表 auto_increment
值,分配给要插入的记录。
4.game
表的auto_increment
增长为3
5.执行插入操作。
真正执行数据插入操作是在4
完成的,而auto_increment
的更新是在4
之前执行的。如果4
以为某些原因执行失败了,整个表的记录数量没有增长,但是auto_increment
却已经增加了。下一次插入操作,继续按照上面的流程执行,主键继续增加,那么必然会导致主键产生空洞不连续的情况,也就是产生了整个表的数据量小于主键的最大值。
获取当前 auto_increment
select auto_increment from information_schema.tables where table_schema='ad' and table_name='game';
result:
12
当前表中的数据:
id game_name game_id
6 game06 6
7 game7 7
10 game010 10
11 game05 5
插入一条违反唯一键约束的记录
insert into game (id,game_name,game_id) value(null,'game05',5);
[SQL]insert into game (id,game_name,game_id) value(null,'game05',5);
[Err] 1062 - Duplicate entry '5' for key 'ix_game_id'
查看此时的auto_incremnt
select auto_increment from information_schema.tables where table_schema='ad' and table_name='game';
result:
13
再次查看当前表中的数据:
id game_name game_id
6 game06 6
7 game7 7
10 game010 10
11 game05 5
数据量没有发生变化,但是自增值取增加了。
除此之外,mysql
对自增锁的优化也会产生主键空洞不连续,因为对于insert … select mysql采用批量获取主键的方式,每次获取2的幂次方个主键,来提高数据插入的并行效率,最后一次批量获取,很大可能会获取到多余的主键。
创建一个表结构和game
相同的表game2
create table game2 like game;
查看此时auto_increment
的值
select auto_increment from information_schema.tables where table_schema='ad' and table_name='game2';
result:1
使用insert ... select
将game
表中的数据复制到game2
中
select * from game;
result:
id game_name game_id
6 game06 6
7 game7 7
10 game010 10
11 game05 5
同步数据
insert into game2(game_name,game_id) select game_name,game_id from game
game
表中共有4条数据,需要分3
次获取主键,每次获取的主键数据分别是 1,2,4
此次数据同步申请了7
个主键。
查看此时auto_increment
的值
select auto_increment from information_schema.tables where table_schema='ad' and table_name='game2';
result:
8
此时game2
表中只有4
条记录,mysql
的这种优化也会造成主键的空洞产生。
用主键的最大值可以衡量数据表中数据量的数据量级,但是无法精确衡量表中的数据量。
感谢阅读,希望本篇文章能对你有所帮助。