雪花算法(SnowFlake)
SnowFlake 是 Twitter 开源的分布式 ID 生成算法,结果是一个 long 型的 ID。其核心思想是:使用 41bit 作为毫秒数,10bit 作为机器的 ID (5个bit是数据中心,5个bit是机器ID),12bit作为毫秒的流水号(意味着每个节点再每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0(表示正数)。
标准算法结构
雪花算法是生成一个64bit的long类型的唯一ID,一共包括了四部分:
0-0000000000000000000000000000000000000000-0000000000-000000000000
- 第一位是符号位,始终是0,表示正数。
- 41位是时间戳,精确到毫秒,因为引入了时间戳,所以生成的 id 基本能够保持自增,同时41bit作为毫秒的时间戳能够表示的时间是69年。
- 10位是机器标识,可以全部用作机器ID,也可以用来标识机房ID + 机器ID。10bit最多可以标识1024台机器。可以手动指定,也可以使用ip或者mac地址
- 12位是计数序列号,也就是同一台机器上同一时间,理论上还可以生成的ID。12bit最多可以区分4096个ID。
存在的问题
- 时钟回拨问题:由于机器的时间是动态调整的,有可能出现时间跑到之前几毫秒,如果这个时候获取到了这种时间,则会出现数据重复。
- 机器id分配及回收问题:目前机器id需要每台机器不一样,这样的方式分配如果机器宕机,对应的id如何回收存在问题。
- 机器id上限:机器id是固定的10位bit,也就是机器个数是有上限的,在某些大集群场景下1024个节点是不够的(场景过于遥远,暂不考虑)。
改良
改良结构
修改后结构
- 时间戳不再时刻与系统时钟同步
- worker id与时间戳互换位置
0-0000000000-00000000000000000000000000000000000000000-000000000000
改良的核心思想
核心思想是解除与操作系统时间戳的实时绑定,生成器只在初始化的时候获取系统当前的时间戳,作为初始时间戳,但之后就不再与系统时间戳保持同步了。之后的递增,只由序列号的递增来驱动。
比如:当前序列号是4095,下一个请求进来,序列号+1溢出了12位空间,序列号归零,溢出的则进位加到时间戳上,从而时间戳+1.
节点ID的生成策略做了修改,优先从本机网卡的MAC地址截取低10位,若未配置有效网卡,则从[0,1023]中随机挑选一个作为节点ID
示例代码
php
<?php
/**
* 雪花算法 ID 生成器
*
* 结构:1位固定0 | 10位workerId | 41位时间戳 | 12位序列号
*/
class IdWorker
{
private int $workerId;
private int $workerIdShifted;
private int $timestampBits = 41;
private int $sequenceBits = 12;
private int $timestampAndSequenceBits;
private int $twepoch;
private int $sequenceMask;
/** @var int[] */
private array $timestampAndSequence;
public function __construct(?int $workerId = null)
{
$this->twepoch = 1693411200000; // 2023-08-31 00:00:00
$this->timestampAndSequenceBits = $this->timestampBits + $this->sequenceBits; // 53
$this->sequenceMask = ~(-1 << $this->sequenceBits); // 4095
$maxWorkerId = ~(-1 << 10); // 1023
$this->timestampAndSequence = [0, 0]; // [timestamp, sequence]
if ($workerId === null) {
$workerId = $this->generateWorkerId();
}
if ($workerId > $maxWorkerId || $workerId < 0) {
throw new InvalidArgumentException("worker id 不能大于 {$maxWorkerId} 或小于 0");
}
$this->workerId = $workerId;
$this->workerIdShifted = $workerId << $this->timestampAndSequenceBits;
$this->initTimestampAndSequence();
}
/**
* 第一次初始化时间戳和序列号
*/
private function initTimestampAndSequence(): void
{
$timestamp = $this->getNewestTimestamp();
$this->timestampAndSequence = [$timestamp, 0];
}
/**
* 生成下一个雪花 ID
*/
public function nextId(): int
{
$this->waitIfNecessary();
// 序列号 +1
$this->timestampAndSequence[1]++;
// sequence 达到上限(4096)时,timestamp+1 并重置 sequence=0
if ($this->timestampAndSequence[1] > $this->sequenceMask) {
$this->timestampAndSequence[0]++;
$this->timestampAndSequence[1] = 0;
}
$timestampWithSequence =
($this->timestampAndSequence[0] << $this->sequenceBits)
| $this->timestampAndSequence[1];
return $this->workerIdShifted | $timestampWithSequence;
}
/**
* 当 QPS 过高导致序列号耗尽时,阻塞等待
*/
private function waitIfNecessary(): void
{
$current = $this->timestampAndSequence[0];
$newest = $this->getNewestTimestamp();
if ($current >= $newest) {
usleep(5000); // 5ms
}
}
/**
* 获取相对时间戳(距 twepoch 的毫秒数)
*/
private function getNewestTimestamp(): int
{
return (int)(microtime(true) * 1000) - $this->twepoch;
}
/**
* 自动生成 workerId:优先用 MAC 地址,失败则随机
*/
private function generateWorkerId(): int
{
try {
return $this->generateWorkerIdFromMac();
} catch (\Throwable $e) {
return $this->generateRandomWorkerId();
}
}
/**
* 根据 MAC 地址生成 workerId
*/
private function generateWorkerIdFromMac(): int
{
$mac = $this->getMacAddress();
// 最大值 768 + 255 = 1023
return (($mac[4] & 0B11) << 8) | ($mac[5] & 0xFF);
}
/**
* 获取首个有效网卡的 MAC 地址(跨平台)
*/
private function getMacAddress(): array
{
$mac = null;
// Linux: 尝试 /sys/class/net/*/address(最可靠)
if (PHP_OS === 'Linux') {
$output = @shell_exec('cat /sys/class/net/*/address 2>/dev/null');
if ($output && preg_match('/([0-9a-f]{2}:){5}[0-9a-f]{2}/i', $output, $matches)) {
$mac = $matches[0];
}
// 备选: ip link
if (!$mac) {
$output = @shell_exec('ip link show 2>/dev/null | grep -i ether');
if ($output && preg_match('/([0-9a-f]{2}:){5}[0-9a-f]{2}/i', $output, $matches)) {
$mac = $matches[0];
}
}
}
// macOS: ifconfig
if (PHP_OS === 'Darwin') {
$output = @shell_exec('ifconfig en0 2>/dev/null | grep ether');
if ($output && preg_match('/([0-9a-f]{2}:){5}[0-9a-f]{2}/i', $output, $matches)) {
$mac = $matches[0];
}
}
// Windows: getmac
if (!$mac && PHP_OS === 'WINNT') {
$output = @shell_exec('getmac');
if ($output) {
$output = str_replace('-', ':', $output);
if (preg_match('/([0-9a-f]{2}:){5}[0-9a-f]{2}/i', $output, $matches)) {
$mac = $matches[0];
}
}
}
if (!$mac) {
throw new RuntimeException('无法获取 MAC 地址');
}
return array_map('hexdec', explode(':', strtolower($mac)));
}
/**
* 随机生成 workerId(0~1023)
*/
private function generateRandomWorkerId(): int
{
return random_int(0, 1023);
}
}
// ===================== Demo =====================
$worker = new IdWorker(null);
echo "<pre>";
for ($i = 0; $i < 5; $i++) {
echo $worker->nextId() . PHP_EOL;
}
输出:
bash
3143859507722829825
3143859507722829826
3143859507722829827
3143859507722829828
3143859507722829829