惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
Microsoft Security Blog
Microsoft Security Blog
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Last Week in AI
Last Week in AI
罗磊的独立博客
腾讯CDC
云风的 BLOG
云风的 BLOG
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
U
Unit 42
I
InfoQ
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
V2EX
美团技术团队
IT之家
IT之家
Stack Overflow Blog
Stack Overflow Blog
F
Fortinet All Blogs
GbyAI
GbyAI
S
SegmentFault 最新的问题

博客园 - 喜欢Ⅰ

先避免毁灭性错误,再谈聪明决策 通往天堂的三个阶梯 人类随机数趣闻 - 即使人们会觉得它更随机,但实际上它更不随机 C 里面如何使用链表 list 项目代码套路 墨菲定律 - 人类一种误判心理 消息队列, 一种取舍的选择 Redis Stream CORS 跨域请求一种后端适配解决方案 自我的智慧 市场教父 André Kostolany Exception Handling Considered Harmful 字符串转整型 atomic 原子自增工程案例 HTTP 尝试获取 Client IP 对炒股看法 吃饱年代 我是个怎样的人 格林童话之祖父和孙子 Linux 守护进程 智慧 ~ 引子 ~ 三则故事 交易人生 大道至简
MySQL CREATE TABLE Template 模板设计简单交流
喜欢Ⅰ · 2022-03-21 · via 博客园 - 喜欢Ⅰ
倒排索引为什么叫倒排索引?

更加具体点, 我们看看 MySQL InnoDB 引擎中怎么定义和实现 AUTO_INCREMENT.

1. InnoDB 中 AUTO_INCREMENT 配置大致说明

  • 1.1 innodb_auto_inc_lock_mode=0 (traditional lock mode)
  • 获取表锁 (AUTO-INC 锁, 特殊表锁), 语句执行结束后释放, 不需要等事务结束.
  • 分配的值也是一个个分配,是连续的. (如果事务 rollback 了这个 auto_increment 值就会浪费掉, 从而造成间隙)
  • 1.2 innodb_autoinc_lock_mode=1 (consecutive lock mode, MySQL 8.0 之前默认选项)
  • 对于不确定插入数量的语句(例如 INSERT ... SELECT, REPLACE ... SELECT 和 LOAD DATA)
  • innodb_autoinc_lock_mode=0 一样获取表锁, 其他的确定数量的语句在执行前先批量获取 id,
  • 之后再走的是轻量级互斥锁, 如果其他事务已经获取表锁, 这个时候也需要等待.
  • 1.3 innodb_autoinc_lock_mode=2 (interleaved lock mode, MySQL 8.0+ 默认)
  • 采用乐观锁, CAS 更新计数器获取. 正常情况性能最好, 因为没有表锁和轻量级互斥锁.
  • 但在高并发引发的 高 CPU load 场景会适得其反, 加剧这种 CPU 浪费.
  • AUTO_INCREMENT CAS 频率高, 同一个语句操作内部 CAS INC 大概率也会让 id 间隙变大.

2. InnoDB 中 AUTO_INCREMENT 实现大致思路

在 MySQL 8.0 之前 AUTO_INCREMENT 值存储在内存中. 每次重启通过 select max id 初始化值.

-- 大致方式通过行级锁(排他锁) MAX(id) -> AUTO_INCREMENT init value
SELECT MAX(ai_col) FROM [table_name] FOR UPDATE;

在 MySQL 8.0 之后, 持久化存储在磁盘. 每次更新会写入 redo log 中, 也会刷入 innodb 引擎系统表中记录下来.

如果 MySQL 正常关闭重启, 会从引擎系统表中获取计数器的值.

如果 MySQL 故障重启, 也会从引擎系统表中获取计数器的值;

并且从最后一个检查点开始扫描 redo log 中记录的计数器值; 取二者最大值作为新值.

但是这个处理逻辑也不能保证最后拿到的值是正确的.

如果在系统文件落盘前崩溃, 那么就可能拿到一个之前使用过的值. 这也是数据备份和同步时候可能引发主键冲突根源.

小部分场景

通过对 InnoDB 的 AUTO_INCREMENT 了解, 大致猜测到他的优缺点和使用领域以及现状.

交流 1: 如果考虑分布式场景呢, 高性能领域呢 ?

这时候推荐使用分布式唯一 ID 生成算法器. (用更复杂大炮干复杂长枪) 替代 AUTO_INCREMENT.

补充说明, 在普通领域 AUTO_INCREMENT 也是个长枪级别 ID 生成器.

原理

-- sequece id 生成器表
CREATE TABLE sequece (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '物理主键, 自增 id',
    stub char(1) NOT NULL DEFAULT '' COMMENT '打桩靶子',
    UNIQUE KEY unique_key_stub (stub)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT = '古老分布式 id 生成器';


-- 获取 id
DELIMITER $
BEGIN
REPLACE INTO sequece(stub) VALUES ('X');
SELECT LAST_INSERT_ID();
COMMIT
$

部署

我们也可以多台机器部署, 设置不同 AUTO_INCREMENT step, 让每个 sequece 产生不同号码.

例如部署 step = 2 个服务结点, 并行获取数据.

一个 from 1, 3, 5, 7, 9 ...

一个 from 2, 4, 6, 8, .. .

详细部署操作指导

-- step 标识增长步长, 也标识分布式机器数

show global variables like 'auto_increment%'

+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| auto_increment_increment | 1     |
| auto_increment_offset    | 1     |
+--------------------------+-------+

-- auto_increment_increment 全局步长
-- auto_increment_offset 自增起始值

-- 设定自增步长
-- set session 设置当前会话链接, set global 设置当前 ID 机器
set global auto_increment_increment=step

for i : [0, step)
    CREATE TABLE sequece (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '物理主键, 自增 id',
        stub char(1) NOT NULL DEFAULT '' COMMENT '打桩靶子',
        UNIQUE KEY unique_key_stub (stub)
    ) ENGINE=InnoDB AUTO_INCREMENT = [offset + i] DEFAULT CHARSET=utf8mb4 COMMENT = '古老分布式 id 生成器';

这种古老 MySQL 分布式 ID 生成器, 方案成熟部署简单. 在高并发领域存在 DB 性能瓶颈.

如果考虑高可用主从架构, 在主服务挂了, 从服务顶上时候存在重复发号可能.

关于 分布式唯一 ID 其它业界解决方案, 后面有机会再聊.

交流 2: 为什么官方推荐 AUTO_INCREMENT 当主键, 而很少见到 UUID 等等?

MySQL InnoDB 引擎默认主键索引是 B+ 线索树索引, 也称为聚簇索引(聚集索引, row key 和 row value 存在连续内存中),

为何叫聚簇索引呢?

在 InnoDB 中, 每个表都会有一个聚簇索引, 在定义了主键(primary key)的情况下,

主键所在的列会被作为聚簇索引存储. 所谓聚簇索引,意思是数据实际上是存储在索引的叶子节点上,

「聚簇」的含义就是和相邻的数据紧凑地存储在一起.

因为不值得同时把数据行存储在两个不同的位置,所以一个表只能有一个聚簇索引.

关于 InnoDB 选择哪个列作为聚簇索引存储,大概的优先级为:

如果定义了主键(primary key), 则使用主键;

如果没有定义主键, 则选择第一个不包含 NULL(NOT NULL)的 UNIQUE KEY;

如果也没有, 则会隐式定义一个主键作为聚簇索引.

MySQL 读取磁盘上的数据是一页一页读取的, 如果某条我们要处理的数据在某一页中,

但是这一页其他数据我们都不关心, 这样的请求多了, 性能会急剧下降, 类似于 CPU 的并发杀手 false sharing.

伪共享 (false sharing) 的非标准定义为:
缓存系统中是以缓存行 (cache line) 为单位存储的. 当多线程修改互相独立的变量时,
如果这些变量共享同一个缓存行, 就会无意中影响彼此的性能, 这就是伪共享.

按照 B+ 线索平衡树的原理, AUTO_INCREMENT 的 ID 能保证最新的数据在一页中被读取, 而且减少了 B+ 树分裂翻转.

UUID 等唯一 ID 由于无序, 插入时, B+ 树会不断翻转, 并且最新的数据可能不在同一页.

很可能会出现, 最新一条数据, 和好几年前的数据在同一页.

例如购物和支付交易的订单, 节日促销的抽奖活动这类业务都有这样的使用场景, 访问频率在最近一天, 一周,

或者几个月内比较活跃, 而超过一段时间内的数据很少访问.

当然架构设计是当下业务和未来业务场景之间取舍.

抛开 MySQL AUTO_INCREMENT 的 ID 分布式和锁性能瑕疵, 在尝试分库分表时候他就变得有点累赘. 

一定要用小业务也可以试试研究 MySQL UUID_SHORT() 函数,生成一个 64 位唯一无符号整数. 

UUID_SHORT() 返回值的构造是这样的:(server_id & 255) << 56 + (server_startup_time_in_seconds << 24) + incremented_variable++;

  • 在 server_id 当前服务器的值介于 0 和 255 之间,您的设置主从服务器中是唯一的
  • 您不会在 mysqld restarts 之间设置服务器主机的系统时间
  • UUID_SHORT() 在 mysqld 重启 之间, 你平均每秒调用的次数少于 1600 万次

详细可以查阅更多资料, 也是一种选择. 

后记

行业技术升级变革同样也是是有条不絮.

本文以非常小的点 CREATE TABLE 为切入同大家交流, 欢迎补充纠错. 

对于我们奋斗在一线开发工程师而言, 最主要不是虚拟世界航母上神仙斗法, 更多是现实世界通过技术传承为商业项目把桩打稳打牢固.