php lua redis,php + redis + lua 实现一个简单的发号器(2)-- 实现篇

1、基础知识

发号器的实现主要用到了下面的一些知识点:

如果你对这些知识已经熟悉,直接往下看即可, 不了解的话就猛戳。

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;

$uuidItem['segment']['diffTime'] = $diffTime;

//服务器

$shift = $shift - self::BITS_SERVER;

$id |= $this->serverId << $shift;

$uuidItem['segment']['serverId'] = $this->serverId;

//业务

$shift = $shift - self::BITS_WORKER;

$id |= $this->workerId << $shift;

$uuidItem['segment']['workerId'] = $this->workerId;

//自增值

$sequenceNumber = $this->getSequence($id);

$uuidItem['segment']['sequenceNumber'] = $sequenceNumber;

if ($sequenceNumber > pow(2, self::BITS_SEQUENCE) - 1) {

usleep(1000);

} else {

$id |= $sequenceNumber;

$uuidItem['uuid'] = strval($id);

return $uuidItem;

}

} 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 = <<

local sequenceKey = KEYS[1]

local sequenceNumber = redis.call("incr", sequenceKey);

redis.call("pexpire", sequenceKey, 100);

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, 运行结果如下图

bVbsmK4?w=1225&h=105

反解uuid

$redis = new Redis;

$redis->connect("127.0.0.1", 6379);

$instance = SignGenerator::getInstance($redis);

$item = $instance->reverseNumber(1369734562062337);

var_dump($item);die();

打印结果如下, 通过对比发现和之前的一致

bVbsmMV?w=1225&h=258

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、测试

测试方法很简单,循环写入5万次,看看是否有重复的uuid出现?

require "./SignGenerator.php";

$redis = new Redis();

$redis->connect('127.0.0.1', 6379);

$instance = SignGenerator::getInstance($redis);

$instance->setServerId(1)->setWorkerId(2);

//循环写入10万次

for($count = 1; $count <= 100000; $count++) {

$uuidItem = $instance->getNumber();

$segment = $uuidItem['segment'];

$uuid = $uuidItem['uuid'];

echo implode("\t", $segment), "\t", $uuid, "\n";

}

执行 php ./SignTest.php >> /tmp/SignTest.log命令,所有的运行结果讲会被保存在/tmp/SignTest.log中。统计最后一列的总数量和去重后的数量是否一致即可。

bVbspOg?w=1360&h=718

6、发现的问题

需要注意的是,由于网络情况的不同,建议将redis中key的过期时间进行调整,这里是100毫秒,否则可能会出现相同的uuid

具体原因如下,相同的key值(相同的diffTime + 相同的workerId + 相同的serverId 会产生相同的key),去获取sequence, 第一个请求者执行完毕后,返回得到1后返回,此时redis 将key过期回收。第二个请求过去,key不存在,返回也得到1,此时会造成相同的uuid

7、参考资料

由于能力和水平的有限,难免会有错误,希望读者及时支出!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值