转:一个PHP实现的ID生成器
通常來說,不管使用什么數據庫,表里都有一個名為 id 的主鍵,既然是主鍵,那么必然要滿足唯一性,對于 MySQL 用戶來說,它多半是一個 auto_increment 自增字段,也有一些別的用戶喜歡使用 UUID 做主鍵,不過對 MySQL(特別是 InnoDB)來說,UUID 通常不是一個好選擇,因為聚簇索引要求物理數據按照主鍵排序,而 UUID 本身是無序的,所以會帶來很多不必要的 IO 消耗。于是乎我們得到一個結論:ID 最好是順序的唯一值。
?
如此說來,就用 MySQL 的?auto_increment?自增字段不就好了?問題是這樣無法滿足高可用性,雖然可以通過多臺服務器設置不同的?auto_increment 步長來提升可用性,但數據庫本身始終就是那塊最短的木板。至于解決方案,網上已經有很多類似的討論:
- 細聊分布式ID生成方法
- 業務系統需要什么樣的ID生成器
- 分布式Unique ID的生成方法一覽
- 微信序列號生成器架構設計及演變
最流行的解決方案,當然是 twitter 的?snowflake,其大致含義是說:為了避免單點故障,在多個節點上運行 ID 生成器服務,每個節點都有自己獨立的標識,ID 以時間因子為前綴,雖然不同的服務器時間可能存在差異,不能保證絕對的順序,但是整體的趨勢還是可以認為是順序的,IO 負擔可以忽略,同時以一個計數器為后綴,從而保證唯一性。
網上現有的開源 ID 生成器,比如?Chronos,都是運行為服務的形式,不過對我而言,這樣有些太重了,于是我用 PHP?實現了一個非服務化的簡版 ID 生成器,雖然它很簡單,但是它并不簡陋,實現了 snowflake 要求的功能:
<?phpclass Sequence {const EPOCH = 1000000000;const TIME_BITS = 30;const NODE_BITS = 10;const COUNT_BITS = 20;private $node;public function __construct($node){$max = $this->max(self::NODE_BITS);if (is_int($node) === false || $node > $max || $node < 0) {throw new \InvalidArgumentException('node');}$this->node = $node;}public function generate($time = null){if ($time === null) {$time = time();}return ($this->time($time) << (self::NODE_BITS + self::COUNT_BITS)) |($this->node << self::COUNT_BITS) |($this->count($time));}public function restore($id){$binary = decbin($id);$position = -(self::NODE_BITS + self::COUNT_BITS);return array('time' => bindec(substr($binary, 0, $position)) + self::EPOCH,'node' => bindec(substr($binary, $position, - self::COUNT_BITS)),'count' => bindec(substr($binary, - self::COUNT_BITS)),);}private function time($time){$key = 'seq:time';if (apcu_fetch($key) === false && sleep(1) === 0) {apcu_add($key, $time);}$time -= self::EPOCH;$max = $this->max(self::TIME_BITS);if (is_int($time) === false || $time > $max || $time < 0) {throw new \InvalidArgumentException('time');}return $time;}private function count($time){$key = "seq:count:{$time}";while (!$count = apcu_inc($key)) {apcu_add($key, mt_rand(0, 9));}$max = $this->max(self::COUNT_BITS);if ($count > $max) {throw new \UnexpectedValueException('count');}return $count;}private function max($bits){return -1 ^ (-1 << $bits);} }?>本文中的實現利用?apcu?來保存數據,但是并不需要以服務的形式存在。其中我們自定義了一個時間的原點,這樣時間的位數可以節省點兒,實際使用時,可以用項目立項的時間戳做為時間原點,這樣更有意義些。以 30 位時間為例,如果時間原點是 1000000000 的話,那么理論上最大值可以保存到 2035-09-18,此外我們給節點留了 10 位,計數器留了 20 位,理論上可以容納最多 1023 個節點,每個節點每秒最多?1048575 個 ID。這些閾值基本都足夠了,多半還沒到達上限,系統就已經掛了。
需要說明的是,如果使用秒級的時間,假設在一秒內重啟 php-fpm,那么有可能會產生不唯一的值,所以我在代碼里加上了 sleep(1) 的邏輯來規避此問題。另外,因為代碼里并沒有嚴格判斷服務器可能出現的時間回退問題,所以還是有可能產生不唯一的值,但需要滿足幾個條件:首先,服務器時間發生了回退;其次,回退后生成 ID 時的時間恰好在以前使用過;最后,服務器因為 LRU 等原因清除了相關的緩存。要滿足這些條件,基本是很難的。也就是說,對于絕大部分 PHP 項目而言,本文的代碼可以認為是足夠強壯的。
此外,生成的 ID 最好別直接用,不然別人可以反解出其中的數據,比如你有多少臺服務器等等,解決辦法是在應用層用?hashids?編碼及解碼,如此一來,數據庫里保存的還是原始的 ID(Bigint),但是用戶看到的卻是 HASH ID,從而更好的保護了數據的安全。
轉載于:https://www.cnblogs.com/dasn/articles/6048385.html
總結
以上是生活随笔為你收集整理的转:一个PHP实现的ID生成器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SQL查询系列之六:SQL模糊查询
- 下一篇: python3 shell 正则表达式