Files
yusheng-php/application/common/library/Snowflake.php
2026-01-13 23:41:55 +08:00

475 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// application/common/library/Snowflake.php
namespace app\common\library;
class Snowflake
{
// 起始时间戳2023-01-01 00:00:00
const EPOCH = 1672502400000;
// 位数分配
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(sprintf(
"Worker ID 必须在 0 到 %d 之间,当前值:%d",
self::MAX_WORKER_ID,
$workerId
));
}
// 验证 datacenterId
if ($datacenterId > self::MAX_DATACENTER_ID || $datacenterId < 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;
// 轻微时钟回拨,等待时钟追上
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 = mt_rand(0, 10) & self::MAX_SEQUENCE;
}
$this->lastTimestamp = $timestamp;
// 计算并返回ID
$id = ((($timestamp - self::EPOCH) << self::TIMESTAMP_LEFT_SHIFT)
| ($this->datacenterId << self::DATACENTER_ID_SHIFT)
| ($this->workerId << self::WORKER_ID_SHIFT)
| $this->sequence);
// 记录生成时间(用于调试)
$this->lastGenerateTime = microtime(true);
return $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);
// 提取各组成部分
$timestampBin = substr($binary, 0, 42);
$datacenterIdBin = substr($binary, 42, 5);
$workerIdBin = substr($binary, 47, 5);
$sequenceBin = 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,
'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主调用方法
* @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
*/
protected static function getWorkerId()
{
// 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;
}
}
// 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']
]
];
}
}