接着上一篇 php + redis + lua 实现一个简单的发号器(1)– 原理篇,本篇讲一下发号器的具体实现。
1、基础知识
发号器的实现主要用到了下面的一些知识点:
1. php 中的位运算的操作和求值
2. 计算机原码、补码、反码的基本概念
3. redis 中 lua 脚本的编写和调试
如果你对这些知识已经熟悉,直接往下看即可, 不了解的话就猛戳。
2、具体实现
先上代码吧,然后再慢慢分析
class SignGenerator
{
CONST BITS_FULL = 64;
CONST BITS_PRE = 1;// 固定
CONST BITS_TIME = 41;// 毫秒时间戳 可以最多支持 69 年
CONST BITS_SERVER = 5; // 服务器最多支持 32 台
CONST BITS_WORKER = 5; // 最多支持 32 种业务
CONST BITS_SEQUENCE = 12; // 一毫秒内支持 4096 个请求
CONST OFFSET_TIME = "2019-05-05 00:00:00";// 时间戳起点时间
/**
* 服务器 id
*/
protected $serverId;
/**
* 业务 id
*/
protected $workerId;
/**
* 实例
*/
protected static $instance;
/**
* redis 服务
*/
protected static $redis;
/**
* 获取单个实例
*/
public static function getInstance($redis)
{if(isset(self::$instance)) {return self::$instance;} else {return self::$instance = new self($redis);
}
}
/**
* 构造初始化实例
*/
protected function __construct($redis)
{if($redis instanceof \Redis || $redis instanceof \Predis\Client) {self::$redis = $redis;} else {throw new \Exception("redis service is lost");
}
}
/**
* 获取唯一值
*/
public function getNumber()
{if(!isset($this->serverId)) {throw new \Exception("serverId is lost");
}
if(!isset($this->workerId)) {throw new \Exception("workerId is lost");
}
do{$id = pow(2,self::BITS_FULL - self::BITS_PRE) << self::BITS_PRE;
// 时间戳 41 位
$nowTime = (int)(microtime(true) * 1000);
$startTime = (int)(strtotime(self::OFFSET_TIME) * 1000);
$diffTime = $nowTime - $startTime;
$shift = self::BITS_FULL - self::BITS_PRE - self::BITS_TIME;
$id |= $diffTime << $shift;
echo "diffTime=",$diffTime,"\t";
// 服务器
$shift = $shift - self::BITS_SERVER;
$id |= $this->serverId << $shift;
echo "serverId=",$this->serverId,"\t";
// 业务
$shift = $shift - self::BITS_WORKER;
$id |= $this->workerId << $shift;
echo "workerId=",$this->workerId,"\t";
// 自增值
$sequenceNumber = $this->getSequence($id);
echo "sequenceNumber=",$sequenceNumber,"\t";
if($sequenceNumber > pow(2, self::BITS_SEQUENCE)) {usleep(1000);
} else {
$id |= $sequenceNumber;
return $id;
}
} while(true);
}
/**
* 反解获取业务数据
*/
public function reverseNumber($number)
{$uuidItem = [];
$shift = self::BITS_FULL - self::BITS_PRE - self::BITS_TIME;
$uuidItem['diffTime'] = ($number >> $shift) & (pow(2, self::BITS_TIME) - 1);
$shift -= self::BITS_SERVER;
$uuidItem['serverId'] = ($number >> $shift) & (pow(2, self::BITS_SERVER) - 1);
$shift -= self::BITS_WORKER;
$uuidItem['workerId'] = ($number >> $shift) & (pow(2, self::BITS_WORKER) - 1);
$shift -= self::BITS_SEQUENCE;
$uuidItem['sequenceNumber'] = ($number >> $shift) & (pow(2, self::BITS_SEQUENCE) - 1);
$time = (int)($uuidItem['diffTime']/1000) + strtotime(self::OFFSET_TIME);
$uuidItem['generateTime'] = date("Y-m-d H:i:s", $time);
return $uuidItem;
}
/**
* 获取自增序列
*/
protected function getSequence($id)
{
$lua = <<<LUA
local sequenceKey = KEYS[1]
local sequenceNumber = redis.call("incr", sequenceKey);
redis.call("pexpire", sequenceKey, 1);
return sequenceNumber
LUA;
$sequence = self::$redis->eval($lua, [$id], 1);
$luaError = self::$redis->getLastError();
if(isset($luaError)) {throw new \ErrorException($luaError);
} else {return $sequence;}
}
/**
* @return mixed
*/
public function getServerId()
{return $this->serverId;}
/**
* @param mixed $serverId
*/
public function setServerId($serverId)
{
$this->serverId = $serverId;
return $this;
}
/**
* @return mixed
*/
public function getWorkerId()
{return $this->workerId;}
/**
* @param mixed $workerId
*/
public function setWorkerId($workerId)
{
$this->workerId = $workerId;
return $this;
}
}
3、运行一把
获取 uuid
$redis = new Redis;
$redis->connect("127.0.0.1", 6379);
$instance = SignGenerator::getInstance($redis);
$instance->setWorkerId(2)->setServerId(1);
$number = $instance->getNumber();
// 于此同时,为了方便同可反解操作做对别,分别记录下来 diffTime,serverId,workerId,sequenceNumber, 运行结果如下图
反解 uuid
$redis = new Redis;
$redis->connect("127.0.0.1", 6379);
$instance = SignGenerator::getInstance($redis);
$item = $instance->reverseNumber(1369734562062337);
var_dump($item);die();
打印结果如下,通过对比发现和之前的一致
4、代码解析
从上面的代码上看,里面大量的使用了 php 的位运算操作,可能有些同学接触的不多,这里以 getNumber 为例,简单解释一下上面的代码,如果你已经很清楚了,那就请直接忽略本段。
首先明白一个基础的概念,计算机所有的数据都是以二进制补码的形式进行存储的,正数的原码 = 反码 = 补码
分析 getNumber 方法的实现过程:
1、初始化发号器
$id = pow(2,self::BITS_FULL - self::BITS_PRE) << self::BITS_PRE;
我们可以认为:pow(2,self::BITS_FULL - self::BITS_PRE) 我们向计算机申请了一块内存,它大概长下面这个样子:高位 <---------------------------------------------------------- 低位
10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
执行位运算,由低位向高位移动,空位使用 0 补齐,变成了现在的这个样子
高位 <---------------------------------------------------------- 低位
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
这不就是 0 么,对的,经过实验测试,直接将 $id = 0, 效果是一样的
所以 $id 的初始化有下面三种
// $id = pow(2, self::BITS_FULL);
// $id = pow(2,self::BITS_FULL - self::BITS_PRE) << self::BITS_PRE;
// $id = 0;
2、为发号器添加时间属性
// 时间戳 41 位
$nowTime = (int)(microtime(true) * 1000);
$startTime = (int)(strtotime(self::OFFSET_TIME) * 1000);
// 计算毫秒差,基于上图,这里 diffTime=326570168
$diffTime = $nowTime - $startTime;
// 计算出位移 的偏移量
$shift = self::BITS_FULL - self::BITS_PRE - self::BITS_TIME;
// 改变 uuid 的时间 bit 位
$id |= $diffTime << $shift;
$id 与 $diffTime 执行位移前的二进制形式
|-------------BITS_PRE + BITS_TIME------------||--------shift---------|
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
10011 01110111 00010000 10111000
$diffTime 执行位移后的二进制形式
|-------------BITS_PRE + BITS_TIME------------||--------shift---------|
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
100 11011101 11000100 00101110 00|--------shift---------|
紧接着同 $id 进行或操作,得到如下结果
|-------------BITS_PRE + BITS_TIME------------||--------shift---------|
00000000 00000100 11011101 11000100 00101110 00000000 00000000 00000000
3、为发号器添加服务器编号
// 在新的 $shift 计算出位移 的偏移量
$shift = $shift - self::BITS_SERVER;
// 改变 uuid 的服务器 bit 位
$id |= $this->serverId << $shift;
$id 与 $serverId 执行位移前的二进制形式
|-------BITS_PRE + BITS_TIME + BITS_SERVER---------||------shift------|
00000000 00000100 11011101 11000100 00101110 00000000 00000000 00000000
1
$serverId 执行位移后的二进制形式
|-------BITS_PRE + BITS_TIME + BITS_SERVER---------||------shift------|
00000000 00000100 11011101 11000100 00101110 00000000 00000000 00000000
10 00000000 00000000
紧接着同 $id 进行或操作,得到如下结果
|-------BITS_PRE + BITS_TIME + BITS_SERVER---------||------shift------|
00000000 00000100 11011101 11000100 00101110 00000010 00000000 00000000
4、为发号器添加业务编号
// 在新的 $shift 计算出位移 的偏移量
$shift = $shift - self::BITS_WORKER;
// 改变 uuid 的业务编号 bit 位
$id |= $this->workerId << $shift;
$id 与 $workerId 执行位移前的二进制形式, $workerId = 2
|---BITS_PRE + BITS_TIME + BITS_SERVER + BITS_WORKDER----||---shift---|
00000000 00000100 11011101 11000100 00101110 00000010 00000000 00000000
10
$workerId 执行位移后的二进制形式
|---BITS_PRE + BITS_TIME + BITS_SERVER + BITS_WORKDER----||---shift---|
00000000 00000100 11011101 11000100 00101110 00000010 00000000 00000000
100000 00000000
紧接着同 $id 进行或操作,得到如下结果
|---BITS_PRE + BITS_TIME + BITS_SERVER + BITS_WORKDER----||---shift---|
00000000 00000100 11011101 11000100 00101110 00000010 00100000 00000000
5、为发号器添加 sequence
// 这里 $sequenceNumber = 1
$id |= $sequenceNumber;
|--BITS_PRE + BITS_TIME + BITS_SERVER + BITS_WORKDER + BITS_SEQUENCE--|
00000000 00000100 11011101 11000100 00101110 00000010 00100000 00000000
1
紧接着同 $id 进行或操作,得到如下结果
|--BITS_PRE + BITS_TIME + BITS_SERVER + BITS_WORKDER + BITS_SEQUENCE--|
00000000 00000100 11011101 11000100 00101110 00000010 00100000 00000001
最后我们得出二进制数据为:100 11011101 11000100 00101110 00000010 00100000 00000001,通过进制转换得到对应的数字就是:1369734562062337
5、参考资料
分布式 ID 生成器 PHP+Swoole 实现 (下) – 代码实现
原码,反码,补码杂谈
由于能力和水平的有限,难免会有错误,希望读者及时支出!