ThinkPHP6 业务分表之一:UID 发号器

随着面试的要求越来越高,经常出现很小的公司,要求你需要掌握分表分库的能力,虽然我之前负责的业务单表数据量最大高达 5000w+,但我们查询的性能还是很好的,可能是我们的索引和查询的 SQL 做的比较好,命中率非常高,所以我们没有需要分的必要,但为了满足就业市场的需求,学习这样子的一种技能还是存在必要的。

分表分库一般分垂直和水平,垂直是指感觉业务来进行库的拆分,比如专门的用户库或者订单库这样子,但垂直还是无法解决单表数据量过大导致的性能会差的问题(这里可能会和上面矛盾,网上多数指的是 2000W 就会影响,但好像没有多少人谈过他们的表结构情况)。水平分指的是某一个表,里面的数据量非常大,我们按照一定的规则来进行一个拆分分流,比如把用户的数据由一直存放在用户表,变为可能这条数据是在用户1号表或者2号表这样子。规则可能是 范围(range)或者哈希(hash),也不知道我喜欢的取模算不算哈希。分了其实会带来一些问题的复杂性,比如分库那如何确保多库事务性的一致性、分布式锁等等很多,然后还有之前我们的 join 查询,现在可能就无法使用呢。

分之前要先预估好你未来发展可能的一个量,定义好要分的库或者表的一个数据,不然回头还要再二次分就会更加麻烦了,不过又不是说不能跑路。

由于这块的内容会比较多,所以会通过多篇文章来进行发布。

我们现在假设项目是新成立的,暂时没有一个技术债。目前我们要先规划一个用户表,打算划分 16 个表,按照取模的方式来查询。最先可能我们要考虑如何定义 UID 的问题,不过对我们 PHPer 来说不是什么难事,毕竟我们基本都不用 UUID 来做 UID 的,占用的空间会比较多,不利于索引。

但是不用 UUID ,选择了 INT 来做 UID,那我们要如何确保它的连续性和唯一性呢?业内常用的可能是雪花算法,但我选择自己写一个简易的发号器。

发号器需要加东西,然后取东西,我们可以利用 Redis List 来很好的实现我们需要的先进先出功能。通过一个命令或者说脚本,我们定时往列表中填上自增的 ID,然后在注册流程中来取最前面的。

流程图

app/common.php

1
2
3
4
5
6
7
8
9
10

if(!function_exists('get_redis')) {
function get_redis() {
return new \Predis\Client('tcp://IP:端口', [
'parameters' => [
'password' => '密码',
],
]);
}
}

app/command/GenerateUID.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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<?php
declare (strict_types = 1);

namespace app\command;

use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;

class GenerateUID extends Command
{
protected $redis;

/*
* 发号器列表 键
*
* @var string
*/
protected $cache_key = 'generate:uid';

/*
* 锁 键
*
* @var string
*/
protected $lock_key = 'generate:uid_lock';

/*
* 发号器列表最大容量,默认为 500000
*
* @var int
*/
protected $max_capacity = 5E5;

public function __construct()
{
$this->redis = get_redis();

parent::__construct();
}

protected function configure()
{
// 指令配置
$this->setName('generate:uid')
->setDescription('生成 UID');
}

protected function execute(Input $input, Output $output)
{
$current_capacity = $this->get_list_length();

// 计算发号器需要增加的数量
$append_capacity = $this->max_capacity - $current_capacity;

$uid_max = $this->get_uid_max();

$output->writeln('当前最大UID:' . $uid_max);
$output->writeln('当前剩余容量:' . $current_capacity);
$output->writeln('最大存储容量:' . $this->max_capacity);
$output->writeln('需要追加容量:' . $append_capacity);

// 如果不需要增加则结束
if($append_capacity === 0) {
return;
}

$data = [];

for($i = 1; $i <= $append_capacity; $i++) {
$data[] = $uid_max + $i;
}

// 把需要加的数据进行分块,方便快速追加
$data = array_chunk($data, 1000);

// 加锁
$lock = $this->redis->executeRaw([
'SET',
$this->lock_key,
1,
'EX',
10 * 60,
'NX',
]);

if($lock !== 'OK') {
$output->writeln('获取锁失败');

return;
}

try {
foreach ($data as $item) {
$this->redis->rpush($this->cache_key, $item);
}
} catch (\Exception $e) {
$output->error($e->getMessage());
} finally {
// 释放锁
$this->redis->del($this->lock_key);
}
}

/*
* 获取发号器列表的长度
*
* @return int
*/
protected function get_list_length() :int
{
return $this->redis->llen($this->cache_key);
}

/*
* 获取当前最大的 UID
*
* @return int
*/
protected function get_uid_max() :int
{
$value = (int) $this->redis->lindex($this->cache_key, -1);

if($value) {
return $value;
}

return 0;
}
}

config/console.php

1
2
3
4
5
6
7
8
<?php
return [
// 指令定义
'commands' => [
...
'generate:uid' => 'app\command\GenerateUID',
],
];

接着就可以运行了。

1
2
3
4
5
$ php think generate:uid
当前最大UID:0
当前剩余容量:0
最大存储容量:500000
需要追加容量:500000

往上