分布式ID设计 数据库主键自增

发布于:2025-05-09 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

分布式 ID 设计举例

1. 表结构的核心作用

2. 如何生成唯一ID?

步骤1:插入一条新记录

步骤2:获取自动生成的ID

3. 实际使用场景演示

4. 为什么需要stub字段?

5. 分布式系统中的应用

6. 常见问题解答

Q1:为什么不直接用AUTO_INCREMENT?

Q2:UUID_SHORT()是什么?

Q3:如果插入失败怎么办?

总结

原理剖析 多服务器共享唯一索引

1. 多服务器共享唯一索引的原理

2. 示例演示:并发插入场景

时间点1:Server A插入记录

时间点2:Server B同时插入相同stub

时间点3:Server B重试并生成新stub

3. 为什么UUID_SHORT()几乎不会冲突?

4. 实际应用中的优化

总结

分库分表情况下怎么使用

方案1:专用ID生成库(推荐)

架构设计

优点

缺点

方案2:号段模式(性能优化)

改进思路

获取ID流程

方案3:UUID(完全去中心化)

方案特点

优点

缺点

方案4:雪花算法(Snowflake)

原理

实现方式

优点

缺点

方案对比与选择建议

分库分表下的最佳实践


分布式 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台服务器同时尝试插入记录时:

  1. 数据库会校验唯一索引:如果两台服务器生成了相同的stub值,数据库会拒绝其中一个插入(报唯一约束冲突错误)。
  2. 成功插入的请求会获得唯一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参数决定)
  • 时间戳:保证不同时刻生成的值不同
  • 计数器:同一毫秒内生成的不同值

因此,只要:

  1. 各数据库实例的server_id不同
  2. 系统时钟同步

不同服务器生成相同UUID_SHORT()的概率极低(几乎为0)。

4. 实际应用中的优化

直接频繁插入数据库会影响性能,实际生产中通常会:

  1. 批量生成ID:一次获取100个ID缓存到本地,用完再获取。
  2. 使用数据库连接池:减少连接开销。
  3. 监控唯一索引冲突率:正常情况下冲突率应接近于0。

总结

  • 唯一索引是共享的:多个服务器连接同一个数据库时,唯一索引校验由数据库统一处理,确保全局唯一性。
  • UUID_SHORT()提供高唯一性:基于服务器ID和时间戳,几乎不会生成重复值。
  • 冲突处理:插入失败时重试即可,实际冲突概率极低。

这种方案巧妙利用了数据库的原子性特性,在分布式环境中实现了简单可靠的唯一ID生成。在分库分表的场景下,原方案需要调整,因为不同数据库实例的自增主键不再全局唯一。以下是几种适用于分库分表的唯一ID生成方案:

分库分表情况下怎么使用

方案1:专用ID生成库(推荐)

架构设计
  • 独立一个数据库(或集群)作为ID生成器,所有服务共享这个库。
  • 保留sequence_id表结构不变,所有服务器通过该表生成ID。
应用服务1 ─┐
应用服务2 ─┼──→ ID生成库(专用MySQL实例)
应用服务3 ─┘
优点
  • 实现简单,复用原有方案。
  • 唯一性由数据库强保证。
缺点
  • ID生成可能成为性能瓶颈。
  • 存在单点故障风险(可通过主从复制解决)。

方案2:号段模式(性能优化)

改进思路
  • 应用每次从ID生成库批量获取一段ID(如1000个),缓存到本地使用,减少数据库访问。
  • 表结构增加current_max_idstep字段,记录当前分配的最大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流程
  1. 初始化:插入业务类型记录。
INSERT INTO id_generator (biz_type, current_max_id, step)
VALUES ('order', 0, 1000);
  1. 获取号段
-- 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
  1. 本地缓存使用:应用本地缓存[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

大规模分布式系统

分库分表下的最佳实践

  1. 优先考虑号段模式:在高性能和实现复杂度之间取得平衡。
  2. 结合数据库分片
    • 按业务类型分库(如订单库、用户库)。
    • 每个库维护独立的id_generator表。
  1. 监控与灾备
    • 监控ID生成服务的QPS和响应时间。
    • 对ID生成库做读写分离和主从备份。

通过合理设计,分库分表环境下依然可以实现可靠的全局唯一ID生成。


网站公告

今日签到

点亮在社区的每一天
去签到