數據庫架構(轉)
1. 業界難題-“跨庫分頁”的四種方案
1). 方法一:全局視野法
a.將order by time offset X limit Y,改寫成order by time offset 0 limit X+Y
b.服務層對得到的N*(X+Y)條數據進行內存排序,內存排序後再取偏移量X後的Y條記錄
這種方法隨著翻頁的進行,性能越來越低。
2). 方法二:業務折衷法-禁止跳頁查詢
a. 用正常的方法取得第一頁數據,並得到第一頁記錄的time_max
b. 每次翻頁,將order by time offset X limit Y,改寫成order by time where time>$time_max limit Y
以保證每次只返回一頁數據,性能為常量。
3). 方法三:業務折衷法-允許模糊數據(多個數據庫平均查出數據,損失一定的精度)
a. 將order by time offset X limit Y,改寫成order by time offset X/N limit Y/N
4). 方法四:二次查詢法
a. 將order by time offset X limit Y,改寫成order by time offset X/N limit Y
b. 找到最小值time_min
c. between二次查詢,order by time between $time_min and $time_i_max
d.設置虛擬time_min,找到time_min在各個分庫的offset,從而得到time_min在全局的offset
e. 得到了time_min在全局的offset,自然得到了全局的offset X limit Y
2. 單KEY業務,數據庫水平切分架構實踐
1).水平切分方法
a.範圍法,以用戶中心的業務主鍵uid為劃分依據,將數據水平切分到兩個數據庫實例上去:
user-db1:存儲0到1千萬的uid數據
user-db2:存儲1到2千萬的uid數據
a).範圍法的優點是:切分策略簡單,根據uid,按照範圍,user- center很快能夠定位到數據在哪個庫上 ; 擴容簡單,如果容量不夠,只要增加user-db3即可
b).範圍法的不足是:
uid必須要滿足遞增的特性
數據量不均,新增的user-db3,在初期的數據會比較少
請求量不均,一般來說,新註冊的用戶活躍度會比較高,故user-db2往往會比user-db1負載要高,導致服務器利用率不平衡
b.哈希法,也是以用戶中心的業務主鍵uid為劃分依據,將數據水平切分到兩個數據庫實例上去:
user-db1:存儲uid取模得1的uid數據
user-db2:存儲uid取模得0的uid數據
a).哈希法的優點是:切分策略簡單,根據uid,按照hash,user-center很快能夠定位到數據在哪個庫上
數據量均衡,只要uid是均勻的,數據在各個庫上的分布一定是均衡的
請求量均衡,只要uid是均勻的,負載在各個庫上的分布一定是均衡的
b).哈希法的不足是:擴容麻煩,如果容量不夠,要增加一個庫,重新hash可能會導致數據遷移,如何平滑的進行數據遷移,是一個需要解決的問題
2).用戶中心水平切分後帶來的問題:
對於uid屬性上的查詢可以直接路由到庫,假設訪問uid=124的數據,取模後能夠直接定位db-user1:對於非uid屬性上的查詢,例如login_name屬性上的查詢,就悲劇了:
假設訪問login_name=shenjian的數據,由於不知道數據落在哪個庫上,往往需要遍歷所有庫,當分庫數量多起來,性能會顯著降低。
3).根據樓主這些年的架構經驗,用戶中心非uid屬性上經常有兩類業務需求:
a.用戶側,前臺訪問,最典型的有兩類需求
用戶登錄:通過login_name/phone/email查詢用戶的實體,1%請求屬於這種類型
用戶信息查詢:登錄之後,通過uid來查詢用戶的實例,99%請求屬這種類型
用戶側的查詢基本上是單條記錄的查詢,訪問量較大,服務需要高可用,並且對一致性的要求較高。
b.運營側,後臺訪問,根據產品、運營需求,訪問模式各異,按照年齡、性別、頭像、登陸時間、註冊時間來進行查詢。
運營側的查詢基本上是批量分頁的查詢,由於是內部系統,訪問量很低,對可用性的要求不高,對一致性的要求也沒這麽嚴格。
這兩類不同的業務需求,應該使用什麽樣的架構方案來解決呢?
4).用戶中心水平切分架構思路
用戶中心在數據量較大的情況下,使用uid進行水平切分,對於非uid屬性上的查詢需求,架構設計的核心思路為:
針對用戶側,應該采用“建立非uid屬性到uid的映射關系”的架構方案
針對運營側,應該采用“前臺與後臺分離”的架構方案
5).用戶中心-用戶側最佳實踐
a.索引表法
思路:uid能直接定位到庫,login_name不能直接定位到庫,如果通過login_name能查詢到uid,問題解決
解決方案:
建立一個索引表記錄login_name->uid的映射關系
用login_name來訪問時,先通過索引表查詢到uid,再定位相應的庫
索引表屬性較少,可以容納非常多數據,一般不需要分庫
如果數據量過大,可以通過login_name來分庫
潛在不足:多一次數據庫查詢,性能下降一倍
b.緩存映射法:
思路:訪問索引表性能較低,把映射關系放在緩存裏性能更佳
解決方案:
login_name查詢先到cache中查詢uid,再根據uid定位數據庫
假設cache miss,采用掃全庫法獲取login_name對應的uid,放入cache
login_name到uid的映射關系不會變化,映射關系一旦放入緩存,不會更改,無需淘汰,緩存命中率超高
如果數據量過大,可以通過login_name進行cache水平切分
潛在不足:多一次cache查詢
c.login_name生成uid:
思路:不進行遠程查詢,由login_name直接得到uid
解決方案:
在用戶註冊時,設計函數login_name生成uid,uid=f(login_name),按uid分庫插入數據
用login_name來訪問時,先通過函數計算出uid,即uid=f(login_name)再來一遍,由uid路由到對應庫
潛在不足:該函數設計需要非常講究技巧,有uid生成沖突風險
d.login_name基因融入uid
思路:不能用login_name生成uid,可以從login_name抽取“基因”,融入uid中。假設分8庫,采用uid%8路由,潛臺詞是,uid的最後3個bit決定這條數據落在哪個庫上,這3個bit就是所謂的“基因”。
解決方案:在用戶註冊時,設計函數login_name生成3bit基因,login_name_gene=f(login_name),如上圖粉色部分
同時,生成61bit的全局唯一id,作為用戶的標識,如上圖綠色部分
接著把3bit的login_name_gene也作為uid的一部分,如上圖屎黃色部分
生成64bit的uid,由id和login_name_gene拼裝而成,並按照uid分庫插入數據
用login_name來訪問時,先通過函數由login_name再次復原3bit基因,login_name_gene=f(login_name),通過login_name_gene%8直接定位到庫
e.用戶中心-運營側最佳實踐
前臺用戶側,業務需求基本都是單行記錄的訪問,只要建立非uid屬性 login_name / phone / email 到uid的映射關系,就能解決問題。
後臺運營側,業務需求各異,基本是批量分頁的訪問,這類訪問計算量較大,返回數據量較大,比較消耗數據庫性能。
如果此時前臺業務和後臺業務公用一批服務和一個數據庫,有可能導致,由於後臺的“少數幾個請求”的“批量查詢”的“低效”訪問,導致數據庫的cpu偶爾瞬時100%,影響前臺正常用戶的訪問(例如,登錄超時)。
而且,為了滿足後臺業務各類“奇形怪狀”的需求,往往會在數據庫上建立各種索引,這些索引占用大量內存,會使得用戶側前臺業務uid/login_name上的查詢性能與寫入性能大幅度降低,處理時間增長。
對於這一類業務,應該采用“前臺與後臺分離”的架構方案:
戶側前臺業務需求架構依然不變,產品運營側後臺業務需求則抽取獨立的web / service / db 來支持,解除系統之間的耦合,對於“業務復雜”“並發量低”“無需高可用”“能接受一定延時”的後臺業務:
可以去掉service層,在運營後臺web層通過dao直接訪問db
不需要反向代理,不需要集群冗余
不需要訪問實時庫,可以通過MQ或者線下異步同步數據
在數據庫非常大的情況下,可以使用更契合大量數據允許接受更高延時的“索引外置”或者“HIVE”的設計方案
f.總結
將以“用戶中心”為典型的“單KEY”類業務,水平切分的架構點,本文做了這樣一些介紹。
水平切分方式:範圍法;哈希法
水平切分後碰到的問題:通過uid屬性查詢能直接定位到庫,通過非uid屬性查詢不能定位到庫
非uid屬性查詢的典型業務:用戶側,前臺訪問,單條記錄的查詢,訪問量較大,服務需要高可用,並且對一致性的要求較高;運營側,後臺訪問,根據產品、運營需求,訪問模式各異,基本上是批量分頁的查詢,由於是內部系統,訪問量很低,對可用性的要求不高,對一致性的要求也沒這麽嚴格
這兩類業務的架構設計思路:
針對用戶側,應該采用“建立非uid屬性到uid的映射關系”的架構方案
針對運營側,應該采用“前臺與後臺分離”的架構方案
用戶前臺側,“建立非uid屬性到uid的映射關系”最佳實踐:
索引表法:數據庫中記錄login_name->uid的映射關系
緩存映射法:緩存中記錄login_name->uid的映射關系
login_name生成uid
login_name基因融入uid
運營後臺側,“前臺與後臺分離”最佳實踐:
前臺、後臺系統web/service/db分離解耦,避免後臺低效查詢引發前臺查詢抖動
可以采用數據冗余的設計方式
可以采用“外置索引”(例如ES搜索系統)或者“大數據處理”(例如HIVE)來滿足後臺變態的查詢需求
3. 100億數據1萬屬性數據架構設計
1). 什麽是數據庫擴展的version + ext方案?
使用ext來承載不同業務需求的個性化屬性,使用version來標識ext裏各個字段的含義。
優點:a.可以隨時動態擴展屬性,擴展性好 ;b.新舊兩種數據可以同時存在,兼容性好
不足:a.ext裏的字段無法建立索引 ; b.ext裏的key值有大量冗余,建議key短一些
2). 如何將不同品類,異構的數據統一存儲起來,采用的就是類似version+ext的方式:
tiezi(tid,uid, time, title, cate, subcate, xxid, ext)
a.一些通用的字段抽取出來單獨存儲
b.通過cate, subcate, xxid等來定義ext是何種含義(和version有點像?)
c.通過ext來存儲不同業務線的個性化需求
3). 解決了海量異構數據的存儲問題,遇到的新問題是:
a.每條記錄ext內key都需要重復存儲,占據了大量的空間,能否壓縮存儲
b.cateid已經不足以描述ext內的內容,品類有層級,深度不確定,ext能否具備自描述性
c.隨時可以增加屬性,保證擴展性
4).統一類目屬性服務
抽象出一個統一的類目、屬性服務,單獨來管理這些信息,而帖子庫ext字段裏json的key,統一由數字來表示,減少存儲空間。
數字是什麽含義,屬於哪個子分類,值的校驗約束,統一都存儲在類目、屬性服務裏。
除此之外,如果ext裏某個key的value不是正則校驗的值,而是枚舉值時,需要有一個對值進行限定的枚舉表來進行校驗
5). 統一檢索服務
數據量很大的時候,不同屬性上的查詢需求,不可能通過組合索引來滿足所有查詢需求,怎麽辦呢?
58同城的先賢們,從一早就確定了“外置索引,統一檢索服務”的技術路線:
a.數據庫提供“帖子id”的正排查詢需求
b.所有非“帖子id”的個性化檢索需求,統一走外置索引
6).元數據與索引數據的操作遵循:
a.對帖子進行tid正排查詢,直接訪問帖子服務
b.對帖子進行修改,帖子服務通知檢索服務,同時對索引進行修改
c.對帖子進行復雜查詢,通過檢索服務滿足需求
4.數據庫秒級平滑擴容架構方案
1).部署方案:
a.並發量大,流量大的互聯網架構,一般來說,數據庫上層都有一個服務層,服務層記錄了“業務庫名”與“數據庫實例”的映射關系,通過數據庫連接池向數據庫路由sql語句以執行
b.隨著數據量的增大,數據要進行水平切分,分庫後將數據分布到不同的數據庫實例(甚至物理機器)上,以達到降低數據量,增強性能的擴容目的
c.互聯網架構需要保證數據庫高可用,常見的一種方式,使用雙主同步+keepalived+虛ip的方式保證數據庫的可用性
d.綜合上文的(2)和(3),線上實際的架構,既有水平切分,又有高可用保證
提問:如果數據量持續增大,分2個庫性能扛不住了,該怎麽辦呢?
回答:繼續水平拆分,拆成更多的庫,降低單庫數據量,增加庫主庫實例(機器)數量,提高性能。
2).停服務方案:暫停所有服務,遷移數據。
回滾方案:如果數據遷移失敗,或者遷移後測試失敗,則將配置改回x庫,恢復服務,改天再掛公告。
方案優點:簡單
方案缺點:a.停服務,不高可用;
b.技術同學壓力大,所有工作要在規定時間內做完,根據經驗,壓力越大約容易出錯(這一點很致命)
c.如果有問題第一時間沒檢查出來,啟動了服務,運行一段時間後再發現有問題,難以回滾,需要回檔,可能會丟失一部分數據
3).秒級、平滑、帥氣方案
a.修改配置
主要修改兩處:
a).數據庫實例所在的機器做雙虛ip,原來%2=0的庫是虛ip0,現在增加一個虛ip00,%2=1的另一個庫同理
b).修改服務的配置(不管是在配置文件裏,還是在配置中心),將2個庫的數據庫配置,改為4個庫的數據庫配置,修改的時候要註意舊庫與辛苦的映射關系:
%2=0的庫,會變為%4=0與%4=2;
%2=1的部分,會變為%4=1與%4=3;
這樣修改是為了保證,拆分後依然能夠路由到正確的數據。
b.reload配置,實例擴容
服務層reload配置,reload可能是這麽幾種方式:
a).比較原始的,重啟服務,讀新的配置文件
b).高級一點的,配置中心給服務發信號,重讀配置文件,重新初始化數據庫連接池
不管哪種方式,reload之後,數據庫的實例擴容就完成了,原來是2個數據庫實例提供服務,現在變為4個數據庫實例提供服務,這個過程一般可以在秒級完成。
整個過程可以逐步重啟,對服務的正確性和可用性完全沒有影響:
a).即使%2尋庫和%4尋庫同時存在,也不影響數據的正確性,因為此時仍然是雙主數據同步的
b).服務reload之前是不對外提供服務的,冗余的服務能夠保證高可用
完成了實例的擴展,會發現每個數據庫的數據量依然沒有下降,所以第三個步驟還要做一些收尾工作
c.收尾工作,數據收縮:
有這些一些收尾工作:
a).把雙虛ip修改回單虛ip
b).解除舊的雙主同步,讓成對庫的數據不再同步增加
c).增加新的雙主同步,保證高可用
d).刪除掉冗余數據,例如:ip0裏%4=2的數據全部幹掉,只為%4=0的數據提供服務啦
這樣下來,每個庫的數據量就降為原來的一半,數據收縮完成。
5. 100億數據平滑數據遷移,不影響服務
針對互聯網很多“數據量較大,並發量較大,業務復雜度較高”的業務場景,在
a.底層表結構變更
b.分庫個數變換
c.底層存儲介質變換
的眾多需求下,需要進行數據遷移,完成“平滑遷移數據,遷移過程不停機,保證系統持續服務”有兩種常見的解決方案。
1).追日誌法,五個步驟:
a.服務進行升級,記錄“對舊庫上的數據修改”的日誌
b.研發一個數據遷移小工具,進行數據遷移
c.研發一個讀取日誌小工具,追平數據差異
d.研發一個數據比對小工具,校驗數據一致性
e.流量切到新庫,完成平滑遷移
2).雙寫法,四個步驟:
a.服務進行升級,記錄“對舊庫上的數據修改”進行新庫的雙寫
b.研發一個數據遷移小工具,進行數據遷移
c.研發一個數據比對小工具,校驗數據一致性
d.流量切到新庫,完成平滑遷移
6. MySQL冗余數據的三種方案
1).為什麽要冗余數據
例如:訂單業務,對用戶和商家都有訂單查詢需求:
Order(oid, info_detail);
T(buyer_id, seller_id, oid);
如果用buyer_id來分庫,seller_id的查詢就需要掃描多庫。
如果用seller_id來分庫,buyer_id的查詢就需要掃描多庫。
此時可以使用數據冗余來分別滿足buyer_id和seller_id上的查詢需求:
T1(buyer_id, seller_id, oid)
T2(seller_id, buyer_id, oid)
同一個數據,冗余兩份,一份以buyer_id來分庫,滿足買家的查詢需求;一份以seller_id來分庫,滿足賣家的查詢需求。
2).服務同步雙寫
顧名思義,由服務層同步寫冗余數據,如上圖1-4流程:
業務方調用服務,新增數據
服務先插入T1數據
服務再插入T2數據
服務返回業務方新增數據成功
優點:
不復雜,服務層由單次寫,變兩次寫
數據一致性相對較高(因為雙寫成功才返回)
缺點:
請求的處理時間增加(要插入兩次,時間加倍)
數據仍可能不一致,例如第二步寫入T1完成後服務重啟,則數據不會寫入T2
3).服務異步雙寫
數據的雙寫並不再由服務來完成,服務層異步發出一個消息,通過消息總線發送給一個專門的數據復制服務來寫入冗余數據,如上圖1-6流程:
業務方調用服務,新增數據
服務先插入T1數據
服務向消息總線發送一個異步消息(發出即可,不用等返回,通常很快就能完成)
服務返回業務方新增數據成功
消息總線將消息投遞給數據同步中心
數據同步中心插入T2數據
優點:請求處理時間短(只插入1次)
缺點:系統的復雜性增加了,多引入了一個組件(消息總線)和一個服務(專用的數據復制服務)
因為返回業務線數據插入成功時,數據還不一定插入到T2中,因此數據有一個不一致時間窗口(這個窗口很短,最終是一致的)
在消息總線丟失消息時,冗余表數據會不一致
不管是服務同步雙寫,還是服務異步雙寫,服務都需要關註“冗余數據”帶來的復雜性。如果想解除“數據冗余”對系統的耦合,引出常用的第三種方案。
如果系統對處理時間比較敏感,引出常用的第二種方案。
4).線下異步雙寫:
為了屏蔽“冗余數據”對服務帶來的復雜性,數據的雙寫不再由服務層來完成,而是由線下的一個服務或者任務來完成,如上圖1-6流程:
業務方調用服務,新增數據
服務先插入T1數據
服務返回業務方新增數據成功
數據會被寫入到數據庫的log中
線下服務或者任務讀取數據庫的log
線下服務或者任務插入T2數據
優點:數據雙寫與業務完全解耦; 請求處理時間短(只插入1次)
缺點:返回業務線數據插入成功時,數據還不一定插入到T2中,因此數據有一個不一致時間窗口(這個窗口很短,最終是一致的)
數據的一致性依賴於線下服務或者任務的可靠性
5).總結:
互聯網數據量大的業務場景,常常:
使用水平切分來降低單庫數據量
使用數據冗余的反範式設計來滿足不同維度的查詢需求
使用服務同步雙寫法能夠很容易的實現數據冗余
為了降低時延,可以優化為服務異步雙寫法
為了屏蔽“冗余數據”對服務帶來的復雜性,可以優化為線下異步雙寫法
內容轉自微信公眾號:架構師之路
數據庫架構(轉)