HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 系统设计实战

    • 系统设计面试教程
    • 系统设计方法论
    • 01-短链系统设计
    • 02 - 秒杀系统设计
    • 03 - IM 即时通讯系统设计
    • 04 - Feed 流系统设计
    • 05 - 分布式 ID 生成器设计
    • 06 - 限流系统设计
    • 第7章:搜索引擎设计
    • 08 - 推荐系统设计
    • 09 - 支付系统设计
    • 10 - 电商系统设计
    • 11 - 直播系统设计
    • 第12章:缓存系统设计
    • 第13章:消息队列设计
    • 第14章:分布式事务
    • 15 - 监控系统设计

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 万,延迟 < 1msP0
高可用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 监控
   - 号段剩余监控
   - 时钟回拨告警

【加分项】
- 画架构图
- 写核心代码
- 分析各方案优缺点
- 提出优化方案

Prev
04 - Feed 流系统设计
Next
06 - 限流系统设计