Blage's Coding Blage's Coding
Home
算法
  • 手写Spring
  • SSM
  • SpringBoot
  • JavaWeb
  • JAVA基础
  • 容器
  • Netty

    • IO模型
    • Netty初级
    • Netty原理
  • JVM
  • JUC
  • Redis基础
  • 源码分析
  • 实战应用
  • 单机缓存
  • MySQL

    • 基础部分
    • 实战与处理方案
    • 面试
  • ORM框架

    • Mybatis
    • Mybatis_Plus
  • SpringCloudAlibaba
  • MQ消息队列
  • Nginx
  • Elasticsearch
  • Gateway
  • Xxl-job
  • Feign
  • Eureka
  • 面试
  • 工具
  • 项目
  • 关于
🌏本站
🧸GitHub (opens new window)
Home
算法
  • 手写Spring
  • SSM
  • SpringBoot
  • JavaWeb
  • JAVA基础
  • 容器
  • Netty

    • IO模型
    • Netty初级
    • Netty原理
  • JVM
  • JUC
  • Redis基础
  • 源码分析
  • 实战应用
  • 单机缓存
  • MySQL

    • 基础部分
    • 实战与处理方案
    • 面试
  • ORM框架

    • Mybatis
    • Mybatis_Plus
  • SpringCloudAlibaba
  • MQ消息队列
  • Nginx
  • Elasticsearch
  • Gateway
  • Xxl-job
  • Feign
  • Eureka
  • 面试
  • 工具
  • 项目
  • 关于
🌏本站
🧸GitHub (opens new window)
  • MySQL

    • 基础部分

      • 初识MySQL
      • 基础架构&日志
      • 事务隔离
      • 全局锁、表锁、行锁
      • 事务的隔离性和行锁
      • 索引
      • 索引深入(中)
      • 内存脏页刷盘
      • 数据库表的空间回收
      • count
      • order by
      • SQL语句性能差异分析
      • 幻读与间隙锁
      • 加锁规则案例分析
      • binlog和redolog如何写入磁盘
      • MySQL一致性与高可用性
      • kill命令
      • 全表扫描与内存占用
      • join
      • 临时表与内存表
      • 自增主键
        • 自增值保存在哪?
        • 自增值修改机制
        • 自增值空洞
          • 场景一、唯一性约束导致自增值插入失败
          • 场景二、事务回滚
          • 场景三、批量数据插入
          • inert into (),()
          • insert ... select批量插入
          • MySQL批量申请自增id策略
        • 自增值的主备同步问题
      • insert操作加锁场景分析
      • grant
      • 分区表
      • 思维导图
    • 实战与处理方案

    • 面试

  • ORM框架

  • 数据库
  • MySQL
  • 基础部分
phan
2023-07-11
目录

自增主键

# 自增主键

自增主键:

能够保证新数据能够按照主键递增的顺序进行插入。

# 自增值保存在哪?

每次执行insert语句之后,id+=1,那么这个id的值保存在哪?

  • MyISAM引擎的表自增值保存在数据文件中。

  • InnoDB引擎中的id自增值保存在内存中。根据MySQL版本号存在如下区别:

    • MySQL5.7及之前的版本:保存在内存中,并且不会进行持久化,每次插入新的数据都会加1。如果遇上重启则内存的最大自增值会丢失,开机后会扫描表获取当前所有行记录id的最大值max(id),并把max(id)+1作为下一个记录的自增值。 重启可以刷新最准确的max(id)值,填补空洞(但为了保证有序性,中间的id空洞并不能解决)。
  • MySQL8.0版本:将自增值的变更记录在redolog当中,如果发生重启就直接依靠redolog进行恢复。

# 自增值修改机制

auto_increment_offset:自增的初始值

auto_increment_increment:自增的步长

假设的当前表的自增值为AUTO_INCREMENT,执行insert语句时,自增字段的值根据以下规则决定:

  • insert(null,4)插入时自增字段为0,null或未指定值,那么就把当前行的自增字段值设置为AUTO_INCREMENT,然后AUTO_INCREMENT自增。
  • insert(X,3)插入行记录时指定自增字段的值X:
    • X>=AUTO_INCREMENT:则将当前自增值AUTO_INCREMENT更新为X+1,当前行的自增字段值为X
    • X<AUTO_INCREMENT:则只需要将当前行的自增字段值为X

# 自增值空洞

假设表t主键id字段自增,c字段作为唯一索引。以下几种情况可能会导致出现自增值不连续的情况:

# 场景一、唯一性约束导致自增值插入失败

此时数据库中的表t已经存在一行记录(1,1,1)

insert into t values(null,1,1);
1
  1. 执行器调用InnoDB引擎写入(0,1,1)这行新的数据
  2. InnoDB发现自增的字段为0、则获取当前表t的自增字段AUTO_INCREMENT=2
  3. 传入的数据变成(2,1,1)
  4. AUTO_INCREMENT字段自增
  5. 执行(2,1,1)数据插入操作。此时才发现表中c字段值1已经出现,因此报Duplicate key错误,返回

可以发现上面的过程中id=2这行数据并没有插入,因此存在“空洞”,主键值不连续。

而造成空洞的主要原因在于,先进行表自增值自增,然后才会执行语句判断唯一性约束。

# 场景二、事务回滚

insert into t values(null,1,1);
begin;
insert into t values(null,2,2);
rollback;
insert into t values(null,2,2);
1
2
3
4
5
  1. 第二个insert语句插入(2,2,2)后,AUTO_INCREMENT=3
  2. 回滚后,表中会删除(2,2,2)这条数据,但是AUTO_INCREMENT自增值并不会回滚。
  3. 第三个insert语句插入(3,2,2)

而这里MySQL之所以不将自增值回滚,主要是为了提升性能。这里如果将“允许自增值回滚”这一条件带入以下多并发线程的场景,分析如下:

  1. A线程插入id=2,B线程插入id=3,当前AUTO_INCREMENT=4
  2. B线程成功提交,而A线程违反唯一键约束,事务回滚,此时根据上面的条件AUTO_INCREMENT会回滚变成2
  3. 接下来,C线程连续插入两条insert语句,分别插入id=2,id=3,就会导致出现主键冲突。
  4. 而要解决上述主键冲突,要么就是每次生成id时判断表中是否已经存在,要么就是按照事务提交顺序释放自增值的锁。无论是哪一种无疑都非常影响MySQL的性能。

综上,如果MySQL允许自增值随着redolog进行回滚,那么在多并发情况下,就不能够保证AUTO_INCREMENT一定大于当前表中的max(id)。

# 场景三、批量数据插入

MySQL5.1.22引入一个自增锁的策略,通过设置innodb_autoinc_lock_mode参数值,采用不同的自增锁:

  • 设置为0:表示语句执行结束后才会释放锁
  • 设置为1:
    • 如果是普通insert语句:申请完毕后直接释放
    • 如果是insert ... select这样的批量插入语句,自增锁会等待语句执行完毕后才被释放
  • 设置为2:所有自增值申请完毕后立即释放

# inert into (),()

insert插入多个value的情况下,因为MySQL可以计算出需要多少个id,因此一次性申请完后多个id后,直接释放锁。

# insert ... select批量插入

insert ... select语句执行时,如果是申请完一个id就释放锁,那么在多并发情况下这条批量语句生成的id并不一定连续,因为可能会受到其它session的影响。而binlog为statement在恢复时可以保证连续,这样就会造成主备不一致。

解决方案有两种:

✨将innodb_autoinc_lock_mode设置为1,让自增锁执行完语句再释放。

✨在生产上,从并发角度考虑更多会采取这种做法:innodb_autoinc_lock_mode设置为2,同时binlog设置为row。这样保证备库应用binlog时直接利用主库记录的id值插入数据(允许乱序插入id+主备一致)。

# MySQL批量申请自增id策略

批量申请自增id策略:每一次申请到的id个数是上一次申请的两倍,当前申请的id用完后才会进行下一轮的申请。

insert into t2(c,d) select c,d from t;
insert into t2 values(null, 5,5);
1
2

假设当前t表只有四行数据:那么执行过程如下:

  1. 首先执行批量插入语句:
    • 第一行数据,第一次申请i一个id,id=1;并将id=1分配给第一行数据
    • 第二行数据,由于第一次申请的id已经用完,因此第二次申请两个id,id=2,3。并将id=2分配给第二行数据
    • 第三行数据,第二次申请的id还没用完,不申请新的id。将id=3分配给第三行
    • 第四行数据,由于第二次申请的id用完了,因此第三次申请四个id,id=4,5,6,7。并将id=4分配给第四行数据
    • 剩余id=5,6,7在insert...select批量语句已经用不到了,因此直接丢弃
  2. 执行当行插入insert,此时申请到的id就是id=8。导致出现自增值空洞。

# 自增值的主备同步问题

场景:多并发情况下线程A先执行insert(null),后写binlog;线程B后执行insert(null),先写binlog。那么binlog恢复时,先恢复执行B线程的插入记录,那么这个自增id会不会与主库不一致?

每个insert语句执行之前,会固定有一句SET INSERT_ID=2,提前设置好当前的自增值,而这个设置自增ID的语句也会同时记录进binlog。

因此binlog恢复时,每个insert语句前都会紧跟着一个记录使用到的自增id的设置语句。

编辑 (opens new window)
#数据库
上次更新: 2023/12/15, 15:49:57
临时表与内存表
insert操作加锁场景分析

← 临时表与内存表 insert操作加锁场景分析→

Theme by Vdoing | Copyright © 2023-2024 blageCoder
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式