diff --git a/application/common/library/Snowflake.php b/application/common/library/Snowflake.php index 2e9e9280..3c365c65 100644 --- a/application/common/library/Snowflake.php +++ b/application/common/library/Snowflake.php @@ -5,121 +5,471 @@ namespace app\common\library; class Snowflake { - // 起始时间戳(2023-01-01) + // 起始时间戳(2023-01-01 00:00:00) const EPOCH = 1672502400000; - // 机器ID位数 - const WORKER_ID_BITS = 5; - const DATACENTER_ID_BITS = 5; - const SEQUENCE_BITS = 12; + // 位数分配 + const WORKER_ID_BITS = 5; // 5位工作机器ID + const DATACENTER_ID_BITS = 5; // 5位数据中心ID + const SEQUENCE_BITS = 12; // 12位序列号 - // 最大值 + // 最大值计算 const MAX_WORKER_ID = -1 ^ (-1 << self::WORKER_ID_BITS); const MAX_DATACENTER_ID = -1 ^ (-1 << self::DATACENTER_ID_BITS); const MAX_SEQUENCE = -1 ^ (-1 << self::SEQUENCE_BITS); - // 移位 + // 移位偏移量 const WORKER_ID_SHIFT = self::SEQUENCE_BITS; const DATACENTER_ID_SHIFT = self::SEQUENCE_BITS + self::WORKER_ID_BITS; const TIMESTAMP_LEFT_SHIFT = self::SEQUENCE_BITS + self::WORKER_ID_BITS + self::DATACENTER_ID_BITS; + // 序列号掩码 + const SEQUENCE_MASK = -1 ^ (-1 << self::SEQUENCE_BITS); + + // 实例存储 + protected static $instances = []; + protected $workerId; protected $datacenterId; protected $sequence = 0; protected $lastTimestamp = -1; + protected $lastGenerateTime = 0; + /** + * 构造函数 + * @param int $workerId 工作机器ID (0-31) + * @param int $datacenterId 数据中心ID (0-31) + * @throws \Exception + */ public function __construct($workerId = 1, $datacenterId = 1) { + // 验证 workerId if ($workerId > self::MAX_WORKER_ID || $workerId < 0) { - throw new \Exception("worker Id can't be greater than " . self::MAX_WORKER_ID . " or less than 0"); + throw new \Exception(sprintf( + "Worker ID 必须在 0 到 %d 之间,当前值:%d", + self::MAX_WORKER_ID, + $workerId + )); } + // 验证 datacenterId if ($datacenterId > self::MAX_DATACENTER_ID || $datacenterId < 0) { - throw new \Exception("datacenter Id can't be greater than " . self::MAX_DATACENTER_ID . " or less than 0"); + throw new \Exception(sprintf( + "Datacenter ID 必须在 0 到 %d 之间,当前值:%d", + self::MAX_DATACENTER_ID, + $datacenterId + )); } $this->workerId = $workerId; $this->datacenterId = $datacenterId; + + // 记录日志 + \think\Log::info(sprintf( + "Snowflake 实例初始化完成,WorkerId: %d, DatacenterId: %d", + $workerId, + $datacenterId + )); } + /** + * 生成下一个ID + * @return int + * @throws \Exception + */ public function nextId() { $timestamp = $this->timeGen(); + // 检查时钟回拨 if ($timestamp < $this->lastTimestamp) { $diff = $this->lastTimestamp - $timestamp; - throw new \Exception("Clock moved backwards. Refusing to generate id for {$diff} milliseconds"); + + // 轻微时钟回拨,等待时钟追上 + if ($diff < 100) { // 100毫秒内的回拨 + usleep($diff * 1000); // 微秒休眠 + $timestamp = $this->timeGen(); + } else { + throw new \Exception(sprintf( + "时钟回拨过大,拒绝生成ID。回拨时间:%d 毫秒", + $diff + )); + } } + // 同一毫秒内生成多个ID if ($this->lastTimestamp == $timestamp) { $this->sequence = ($this->sequence + 1) & self::MAX_SEQUENCE; + + // 序列号溢出,等待下一毫秒 if ($this->sequence == 0) { $timestamp = $this->tilNextMillis($this->lastTimestamp); } } else { - $this->sequence = 0; + // 新的毫秒,重置序列号,使用随机初始值避免重复 + $this->sequence = mt_rand(0, 10) & self::MAX_SEQUENCE; } $this->lastTimestamp = $timestamp; - return (($timestamp - self::EPOCH) << self::TIMESTAMP_LEFT_SHIFT) | - ($this->datacenterId << self::DATACENTER_ID_SHIFT) | - ($this->workerId << self::WORKER_ID_SHIFT) | - $this->sequence; - } + // 计算并返回ID + $id = ((($timestamp - self::EPOCH) << self::TIMESTAMP_LEFT_SHIFT) + | ($this->datacenterId << self::DATACENTER_ID_SHIFT) + | ($this->workerId << self::WORKER_ID_SHIFT) + | $this->sequence); - protected function tilNextMillis($lastTimestamp) - { - $timestamp = $this->timeGen(); - while ($timestamp <= $lastTimestamp) { - $timestamp = $this->timeGen(); - } - return $timestamp; - } + // 记录生成时间(用于调试) + $this->lastGenerateTime = microtime(true); - protected function timeGen() - { - return floor(microtime(true) * 1000); + return $id; } /** - * 解析雪花ID - * @param int $id + * 等待到下一毫秒 + * @param int $lastTimestamp 最后时间戳 + * @return int + */ + protected function tilNextMillis($lastTimestamp) + { + $timestamp = $this->timeGen(); + $waitCount = 0; + + while ($timestamp <= $lastTimestamp) { + // 等待时间过长记录警告 + if ($waitCount++ > 100) { + \think\Log::warning("Snowflake 等待下一毫秒时间过长,已等待 {$waitCount} 次循环"); + } + + // 微秒级休眠,避免CPU占用过高 + usleep(100); // 休眠100微秒 + $timestamp = $this->timeGen(); + } + + return $timestamp; + } + + /** + * 获取当前时间戳(毫秒) + * @return int + */ + protected function timeGen() + { + // 使用微秒时间,提高精度 + return (int)(microtime(true) * 1000); + } + + /** + * 解析雪花ID为各个组成部分 + * @param int $id 雪花ID * @return array */ public static function parseId($id) { + // 转换为64位二进制字符串 $binary = decbin($id); $binary = str_pad($binary, 64, '0', STR_PAD_LEFT); - $timestamp = bindec(substr($binary, 0, 42)); - $timestamp = $timestamp + self::EPOCH; + // 提取各组成部分 + $timestampBin = substr($binary, 0, 42); + $datacenterIdBin = substr($binary, 42, 5); + $workerIdBin = substr($binary, 47, 5); + $sequenceBin = substr($binary, 52, 12); - $datacenterId = bindec(substr($binary, 42, 5)); - $workerId = bindec(substr($binary, 47, 5)); - $sequence = bindec(substr($binary, 52, 12)); + // 转换为十进制 + $timestamp = bindec($timestampBin) + self::EPOCH; + $datacenterId = bindec($datacenterIdBin); + $workerId = bindec($workerIdBin); + $sequence = bindec($sequenceBin); + + // 转换为可读时间 + $dateTime = date('Y-m-d H:i:s', (int)($timestamp / 1000)); + $millisecond = $timestamp % 1000; return [ + 'id' => $id, 'timestamp' => $timestamp, - 'datacenterId' => $datacenterId, - 'workerId' => $workerId, - 'sequence' => $sequence + 'datetime' => $dateTime, + 'millisecond' => $millisecond, + 'datacenter_id' => $datacenterId, + 'worker_id' => $workerId, + 'sequence' => $sequence, + 'binary' => $binary, + 'parts' => [ + 'timestamp' => $timestampBin, + 'datacenter_id' => $datacenterIdBin, + 'worker_id' => $workerIdBin, + 'sequence' => $sequenceBin + ] ]; } /** - * 生成ID(单例模式) + * 生成ID(主调用方法) + * @param int|null $workerId 工作机器ID + * @param int|null $datacenterId 数据中心ID + * @return int + * @throws \Exception + */ + public static function generate($workerId = null, $datacenterId = null) + { + // 获取配置 + if ($workerId === null) { + $workerId = self::getWorkerId(); + } + + if ($datacenterId === null) { + $datacenterId = self::getDatacenterId(); + } + + // 创建实例键 + $instanceKey = "{$workerId}_{$datacenterId}"; + + // 检查是否已有实例 + if (!isset(self::$instances[$instanceKey])) { + self::$instances[$instanceKey] = new self($workerId, $datacenterId); + } + + // 生成ID + return self::$instances[$instanceKey]->nextId(); + } + + /** + * 获取工作机器ID * @return int */ - public static function generate() + protected static function getWorkerId() { - static $instance = null; - if (!$instance) { - // 从配置获取workerId和datacenterId - $workerId = config('snowflake.worker_id') ?: 1; - $datacenterId = config('snowflake.datacenter_id') ?: 1; - $instance = new self($workerId, $datacenterId); + // 1. 从配置文件获取(FastAdmin 使用 application/extra/ 目录) + $config = config('snowflake'); + if ($config && isset($config['worker_id'])) { + $workerId = (int)$config['worker_id']; + if ($workerId >= 0 && $workerId <= self::MAX_WORKER_ID) { + return $workerId; + } } - return $instance->nextId(); + + // 2. 从环境变量获取(FastAdmin 使用 .env 文件) + if (function_exists('env')) { + $envWorkerId = env('SNOWFLAKE_WORKER_ID'); + if ($envWorkerId !== null && $envWorkerId !== false) { + $workerId = (int)$envWorkerId; + if ($workerId >= 0 && $workerId <= self::MAX_WORKER_ID) { + return $workerId; + } + } + } + + // 3. 自动生成(基于服务器信息和进程ID) + // 使用服务器IP的最后一段 + 进程ID + $serverIp = self::getServerIp(); + $ipParts = explode('.', $serverIp); + $ipLastPart = end($ipParts); + $processId = getmypid() ?: 1; + + // 计算唯一的工作ID + $workerId = (($ipLastPart * 1000) + $processId) % (self::MAX_WORKER_ID + 1); + + // 确保在有效范围内 + $workerId = max(0, min($workerId, self::MAX_WORKER_ID)); + + return (int)$workerId; + } + + /** + * 获取数据中心ID + * @return int + */ + protected static function getDatacenterId() + { + // 1. 从配置文件获取 + $config = config('snowflake'); + if ($config && isset($config['datacenter_id'])) { + $datacenterId = (int)$config['datacenter_id']; + if ($datacenterId >= 0 && $datacenterId <= self::MAX_DATACENTER_ID) { + return $datacenterId; + } + } + + // 2. 从环境变量获取 + if (function_exists('env')) { + $envDatacenterId = env('SNOWFLAKE_DATACENTER_ID'); + if ($envDatacenterId !== null && $envDatacenterId !== false) { + $datacenterId = (int)$envDatacenterId; + if ($datacenterId >= 0 && $datacenterId <= self::MAX_DATACENTER_ID) { + return $datacenterId; + } + } + } + + // 3. 自动生成(基于服务器IP) + $serverIp = self::getServerIp(); + $ipParts = explode('.', $serverIp); + + // 使用IP前两段计算数据中心ID + $part1 = isset($ipParts[0]) ? (int)$ipParts[0] : 0; + $part2 = isset($ipParts[1]) ? (int)$ipParts[1] : 0; + $datacenterId = (($part1 * 10) + $part2) % (self::MAX_DATACENTER_ID + 1); + + // 确保在有效范围内 + $datacenterId = max(0, min($datacenterId, self::MAX_DATACENTER_ID)); + + return (int)$datacenterId; + } + + /** + * 获取服务器IP + * @return string + */ + protected static function getServerIp() + { + // 多种方式获取服务器IP + if (isset($_SERVER['SERVER_ADDR'])) { + return $_SERVER['SERVER_ADDR']; + } + + if (function_exists('gethostname')) { + $hostname = gethostname(); + if ($hostname) { + $ip = gethostbyname($hostname); + if ($ip !== $hostname) { + return $ip; + } + } + } + + // 尝试获取本机IP + if (function_exists('shell_exec') && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + $ip = shell_exec("hostname -I | awk '{print $1}'"); + if ($ip) { + return trim($ip); + } + } + + // 默认返回127.0.0.1 + return '127.0.0.1'; + } + + /** + * 批量生成ID + * @param int $count 生成数量 + * @param int|null $workerId 工作机器ID + * @param int|null $datacenterId 数据中心ID + * @return array + * @throws \Exception + */ + public static function batchGenerate($count = 10, $workerId = null, $datacenterId = null) + { + if ($count <= 0 || $count > 10000) { + throw new \Exception("生成数量必须在 1-10000 之间"); + } + + $ids = []; + for ($i = 0; $i < $count; $i++) { + $ids[] = self::generate($workerId, $datacenterId); + } + + return $ids; + } + + /** + * 验证ID是否有效 + * @param int $id 雪花ID + * @return bool + */ + public static function validate($id) + { + if (!is_numeric($id) || $id <= 0) { + return false; + } + + try { + $parsed = self::parseId($id); + + // 检查时间戳是否合理(不能超过当前时间+10年) + $currentTime = (int)(microtime(true) * 1000); + $maxTime = $currentTime + (10 * 365 * 24 * 60 * 60 * 1000); // 10年后 + + if ($parsed['timestamp'] > $maxTime || $parsed['timestamp'] < self::EPOCH) { + return false; + } + + // 检查ID组成部分是否在有效范围内 + if ($parsed['datacenter_id'] > self::MAX_DATACENTER_ID || + $parsed['worker_id'] > self::MAX_WORKER_ID || + $parsed['sequence'] > self::MAX_SEQUENCE) { + return false; + } + + return true; + } catch (\Exception $e) { + return false; + } + } + + /** + * 获取ID生成统计信息 + * @return array + */ + public function getStats() + { + return [ + 'worker_id' => $this->workerId, + 'datacenter_id' => $this->datacenterId, + 'last_timestamp' => $this->lastTimestamp, + 'last_generate_time' => $this->lastGenerateTime, + 'max_worker_id' => self::MAX_WORKER_ID, + 'max_datacenter_id' => self::MAX_DATACENTER_ID, + 'max_sequence' => self::MAX_SEQUENCE, + 'epoch' => self::EPOCH, + 'epoch_datetime' => date('Y-m-d H:i:s', self::EPOCH / 1000) + ]; + } + + /** + * 获取ID的详细信息(调试用) + * @param int $id + * @return array + */ + public static function getDetail($id) + { + if (!self::validate($id)) { + return ['error' => '无效的雪花ID']; + } + + $parsed = self::parseId($id); + + // 添加生成时间估算 + $generateTime = date('Y-m-d H:i:s', $parsed['timestamp'] / 1000); + $now = time(); + $generateTimestamp = $parsed['timestamp'] / 1000; + $timeDiff = $now - $generateTimestamp; + + if ($timeDiff < 60) { + $timeAgo = "刚刚"; + } elseif ($timeDiff < 3600) { + $timeAgo = floor($timeDiff / 60) . "分钟前"; + } elseif ($timeDiff < 86400) { + $timeAgo = floor($timeDiff / 3600) . "小时前"; + } else { + $timeAgo = floor($timeDiff / 86400) . "天前"; + } + + return [ + 'id' => $parsed['id'], + 'timestamp' => $parsed['timestamp'], + 'datetime' => $parsed['datetime'], + 'datetime_full' => $generateTime . '.' . str_pad($parsed['millisecond'], 3, '0', STR_PAD_LEFT), + 'time_ago' => $timeAgo, + 'datacenter_id' => $parsed['datacenter_id'], + 'worker_id' => $parsed['worker_id'], + 'sequence' => $parsed['sequence'], + 'binary' => chunk_split($parsed['binary'], 8, ' '), + 'binary_parts' => [ + 'timestamp' => $parsed['parts']['timestamp'], + 'datacenter' => $parsed['parts']['datacenter_id'], + 'worker' => $parsed['parts']['worker_id'], + 'sequence' => $parsed['parts']['sequence'] + ] + ]; } } \ No newline at end of file diff --git a/application/extra/snowflake.php b/application/extra/snowflake.php index 1a3e94d9..a4409e4a 100644 --- a/application/extra/snowflake.php +++ b/application/extra/snowflake.php @@ -1,5 +1,27 @@ 1, // 工作ID(1-31) - 'datacenter_id' => 1, // 数据中心ID(1-31) + // 工作机器ID (0-31) + // -1 表示自动生成 + 'worker_id' => -1, + + // 数据中心ID (0-31) + // -1 表示自动生成 + 'datacenter_id' => -1, + + // 是否启用日志记录 + 'log_enabled' => false, + + // 时钟回拨处理策略 + 'clock_backwards_strategy' => 'wait', + + // 最大等待时间(毫秒) + 'max_wait_time' => 100, + + // ID生成模式 + 'mode' => 'auto', + + // 服务器标识 + 'server_identifier' => '', ]; \ No newline at end of file