资讯专栏INFORMATION COLUMN

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

iOS122 / 507人阅读

摘要:接着上一篇实现一个简单的发号器原理篇,本篇讲一下发号器的具体实现。统计最后一列的总数量和去重后的数量是否一致即可。

接着上一篇 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 PredisClient) {
            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"] = $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 = <<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、测试

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

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("	", $segment), "	", $uuid, "
";
}

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

6、发现的问题

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

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

7、参考资料

分布式ID生成器PHP+Swoole实现(下) - 代码实现

原码,反码,补码杂谈

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/31427.html

相关文章

  • php + redis + lua 实现一个简单的发号器(1)-- 原理

    摘要:出于以上两个原因,我们需要自己的发号器来产生。与此同时,为了保证执行,具有原子性,我们使用来进行实现。由于能力和水平有限,难免会有纰漏,希望及时指出。参考文章分布式生成器实现上实现原理 1、为什么要实现发号器 很多地方我们都需要一个全局唯一的编号,也就是uuid。举一个常见的场景,电商系统产生订单的时候,需要有一个对应的订单编号。在composer上我们也可以看到有很多可以产生uuid...

    rottengeek 评论0 收藏0
  • 分布式系统全局发号器的几点思考

    摘要:为什么需要发号器在分布式系统中,经常需要对大量的数据消息请求等进行唯一标识,例如对于分布式系统,服务间相互调用需要唯一标识,调用链路分析,日志追踪的时候需要使用这个唯一标识。 原文链接:何晓东 博客 文章起源于 康神交流群的 panda大佬和boss li关于发号器的一些交流,特此感谢让我们学到了新知识。 为什么需要发号器 在分布式系统中,经常需要对大量的数据、消息、http 请求等进...

    dayday_up 评论0 收藏0
  • 基于Redis作为发号器生成短网址Python实践

    摘要:实现发号器使用的函数个字符作为进制符号转成进制为代码作为发号器生成短网址,假如域名为通过解码到原本文为原创首发于继续阅读全文 showImg(https://segmentfault.com/img/bV9f8g?w=1120&h=126); 描述 如何将长地址URL转换为短地址URL,一个比较理想的解决方案就是使用发号器生成一个唯一的整数ID,然后转换为62进制,作为短地址URL。 ...

    trilever 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<