引言
ID就像身份证号,每个人都有对应的且唯一身份证号码,在日常工作生活中,我们也时常需要给物品或某一事件一个唯一的标识来区分它们之间谁是谁。尤其是在当今的分布式系统中,生成唯一ID是一项至关重要的任务。无论是数据库中的主键,还是分布式环境中的任务ID,都需要确保ID的唯一性、时序性和高效性。传统的自增ID虽然简单,但在分布式环境下却无法保证全局唯一性。因此,诸如Twitter提出的雪花算法(Snowflake)成为了解决这一问题的重要方案。
本文将介绍雪花算法的原理和优势,探讨为什么在生成唯一ID时,雪花算法或基于它的变种算法被广泛应用。
基本概念
什么是雪花算法
雪花算法由Twitter在2010年提出,旨在为其分布式系统生成全球唯一的ID。雪花算法生成的ID是一个64位的长整数(Long类型),其组成如下:
- 符号位(1位):始终为0,因为生成的ID都是正数。
- 时间戳(41位):表示从某个自定义的起始时间(Epoch)到当前时间的毫秒数。41位能够支持长达约69年的时间范围。
- 数据中心标识(5位):用于标识数据中心,可以支持最多32个数据中心。
- 机器标识(5位):用于标识同一个数据中心内的机器,最多支持32台机器。
- 序列号(12位):在同一毫秒内生成的序列号,最多可以生成4096个不同的ID。
这样,通过组合这些不同的部分,雪花算法能够在分布式环境中生成唯一且有序的ID。
特点
雪花算法的设计具有以下几个重要特点:
分布式高效生成:由于每个节点都可以根据自身的时间戳和序列号生成ID,避免了分布式锁的开销。
在大规模分布式系统中,ID的生成速度直接影响系统的性能。自增ID由于需要集中管理,很难在多节点环境中高效生成。而雪花算法依赖于本地时钟和序列号生成ID,无需与中心节点通信,因此在每台机器上都可以独立生成ID,效率极高。
全局唯一性:通过不同的数据中心标识和机器标识,确保在不同节点生成的ID不冲突。
分布式系统最重要的需求之一就是全局唯一性。传统的ID生成方法,如UUID(通用唯一标识符),虽然可以保证唯一性,但生成的ID往往很长且无序。雪花算法通过时间戳、数据中心和机器标识的组合,不仅能够保证唯一性,还能生成较短的、有序的ID,便于数据库的存储和检索。
趋势递增:由于ID的主要部分是时间戳,生成的ID大致呈递增趋势,有助于数据库索引优化。
雪花算法生成的ID具有趋势递增的特点,这对于数据库索引的优化具有极大优势。传统的UUID由于是随机生成的,无法确保有序性,容易导致数据库索引碎片化,降低查询效率。而雪花算法的ID因基于时间戳生成,通常是递增的,有助于数据库索引的性能优化。
可扩展性:雪花算法允许通过增加标识位数或调整部分结构,扩展到更多的场景和需求。
雪花算法的灵活性使其在应对不同的业务场景时可以轻松调整。通过增加或调整标识位,可以支持更多的数据中心或机器节点。此外,在一些实现中,还可以通过缩短时间戳部分来延长序列号的位数,以支持每毫秒更多的ID生成需求。
应用场景
雪花算法由于其高效性、全局唯一性和趋势递增的特点,广泛应用于各类分布式系统中。以下是几个常见的应用场景:
分布式数据库
在分布式数据库中,数据分布在多个节点上,因此生成唯一的主键ID至关重要。雪花算法能够在各个节点独立生成唯一ID,避免了集中生成ID的瓶颈和复杂性。
消息队列系统
消息队列系统中的每条消息都需要有唯一的ID来进行追踪和处理。雪花算法可以确保在高并发环境下,生成的消息ID是唯一且有序的,便于后续的消息处理。
微服务架构
在微服务架构中,多个服务之间经常需要共享或传递ID来标识请求和事务。雪花算法的ID生成方式能够确保不同服务生成的ID不会冲突,便于跨服务的协同工作。
订单系统
订单系统需要为每个订单生成唯一的订单号,尤其在电商平台上,订单量巨大且并发高。雪花算法的高效性和全局唯一性,能够很好地满足订单号生成的需求。
雪花算法的变种
随着业务需求的不断发展,雪花算法的基本结构也被许多公司进行了改进和优化,衍生出一些变种算法。
百度的UidGenerator
百度的UidGenerator是基于雪花算法的优化版本。其主要改进点在于:
- 扩大了机器ID的范围,以支持更多的节点。
- 对时间戳进行了预处理,防止时钟回拨带来的ID冲突问题。
美团的Leaf
美团的Leaf是一种支持分布式系统生成唯一ID的解决方案,其提供了两种ID生成方式:基于数据库自增的号段模式和基于雪花算法的号段模式。Leaf在高并发场景下表现出色,且通过数据库模式解决了时间回拨等问题。
后面有机会我们将详细向大家介绍这两种雪花算法变种的优势在哪
为什么一定要使用雪花算法
分布式环境的必然选择
在分布式系统中,唯一ID的生成涉及多个节点和服务的协调,传统的集中式ID生成方法往往无法满足大规模系统的需求。雪花算法无需中心化的ID生成服务,极大提升了系统的扩展性和容错性。
高效和可靠性并存
雪花算法不仅可以高效生成ID,还能确保生成的ID具有全局唯一性、时序性和分布式环境下的稳定性。而这些特点正是分布式系统中不可或缺的。
优秀的兼容性
雪花算法的简单设计使得它可以轻松与各类技术栈和业务场景集成。无论是消息队列、订单系统,还是数据库主键生成,雪花算法都能发挥出色的作用。
代码示例
接下来我们来看看手搓一个简单的雪花算法要怎么写出关键代码
JAVA版本:
public class SnowflakeIdGenerator {
// 起始的时间戳(这里定义为2023-01-01)
private final long twepoch = 1672531200000L;
// 机器ID所占的位数(5位)
private final long workerIdBits = 5L;
// 数据中心ID所占的位数(5位)
private final long datacenterIdBits = 5L;
// 支持的最大机器ID
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 支持的最大数据中心ID
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 序列在ID中占的位数(12位)
private final long sequenceBits = 12L;
// 机器ID左移12位
private final long workerIdShift = sequenceBits;
// 数据中心ID左移17位(12 + 5)
private final long datacenterIdShift = sequenceBits + workerIdBits;
// 时间戳左移22位(12 + 5 + 5)
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 序列号最大值(4095)
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
// 机器ID
private long workerId;
// 数据中心ID
private long datacenterId;
// 序列号
private long sequence = 0L;
// 上次生成ID的时间戳
private long lastTimestamp = -1L;
// 构造函数,传入机器ID和数据中心ID
public SnowflakeIdGenerator(long workerId, long datacenterId) {
// 校验workerId是否在合理范围
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("机器ID不能大于最大ID%d和小于0", maxWorkerId));
}
// 校验datacenterId是否在合理范围
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("数据中心ID不能大于最大ID%d和小于0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// 生成唯一ID的核心方法
public synchronized long nextId() {
// 获取当前时间戳(毫秒)
long timestamp = System.currentTimeMillis();
// 如果当前时间小于上一次生成ID的时间,抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException("时间回拨 拒绝生成ID ,需等待 " + (lastTimestamp - timestamp) + " 毫秒");
}
// 如果在同一毫秒内生成ID,则增加序列号
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
// 如果序列号达到最大值(4096),等待下一毫秒
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 如果是新的毫秒,序列号重置为0
sequence = 0L;
}
// 更新上次生成ID的时间戳
lastTimestamp = timestamp;
// 生成ID并返回,ID由时间戳、数据中心ID、机器ID和序列号组成
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
// 等待直到下一毫秒
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
public static void main(String[] args) {
// 创建一个雪花算法生成器,传入机器ID和数据中心ID
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
// 生成10个ID并输出
for (int i = 0; i < 10; i++) {
// 输出生成的ID
System.out.println("生成的唯一ID: " + idGenerator.nextId());
}
}
}
Python版本:
import time
class SnowflakeIdGenerator:
def __init__(self, worker_id, datacenter_id, sequence=0):
# 起始的时间戳(2023-01-01)
self.twepoch = 1672531200000
# 机器ID和数据中心ID的位数
self.worker_id_bits = 5
self.datacenter_id_bits = 5
self.sequence_bits = 12
# 计算最大值(机器ID和数据中心ID)
self.max_worker_id = -1 ^ (-1 << self.worker_id_bits)
self.max_datacenter_id = -1 ^ (-1 << self.datacenter_id_bits)
# 位移偏移量,用于生成ID
self.worker_id_shift = self.sequence_bits
self.datacenter_id_shift = self.sequence_bits + self.worker_id_bits
self.timestamp_left_shift = self.sequence_bits + self.worker_id_bits + self.datacenter_id_bits
# 序列号掩码(12位:0b111111111111)
self.sequence_mask = -1 ^ (-1 << self.sequence_bits)
# 校验worker_id和datacenter_id是否在合理范围内
if worker_id > self.max_worker_id or worker_id < 0:
raise ValueError(f"worker_id 不能大于 {self.max_worker_id} 或小于 0")
if datacenter_id > self.max_datacenter_id or datacenter_id < 0:
raise ValueError(f"datacenter_id 不能大于 {self.max_datacenter_id} 或小于 0")
self.worker_id = worker_id
self.datacenter_id = datacenter_id
self.sequence = sequence
self.last_timestamp = -1
# 获取当前时间戳(以毫秒为单位)
def _current_timestamp(self):
return int(time.time() * 1000)
# 等待下一毫秒
def _til_next_millis(self, last_timestamp):
timestamp = self._current_timestamp()
while timestamp <= last_timestamp:
timestamp = self._current_timestamp()
return timestamp
# 生成唯一ID的核心方法
def next_id(self):
timestamp = self._current_timestamp()
# 如果当前时间戳小于上一次生成ID的时间,抛出异常
if timestamp < self.last_timestamp:
raise Exception(f"系统时钟回拨,拒绝生成ID,需等待 {self.last_timestamp - timestamp} 毫秒")
# 在同一毫秒内,增加序列号
if self.last_timestamp == timestamp:
self.sequence = (self.sequence + 1) & self.sequence_mask
# 如果序列号用完,则等待下一毫秒
if self.sequence == 0:
timestamp = self._til_next_millis(self.last_timestamp)
else:
# 新的一毫秒,序列号重置为0
self.sequence = 0
# 更新上一次生成ID的时间戳
self.last_timestamp = timestamp
# 返回生成的唯一ID,基于时间戳、数据中心ID、机器ID和序列号
return ((timestamp - self.twepoch) << self.timestamp_left_shift) | (self.datacenter_id << self.datacenter_id_shift) | (self.worker_id << self.worker_id_shift) | self.sequence
if __name__ == "__main__":
# 创建一个雪花算法生成器,传入机器ID和数据中心ID
id_generator = SnowflakeIdGenerator(1, 1)
# 生成10个ID并输出
for _ in range(10):
# 输出生成的唯一ID
print("生成的唯一ID:", id_generator.next_id())
# 创建一个雪花算法生成器,传入机器ID和数据中心ID
id_generator = SnowflakeIdGenerator(1, 1)
# 生成10个ID并输出
for _ in range(10):
# 输出生成的唯一ID
print(“生成的唯一ID:”, id_generator.next_id())
总结
雪花算法通过时间戳、数据中心标识、机器标识和序列号的组合,能够在分布式环境中高效生成全局唯一且有序的ID。它的高效性、趋势递增性和可扩展性,使其成为分布式系统中生成唯一ID的首选方案。随着业务需求的不断变化,雪花算法及其衍生版本在各种应用场景中得到了广泛应用,并不断优化改进。
欢迎关注公众:“全栈开发指南针”
这里是技术潮流的风向标,也是你代码旅程的导航仪!🚀
Let’s code and have fun! 🎉