php雪花id必须手写类,禁用time()防时钟回拨;需维护$lasttimestamp比较并抛异常,用floor(hrtime(true)/1e6)防浮点误差,确保单机单worker单毫秒内不重复。

PHP 里没有现成的 snowflake 函数,必须手写类;直接用 time() 或 microtime(true) 当时间戳会因时钟回拨导致 ID 重复,这是最常踩的坑。
为什么不能直接用 time() 做时间戳
雪花 ID 的单调递增性完全依赖时间戳段。系统时间一旦回拨(比如 NTP 同步、VM 恢复、手动校时),time() 返回值变小,而内存里的 $sequence 可能还没归零,就会生成和之前完全相同的 timestamp + worker_id + sequence 组合。
- 必须维护一个类属性
$lastTimestamp,每次生成前比较当前毫秒时间 - 若当前时间 ≤
$lastTimestamp,立刻抛异常(不要 sleep 等待——线上服务等不起) - 只在严格递增时更新
$lastTimestamp,并重置$sequence;否则只递增$sequence - 测试时可用
date_default_timezone_set('UTC'); touch /tmp/ntp_test && ntpdate -s time1.aliyun.com模拟回拨
hrtime(true) 是更安全的时间源,但要注意类型转换
PHP 7.3+ 支持 hrtime(true) 返回纳秒级浮点数,精度远高于 time(),但直接 (int)($ts / 1000000) 强转有风险:若浮点数在边界处精度丢失(如 1745158392000.999),取整后可能少 1 毫秒。
- 推荐写法:
$ms = floor($ts / 1000000);,避免截断误差 - 生成 ID 后立即检查是否为正整数:
if (!is_int($id) || $id ,Windows 上 32 位 PHP 容易出负数 - 别用
gmp_add()或bcadd()做拼接——ID 只用于存储和比较,不参与计算,徒增开销
多进程下 $sequence 计数器必然冲突,必须外置或隔离
PHP-FPM 或 pcntl_fork() 场景中,每个 worker 进程都复制了类的静态状态,$sequence 在各进程内独立从 0 开始,同一毫秒极易生成重复 ID。
- 禁止在构造函数里初始化
$sequence = 0—— 必须由外部传入或运行时动态分配 - 低并发可改用文件锁:
fopen('/tmp/sf_seq.lock', 'c')+flock()读写序列号 - 生产环境推荐 Redis:
INCR "snowflake:seq:{$workerId}",并设EXPIRE防堆积 - 若用 Swoole,可在
WorkerStart回调中为每个 worker 分配唯一$workerId并重置$sequence = 0
MySQL 存储和 ORM 集成的关键适配点
64 位雪花 ID 超出 MySQL INT 范围,且 Laravel/Eloquent 默认不接受显式主键值,这两点不处理就会报错或截断。
- 数据库字段必须定义为
BIGINT UNSIGNED,不是BIGINT(避免符号位干扰排序) - Laravel 中需在模型里加
public $incrementing = false;和protected $keyType = 'string'; - 插入时不能用
User::create([...]),得写User::insert(['id' => $id, ...])或先 new 再 save() - 索引性能注意:InnoDB 聚簇索引按主键物理排序,雪花 ID 时间前缀带来局部有序,但
worker_id离散,页分裂比纯自增略高,压测时关注Innodb_buffer_pool_reads
真正难的不是写完算法,而是让 $lastTimestamp 和 $sequence 在各种部署形态(FPM、Swoole、CLI 多进程、容器重启)下始终满足“单机单 worker 单毫秒内不重复”——这个约束比代码逻辑本身更考验对运行时的理解。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











