概述

雪花算法(Snowflake)是一种分布式唯一 ID 生成算法,由 Twitter 公司设计,用于解决分布式系统中生成全局唯一 ID 的需求。它能在高并发场景下高效生成不重复的 ID,且生成的 ID 具备有序性,便于数据库索引优化。

结构

雪花算法生成的 ID 是一个 64 为长整型 long 数字:

  • 符号位(1 位):二进制中最高位为符号位,默认为 0,表示生成的 ID 为正
  • 时间戳(41 位):记录当前时间戳-算法指定的起始时间戳这个相对时间
  • 工作节点(10 位):用于区分分布式系统中的不同节点(机器或进程),确保不同节点生成的 ID 不冲突。10 位最多支持 1024 个节点,可根据实际需求拆分(如 5 位数据中心 ID + 5 位机器 ID,支持 32 个数据中心,每个中心 32 台机器)。
  • 序列号(12 位):用于解决同一节点在同一毫秒内生成多个 ID 的冲突问题。

应用场景

  • 分布式数据库表的主键 ID,常用于分库分表
  • 分布式系统中的订单号、流水号等唯一标识
  • 日志追踪、消息队列等需要唯一 ID 的场景

优势

  • 全局唯一:通过时间戳、节点 ID 和序列号的组合,确保在分布式环境下生成的 ID 不重复。
  • 有序性:ID 随时间递增,便于数据库建立索引(有序 ID 比 UUID 等无序 ID 的索引性能更好)。
  • 可解析:通过 ID 可反推生成时间、节点信息,便于问题追踪

潜在问题

  • 若服务器时钟发生回拨,可能生成重复 ID。可暂停 ID 生成直至追上之前的时间
  • 若节点 ID 配置重复,会导致不同节点生成相同 ID。可通过分布式协调工具自动分配节点 ID,确保唯一性
  • 同一节点同一毫秒最多生成 4096 个 ID,超高并发场景可能不够用。可适当调整位数分配(如减少节点 ID 位数,增加序列号位数)

算法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class SnowflakeIdGenerator {
// 起始时间戳(示例:2020-01-01 00:00:00)
private final long startTime = 1577808000000L;

// 节点ID位数(5位数据中心 + 5位机器)
private final long datacenterIdBits = 5L;
private final long workerIdBits = 5L;
private final long maxDatacenterId = ~(-1L << datacenterIdBits); // 31
private final long maxWorkerId = ~(-1L << workerIdBits); // 31

// 序列号位数
private final long sequenceBits = 12L;
private final long sequenceMask = ~(-1L << sequenceBits); // 4095

// 位移量
private final long workerIdShift = sequenceBits; // 12
private final long datacenterIdShift = sequenceBits + workerIdBits; // 17
private final long timestampShift = sequenceBits + workerIdBits + datacenterIdBits; // 22

private final long datacenterId; // 数据中心ID
private final long workerId; // 机器ID
private long sequence = 0L; // 序列号
private long lastTimestamp = -1L; // 上一次生成ID的时间戳

// 构造方法,传入数据中心ID和机器ID
public SnowflakeIdGenerator(long datacenterId, long workerId) {
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException("数据中心ID超出范围");
}
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("机器ID超出范围");
}
this.datacenterId = datacenterId;
this.workerId = workerId;
}

// 生成下一个ID
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();

// 处理时钟回拨
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成ID");
}

// 同一毫秒内,序列号自增
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
// 序列号溢出(超过4095),等待下一毫秒
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 不同毫秒,序列号重置为0
sequence = 0L;
}

lastTimestamp = timestamp;

// 组合ID:时间戳位移 + 数据中心ID位移 + 机器ID位移 + 序列号
return ((timestamp - startTime) << timestampShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}

// 等待至下一毫秒
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}

分库分表

在分库分表场景中,确定数据存储在哪个数据库(分库)和哪个表(分表)的过程称为「数据路由」,核心是通过「分片键(Shard Key)」和预设的「路由算法」计算目标位置。
核心:确定分片键,用于计算数据存储位置的字段,需满足高频查询中常作为条件,能让数据均匀分布到各分库分表。

  • 订单表常用「用户 ID」或「订单 ID」作为分片键;
  • 日志表常用「时间戳」作为分片键;
  • 地区相关表常用「地区 ID」作为分片键

路由算法

哈希算法

原理:对分片键进行哈希计算,再通过取模(mod)确定目标库和表。

  • 分库:库索引 = hash(分片键) % 分库数量
  • 分库:库索引 = hash(分片键) % 分库数量

哈希算法可以使用 MD5
示例:假设分 2 个库(db0、db1),每个库分 3 个表(t0、t1、t2),分片键为「用户 ID=1001」

  • 计算 hash(1001)=5234(假设哈希值)
  • 分库索引:5234%2=0,路由到 db0
  • 分表索引:5234%3=1,路由到 db0.t1
    优点:
  • 数据分布均匀,避免单库/单表负载过高
  • 计算简单,路由速度快
    缺点:
  • 扩容困难,会导致大量数据需要迁移
  • 不支持范围查询

范围分片算法

原理:按分片键的「连续范围」划分库表。例如按 ID 范围、时间范围等。

  • 分库:若 分片键 ∈ [A, B) → 路由到 db0;若 ∈ [B, C) → 路由到 db1
  • 分表:每个库内再按更小的范围分表(如 db0 包含 t0: [A, A1)、t1: [A1, B))
    优点:
  • 支持范围查询
  • 扩容简单
    缺点:
  • 当某段时间热点数据暴增,数据分库可能不均
  • 热点数据集中

雪花算法在分布式数据库中的应用

分库分表的核心目的是将单库单表的海量数据拆分到多个数据库(分库)或多个表(分表)中,以解决单库性能瓶颈(如磁盘 IO、连接数)和单表过大(如千万 / 亿级数据导致查询缓慢)的问题。

但拆分后,传统的「数据库自增 ID」会暴露出致命问题:

  • 每个分表都会独立维护自增 ID,导致不同分表中出现重复 ID,无法作为全局唯一标识。
  • 破坏 ID 的有序性
  • 扩展性差

雪花算法

  • 全局唯一性:确保所有分库分表的 ID 不重复
    在分库分表场景中,我们可以将「工作节点 ID」与分库 / 分表的节点绑定:
    • 将 10 位节点 ID 拆分为「5 位分库 ID + 5 位分表 ID」,支持最多 32 个库(2^5)和每个库 32 个表(2^5),共 1024 个分表节点。
    • 每个分表节点被分配唯一的「节点 ID」,确保其生成的 ID 与其他节点完全不同(即使同一毫秒生成,节点 ID 不同则最终 ID 不同)。
  • 时间有序性:优化分库分表的索引性能
    分库分表后,数据库的索引(尤其是 InnoDB 的聚簇索引)性能严重依赖主键的有序性:
    • 若主键无序(如 UUID),插入时会导致索引页频繁分裂,大幅降低写入性能;
    • 雪花算法的 ID 包含「41 位时间戳」,且同一毫秒内的序列号自增,因此生成的 ID 严格按时间递增(全局有序)。

雪花算法的具体用法

雪花算法生成的 ID(64 位长整型)本身包含了时间戳、节点 ID、序列号等结构化信息,这些特性使其非常适合作为分库分表的「分片键」。
核心思路:雪花 ID 的 64 位结构(从高到低)为:
1 位符号位(0) + 41 位时间戳 + 10 位节点 ID + 12 位序列号

其中,时间戳(有序) 和 整体 ID(全局有序) 是分片的关键依据。常见的分片方式有两种:基于 ID 的哈希分片和基于时间戳的范围分片,分别适用于不同业务场景。

根据雪花 ID 的哈希分片

将雪花 ID 作为完整的分片键,通过哈希算法(如取模)计算数据应存入的库和表。由于雪花 ID 是全局唯一的长整型,其哈希值分布均匀,能避免数据倾斜。
步骤:

  1. 确定分片键:使用雪花 ID 作为分片键
  2. 计算分库索引:直接对雪花 ID 进行取模
1
2
分库索引 = (雪花ID) % 分库总数
(或:hash(雪花ID) % 分库总数,哈希可进一步打散分布)
  1. 计算分表索引:在确定的库内,对分表数量进行取模:
1
2
分表索引 = (雪花ID / 分库总数) % 分表总数
(或:hash(雪花ID) % (分库总数 * 分表总数) → 再映射到库和表)

优点:

  • 数据分布均匀:雪花 ID 本身包含随机因素(节点 ID、序列号),哈希后能均匀分散到各库表,避免热点。
  • 适配单点查询:通过 ID 查询时(如「查 ID=xxx 的订单」),直接计算路由,效率极高。
    缺点:
  • 哈希会打乱 ID 的时间顺序,导致同一时间生成的 ID 可能分布在不同表中,不适合「查最近 1 小时数据」这类范围查询
  • 扩容需迁移大量数据

根据时间戳的范围查询

雪花 ID 的高 41 位是「相对时间戳」(当前时间 - 起始时间),这部分是随时间单调递增的。可提取这部分时间戳作为分片依据,按时间范围划分库表(如按天、按月分片)。
步骤:

  • 提取时间戳:从雪花 ID 中解析出 41 位时间戳
1
2
3
// 假设雪花算法起始时间为startTime,时间戳位移为timestampShift(如22位)
long snowflakeId = 1234567890123456789L;
long timestamp = (snowflakeId >> timestampShift) + startTime; // 得到毫秒级时间戳
  • 按时间范围分库分表:
    • 分库:按较大时间粒度(如按季度)→ 库索引 = 季度编号 % 分库总数
    • 分表:在库内按较小时间粒度(如按天)→ 表索引 = 天数 % 分表总数

优点:

  • 适合范围查询
  • 扩容简单
    缺点:
  • 当写入热点数据时可能导致某个库的压力太大

根据工作节点的分片

雪花 ID 中的 10 位「工作结点」标识了生成该 ID 的节点

  • 分库索引:工作节点 ID%分库总数
  • 分表索引:(工作节点 ID/分库总数)%分表总数

节点 ID 的分配和管理

主流分库分表中间件(如 Sharding-JDBC)支持基于雪花 ID 的分片配置,无需手动计算,本质是对 JDBC 接口的封装 —— 应用通过 Sharding-JDBC 的 DataSource 获取连接,所有分库分表逻辑(路由、解析、聚合)在连接层透明处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
spring:
shardingsphere:
datasource:
names: db0, db1 # 2个分库
db0: # 分库0配置
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/db0
username: root
password: 123456
db1: # 分库1配置(同db0,仅库名不同)
rules:
sharding:
tables:
t_order: # 订单表(分片表)
actual-data-nodes: db${0..1}.t_order_${0..2} # 2库×3表=6个分表
database-strategy: # 分库策略(雪花ID哈希)
standard:
sharding-column: id # 分片键=雪花ID(主键)
sharding-algorithm-name: order_db_hash
table-strategy: # 分表策略(雪花ID哈希)
standard:
sharding-column: id
sharding-algorithm-name: order_table_hash
sharding-algorithms:
order_db_hash: # 分库哈希算法
type: HASH_MOD
props:
sharding-count: 2 # 分库数量
order_table_hash: # 分表哈希算法
type: HASH_MOD
props:
sharding-count: 3 # 每个分库的分表数量

工作流程

  1. 引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- Sharding-JDBC Spring Boot Starter -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.3.2</version> <!-- 建议使用最新稳定版 -->
</dependency>

<!-- 数据库驱动(以MySQL为例) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>

<!-- ORM框架(以MyBatis为例,可选) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
  1. 创建数据库和表
    根据分库分表规划,提前创建实际的数据库(分库)和表(分表)。
  2. 定义实体类
    Sharding-JDBC 对应用代码几乎无侵入,原来操作单表的逻辑无需修改,仅需通过 Sharding-JDBC 提供的数据源访问数据库。
1
2
3
4
5
6
7
8
public class Order {
private Long id; // 雪花算法ID(分片键)
private Long userId; // 用户ID
private BigDecimal amount;
private LocalDateTime createTime;

// getter、setter省略
}
  1. 定义 DAO 层(Mybatis)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Mapper
public interface OrderMapper {
// 插入订单(ID由雪花算法生成)
@Insert("INSERT INTO t_order (id, user_id, amount, create_time) VALUES (#{id}, #{userId}, #{amount}, #{createTime})")
int insert(Order order);

// 根据ID查询(自动路由到对应分库分表)
@Select("SELECT * FROM t_order WHERE id = #{id}")
Order selectById(Long id);

// 批量查询(若包含ID,自动路由;否则扫描所有分表)
@Select("SELECT * FROM t_order WHERE user_id = #{userId}")
List<Order> selectByUserId(Long userId);
}
  1. 插入数据
    此时,插入数据时可省略 id 字段,Sharding-JDBC 会自动生成雪花 ID:
1
2
3
4
5
6
// 插入时无需设置id,由Sharding-JDBC自动生成
Order order = new Order();
order.setUserId(1001L);
order.setAmount(new BigDecimal("99.99"));
order.setCreateTime(LocalDateTime.now());
orderMapper.insert(order);
  1. 业务层调用
    与单表逻辑一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;

// 创建订单
public Long createOrder(Order order) {
orderMapper.insert(order);
return order.getId(); // 返回自动生成的雪花ID
}

// 查询订单详情(基于ID,自动路由)
public Order getOrder(Long id) {
return orderMapper.selectById(id);
}

// 按用户ID查询(非分片键,需全局索引优化,否则扫描全部分表)
public List<Order> getOrdersByUserId(Long userId) {
return orderMapper.selectByUserId(userId);
}
}
1
2
3
select * from t_order where id = 1234567890123456789; -- 雪花ID
-- 范围查询 --
select * from t_order where id between A and B:
  1. 中间件(如 Sharding-JDBC)提取分片键 id=1234567890123456789;
  2. 按预设哈希规则计算路由:如 db1.t2;
  3. 直接向 db1.t2 发送查询,返回结果;
  4. 应用层无需感知分库分表,代码与单表查询一致。