dream

一个菜鸟程序员的成长历程

0%

分布式必备发号器

发号器

生成唯一id的需求很多,我们经常会用到,不管是单库单表中的唯一,还是分布式的唯一。

SnowFlake 算法

说一下SnowFlake算法,这个算法是一个生成唯一id的算法。

使用的是一个64位的二进制串,把这个串分成了几个部分。

  • 符号位 占一个位置 0 为正
  • 时间戳位 占41个位置,使用毫秒级时间戳
  • 机器位 占10个位置, 可以支持2的10次方-1个机器使用
  • 序号位 占12个位置, 同一毫秒内可以生成2的12次方-1个id

但是我们的业务很少用到这个级别的发号器,所以可以把时间改为秒级,下面是我改版后的SnowFlake算法组成:

  • 符号位 占一个位置 0 为正
  • 时间戳位 占38个位置,使用秒级时间戳
  • 机器位 占5个位置, 可以支持2的5次方-1个机器使用
  • 业务位 占8个位置, 可以支持2的8次方-1个业务
  • 序号位 占12个位置, 同一毫秒内可以生成2的12次方-1个id

我这里面增加了业务位,因为这样可以把每个业务都分开,保证每个业务每个机器每秒内可以生成1024个id。

看一下我们php的实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<?php

class SnowFlake {

const FIRST = 0; //首位 符号位 表示正负0为正 1为负

const TIME_LENGTH = 38; //时间戳位数 二进制的位数

const MACHINE_LENGTH = 5; //机器码位数 二进制的位数

const BUSINESS_LENGTH = 8; //业务位数 二进制的位数

const SEQUENCE_LENGTH = 12; //序列号位数 二进制的位数

private $machineId = 1;

//上一次发号时间
private $oldTime;

private $sequence;

//业务对应的业务id
private $businessArr = [
'order' => 1,
];

function __construct($machineId = 1)
{
if (strlen(decbin($machineId)) > self::MACHINE_LENGTH) {
return '机器id超长!';
}
$this->machineId = $machineId;
//初始化时间戳
$this->oldTime = $this->getTime();
//初始化序号
$this->sequence = 1;
}

/**
* 生成唯一id
* @param string $businessType 业务类型
*/
function generate($businessType) {
$time = $this->getTime();
//比较时间戳
if ($time == $this->oldTime) {
//在同一毫秒内创建,序号递增
if (strlen(decbin($this->sequence)) >= self::SEQUENCE_LENGTH) {
return '到达最大发号个数';
}
++$this->sequence;
} else {
//到达下一个时间,重置序号
$this->sequence = 1;
}

$businessId = $this->getBusinessId($businessType);

//字符位偏移量
$firstShift = self::TIME_LENGTH + self::MACHINE_LENGTH + self::BUSINESS_LENGTH + self::SEQUENCE_LENGTH;
//时间戳偏移量
$timeShift = self::MACHINE_LENGTH + self::BUSINESS_LENGTH + self::SEQUENCE_LENGTH;
//机器位偏移量
$machineShift = self::BUSINESS_LENGTH + self::SEQUENCE_LENGTH;
//业务偏移量
$businessShift = self::SEQUENCE_LENGTH;

$res = self::FIRST << $firstShift | $time << $timeShift | $this->machineId << $machineShift | $businessId << $businessShift | $this->sequence;

//写入时间
$this->oldTime = $time;
return $res;
}

/**
* 获取毫秒级时间戳
*/
function getTime() {
return time();
}

/**
* 获取业务id
*/
function getBusinessId($businessType) {
return $this->businessArr[$businessType];
}
}

最后一步是把每个部分归位,左移相应的偏移量,到达相应的位置,比如时间戳:

1
1011110100000010111110100010100 << 25 = 10111101000000101111101000101000000000000000000000000000

机器位

1
2
1 << 20 = 100000000000000000000

业务位

1
1 << 12 = 1000000000000

序号位不用左移

最后将这些进行或操作,二进制的或操作就是将每一位进行对比,如果都是0则返回0,有一个1就返回1,两个都是1也返回1。

1
2
3
4
5
6
7
时间戳或上机器位就是这个结果,这也相当于两个加在一起了
10111101000000101111101000101000000000000000000000000000 | 100000000000000000000 = 10111101000000101111101000101000000100000000000000000000

再或上业务位和符号位

10111101000000101111101000101000000100000000000000000000 | 1000000000000 | 1 = 10111101000000101111101000101000000100000010000000000001

最后把每部分或操作之后就是我们要的最终结果,不管是左移操作还是或操作,实际上都是对变量里面的二进制进行的操作,操作之后会转换成我们看到的十进制消息,也就是53202044035534850

在这里面还用到了decbin()这个函数,这个函数的作用就是将十进制转换成二进制。

我们使用这个发号器生成的唯一id隐含了我们的业务性,也保证了唯一性,如果你的业务很庞大,可以使用毫秒级时间戳或者扩大序号位,都可以根据实际使用情况调整。

下面是这个算法的github地址

https://github.com/Thepatterraining/design-pattern/tree/master/app/Http/Models/SnowFlake