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
相关推荐
一只数据集2 小时前
全尺寸人形机器人灵巧手力觉触觉数据集-2908条ROSbag数据覆盖14大应用场景深度解析
大数据·人工智能·算法·机器人
尘中客2 小时前
【2026最新】如何用 WordPress 零代码搭建八字排盘/紫微斗数网站(附免费开源插件)
php·api·wordpress·建站源码·网站引流
罗西的思考3 小时前
【GUI-Agent】阿里通义MAI-UI 代码阅读(2)--- 实现
人工智能·算法·机器学习
刀法如飞4 小时前
TypeScript 数组去重的 20 种实现方式,哪一种你还不知道?
前端·javascript·算法
sali-tec5 小时前
C# 基于OpenCv的视觉工作流-章66-直线夹角
图像处理·人工智能·opencv·算法·计算机视觉
AC赳赳老秦5 小时前
接口测试自动化:用 OpenClaw 对接 Postman,实现批量回归测试、测试报告自动生成与推送
java·人工智能·python·算法·elasticsearch·deepseek·openclaw
_风满楼5 小时前
TDD实战-会议室冲突检测的红绿重构循环
前端·javascript·算法
pq2176 小时前
java实现遗传算法
算法
计算机安禾6 小时前
【计算机网络】第17篇:TCP拥塞控制的迭代——从Reno到CUBIC、BBR的理论转向
tcp/ip·计算机网络·php