全局ID生成器
文章目录
一、全局ID生成器的定义
定义
- 全局ID生成器是一种在分布式系统中生成唯一标识符(ID)的机制,确保生成的ID在整个系统范围内(可能跨多个服务、数据库或数据中心)具有唯一性。它是解决分布式环境下数据唯一性、数据关联和事务协调等问题的核心技术之一。
核心作用
- 唯一性:确保每个生成的ID在全局范围内不重复。
- 可识别性:通过ID快速定位资源或关联业务(如订单、用户、消息等),不能被看出太明显的规律。
- 扩展性:支持系统规模增长(如分库分表、多节点部署)时仍能高效生成ID,高效应对海量数据。
二、全局ID生成器需满足的特征
全局ID生成器,是分布式系统环境下生成全局唯一ID的工具,一般需要满足下列特征:
1. 唯一性(Uniqueness)
- 核心要求:生成的ID在全局范围内绝对唯一。
- 意义:避免数据冲突(如主键重复、消息覆盖)。
- 实现方式:
- 算法设计(如Snowflake通过时间戳+机器ID+序列号保证唯一)。
- 中心化协调(如数据库自增ID依赖数据库的唯一约束)。
2. 高性能(High Performance)
- 要求:生成ID的速度快,延迟低,支持高并发场景。
- 意义:避免成为系统瓶颈(如秒杀场景下每秒生成数十万ID)。
- 实现方式:
- 本地生成(如Snowflake在内存中生成,无需网络请求)。
- 批量预分配(如数据库号段模式一次性获取多个ID)。
3. 可扩展性(Scalability)
- 要求:支持系统横向扩展(如新增节点)时无需重构ID生成逻辑。
- 意义:适应业务增长,避免单点瓶颈。
- 实现方式:
- 分布式算法(如Snowflake允许动态增加机器ID)。
- 去中心化设计(如UUID无需中心节点)。
4. 有序性(Orderliness)
- 要求:生成的ID按时间递增或可排序。
- 意义:便于数据库索引优化、分页查询和业务排序(如按时间排序订单)。
- 实现方式:
- 时间戳高位(如Snowflake将时间戳放在ID的高位)。
- 数据库自增ID天然有序,但分布式下需分片步长策略。
5. 可靠性(Reliability)
- 要求:生成过程容错,避免单点故障。
- 意义:保障系统高可用(如机器宕机不影响ID生成)。
- 实现方式:
- 去中心化算法(如Snowflake无单点依赖)。
- 冗余设计(如Leaf通过ZooKeeper管理机器ID,支持故障转移)。
6. 安全性(Security)
- 要求:防止ID被猜测或遍历(如避免暴露业务量或用户隐私)。
- 意义:防止恶意攻击(如通过ID遍历批量查询数据)。
- 实现方式:
- 加入随机性(如UUIDv4的随机部分)。
- 加密或哈希处理(如美团的Leaf-Segment加密号段)。
7. 兼容性(Compatibility)
- 要求:ID格式适配现有技术栈(如数据库类型、网络传输)。
- 意义:降低集成成本(如避免使用超长ID导致存储浪费)。
- 实现方式:
- 数值型ID(如Snowflake的64位Long类型,兼容MySQL BIGINT)。
- 字符串ID(如UUID的128位字符串,适配NoSQL数据库)。
三、全局唯一ID生成策略:
1. UUID
- 原理:生成128位随机字符串(如 550e8400-e29b-41d4-a716-446655440000)。
- 优点:简单、无需中心化协调。
- 缺点:无序、长度长、存储和索引效率低。
- 适用场景:非高频查询场景,如日志跟踪、临时标识。
- 优化:使用UUIDv4(随机生成)避免隐私问题。
2. 数据库自增ID
- 原理:利用数据库自增主键生成唯一ID。
- 优点:简单、天然有序。
- 缺点:单点瓶颈、横向扩展困难。
- 优化:
- 分库分表:为每个分片设置不同步长(如库1步长=2,起始值=1;库2步长=2,起始值=2)。
- 批量获取:一次性申请多个ID(如 INSERT … SELECT MAX(id)+N)。
3. Snowflake算法(Twitter开源)
- ID结构(64位):
| 符号位(1) | 时间戳(41) | 机器ID(10) | 序列号(12) |
- 优点:有序、高性能、去中心化。
- 缺点:依赖系统时钟(时钟回拨需处理)。
- 实现:
public class Snowflake { private final long machineId; // 机器ID(0~1023) private long lastTimestamp = -1L; private long sequence = 0L; public synchronized long nextId() { long timestamp = System.currentTimeMillis(); if (timestamp < lastTimestamp) throw new RuntimeException("时钟回拨"); if (timestamp == lastTimestamp) { sequence = (sequence + 1) & 0xFFF; // 序列号自增,溢出则等待下一毫秒 if (sequence == 0) timestamp = waitNextMillis(); } else { sequence = 0; } lastTimestamp = timestamp; return ((timestamp - TWEPOCH) << 22) | (machineId << 12) | sequence; } }
4. Redis自增
原理:利用Redis的原子操作 INCR 或 INCRBY 生成自增ID。使用Java的Long类型存储,为了增加ID的安全性,我们可以不直接只用Redis自增的数值,而是拼接一些其他信息:
ID的组成部分(Java Long类型存储,占用8个字节,也就是64个比特位): 0 - 0000000 00000000 00000000 00000000 - 00000000 00000000 00000000 00000000 ↑ ↑ ↑ 符号位 |<------- 时间戳(31 bit)-------->| |<-------- 序列号(32 bit)-------->|
- 符号位: 1 bit,永远为0(表示正数)
- 时间戳:31 bit,以秒为单位,以2000年1月1日00时作为参照系,用
当前时间 - 参照时间 = 时间戳
,2^31 秒 约等于69年。 - 序列号:31 bit,Redis自增的值,该方式理论上支持每秒产生 2^32 个不同ID。
优点:高性能、简单。
缺点:依赖Redis可用性,需处理持久化问题。
优化:集群模式 + 多Key分片(如 order_id:shard_1)。
实现:
@Component public class RedisIdGenerator { /** * 开始时间戳 2000年1月1日零时零分零秒 */ private static final long BEGIN_TIMESTAMP = 946684800L; /** * 序列号位数 */ private static final int COUNT = 32; private StringRedisTemplate stringRedisTemplate; @Autowired public RedisIdGenerator(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } public long nextId(String keyPrefix) { // 1.生成时间戳 LocalDateTime now = LocalDateTime.now(); long nowSecond = now.toEpochSecond(ZoneOffset.UTC); long timestamp = nowSecond - BEGIN_TIMESTAMP; // 2.生成序列号 String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd")); // 使用冒号分割,方便统计 Long increment = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date); // 加入日期,防止超出2^32上限(Redis自增上限为2^64) long incrementUnbox = 0L; if (increment != null) { incrementUnbox = increment; } // 3.拼接并返回 return timestamp << COUNT | incrementUnbox; // 左移 32位,拼接时间戳和序列号 } public static void main(String[] args) { // 2000年1月1日零时零分零秒 的秒数 LocalDateTime time = LocalDateTime.of(2000, 1, 1, 0, 0, 0); long second = time.toEpochSecond(ZoneOffset.UTC); System.out.println("second = " + second); // second = 946684800 } }
5. 数据库号段模式
- 原理:从数据库批量获取号段(如一次分配1000个ID),缓存在本地使用。
- 表设计:
CREATE TABLE id_segment ( biz_tag VARCHAR(32) PRIMARY KEY, -- 业务标识 max_id BIGINT NOT NULL, -- 当前最大ID step INT NOT NULL -- 每次步长 );
- 优点:减少数据库压力、可扩展性强。
- 缺点:需维护号段表,处理并发冲突。
6. Leaf(美团开源)
- 混合模式:
- 号段模式:类似数据库号段,依赖数据库。
- Snowflake模式:依赖ZooKeeper分配机器ID。
- 优点:高可用、灵活切换模式。
- 实现:通过ZooKeeper管理机器ID,支持号段预分配。
四、策略对比
策略 | 唯一性 | 有序性 | 性能 | 可靠性 | 典型场景 |
---|---|---|---|---|---|
UUID | ✅ | ❌ | 高 | ✅ | 日志跟踪、临时标识 |
数据库自增ID | ✅ | ✅ | 低 | ❌(单点) | 中小规模分库分表 |
Snowflake | ✅ | ✅ | 极高 | ✅(去中心化) | 高并发订单、消息队列 |
Redis INCR | ✅ | ✅ | 高 | ❌(依赖Redis) | 短期唯一ID(如会话ID) |
Leaf(美团) | ✅ | ✅ | 高 | ✅(混合模式) | 大规模分布式系统 |
五、选型建议
场景 | 推荐方案 | 关键考虑 |
---|---|---|
高性能、有序ID | Snowflake | 处理时钟回拨(NTP同步、异常等待) |
简单、无序 | UUID v4 | 适合临时标识 |
依赖Redis | Redis INCR | 需保障Redis高可用 |
数据库友好、可控 | 数据库号段模式 | 适合中小规模分布式系统 |
企业级复杂场景 | Leaf | 结合号段和Snowflake优势 |
实际选型建议
- 高并发有序ID:优先选择Snowflake或其变种(如美团的Leaf-Segment)。
- 简单临时标识:使用UUID v4(如用户临时Token)。
- 数据库友好场景:数据库号段模式(如分库分表的预分配ID)。
- 强一致性需求:结合Redis或ZooKeeper的分布式锁生成ID。
常见问题处理
- 时钟回拨:
- 方案:记录上次时间戳,发现回拨时抛出异常或等待时钟追上。
- 优化:使用NTP同步服务器时间,或扩展Snowflake增加时间戳位数。
- 机器ID分配:
- 方案:ZooKeeper/配置中心动态分配,或按数据中心+机器ID编码。
根据业务规模、一致性要求及运维成本选择合适的策略,必要时可组合使用多种方案。