Snowflake   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 308
Duplicated Lines 0 %

Importance

Changes 12
Bugs 6 Features 0
Metric Value
eloc 105
c 12
b 6
f 0
dl 0
loc 308
rs 9.52
wmc 36

17 Methods

Rating   Name   Duplication   Size   Complexity  
A setDataCenterId() 0 7 3
A setSequenceBits() 0 7 1
A setWorkerIdBits() 0 4 1
A getCacheKey() 0 3 1
A setWorkerId() 0 7 3
A setDataCenterIdBits() 0 4 1
A getData() 0 7 2
A leftShift() 0 3 1
A getSequence() 0 9 1
A info() 0 22 5
A setLastTimestamp() 0 4 1
A getLastTimestamp() 0 4 1
A next() 0 33 5
A __construct() 0 10 1
A timeGen() 0 3 1
A setTwEpoch() 0 17 6
A tilNextMillis() 0 7 2
1
<?php
2
/**
3
 * Snowflake
4
 *
5
 * SnowFlake的结构如下(每部分用-分开):
6
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
7
 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
8
 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
9
 * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
10
 * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
11
 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
12
 * 加起来刚好64位,为一个Long型。
13
 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
14
 *
15
 * @author herry<[email protected]>
16
 * @version 1.0
17
 * @copyright © 2020 MuCTS.com All Rights Reserved.
18
 */
19
20
namespace MuCTS\Laravel\Snowflake;
21
22
use Exception;
23
use Illuminate\Support\Carbon;
24
use Illuminate\Support\Collection;
25
use Illuminate\Support\Facades\Cache;
26
use Illuminate\Support\Str;
27
use MuCTS\Laravel\Snowflake\Exceptions\GenerateException;
28
use MuCTS\Laravel\Snowflake\Exceptions\InvalidArgumentException;
29
30
/**
31
 * Class Snowflake
32
 * @package MuCTS\Laravel\Snowflake
33
 */
34
final class Snowflake
35
{
36
    /** @var Carbon 开始时间截 (2020-01-01) */
37
    private Carbon $twEpoch;
38
39
    /** @var int 机器id所占的位数 */
40
    private int $workerIdBits;
41
42
    /** @var int 数据标识id所占的位数 */
43
    private int $dataCenterIdBits;
44
45
    /** @var int 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
46
    private int $maxWorkerId;
47
48
    /** @var int 支持的最大数据标识id,结果是31 */
49
    private int $maxDataCenterId;
50
51
    /** @var int 序列在id中占的位数 */
52
    private int $sequenceBits;
53
54
    /** @var int 机器ID向左移12位 */
55
    private int $workerIdShift;
56
57
    /** @var int 数据标识id向左移17位(12+5) */
58
    private int $dataCenterIdShift;
59
60
    /** @var int 时间截向左移22位(5+5+12) */
61
    private int $timestampShift;
62
63
    /** @var int 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
64
    private int $sequenceMask;
65
66
    /** @var int 工作机器ID(0~31) */
67
    private int $workerId;
68
69
    /** @var int 数据中心ID(0~31) */
70
    private int $dataCenterId;
71
72
    /**
73
     * 构造函数
74
     * @param array|null $config
75
     * @throws Exception
76
     */
77
    public function __construct(?array $config = null)
78
    {
79
        $config = $config ?? config('snowflake');
80
        $this->setTwEpoch($config['tw_epoch']);
81
        $this->setWorkerIdBits($config['worker_id_bits']);
82
        $this->setDataCenterIdBits($config['data_center_id_bits']);
83
        $this->setSequenceBits($config['sequence_bits']);
84
85
        $this->setWorkerId($config['worker_id']);
86
        $this->setDataCenterId($config['data_center_id']);
87
    }
88
89
    /**
90
     * Set snowflake start epoch carbon
91
     *
92
     * @param Carbon|int|float|string $twEpoch
93
     * @return $this
94
     * @throws InvalidArgumentException
95
     */
96
    public function setTwEpoch($twEpoch): self
97
    {
98
        try {
99
            if (is_numeric($twEpoch)) {
100
                $twEpoch = Carbon::createFromTimestampMs(is_string($twEpoch) ? floatval($twEpoch) : $twEpoch);
101
            } elseif (!($twEpoch instanceof Carbon)) {
102
                $twEpoch = Carbon::parse($twEpoch);
103
            }
104
        } catch (Exception $exception) {
105
            throw new InvalidArgumentException(sprintf('This\'s not a valid datetime format,exception message:%s',
106
                $exception->getMessage()), $exception->getCode(), $exception->getPrevious());
107
        }
108
        if (Carbon::now()->lt($twEpoch)) {
109
            throw new InvalidArgumentException('The time of setting must not be greater than the current time.');
110
        }
111
        $this->twEpoch = $twEpoch;
112
        return $this;
113
    }
114
115
    /**
116
     * Set worker id bits
117
     *
118
     * @param int $workerIdBits
119
     * @return void
120
     */
121
    private function setWorkerIdBits(?int $workerIdBits): void
122
    {
123
        $this->workerIdBits = $workerIdBits ?? 5;
124
        $this->maxWorkerId = -1 ^ (-1 << $this->workerIdBits);
125
    }
126
127
    /**
128
     * Set data center id bits
129
     *
130
     * @param int|null $dataCenterIdBits
131
     * @return void
132
     */
133
    private function setDataCenterIdBits(?int $dataCenterIdBits): void
134
    {
135
        $this->dataCenterIdBits = $dataCenterIdBits ?? 5;
136
        $this->maxDataCenterId = -1 ^ (-1 << $this->dataCenterIdBits);
137
    }
138
139
    /**
140
     * Set sequence bits
141
     *
142
     * @param int|null $sequenceBits
143
     * @return void
144
     */
145
    private function setSequenceBits(?int $sequenceBits): void
146
    {
147
        $this->sequenceBits = $sequenceBits ?? 12;
148
        $this->workerIdShift = $this->sequenceBits;
149
        $this->sequenceMask = -1 ^ (-1 << $this->sequenceBits);
150
        $this->dataCenterIdShift = $this->sequenceBits + $this->workerIdBits;
151
        $this->timestampShift = $this->sequenceBits + $this->workerIdBits + $this->dataCenterIdBits;
152
    }
153
154
    /**
155
     * Set worker id
156
     *
157
     * @param int|null $workerId
158
     * @return $this
159
     * @throws Exception
160
     */
161
    public function setWorkerId(?int $workerId): self
162
    {
163
        $this->workerId = $workerId ?? 1;
164
        if ($this->workerId > $this->maxWorkerId || $this->workerId < 0) {
165
            throw new InvalidArgumentException(sprintf('worker Id can\'t be greater than %d or less than 0', $this->maxWorkerId));
166
        }
167
        return $this;
168
    }
169
170
    /**
171
     * Set data center id
172
     *
173
     * @param int|null $dataCenterId
174
     * @return $this
175
     * @throws Exception
176
     */
177
    public function setDataCenterId(?int $dataCenterId): self
178
    {
179
        $this->dataCenterId = $dataCenterId ?? 1;
180
        if ($this->dataCenterId > $this->maxDataCenterId || $this->dataCenterId < 0) {
181
            throw new InvalidArgumentException(sprintf('data center Id can\'t be greater than %d or less than 0', $this->maxDataCenterId));
182
        }
183
        return $this;
184
    }
185
186
    /**
187
     * get Cache key
188
     *
189
     * @param mixed $param
190
     * @return string
191
     */
192
    private function getCacheKey($param): string
193
    {
194
        return 'mcts:sf:' . substr(md5(json_encode([__CLASS__, $param, $this->dataCenterId, $this->workerId])), -5);
195
    }
196
197
    /**
198
     * 获得下一个ID (该方法是线程安全的)
199
     * @return string
200
     * @throws Exception
201
     */
202
    public function next(): string
203
    {
204
        $id = Cache::lock($this->getCacheKey('lock'))->get(function () {
205
            $timestamp = $this->timeGen();
206
207
            // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
208
            if ($timestamp->lt($this->getLastTimestamp())) {
209
                throw new GenerateException(
210
                    sprintf("Clock moved backwards.  Refusing to generate id for %d milliseconds", $this->getLastTimestamp()->diffInMilliseconds($timestamp)));
211
            }
212
213
            // 如果是同一时间生成的,则进行毫秒内序列
214
            $sequence = $this->getSequence($timestamp);
215
            // 毫秒内序列溢出,阻塞到下一个毫秒,获得新的时间戳
216
            while ($timestamp->eq($this->getLastTimestamp()) && $sequence == 0) {
217
                $timestamp = $this->tilNextMillis($timestamp);
218
                $sequence = $this->getSequence($timestamp);
219
            }
220
221
            //上次生成ID的时间截
222
            $this->setLastTimestamp($timestamp);
223
224
            $gmpTimestamp = gmp_init($this->leftShift($timestamp->diffInMilliseconds($this->twEpoch), $this->timestampShift));
225
            $gmpDataCenterId = gmp_init($this->leftShift($this->dataCenterId, $this->dataCenterIdShift));
226
            $gmpWorkerId = gmp_init($this->leftShift($this->workerId, $this->workerIdShift));
227
            $gmpSequence = gmp_init($sequence);
228
229
            return gmp_strval(gmp_or(gmp_or(gmp_or($gmpTimestamp, $gmpDataCenterId), $gmpWorkerId), $gmpSequence));
230
        });
231
        if (!is_string($id)) {
232
            throw new GenerateException('Failure to generate snowflakes id due to concurrent locking.');
233
        }
234
        return $id;
235
    }
236
237
    /**
238
     * 解析ID信息
239
     *
240
     * @param string $snowflakeId
241
     * @return Collection
242
     * @throws InvalidArgumentException
243
     */
244
    public function info(string $snowflakeId): Collection
245
    {
246
        if (strlen($snowflakeId) == 0 || !is_numeric($snowflakeId) || strpos($snowflakeId, '.')) {
247
            throw new InvalidArgumentException(sprintf('%s is not a valid snowflake id', $snowflakeId));
248
        }
249
        $snowflakeId = gmp_strval($snowflakeId, 2);
250
        $len = strlen($snowflakeId);
251
        $items = collect([
252
            'snowflake_id' => 0,
253
            'sequence' => 0,
254
            'worker_id' => 0,
255
            'data_center_id' => 0,
256
            'timestamp' => 0
257
        ])->map(function ($value, $key) use ($len, $snowflakeId) {
258
            $shift = $this->getData(Str::camel($key) . 'Shift', 0);
259
            if ($len < $shift) {
260
                throw new InvalidArgumentException(sprintf('\'%s\' is not valid snowflake id.', strval('0b' . $snowflakeId)));
261
            }
262
            $bits = $this->getData(Str::camel($key) . 'Bits', $len - $shift);
263
            return gmp_intval(gmp_init('0b' . substr($snowflakeId, $len - $shift - $bits ?? 0, $bits)));
264
        });
265
        return $items->put('datetime', $this->twEpoch->clone()->addMilliseconds($items->get('timestamp', 0)));
266
    }
267
268
    /**
269
     * 获取配置信息
270
     *
271
     * @param string $name
272
     * @param int $default
273
     * @return int
274
     */
275
    private function getData(string $name, int $default = 0): int
276
    {
277
        $name = Str::camel($name);
278
        if (property_exists($this, $name)) {
279
            return intval($this->{$name});
280
        }
281
        return $default;
282
    }
283
284
    /**
285
     * 上次生成ID的时间截
286
     */
287
    private function getLastTimestamp(): ?Carbon
288
    {
289
        return Cache::rememberForever($this->getCacheKey('last timestamp'), function () {
290
            return null;
291
        });
292
    }
293
294
    private function setLastTimestamp(Carbon $timestamp): self
295
    {
296
        Cache::forever($this->getCacheKey('last timestamp'), $timestamp);
297
        return $this;
298
    }
299
300
    /**
301
     * 毫秒内序列(0~4095)
302
     * @param Carbon $tts
303
     * @return int
304
     */
305
    private function getSequence(Carbon $tts): int
306
    {
307
        $key = $this->getCacheKey([$tts->timestamp, $tts->millisecond]);
308
        $sequence = Cache::remember($key, 1, function () {
309
            return -1;
310
        });
311
        $sequence = ($sequence + 1) & $this->sequenceMask;
312
        Cache::put($key, $sequence, 1);
313
        return $sequence;
314
    }
315
316
    /**
317
     * 阻塞到下一个毫秒,直到获得新的时间戳
318
     * @param Carbon $lastTimestamp 上次生成ID的时间截
319
     * @return Carbon 当前时间戳
320
     */
321
    protected function tilNextMillis(Carbon $lastTimestamp): Carbon
322
    {
323
        $timestamp = $this->timeGen();
324
        while ($timestamp->lte($lastTimestamp)) {
325
            $timestamp = $this->timeGen();
326
        }
327
        return $timestamp;
328
    }
329
330
    /**
331
     * 返回以毫秒为单位的当前时间
332
     * @return Carbon 当前时间(毫秒)
333
     */
334
    protected function timeGen(): Carbon
335
    {
336
        return Carbon::now();
337
    }
338
339
    protected function leftShift(int $a, int $b): string
340
    {
341
        return bcmul($a, bcpow(2, $b));
342
    }
343
}