PHP 一种改良版的雪花算法

雪花算法(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
相关推荐
小羊在睡觉3 小时前
力扣84. 柱状图中最大的矩形
后端·算法·leetcode·golang·go
3DVisionary3 小时前
蓝光三维扫描:医疗制造的精度焦虑怎么解
人工智能·算法·制造·蓝光三维扫描·医疗制造·三维检测·义齿检测
好评笔记4 小时前
机器学习面试八股——常用损失函数
人工智能·深度学习·算法·机器学习·校招
weixin_468466854 小时前
全局与局部注意力机制新手实战指南
人工智能·python·深度学习·算法·自然语言处理·transformer·注意力机制
_日拱一卒4 小时前
LeetCode:994腐烂的橘子
java·数据结构·算法·leetcode·深度优先
珂朵莉MM4 小时前
第七届全球校园人工智能算法精英大赛-算法巅峰赛产业命题赛第3赛季优化题--束搜索
人工智能·算法
Ether IC Verifier5 小时前
SystemVerilog 数据类型详解
php·systemverilog·uvm·ic验证
Omics Pro5 小时前
首个!外源天然产物综合性代谢图谱
数据库·人工智能·算法·机器学习·r语言
弥树子5 小时前
踩坑记录:服务器内网调用接口,真实请求URL与官方公开URL不一致问题排查
开发语言·php
voidmort5 小时前
3. 微调(Fine-tuning)与强化学习(RL)的核心思想
python·深度学习·算法