九章系統設計 TinyURL筆記
SN-提問:qps+容量
AK-畫圖:流程,可行解
E-優化:流量/存儲mysql
長到短
短到長程序員
日活用戶:100M
每人天天使用:(寫)長到短0.1,(讀)短到長1
日request:寫10M,讀100M
qps:寫100,讀1K
peak qps: 寫200,讀2K
(千級別的qps能夠單臺SSD MySQL搞定)web
天天新產生10M個長連接<->短連接映射
每一個映射(長連接+短連接)平均大小100B
天天1GB,1T的硬盤能夠用3年
存儲對於這種系統不是問題,netflix這種纔有存儲上的問題面試
經過SN的分析,能夠知道要作多大致量的系統,能夠感受到這個系統不是很難,單臺電腦配個ssd硬盤,用mysql就能夠搞定
下面開始AK細化流程,畫圖,給出可行解算法
就一個service,URLService.sql
Core(Business Logic)層:
類:URLService
接口:數據庫
URLService.encode(String long_url)api
URLService.decode(String short_url)瀏覽器
Web層:
REST API:緩存
GET: /{short_url}
返回一個http redirect response(301)
POST
goo.gl作法: POST: https://goo.gl/api/shorten
bit.ly作法: POST: https://bitly.com/data/shorten
Request Body: {url=long_url}
返回OK(200),data裏帶着生成的short_url
第一步:select 選存儲結構 -> 內存 or 文件系統 or 數據庫 -> SQL or NoSQL?
第二步:schema 細化數據表
SQL vs NoSQL?
是否須要支持Transaction?
NoSQL不支持Transaction.
是否須要豐富的SQL Query?
NoSQL的SQL Query豐富度不如SQL,不過目前差距正在縮小.
是否追求效率(想偷懶)?
大多數Web Framework與SQL數據庫兼容得很好(自帶ORM),意味着能夠少些不少代碼.
是否須要AUTO_INCREMENT ID?
NoSQL作不到1,2,3,4,5...NoSQL只能作到一個全局unique的Object_id.
對QPS要求高不高?
NoSQL性能高,好比Memcached的qps能夠到million級別,MondoDB能夠到10k級別,MySQL只能在K這個級別.
對Scalability的要求有多高?
SQL須要程序員本身寫代碼來scale; NoSQL這些都是自帶的(sharding,replica).
Mark一下,寫到這的時候,收到hr的郵件,因爲uberChina和滴滴出行合併,面試推遲,系統設計的學習也要告一段落了,很少說。。
綜上:
transaction? 不須要 -> nosql
sql query? 不須要 -> nosql
是否追求效率? 原本也沒多少代碼 -> nosql
對qps要求高? 讀2k,寫200,真心不高 -> sql
對scalability要求高? 存儲和QPS要求都不高,單機就能夠了 -> sql
要auto_increment_id? 咱們的算法要! -> sql
ok,那麼來聊一聊系統的算法是什麼,有以下方案:
1.hash function:
把long_url用md5/sha1哈希
md5把一個string轉化成128位二進制數,通常用32位十六進制數(16byte)表示:
http://site.douban.com/chuan -> c93a360dc7f3eb093ab6e304db516653
sha1把一個string轉化成160位二進制數,通常用40位十六進制數(20byte)表示:
http://site.douban.com/chuan -> dff85871a72c73c3eae09e39ffe97aea63047094
這兩個算法能夠保證哈希值分佈很隨機,可是衝突是不可避免的,任何一個哈希算法都不可避免有衝突。
優勢:簡單,能夠根據long_url直接生成;假設一個url中一個char佔兩個字節,平均長度爲30的話,原url佔大小60byte,hash以後要16byte。咱們能夠取md5的前6位,這樣就更節省。
缺點:難以保證哈希算法沒有衝突
解決衝突方案:1.拿(long_url + timestamp)來哈希;2.衝突的話,重試(timestamp會變,會生成新的hash)
綜上,流量很少時,可行;可是,當url超過了假設1 billion的時候,衝突會很是多,效率很是低。
2.base62:
將六位的short_url看作是一個62進制數(0-9,a-z,A-Z),能夠表示62^6=570億個數。整個互聯網的網頁數在trillion級別,即一萬億這個級別。6位足夠。
每一個short_url對應一個十進制整數,這個整數就能夠是sql數據庫中的自增id,即auto_increment_id。
public class URLService { HashMap<String, Integer> ltos; HashMap<Integer, String> stol; static int COUNTER; String elements; URLService() { ltos = new HashMap<String, Integer>(); stol = new HashMap<Integer, String>(); COUNTER = 1; elements = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; } public String longToShort(String url) { String shorturl = base10ToBase62(COUNTER); ltos.put(url, COUNTER); stol.put(COUNTER, url); COUNTER++; return "http://tiny.url/" + shorturl; } public String shortToLong(String url) { url = url.substring("http://tiny.url/".length()); int n = base62ToBase10(url); return stol.get(n); } public int base62ToBase10(String s) { int n = 0; for (int i = 0; i < s.length(); i++) { n = n * 62 + convert(s.charAt(i)); } return n; } public int convert(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'z') { return c - 'a' + 10; } if (c >= 'A' && c <= 'Z') { return c - 'A' + 36; } return -1; } public String base10ToBase62(int n) { StringBuilder sb = new StringBuilder(); while (n != 0) { sb.insert(0, elements.charAt(n % 62)); n /= 62; } while (sb.length() != 6) { sb.insert(0, '0'); } return sb.toString(); } }
一個表:兩列(id,long_url),其中id爲主鍵(自帶index),long_url將其index,這樣一張表能夠雙向查!
到如今有了work solution!能夠達到weak hire。
基本的系統架構是:
瀏覽器 <-> Web <-> Core <-> DB
讀:利用Memcached提升響應速度,get的時候先去cache找,沒有就從數據庫裏找;能夠把90%的讀請求都引流到cache上
不一樣地區,使用不一樣的Web服務器和緩存服務器,全部地區share一個db,用於緩存沒hit的狀況
經過動態DNS解析能夠把不一樣地區的用戶match到最近的Web服務器
面臨問題:
Cache資源不夠
寫操做愈來愈多
愈來愈多的cache miss率
怎麼作:
拆數據庫。
拆數據庫有兩種,一種是把不一樣的表放到不一樣的機器(vertical sharding),另外一種是把數據散列到不一樣的機器(horizontal)。
最好用的是horizontal sharding。
當前的表結構是:(id, long_url),既須要用id查long_url,也須要用long_url查id,如何分,把哪列做爲sharding key呢?
一個簡單可行的辦法是,按id取模sharding,由於讀(短到長)的需求是主要的;寫的時候就廣播給全部機器,因爲機器不會太多,也是可行的。
此時一個新的問題來了,n臺機器如何共享一個全局自增id?
兩個辦法:開一臺新的機器專門維護這個全局自增id,或者用zookeeper。都很差。
因此咱們不用全局自增id。
業內的作法是,把sharding key做爲第一位直接放到short_url裏。這樣就不須要全局自增id,每臺機器自增就行了。
用consistent hashing將環分爲62份(這個無所謂,由於預估機器不會超過這個數目,也能夠設成360或者別的數,每次新加一個機器能夠把區間最大的分一半)每一個機器在環上負責一段區間。
具體作法:
新來一個long_url -> hash(long_url)%62 -> 把long_url放到hash value對應的機器裏 -> 在這臺機器上生成short_url -> 返回short_url
來一個short_url請求 -> 提取short_url的第一位獲得sharding key -> 到sharding key對應的機器裏找 -> 返回long_url
新增一臺機器 -> 找原來機器裏負責range(0-61)最大的機器 -> 將其range減半 -> 把一半放到新增機器上
中國的db放到中國,美國的db放到美國。
用地域信息做爲sharding key,好比中國網站都放到0開頭的,美國網站都放在1開頭的。
單獨建一張表,存custom_url <--> long_url當查詢時,先從custom表裏查,再從url表裏查。注意,千萬不要在url表裏插一列custom,這樣這列大部分的值爲空。