.editorconfig 0000644 00000000521 14713400444 0007220 0 ustar 00 root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = false
[*.{vue,js,scss}]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false LICENSE 0000644 00000002051 14713400444 0005550 0 ustar 00 MIT License
Copyright (c) 2020 godruoyi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
README-zh_CN.md 0000644 00000012641 14713400444 0007027 0 ustar 00
An ID Generator for PHP based on Snowflake Algorithm (Twitter announced).
## 说明
雪花算法的 PHP 实现

Snowflake 是 Twitter 内部的一个 ID 生算法,可以通过一些简单的规则保证在大规模分布式情况下生成唯一的 ID 号码。其组成为:
* 第一个 bit 为未使用的符号位。
* 第二部分由 41 位的时间戳(毫秒)构成,他的取值是当前时间相对于某一时间的偏移量。
* 第三部分和第四部分的 5 个 bit 位表示数据中心和机器ID,其能表示的最大值为 2^5 -1 = 31。
* 最后部分由 12 个 bit 组成,其表示每个工作节点**每毫秒**生成的序列号 ID,同一毫秒内最多可生成 2^12 -1 即 4095 个 ID。
需要注意的是:
* 在分布式环境中,5 个 bit 位的 datacenter 和 worker 表示最多能部署 31 个数据中心,每个数据中心最多可部署 31 台节点。
* 41 位的二进制长度最多能表示 2^41 -1 毫秒即 69 年,所以雪花算法最多能正常使用 69 年,为了能最大限度的使用该算法,你应该为其指定一个开始时间。
> 由上可知,雪花算法生成的 ID 并不能保证唯一,如当两个不同请求同一时刻进入相同的数据中心的相同节点时,而此时该节点生成的 sequence 又是相同时,就会导致生成的 ID 重复。
所以要想使用雪花算法生成唯一的 ID,就需要保证同一节点同一毫秒内生成的序列号是唯一的。基于此,我们在 SDK 中集成了多种序列号提供者:
* RandomSequenceResolver(随机生成)
* RedisSequenceResolver (基于 redis psetex 和 incrby 生成)
* PredisSequenceResolver (基于 redis psetex 和 incrby 生成)
* LaravelSequenceResolver(基于 redis psetex 和 incrby 生成)
* SwooleSequenceResolver(基于 swoole_lock 锁)
* FileLockResolver(基于 PHP 文件锁)
> **Warning**
> RandomSequenceResolver 序列号提供者在高并发情况下可能会导致生成的 ID 重复,如果你的应用场景中可能会出现高并发的情况,建议使用 RedisSequenceResolver 或者 LaravelSequenceResolver。
## 要求
1. PHP >= 8.1
2. **[Composer](https://getcomposer.org/)**
## 安装
```shell
$ composer require godruoyi/php-snowflake -vvv
```
## 使用
1. 简单使用.
```php
$snowflake = new \Godruoyi\Snowflake\Snowflake;
$snowflake->id();
// 1537200202186752
```
2. 指定数据中心ID及机器ID.
```php
$snowflake = new \Godruoyi\Snowflake\Snowflake($datacenterId, $workerId);
$snowflake->id();
```
3. 指定开始时间.
```php
$snowflake = new \Godruoyi\Snowflake\Snowflake;
$snowflake->setStartTimeStamp(strtotime('2019-08-08')*1000);
$snowflake->id();
```
## 高级
1. 在 Laravel 中使用
因为 SDK 相对简单,我们并没有提供 Laravel 的扩展包,你可通过下面的方式快速集成到 Laravel 中。
```php
// App\Providers\AppServiceProvider
use Godruoyi\Snowflake\Snowflake;
use Godruoyi\Snowflake\LaravelSequenceResolver;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton('snowflake', function ($app) {
return (new Snowflake())
->setStartTimeStamp(strtotime('2019-10-10')*1000)
->setSequenceResolver(new LaravelSequenceResolver($app->get('cache.store')));
});
}
}
}
```
2. 自定义序列号解决器
你可以通过实现 Godruoyi\Snowflake\SequenceResolver 接口来自定义序列号解决器。
```php
class YourSequence implements SequenceResolver
{
/**
* {@inheritdoc}
*/
public function sequence(int $currentTime)
{
// Just test.
return mt_rand(0, 1);
}
}
// usage
$snowflake->setSequenceResolver(new YourSequence);
$snowflake->id();
```
你也可以直接使用闭包:
```php
$snowflake = new \Godruoyi\Snowflake\Snowflake;
$snowflake->setSequenceResolver(function ($currentTime) {
static $lastTime;
static $sequence;
if ($lastTime == $currentTime) {
++$sequence;
} else {
$sequence = 0;
}
$lastTime = $currentTime;
return $sequence;
})->id();
```
## License
MIT
README.md 0000644 00000013223 14713400444 0006025 0 ustar 00
An ID Generator for PHP based on Snowflake Algorithm (Twitter announced).
## Description
Snowflake & Sonyflake algorithm PHP implementation [中文文档](https://github.com/godruoyi/php-snowflake/blob/master/README-zh_CN.md).

Snowflake is a network service that generates unique ID numbers at high scale with simple guarantees.
1. The first bit is an unused sign bit.
2. The second part consists of a 41-bit timestamp (in milliseconds) representing the offset of the current time relative to a certain reference time.
3. The third and fourth parts are represented by 5 bits each, indicating the data centerID and workerID. The maximum value for both is 31 (2^5 -1).
4. The last part consists of 12 bits, which represents the length of the serial number generated per millisecond per working node. A maximum of 4095 IDs can be generated in the same millisecond (2^12 -1).
If you want to generate unique IDs using the snowflake algorithm, you must ensure that sequence numbers generated within the same millisecond on the same node are unique.
Based on this requirement, we have created this package which integrates multiple sequence number providers.
* RandomSequenceResolver (Random Sequence Number, UnSafe)
* FileLockResolver (Uses PHP file lock `fopen/flock`, **Concurrency Safety**)
* RedisSequenceResolver (Redis psetex and incrby, **Concurrency safety**)
* PredisSequenceResolver (redis psetex and incrby, **Concurrency Safety**)
* LaravelSequenceResolver (Laravel Cache [add](https://github.com/laravel/framework/blob/11.x/src/Illuminate/Contracts/Cache/Repository.php#L39) lock mechanism)
* SwooleSequenceResolver (swoole_lock for **Concurrency Safety**)
## Requirement
1. PHP >= 8.1
2. **[Composer](https://getcomposer.org/)**
## Installation
```shell
$ composer require godruoyi/php-snowflake -vvv
# Install `predis/predis` package if you are using PredisSequenceResolver
$ composer require "predis/predis"
# Install `Redis` extensions if you are using RedisSequenceResolver
$ pecl install redis
# Install `Swoole` extensions if you are using SwooleSequenceResolver
$ pecl install swoole
```
## Usage
1. simple to use.
```php
$snowflake = new \Godruoyi\Snowflake\Snowflake;
$snowflake->id();
// 1537200202186752
```
2. Specify the data center ID and machine ID.
```php
$snowflake = new \Godruoyi\Snowflake\Snowflake($datacenterId, $workerId);
$snowflake->id();
```
3. Specify start time.
```php
$snowflake = new \Godruoyi\Snowflake\Snowflake;
$snowflake->setStartTimeStamp(strtotime('2019-09-09')*1000); // millisecond
$snowflake->id();
```
> The maximum value of a 41-bit timestamp (in milliseconds) can represent up to 69 years, so the Snowflake algorithm can run safely for 69 years. In order to make the most of it, we recommend setting a start time.
4. Use Sonyflake
```php
$sonyflake = new \Godruoyi\Snowflake\Sonyflake;
$sonyflake->id();
```
## Advanced
1. Used in Laravel.
Since the SDK is quite straightforward, we do not offer a specific extension for Laravel. However, you can easily integrate it into your Laravel project by following these steps.
```php
// App\Providers\AppServiceProvider
use Godruoyi\Snowflake\Snowflake;
use Godruoyi\Snowflake\LaravelSequenceResolver;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton('snowflake', function ($app) {
return (new Snowflake())
->setStartTimeStamp(strtotime('2019-10-10')*1000)
->setSequenceResolver(new LaravelSequenceResolver($app->get('cache.store')));
});
}
}
```
2. Custom
To customize the sequence number resolver, you need to implement the Godruoyi\Snowflake\SequenceResolver interface.
```php
class YourSequence implements SequenceResolver
{
/**
* {@inheritdoc}
*/
public function sequence(int $currentMillisecond)
{
// Just test.
return mt_rand(0, 1);
}
}
// usage
$snowflake->setSequenceResolver(new YourSequence);
$snowflake->id();
```
And you also can use the Closure:
```php
$snowflake = new \Godruoyi\Snowflake\Snowflake;
$snowflake->setSequenceResolver(function ($currentMillisecond) {
static $lastTime;
static $sequence;
if ($lastTime == $currentMillisecond) {
++$sequence;
} else {
$sequence = 0;
}
$lastTime = $currentMillisecond;
return $sequence;
})->id();
```
## License
MIT
composer.json 0000644 00000002445 14713400444 0007274 0 ustar 00 {
"name": "godruoyi/php-snowflake",
"description": "An ID Generator for PHP based on Snowflake Algorithm (Twitter announced).",
"license": "MIT",
"keywords": [
"unique id",
"snowflake algorithm",
"php snowflake",
"laravel snowflake",
"order id",
"unique order id",
"php unique id",
"sonyflake",
"php sonyflake"
],
"homepage": "https://github.com/godruoyi/php-snowflake",
"authors": [{
"name": "Godruoyi",
"email": "g@godruoyi.com"
}],
"require": {
"php-64bit": ">=8.1"
},
"require-dev": {
"phpunit/phpunit": "^10",
"laravel/pint": "^1.10",
"phpstan/phpstan": "^1.10",
"illuminate/contracts": "^10.0 || ^11.0",
"predis/predis": "^2.0",
"ext-redis": "*",
"ext-swoole": "*"
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests"
}
},
"autoload": {
"psr-4": {
"Godruoyi\\Snowflake\\": "src"
}
},
"scripts": {
"test": "vendor/bin/phpunit",
"phpstan": "vendor/bin/phpstan",
"pint": "vendor/bin/pint --config pint.json"
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
}
}
src/FileLockResolver.php 0000644 00000015763 14713400444 0011273 0 ustar 00
*
* This source file is subject to the MIT license that is bundled.
*/
namespace Godruoyi\Snowflake;
use Throwable;
class FileLockResolver implements SequenceResolver
{
/**
* We should always use exclusive lock to avoid the problem of concurrent access.
*/
public const FlockLockOperation = LOCK_EX;
public const FileOpenMode = 'r+';
/**
* For each lock file, we save 6,000 items, It can contain data generated within 10 minutes,
* we believe is sufficient for the snowflake algorithm.
*
* 10m = 600s = 6000 ms
*/
public static int $maxItems = 6000;
public static int $shardCount = 32;
/**
* @throws SnowflakeException
*/
public function __construct(protected string $lockFileDir)
{
$this->lockFileDir = $this->preparePath($lockFileDir);
}
/**
* @throws SnowflakeException
*/
public function sequence(int $currentTime): int
{
$filePath = $this->createShardLockFile($this->getShardLockIndex($currentTime));
return $this->getSequence($filePath, $currentTime);
}
/**
* Get next sequence. move lock/unlock in the same method to avoid lock file not release, this
* will be more friendly to test.
*
* @throws SnowflakeException
*/
protected function getSequence(string $filePath, int $currentTime): int
{
$f = null;
if (! file_exists($filePath)) {
throw new SnowflakeException(sprintf('the lock file %s not exists', $filePath));
}
try {
$f = @fopen($filePath, static::FileOpenMode);
if (! $f) {
throw new SnowflakeException(sprintf('can not open this file %s', $filePath));
}
// we always use exclusive lock to avoid the problem of concurrent access.
// so we don't need to check the return value of flock.
flock($f, static::FlockLockOperation);
} catch (Throwable $e) {
$this->unlock($f);
throw new SnowflakeException(sprintf('can not open/lock this file %s', $filePath), $e->getCode(), $e);
}
// We may get this error if the file contains invalid json, when you get this error,
// may you can try to delete the invalid lock file directly.
if (is_null($contents = $this->getContents($f))) {
$this->unlock($f);
throw new SnowflakeException(sprintf('file %s is not a valid lock file.', $filePath));
}
$this->updateContents($contents = $this->incrementSequenceWithSpecifyTime(
$this->cleanOldSequences($contents), $currentTime
), $f);
$this->unlock($f);
return $contents[$currentTime];
}
/**
* Unlock and close file.
*
* @param resource|false|null $f
*/
protected function unlock($f): void
{
if (is_resource($f)) {
flock($f, LOCK_UN);
fclose($f);
}
}
/**
* @param array $contents
* @param resource $f
*/
public function updateContents(array $contents, $f): bool
{
return ftruncate($f, 0) && rewind($f)
&& (fwrite($f, serialize($contents)) !== false);
}
/**
* Increment sequence with specify time. if current time is not set in the lock file
* set it to 1, otherwise increment it.
*
* @param array $contents
* @return array
*/
public function incrementSequenceWithSpecifyTime(array $contents, int $currentTime): array
{
$contents[$currentTime] = isset($contents[$currentTime]) ? $contents[$currentTime] + 1 : 1;
return $contents;
}
/**
* Clean the old content, we only save the data generated within 10 minutes.
*
* @param array $contents
* @return array
*/
public function cleanOldSequences(array $contents): array
{
ksort($contents); // sort by timestamp
if (count($contents) > static::$maxItems) {
$contents = array_slice($contents, -static::$maxItems, null, true);
}
return $contents;
}
/**
* Remove all lock files, we only delete the file that name is match the pattern.
*/
public function cleanAllLocksFile(): void
{
$files = glob($this->lockFileDir.'/*');
if (! $files) {
return;
}
foreach ($files as $file) {
if (is_file($file) && preg_match('/snowflake-(\d+)\.lock$/', $file)) {
unlink($file);
}
}
}
/**
* Get resource contents, If the contents are invalid json, return null.
*
* @param resource $f
* @return array|null
*/
public function getContents($f): ?array
{
$content = '';
while (! feof($f)) {
$content .= fread($f, 1024);
}
$content = trim($content);
if (empty($content)) {
return [];
}
try {
if (is_array($data = @unserialize($content))) {
return $data;
}
} catch (Throwable) {
}
return null;
}
/**
* @see https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
*/
public function fnv(string $str): float
{
$hash = 2166136261;
$length = strlen($str);
for ($i = 0; $i < $length; $i++) {
$hash ^= ord($str[$i]);
$hash *= 0x01000193;
$hash &= 0xFFFFFFFF;
}
return $hash;
}
/**
* Shard lock file index.
*/
public function getShardLockIndex(int $currentTime): int
{
return $this->fnv((string) $currentTime) % self::$shardCount;
}
/**
* Check path is exists and writable.
*
* @throws SnowflakeException
*/
protected function preparePath(string $lockFileDir): string
{
if (! is_dir($lockFileDir)) {
throw new SnowflakeException("{$lockFileDir} is not a directory.");
}
if (! is_writable($lockFileDir)) {
throw new SnowflakeException("{$lockFileDir} is not writable.");
}
return $lockFileDir;
}
/**
* Generate shard lock file.
*
* @throws SnowflakeException
*/
protected function createShardLockFile(int $index): string
{
$path = $this->filePath($index);
if (file_exists($path)) {
return $path;
}
$f = fopen($path, 'a');
if (! $f) {
throw new SnowflakeException(sprintf('can not create lock file %s', $path));
}
$this->unlock($f);
return $path;
}
/**
* Format lock file path with shard index.
*/
protected function filePath(int $index): string
{
return sprintf('%s%ssnowflake-%s.lock', rtrim($this->lockFileDir, DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR, $index);
}
}
src/LaravelSequenceResolver.php 0000644 00000001623 14713400444 0012650 0 ustar 00
*
* This source file is subject to the MIT license that is bundled.
*/
namespace Godruoyi\Snowflake;
use Illuminate\Contracts\Cache\Repository;
class LaravelSequenceResolver implements SequenceResolver
{
/**
* The cache prefix.
*/
protected string $prefix = '';
/**
* Init resolve instance, must be connected.
*/
public function __construct(protected Repository $cache)
{
}
public function sequence(int $currentTime): int
{
$key = $this->prefix.$currentTime;
if ($this->cache->add($key, 1, 10)) {
return 0;
}
return $this->cache->increment($key) | 0;
}
public function setCachePrefix(string $prefix): self
{
$this->prefix = $prefix;
return $this;
}
}
src/PredisSequenceResolver.php 0000644 00000002114 14713400444 0012504 0 ustar 00
*
* This source file is subject to the MIT license that is bundled.
*/
namespace Godruoyi\Snowflake;
use Predis\Client as PredisClient;
class PredisSequenceResolver implements SequenceResolver
{
/**
* The cache prefix.
*/
protected string $prefix = '';
/**
* The default redis lua script
*/
protected static string $script = <<<'LUA'
if redis.call('set', KEYS[1], ARGV[1], "EX", ARGV[2], "NX") then
return 0
else
return redis.call('incr', KEYS[1])
end
LUA;
public function __construct(protected PredisClient $predisClient)
{
}
public function sequence(int $currentTime): int
{
return $this->predisClient->eval(self::$script, 1, $this->prefix.$currentTime, '0', '10') | 0;
}
/**
* Set cache prefix.
*/
public function setCachePrefix(string $prefix): self
{
$this->prefix = $prefix;
return $this;
}
}
src/RandomSequenceResolver.php 0000644 00000002142 14713400444 0012477 0 ustar 00
*
* This source file is subject to the MIT license that is bundled.
*/
namespace Godruoyi\Snowflake;
use Exception;
class RandomSequenceResolver implements SequenceResolver
{
/**
* The last timestamp.
*/
protected int $lastTimeStamp = -1;
/**
* The sequence.
*/
protected int $sequence = 0;
/**
* Max sequence number in single ms.
*/
protected int $maxSequence = Snowflake::MAX_SEQUENCE_SIZE;
/**
* @throws Exception
*/
public function sequence(int $currentTime): int
{
if ($this->lastTimeStamp === $currentTime) {
$this->sequence++;
$this->lastTimeStamp = $currentTime;
return $this->sequence;
}
$this->sequence = random_int(0, $this->maxSequence);
$this->lastTimeStamp = $currentTime;
return $this->sequence;
}
public function setMaxSequence(int $maxSequence): void
{
$this->maxSequence = $maxSequence;
}
}
src/RedisSequenceResolver.php 0000644 00000002510 14713400444 0012324 0 ustar 00
*
* This source file is subject to the MIT license that is bundled.
*/
namespace Godruoyi\Snowflake;
use Redis;
use RedisException;
class RedisSequenceResolver implements SequenceResolver
{
/**
* The cache prefix.
*/
protected string $prefix = '';
/**
* The default redis lua script
*/
protected static string $script = <<<'LUA'
if redis.call('set', KEYS[1], ARGV[1], "EX", ARGV[2], "NX") then
return 0
else
return redis.call('incr', KEYS[1])
end
LUA;
/**
* Init resolve instance, must be connected.
*
* @throws RedisException
*/
public function __construct(protected Redis $redis)
{
if (! $redis->ping()) {
throw new RedisException('Redis server went away');
}
}
/**
* @throws RedisException
*/
public function sequence(int $currentTime): int
{
// 10 seconds
return $this->redis->eval(self::$script, [$this->prefix.$currentTime, '0', '10'], 1) | 0;
}
/**
* Set cache prefix.
*/
public function setCachePrefix(string $prefix): self
{
$this->prefix = $prefix;
return $this;
}
}
src/SequenceResolver.php 0000644 00000000472 14713400444 0011342 0 ustar 00
*
* This source file is subject to the MIT license that is bundled.
*/
namespace Godruoyi\Snowflake;
interface SequenceResolver
{
public function sequence(int $currentTime): int;
}
src/Snowflake.php 0000644 00000012301 14713400444 0007773 0 ustar 00
*
* This source file is subject to the MIT license that is bundled.
*/
namespace Godruoyi\Snowflake;
use Closure;
class Snowflake
{
public const MAX_TIMESTAMP_LENGTH = 41;
public const MAX_DATACENTER_LENGTH = 5;
public const MAX_WORKID_LENGTH = 5;
public const MAX_SEQUENCE_LENGTH = 12;
public const MAX_SEQUENCE_SIZE = (-1 ^ (-1 << self::MAX_SEQUENCE_LENGTH));
/**
* The data center id.
*/
protected int $datacenter;
/**
* The worker id.
*/
protected int $workerId;
/**
* The Sequence Resolver instance.
*/
protected SequenceResolver|null|Closure $sequence = null;
/**
* The start timestamp.
*/
protected ?int $startTime = null;
/**
* Default sequence resolver.
*/
protected ?SequenceResolver $defaultSequenceResolver = null;
/**
* Build Snowflake Instance.
*/
public function __construct(int $datacenter = 0, int $workerId = 0)
{
$maxDataCenter = -1 ^ (-1 << self::MAX_DATACENTER_LENGTH);
$maxWorkId = -1 ^ (-1 << self::MAX_WORKID_LENGTH);
// If not set datacenter or workid, we will set a default value to use.
$this->datacenter = $datacenter > $maxDataCenter || $datacenter < 0 ? random_int(0, 31) : $datacenter;
$this->workerId = $workerId > $maxWorkId || $workerId < 0 ? random_int(0, 31) : $workerId;
}
/**
* Get snowflake id.
*/
public function id(): string
{
$currentTime = $this->getCurrentMillisecond();
while (($sequence = $this->callResolver($currentTime)) > (-1 ^ (-1 << self::MAX_SEQUENCE_LENGTH))) {
usleep(1);
$currentTime = $this->getCurrentMillisecond();
}
$workerLeftMoveLength = self::MAX_SEQUENCE_LENGTH;
$datacenterLeftMoveLength = self::MAX_WORKID_LENGTH + $workerLeftMoveLength;
$timestampLeftMoveLength = self::MAX_DATACENTER_LENGTH + $datacenterLeftMoveLength;
return (string) ((($currentTime - $this->getStartTimeStamp()) << $timestampLeftMoveLength)
| ($this->datacenter << $datacenterLeftMoveLength)
| ($this->workerId << $workerLeftMoveLength)
| ($sequence));
}
/**
* Parse snowflake id.
*
* @return array
*/
public function parseId(string $id, bool $transform = false): array
{
$id = decbin((int) $id);
$data = [
'timestamp' => substr($id, 0, -22),
'sequence' => substr($id, -12),
'workerid' => substr($id, -17, 5),
'datacenter' => substr($id, -22, 5),
];
return $transform ? array_map(static function ($value) {
return bindec($value);
}, $data) : $data;
}
/**
* Get current millisecond time.
*/
public function getCurrentMillisecond(): int
{
return floor(microtime(true) * 1000) | 0;
}
/**
* Set start time (millisecond).
*
* @throws SnowflakeException
*/
public function setStartTimeStamp(int $millisecond): self
{
$missTime = $this->getCurrentMillisecond() - $millisecond;
if ($missTime < 0) {
throw new SnowflakeException('The start time cannot be greater than the current time');
}
$maxTimeDiff = -1 ^ (-1 << self::MAX_TIMESTAMP_LENGTH);
if ($missTime > $maxTimeDiff) {
throw new SnowflakeException(sprintf('The current microtime - starttime is not allowed to exceed -1 ^ (-1 << %d), You can reset the start time to fix this', self::MAX_TIMESTAMP_LENGTH));
}
$this->startTime = $millisecond;
return $this;
}
/**
* Get start timestamp (millisecond), If not set default to 2019-08-08 08:08:08.
*/
public function getStartTimeStamp(): float|int
{
if (! is_null($this->startTime)) {
return $this->startTime;
}
// We set a default start time if you not set.
$defaultTime = '2019-08-08 08:08:08';
return strtotime($defaultTime) * 1000;
}
/**
* Set Sequence Resolver.
*/
public function setSequenceResolver(Closure|SequenceResolver $sequence): self
{
$this->sequence = $sequence;
return $this;
}
/**
* Get Sequence Resolver.
*/
public function getSequenceResolver(): null|Closure|SequenceResolver
{
return $this->sequence;
}
/**
* Get Default Sequence Resolver.
*/
public function getDefaultSequenceResolver(): SequenceResolver
{
return $this->defaultSequenceResolver ?: $this->defaultSequenceResolver = new RandomSequenceResolver();
}
/**
* Call resolver.
*/
protected function callResolver(int $currentTime): int
{
$resolver = $this->getSequenceResolver();
if (! is_null($resolver) && is_callable($resolver)) {
return $resolver($currentTime);
}
return ! ($resolver instanceof SequenceResolver)
? $this->getDefaultSequenceResolver()->sequence($currentTime)
: $resolver->sequence($currentTime);
}
}
src/SnowflakeException.php 0000644 00000000445 14713400444 0011660 0 ustar 00
*
* This source file is subject to the MIT license that is bundled.
*/
namespace Godruoyi\Snowflake;
use Exception;
class SnowflakeException extends Exception
{
}
src/Sonyflake.php 0000644 00000007502 14713400444 0010004 0 ustar 00
*
* This source file is subject to the MIT license that is bundled.
*/
namespace Godruoyi\Snowflake;
class Sonyflake extends Snowflake
{
public const MAX_TIMESTAMP_LENGTH = 39;
public const MAX_MACHINEID_LENGTH = 16;
public const MAX_SEQUENCE_LENGTH = 8;
public const MAX_SEQUENCE_SIZE = (-1 ^ (-1 << self::MAX_SEQUENCE_LENGTH));
/**
* Build Sonyflake Instance.
*
* @param int $machineId machine ID 0 ~ 65535 (2^16)-1
*/
public function __construct(protected int $machineId = 0)
{
$maxMachineID = -1 ^ (-1 << self::MAX_MACHINEID_LENGTH);
if ($this->machineId < 0 || $this->machineId > $maxMachineID) {
throw new \InvalidArgumentException("Invalid machine ID, must be between 0 ~ {$maxMachineID}.");
}
}
/**
* Get Sonyflake id.
*
* @throws SnowflakeException
*/
public function id(): string
{
$elapsedTime = $this->elapsedTime();
while (($sequence = $this->callResolver($elapsedTime)) > (-1 ^ (-1 << self::MAX_SEQUENCE_LENGTH))) {
$nextMillisecond = $this->elapsedTime();
while ($nextMillisecond === $elapsedTime) {
usleep(1);
$nextMillisecond = $this->elapsedTime();
}
$elapsedTime = $nextMillisecond;
}
$this->ensureEffectiveRuntime($elapsedTime);
return (string) ($elapsedTime << (self::MAX_MACHINEID_LENGTH + self::MAX_SEQUENCE_LENGTH)
| ($this->machineId << self::MAX_SEQUENCE_LENGTH)
| ($sequence));
}
/**
* Set start time (millisecond).
*
* @throws SnowflakeException
*/
public function setStartTimeStamp(int $millisecond): self
{
$elapsedTime = floor(($this->getCurrentMillisecond() - $millisecond) / 10) | 0;
if ($elapsedTime < 0) {
throw new SnowflakeException('The start time cannot be greater than the current time');
}
$this->ensureEffectiveRuntime($elapsedTime);
$this->startTime = $millisecond;
return $this;
}
/**
* Parse snowflake id.
*
* @return array
*/
public function parseId(string $id, bool $transform = false): array
{
$id = decbin((int) $id);
$length = self::MAX_SEQUENCE_LENGTH + self::MAX_MACHINEID_LENGTH;
$data = [
'sequence' => substr($id, -1 * self::MAX_SEQUENCE_LENGTH),
'machineid' => substr($id, -1 * $length, self::MAX_MACHINEID_LENGTH),
'timestamp' => substr($id, 0, strlen($id) - $length),
];
return $transform ? array_map(static function ($value) {
return bindec($value);
}, $data) : $data;
}
/**
* Get current timestamp.
*/
public function getDefaultSequenceResolver(): SequenceResolver
{
if ($this->defaultSequenceResolver) {
return $this->defaultSequenceResolver;
}
$resolver = new RandomSequenceResolver();
$resolver->setMaxSequence(self::MAX_SEQUENCE_SIZE);
return $this->defaultSequenceResolver = $resolver;
}
/**
* The Elapsed Time, unit: 10millisecond.
*/
private function elapsedTime(): int
{
return floor(($this->getCurrentMillisecond() - $this->getStartTimeStamp()) / 10) | 0;
}
/**
* Make sure it's an effective runtime
*
* @throws SnowflakeException
*/
private function ensureEffectiveRuntime(int $elapsedTime): void
{
$maxRunTime = -1 ^ (-1 << self::MAX_TIMESTAMP_LENGTH);
if ($elapsedTime > $maxRunTime) {
throw new SnowflakeException('Exceeding the maximum life cycle of the algorithm');
}
}
}
src/SwooleSequenceResolver.php 0000644 00000003135 14713400444 0012532 0 ustar 00
*
* This source file is subject to the MIT license that is bundled.
*/
namespace Godruoyi\Snowflake;
class SwooleSequenceResolver implements SequenceResolver
{
/**
* The las ttimestamp.
*/
protected ?int $lastTimeStamp = -1;
/**
* The sequence.
*/
protected int $sequence = 0;
/**
* The swoole lock.
*/
protected \Swoole\Lock $lock;
/**
* The cycle count.
*/
protected int $count = 0;
/**
* Init swoole lock.
*/
public function __construct()
{
$this->lock = new \Swoole\Lock(SWOOLE_MUTEX);
}
/**
* @throws SnowflakeException
*/
public function sequence(int $currentTime): int
{
// If swoole lock failure,we will return a big number, and recall this method when next millisecond.
if (! $this->lock->trylock()) {
if ($this->count >= 10) {
throw new SnowflakeException('Swoole lock failure, Unable to get the program lock after many attempts.');
}
$this->count++;
// return a big number
return 999999;
}
if ($this->lastTimeStamp === $currentTime) {
$this->sequence++;
} else {
$this->sequence = 0;
}
$this->lastTimeStamp = $currentTime;
$this->lock->unlock();
return $this->sequence;
}
public function resetLock(\Swoole\Lock $lock): void
{
$this->lock = $lock;
}
}