05 - 分布式 ID 生成器设计
面试频率: 难度等级: 推荐时长: 40-50 分钟
目录
需求分析与澄清
业务场景
分布式 ID 生成器是分布式系统的基础组件,用于生成全局唯一的 ID。典型应用场景:
- 订单号生成(电商系统)
- 用户 ID 分配(社交系统)
- 消息 ID(IM 系统)
- 分布式追踪 Trace ID
- 数据库分库分表主键
核心挑战
分布式 ID 生成器的五大挑战:
┌─────────────────────────────────────────────────────────┐
│ 1. 全局唯一性 │
│ - 多台机器并发生成,绝不重复 │
│ - 单机故障不影响唯一性 │
│ │
│ 2. 高性能 │
│ - QPS: 百万级 │
│ - 延迟: < 1ms │
│ │
│ 3. 高可用 │
│ - 单点故障自动切换 │
│ - 容错能力强 │
│ │
│ 4. 趋势递增 │
│ - MySQL InnoDB 索引友好 │
│ - 避免页分裂 │
│ │
│ 5. 信息安全 │
│ - 不能暴露业务信息(如订单量) │
│ - 防止恶意遍历 │
└─────────────────────────────────────────────────────────┘
功能性需求
面试官视角的关键问题
面试官: "设计一个分布式 ID 生成器。"
你需要主动澄清以下需求:
Q1: ID 格式要求?
纯数字 ID: 123456789(int64, 19 位)
字符串 ID: "a1b2c3d4"(可读性好)
UUID: "550e8400-e29b-41d4-a716-446655440000"(全球唯一)
【本次设计】
- 主要支持 int64 数字 ID(最常用)
- 预留字符串 ID 接口
Q2: 性能要求?
QPS: 100 万(单机 10 万)
延迟: < 1ms (P99)
可用性: 99.99%
Q3: 有序性要求?
严格递增: 1, 2, 3, 4, 5...(单机可实现)
趋势递增: 1, 3, 2, 5, 4...(允许局部乱序)
无序: 随机(UUID)
【本次设计】
- 支持趋势递增(兼顾性能和有序性)
- MySQL 索引友好
Q4: 信息泄露?
安全敏感: 订单号(不能暴露订单量)
不敏感: 日志追踪 ID
【本次设计】
- 不包含明显时间戳
- 不连续递增(避免推算总量)
Q5: 是否可回退时钟?
允许: 容忍时钟回拨(复杂)
不允许: 拒绝服务(简单)
【本次设计】
- 检测时钟回拨并等待恢复
- 严重回拨则拒绝服务
非功能性需求
| 需求类型 | 具体要求 | 优先级 |
|---|---|---|
| 全局唯一 | 100% 不重复 | P0 |
| 高性能 | QPS 100 万,延迟 < 1ms | P0 |
| 高可用 | 99.99% 可用性 | P0 |
| 趋势递增 | 大致按时间递增 | P1 |
| 信息安全 | 不泄露业务信息 | P1 |
容量估算
QPS 估算
【业务规模】
日活用户: 1 亿
每用户日均请求: 50 次(订单、消息等)
日请求总量: 1亿 * 50 = 50 亿
平均 QPS: 50亿 / 86400 ≈ 6 万
峰值 QPS: 6万 * 10 = 60 万
【服务器规模】
单机 QPS: 10 万(内存生成)
所需服务器: 60万 / 10万 = 6 台
实际部署: 10 台(考虑冗余)
ID 容量估算
【int64 ID】
范围: -2^63 ~ 2^63-1 (使用正数部分)
最大值: 9,223,372,036,854,775,807 (19 位)
【可用时间】
假设每秒生成 100 万个 ID:
可用天数 = 2^63 / (100万 * 86400) ≈ 106,751,991 天 ≈ 292,471 年
结论: int64 绝对够用
存储估算
【数据库号段模式】
表结构: (biz_type, max_id, step)
单条记录: 100 Bytes
业务类型: 100 个
总存储: 100 * 100B = 10 KB(可忽略)
【Redis 缓存】
缓存预分配号段: 10 个业务 * 10 万号段 * 8 Bytes = 8 MB
总计: < 100 MB
方案对比
方案 1: UUID (Universally Unique Identifier)
原理
UUID v4 (随机):
- 128 bits (16 字节)
- 格式: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
- 示例: 550e8400-e29b-41d4-a716-446655440000
生成算法:
- 随机数 + 时间戳 + MAC 地址(v1)
- 纯随机(v4)
代码实现
package uuid
import (
"github.com/google/uuid"
)
// GenerateUUID 生成 UUID
func GenerateUUID() string {
return uuid.New().String()
}
// 示例
func main() {
id := GenerateUUID()
fmt.Println(id) // 输出: 550e8400-e29b-41d4-a716-446655440000
}
优缺点
优点:
本地生成,无网络开销
性能极高(单机百万 QPS)
全球唯一(理论上)
无单点故障
缺点:
字符串类型,存储占用大(36 字节)
无序,索引性能差(MySQL InnoDB)
不可读,难以排序
有重复风险(极低概率)
适用场景:
- 不需要数字 ID
- 不需要有序性
- 分布式追踪 Trace ID
方案 2: 数据库自增 ID
原理
-- MySQL 自增主键
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(32),
...
);
INSERT INTO orders (order_no) VALUES ('ORDER_001');
-- 返回自增 ID: 1
INSERT INTO orders (order_no) VALUES ('ORDER_002');
-- 返回自增 ID: 2
分布式扩展
【单机问题】
- 单点故障
- 性能瓶颈(QPS 1000-2000)
【分布式方案】
多台 MySQL,设置不同的起始值和步长:
Server 1: ID = 1, 4, 7, 10, 13... (起始值 1, 步长 3)
Server 2: ID = 2, 5, 8, 11, 14... (起始值 2, 步长 3)
Server 3: ID = 3, 6, 9, 12, 15... (起始值 3, 步长 3)
配置:
SET @@auto_increment_offset = 1; -- 起始值
SET @@auto_increment_increment = 3; -- 步长
package db
import (
"database/sql"
)
type DBIDGenerator struct {
dbs []*sql.DB // 多个数据库实例
idx int // 轮询索引
}
func (g *DBIDGenerator) NextID() (int64, error) {
// 轮询选择数据库
db := g.dbs[g.idx%len(g.dbs)]
g.idx++
// 插入空记录获取 ID
result, err := db.Exec("INSERT INTO id_generator (stub) VALUES ('a')")
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
return id, err
}
优缺点
优点:
严格递增,有序
实现简单
数字 ID
缺点:
性能低(QPS 2000-3000)
依赖数据库,可用性差
扩容困难(需要重新分配步长)
ID 连续,容易推算总量
适用场景:
- QPS 低(< 1000)
- 单机系统
- 要求严格递增
方案 3: Redis INCR
原理
Redis INCR 命令:
- 原子性递增
- 单线程,无竞争
- 性能极高(10 万 QPS)
INCR order_id → 返回 1
INCR order_id → 返回 2
INCR order_id → 返回 3
代码实现
package redis
import (
"github.com/go-redis/redis/v8"
)
type RedisIDGenerator struct {
rdb *redis.Client
}
func (g *RedisIDGenerator) NextID(bizType string) (int64, error) {
key := fmt.Sprintf("id:%s", bizType)
// 原子递增
id, err := g.rdb.Incr(context.Background(), key).Result()
return id, err
}
// 示例
func main() {
gen := &RedisIDGenerator{rdb: redis.NewClient(&redis.Options{Addr: "localhost:6379"})}
id1, _ := gen.NextID("order") // 返回 1
id2, _ := gen.NextID("order") // 返回 2
}
分布式扩展
【多 Redis 实例】
类似数据库自增,设置不同起始值和步长:
Redis 1: 1, 4, 7, 10...
Redis 2: 2, 5, 8, 11...
Redis 3: 3, 6, 9, 12...
实现:
EVAL "return redis.call('INCRBY', KEYS[1], ARGV[1])" 1 order_id 3
优缺点
优点:
性能高(10 万 QPS)
实现简单
严格递增(单实例)
缺点:
依赖 Redis,可用性问题
持久化风险(RDB/AOF 丢数据)
扩容困难
ID 连续,信息泄露
适用场景:
- 性能要求高
- 可接受短暂不可用
- 对有序性要求不严格
方案 4: 号段模式(美团 Leaf)
原理
核心思想:批量预分配 ID
流程:
1. 服务启动时,从 DB 获取一个号段 [1000, 2000)
2. 在内存中分配 ID: 1000, 1001, 1002...
3. 号段用完前(如剩余 10%),异步获取下一个号段 [2000, 3000)
4. 双 buffer 切换,无缝衔接
优势:
- 批量获取,减少 DB 压力(1000 次请求只访问 1 次 DB)
- 内存分配,性能极高
- 趋势递增
数据库表结构
CREATE TABLE id_segment (
biz_type VARCHAR(64) PRIMARY KEY COMMENT '业务类型',
max_id BIGINT UNSIGNED NOT NULL COMMENT '当前最大 ID',
step INT UNSIGNED NOT NULL DEFAULT 1000 COMMENT '步长',
description VARCHAR(255) DEFAULT '' COMMENT '描述',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_updated (updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ID 号段表';
-- 初始化数据
INSERT INTO id_segment (biz_type, max_id, step, description) VALUES
('order', 0, 1000, '订单 ID'),
('user', 0, 1000, '用户 ID'),
('message', 0, 10000, '消息 ID');
代码实现
package leaf
import (
"database/sql"
"sync"
)
// Segment 号段
type Segment struct {
Start int64 // 起始 ID
End int64 // 结束 ID
Pos int64 // 当前位置
}
// SegmentIDGenerator 号段模式 ID 生成器
type SegmentIDGenerator struct {
db *sql.DB
bizType string
step int
current *Segment // 当前号段
next *Segment // 下一个号段(双 buffer)
mu sync.Mutex
loading bool // 是否正在加载下一个号段
}
func NewSegmentIDGenerator(db *sql.DB, bizType string) *SegmentIDGenerator {
gen := &SegmentIDGenerator{
db: db,
bizType: bizType,
step: 1000,
}
// 初始化第一个号段
gen.current = gen.fetchNextSegment()
return gen
}
// NextID 获取下一个 ID
func (g *SegmentIDGenerator) NextID() (int64, error) {
g.mu.Lock()
defer g.mu.Unlock()
// 检查当前号段是否即将用完(剩余 10%)
if g.current.Pos >= int64(float64(g.current.End-g.current.Start)*0.9) && !g.loading {
// 异步加载下一个号段
g.loading = true
go g.loadNextSegment()
}
// 检查当前号段是否用完
if g.current.Pos >= g.current.End {
// 切换到下一个号段
if g.next == nil {
// 同步等待加载
g.next = g.fetchNextSegment()
}
g.current = g.next
g.next = nil
g.loading = false
}
// 分配 ID
id := g.current.Pos
g.current.Pos++
return id, nil
}
// loadNextSegment 异步加载下一个号段
func (g *SegmentIDGenerator) loadNextSegment() {
g.next = g.fetchNextSegment()
}
// fetchNextSegment 从数据库获取下一个号段
func (g *SegmentIDGenerator) fetchNextSegment() *Segment {
// 开启事务
tx, _ := g.db.Begin()
defer tx.Rollback()
// 查询当前 max_id
var maxID int64
var step int
query := "SELECT max_id, step FROM id_segment WHERE biz_type = ? FOR UPDATE"
tx.QueryRow(query, g.bizType).Scan(&maxID, &step)
// 更新 max_id
newMaxID := maxID + int64(step)
updateSQL := "UPDATE id_segment SET max_id = ? WHERE biz_type = ?"
tx.Exec(updateSQL, newMaxID, g.bizType)
// 提交事务
tx.Commit()
// 返回新号段
return &Segment{
Start: maxID + 1,
End: newMaxID + 1,
Pos: maxID + 1,
}
}
优缺点
优点:
性能极高(10 万 QPS,纯内存操作)
趋势递增
数据库压力小(批量获取)
可扩展(多实例)
缺点:
依赖数据库(可用性)
ID 不连续(服务重启号段浪费)
有少量信息泄露(可推算大致数量)
适用场景:
- 性能要求高(10 万+ QPS)
- 需要趋势递增
- 订单号、用户 ID 等
- **最常用方案**
方案 5: Snowflake(雪花算法)
原理
Snowflake ID 结构(64 bits):
┌─────────────────────────────────────────────────────────┐
│ 0 │ 41 bits 时间戳 │ 10 bits 机器ID │ 12 bits 序列号 │
└─────────────────────────────────────────────────────────┘
1bit 41 bits 10 bits 12 bits
(符号) (毫秒级时间戳) (机器标识) (毫秒内序列)
各部分说明:
- 1 bit 符号位: 固定为 0(正数)
- 41 bits 时间戳: 毫秒级,可用 69 年
(2^41 / 1000 / 60 / 60 / 24 / 365 ≈ 69 年)
- 10 bits 机器 ID: 最多 1024 台机器
(可拆分为 5 bits 数据中心 + 5 bits 机器)
- 12 bits 序列号: 每毫秒最多 4096 个 ID
性能:
- 单机 QPS: 4096 * 1000 = 409 万
- 实际 QPS: 100 万+(考虑竞争)
代码实现
package snowflake
import (
"errors"
"sync"
"time"
)
const (
epoch = int64(1609459200000) // 起始时间戳: 2021-01-01 00:00:00
timestampBits = uint(41)
machineIDBits = uint(10)
sequenceBits = uint(12)
maxMachineID = int64(-1) ^ (int64(-1) << machineIDBits) // 1023
maxSequence = int64(-1) ^ (int64(-1) << sequenceBits) // 4095
machineIDShift = sequenceBits
timestampShift = sequenceBits + machineIDBits
)
type Snowflake struct {
mu sync.Mutex
timestamp int64 // 上次生成 ID 的时间戳
machineID int64 // 机器 ID (0-1023)
sequence int64 // 序列号 (0-4095)
}
func NewSnowflake(machineID int64) (*Snowflake, error) {
if machineID < 0 || machineID > maxMachineID {
return nil, errors.New("machine ID 超出范围")
}
return &Snowflake{
timestamp: 0,
machineID: machineID,
sequence: 0,
}, nil
}
func (s *Snowflake) NextID() (int64, error) {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().UnixNano() / 1e6 // 毫秒
if now < s.timestamp {
// 时钟回拨
return 0, errors.New("时钟回拨,拒绝生成 ID")
}
if now == s.timestamp {
// 同一毫秒内,序列号递增
s.sequence = (s.sequence + 1) & maxSequence
if s.sequence == 0 {
// 序列号溢出,等待下一毫秒
now = s.waitNextMillis(s.timestamp)
}
} else {
// 新的毫秒,序列号重置
s.sequence = 0
}
s.timestamp = now
// 组装 ID
id := ((now - epoch) << timestampShift) |
(s.machineID << machineIDShift) |
s.sequence
return id, nil
}
func (s *Snowflake) waitNextMillis(lastTimestamp int64) int64 {
now := time.Now().UnixNano() / 1e6
for now <= lastTimestamp {
now = time.Now().UnixNano() / 1e6
}
return now
}
// 解析 Snowflake ID
func ParseSnowflakeID(id int64) (timestamp int64, machineID int64, sequence int64) {
timestamp = (id >> timestampShift) + epoch
machineID = (id >> machineIDShift) & maxMachineID
sequence = id & maxSequence
return
}
时钟回拨处理
// 方案 1: 拒绝服务(简单)
if now < s.timestamp {
return 0, errors.New("时钟回拨")
}
// 方案 2: 等待追赶(回拨幅度小)
if now < s.timestamp {
offset := s.timestamp - now
if offset <= 5 { // 回拨 5ms 内,等待
time.Sleep(time.Duration(offset) * time.Millisecond)
now = time.Now().UnixNano() / 1e6
} else {
return 0, errors.New("时钟回拨过大")
}
}
// 方案 3: 使用备用机器 ID(复杂)
if now < s.timestamp {
s.machineID = (s.machineID + 1) & maxMachineID
s.timestamp = now
s.sequence = 0
}
优缺点
优点:
性能极高(单机 100 万+ QPS)
趋势递增(大致按时间递增)
无依赖,本地生成
包含时间信息(可解析)
全局唯一(理论上)
缺点:
依赖时钟(时钟回拨问题)
机器 ID 需要手动分配
有序性不如号段模式
有信息泄露(可反推时间)
适用场景:
- 性能要求极高(100 万+ QPS)
- 多数据中心部署
- 分布式追踪
- **互联网大厂常用**
方案对比总结
| 方案 | 性能 | 有序性 | 依赖 | 复杂度 | 推荐指数 |
|---|---|---|---|---|---|
| UUID | 无序 | 无 | 低 | ||
| 数据库自增 | 严格递增 | MySQL | 低 | ||
| Redis INCR | 严格递增 | Redis | 低 | ||
| 号段模式 | 趋势递增 | MySQL | 中 | ||
| Snowflake | 趋势递增 | 无 | 中 |
推荐方案:
- 通用场景: 号段模式(美团 Leaf)
- 极致性能: Snowflake
- 简单场景: Redis INCR
- 追踪场景: UUID
架构设计
号段模式架构(推荐)
客户端
↓
┌────────────────────────────────────────┐
│ 负载均衡(Nginx/LVS) │
└────────────────────────────────────────┘
↓
┌────────────────────────────────────────┐
│ ID 生成服务集群(10 台) │
│ ┌──────────────────────────────────┐ │
│ │ 内存号段分配 │ │
│ │ - 双 buffer 切换 │ │
│ │ │ - 异步加载下一号段 │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘
↓
┌────────────────────────────────────────┐
│ MySQL 主从 │
│ - id_segment 表(号段元数据) │
│ - 读写分离 │
└────────────────────────────────────────┘
Snowflake 架构(高性能)
客户端
↓
┌────────────────────────────────────────┐
│ 负载均衡(Nginx/LVS) │
└────────────────────────────────────────┘
↓
┌────────────────────────────────────────┐
│ Snowflake 服务集群(10 台) │
│ ┌──────────────────────────────────┐ │
│ │ 本地生成 ID │ │
│ │ - 机器 ID: 手动配置或 ZK 分配 │ │
│ │ - 无状态服务 │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘
↓
┌────────────────────────────────────────┐
│ ZooKeeper(可选) │
│ - 自动分配机器 ID │
│ - 避免冲突 │
└────────────────────────────────────────┘
混合架构(生产级)
客户端
↓
┌────────────────────────────────────────┐
│ API Gateway │
│ - 路由到不同 ID 生成服务 │
└────────────────────────────────────────┘
↓
┌─────────────────┬──────────────────────┐
│ 号段模式 │ Snowflake │
│ (订单、用户) │ (日志、追踪) │
└─────────────────┴──────────────────────┘
优化方案
1. 号段模式优化
动态步长调整
// 根据 QPS 动态调整步长
func (g *SegmentIDGenerator) adjustStep() {
// 统计最近 1 分钟的 QPS
qps := g.getRecentQPS()
if qps > 10000 {
// 高 QPS,增大步长
g.step = 10000
} else if qps > 1000 {
g.step = 1000
} else {
g.step = 100
}
log.Infof("动态调整步长: %d", g.step)
}
多业务隔离
// 不同业务类型使用不同的号段生成器
type IDGeneratorManager struct {
generators map[string]*SegmentIDGenerator
mu sync.RWMutex
}
func (m *IDGeneratorManager) GetGenerator(bizType string) *SegmentIDGenerator {
m.mu.RLock()
gen, exists := m.generators[bizType]
m.mu.RUnlock()
if !exists {
m.mu.Lock()
defer m.mu.Unlock()
// 双重检查
if gen, exists = m.generators[bizType]; !exists {
gen = NewSegmentIDGenerator(m.db, bizType)
m.generators[bizType] = gen
}
}
return gen
}
func (m *IDGeneratorManager) NextID(bizType string) (int64, error) {
gen := m.GetGenerator(bizType)
return gen.NextID()
}
2. Snowflake 优化
机器 ID 自动分配(ZooKeeper)
package snowflake
import (
"github.com/go-zookeeper/zk"
)
type SnowflakeWithZK struct {
*Snowflake
zkConn *zk.Conn
zkPath string
}
func NewSnowflakeWithZK(zkServers []string, zkPath string) (*SnowflakeWithZK, error) {
// 连接 ZooKeeper
conn, _, err := zk.Connect(zkServers, time.Second*10)
if err != nil {
return nil, err
}
// 自动分配机器 ID
machineID, err := allocateMachineID(conn, zkPath)
if err != nil {
return nil, err
}
sf, _ := NewSnowflake(machineID)
return &SnowflakeWithZK{
Snowflake: sf,
zkConn: conn,
zkPath: zkPath,
}, nil
}
func allocateMachineID(conn *zk.Conn, zkPath string) (int64, error) {
// 创建临时顺序节点
path, err := conn.Create(
zkPath+"/worker-",
[]byte{},
zk.FlagEphemeral|zk.FlagSequence,
zk.WorldACL(zk.PermAll),
)
if err != nil {
return 0, err
}
// 从路径中提取序号作为机器 ID
// 例如: /snowflake/worker-0000000001 → 机器 ID = 1
parts := strings.Split(path, "-")
seq := parts[len(parts)-1]
machineID, _ := strconv.ParseInt(seq, 10, 64)
log.Infof("分配机器 ID: %d", machineID)
return machineID, nil
}
批量生成(减少锁竞争)
// 批量生成 ID(一次生成 100 个)
func (s *Snowflake) NextIDBatch(count int) ([]int64, error) {
ids := make([]int64, 0, count)
s.mu.Lock()
defer s.mu.Unlock()
for i := 0; i < count; i++ {
id, err := s.nextIDWithoutLock()
if err != nil {
return ids, err
}
ids = append(ids, id)
}
return ids, nil
}
func (s *Snowflake) nextIDWithoutLock() (int64, error) {
now := time.Now().UnixNano() / 1e6
if now < s.timestamp {
return 0, errors.New("时钟回拨")
}
if now == s.timestamp {
s.sequence = (s.sequence + 1) & maxSequence
if s.sequence == 0 {
now = s.waitNextMillis(s.timestamp)
}
} else {
s.sequence = 0
}
s.timestamp = now
id := ((now - epoch) << timestampShift) |
(s.machineID << machineIDShift) |
s.sequence
return id, nil
}
3. 高可用优化
【多数据中心部署】
Region 1 (华北):
- Snowflake 机器 ID: 0-199
- 号段起始: 1, 1001, 2001...
Region 2 (华东):
- Snowflake 机器 ID: 200-399
- 号段起始: 500, 1500, 2500...
Region 3 (华南):
- Snowflake 机器 ID: 400-599
- 号段起始: 250, 1250, 2250...
【故障切换】
- 主节点故障,自动切换到备节点
- ZooKeeper 监听节点状态
- 30 秒内完成切换
监控告警
核心监控指标
var (
// ID 生成 QPS
IDGenerateQPS = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "id_generate_total",
Help: "Total ID generated",
},
[]string{"biz_type"},
)
// ID 生成延迟
IDGenerateLatency = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "id_generate_latency_seconds",
Help: "ID generate latency",
Buckets: prometheus.DefBuckets,
},
[]string{"biz_type"},
)
// 号段剩余量
SegmentRemaining = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "id_segment_remaining",
Help: "Remaining IDs in current segment",
},
[]string{"biz_type"},
)
// 时钟回拨次数
ClockBackwardCount = promauto.NewCounter(
prometheus.CounterOpts{
Name: "id_clock_backward_total",
Help: "Total clock backward events",
},
)
)
// 使用示例
func (g *SegmentIDGenerator) NextID() (int64, error) {
timer := prometheus.NewTimer(IDGenerateLatency.WithLabelValues(g.bizType))
defer timer.ObserveDuration()
id, err := g.nextID()
if err == nil {
IDGenerateQPS.WithLabelValues(g.bizType).Inc()
SegmentRemaining.WithLabelValues(g.bizType).Set(float64(g.current.End - g.current.Pos))
}
return id, err
}
告警规则
groups:
- name: id_generator_alerts
rules:
# ID 生成失败率过高
- alert: HighIDGenerateFailureRate
expr: rate(id_generate_errors_total[1m]) / rate(id_generate_total[1m]) > 0.01
for: 2m
labels:
severity: critical
annotations:
summary: "ID 生成失败率过高"
description: "失败率 {{ $value }} 超过 1%"
# 号段即将用完
- alert: SegmentAlmostEmpty
expr: id_segment_remaining < 100
for: 1m
labels:
severity: warning
annotations:
summary: "号段即将用完"
description: "业务 {{ $labels.biz_type }} 剩余 ID: {{ $value }}"
# 时钟回拨
- alert: ClockBackward
expr: increase(id_clock_backward_total[5m]) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "检测到时钟回拨"
description: "最近 5 分钟发生 {{ $value }} 次时钟回拨"
面试问答
为什么不用 UUID?
【UUID 的问题】
1. 无序性
- UUID 是随机生成
- MySQL InnoDB 使用 B+ 树索引
- 无序插入导致页分裂,性能下降
- 有序 ID 插入性能高 10 倍以上
2. 存储空间大
- UUID: 36 字节(字符串)或 16 字节(二进制)
- int64: 8 字节
- 对于海量数据,存储成本高
3. 不可读
- UUID: "550e8400-e29b-41d4-a716-446655440000"
- 不便于排序、比较、调试
【何时使用 UUID】
- 分布式追踪 Trace ID
- 临时文件名
- 不需要数字 ID 的场景
Snowflake 时钟回拨怎么处理?
【时钟回拨原因】
1. 人工调整系统时间
2. NTP 时间同步(突然回拨)
3. 虚拟机时间漂移
【处理方案】
1. 拒绝服务(简单)
- 检测到回拨,直接返回错误
- 优点: 简单可靠
- 缺点: 可用性差
2. 等待追赶(小幅回拨)
- 回拨 5ms 内,sleep 等待
- 超过 5ms 拒绝服务
- 优点: 兼顾可用性和正确性
- 缺点: 有延迟
3. 使用备用机器 ID
- 回拨时切换到备用机器 ID
- 继续生成 ID
- 优点: 可用性高
- 缺点: 复杂,需要预留机器 ID
4. 使用单调时钟(推荐)
- Go: time.Now().UnixNano() → monotonic clock
- Linux: clock_gettime(CLOCK_MONOTONIC)
- 不受系统时间调整影响
- 但无法跨机器比较
【最佳实践】
- 部署 NTP 服务,平滑同步时间
- 禁止人工调整时间
- 监控时钟回拨事件
- 方案 2(等待追赶)+ 方案 3(备用ID)
号段模式 vs Snowflake,如何选择?
【号段模式】
优点:
严格趋势递增(号段内连续)
无时钟依赖
扩展简单(加机器即可)
缺点:
依赖数据库(可用性)
服务重启号段浪费
适用:
- 订单号、用户 ID(需要趋势递增)
- 中小规模系统(QPS < 100 万)
【Snowflake】
优点:
性能极高(100 万+ QPS)
无依赖(本地生成)
包含时间信息
缺点:
依赖时钟
机器 ID 需要分配
适用:
- 高性能场景(QPS > 100 万)
- 多数据中心
- 消息 ID、追踪 ID
【选择建议】
- 通用场景: 号段模式(简单可靠)
- 极致性能: Snowflake
- 不需要数字ID: UUID
如何保证全局唯一性?
【号段模式】
1. 数据库事务
- UPDATE id_segment SET max_id = max_id + step
- 事务保证原子性
- 不同机器获取的号段不重叠
2. 分段加锁
- 每次获取号段时加锁(FOR UPDATE)
- 避免并发冲突
【Snowflake】
1. 机器 ID 唯一
- 手动分配或 ZK 自动分配
- 1024 台机器,每台唯一 ID
2. 单机内原子性
- 时间戳 + 序列号组合
- 同一毫秒内序列号递增
- 互斥锁保证原子性
3. 时间戳单调递增
- 检测时钟回拨
- 拒绝生成 ID
【极端情况】
- 数据库主从延迟: 使用事务,不受影响
- 网络分区: Snowflake 无依赖,继续生成
- 机器 ID 冲突: ZK 分配避免冲突
如何实现趋势递增?
【为什么需要趋势递增】
MySQL InnoDB 使用 B+ 树索引:
- 顺序插入: 直接追加到叶子节点末尾
- 随机插入: 需要查找位置,可能导致页分裂
- 页分裂: 性能下降 10 倍
【实现方式】
1. 号段模式
- 号段内严格递增
- 号段间趋势递增
- 示例: [1-1000], [1001-2000], [2001-3000]
2. Snowflake
- 时间戳占高位(41 bits)
- 大部分时间递增
- 同一毫秒内,序列号递增
3. 时间戳 + 随机数
- 高位: 时间戳(秒级)
- 低位: 随机数
- 1 秒内无序,秒级递增
【性能测试】
INSERT INTO orders (id, ...) VALUES (?, ...)
顺序 ID: 5000 TPS
随机 ID: 500 TPS(性能下降 90%)
号段模式如何实现高可用?
【问题】
- 单点数据库故障,无法获取号段
【解决方案】
1. 数据库主从
- 主库故障,自动切换到从库
- 30 秒内恢复
2. 双 buffer
- 提前加载下一个号段
- 数据库短暂不可用不影响
3. 本地缓存号段
- 服务启动时,预加载多个号段
- 数据库故障时,使用缓存号段
4. 多数据库实例
- 不同业务使用不同数据库
- 降低单点风险
【可用性分析】
数据库可用性: 99.9%
双 buffer: 容忍 30 秒故障
综合可用性: 99.99%+
Snowflake 机器 ID 如何分配?
【方案 1: 手动配置】
优点: 简单可靠
缺点: 扩容麻烦,容易冲突
【方案 2: 配置中心】
- 使用 ZooKeeper、etcd、Consul
- 服务启动时申请机器 ID
- 创建临时节点,自动分配序号
实现:
/snowflake
├── worker-0000000001 (机器 ID: 1)
├── worker-0000000002 (机器 ID: 2)
└── worker-0000000003 (机器 ID: 3)
【方案 3: IP 地址】
- 使用 IP 地址最后 10 bits
- 例如: 192.168.1.100 → 机器 ID = 100
- 简单但不灵活
【方案 4: 数据库分配】
CREATE TABLE snowflake_machine (
machine_id INT PRIMARY KEY,
ip_address VARCHAR(15),
last_heartbeat TIMESTAMP
);
服务启动:
1. 查询空闲的机器 ID
2. 更新 IP 和心跳
3. 定期刷新心跳(30 秒)
4. 心跳超时释放机器 ID
【推荐】
方案 2(ZooKeeper)或方案 4(数据库)
如何防止 ID 被恶意遍历?
【风险】
连续递增 ID:
- 用户 ID: 1, 2, 3, 4, 5...
- 恶意遍历: /api/users/1, /api/users/2...
- 泄露用户信息
【解决方案】
1. ID 加密(可逆)
- 原始 ID: 12345
- 加密后: "a1b2c3d4"
- 展示给用户: "a1b2c3d4"
- 内部解密: 12345
2. 混淆算法
- 原始 ID: 12345
- 混淆后: 87654 (规则可逆)
- 使用异或、位移等
3. UUID 映射
- 数据库: id=12345, uuid="550e8400-..."
- API 使用 UUID
- 内部查询: SELECT * FROM users WHERE uuid=?
4. 权限控制(根本方案)
- 检查用户权限
- 不能访问他人信息
- ID 不是唯一防线
【实现示例】
func EncryptID(id int64, key string) string {
// 简单异或加密
encrypted := id ^ hashKey(key)
return base62Encode(encrypted)
}
func DecryptID(encrypted string, key string) int64 {
id := base62Decode(encrypted)
return id ^ hashKey(key)
}
号段用完了怎么办?
【场景】
- 号段剩余 10%,开始异步加载下一号段
- 但 QPS 突增,号段提前用完
- 下一号段还没加载完
【解决方案】
1. 双 buffer(已实现)
- 提前加载下一号段
- 通常能覆盖 99% 情况
2. 同步加载(兜底)
- 当前号段用完,下一号段未就绪
- 阻塞等待加载完成
- 用户请求延迟增加(100-200ms)
3. 增大步长
- 动态调整步长
- 高 QPS 时,step = 10000
- 低 QPS 时,step = 1000
4. 多实例负载均衡
- 10 台机器,每台独立号段
- 单台压力过大,自动分流
【监控告警】
- 号段剩余 < 20%,预警
- 号段剩余 < 5%,告警
- 号段用完且同步加载,严重告警
从 0 到 1 设计分布式 ID 生成器,步骤是什么?
【设计步骤】
1. 需求分析(5 分钟)
- 规模: QPS 100 万
- 格式: int64 数字 ID
- 有序: 趋势递增
2. 方案选型(5 分钟)
- UUID: 性能高但无序,不推荐
- 数据库自增: 性能低,不推荐
- Redis INCR: 性能中,可用性差
- 号段模式: 性能高,推荐
- Snowflake: 性能极高,推荐
3. 架构设计(15 分钟)
【号段模式】
- 数据库存储号段元数据
- 服务批量获取号段
- 内存分配 ID
- 双 buffer 切换
【Snowflake】
- 64 bits: 时间戳 + 机器ID + 序列号
- 本地生成,无依赖
- 机器 ID 自动分配(ZK)
4. 核心代码(15 分钟)
- 号段获取逻辑
- 双 buffer 切换
- Snowflake ID 生成
5. 优化方案(5 分钟)
- 动态步长调整
- 批量生成 ID
- 高可用方案
6. 监控告警(5 分钟)
- QPS 监控
- 号段剩余监控
- 时钟回拨告警
【加分项】
- 画架构图
- 写核心代码
- 分析各方案优缺点
- 提出优化方案