php + redis + lua 實現一個簡單的發號器(1)-- 原理篇
1、為什麼要實現發號器
很多地方我們都需要一個全域性唯一的編號,也就是uuid。舉一個常見的場景,電商系統產生訂單的時候,需要有一個對應的訂單編號。在composer上我們也可以看到有很多可以產生uuid的優秀元件。那麼,為什麼我們還要自己實現發號器,來產生uuid呢?想了一下,主要有兩個原因吧:
1、我希望uuid是可反解的,通過反解uuid可以得出和我業務相關的資料。而我看到的composer關於uuid的相關元件,生成的都是一串指定格式的字串,我很難將它同具體的業務關聯起來。
2、我希望通過uuid是可以隨著並放量進行調整的。比如說原有支援1秒鐘可以產生1000個uuid,但隨著業務規模增長,我希望變成可以支援1秒鐘產生一萬個。而且,最好改下配置就可以了。
出於以上兩個原因,我們需要自己的發號器來產生uuid。那麼,下一個問題是,我們應該如何實現發號器,實現發號器的原理又是什麼呢?
2、snowFlake演算法
關於發號器的實現原理,可能大家都聽過鼎鼎大名的snowflake演算法 -- 雪花演算法,Twitter的分散式自增Id演算法。國內的新浪微博也有自己實現的發號器演算法,具體實現細節雖有不同,但是原理相通,明白其中一個即可。這裡我們主要介紹snowflake。
關於snowflaw的介紹,已經有很多文章進行介紹,而且寫的也很不錯,我沒有必要在重寫一遍,拿來貼上即可,出於對作者的尊重,我會將原文連結新增到參考連結中。
推特的分散式自增ID演算法,使用long (8 × 8 = 64 byte)來儲存uuid。其中1bit留給固定符號位0,41bit留給毫秒時間戳,10bit給MachineID,也就是機器要預先配置,剩下12位留Sequence(可支援1毫秒內4096個請求)。
也許有的人會問如果超過了1毫秒4096個請求怎麼辦?一般的做法是,讓它等上1毫秒,促使41bit的時間戳變化。
這裡我們將MachineId進行了拆分,5byte留給機器(最多可以支援32機器),5byte留給了業務號(最多可支援32種業務)
這裡的時間戳儲存的是當前時間與固定過去時間得一個差值,不是當前時間。這樣的好處是能使用更長時間,而且不受年份限制,只取決於從什麼時候開始用的,2^41 / 1000360024*365=69年。
如果儲存的是當前時間戳,最多隻能使用到2039年。2^41=2199023255552=2039/9/7 23:47:35
理論上單機速度:2^12*1000 = 4 096 000/s
3、如何保證在單位時間內持續遞增
通過對snowflake的初步瞭解,發現,其實發號器也是建立在時間戳基礎之上的,因為時間是天然的唯一元素。但是,如何在單位時間內,比如說一秒鐘或者一毫秒之內,保證Sequence持續遞增才是發號器實現的關鍵。
這裡我們實現的方式比較簡單,直接使用redis的incr進行計數,對應的key就是毫秒時間戳。出於redis記憶體回收的考慮,我們需要將每一個key設定過期時間。如果key是秒級別的時間戳,那麼過期時間就是1秒;如果key毫秒級別的時間戳,那麼過期時間就是1毫秒。
與此同時,為了保證執行incr,expire(pexpire)具有原子性,我們使用lua來進行實現。
好了,實現的思路大致如此。由於能力和水平有限,難免會有紕漏,希望及時指出。