帳號可用性檢查:Bloom Filter 只負責快,唯一約束才負責正確
即時顯示帳號名稱可用,不應把每次按鍵都送進主資料庫。實用架構是前端驗證、debounce 與取消舊請求,再用 Bloom filter 排除大多數『一定沒用過』的名稱,剩餘少數才查快取或唯讀副本;真正註冊時仍由資料庫唯一約束處理競態。
註冊頁上的綠色勾勾看似只是一個資料庫查詢,但在高流量產品中,每個按鍵都可能放大成後端負載。更穩健的設計不是追求一個永遠正確的預查,而是把問題拆成:提示要快,寫入才必須正確。
四層架構
1. 前端先減少根本不該送出的請求
- 本地格式驗證:長度、允許字元、保留名稱等規則先在客戶端擋掉,伺服器仍需再次驗證。
- Debounce:使用者停止輸入約 250ms 後才查詢,避免每個字元都觸發 request。
- 取消舊請求:輸入改變就取消上一個查詢,防止較慢的舊回應覆蓋較新的結果。
原文的核心原則是:最便宜的 request,就是根本沒有送出去的 request。
2. Bloom filter 快速回答「一定不在集合裡」
Bloom filter 用位元陣列和多個雜湊函式表示已占用名稱。只要查到任一位元為 0,就能確定名稱從未被加入;若所有位元都是 1,只能說「可能已被使用」,因為會有 false positive。
這種不對稱正適合帳號檢查:大部分新名稱可在記憶體中以微秒級速度排除,只有「可能被用過」的少數請求才需要查儲存層。
3. 快取或唯讀副本處理「可能已占用」
Bloom filter 的 maybe 再交給 Redis、熱門名稱快取,或具有唯一索引的唯讀副本。即使快取過期或 replica lag,把剛被註冊的名稱暫時顯示成可用,也不會造成資料完整性破壞,因為預查本來就只是提示。
4. 資料庫唯一約束決定真正擁有者
兩位使用者可能同時看到同一名稱的綠勾並送出註冊。任何 check-then-write 流程都存在競態,只有寫入當下的 unique constraint 能保證唯一:第一筆 INSERT 成功,第二筆得到衝突並回覆「名稱剛被使用」。
Availability check is a hint; the insert is the answer.
為什麼前面幾層可以稍微不準
| 層級 | 目的 | 可能不準嗎 |
|---|---|---|
| 前端驗證/debounce | 消除無效流量 | 格式規則本身不應不準 |
| Bloom filter | 快速判斷 definitely absent | 可 false positive,也可能因副本稍舊而暫時 false negative |
| 快取/唯讀副本 | 便宜查詢 maybe | 可因 cache staleness 或 replica lag 延遲 |
| 唯一約束 | 保證真正唯一 | 不可妥協 |
實作時還要補上的邊界
- 刪除:一般 Bloom filter 無法安全清除單一項目;可定期重建或使用 counting Bloom filter。