概述
雪花算法(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 { private final long startTime = 1577808000000L;
private final long datacenterIdBits = 5L; private final long workerIdBits = 5L; private final long maxDatacenterId = ~(-1L << datacenterIdBits); private final long maxWorkerId = ~(-1L << workerIdBits);
private final long sequenceBits = 12L; private final long sequenceMask = ~(-1L << sequenceBits);
private final long workerIdShift = sequenceBits; private final long datacenterIdShift = sequenceBits + workerIdBits; private final long timestampShift = sequenceBits + workerIdBits + datacenterIdBits;
private final long datacenterId; private final long workerId; private long sequence = 0L; private long lastTimestamp = -1L;
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; }
public synchronized long nextId() { long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) { throw new RuntimeException("时钟回拨,拒绝生成ID"); }
if (timestamp == lastTimestamp) { sequence = (sequence + 1) & sequenceMask; if (sequence == 0) { timestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L; }
lastTimestamp = timestamp;
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 是全局唯一的长整型,其哈希值分布均匀,能避免数据倾斜。
步骤:
- 确定分片键:使用雪花 ID 作为分片键
- 计算分库索引:直接对雪花 ID 进行取模
1 2
| 分库索引 = (雪花ID) % 分库总数 (或:hash(雪花ID) % 分库总数,哈希可进一步打散分布)
|
- 计算分表索引:在确定的库内,对分表数量进行取模:
1 2
| 分表索引 = (雪花ID / 分库总数) % 分表总数 (或:hash(雪花ID) % (分库总数 * 分表总数) → 再映射到库和表)
|
优点:
- 数据分布均匀:雪花 ID 本身包含随机因素(节点 ID、序列号),哈希后能均匀分散到各库表,避免热点。
- 适配单点查询:通过 ID 查询时(如「查 ID=xxx 的订单」),直接计算路由,效率极高。
缺点:
- 哈希会打乱 ID 的时间顺序,导致同一时间生成的 ID 可能分布在不同表中,不适合「查最近 1 小时数据」这类范围查询
- 扩容需迁移大量数据
根据时间戳的范围查询
雪花 ID 的高 41 位是「相对时间戳」(当前时间 - 起始时间),这部分是随时间单调递增的。可提取这部分时间戳作为分片依据,按时间范围划分库表(如按天、按月分片)。
步骤:
- 提取时间戳:从雪花 ID 中解析出 41 位时间戳
1 2 3
| 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 db0: 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: rules: sharding: tables: t_order: actual-data-nodes: db${0..1}.t_order_${0..2} database-strategy: standard: sharding-column: id sharding-algorithm-name: order_db_hash table-strategy: 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.3.2</version> </dependency>
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency>
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.0</version> </dependency>
|
- 创建数据库和表
根据分库分表规划,提前创建实际的数据库(分库)和表(分表)。
- 定义实体类
Sharding-JDBC 对应用代码几乎无侵入,原来操作单表的逻辑无需修改,仅需通过 Sharding-JDBC 提供的数据源访问数据库。
1 2 3 4 5 6 7 8
| public class Order { private Long id; private Long userId; private BigDecimal amount; private LocalDateTime createTime;
}
|
- 定义 DAO 层(Mybatis)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Mapper public interface OrderMapper { @Insert("INSERT INTO t_order (id, user_id, amount, create_time) VALUES (#{id}, #{userId}, #{amount}, #{createTime})") int insert(Order order);
@Select("SELECT * FROM t_order WHERE id = #{id}") Order selectById(Long id);
@Select("SELECT * FROM t_order WHERE user_id = #{userId}") List<Order> selectByUserId(Long userId); }
|
- 插入数据
此时,插入数据时可省略 id 字段,Sharding-JDBC 会自动生成雪花 ID:
1 2 3 4 5 6
| Order order = new Order(); order.setUserId(1001L); order.setAmount(new BigDecimal("99.99")); order.setCreateTime(LocalDateTime.now()); orderMapper.insert(order);
|
- 业务层调用
与单表逻辑一致
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(); }
public Order getOrder(Long id) { return orderMapper.selectById(id); }
public List<Order> getOrdersByUserId(Long userId) { return orderMapper.selectByUserId(userId); } }
|
1 2 3
| select * from t_order where id = 1234567890123456789;
select * from t_order where id between A and B:
|
- 中间件(如 Sharding-JDBC)提取分片键 id=1234567890123456789;
- 按预设哈希规则计算路由:如 db1.t2;
- 直接向 db1.t2 发送查询,返回结果;
- 应用层无需感知分库分表,代码与单表查询一致。