MySQL AutoIncrement--自增锁模式
自增锁模式
在MYSQL 5.1.22版本前,自增列使用AUTO_INC Locking方式来实现,即采用一种特殊的表锁机制来保证并发插入下自增操作依然是串行操作,为提高插入效率,该锁会在插入语句完成后立即释放,而不是插入语句所在事务提交时释放。该设计并发性能太差,尤其在大批量数据在一条语句中插入时(INSERT SELECT ), 会导致该语句长时间持有这个“表锁”,从而阻塞其他事务的插入操作。
在MYSQL 5.1.22版本开始,InnoDB存储引使用一种轻量级互斥锁(Mutex)来控制自增列增长,并提供innodb_autoinc_lock_mode参数来控制。
自增长方式可分为下面四类:
1.INSERT-LIKE:指所有的插入语句,比如 INSERT、REPLACE、INSERT…SELECT、REPLACE…SELECT,LOAD DATA等。 2.Simple insert:指在插入前就能确定插入行数的语句,包括INSERT、REPLACE,不包含INSERT…ON DUPLICATE KEY UPDATE这类语句。 3.Bulk inserts:指在插入前不能确定得到插入行的语句。如INSERT…SELECT,REPLACE…SELECT,LOAD DATA. 4.Mixed-mode inserts:指其中一部分是子增长的,有一部分是确定的。
innodb_autoinc_lock_mode参数取值
1.innodb_autoinc_lock_mode=0 传统锁定模式, 5.1.22之前的方式,也就是所有INSERT-LIKE操作都用AUTO-inc locking。 2.innodb_autoinc_lock_mode=1 连续锁定模式,这个参数是5.1.22之后出现的也是之后的默认值,对于SIMPLE INSERT,使用轻量级互斥锁,对于BULK INSERT,使用AUTO-inc locking。 3.innodb_autoinc_lock_mode=2 交错锁定模式,指不管什么情况都使用轻量级互斥的锁,效率最高,但是复制只能使用row-basereplication,因为statement-base replication会出现问题。
当innodb_autoinc_lock_mode=1时,"Simple insert"操作能在插入前知道插入的记录数量,因此无需在整个插入操作过程中持有表级别的AUTO-INC锁,MySQL通过轻量级互斥锁来控制INSERT操作获取自增值的过程,并在INSERT操作获取到自增值后快速释放互斥锁,通过降低锁颗粒度和锁持续周期,实现"Simple insert"操作并发执行。当其他事物对表持有AUTO-INC锁时,"Simple insert"操作也会升级使用AUTO-INC锁并被阻塞。
当innodb_autoinc_lock_mode=1时,在语句复制格式下(BINLOG_FORMAT=STATEMENT),BINLOG中没有记录主库执行过程中获取到的所有自增值及其对应行的信息,要保证"Bulk insert"操作主从复制数据一致就必须保证语句在主库和从库执行时获取到相同自增值,而因此只能通过控制“获取连续自增值”的方式来实现,同时为避免受其他事务插入操作影响,就必须在表级别加锁且保证持有锁至语句结束。
在行复制格式下(BINLOG_FORMAT=ROW),主库BINLOG中保存有记录的所有列信息包括自增列值,因此无需通过AUTO-INC锁来保证主从数据一致。
在MySQL 8.0版本前,参数BINLOG_FORMAT的默认值为STATEMENT,参数innodb_autoinc_lock_mode的默认值为1。
在MySQL 8.0版本后,参数BINLOG_FORMAT的默认值被调整为ROW格式,参数innodb_autoinc_lock_mode的默认值为2。
自增值间隙
在MySQL中,获取自增值的操作是非事务性,获取自增值的操作产生的锁在语句执行过程中或执行完成里便被释放而不会持续到事务提交和回滚,获取到自增值也不会随事务回滚而回滚,因此不能依赖MySQL自增列来实现表中列值连续无间隙。
表中自增列作为代理键,只能用来标识和定位记录,而不应该承载业务逻辑,因此也不建议对自增列值进行显式更新。
AUTO_INC锁导致的死锁
在业务繁忙的表上通过pt-osc进行DDL操作变更时,可能会出现死锁,死锁场景如下:
pt-osc工具执行对tb001执行DDL变更操作,创建临时表_tb001_new来执行变更并拷贝数据,并在tb001上创建增删改三种触发器。 步骤1、事务1执行业务操作对表tb001进行数据更新,对更新记录加排他锁(X) 步骤2、pt-osc的拷贝线程拷贝数据,事务2执行INSERT INTO _tb001_new SELECT FROM tb001 WHERE ... LOCK IN SHARE MODE的操作,先执行INSERT INTO操作申请_tb001_new表上AUTO-INC锁,申请成功 步骤3、事务1更新操作触发表tb0011上update触发器执行中执行REPLACE INTO操作,向表_tb001_new更新插入数据,申请_tb001_new表上AUTO-INC锁,由于事务2持有_tb001_new表上AUTO-INC锁,因此事务1被阻塞。 步骤4、事务2获取_tb001_new表上AUTO-INC锁后,再执行SELECT FROM tb001 WHERE ... LOCK IN SHARE MODE操作,申请某个范围内所有记录上的共享锁(S),由于事务1持有其中一条或多条记录的排他锁(X),导致事务2被阻塞。 步骤5、事务2被阻塞,触发死锁检测,发现事务1和事务2相互等待锁资源形成死锁,挑选事务1或事务2进行事务回滚。
上述死锁解决办法:
1、在行复制格式下(BINLOG_FORMAT=ROW),可以考虑调整参数innodb_autoinc_lock_mode的值为2(非动态参数,需重启服务)
2、通过降低锁冲突概率来降低死锁发生概率:
1、在业务低峰期进行DDL变更操作 2、优化业务SQL降低单个事务修改记录数 3、调整pt-osc单次拷贝操作的数据量
参考资料:
https://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_autoinc_lock_mode