分布式ID生成方案的深度解析与Java实现

在分布式系统中,生成全局唯一的ID是一项核心需求,广泛应用于订单编号、用户信息、日志追踪等场景。分布式ID不仅需要保证全局唯一性,还要满足高性能、高可用性以及一定的可读性要求。本文将深入探讨分布式ID的概念、设计要点、常见生成方案,并通过Java代码实现几种典型方案,旨在帮助开发者理解分布式ID的技术本质,并提供实践参考。


一、分布式ID的背景与挑战

分布式系统由多个节点组成,节点间通常通过网络通信,缺乏全局时钟或统一协调机制。传统的单机环境下,自增ID(如数据库主键)可以轻松满足唯一性需求,但在分布式环境中,节点独立运行,简单地依赖数据库自增ID可能导致冲突或性能瓶颈。因此,分布式ID需要解决以下核心问题:

  1. 全局唯一性:在所有节点生成的ID必须全局唯一,不能出现重复。
  2. 高性能:生成ID的速度要快,通常要求毫秒级甚至微秒级响应。
  3. 高可用性:ID生成服务需保证24/7可用,单点故障不能影响整体功能。
  4. 有序性:某些场景(如日志排序)要求ID具有时间单调递增或趋势递增的特性。
  5. 可读性:ID可能需要包含业务信息(如时间、地域),便于调试或分析。
  6. 扩展性:系统规模扩大时,ID生成方案需支持水平扩展。

这些要求使得分布式ID生成成为分布式系统设计中的一个复杂问题。以下我们将分析几种主流方案,探讨其优缺点,并提供Java实现。


二、分布式ID的常见生成方案

分布式ID生成方案可以分为以下几类,每类方案在不同场景下有其适用性:

1. 数据库自增ID

利用关系型数据库(如MySQL)的自增主键生成ID,简单易用,但在分布式场景下性能受限。

  • 优点:实现简单,ID单调递增,易于理解。
  • 缺点:数据库写入成为性能瓶颈,高并发下可能导致锁竞争;扩展性差,依赖数据库可用性。
  • 适用场景:低并发、对性能要求不高的业务。

2. UUID

UUID(Universally Unique Identifier)是基于随机数或时间戳生成的128位标识符。

  • 优点:完全去中心化,生成无需协调,冲突概率极低。
  • 缺点:长度过长(36字符),存储和传输成本高;无序性导致数据库索引性能下降。
  • 适用场景:对唯一性要求高但对性能和可读性要求低的场景。

3. 基于时间戳的Snowflake算法

Snowflake算法由Twitter提出,是一种基于时间戳的分布式ID生成方案,ID为64位整数,结构通常包括:

  • 时间戳:表示ID生成的时间,占41位(支持约69年)。

  • 机器ID:标识生成节点,占10位(支持1024个节点)。

  • 序列号:同一毫秒内的计数器,占12位(每毫秒支持4096个ID)。

  • 符号位:占1位,通常为0。

  • 优点:高性能,ID趋势递增,支持高并发,结构清晰。

  • 缺点:依赖系统时钟,时间回拨可能导致ID冲突;机器ID需手动分配。

  • 适用场景:高并发、需要趋势递增ID的业务,如订单系统。

4. 数据库分段(Leaf-Segment)

由美团提出的Leaf方案,通过数据库预分配ID段(如1000个ID),节点从内存中获取ID,耗尽后再从数据库申请新段。

  • 优点:简单可靠,支持批量获取,减少数据库压力。
  • 缺点:数据库仍是潜在瓶颈,需处理段分配的并发问题。
  • 适用场景:对性能要求适中、希望简单实现的场景。

5. 分布式协调服务(如ZooKeeper)

使用ZooKeeper等分布式协调服务生成递增ID,基于其顺序节点特性。

  • 优点:强一致性,ID严格递增。
  • 缺点:性能较低,依赖外部服务,增加了系统复杂性。
  • 适用场景:对一致性要求极高的场景,如金融系统。

6. Redis生成ID

利用Redis的原子递增操作(如INCR命令)生成ID。

  • 优点:高性能,简单易用。
  • 缺点:依赖Redis可用性,持久化可能导致ID丢失;ID无业务含义。
  • 适用场景:高并发、对可读性要求低的场景。

三、分布式ID的设计要点

在选择或设计分布式ID生成方案时,需考虑以下关键因素:

  1. 时钟依赖:基于时间戳的方案(如Snowflake)需处理时钟回拨问题,可通过拒绝生成或等待解决。
  2. ID长度:ID长度影响存储效率,64位整数是常见选择,兼容大多数数据库和系统。
  3. 分区策略:机器ID或业务ID的分配需避免冲突,可通过配置中心或数据库管理。
  4. 容错性:生成服务需支持故障转移,主备切换或多节点负载均衡。
  5. 可扩展性:方案需适应节点增加,动态分配ID空间。
  6. 业务定制:某些场景要求ID嵌入业务信息,如区域、业务类型等。

四、Java实现:Snowflake算法与Leaf-Segment方案

下面我们通过Java代码实现两种典型的分布式ID生成方案:Snowflake算法和Leaf-Segment方案,并附上详细注释和使用示例。

1. Snowflake算法实现

Snowflake算法因其高性能和趋势递增特性,成为许多分布式系统的首选。以下是一个线程安全的Java实现,支持时间回拨处理。

public class SnowflakeIdGenerator {// 起始时间戳(2023-01-01 00:00:00)private static final long START_TIMESTAMP = 1672502400000L;// 各部分位数private static final long WORKER_ID_BITS = 10L; // 机器ID占10位private static final long SEQUENCE_BITS = 12L;  // 序列号占12位// 最大值private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); // 1023private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);   // 4095// 位移量private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;// 内部状态private long workerId;private long sequence = 0L;private long lastTimestamp = -1L;public SnowflakeIdGenerator(long workerId) {if (workerId > MAX_WORKER_ID || workerId < 0) {throw new IllegalArgumentException("Worker ID must be between 0 and " + MAX_WORKER_ID);}this.workerId = workerId;}public synchronized long nextId() {long timestamp = System.currentTimeMillis();// 处理时间回拨if (timestamp < lastTimestamp) {throw new RuntimeException("Clock moved backwards. Refusing to generate ID.");}// 同一毫秒内,增加序列号if (lastTimestamp == timestamp) {sequence = (sequence + 1) & MAX_SEQUENCE;if (sequence == 0) {// 序列号溢出,等待下一毫秒timestamp = waitNextMillis(lastTimestamp);}} else {sequence = 0L; // 新毫秒,重置序列号}lastTimestamp = timestamp;// 组装IDreturn ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)| (workerId << WORKER_ID_SHIFT)| sequence;}private long waitNextMillis(long lastTimestamp) {long timestamp = System.currentTimeMillis();while (timestamp <= lastTimestamp) {timestamp = System.currentTimeMillis();}return timestamp;}public static void main(String[] args) {SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1);for (int i = 0; i < 10; i++) {System.out.println(idGenerator.nextId());}}
}

代码说明

  • 结构:ID由41位时间戳(支持约69年)、10位机器ID(支持1024个节点)、12位序列号(每毫秒4096个ID)组成。
  • 时间回拨:通过抛出异常拒绝生成,实际生产中可改为等待或使用缓存时间。
  • 线程安全:使用 synchronized 确保并发安全,适用于中等并发场景。
  • 使用示例:运行 main 方法将生成10个唯一ID,输出类似 1234567890123 的64位整数。

优化建议

  • 高并发:可引入线程池或异步生成,提升吞吐量。
  • 机器ID分配:通过ZooKeeper或数据库动态分配workerId。
  • 时间回拨改进:维护一个时间缓存,或在回拨时借用序列号空间。

2. Leaf-Segment方案实现

Leaf-Segment方案通过数据库预分配ID段,节点从内存获取ID,适合简单可靠的场景。以下是Java实现,假设使用MySQL存储ID段。

首先,创建数据库表:

CREATE TABLE id_segment (biz_tag VARCHAR(50) PRIMARY KEY COMMENT '业务标签',max_id BIGINT NOT NULL DEFAULT 0 COMMENT '当前最大ID',step INT NOT NULL DEFAULT 1000 COMMENT '步长',update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
INSERT INTO id_segment (biz_tag, max_id, step) VALUES ('order', 0, 1000);

Java实现:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;public class LeafSegmentIdGenerator {private String bizTag;private String jdbcUrl = "jdbc:mysql://localhost:3306/test?useSSL=false";private String username = "root";private String password = "password";private volatile long currentId;private volatile long maxId;private final int step;public LeafSegmentIdGenerator(String bizTag) {this.bizTag = bizTag;this.step = 1000; // 默认步长loadSegment(); // 初始化ID段}public synchronized long nextId() {if (currentId >= maxId) {loadSegment(); // ID段耗尽,重新加载}return currentId++;}private void loadSegment() {try (Connection conn = DriverManager.getConnection(jdbcUrl, username, password)) {conn.setAutoCommit(false);// 获取当前最大ID并更新String updateSql = "UPDATE id_segment SET max_id = max_id + step WHERE biz_tag = ?";PreparedStatement updateStmt = conn.prepareStatement(updateSql);updateStmt.setString(1, bizTag);int rows = updateStmt.executeUpdate();if (rows == 0) {throw new RuntimeException("Failed to update ID segment for " + bizTag);}// 查询新ID段String selectSql = "SELECT max_id FROM id_segment WHERE biz_tag = ?";PreparedStatement selectStmt = conn.prepareStatement(selectSql);selectStmt.setString(1, bizTag);ResultSet rs = selectStmt.executeQuery();if (rs.next()) {maxId = rs.getLong("max_id");currentId = maxId - step + 1;} else {throw new RuntimeException("No segment found for " + bizTag);}conn.commit();} catch (Exception e) {throw new RuntimeException("Failed to load ID segment", e);}}public static void main(String[] args) {LeafSegmentIdGenerator idGenerator = new LeafSegmentIdGenerator("order");for (int i = 0; i < 10; i++) {System.out.println(idGenerator.nextId());}}
}

代码说明

  • 逻辑:节点从数据库获取一个ID段(如1001-2000),在内存中递增生成ID,耗尽后再申请新段。
  • 数据库交互:使用乐观锁(UPDATE直接修改)确保并发安全,事务保证数据一致性。
  • 业务隔离:通过 biz_tag 支持多业务隔离,如“order”和“user”可独立分配ID。
  • 使用示例:运行 main 方法将生成连续的ID,如 1001, 1002, ...

优化建议

  • 批量获取:增加步长(如10000),减少数据库访问。
  • 双缓冲:异步加载下一段ID,避免生成延迟。
  • 高可用:引入主备数据库或缓存(如Redis)提高可靠性。

五、各方案对比与选择

以下是对上述方案的对比总结:

方案唯一性性能有序性可读性扩展性依赖性适用场景
数据库自增ID数据库低并发简单业务
UUID对性能敏感、无序性可接受
Snowflake趋势时钟高并发、需要趋势递增
Leaf-Segment数据库中等并发、简单实现
ZooKeeperZooKeeper强一致性需求
RedisRedis高并发、无可读性要求

选择建议

  • 高并发场景:Snowflake或Redis,性能优异,适合订单、日志等系统。
  • 简单实现:Leaf-Segment,易于部署,适合中小规模业务。
  • 强一致性:ZooKeeper,适用于金融等对ID顺序敏感的场景。
  • 无序可接受:UUID,适合快速开发或临时场景。

六、分布式ID的未来趋势

随着分布式系统规模的扩大,ID生成方案也在不断演进。以下是一些值得关注的趋势:

  1. 云原生集成:云服务(如AWS、阿里云)提供托管ID生成服务,降低开发成本。
  2. 多租户支持:ID方案需支持多租户隔离,嵌入租户标识。
  3. AI优化:通过机器学习预测ID需求,优化分配策略。
  4. 去中心化趋势:基于区块链或P2P网络生成ID,减少对中心化服务的依赖。

七、实践中的注意事项

  1. 测试覆盖:对ID生成方案进行并发测试,确保唯一性和性能。
  2. 监控告警:监控ID生成速率、时间回拨等异常情况,及时干预。
  3. 文档化:记录ID结构(如Snowflake的位分配),便于维护和调试。
  4. 回滚策略:为ID生成服务设计降级方案,如切换到备用算法。
  5. 合规性:在涉及用户数据的场景中,确保ID不泄露敏感信息。

八、总结

分布式ID生成是分布式系统中的核心技术之一,其设计需要在唯一性、性能、可用性和可读性之间找到平衡。本文详细分析了数据库自增ID、UUID、Snowflake、Leaf-Segment、ZooKeeper和Redis等方案的优缺点,并通过Java代码实现了Snowflake和Leaf-Segment两种主流方案。实践表明,Snowflake因其高性能和趋势递增特性成为许多高并发场景的首选,而Leaf-Segment则以简单可靠著称。开发者应根据业务需求选择合适的方案,并结合监控和优化确保系统稳定运行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/901035.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

记 etcd 无法在docker-compose.yml启动后无法映射数据库目录的问题

1、将etcd 单独提取 Dockerfile&#xff0c;指定配置文件和数据目录 #镜像 FROM bitnami/etcd:3.5.11 #名称 ENV name"etcd" #重启 ENV restart"always" #运行无权限 ENV ALLOW_NONE_AUTHENTICATION"yes" #端口 EXPOSE 2379 2380 #管理员权限才…

怎样才不算干扰球·棒球1号位

在棒球运动中&#xff0c;"干扰球"&#xff08;Interference&#xff09;是指球员或场外人员非法影响了比赛的正常进行。以下情况通常 不构成干扰&#xff0c;属于合法行为或无需判罚&#xff1a; 1. 击跑员&#xff08;Batter-Runner&#xff09;合法跑垒 跑垒限制…

PyTorch实现多输入输出通道的卷积操作

本文通过代码示例详细讲解如何在PyTorch中实现多输入通道和多输出通道的卷积运算&#xff0c;并对比传统卷积与1x1卷积的实现差异。 1. 多输入通道互相关运算 当输入包含多个通道时&#xff0c;卷积核需要对每个通道分别进行互相关运算&#xff0c;最后将结果相加。以下是实现…

深入解析 MySQL 中的日期时间函数:DATE_FORMAT 与时间查询优化、DATE_ADD、CONCAT

深入解析 MySQL 中的日期时间函数&#xff1a;DATE_FORMAT 与时间查询优化 在数据库管理和应用开发中&#xff0c;日期和时间的处理是不可或缺的一部分。MySQL 提供了多种日期和时间函数来满足不同的需求&#xff0c;其中DATE_FORMAT函数以其强大的日期格式化能力&#xff0c;…

SSH配置优化:提升本地内网Linux服务器远程连接速度与稳定性

文章目录 引言一. 理解SSH连接过程与影响因素二. 服务器端SSH配置优化三. 客户端SSH配置优化四. 高级技巧五. 内网穿透突破公网IP限制总结 引言 SSH (Secure Shell) 是一种网络协议&#xff0c;用于加密的网络服务&#xff0c;常用于远程登录和管理Linux服务器。对于本地内网的…

BERT - MLM 和 NSP

本节代码将实现BERT模型的两个主要预训练任务&#xff1a;掩码语言模型&#xff08;Masked Language Model, MLM&#xff09; 和 下一句预测&#xff08;Next Sentence Prediction, NSP&#xff09;。 1. create_nsp_dataset 函数 这个函数用于生成NSP任务的数据集。 def cr…

“实时滚动”插件:一个简单的基于vue.js的无缝滚动

1、参考连接&#xff1a; 安装 | vue-seamless-scroll 2、使用步骤&#xff1a; 第一步&#xff1a;安装 yarn add vue-seamless-scroll 第二步&#xff1a;引入 import vueSeamlessScroll from vue-seamless-scroll/src 第三步&#xff1a;注册 components: { vueSeamless…

【蓝桥杯】赛前练习

1. 排序 import os import sysn=int(input()) data=list(map(int,input().split(" "))) data.sort() for d in data:print(d,end=" ") print() for d in data[::-1]:print(d,end=" ")2. 走迷宫BFS import os import sys from collections import…

pyTorch-迁移学习-学习率衰减-四种天气图片多分类问题

目录 1.导包 2.加载数据、拼接训练、测试数据的文件夹路径 3.数据预处理 3.1 transforms.Compose数据转化 3.2分类存储的图片数据创建dataloader torchvision.datasets.ImageFolder torch.utils.data.DataLoader 4.加载预训练好的模型(迁移学习) 4.1固定、修改预训练…

第十四届蓝桥杯大赛软件赛国赛Python大学B组题解

文章目录 弹珠堆放划分偶串交易账本背包问题翻转最大阶梯最长回文前后缀贸易航线困局 弹珠堆放 递推式 a i a i − 1 i a_ia_{i-1}i ai​ai−1​i&#xff0c; n 20230610 n20230610 n20230610非常小&#xff0c;直接模拟 答案等于 494 494 494 划分 因为总和为 1 e 6 1e6…

Python 和 JavaScript两种语言的相似部分-由DeepSeek产生

Python 和 JavaScript 作为两种流行的编程语言&#xff0c;虽然在设计目标和应用场景上有差异&#xff08;Python 偏向后端和脚本&#xff0c;JavaScript 偏向前端和动态交互&#xff09;&#xff0c;但它们的语法存在许多相似之处。以下是两者在语法上的主要共同点及对比&…

改善 Maven 的依赖性

大家好&#xff0c;这里是架构资源栈&#xff01;点击上方关注&#xff0c;添加“星标”&#xff0c;一起学习大厂前沿架构&#xff01; 建议使用mvn dependency:analyze命令来摆脱已声明但未使用的依赖项&#xff1a; 还有另一个用例&#xff0c; mvn dependency:analyze 它可…

【SQL】子查询详解(附例题)

子查询 子查询的表示形式为&#xff1a;(SELECT 语句)&#xff0c;它是IN、EXISTS等运算符的运算数&#xff0c;它也出现于FROM子句和VALUES子句。包含子查询的查询叫做嵌套查询。嵌套查询分为相关嵌套查询和不想关嵌套查询 WHERE子句中的子查询 比较运算符 子查询的结果是…

Stable Diffusion 扩展知识实操整合

本文的例子都是基于秋叶整合包打开的webui实现的 一、ADetailer——改善人脸扭曲、恶心 After detailer插件可以自动检测生成图片的人脸&#xff0c;针对人脸自动上蒙版&#xff0c;自动进行重绘&#xff0c;整个流程一气呵成&#xff0c;因此可以避免许多重复的操作。除此之…

freertos内存管理简要概述

概述 内存管理的重要性 在嵌入式系统中&#xff0c;内存资源通常是有限的。合理的内存管理可以确保系统高效、稳定地运行&#xff0c;避免因内存泄漏、碎片化等问题导致系统崩溃或性能下降。FreeRTOS 的内存管理机制有助于开发者灵活地分配和释放内存&#xff0c;提高内存利用…

按规则批量修改文件扩展名、删除扩展名或添加扩展名

文件的扩展名是多种多样的&#xff0c;有些不同文件的扩展名之间相互是可以直接转换的。我们工作当中最常见的就是 doc 与 docx、xls 与 xlsx、jpg 与 jpeg、html 与 htm 等等&#xff0c;这些格式在大部分场景下都是可以相互转换 能直接兼容的。我们今天要介绍的就是如何按照一…

热门面试题第15天|最大二叉树 合并二叉树 验证二叉搜索树 二叉搜索树中的搜索

654.最大二叉树 力扣题目地址(opens new window) 给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下&#xff1a; 二叉树的根是数组中的最大元素。左子树是通过数组中最大值左边部分构造出的最大二叉树。右子树是通过数组中最大值右边部分构造出的最大…

MySQL学习笔记7【InnoDB】

Innodb 1. 架构 1.1 内存部分 buffer pool 缓冲池是主存中的第一个区域&#xff0c;里面可以缓存磁盘上经常操作的真实数据&#xff0c;在执行增删查改操作时&#xff0c;先操作缓冲池中的数据&#xff0c;然后以一定频率刷新到磁盘&#xff0c;这样操作明显提升了速度。 …

RNN、LSTM、GRU汇总

RNN、LSTM、GRU汇总 0、论文汇总1.RNN论文2、LSTM论文3、GRU4、其他汇总 1、发展史2、配置和架构1.配置2.架构 3、基本结构1.神经元2.RNN1. **RNN和前馈网络区别&#xff1a;**2. 计算公式&#xff1a;3. **梯度消失:**4. **RNN类型**:&#xff08;查看发展史&#xff09;5. **…

django数据迁移操作受阻

错误信息&#xff1a; django.db.utils.OperationalError: (1227, Access denied; you need (at least one of) the SYSTEM_VARIABLES_ADMIN or SESSION_VARIABLES_ADMIN privilege(s) for this operation)根据错误信息分析&#xff0c;该问题是由于MySQL用户 缺乏SYSTEM_VARI…