在后端开发中,分库分表后,数据库自增ID就面临冲突的挑战。每个分片如果都从1开始自增,合并数据时ID重复的问题便无法避免。此时,一个可靠的分布式 ID 生成服务就显得至关重要。
本文我们将使用Rust从零开始构建这样一个服务,它同时支持三种主流的ID生成算法:Snowflake、ULID和NanoID。你可以根据自己的业务场景灵活选择。
几种常见分布式ID方案剖析
在动手之前,我们先简单回顾和对比几种常见的方案,这有助于理解我们为何选择实现这几种算法。
UUID
生成非常方便,但其长度(36个字符)和无序性是主要缺点。无序性会导致数据库索引产生大量随机写入,影响性能。
数据库号段模式
性能通常不错,但其强依赖数据库。一旦数据库故障,整个服务将不可用,在可用性要求高的场景下存在风险。
Snowflake
这是Twitter开源的经典方案,生成64位整数。其性能高,并且生成的ID基于时间戳,整体上是有序的。主要缺点是对机器时钟敏感,时钟回拨可能导致ID重复。
ULID
近年来较为流行的方案,长度26个字符,使用Base32编码。相比UUID,它既保证了有序性(字典序可排序),也缩短了长度。
NanoID
一个更为简单的方案,本质是生成一个安全的随机字符串。它短小精悍、URL安全,非常适合用作短链接标识符等场景。
我们今天的目标,就是使用Rust将上述三种算法优雅地实现出来。
如何设计项目结构?
清晰的架构是项目成功的基础。我们采用分层设计,核心思路分为三层:

- Generator (生成器层):负责核心的ID生成逻辑,我们将为每种算法提供一个独立的实现。
- Storage (存储层):负责持久化关键状态。例如Snowflake算法需要记录上次的时间戳和序列号,以便在服务重启后能继续工作且不产生重复ID。
- Transport (传输层):负责对外提供服务,我们计划同时支持HTTP和gRPC两种协议。
这种分层设计的好处是每一层都可以独立替换和扩展。例如,你想将状态存储从文件换成Redis,只需实现一个新的 RedisStorage 并遵守 Storage trait即可,生成器代码完全无需改动。
基于此,我们的项目目录结构设计如下:
src/
├── lib.rs # 库入口,导出公共接口
├── bin/
│ └── globuid.rs # CLI 入口
├── generator/
│ ├── mod.rs # IdGenerator trait
│ ├── snowflake.rs # Snowflake 实现
│ ├── ulid.rs # ULID 实现
│ └── nanoid.rs # NanoID 实现
├── storage/
│ ├── mod.rs # Storage trait
│ ├── memory.rs # 内存存储
│ └── file.rs # 文件存储
├── http/
│ └── server.rs # HTTP 服务
└── grpc/
└── server.rs # gRPC 服务
定义核心接口
首先需要定义统一的ID生成接口。由于不同算法生成的ID格式不同(Snowflake是u64,ULID和NanoID是String),我们使用一个枚举来统一封装。
pub enum Id {
Numeric64(u64),
String(String),
}
impl Id {
pub fn as_string(&self) -> String {
match self {
Id::Numeric64(n) => n.to_string(),
Id::String(s) => s.clone(),
}
}
}
接下来是生成器层的核心Trait定义。这里我们使用异步Trait模式,以适应可能的异步I/O操作。
pub trait IdGenerator: Send + Sync {
type Error: std::error::Error + Send + Sync + 'static;
fn generate(&self)
-> Pin<Box<dyn Future<Output = Result<Id, Self::Error>> + Send + '_>>;
fn generate_batch(&self, count: usize)
-> Pin<Box<dyn Future<Output = Result<Vec<Id>, Self::Error>> + Send + '_>> {
// 默认实现:循环调用 generate
Box::pin(async move {
let mut ids = Vec::with_capacity(count);
for _ in 0..count {
ids.push(self.generate().await?);
}
Ok(ids)
})
}
}
存储层的Trait则较为简单,主要用于Snowflake这类需要状态的生成器。
#[derive(Default, Serialize, Deserialize)]
pub struct GeneratorState {
pub worker_id: u16,
pub last_timestamp: u64,
pub last_sequence: u64,
}
pub trait Storage: Send + Sync {
fn load(&self) -> Pin<Box<dyn Future<Output = Result<GeneratorState, Box<dyn Error>>> + Send + '_>>;
fn save(&self, state: GeneratorState) -> Pin<Box<dyn Future<Output = Result<(), Box<dyn Error>>> + Send + '_>>;
}
实现Snowflake算法
Snowflake算法生成的ID是一个64位整数,其结构如下:
| 1 bit 符号位 | 41 bits 时间戳 | 10 bits 机器 ID | 12 bits 序列号 |
其中,41位时间戳(毫秒级)大约可以使用69年;10位机器ID最多支持1024台机器;12位序列号意味着同一毫秒内最多可生成4096个ID。
其核心生成逻辑如下:
pub async fn generate_u64(&self) -> Result<u64, SnowflakeError> {
let mut state = self.state.lock().await;
let current_timestamp = self.current_timestamp()?;
let (timestamp, sequence) = if current_timestamp < state.last_timestamp {
// 时钟回拨了,这很危险
return Err(SnowflakeError::ClockMovedBackwards);
} else if current_timestamp == state.last_timestamp {
// 同一毫秒,序列号加 1
let sequence = state.last_sequence + 1;
if sequence > 4095 {
// 这一毫秒的 ID 用完了,等下一毫秒
(self.wait_for_next_millis(state.last_timestamp).await, 0)
} else {
(current_timestamp, sequence)
}
} else {
// 新的一毫秒,序列号从 0 开始
(current_timestamp, 0)
};
// 拼装 ID
let id = ((timestamp - self.config.epoch) << 22)
| (self.config.worker_id as u64) << 12
| sequence;
// 保存状态
state.last_timestamp = timestamp;
state.last_sequence = sequence;
self.storage.save(...).await?;
Ok(id)
}
在实现Snowflake时,有几个关键点需要注意:
- 时钟回拨:如果服务器时钟发生回拨,可能导致ID重复。处理方式通常是报错或等待时钟追回。生产环境建议配合NTP服务或使用分布式协调服务(如ZooKeeper)进行时钟同步。
- 序列号耗尽:在极高并发下,单机单毫秒可能生成超过4096个ID,此时需要等待至下一毫秒。
- 状态持久化:每次生成ID后都需要保存最后的时间戳和序列号,以保证服务重启后ID生成的连续性。
实现ULID算法
ULID的结构由两部分组成:
| 48 bits 时间戳 | 80 bits 随机数 |
48位时间戳(毫秒级)可以支持到公元10889年。80位的随机部分,使得每毫秒可以生成 2^80 个ID,几乎不可能重复。最终使用Crockford‘s Base32编码为26个字符。这种编码的特点是:
- 剔除了I、L、O、U等容易混淆的字符。
- 保持字典序与时间序一致。
- URL安全,无特殊字符。
核心实现片段:
pub fn generate_string(&self) -> Result<String, UlidError> {
let timestamp = Self::current_timestamp()?;
let mut bytes = [0u8; 16];
// 前 48 位放时间戳
bytes[0..6].copy_from_slice(×tamp.to_be_bytes()[2..8]);
// 后 80 位放随机数
getrandom::fill(&mut bytes[6..16])?;
// 单调递增模式:同一毫秒内,随机部分递增
if self.config.monotonic {
let last_ts = self.last_timestamp.load(Ordering::SeqCst);
if timestamp == last_ts {
// 递增随机部分,确保有序
// ...
}
}
Ok(Self::encode_ulid(&bytes))
}
fn encode_ulid(bytes: &[u8; 16]) -> String {
const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
// 128 bits 编码成 26 个字符(每字符 5 bits)
// ...
}
ULID还支持“单调递增”模式。开启后,同一毫秒内生成的ID会确保严格递增,这为某些需要绝对有序性的场景提供了保障。
实现NanoID算法
NanoID本质上是一个随机字符串生成器,默认长度为21个字符,使用URL安全的字母表。它的核心挑战在于:如何保证生成的字符均匀分布在字母表里?简单的取模运算会引入偏差。
NanoID采用“掩码+拒绝采样”的方法来解决这个问题:
pub fn new(config: NanoIdConfig) -> Self {
let alphabet_len = config.alphabet.len();
// 计算掩码:找到最小的 (2^n - 1) >= alphabet_len - 1
// 比如字母表 64 个字符,掩码就是 63 (0b111111)
let mask = {
let mut m = 1u8;
while m < alphabet_len as u8 - 1 {
m = m * 2 + 1;
}
m
};
Self { config, mask }
}
pub fn generate_string(&self) -> Result<String, NanoIdError> {
let mut result = String::with_capacity(self.config.length);
let mut bytes = vec![0u8; self.step * 2];
while result.len() < self.config.length {
getrandom::fill(&mut bytes)?;
for &byte in &bytes {
let index = byte & self.mask; // 用掩码过滤
if (index as usize) < self.config.alphabet.len() {
// 索引有效,使用这个字符
result.push(self.config.alphabet[index as usize] as char);
if result.len() >= self.config.length {
break;
}
}
// 索引无效,丢弃这个字节,继续下一个
}
}
Ok(result)
}
掩码的作用是将随机字节映射到一个比字母表稍大的范围,然后拒绝掉超出字母表范围的索引,从而保证每个字符被选中的概率均等。
存储层实现
对于Snowflake这类需要状态的生成器,我们实现了两种基础的存储方式:
MemoryStorage:内存存储,重启后状态丢失。适用于单实例或测试环境。
pub struct MemoryStorage {
state: Arc<RwLock<GeneratorState>>,
}
FileStorage:文件存储(JSON格式),重启后可恢复状态,适用于简单的生产环境。
pub struct FileStorage {
path: PathBuf,
}
impl Storage for FileStorage {
fn save(&self, state: GeneratorState) -> Pin<Box<...>> {
Box::pin(async move {
let data = serde_json::to_vec_pretty(&state)?;
let mut file = fs::File::create(&self.path).await?;
file.write_all(&data).await?;
file.sync_all().await?; // 确保刷到磁盘
Ok(())
})
}
}
如果需要分布式部署,可以基于此Trait实现 RedisStorage 或数据库存储。
HTTP服务实现
我们使用Axum框架来提供HTTP API。路由设计非常简单:
pub fn create_router<G: IdGenerator + 'static>() -> Router<Arc<ServerState<G>>> {
Router::new()
.route("/id", get(generate_id))
.route("/id/batch", get(generate_batch))
.route("/health", get(health_check))
}
async fn generate_id<G: IdGenerator>(
State(state): State<Arc<ServerState<G>>>,
) -> Result<Json<IdResponse>, (StatusCode, Json<ErrorResponse>)> {
let id = state.generator.generate().await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e.to_string() })))?;
Ok(Json(IdResponse { id: id.as_string() }))
}
接口非常直观:
GET /id:生成一个ID。
GET /id/batch?count=100:批量生成指定数量的ID。
GET /health:健康检查端点。
gRPC服务实现
对于内部服务间调用,gRPC通常能提供更好的性能。首先定义proto文件:
syntax = "proto3";
package globuid;
service GlobUid {
rpc Generate(GenerateRequest) returns (GenerateResponse);
rpc GenerateBatch(GenerateBatchRequest) returns (GenerateBatchResponse);
}
message GenerateRequest {}
message GenerateResponse { string id = 1; }
message GenerateBatchRequest { uint32 count = 1; }
message GenerateBatchResponse {
repeated string ids = 1;
uint32 count = 2;
}
然后使用Tonic框架来实现这个服务:
pub struct GlobUidService<G: IdGenerator> {
generator: Arc<G>,
}
#[tonic::async_trait]
impl<G: IdGenerator + 'static> GlobUid for GlobUidService<G> {
async fn generate(&self, _: Request<GenerateRequest>) -> Result<Response<GenerateResponse>, Status> {
let id = self.generator.generate().await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(GenerateResponse { id: id.as_string() }))
}
}
命令行工具
我们提供一个CLI工具,方便直接启动服务或测试。
#[derive(Parser)]
struct Args {
#[arg(short = 'a', long, default_value = "snowflake")]
algorithm: String, // snowflake / ulid / nanoid
#[arg(short, long, default_value = "0")]
worker_id: u16,
#[arg(short, long, default_value = "8080")]
port: u16,
#[arg(short = 'P', long, default_value = "http")]
protocol: String, // http / grpc
}
使用方法示例:
# 编译
cargo build --release --features full
# 启动 Snowflake HTTP 服务
./globuid --algorithm snowflake --worker-id 1 --port 8080
# 启动 ULID gRPC 服务
./globuid -a ulid -P grpc -p 50051
# 用文件存储
./globuid --storage file --storage-path ./state.json
作为库使用
除了作为独立服务运行,你也可以将其作为库直接集成到你的Rust项目中。
use globuid::{Snowflake, SnowflakeConfig, MemoryStorage, Ulid, NanoId};
#[tokio::main]
async fn main() {
// Snowflake
let config = SnowflakeConfig { worker_id: 1, ..Default::default() };
let storage = Arc::new(MemoryStorage::new());
let snowflake = Snowflake::new(config, storage).await.unwrap();
println!("Snowflake: {}", snowflake.generate_u64().await.unwrap());
// ULID
let ulid = Ulid::with_default();
println!("ULID: {}", ulid.generate_string().unwrap());
// NanoID
let nanoid = NanoId::default();
println!("NanoID: {}", nanoid.generate_string().unwrap());
}
总结
通过这个项目,我们可以体会到Rust在后端开发中的一些核心优势:
类型系统保证正确性:通过Trait进行抽象,编译器能帮助我们检查接口契约。Send + Sync 的约束确保了线程安全。
零成本抽象:泛型在编译时单态化,几乎没有运行时开销。Feature flags允许用户只编译他们需要的功能。
异步优先:所有I/O操作都基于异步设计,配合Tokio运行时,在高并发场景下能充分发挥性能。
组合优于继承:Generator、Storage、Transport三层分离的设计,使得每一层都可以独立替换和扩展,比传统的继承体系更加灵活和清晰。
这个项目完整地展示了一个可生产使用的分布式ID生成器的构建思路。完整的源码实现可以访问 https://github.com/lispking/globuid 进行查阅和学习。对于 Rust 和系统设计感兴趣的开发者,也欢迎在云栈社区进行更深入的交流和探讨。