閒談軟體設計:再聊 UUID

閱讀時間約 9 分鐘
這是 UUID 三部曲的第二部曲,主要是聊怎麼讓 UUID 變的更短一點,想了解 UUID 的原理可以參考 閒談軟體設計:UUID,接下來第三部曲會聊特殊應用,以及如何善用變形。

前言

這文章是因為過去使用大量的 UUID,但 UUID 有個致命的缺點是,它實在太長了,128 bits 用 Hex 表示法,至少要 32 個字元,如果再加上分隔符號,就要 36 個字元,把這放在面向使用者者的頁面上,應該不會有人會記得住吧!但 UUID 就真的只能這麼長嗎?其實是可以再短一點的。

開始變短

剛剛才說到目前的表示法是 Hex (十六進制),也就是將 128 bits 每 4 bits 為一組,然後用 0 到 F,分別表示 0 (0000) 到 15 (1111),這也就是為什麼長度會是 32 個字元 (128 / 4),所以想要更短,最簡單的作法是 5 bits 為一組用 32 個字母來表示,或是用 6 bits 為一組用 64 個字母來表示,這時長度也就可以縮短到 26 (ceil(128 / 5)) 字元或 22 (ceil(128 / 6)) 字元了。用 7 bits 一組也不是不行,只是找到易讀的 128 個字母就是件困難的事了。

這次文章的範例和往常用 Java 不同,是用 Kotlin,但我還是盡量讓程式碼簡單一點。首先,我們先替既有的 Java 類別 UUID 加上一個 extension method [關於 extension 我個人比較喜歡 Swift 的寫法],能給定一組字母,然後用這組字母來編碼 UUID。

由於剛剛提到,是用幾個 bits 為一組 (事實上這非強制性,只是在解釋上比較方便),因此我們先檢查給定的字母數量是否為 2 的次方,於是第一個版本就出來了,還沒有功能,單純檢查輸入是否合法,另外 Kotlin 支援預設參數,我順便加上由 0 到 9,大小寫的英文字母及兩個特殊符號的字母組合。

然後寫個測試,驗證一下是否真的會拋出例外:

接著,我們是希望變短,而不是變長,因此字母不該小於 24 個,因此再加上一個指數不得小於 4 的限制:

然後再補上一個測試案例:

到這邊,我個人是覺得這些檢查佔據太多行數,抽象層級和接下來要專注的內容不同,所以直接將驗證的邏輯抽成一個函式,這時主函式就清爽多了。如果要再增加對參數檢查的邏輯 (例如,不可有重複的字母),也是寫在剛抽出去的函式中,增加可讀性。

有一點二進制基礎的大概都知道將 10 進制的數字轉換成 2 進制,就是一直除以 2 取餘數 (或是用位元運算往右 shift 取被 shift 的那個 bit),同理,假設是 5 bits 一組,那就是取 32 的餘數 (或是往右 shift 5 bits)。

但 UUID 長達 128 bits,多數語言是沒有內建型別可以表達這麼長的數,Java 的 long 也才 64 bits (事實上 UUID 內部就是用兩個 long 表達),那怎麼把個 long 一起取餘數或是 shift 呢?這時候可以靠 BigInteger 了,有個建構子可以從指定的 ByteArray 建立 BigInteger 物件。

因此,接下來就是將 UUID 轉成 ByteArray,因此新增一個 toBytes 的extension method,由於 Java 沒有正整數的型別,所以 UUID 類別的兩個成員 mostSignificantBits 和 leastSignificantBits 都有可能是負數,因此像 BigInteger("${mostSignificantBits}${leastSignificantBits}") 這樣的寫法是會出錯的。這裡採用保守的作法,建立一個 17 bytes 的 buffer,然後依序放入 0、 mostSignificantBits 和 leastSignificantBits,第一個 0 確保整個 17 bytes 被視為正整數處理。

再來就是很單純地取餘數,拿餘數查表,將查到的字母接在 buffer 的後面,最後記得將整個字串反轉 (因為計算過程是將低位串到高位)。BigInteger 其實支援左移或是右移等運算,速度應該比較快,但不熟位元運算的人閱讀起來可能需要花點時間,所以這裡還是用取餘數的方式。

驗證結果

寫完後當然要寫個測試驗證一下對不對,既然要測試,就得先找出正確解,所以文章一開始的圖片就是解釋:怎麼從一個 UUID 找出編碼後的結果。這裡是每 6 bits 一組,綠色是高位元的 64 bits,紫色則是低位元的 64 bits,會發現有個 a 是由高位元的最小 2 bits 搭配低位元的最高 4 bits 組成的,這也是為什麼需要兩個 Long 一起運算的原因了,因為除非是 4 bits 或 8 bits 一組,不然無法整除 Long,一定會發生要向另一個 Long 借位的情形發生 [不借位其實也是可行的,只是編碼出來的長度會再加 1]。

raw-image

從測試案例就可以看出來長度明顯縮短不少:

53e6693a-0aaa-4e94-a116–2a397fc89975 (一般形式)
53e6693a-0aaa4e94a1162a397fc89975 (去除分隔符號)
1jVCAW2GFeBa4mazB-O9BR (短模式)

既然能夠編碼,那是否可以解回來呢?當然也是可以,這裡就替 String 增加一個 fromShortId 的 extension method,一樣可以指定字母表,只是程式會檢查給定的字母數量是否足以解碼 (這裡就偷懶沒將驗證抽出去成獨立的函式了),後續的邏輯就很簡單,依序查字母所在的 index,乘上倍數後加上 index,最後一樣要記得處理一下正負數 (line 14) 就完成了。

當然,也是要寫個測試驗證一下解碼對不對:

最後再加上個測試案例,不是用預設的字母表,這裡使用的是 0 到 9,以及大小的 A 到 Z,排除掉容易跟 1 搞混的 I,以及和 0 搞混的 O 和 Q 後共 32 個字母,從測試案例就可以看出,有縮短但幅度就沒那麼明顯了。

fbcf8ca0-a0b7–4e8c-8a05-f0be7c72f022 (一般形式)
fbcf8ca0a0b74e8c8a05f0be7c72f022 (去除分隔符號)
7USX6A185P9T68L1FGPSX75V12 (base 32)

好啦,有人會說,這不就是 Base64 和 Base 32 嗎?是也不是,因為我寫的演算法跟標準不太一樣,只是用這麼編碼方式來呈現 UUID 是較少見的 [維基百科:在 Java 持久化系統 Hibernate 中,就採用了 Base64 來將一個較長的唯一識別碼 (一般為128-bit的 UUID) 編碼為一個字串,用作 HTTP 表單和 HTTP GET URL 中的參數],原因為何我就不清楚了。

邊解碼效能測試

順手寫了個簡單的程式測一下兩者的效能差距,由於單獨一個 toString() 或是 shortId() 的執行時間很短,所以程式是每十萬次為單位量測時間,大家有興趣可以跑跑看。

我想從圖表就看的出來,雖然程式還有優化的可能 (把除法改成位元運算),但那個差距是相當明顯 (單位是 ms)。

raw-image

更短?

說真的,即便將長度從 36 個字元縮短到本文中的 22 或 26 個字元,仍然是一個很長的 ID,一般來說,人的短期記憶力有所謂七加減二的說法,也就是能記住五至九個字元的片段,太長就記不住了,用 UUID 或是較短的其他 ID 產生器,這就得有些取捨了。

以五個字元來說,數字加上英文字母大小寫,最多就 62 的五次方種組合,也就是 916,132,832 個組合,又或是不分大小寫並排除易混淆字母只用 32 個字元,也就是 32 的五次方共 33,554,432 個組合,碰撞機率還是有的,可搭配資料庫紀錄已配發的 ID,或加上 namespace 來避免重複。

當長度增加到九個字元,這時 62 的九次方 (13,537,086,546,263,552) 或是 32 的九次方 (35,184,372,088,832),這時碰撞的機率非常的低,也就是說一次的 Random.nextLong() 加上指定的編碼就能產生指定長度的 ID,算是相當快速 (UUID 需要兩次的 random) 且方便的,果不其然,可以在 GitHub 的專案看到相似的概念:

題外話,這次的 short UUID 其實在GitHub 也能找到類似的專案:


文中的範例我放在 GitHub 上,想優化或修改可以去下載。目前 GitHub 的版本跟本文有點不一樣,演算法沒變,主要是用了 Kotlin 的 companion object 稍微改寫一下。


52會員
102Content count
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
這是 UUID 三部曲的首部曲,接下來兩周陸續更新第二部與第三部,首部曲先聊聊 UUID 的前世今生,二部曲會聊聊如何縮短 UUID 的長度,第三部曲則是聊聊特殊應用,希望能幫助到大家。
在大型主機的年代 (這裡要澄清一下,本人沒有歷經那個年代,不然都把自己講老了),就已經有 clent server 的概念:由 client (計算能力較差的終端機) 向 server (計算能力較好的大型主機) 請求服務,幫助理解與管理分散式系統程式的複雜度。
每次看到 REST 就讓我想起以前念研究所和老師一起想論文題目時,曾提過國外常會玩這種文字遊戲,像是將 Representational State Transfer 變成一個很簡單的單字 REST,但我的東西不管怎麼想,卻想不出什麼有趣的東西 Orz
稍微複雜一點的 query 其實代表著某些商業邏輯,若把這一段程式放到 repository 的實作層,會變成這些商業邏輯被隱藏起來了,如果有個好的描述語言,我倒覺得很好讀,也可以清楚知道背後的商業邏輯是什麼,是很好的一件事。
簡單說,Repository 提供像是 array 或是 dictionary 的容器,對程式來說,就好像記憶體中有所有的物件,不用去想物件其實是從資料庫來的。作法是在 data mapping layer 之上再提供一個 collection 的介面來存取物件,將資料庫的細節從商業邏輯層抽離。
不是在軟體建置之初就決定所有一切的行為,而是執行時根據配置 (configuration) 或執行時的環境 (例如:特定目錄有什麼 plug-in) 決定有哪些行為,話是這麼說,但要實作一個可以載入 plug-in 的軟體 (以下稱作 host application) 倒是有不少事情要考慮。
這是 UUID 三部曲的首部曲,接下來兩周陸續更新第二部與第三部,首部曲先聊聊 UUID 的前世今生,二部曲會聊聊如何縮短 UUID 的長度,第三部曲則是聊聊特殊應用,希望能幫助到大家。
在大型主機的年代 (這裡要澄清一下,本人沒有歷經那個年代,不然都把自己講老了),就已經有 clent server 的概念:由 client (計算能力較差的終端機) 向 server (計算能力較好的大型主機) 請求服務,幫助理解與管理分散式系統程式的複雜度。
每次看到 REST 就讓我想起以前念研究所和老師一起想論文題目時,曾提過國外常會玩這種文字遊戲,像是將 Representational State Transfer 變成一個很簡單的單字 REST,但我的東西不管怎麼想,卻想不出什麼有趣的東西 Orz
稍微複雜一點的 query 其實代表著某些商業邏輯,若把這一段程式放到 repository 的實作層,會變成這些商業邏輯被隱藏起來了,如果有個好的描述語言,我倒覺得很好讀,也可以清楚知道背後的商業邏輯是什麼,是很好的一件事。
簡單說,Repository 提供像是 array 或是 dictionary 的容器,對程式來說,就好像記憶體中有所有的物件,不用去想物件其實是從資料庫來的。作法是在 data mapping layer 之上再提供一個 collection 的介面來存取物件,將資料庫的細節從商業邏輯層抽離。
不是在軟體建置之初就決定所有一切的行為,而是執行時根據配置 (configuration) 或執行時的環境 (例如:特定目錄有什麼 plug-in) 決定有哪些行為,話是這麼說,但要實作一個可以載入 plug-in 的軟體 (以下稱作 host application) 倒是有不少事情要考慮。
你可能也想看
Thumbnail
重點摘要: 1.9 月降息 2 碼、進一步暗示年內還有 50 bp 降息 2.SEP 上修失業率預期,但快速的降息速率將有助失業率觸頂 3.未來幾個月經濟數據將繼續轉弱,經濟復甦的時點或是 1Q25 季底附近
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。
Thumbnail
重點摘要: 1.9 月降息 2 碼、進一步暗示年內還有 50 bp 降息 2.SEP 上修失業率預期,但快速的降息速率將有助失業率觸頂 3.未來幾個月經濟數據將繼續轉弱,經濟復甦的時點或是 1Q25 季底附近
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。