全局唯一ID生成器
全局唯一 ID 生成器 (GID
)
在构建分布式系统时,生成 全局唯一、趋势递增 且 高性能 的 ID 是一个核心需求。为了满足这一需求,我们引入了一个基于 Twitter Snowflake 算法的全局唯一 ID 生成器,并通过一个极其简洁的异步函数 GID()
来调用。
核心目标
你只需要调用 let id: i64 = GID().await;
就可以获得一个在整个系统中绝对唯一的 i64
类型的 ID。
什么是雪花 ID (Snowflake ID)?
雪花 ID 是一个 64 位的整数 (i64
),它的二进制结构被精心划分为四个部分,每一部分都有特定的含义:
(图片引用自 Twitter 原始设计)
部分 (Part) | 位数 (Bits) | 描述 (Description) |
---|---|---|
1. 符号位 | 1 bit | 固定为 0 ,确保生成的 ID 总是正数。 |
2. 时间戳 | 41 bits | 从一个自定义的起始时间点(Epoch)到现在的毫秒数。可以使用约 69 年。 |
3. 工作节点 ID | 10 bits | 标识不同的机器或进程。我们的实现将其分为 machine_id 和 node_id ,以支持更灵活的部署拓扑。 |
4. 序列号 | 12 bits | 一个在同一毫秒内自增的计数器。允许每个节点在同一毫秒内生成 4096 (2^12 ) 个不同的 ID。 |
这种结构的优势
- 全局唯一: 结合了时间戳和独一无二的工作节点 ID,保证了在分布式环境下生成的 ID 不会重复。
- 趋势递增: ID 的主要部分是时间戳,因此生成的 ID 总体上是随着时间单调递增的。这对于数据库索引(如 B-Tree)非常友好。
- 高性能: ID 的生成完全在内存中完成,不依赖任何外部服务(如 Redis 或数据库),性能极高。
- 高可用: 每个服务实例都可以独立生成 ID,生成器是去中心化的,不存在单点故障。
如何使用 GID()
在我们的应用中,你不需要关心 SnowflakeIdGenerator
结构体的内部实现。我们已经将其封装成了一个全局可用的异步函数 GID()
。
使用方法极其简单,只需在 async
函数中调用并 .await
即可。
示例:在祺洛 中创建一个新订单
use crate::utils::gid::GID; // 假设 GID 函数在这个路径下
// 创建订单的 Handler
async fn create_order(
VJson(payload): VJson<CreateOrderPayload>,
) -> impl IntoResponse {
// 只需这一行,即可获得一个全新的、唯一的订单 ID
let new_order_id: i64 = GID().await;
let new_order = Order {
id: new_order_id,
user_id: payload.user_id,
product: payload.product,
// ... 其他字段
};
// ... 将 new_order 保存到数据库 ...
ApiResponse::ok(new_order)
}
为什么需要 `.await`?
GID()
被设计为异步函数,是因为底层的 SnowflakeIdGenerator
实例是 全局共享 的资源。为了保证在多线程环境下的绝对安全(防止多个线程同时修改序列号和时间戳),我们在其内部使用了异步锁(如 tokio::sync::Mutex
)。
调用 .await
会获取这个锁,在生成 ID 后立即释放。这个过程非常快,但异步的语法是保证其线程安全的关键。
核心实现亮点
我们的 SnowflakeIdGenerator
实现包含了一些关键特性来保证其健壮性:
原子操作 (Atomics)
sequence
(序列号) 和last_timestamp
(最后时间戳) 都使用了AtomicI64
。这使得在多线程环境下对它们的读写操作是原子性的,避免了数据竞争,性能远高于传统的互斥锁。处理高并发 (Handling High Concurrency)
- 如果在同一毫秒内的请求超过了 4096 个,生成器会进入一个 自旋等待 (
spin_loop
)。 - 它会快速循环,直到下一毫秒的到来,然后重置序列号并生成新的 ID。这是一种低延迟的等待策略,避免了线程的上下文切换。
- 如果在同一毫秒内的请求超过了 4096 个,生成器会进入一个 自旋等待 (
时钟回拨保护 (Clock Skew Protection) 这是一个非常重要的安全机制。
if current_timestamp < self.last_timestamp.load(Ordering::Relaxed) { panic!("时钟回退错误"); }
- 如果服务器的系统时间被向后调整(例如,由于 NTP 时间同步错误),可能会导致生成重复的 ID。
- 我们的实现会检测到这种情况,并立即 触发
panic!
,使服务崩溃。这是一种快速失败 (Fail-fast) 策略,旨在防止生成脏数据,并立即警告运维人员修复时间问题。
重要注意事项
部署时必须注意!
machine_id
和node_id
的唯一性- 在应用启动时,必须为每个 独立的、同时运行的 服务实例配置 独一无二 的
machine_id
和node_id
组合。 - 如果两个实例使用了相同的 ID 配置,它们将会生成重复的全局 ID!
- 通常,这些 ID 可以通过环境变量、配置文件或服务发现机制(如 Kubernetes 的 Pod 名称哈希)来注入。
- 在应用启动时,必须为每个 独立的、同时运行的 服务实例配置 独一无二 的
保证服务器时间同步
- 强烈建议在所有服务器上部署和启用 NTP (Network Time Protocol) 服务,以确保系统时间的准确性,避免时钟回拨问题。
通过遵循以上原则,GID()
将为我们的系统提供稳定、可靠且高性能的唯一 ID 生成服务。