Sdcb Chats 技術(shù)博客:數(shù)據(jù)庫 ID 選型的曲折之路 - 從 Guid 到自增 ID,再到 Guid
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
在軟件開發(fā)中,數(shù)據(jù)庫主鍵的選擇,Guid 還是自增整數(shù) ID,一直是一個(gè)備受開發(fā)者關(guān)注和討論的經(jīng)典話題。作為開源 ChatGPT 前端項(xiàng)目 Sdcb Chats 的開發(fā)者,我們?cè)谶@個(gè)問題上也經(jīng)歷了一系列探索和演進(jìn),頗具代表性。Sdcb Chats 項(xiàng)目致力于打造一個(gè)強(qiáng)大、易用、可高度定制的 ChatGPT 及大語言模型前端,幫助用戶輕松連接、管理和使用各種主流的大語言模型。 總的來說,Sdcb Chats 的 ID 策略經(jīng)歷了從最初使用 Guid,到遷移至自增 ID,再到界面顯示加密 ID,最終又回歸到界面顯示 Guid 的過程,其中蘊(yùn)含著許多有趣的思考和實(shí)踐經(jīng)驗(yàn),也反映了我們?cè)陧?xiàng)目迭代過程中對(duì)性能、安全和用戶體驗(yàn)的不斷權(quán)衡與優(yōu)化。 第一階段 - 擁抱 Guid:“一步到位”的方案項(xiàng)目初期,我的好友 G 負(fù)責(zé)總體系統(tǒng)設(shè)計(jì),包括前端和數(shù)據(jù)庫。他果斷選擇了 Guid 作為主鍵方案。這在當(dāng)時(shí)是很自然的選擇,因?yàn)樵谄毡榈募夹g(shù)認(rèn)知中,自增 ID 在分布式系統(tǒng)中似乎存在諸多不便,而 Guid(全局唯一標(biāo)識(shí)符)則被視為一種更現(xiàn)代、更通用的解決方案。Guid 的核心優(yōu)勢(shì)在于其全局唯一性,能夠在不同的數(shù)據(jù)庫和服務(wù)器之間獨(dú)立生成,無需擔(dān)心 ID 沖突問題。 以下是項(xiàng)目初期基于 PostgreSQL 設(shè)計(jì)的數(shù)據(jù)庫創(chuàng)建腳本鏈接(如果您感興趣可以查看): 例如,這是
然而,隨著項(xiàng)目的深入發(fā)展,Guid 方案的一些局限性逐漸顯現(xiàn)。首先,Guid 較長(zhǎng)的長(zhǎng)度和復(fù)雜的結(jié)構(gòu)在某些場(chǎng)景下給數(shù)據(jù)庫性能帶來了一定負(fù)擔(dān)。尤其是在數(shù)據(jù)量快速增長(zhǎng)的情況下,索引體積增大,查詢速度變慢等問題開始凸顯。作為負(fù)責(zé)維護(hù)公司內(nèi)部 Chats 數(shù)據(jù)庫服務(wù)器的人,我注意到核心表 第二階段 - 性能至上:遷移至自增 IDChats 項(xiàng)目的重構(gòu)是一項(xiàng)系統(tǒng)性工程,數(shù)據(jù)庫的大規(guī)模重構(gòu)只是其中關(guān)鍵環(huán)節(jié)之一。實(shí)際上,軟件重構(gòu)是一個(gè)持續(xù)迭代的過程,貫穿于項(xiàng)目的整個(gè)生命周期。 在我看來,項(xiàng)目重構(gòu)如同軟件的自我革新,需要開發(fā)者具備“刀刃向內(nèi)”的勇氣和決心。當(dāng)我們審視代碼,發(fā)現(xiàn)不足之處,就如同在前進(jìn)的道路上遇到了障礙。我們當(dāng)然可以選擇繞行,暫時(shí)規(guī)避問題,但這些技術(shù)債務(wù)會(huì)像隱患一樣潛伏下來,并在未來某個(gè)時(shí)刻影響系統(tǒng)的穩(wěn)定性和可維護(hù)性。特別是對(duì)于數(shù)據(jù)庫這種核心模塊,開發(fā)者往往出于謹(jǐn)慎,傾向于避免改動(dòng)既有結(jié)構(gòu)和數(shù)據(jù)。但長(zhǎng)此以往,問題會(huì)逐漸累積,最終侵蝕系統(tǒng)的健康。因此,正視并解決這些問題才是負(fù)責(zé)任的做法。 當(dāng)然,在 Chats 項(xiàng)目的重構(gòu)中,我也有著得天獨(dú)厚的優(yōu)勢(shì)。作為后端設(shè)計(jì)的主導(dǎo)者和核心開發(fā)者,我對(duì)系統(tǒng)的每一個(gè)細(xì)節(jié)都了如指掌。正所謂“船小好調(diào)頭”,即使是數(shù)據(jù)庫大規(guī)模遷移這樣的“大手術(shù)”,我也能快速?zèng)Q策、高效執(zhí)行。事實(shí)上,Chats 數(shù)據(jù)庫已經(jīng)經(jīng)歷過多次重要的數(shù)據(jù)遷移,各位可以通過項(xiàng)目倉庫中的數(shù)據(jù)庫遷移腳本了解詳情:https://github.com/sdcb/chats/tree/ebefd93cb187961f8c69dcf04163433ce753a5f3/src/scripts/db-migration。 將主鍵從 Guid 切換為自增 int ID,最直接的好處就是性能的提升,具體體現(xiàn)在以下幾個(gè)方面:
當(dāng)然,從 Guid 遷移到自增 ID 并非一帆風(fēng)順,最大的挑戰(zhàn)在于數(shù)據(jù)遷移的復(fù)雜性。我們需要編寫嚴(yán)謹(jǐn)?shù)臄?shù)據(jù)遷移腳本,確保數(shù)據(jù)遷移過程中數(shù)據(jù)的一致性和完整性不受破壞。同時(shí),還需要仔細(xì)評(píng)估遷移可能帶來的業(yè)務(wù)影響,例如外鍵關(guān)聯(lián)的更新,以及應(yīng)用程序代碼的調(diào)整。幸運(yùn)的是,正如前面提到的,Chats 項(xiàng)目已經(jīng)積累了多次數(shù)據(jù)庫遷移的經(jīng)驗(yàn),這為我們這次從 Guid 到自增 ID 的遷移奠定了堅(jiān)實(shí)的基礎(chǔ)。 例如,這段 339 行的 C# 數(shù)據(jù)庫遷移腳本(在 LINQPad 中編寫): 在遷移腳本中,我們使用了類似
通過這樣的映射,我們可以在數(shù)據(jù)遷移過程中,將舊的 Guid 主鍵平滑地轉(zhuǎn)換為新的自增 ID,并確保數(shù)據(jù)關(guān)聯(lián)關(guān)系的正確性。 第三階段 - 安全升級(jí):界面 ID 加密完成數(shù)據(jù)庫主鍵從 Guid 到自增 ID 的遷移后,我們又面臨了新的問題:如何在用戶界面上安全地展示 ID。最初,我們直接沿用了數(shù)據(jù)庫的自增 ID,將其暴露在前端界面和 API 接口中。然而,這種做法很快引發(fā)了一些安全性和用戶體驗(yàn)方面的問題。 例如,當(dāng)創(chuàng)建一個(gè)新的聊天會(huì)話時(shí),界面 URL 可能會(huì)顯示為 此外,從用戶體驗(yàn)的角度來看,連續(xù)的數(shù)字 ID 也顯得不夠?qū)I(yè)和優(yōu)雅。用戶可能會(huì)覺得這些 ID 過于簡(jiǎn)單和隨意,與他們對(duì)現(xiàn)代聊天應(yīng)用的期望不符。 為了解決這些問題,我們決定對(duì)界面上顯示的 ID 進(jìn)行加密處理。 最初,我使用了這段 C# 代碼來實(shí)現(xiàn)整數(shù) ID 的加密:
在這個(gè)實(shí)現(xiàn)中,整數(shù) ID 首先被轉(zhuǎn)換為小端序的字節(jié)數(shù)組,然后使用 AES 算法的 CBC 模式進(jìn)行加密。加密后的數(shù)據(jù)被編碼為 Base64 URL 格式,以便在 URL 中安全傳輸。最終,URL 可能呈現(xiàn)為如下形式: 通過這種加密方式,我們不僅提升了系統(tǒng)的安全性,也改善了用戶體驗(yàn)。加密后的 ID 看起來更加復(fù)雜和專業(yè),有效避免了簡(jiǎn)單數(shù)字序列帶來的潛在問題。 其中,初始化向量 IV 的生成方式如下。我定義了一個(gè)
每個(gè)枚舉值代表一種加密目的。代碼會(huì)根據(jù)不同的目的生成不同的 IV。這樣做的目的是確保即使同一個(gè)整數(shù) ID 在不同的上下文(例如 ChatId 和 FileId)中被加密,也會(huì)產(chǎn)生不同的加密結(jié)果。這種做法提升了安全性和靈活性,我們可以在不同的場(chǎng)景下復(fù)用相同的加密機(jī)制,而無需擔(dān)心 ID 重復(fù)或沖突。 可能有朋友會(huì)問,為什么不使用隨機(jī) IV,并將 IV 添加到加密后的 ID 中,這樣安全性不是更高嗎?這主要是基于以下兩點(diǎn)考慮: 首先,前端的某些計(jì)算邏輯依賴于穩(wěn)定的 ID。在我們的前端代碼中,特別是在聊天會(huì)話管理和消息渲染方面,我們大量使用了基于 ID 的緩存和狀態(tài)管理機(jī)制。例如,當(dāng)用戶在一個(gè)聊天窗口中滾動(dòng)瀏覽消息時(shí),前端會(huì)根據(jù)消息的 ID 來渲染消息之間的父子關(guān)系。如果使用隨機(jī) IV,即使是同一個(gè)聊天會(huì)話,在不同的時(shí)間或不同的上下文中被加密,生成的 ID 都會(huì)不同。這會(huì)導(dǎo)致前端緩存失效,狀態(tài)管理混亂,最終引發(fā)難以追蹤的 bug。想象一下,用戶明明還在同一個(gè)聊天中,但由于 ID 變化,前端卻認(rèn)為這是一個(gè)新的聊天,之前的消息緩存全部失效,這無疑會(huì)造成糟糕的用戶體驗(yàn)。為了保證前端邏輯的穩(wěn)定性和可預(yù)測(cè)性,我們需要確保在同一上下文中,同一個(gè)整數(shù) ID 加密后的結(jié)果始終一致。 其次,不固定的 IV 會(huì)顯著增加 ID 的長(zhǎng)度。如果將隨機(jī)生成的 IV 也附加到加密后的 ID 中,最終的 ID 長(zhǎng)度會(huì)大大增加。AES 算法的 IV 通常為 16 字節(jié),轉(zhuǎn)換為 Base64 URL 編碼后,會(huì)增加約 21 個(gè)字符的長(zhǎng)度(16 * 4 / 3 ≈ 21.3)。原本加密后的 ID 已經(jīng)比純數(shù)字 ID 長(zhǎng)了不少,如果再加上 20 多個(gè)字符的 IV,整個(gè) ID 會(huì)顯得非常臃腫,尤其是在 URL 中展示時(shí),既不美觀,也增加了 URL 的長(zhǎng)度負(fù)擔(dān)。我們希望在保證安全性的前提下,盡可能保持 ID 的簡(jiǎn)潔易用。 因此,綜合考慮前端的穩(wěn)定性和 ID 長(zhǎng)度,我們最終選擇了使用基于 第四階段 - 兼顧用戶感知:界面顯示為 Guid(當(dāng)前方案)經(jīng)過一段時(shí)間的實(shí)際運(yùn)行,我們意識(shí)到,雖然加密 ID 解決了安全性問題,但在某些場(chǎng)景下,用戶仍然希望看到一種更具辨識(shí)度的 ID 格式。因此,我們最終決定在界面上將 ID 顯示為 Guid 格式。 這種做法的優(yōu)點(diǎn)在于,Guid 格式的 ID 看起來更加隨機(jī)和復(fù)雜,更符合用戶對(duì)現(xiàn)代應(yīng)用的普遍認(rèn)知,同時(shí)也有效避免了直接暴露自增 ID 的問題。在具體實(shí)現(xiàn)上,我們將加密后的 ID 轉(zhuǎn)換為 Guid 格式進(jìn)行展示。這樣一來,用戶在界面上看到的 ID 既安全又專業(yè)。 細(xì)心的朋友可能已經(jīng)注意到,由于我的輸入長(zhǎng)度為 4 字節(jié)或 8 字節(jié)(分別對(duì)應(yīng) int32 和 int64 類型的 ID),AES CBC 加密后的輸出長(zhǎng)度固定為 16 字節(jié)(但前端代碼額外增加了一個(gè)字節(jié)作為版本號(hào)前綴,固定為 0)。而一個(gè) Guid 的長(zhǎng)度恰好也是 16 字節(jié)。因此,只需將 Base64Url 序列化方式替換為 Guid 序列化,即可輕松將加密 ID 轉(zhuǎn)換為 Guid 形式: https://github.com/sdcb/chats/blob/r-407/src/BE/Services/UrlEncryption/Utils.cs#L50-L60
可能有朋友會(huì)進(jìn)一步追問,為什么堅(jiān)持使用 AES CBC 算法,而不是現(xiàn)在更流行的 AES GCM 算法呢?這又可以展開一篇長(zhǎng)文討論。簡(jiǎn)單來說: 首先,AES GCM 的隨機(jī)性高度依賴于 nonce 的唯一性。Nonce(Number used once)是一個(gè)一次性使用的隨機(jī)數(shù),在 AES GCM 中扮演著至關(guān)重要的角色,類似于 AES CBC 中的 IV(Initialization Vector,初始化向量)。如果 nonce 在多次加密中重復(fù)使用,尤其是在加密序列化的、遞增的 ID 時(shí),AES GCM 的安全性會(huì)大打折扣,甚至可能暴露出加密模式的規(guī)律性。 在我們的場(chǎng)景中,雖然我們?yōu)槊糠N 為了更直觀地說明問題,請(qǐng)看以下 C# 代碼示例:
輸出結(jié)果如下:
請(qǐng)注意觀察 相比之下,AES CBC 雖然也依賴 IV,但即使 IV 固定,只要密鑰安全,其加密結(jié)果的隨機(jī)性依然能得到較好的保證。尤其是在我們使用了填充模式(Padding)的情況下,即使輸入數(shù)據(jù)存在一定的規(guī)律性,也能有效地隱藏這種規(guī)律。 其次,AES GCM 的輸出長(zhǎng)度會(huì)顯著增加,難以適配 Guid 格式。AES GCM 在提供加密功能的同時(shí),還提供了數(shù)據(jù)完整性校驗(yàn)功能,這是通過附加一個(gè)認(rèn)證標(biāo)簽(Authentication Tag,簡(jiǎn)稱 Tag)來實(shí)現(xiàn)的。這個(gè) Tag 通常是 12 到 16 字節(jié),用于驗(yàn)證數(shù)據(jù)的完整性和真實(shí)性,防止數(shù)據(jù)被篡改。除了 Tag 之外,AES GCM 還需要一個(gè)顯式的 nonce 作為輸入。對(duì)于我們來說,nonce 至少需要 12 字節(jié)才能保證足夠的安全性。 這意味著,如果我們使用 AES GCM 加密一個(gè) 4 字節(jié)的 int32 ID,最終的輸出長(zhǎng)度將至少是:4 字節(jié)(密文) + 12 字節(jié)(nonce) + 12 字節(jié)(最小 Tag 大?。?= 28 字節(jié)。即使我們加密一個(gè) 8 字節(jié)的 int64 ID,輸出長(zhǎng)度也會(huì)超過 32 字節(jié)。這樣的長(zhǎng)度,無論如何都無法直接塞到一個(gè) 16 字節(jié)的 Guid 中。而且,為了將 nonce 和 tag 都塞進(jìn)去,我們勢(shì)必需要設(shè)計(jì)更復(fù)雜的序列化方案,這會(huì)增加前端和后端的處理復(fù)雜度,也可能導(dǎo)致 ID 格式的不統(tǒng)一,例如一部分 ID 是 Guid,一部分是更長(zhǎng)的 Base64 編碼字符串,這會(huì)給前端開發(fā)帶來額外的困擾。 我們之所以最終選擇將加密后的 ID 展示為 Guid 格式,一個(gè)重要的考量就是希望保持 ID 的統(tǒng)一性和簡(jiǎn)潔性。Guid 作為一個(gè) 16 字節(jié)的固定長(zhǎng)度標(biāo)識(shí)符,在很多場(chǎng)景下都非常方便使用和處理。如果我們?yōu)榱俗非?AES GCM 的“更高安全性”而犧牲了 ID 的簡(jiǎn)潔性和統(tǒng)一性,反而可能會(huì)得不償失。 最后,AES GCM 的額外安全優(yōu)勢(shì)在我們的應(yīng)用場(chǎng)景下并非不可或缺。AES GCM 最主要的優(yōu)勢(shì)在于它提供的認(rèn)證加密(Authenticated Encryption)功能,即在加密的同時(shí),也保證了數(shù)據(jù)的完整性和真實(shí)性。這意味著,如果數(shù)據(jù)在傳輸過程中被篡改,解密時(shí)會(huì)立即發(fā)現(xiàn)并報(bào)錯(cuò)。這種認(rèn)證功能對(duì)于一些對(duì)數(shù)據(jù)完整性要求極高的場(chǎng)景非常重要,例如金融交易、電子簽名等。 然而,在我們的 Chats 應(yīng)用中,我們對(duì) ID 的安全性需求主要集中在防止惡意猜測(cè)和未經(jīng)授權(quán)的訪問,而不是防止數(shù)據(jù)篡改。即使加密后的 ID 在傳輸過程中被篡改,最終解密出來的 ID 也大概率無法在數(shù)據(jù)庫中找到對(duì)應(yīng)的記錄,或者即使找到了,后續(xù)的業(yè)務(wù)邏輯也會(huì)進(jìn)行權(quán)限驗(yàn)證,確保用戶只能訪問自己擁有的聊天或消息。 更重要的是,即使我們使用 AES CBC,也并非完全沒有數(shù)據(jù)完整性驗(yàn)證機(jī)制。首先,AES CBC 配合填充模式(例如 PKCS7 Padding)本身就提供了一定程度的完整性校驗(yàn)。對(duì)于 int32 類型的 ID,AES CBC 加密后會(huì)生成 16 字節(jié)的密文,其中有 12 字節(jié)實(shí)際上是填充數(shù)據(jù)。如果密文被篡改,解密時(shí)填充校驗(yàn)會(huì)失敗,從而可以檢測(cè)到數(shù)據(jù)損壞。雖然這種校驗(yàn)強(qiáng)度不如 AES GCM 的 Tag 那么高,但也足以應(yīng)對(duì)一般的篡改嘗試。 其次,在我們的系統(tǒng)中,解密后的 ID 最終會(huì)用于數(shù)據(jù)庫查詢。即使攻擊者能夠繞過 AES CBC 的填充校驗(yàn),篡改了加密后的 ID,解密出來的錯(cuò)誤 ID 在數(shù)據(jù)庫中大概率也找不到對(duì)應(yīng)的記錄。即使碰巧找到了記錄,我們也會(huì)在數(shù)據(jù)庫層面和業(yè)務(wù)邏輯層面進(jìn)行多重權(quán)限驗(yàn)證,確保數(shù)據(jù)的安全性。 因此,綜合考慮以上三點(diǎn),我們最終權(quán)衡之后,仍然選擇了 AES CBC 算法。它在保證足夠安全性的前提下,能夠生成 16 字節(jié)的密文,完美適配 Guid 格式,并且實(shí)現(xiàn)相對(duì)簡(jiǎn)單,性能也更優(yōu)。當(dāng)然,技術(shù)選型永遠(yuǎn)是一個(gè)不斷演進(jìn)的過程,未來如果我們的安全需求發(fā)生變化,或者 AES GCM 在性能和易用性方面有了新的提升,我們也不排除會(huì)重新評(píng)估并切換到 AES GCM 的可能性。 總結(jié)與展望回顧 Sdcb Chats 項(xiàng)目 ID 演進(jìn)的四個(gè)階段,從最初擁抱 Guid 的“一步到位”,到為了性能考量轉(zhuǎn)向自增 ID,再到為了安全和體驗(yàn)在界面上加密 ID,最終又回歸到使用 Guid 形式展示,這的確是一段曲折而又充滿思考的旅程。 在這個(gè)過程中,我們不斷地在性能、安全性、用戶體驗(yàn)和開發(fā)效率之間權(quán)衡取舍。沒有一勞永逸的完美方案,只有在持續(xù)迭代和演進(jìn)中,才能找到最適合當(dāng)前階段的最佳實(shí)踐。每一次看似“倒退”的改動(dòng),實(shí)際上都基于更深入的理解和更全面的考量。例如,從 Guid 到自增 ID 的轉(zhuǎn)變,是為了解決實(shí)際存在的數(shù)據(jù)庫性能瓶頸;而界面上從加密 ID 到 Guid 的回歸,則是在安全性得到保障的前提下,更好地滿足用戶對(duì)“現(xiàn)代感”和“專業(yè)性”的用戶感知。 這段經(jīng)歷也印證了軟件開發(fā)中一個(gè)重要的理念:沒有銀彈。技術(shù)選型需要結(jié)合具體的應(yīng)用場(chǎng)景和需求,持續(xù)地監(jiān)控和評(píng)估,并根據(jù)實(shí)際情況靈活調(diào)整。我們不能因?yàn)椤按蠹叶颊f Guid 好”就盲目跟風(fēng),也不能因?yàn)椤靶阅苤辽稀本秃雎园踩院陀脩趔w驗(yàn)。只有深入理解各種方案的優(yōu)缺點(diǎn),才能做出最明智的選擇。 而 Sdcb Chats 項(xiàng)目的 ID 演進(jìn)之路,也正是開源項(xiàng)目不斷迭代、持續(xù)進(jìn)化的一個(gè)縮影。我們始終秉持著開放、務(wù)實(shí)的態(tài)度,積極擁抱變化,勇于嘗試新的技術(shù)方案,并不斷地從實(shí)踐中總結(jié)經(jīng)驗(yàn)教訓(xùn)。 如果您對(duì)我們這曲折的 ID 選型故事,以及 Sdcb Chats 項(xiàng)目本身感興趣,歡迎繼續(xù)了解! Sdcb Chats:一個(gè)強(qiáng)大的開源 ChatGPT 前端 我是開源項(xiàng)目 Sdcb Chats 的作者。Sdcb Chats 定位為一個(gè)強(qiáng)大且易于部署的 ChatGPT 前端,旨在幫助用戶輕松接入和管理各種主流的大語言模型。 Sdcb Chats 的主要特性包括:
無論您是個(gè)人開發(fā)者、技術(shù)愛好者,還是企業(yè)用戶,Sdcb Chats 都能為您提供一個(gè)強(qiáng)大、靈活、易用的 ChatGPT 前端解決方案。 如果您覺得 Sdcb Chats 對(duì)您有所幫助,或者您認(rèn)同我們的技術(shù)理念和開源精神,請(qǐng)?jiān)?GitHub 上給我們一個(gè) Star ?。您的支持是我們持續(xù)前進(jìn)的最大動(dòng)力! GitHub 倉庫地址: https://github.com/sdcb/chats 希望這篇博客和項(xiàng)目介紹能幫助您對(duì) Sdcb Chats 項(xiàng)目有更深入的了解。期待您的關(guān)注和參與,讓我們一起打造更優(yōu)秀的開源項(xiàng)目! 轉(zhuǎn)自https://www.cnblogs.com/sdcb/p/18691585/sdcb-chats-id-in-url 該文章在 2025/2/5 9:49:38 編輯過 |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |