目录
分布式 ID 设计举例
我将通过一个更直观的分步示例来说明分布式ID设计 数据库主键自增工作原理。
让我们假设你是一家电商平台的开发者,需要为每个订单生成唯一的ID。
1. 表结构的核心作用
sequence_id
表的核心是利用MySQL的自增主键特性来生成唯一ID。每次插入新记录时,数据库会自动分配一个比当前最大ID大1的新ID。
CREATE TABLE `sequence_id` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, -- 自动生成的唯一ID
`stub` char(10) NOT NULL DEFAULT '', -- 占位符,确保每次插入唯一
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`) -- 强制stub值唯一
) ENGINE=InnoDB;
2. 如何生成唯一ID?
步骤1:插入一条新记录
每次需要生成ID时,向表中插入一条新记录。stub
字段需要填入一个唯一值(可以用UUID、时间戳等)。
-- 插入新记录(UUID_SHORT()生成短唯一值)
INSERT INTO sequence_id (stub) VALUES (UUID_SHORT());
步骤2:获取自动生成的ID
插入成功后,使用LAST_INSERT_ID()
函数获取刚生成的自增ID。
-- 获取刚生成的ID
SELECT LAST_INSERT_ID(); -- 例如返回 1001
3. 实际使用场景演示
假设你需要为用户创建一个新订单,以下是完整流程:
-- 步骤1:生成唯一ID
INSERT INTO sequence_id (stub) VALUES (UUID_SHORT());
SET @order_id = LAST_INSERT_ID(); -- 保存生成的ID到变量
-- 步骤2:使用这个ID创建订单
INSERT INTO orders (order_id, customer_id, product_name, amount)
VALUES (@order_id, 123, 'iPhone', 999.99);
-- 结果:orders表中会新增一条记录,order_id为刚生成的唯一值
4. 为什么需要stub
字段?
stub
字段的唯一索引是这个方案的关键:
- 防止重复插入:如果两次插入的
stub
值相同,数据库会报错(唯一约束冲突),从而避免生成重复ID。 - 保证原子性:数据库的唯一索引校验是原子操作,即使多个服务器同时插入,也只会有一个成功。
5. 分布式系统中的应用
如果你的系统有多个服务器(例如3个API服务器),每个服务器都可以独立执行上述插入操作,最终生成的ID仍然是全局唯一的:
服务器1 → INSERT → 生成ID:1001
服务器2 → INSERT → 生成ID:1002
服务器3 → INSERT → 生成ID:1003
6. 常见问题解答
Q1:为什么不直接用AUTO_INCREMENT
?
- 如果有多个数据库实例(分布式系统),直接用
AUTO_INCREMENT
会导致ID冲突。而这个方案通过唯一索引确保全局唯一。
Q2:UUID_SHORT()
是什么?
- 它是MySQL内置函数,生成64位整数(类似
1234567890123456789
),几乎不会重复。你也可以用UUID()
生成字符串形式的唯一值。
Q3:如果插入失败怎么办?
- 如果
stub
重复导致插入失败,只需重试即可(概率极低)。
总结
这个表的设计本质是利用数据库的自增主键和唯一索引特性,在分布式环境中生成全局唯一ID。每次插入新记录就像“领号机”一样,保证每个号码都是唯一且递增的。
在分布式系统中,多个服务器共享同一个数据库时,唯一索引是共享的,这正是这个方案的核心优势。我通过具体例子解释这个机制:
原理剖析 多服务器共享唯一索引
1. 多服务器共享唯一索引的原理
假设你有3台API服务器(Server A、B、C),它们共同连接到同一个MySQL数据库:
Server A ─┐
Server B ─┼──→ MySQL数据库(包含sequence_id表)
Server C ─┘
当3台服务器同时尝试插入记录时:
- 数据库会校验唯一索引:如果两台服务器生成了相同的
stub
值,数据库会拒绝其中一个插入(报唯一约束冲突错误)。 - 成功插入的请求会获得唯一ID:由于自增主键的特性,每个成功插入的记录都会获得一个全局唯一的ID。
2. 示例演示:并发插入场景
时间点1:Server A插入记录
-- Server A执行
INSERT INTO sequence_id (stub) VALUES ('abc123'); -- 假设stub值为'abc123'
- 数据库检查
stub
唯一索引,发现'abc123'未被使用,插入成功。 - 生成自增ID:1001
时间点2:Server B同时插入相同stub
-- Server B执行
INSERT INTO sequence_id (stub) VALUES ('abc123'); -- 与Server A冲突的stub
- 数据库检查
stub
唯一索引,发现'abc123'已存在,插入失败。 - Server B收到错误:
Duplicate entry 'abc123' for key 'stub'
时间点3:Server B重试并生成新stub
-- Server B重新生成stub并插入
INSERT INTO sequence_id (stub) VALUES ('def456'); -- 新的唯一stub
- 插入成功,生成自增ID:1002
3. 为什么UUID_SHORT()几乎不会冲突?
UUID_SHORT()
生成的是64位整数,格式为:
server_id * 2^56 + 当前时间戳(毫秒) + 自增计数器
- server_id:数据库实例的唯一ID(由
server_id
参数决定) - 时间戳:保证不同时刻生成的值不同
- 计数器:同一毫秒内生成的不同值
因此,只要:
- 各数据库实例的
server_id
不同 - 系统时钟同步
不同服务器生成相同UUID_SHORT()
的概率极低(几乎为0)。
4. 实际应用中的优化
直接频繁插入数据库会影响性能,实际生产中通常会:
- 批量生成ID:一次获取100个ID缓存到本地,用完再获取。
- 使用数据库连接池:减少连接开销。
- 监控唯一索引冲突率:正常情况下冲突率应接近于0。
总结
- 唯一索引是共享的:多个服务器连接同一个数据库时,唯一索引校验由数据库统一处理,确保全局唯一性。
- UUID_SHORT()提供高唯一性:基于服务器ID和时间戳,几乎不会生成重复值。
- 冲突处理:插入失败时重试即可,实际冲突概率极低。
这种方案巧妙利用了数据库的原子性特性,在分布式环境中实现了简单可靠的唯一ID生成。在分库分表的场景下,原方案需要调整,因为不同数据库实例的自增主键不再全局唯一。以下是几种适用于分库分表的唯一ID生成方案:
分库分表情况下怎么使用
方案1:专用ID生成库(推荐)
架构设计
- 独立一个数据库(或集群)作为ID生成器,所有服务共享这个库。
- 保留
sequence_id
表结构不变,所有服务器通过该表生成ID。
应用服务1 ─┐
应用服务2 ─┼──→ ID生成库(专用MySQL实例)
应用服务3 ─┘
优点
- 实现简单,复用原有方案。
- 唯一性由数据库强保证。
缺点
- ID生成可能成为性能瓶颈。
- 存在单点故障风险(可通过主从复制解决)。
方案2:号段模式(性能优化)
改进思路
- 应用每次从ID生成库批量获取一段ID(如1000个),缓存到本地使用,减少数据库访问。
- 表结构增加
current_max_id
和step
字段,记录当前分配的最大ID和步长。
CREATE TABLE `id_generator` (
`biz_type` varchar(50) NOT NULL COMMENT '业务类型',
`current_max_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '当前最大ID',
`step` int(11) NOT NULL DEFAULT 1000 COMMENT '每次获取的号段长度',
PRIMARY KEY (`biz_type`)
) ENGINE=InnoDB;
获取ID流程
- 初始化:插入业务类型记录。
INSERT INTO id_generator (biz_type, current_max_id, step)
VALUES ('order', 0, 1000);
- 获取号段:
-- 1. 开启事务
START TRANSACTION;
-- 2. 查询当前号段并更新
SELECT current_max_id, step
FROM id_generator
WHERE biz_type = 'order'
FOR UPDATE; -- 行锁
-- 3. 更新当前最大ID(假设当前为1000,步长1000)
UPDATE id_generator
SET current_max_id = current_max_id + step
WHERE biz_type = 'order';
-- 4. 提交事务
COMMIT;
-- 返回结果:当前号段为 1001~2000
- 本地缓存使用:应用本地缓存
[1001, 2000]
,用完再获取下一段。
方案3:UUID(完全去中心化)
方案特点
- 使用
UUID()
或UUID_SHORT()
直接生成全局唯一ID,不依赖数据库。 - 例如:
-- 生成标准UUID(字符串形式)
SELECT UUID(); -- 输出:550e8400-e29b-41d4-a716-446655440000
-- 生成短UUID(数字形式,需数据库配置server_id)
SELECT UUID_SHORT(); -- 输出:1234567890123456789
优点
- 完全去中心化,无性能瓶颈。
- 实现简单,无需额外维护ID生成库。
缺点
- ID是随机的,非递增,不适合作为数据库主键(性能较差)。
- UUID字符串占用空间大(36字节),UUID_SHORT可能存在时钟回拨问题。
方案4:雪花算法(Snowflake)
原理
- 将ID划分为:时间戳 + 工作机器ID + 序列号。
- 例如:
0 | 0001100 10100010 10111110 10001001 | 00001 | 00000 0000000000
└┘ └─────────────────────────────┘ └────┘ └──────────────────┘
1bit 41bit时间戳 5bit机器ID 12bit序列号
实现方式
- 独立服务:搭建专门的ID生成服务,基于Snowflake算法生成ID。
- 客户端库:在应用中集成Snowflake算法库(需分配唯一机器ID)。
优点
- 高性能(本地生成,无网络开销)。
- 趋势递增(适合数据库索引)。
缺点
- 需要分配和管理机器ID。
- 依赖系统时钟(时钟回拨会导致ID重复)。
方案对比与选择建议
方案 |
优点 |
缺点 |
适用场景 |
专用ID生成库 |
实现简单,强一致性 |
性能瓶颈,单点风险 |
中小规模系统,ID生成不频繁 |
号段模式 |
高性能,减少数据库访问 |
架构复杂度增加 |
高并发场景 |
UUID |
完全去中心化,无依赖 |
ID非递增,占用空间大 |
对ID有序性无要求的场景 |
雪花算法 |
高性能,趋势递增 |
依赖时钟,需管理机器ID |
大规模分布式系统 |
分库分表下的最佳实践
- 优先考虑号段模式:在高性能和实现复杂度之间取得平衡。
- 结合数据库分片:
-
- 按业务类型分库(如订单库、用户库)。
- 每个库维护独立的
id_generator
表。
- 监控与灾备:
-
- 监控ID生成服务的QPS和响应时间。
- 对ID生成库做读写分离和主从备份。
通过合理设计,分库分表环境下依然可以实现可靠的全局唯一ID生成。