在真實的業務場景中生成唯一數是很常用的功能,也是面試必考題。最近面試一個PHP開發崗位,無意中聊到這個話題,然后順著話題一直拓展。今天說下在面試中,面試官問這個問題想得到怎樣的答案。
每種編程語言都提供了唯一數生成函數,但是都有條件限制,先看看網上都有哪些生成唯一數的方法。
一. 散列+時間+隨機值
md5(time() . mt_rand(1,1000000));
time()
函數獲取當前時間戳,mt_rand(1,1000000)
函數獲取一個1到1000000的隨機值,看著好像生成的數能唯一,但這行代碼的問題其實非常多。
在代碼中時間是很不靠譜的參數,如果用戶1秒內發送了2個請求,那么time()
字段毫無意義。
在代碼中隨機數其實并不隨機,常見的隨機數都需要有隨機種子,而為了保證種子不被猜到,編程語言默認會使用當前系統時間作為種子。又變成了依賴時間的一個參數,所以這種方案不可取。
二. 微秒+進程編號
uniqid();
uniqid()
函數可以得到一個基于微秒和進程編號的唯一ID。對于php-fpm來說,每個請求都獨占一個進程,一個進程會串行的處理多個請求。所以通過進程編號+微秒時間看上去能生成唯一ID。但深究之后發現并不靠譜。
1秒等于100萬微秒,現在問題會變成一個進程能在百萬分之一秒內處理多個請求嗎?答案是可以的,用當前最普通的CPU來說,單核心1秒就可以計算20億次,1微秒可以計算2千次。從計算機調度的角度來說,2千次同時處理到一個進程的兩個請求是完全可能的。所以又變成了依賴時間的一個參數,這種方案不可取。
你還能使用納秒,皮秒等精度更高的時間參數,但以發展的遠光看問題,未來CPU的算力會越來越快,依賴時間的唯一性會越來越差。無論怎么努力,只依靠編程語言自身得到唯一ID是非常困難的,也是我不推薦的。
三. 數據庫
靠編程語言自己走不通,那就通過第三方工具,這種方案是靠譜的。但實現起來也有很多種方法。
LOCK TABLES id_maker;
//拿到id
select id from id_maker;
//加1更新
update id_maker set id=id+1;
UNLOCK id_maker;
這是我曾經見過的一種id_maker用法,這樣每次得到的id都是唯一并且有序的。但問題就是需要鎖表,性能不高,在高并發下會發生資源搶占導致數據庫崩潰。
//id_maker表有自增id和內容data兩個字段
insert into id_maker(data) values('');
select last_insert_id();
這種方式性能就高很多,insert語句不會鎖表,自增id百分百保證唯一性且有序。唯一的問題是需要定期刪除歷史數據,對于大部分項目我都建議使用這種方式生成唯一ID。
除了MySQL還有MongoDB,Redis等其他數據庫方案,方法大同小異。但是這個方案有局限性,當我們的業務發展到成千上萬臺服務器時通過一個數據庫的一張表去生成ID會導致性能下降拖垮其他服務,還會形成單點依賴。
四. 微秒+服務器編號+計數器
這是twitter使用的一種方案,名字叫SnowFlake,這種方案的原理非常簡單,所以很容易基于SnowFlake算法做擴展。比如PHP并不會常駐內存,計數器的實現就比較麻煩,但只要原理清楚了使用擴展或者數據庫很容易實現。
這個方案的最大優點就是在龐大的集群中,每個服務靠自己就能算出全局的唯一ID。服務器編號和微秒能確定到具體服務器,計數器可以理解為只為當前服務器提供的id_maker
,這樣生成的唯一編號無懈可擊。