接着上一篇 php + redis + lua 實現一個簡單的發號器(1)-- 原理篇,本篇講一下發號器的具體實現。php
發號器的實現主要用到了下面的一些知識點:html
1. php中的位運算的操做和求值redis
2. 計算機原碼、補碼、反碼的基本概念segmentfault
若是你對這些知識已經熟悉,直接往下看便可, 不瞭解的話就猛戳。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中。統計最後一列的總數量和去重後的數量是否一致便可。
須要注意的是,因爲網絡狀況的不一樣,建議將redis中key的過時時間進行調整,這裏是100毫秒,不然可能會出現相同的uuid
具體緣由以下,相同的key值(相同的diffTime + 相同的workerId + 相同的serverId 會產生相同的key),去獲取sequence, 第一個請求者執行完畢後,返回獲得1後返回,此時redis 將key過時回收。第二個請求過去,key不存在,返回也獲得1,此時會形成相同的uuid
分佈式ID生成器PHP+Swoole實現(下) - 代碼實現
因爲能力和水平的有限,不免會有錯誤,但願讀者及時支出!