为了保存机器上报信息(业务需求是每个机器只需保存最新的一条记录),原 SQL 语句如下(其中,machineId 的为唯一索引,t_report_pad 的 id 为 bigint 类型的自增主键):
insert into t_report_pad
(machine_id,pad_device_model,app_version,app_version_code,pad_version,pad_version_code,upgrade_status,reason,create_time,update_time)
values
(#{machineId},#{padDeviceModel},#{appVersion},#{appVersionCode},#{padVersion},#{padVersionCode},#{upgradeStatus},#{reason},now(),now())
这种情况下,当同一个 machine_id 多次发送 SQL 的时候,会造成唯一主键冲突问题,从而导致后面的机器信息添加不了,为了解决这种情况,业务中使用了 insert … on duplocate key update 语句优化原有语句,修改后的 SQL 语句如下,自此业务功能正常使用。
insert into t_report_pad
(machine_id,pad_device_model,app_version,app_version_code,pad_version,pad_version_code,upgrade_status,reason,create_time,update_time)
values
(#{machineId},#{padDeviceModel},#{appVersion},#{appVersionCode},#{padVersion},#{padVersionCode},#{upgradeStatus},#{reason},now(),now())
ON DUPLICATE KEY UPDATE
pad_device_model=#{padDeviceModel},
app_version=#{appVersion},
pad_version=#{padVersion},
pad_version_code=#{padVersionCode},
app_version_code=#{appVersionCode},
upgrade_status=#{upgradeStatus},
reason=#{reason},
update_time=now()
运行一段时间之后,发现 t_report_pad 表中的自增主键 id 的值已经超过 int 类型的最大值,并且由于业务代码中使用 int 类型进行接收,从而导致业务功能出现异常(这里代码和 MySQL 中 ID 的类型不一致是因为, DBA 事前已经先修改了 ID 的数据类型,但是开发人员忘记同步修改代码中 Java 对象的类型),于是紧急修复了一个版本解决线上问题。
(1)现象:
深入分析之后,发现 DB 表中的数据量只有 600W 条,但是自增主键 ID 却高达 21 亿,并且很多自增主键都不连续,那为什么会出现这种现象呢?是不是代码中存在bug?亦或是MySQL 中的自增主键步长设置有问题?亦或是其他?本着打破砂锅问到底的心态,查询多方资料之后,最终终于找到这种现象的原因:对于这种自增主键出现间隙的情况,既不是代码的bug,也不是DB参数设置的问题,而是 mysql 中 为了优化高并发下 auto_increment 自增主键获取的性能,而默认配置的一种算法模式(该算法模式通过 innodb_autoinc_lock_mode 参数配置)导致的,特别是在使用 insert ... on duplicate key update 语法的时候,很容易出现主键间隙,是一种正常的使用现象。
(2)分析过程:
① 分析过程1 - 查看代码中业务逻辑是否正常:查看 SQL 语句中是否手动设置了 ID 列的值,从上面的 SQL 语句可以很容易看出,并没有手动设置 id 的值,默认都是使用自增主键生成的数值。接着查看代码中是否存在删除表中对应的行记录,从而导出出现主键间隙的情况,经查看也不存在。
② 分析过程2 - 查看自增主键的步长设置:
输入:
show session variables like 'auto_increment%'
结果如下:
结果显示 DB 的自增主键的步长设置正常,默认都是自增 1,所以主键间隙问题也不是由这种情况导致的。
③ 分析过程3 - 自增主键的算法模式设置(最终定位到的问题所在):
该参数只要由 innodb_autoinc_lock_mode 参数影响,具体看文章的第二、三部分
MySQL的中 auto_increment 类型的属性主要用于为一个表中记录自动生成 ID。
(1)当插入记录时,如果为 auto_increment 数据列明确指定了一个数值,则会出现两种情况:
- 情况一:如果插入的值与已有编号重复,则会出现报错异常,因为 auto_increment 数据列的值必须是唯一的;
- 情况二:如果插入的值大于已有编号,则会把该插入到数据列中,并使在下一个编号将从这个新值开始递增。也就是说,会跳过一些编号。
(2)如果自增序列的最大值被删除了,则在插入新记录时,该值被重用。
(3)如果使用 update 命令更新自增列,列值与已有的值重复,则会出错。如果大于已有值,则下一个编号从该值开始递增。
(1)在 MySQL 5.1.22 前,MySQL 的 "insert-like" 会在执行语句的过程中使用一个表级的自增锁(AUTO-INC Lock)将整个表锁住,直到整个语句结束(而不是事务结束)。在此期间会阻塞其他的 insert-like、update 等语句,所以推荐使用程序将这些语句分成多条语句,一一插入,减少单一时间的锁表时间。
insert-like语句:insert,insert … select,replace,replace … select,load data,insert ... values(),values()
(2)在 MySQL 5.1.22 之后,MySQL 进行了改进,引入了参数 innodb_autoinc_lock_mode,通过这个参数控制 MySQL 的锁表逻辑。
innodb_autoinc_lock_mode 参数可用于配置自动增量锁定算法的模式,从而进行在可预测的自动递增值序列和插入操作的最大并发性之间进行权衡。
3.1、insert 语句的 分类:
前面提到了 insert-like 语句,就是指所有可以向表中增加行的语句,再进行细分的话,还可以分成三种类型:
- (1)simple inserts:通过分析 insert 语句可以预先确定要插入的行数,包括 insert、insert ... values()、values() 语句
- (2)bulk inserts:通过分析 insert 语句不能确定插入数量,包括 insert … select、replace … select、load data 语句。
- (3)mixed-mode inserts:不确定是否需要分配 auto_increment id,如 insert into t (id,name) values (1,'a'),(null,'b'),(5,'c') 以及 insert… on duplicate key update。对于后者,它的最坏情况实际上是在 insert 语句后面又跟了一个 update,其中 auto_increment 列的分配值不一定会在 update 阶段使用
3.2、innodb_autoinc_lock_mode 模式说明:
- 0:这个表示 tradition,即传统模式(每次都会产生表锁)
- 1:这个表示 consecutive,即连续模式(会产生一个轻量锁,simple insert 会获得批量的锁,保证连续插入)
- 2:这个表示 interleaved,即交错模式(不会锁表,来一个处理一个,并发最高)
(1)tradition 模式(innodb_autoinc_lock_mode=0):
这种方式和 MySQL 5.1.22 之前一样,主要是提供一个向后兼容的能力。在该模式下,所有 insert 语句都要在开始执行的时候获得一个表级的 auto_inc 锁,直到语句结束(不是事务结束)时才释放这把锁,保证自增长值的分配是可预见、连续性、可重复性的,并确保给任何给定语句分配的自动递增值是连续的。但是由于 auto_inc 锁 需要一直保持到语句的结束,造成并发性较差。
在主从复制的安全性方面,在 statement-based replication 的情况下,这意味着当在从服务器上复制SQL语句时,自动增量列使用与主服务器上相同的值。因为在该模式下,多个INSERT语句的执行结果是确定性的,SLAVE 可以再现与 MASTER 相同的数据,保证基于语句复制的安全性。
(2)consecutive 模式(innodb_autoinc_lock_mode=1):
MySQL 5.1.22 之后版本的默认模式,并发度相对加高,这个模式的好处是 auto_inc 锁在特定情况下不需要一直保持到语句的结束,只要语句得到了相应的值后就可以提前释放锁。这种模式下:
- 比如 insert into t1 (id,c2) values (1,’a'), (null,’b'), (5,’c'), (null,’d');会一次性的分配4个id,而不管用户是否指定了部分id;
- insert … on duplocate key update 一次性分配,而不管将来插入过程中是否会因为 duplicate key 而仅仅执行 update 操作。
在主从复制的安全性方面,无论是 statement-based 还是 row-based 方式,该模式也都可以保证基于语句的主从复制的安全。
(3)interleaved 模式(innodb_autoinc_lock_mode=2):
该模式下,所有的 insert-like 语句都不会使用表级 auto-inc 锁,这种模式是来一个分配一个,只锁住 id 分配的过程,并且可以同时执行多个语句,是性能最好和最可扩展的锁定模式。它和 innodb_autoinc_lock_mode = 1 的区别在于,不会预分配多个。
由于可以同时为多个语句生成自增长值(即跨语句交叉编号),可能会造成同一个语句插入的行生成的 auto_incremant 值不连续,也就是存在间隙。比如执行 “bulk inserts” 时,则在给任何给定语句分配的自动递增值中可能存在间隙。但是如果执行的是语句是 “simple inserts”,其中要插入的行数可提前知道,那么不会有间隙。
最后在主从复制replication的安全性方面,当 binlog_format 为 statement-based 时(简称 SBR,statement-based replication),则会存在问题,因为是来一个分配一个,当并发执行时,“bulk inserts” 在分配时会同时向其他的 insert 分配,从而出现主从不一致(从库执行结果和主库执行结果不一样),因为 binlog 只会记录开始的insert id。但是如果 binlog_format 为 row-based 则不会出现问题。
3.3、innodb_autoinc_lock_mode 的主从安全性问题总结:
(1)主从的安全性:如果 binlog_format 使用基于行的或混合模式的复制,则所有自动增量锁定模式都是安全的,因为基于行的复制对SQL语句的执行顺序不敏感(混合模式会在遇到不安全的语句是使用基于行的复制模式),所以可以设置 innodb_autoinc_lock_mode = 2 可以获得更好的并发度。但是当 binlog_format 是基于语句复制 statement-base 的情况下,可设置 innodb_autoinc_lock_mode = 1,保证复制安全的同时,获得简单insert语句的最大并发度
(2)innodb_autoinc_lock_mode 参数的设置是针对 innoDB 存储引擎,在 myisam 引擎情况下,无论什么样自增id锁都是表级锁。
通过上面文章的描述,最终可以确定导致自增主键出现间隙的原因在于,mixed-mode inserts 中的 insert… on duplicate key update,它的最坏情况实际上是在 insert 语句后面又跟了一个 update,其中 auto_increment 列的分配值不一定会在 update 阶段使用,从而导致自增主键间隙出现。
1、为了防止出现同样的线上问题,有什么解决方案呢:
(1)对于旧表的主键类型,在修改 DB 表中的字段类型的时候,必须同步修改代码中的对象属性类型
(2)后续对于新表的主键类型,直接设置为 bigint,原因在于:
- 当 mysql 表中的数据量较少时,int 类型的主键 与 bigint 类型的主键,对 DB 的性能与存储压力可以忽略不计。
- 假如表中的数据比较多但还未达到 int 的最大值 21 亿时,但是出现了 DB 性能或者磁盘存储压力,靠使用 int 代替 bigint 类型带来的提升其实是微乎其微的。
- 当 mysql 表中的数据量超过21亿时,int 类型的主键升级为 bigint 类型是必须的。
(3)不使用uuid 的原因:
一般情况下,MySQL数据库对于主键,推荐使用自增ID,因为在MySQL的 InnoDB 存储引擎中,主键索引是聚簇索引,主键索引的B+树的叶子节点按照顺序存储了主键值及数据,如果主键索引是自增ID,只需要按顺序往后排列即可,如果是UUID,ID是随机生成的,在数据插入时会造成大量的数据移动,产生大量的内存碎片,造成插入性能的下降。除此之外,UUID占用空间较大,建立的索引越多,造成的影响越大。
2、其他导致自增列存在间隙的情况:
(1)如果生成自动递增值的事务回滚,那些自动递增值将丢失。 一旦为自动增量列生成了值,无论是否完成 “insert-like” 语句以及包含事务是否回滚,这些值都不能回滚,也就是这种丢失的值不被重用。 因此,存储在表的 auto_increment 列中的值可能存在间隙。
(2)如果用户在 insert 中为 auto_increment 指定 null 或者 0,InnoDB会将该行视为未指定值,并为其生成新值。
(3)如果手动为 auto_increment 列分配了一个负值,则不会触发自动增量机制的行为。
(4)DB 自增主键的步长设置
参考文章:
https://blog.csdn.net/fwkjdaghappy1/article/details/7663331
https://blog.csdn.net/ashic/article/details/53810319