系統設計:設計 URL 短鏈接工具

這是一個系統設計問題,要求從頭開始設計一個類似於 TinyURL 或 Bitly 的 URL 短鏈接工具。我們將涵蓋從設計需求、架構和組件設計到高性能擴展和安全最佳實踐的各個方面。

定義範圍:功能性和非功能性需求

首先,我們需要定義該系統的功能性和非功能性需求。

我們有兩個功能性需求:

  1. 給定一個長 URL 時,我們必須創建一個短 URL2. 給定一個短 URL 時,我們必須將用戶重定向到長 URL。

該服務的非功能性需求是優先考慮低延遲(快速響應)和高可用性(始終在線)。

明確業務問題

以下是一些我們可能需要明確的問題,以確保我們對系統的規模有一致的理解:

使用情況:估計我們每秒需要創建多少個 URL(假設是 1000 個)。• 字符:我們可以使用數字和字母(字母數字)還是其他符號?(我們假設使用字母數字字符)。• 唯一性:每次生成的短 URL 是否唯一,即使多個用戶提交相同的長 URL?(在這個設計中,我們假設是唯一的)。

估算:數據計算

有了這些信息,我們需要計算縮短後的 URL 應該有多長。當然,我們希望它儘可能短,但我們需要考慮到每年的 URL 創建數量。

首先,讓我們估算一個顯著時期內所需的唯一 URL 數量。常見的方法是計劃至少幾年的運營。爲了簡化計算,我們假設計算 10 年的數據。

一年中的秒數:每分鐘 60 秒 × 每小時 60 分鐘 × 每天 24 小時 × 每年 365 天 = 31.536 百萬秒 •10 年中的總秒數:31.536 百萬 × 10 = 315.36 百萬秒 •10 年中的總 URL 數:1000 × 315.36 百萬 = 315.36 十億個唯一 URL

這意味着我們的數據庫每秒需要處理 1000 次寫入,每年將生成 1000 × 60 × 60 × 24 × 365 = 31.5B 個 URL。如果我們假設讀取次數通常是寫入次數的 10 倍,這意味着我們每秒將獲得超過 10 × 1000 = 10000 次讀取

現在,我們需要弄清楚多少個字符能爲我們未來十年的量提供足夠的唯一短 URL。考慮到字符集大小爲 62,可以按如下計算 URL 標識符的長度:

•62¹ = 62 個唯一 URL(1 個字符)•62² = 3844 個唯一 URL(2 個字符)•… 等等。

繼續這種計算,我們看到 62⁷(大約 3.5 萬億)是第一個大於我們預計的 3150 億 URL 所需的值。

因此,爲了支持我們未來十年的預期增長,我們的縮短 URL 需要至少 7 個字符。

高層次架構

我們的系統將有以下關鍵組件:

用戶:用戶發送他們的長 URL 以生成短 URL,或發送短 URL,我們需要將他們重定向到長 URL。

負載均衡器:所有這些請求通過負載均衡器,它將流量分配到多個 Web 服務器實例,以確保高可用性和負載均衡。

Web 服務器:這些服務器副本負責處理傳入的 HTTP 請求。

URL 短鏈接服務:我們還需要一個包含生成短 URL、存儲 URL 映射和檢索原始 URL 以進行重定向的核心邏輯的 URL 短鏈接服務。

數據庫:存儲短 URL 及其長 URL 之間的連接。在設計數據庫之前,我們需要考慮縮短 URL 的潛在存儲需求。

每個 URL 將包括唯一標識符(大約 7 個字節)、長 URL(最多 100 個字節)和用戶元數據(估計爲 500 個字節)。這意味着我們每個 URL 需要最多 1000 個字節。根據我們的預期量,這相當於大約 315TB 的數據。

在繼續之前,讓我們先考慮一下單個 Web 服務器的 API 設計。

API 設計

讓我們定義服務的基本 API 操作。根據我們的功能需求,我們將使用 REST API,並需要兩個端點。

1. 創建短 URL (POST **/urls**)

輸入:包含長 URL 的 JSON 負載 {“longUrl”: “[https://example.com/very-long-url](https://example.com/very-long-url)"}

輸出:帶有短 URL 的 JSON 負載 {“shortUrl”: “[https://tiny.url/3ad32p9](https://tiny.url/3ad32p9)"} 和 201 Created 狀態碼。

如果請求無效或格式錯誤,我們將返回 400 Bad Request 響應,如果請求的 URL 已經存在於系統中,我們將返回 409 Conflict

2. 重定向到長 URL (GET **/urls/{shortUrlId}**)

輸入:shortUrlId 路徑參數

輸出:帶有 301 Moved Permanently 的響應,響應體中包含新創建的短 URL 作爲 JSON { "shortUrl": "https://tiny.url/3ad32p9" }

301 狀態碼指示瀏覽器緩存信息,這意味着下次用戶輸入短 URL 時,瀏覽器會自動重定向到長 URL 而不需要再次訪問服務器。

然而,如果你想跟蹤每個請求的分析並確保它通過你的系統,可以使用 302 狀態碼。

數據庫:存儲短 URL

下一部分是數據庫層。該層存儲短 URL 和長 URL 之間的映射。它應該針對快速讀寫操作進行優化。

模式可以很簡單:短 URL id 的主鍵,以及長 URL 和可能的創建元數據字段。

{
  "shortUrlId": "3ad32p9",
  "longUrl": "https://example.com/very-long-url",
  "creationDate": "2024-03-08T12:00:00Z",
  "userId": "user123",
  "clicks": 1023,
  "metadata": {
    "title": "Example Web Page",
    "tags": ["example", "web", "url shortener"],
    "expireDate": "2025-03-08T12:00:00Z"
  },
  "isActive": true
}

在這裏,我們主要需要考慮數據庫的讀取次數。如果我們通常每秒有 1000 次寫入,那麼我們可以假設至少每秒有 10 到 100000 次讀取。

在這種情況下,我們需要使用支持快速讀取和寫入的高性能數據庫。這意味着我們需要使用 NoSQL 數據庫(如 MongoDB 這樣的文檔存儲、Cassandra 這樣的寬列存儲或 DynamoDB 這樣的鍵值存儲),因爲它們專門設計用於處理大量的擴展。

它不會是 ACID 兼容的,但我們不關心這一點,因爲我們不會進行大量的 JOIN 或複雜的查詢,我們不需要那些 ACID 規則和原子事務。

URL 短鏈接服務

該系統的核心部分之一是 URL 短鏈接服務。該服務生成短 URL,且不會在不同的長 URL 指向相同的短 URL 時引入衝突。

有多種方法可以實現這個服務;以下是其中一些:

• 哈希:生成長 URL 的哈希,並使用其中的一部分作爲標識符。然而,哈希可能導致衝突。• 自增 ID:使用數據庫的自增 ID 並將其編碼爲一個短字符串。這確保了唯一性,但可能是可預測的。• 自定義算法:設計一個自定義算法,用字符的混合來生成唯一 ID,以確保唯一性和不可預測性。

例如,爲了避免衝突,有一個非常簡單的方法——我們可以生成

所有可能的 7 字符鍵,並將它們存儲在數據庫中作爲鍵,其中鍵是生成的 URL,值是布爾值;如果爲 true,則表示該 URL 已被使用,如果爲 false,則可以使用該 URL 創建新映射。

因此,每當用戶請求生成一個鍵時,我們可以從這個數據庫中找到一個當前未使用的 URL,並將其映射到請求體中的長 URL。

你認爲我們在這種情況下會使用 SQL 還是 NoSQL 數據庫?考慮一種場景:兩個用戶請求縮短他們的長 URL,並且他們都被映射到這個數據庫中的同一個鍵。

在這種情況下,URL 將被映射到其中一個請求,另一個將被破壞。所以,我們將使用 SQL,因爲它具有 ACID 屬性。我們可以爲這裏的每個會話創建一個事務,以在隔離中執行這些步驟,在這種情況下我們不會有這種問題。

高可用性和低延遲

我們的當前系統顯然無法處理每秒 1000 個 URL 的流量。

緩存

爲了使其更具可擴展性,我們首先需要一個緩存層(例如 Redis)來緩存流行的 URL,以便在內存中快速檢索。

鑑於某些 URL 可能比其他 URL 訪問頻率更高,我們需要一種優先考慮頻繁訪問項的逐出策略。兩種適合此場景的緩存逐出策略是:

LRU 逐出策略:首先刪除最近最少訪問的項目。對於 URL 短鏈接服務,這種策略非常有效,因爲它確保緩存保持最新和最頻繁訪問的 URL,這可以顯著減少流行鏈接的訪問時間。• 或者基於 TTL 的逐出策略:爲每個緩存條目分配一個固定的生存時間(TTL)。一旦條目的 TTL 過期,它將從緩存中移除。對於只在短時間內流行的 URL,TTL 策略對 URL 短鏈接服務很有用。

TTL 還可以幫助我們自動刷新緩存內容,並可以與其他策略(如 LRU)結合使用,以更有效地管理緩存。

數據庫擴展:結合複製和分片

我們需要實現複製和分片策略,以確保數據庫支持高可用性、容錯性和可擴展性。

考慮到我們的 7 字符集有 3.5T 個唯一 URL,我們可以使用基於鍵的分片將 URL 記錄均勻分佈在多個分片上。

假設我們將其分佈在 3 個分片上,每個分片將存儲大約 1.16T 個 URL。這確保了隨着 URL 數量的增長系統的可擴展性。

我們還可以在每個分片內實現主從複製,以確保高可用性和容錯性。這種設置允許在節點故障時快速故障轉移和恢復。

另外,如果服務面向全球用戶,我們可以考慮地理分片和複製,以最小化延遲並改善不同地區的用戶體驗。

這種組合允許服務處理大量 URL 縮短和重定向,並且幾乎沒有停機時間和快速響應時間。

安全考慮

以下是我們服務的一些安全考慮:

輸入驗證:我們必須對用戶提交的每個 URL 進行消毒。我們必須檢查有效的協議(HTTP、HTTPS 等)並確保 URL 格式正確。這有助於防止注入攻擊。• 速率限制:我們可以通過限制單個源的請求次數來保護我們的服務免受 DDoS 攻擊。可以考慮使用令牌桶算法。• 監控和日誌記錄:需要一個強大的日誌記錄系統(如 ELK 堆棧)。它允許我們分析日誌以查找瓶頸和可疑活動,並確保整體系統健康。• 混淆:我們不希望輕易預測的短 URL。爲了阻止攻擊者猜測有效鏈接,我們可以在生成算法中添加隨機性。• 鏈接到期:可選地,我們可以考慮允許用戶爲他們的短 URL 設置到期日期。這可以限制潛在惡意鏈接的生命週期。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/NRbT2JQksWrkxmFBAw1ITA