MySQL的主键可以是自增的,那么如果在断电重启后新增的值还会延续断电前的自增值吗?自增值默认为1,那么可不可以改变呢?下面就说一下MySQL的自增值。
特点
保存策略
1、如果存储引擎是MyISAM,那么这个自增值是存储在数据文件中的;
2、如果是InnoDB引擎,1)在5.6之前是存储在内存中,没有持久化,在重启后会去找最大的键值,举个例子,如果一个表当前数据行里最大id是10,AUTO_INCREMENT=11。这时候,我们删除id=10的行,AUTO_INCREMENT还是11。但如果马上重启实例,重启后这个表的AUTO_INCREMENT就会变成10;
2)在8.0开始,自增值就保存在redolog中,重启后会从redolog中读取之前保存的自增值。
自增值的确定
1、如果插入数据时id字段指定为0、null或未指定,那么就把这个表当前的AUTO_INCREMENT值填到自增字段,并且会以auto_increment_offset作为初始值,auto_increment_increment为步长,找出第一个大于当前自增值的值作为新的自增值。
2、如果插入的数据的id字段指定了具体的值,就直接使用语句里的值。
自增值的修改
假设某次要输入的值是X,当前的自增值是Y。那么:
1、如果X<Y,那么这个表的自增值不变;
2、如果X≥Y,那么就把当前自增值修改为新的自增值。
执行过程
假设有表t,id是自增主键,在已有(1,1,1)的情况下,插入一条(null,1,1),那么执行过程就如下:
1、执行器调用InnoDB引擎接口写入一行,传入的这一行的值是(0,1,1);
2、InnoDB发现用户没有指定自增id的值,获取表t当前的自增值2;
3、将传入的行的值改成(2,1,1);
4、将表的自增值改成3;
5、继续执行插入数据操作,由于已经存在c=1的记录,所以报Duplicatekeyerror,语句返回。
带来的问题
由于上面说得这种特性,在一些场景中会出现主键不连续的现象。
场景1:添加数据时唯一索引重复
在c列索引重复后,原本要分配的主键值2就会被丢弃,而下次再次插入就从2开始计算,也就变成了3。
场景2:事务回滚
insertintotvalues(null,1,1);begin;insertintotvalues(null,2,2);rollback;insertintotvalues(null,2,2);//插入的行是(3,2,2)
在第二条语句回滚后分配给其的主键2也会被丢弃。
场景3:特殊批插入优化导致
这里说得特殊的批插入指的是insert…select、replace…select和loaddata语句。为什么说这些语句可能会导致?这就要说到自增锁了。首先自增锁是为了避免多线程冲突,因为在多线程下,如果同时有多个线程来获取自增值,那么就可能会导致同一个自增值被分配给多条记录,导致逐渐冲突。所以需要自增锁,而为什么前面说得这些批插入语句会导致主键不连续,在下面自增锁部分会说到。
问题:在说自增锁之前,先思考一个问题,为什么对于前两个场景,不把自增主键值设为可以回滚的?这样不就可以避免不连续了么?
答:因为设计成可回滚的会导致性能下降,看下面这个场景。
1、假设事务A申请到了id=2,事务B申请到id=3,那么这时候表t的自增值是4,之后继续执行。
2、事务B正确提交了,但事务A出现了唯一键冲突。
3、如果允许事务A把自增id回退,也就是把表t的当前自增值改回2,那么就会出现这样的情况:表里面已经有id=3的行,而当前的自增id值是2。
4、接下来,继续执行的其他事务就会申请到id=2,然后再申请到id=3。这时,就会出现插入语句报错“主键冲突”。
而为了解决上面这个问题,就需要从下面两个方法中选一个。
方法一、每次申请id之前,先判断表里面是否已经存在这个id。如果存在,就跳过这个id。但是,这个方法的成本很高。因为,本来申请id是一个很快的操作,现在还要再去主键索引树上判断id是否存在。
方法二:把自增id的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。
所以,综合来看,比如取消自增值回滚的功能。
自增锁
自增锁是为了避免在多线程中多个线程获取到同一个主键值,导致主键冲突。
加锁策略
5.0版本:范围是语句,只有等到语句执行完后才会释放。
5.1.22开始:引入了一个innodb_autoinc_lock_mode参数,根据参数值的不同执行不同的策略。默认是1。
1、参数等于0,表示采用之前的策略,即语句执行结束就会释放。
2、参数等于1,对于普通insert语句,自增锁在申请之后立马释放;
对于insert...select这样的批量插入数据的语句,会等到语句执行完才会释放。加锁范围是select所涉及到的范围和间隙。
3、参数等于3,所有的申请自增主键的动作都是申请后就释放锁。
问题:为什么默认情况下,insert...select这样的批操作要使用语句级的锁?为什么参数默认不是2?
答:因为对于insert...select这样的批量插入数据的语句,可能会导致主从不一致的情况发生。
在sessionB执行完"createtablet2liket"后,sessionA和sessionB同时操作t2。如果没有锁,那么执行过程就可能会出现下面的情况。
sessionB先插入了两个记录,(1,1,1)、(2,2,2);然后,sessionA来申请自增id得到id=3,插入了(3,5,5);之后,sessionB继续执行,插入两条记录(4,3,3)、(5,4,4)。
虽然这样看起来确实没有什么问题,但是如果是在集群中,主机这样执行,提示binlog是statement格式的,那么从机执行的顺序就有可能和主机不一致,最终导致主从不一致。所以需要在批量插入时加锁。而如果设置为2,那么如果binlog不是row,就会导致主从数据不一致。
所以,要想保证数据一致,也保证系统的并发性,可以有两种方案:
方案一:将binlog格式设为statement,innodb_autoinc_lock_mode设为1。
方案二:将binlog格式设为row,innodb_autoinc_lock_mode设为2。一般我们为了保证MySQL的高可用,都将binlog设为row,所以一般选择第二种方案。
批插入的优化
在批插入时,由于不知道一次性插入的语句有多少,如果记录多达几千万甚至上亿条,那么每次插入都需要分配一次自增值,这样效率会很慢,所以MySQL对批操作进行了优化:
1、语句执行过程中,第一次申请自增id,会分配1个;
2、1个用完以后,这个语句第二次申请自增id,会分配2个;
3、2个用完以后,还是这个语句,第三次申请自增id,会分配4个;
4、依此类推,同一个语句去申请自增id,每次申请到的自增id个数都是上一次的两倍。
举个例子,执行下面的代码
insertintotvalues(null,1,1);insertintotvalues(null,2,2);insertintotvalues(null,3,3);insertintotvalues(null,4,4);createtablet2liket;insertintot2(c,d)selectc,dfromt;insertintot2values(null,5,5);
insert…select,实际上往表t2中插入了4行数据。但是,这四行数据是分三次申请的自增id,第一次申请到了id=1,第二次被分配了id=2和id=3,第三次被分配到id=4到id=7。由于这条语句实际只用上了4个id,所以id=5到id=7就被浪费掉了。之后,再执行insertintot2values(null,5,5),实际上插入的数据就是(8,5,5)。这就是前面说到主键不连续的第三种情况。
insert...select前后操作同一个表会用到临时表
假设有表结构
CREATETABLE`t`(`id`int(11)NOTNULLAUTO_INCREMENT,`c`int(11)DEFAULTNULL,`d`int(11)DEFAULTNULL,PRIMARYKEY(`id`),UNIQUEKEY`c`(`c`))ENGINE=InnoDB;insertintotvalues(null,1,1);insertintotvalues(null,2,2);insertintotvalues(null,3,3);insertintotvalues(null,4,4);createtablet2liket
如果执行的语句是:
insertintot2(c,d)(selectc+1,dfromtforceindex(c)orderbycdesclimit1);
如果我们查询慢日志,会发现
扫描行数是1,也就是直接在t上通过索引找到那一条记录,然后插入t2表。
如果将这条语句改成
insertintot(c,d)(selectc+1,dfromtforceindex(c)orderbycdesclimit1);
那么此时查看慢日志就会发现变成了5,这是为什么?就算全查出来也只会是4条,这时我们查看扫描行数的变化
发现前后变化是4行,所以确定了是使用了临时表,那么就可以确定过程是:
1、创建临时表,表里有两个字段c和d。
2、按照索引c扫描表t,依次取c=4、3、2、1,然后回表,读到c和d的值写入临时表。这时,Rows_examined=4。
3、由于语义里面有limit1,所以只取了临时表的第一行,再插入到表t中。这时,Rows_examined的值加1,变成了5。
至于为什么需要临时表,这是为了防止在读取时,读到了刚刚插入的值。
优化
因为select返回的记录数较少,所以可以使用内存临时表来优化,
createtemporarytabletemp_t(cint,dint)engine=memory;insertintotemp_t(selectc+1,dfromtforceindex(c)orderbycdesclimit1);insertintotselect*fromtemp_t;droptabletemp_t;
这样扫描的总行数只有select的1加上临时表上的1。
最后
对于唯一索引的冲突,可以使用insertinto…onduplicatekeyupdate来进行冲突后的更新处理,假设表t中有(1,1,1)、(2,2,2)两条记录,那么执行:
在插入时发现冲突就对冲突的记录进行修改操作。