php + redis + lua 實現一個簡單的發號器(2)-- 實現篇

接着上一篇 php + redis + lua 實現一個簡單的發號器(1)-- 原理篇,本篇講一下發號器的具體實現。php

一、基礎知識

發號器的實現主要用到了下面的一些知識點:html

1. php中的位運算的操做和求值redis

2. 計算機原碼、補碼、反碼的基本概念segmentfault

3. redis中lua腳本的編寫和調試服務器

若是你對這些知識已經熟悉,直接往下看便可, 不瞭解的話就猛戳。swoole

二、具體實現

先上代碼吧,而後再慢慢分析網絡

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'] = $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 = <<<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;
    }
}

三、運行一把

獲取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, 運行結果以下圖

圖片描述

反解uuidide

$redis = new Redis;

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

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

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

var_dump($item);die();

打印結果以下, 經過對比發現和以前的一致

圖片描述

四、代碼解析

從上面的代碼上看,裏面大量的使用了php的位運算操做,可能有些同窗接觸的很少,這裏以getNumber爲例,簡單解釋一下上面的代碼,若是你已經很清楚了,那就請直接忽略本段。測試

首先明白一個基礎的概念,計算機全部的數據都是以二進制補碼的形式進行存儲的,正數的原碼 = 反碼 = 補碼

分析getNumber方法的實現過程:

一、初始化發號器

$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;

二、爲發號器添加時間屬性

//時間戳 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

三、爲發號器添加服務器編號

//在新的$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

四、爲發號器添加業務編號

//在新的$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

五、爲發號器添加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萬次,看看是否有重複的uuid出現?

<?php
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中。統計最後一列的總數量和去重後的數量是否一致便可。

test.gif

六、發現的問題

須要注意的是,因爲網絡狀況的不一樣,建議將redis中key的過時時間進行調整,這裏是100毫秒,不然可能會出現相同的uuid

具體緣由以下,相同的key值(相同的diffTime + 相同的workerId + 相同的serverId 會產生相同的key),去獲取sequence, 第一個請求者執行完畢後,返回獲得1後返回,此時redis 將key過時回收。第二個請求過去,key不存在,返回也獲得1,此時會形成相同的uuid

七、參考資料

分佈式ID生成器PHP+Swoole實現(下) - 代碼實現

原碼,反碼,補碼雜談

因爲能力和水平的有限,不免會有錯誤,但願讀者及時支出!

相關文章
相關標籤/搜索