Note: For the coding companion problem, please see: Encode and Decode TinyURL.
How would you design a URL shortening service that is similar to TinyURL?
Background:
TinyURL is a URL shortening service where you enter a URL such as https://leetcode.com/problems/design-tinyurl
and it returns a short URL such as http://tinyurl.com/4e9iAk
.
Requirements:
- For instance, "http://tinyurl.com/4e9iAk" is the tiny url for the page
"https://leetcode.com/problems/design-tinyurl"
. The identifier (the highlighted part) can be any string with 6 alphanumeric characters containing0-9
,a-z
,A-Z
. - Each shortened URL must be unique; that is, no two different URLs can be shortened to the same URL.
Note about Questions:
Below are just a small subset of questions to get you started. In real world, there could be many follow ups and questions possible and the discussion is open-ended (No one true or correct way to solve a problem). If you have more ideas or questions, please ask in Discuss and we may compile it here!
Questions:
- How many unique identifiers possible? Will you run out of unique URLs?
- Should the identifier be increment or not? Which is easier to design? Pros and cons?
- Mapping an identifier to an URL and its reversal - Does this problem ring a bell to you?
- How do you store the URLs? Does a simple flat file database work?
- What is the bottleneck of the system? Is it read-heavy or write-heavy?
- Estimate the maximum number of URLs a single machine can store.
- Estimate the maximum number of queries per second (QPS) for decoding a shortened URL in a single machine.
- How would you scale the service? For example, a viral link which is shared in social media could result in a peak QPS at a moment's notice.
- How could you handle redundancy? i,e, if a server is down, how could you ensure the service is still operational?
- Keep URLs forever or prune, pros/cons? How we do pruning? (Contributed by @alex_svetkin)
- What API would you provide to a third-party developer? (Contributed by @alex_svetkin)
- If you can enable caching, what would you cache and what's the expiry time? (Contributed by @Humandroid)
這道系統設計的題跟之前的算法還是不一樣的,代碼只是其中的一部分,估計大部分還是要跟面試官侃大山,博主也不太熟悉這類題目,還是照着ztlevi大神的帖子來寫吧。
S: Scenario 場景
長URL和短URL的相互轉換
N: Need 需求
- QPS (Queires Per Second) 每秒查詢數
- 日活用戶:100M
- 每日人均使用量:(寫)long2short 0.1,(讀) short2long 1
- 每日請求量:寫 10M,讀 100M
- QPS:一天共有86400秒,約100K。寫 100, 讀 1K
- 峰值QPS:寫 200, 讀 2K
(千級的量可以用一個單SSD的MySQL機器來處理)
- Storage 存儲
- 每天10M個新映射(長URL到短URL)
- 一個映射大約占100B的大小
- 每天1GB,1TB大約能扛三年
對於這種系統來說,存儲不是問題。只有像Netflix那樣的系統可能會有存儲問題。通過SN分析,我們對系統有了一個大框架印象,這個系統可以使用單SSD機器來實現。
A: API 接口
只有一種類型的服務:URLService
- Core (Business Logic) Layer
- Class: URLService
- Interface:
- URLService.encode(string long_url)
- URLService.decode(stirng short_url)
- Web Layer
- REST API:
- GET: /{short_url}, return a http redirect response (301)
- POST: goo.gl method - google shorten URL
Request Body: {url=longUrl} e.g. {"longUrl": "http://www.google.com/"}
Return OK(200), short_url is included in the data
K: Data Access 數據訪問
Step 1: Pick a storage structure 選擇一個存儲結構
- SQL VS NoSQL?
- 需要支持事務Transactions嗎?NoSQL不支持事務Transactions。
- 需要Rich SQL Query嗎? NoSQL不支持SQL那么多的Query。
- 需要高效開發嗎?大多數的網絡框架對SQL的支持性非常好,意味着系統不需要太多的代碼。
- 需要AUTO_INCREMENT ID嗎? NoSQL不支持這個,僅有一個全局衛衣的Object_id。
- 需要高QPS嗎?NoSQL有高性能。比如Memcached的QPS可達到百萬級,MondoDB可達萬級,MySQL只有千級。
- 系統的可伸縮性Scalability有多高?SQL需要開發者寫代碼去伸縮Scale,而NoSQL自帶該功能(Sharding,replica)。
- Answer 回答:
- 不需要 -> NoSQL
- 不需要 -> NoSQL
- 無所謂,因為只有很少的代碼 -> NoSQL
- 算法需要AUTO_INCREMENT ID -> SQL
- 寫 200,讀 2K,不高 -> SQL
- 不高 -> SQL
- System Alogrithm 系統算法
- Hash 函數
long_url => md5/sha1
- md5將一個字符串轉為128位,通常用16個字節的十六進制來表示:
http://site.douban.com/chuan -> c93a360dc7f3eb093ab6e304db516653
- sha1將字符串轉為160位,通常用20個字節的十六進制來表示:
http://site.douban.com/chuan -> dff85871a72c73c3eae09e39ffe97aea63047094
這兩個算法使得哈希值是隨機分布的,但是沖突Conflicts無法避免。任何哈希算法都無法避免沖突問題。
- 優點:簡單。我們用轉換字符串的前6個字符
- 缺點:沖突
解決方法 1. 使用(long_url + timestamp)作為哈希函數的關鍵字Key。2. 當沖突時,重新生成哈希值(生成的值不同因為時間戳改變了)。
總之,當urls的個數超過十億個,可能會有大量的沖突使得系統不高效。
- base62
將short_url用62 base標記。6位可以表示62^6 57 billion。
每個short_url表示一個十進制數,可以當作SQL數據庫中的AUTO_INCREMENT ID。
class URLService { public: URLService() { COUNTER = 1; elements = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; } string longToShort(string url) { string short_url = base10ToBase62(COUNTER); long2short[url] = COUNTER; short2long[COUNTER] = url; ++COUNTER; return "http://tiny.url/" + short_url; } string shortToLong(string url) { string prefix = "http://tiny.url/"; url = url.substr(prefix.size()); int n = base62ToBase10(url); return short2long[n]; } int base62ToBase10(string s) { int n = 0; for (int i = 0; i < s.size(); ++i) { n = n * 62 + convert(s[i]); } return n; } int convert(char c) { if (c >= '0' && c <= '9') { return c - '0'; } else if (c >= 'a' && c <= 'z') { return c - 'a' + 10; } else if (c >= 'A' && c <= 'Z') { return c - 'A' + 36; } return -1; } string base10ToBase62(int n) { string str = ""; while (n != 0) { str.insert(str.begin(), elements[n % 62]); n /= 62; } while (str.size() != 6) { str.insert(str.begin(), '0'); } return str; } private: unordered_map<string, int> long2short; unordered_map<int, string> short2long; int COUNTER; string elements; };
Step 2: Database Schema 數據庫概要
一個表(id, long_url)。id是主鍵,通過long_url排序。基本的系統架構為:
Browser <-> Web <-> Core <-> DB
O: Optimize 優化
如何提高響應速度?
- 在網絡服務器和數據庫之間提高響應速度
使用Memcached來提高響應速度。當獲得long_url時,先在緩存中搜索。我們可以把90%的讀請求放在緩存當中。
- 在網絡服務器和用戶瀏覽器之間提高響應速度
不同的地區使用不同的網絡服務器和緩存服務器。所有的地區共享一個數據庫用來匹配用戶到最近的網絡服務器(通過DNS),當他們不在緩存中的時候。
如果我們需要多於一台的MySQL機器?
- 問題:
- 緩存用完了
- 越來越多的請求
- 越來越多的緩存丟失
- 解決方案:
- 垂直切分 Vertical Sharding
- 水平切分 Horizontal Sharding
最好的方式是水平切分。當前的表結構是(id, long_url),哪列可以當作切分關鍵字。
一個簡單的方法是id模塊切分。
現在有另一個問題:如何能使多個機器共享一個全局的AUTO_INCREMENT ID?
兩種方法:1. 多使用一個機器去維護id。2. 使用zookeeper。都很操蛋。
所以,我們不適用AUTO_INCREMENT ID
好處是將切分關鍵字當作short_url的第一個字節。
另一種方法是用統一的哈希將循環斷成62份。有多少份並沒有啥關系,因為可能並沒有62台機器(可能有360或其他的)。每台機器都是為循環的一部分的服務負責。
write long_url -> hash(long_url)%62 -> put long_url to the specific machine according to hash value -> generate short_url on this machine -> return short_url
short_url request -> get the sharding key (first byte of the short_url) -> search in the corresponding machine based on sharding key -> return long_url
每當我們增加一台新機器,將最多使用的機器的一半范圍放到心的機器中。
更多優化
將中文服務器放在中國,美國的服務器放在美國。使用地理信息當作切分關鍵字,例如,0是中國的網站,1是美國的網站。
參考資料:
https://discuss.leetcode.com/topic/95853/a-complete-solution-for-tinyurl-leetcode-system-design