摘要:接著上一篇實現一個簡單的發號器原理篇,本篇講一下發號器的具體實現。統計最后一列的總數量和去重后的數量是否一致即可。
接著上一篇 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 = <<3、運行一把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, 運行結果如下圖
反解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萬次,看看是否有重復的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實現(下) - 代碼實現
原碼,反碼,補碼雜談
由于能力和水平的有限,難免會有錯誤,希望讀者及時支出!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/31427.html
摘要:出于以上兩個原因,我們需要自己的發號器來產生。與此同時,為了保證執行,具有原子性,我們使用來進行實現。由于能力和水平有限,難免會有紕漏,希望及時指出。參考文章分布式生成器實現上實現原理 1、為什么要實現發號器 很多地方我們都需要一個全局唯一的編號,也就是uuid。舉一個常見的場景,電商系統產生訂單的時候,需要有一個對應的訂單編號。在composer上我們也可以看到有很多可以產生uuid...
摘要:為什么需要發號器在分布式系統中,經常需要對大量的數據消息請求等進行唯一標識,例如對于分布式系統,服務間相互調用需要唯一標識,調用鏈路分析,日志追蹤的時候需要使用這個唯一標識。 原文鏈接:何曉東 博客 文章起源于 康神交流群的 panda大佬和boss li關于發號器的一些交流,特此感謝讓我們學到了新知識。 為什么需要發號器 在分布式系統中,經常需要對大量的數據、消息、http 請求等進...
摘要:實現發號器使用的函數個字符作為進制符號轉成進制為代碼作為發號器生成短網址,假如域名為通過解碼到原本文為原創首發于繼續閱讀全文 showImg(https://segmentfault.com/img/bV9f8g?w=1120&h=126); 描述 如何將長地址URL轉換為短地址URL,一個比較理想的解決方案就是使用發號器生成一個唯一的整數ID,然后轉換為62進制,作為短地址URL。 ...
閱讀 2582·2021-11-18 10:02
閱讀 1715·2021-09-30 10:00
閱讀 5333·2021-09-22 15:27
閱讀 1215·2019-08-30 15:54
閱讀 3677·2019-08-29 11:13
閱讀 2953·2019-08-29 11:05
閱讀 3329·2019-08-29 11:01
閱讀 576·2019-08-26 13:52