學自:(沈劍,2019中國系統架構師大會)
一、前言
作爲架構師,在數據庫架構設計上,至少四個方面是需要系統性考慮的:
一、如何保證數據庫的高可用
(1)讀庫高可用,如何保證?
(2)寫庫單點,如何消除?
(3)服務層,站點層,如何高可用?
二、如何提升數據庫的讀寫性能
(1)索引爲何會降低讀性能?
(2)一主多從真的好麼?
(3)數據庫寫入性能如何線性提升?
三、如何保證數據的一致性
(1)主從有延時,如何保證一致性?
(2)緩存與數據庫,如何保證一致性?
四、如何保證數據庫的擴展性
(1)表要增加一個屬性,如何擴展?
(2)數據量又暴漲了,該怎麼辦?
(3)數據要遷移了,如何不停機?
(4)分庫之後,跨庫分頁如何實現?
二、數據庫工程架構,要設計些什麼
任何脫離業務的架構設計,都是耍流氓:
1、依據“業務模式”設計庫結構、表結構
2、依據“訪問模式”設計索引結構
此外,數據庫工程架構,還要設計些什麼呢?
1、高可用
2、讀性能
3、一致性
4、擴展性
三、基本概念
1、單庫
創業公司初期都是單庫,比如一個庫裏有300個表。
但後期一個庫並不能很容易的拆成多個庫,因爲多個表有join操作,join操作不能跨庫。
所以,單庫在最初就要考慮後續的拆分問題。
2、複製(replication)與分組(group)
2.1、一主多從,解決了:
- 讀性能擴展:通過加slave節點擴展
- 讀高可用:通過slave節點數據冗餘
2.2、一主多從帶來的問題:
- 主從延遲
2.3、沒有解決的問題:
- 主高可用
- 數據存儲容量:原來只能存1T數據,分組後最多還是隻能存1T
3、分片(sharding)
3.1、分片解決了:
- 存儲容量擴容
- 讀性能擴展
- 寫性能擴容
3.2、分片帶來的問題:
- SQL擴展的問題:如求Max無法跨“片”,從而犧牲了一些SQL特性。
3.3、分片沒有解決:
- 高可用問題
- 會引發路由規則(router rule)問題
關於路由規則,常見的路由規則有:
1、範圍路由:
Server1: 1 ~ 1億
Server2: 1億 ~ 2億
Server3:…
問題:每臺server的存儲和訪問的負載都不均衡
優點:擴展方便
2、hash(一致性hash,hashcode對n取模)
可解決:存儲和訪問的負載均衡
帶來問題:遷移、擴展的問題。
實際上路由規則和業務是耦合的。
4、互聯網數據量大場景,線上實際既有分組又有分片
5、垂直拆分
把表拆分成user_base和user_ext兩類:
- user_base表:存儲字段小,訪問頻度高的數據
- user_ext表:存儲字段大,訪問不頻繁的數據
5.1、垂直拆分解決了:
- 提升讀寫性能:因爲user_base表字段小,訪問頻度高,可充分使用DB buffer緩存(buffer:以行爲單位,把磁盤數據提前加載到內存)
5.2、垂直拆分帶來的問題:
- 原來只需要一個SQL,現在可能需要兩個SQL
5.3、垂直拆分沒有解決:
- 擴展性
四、高可用
1、怎樣驗證你的系統是否高可用呢?
去線上隨便關一臺機器,看對用戶是否有影響。
理論上,對於要求高可用的系統,系統的每一層都需要高可用。
2、數據層怎樣做到高可用呢?
2.1、redis怎樣做到高可用?
Jedis會自動支持主從高可用:主掛了,會自動調用從。
2.2、數據庫做到高可用的思路:複製+冗餘
例如google CFS也是複製了3份文件
緩存的本質也是數據冗餘。
數據層冗餘會引發一致性問題
2.2.1、如何保證讀庫高可用?分組:讀庫冗餘
讀數據庫時,數據庫連接池會自動做到把請求發送給可用的讀庫。
2.2.2、如何保證寫庫高可用?雙寫:寫庫冗餘
帶來的問題:
- 一致性問題:如自增主鍵ID,雙寫時可能會重複。解決方案:一般是奇數,一遍是偶數;或由業務費來保證ID不重複
2.2.3、如何保證讀寫高可用?
讀寫都放在主庫上,同時同步到從庫,主庫故障時從庫頂上。這樣讀寫一致性問題會得到緩解。
五、怎樣提升數據庫讀性能
1、索引怎樣用來提升讀性能
1.1、索引是越多性能越好嗎?
過多的索引會導致寫性能降低、且索引佔用內存大導致命中率低:因爲數據庫的內存緩存buffer是有限的,所以過多,導致內存buffer緩存不下,這樣在查詢索引數據時,仍需要讀磁盤,從而導致性能下降
1.2、索引提升讀性能最佳實踐
對於一主二從的場景:
- master寫庫:不用建索引
- slave讀庫:需要建索引
2、提升讀性能:增加從庫
增加從庫會帶來什麼問題?
- 從庫越多,同步越慢
- 數據不一致
3、提升讀性能:增加緩存
常見玩法:app–>service–>cache–>mysql-m–><–mysql s(m)
3.1、增加緩存會帶來什麼問題?
Cache Aside Pattern
Cache Aside Pattern最經典的緩存+數據庫讀寫的模式。
術語標準解釋:
- 如果應用程序更新信息,則可以通過對數據存儲進行修改,並使緩存中的相應項目無效,從而遵循直寫策略。
- 當下一個項目需要時,使用cache-aside策略將導致更新的數據從數據存儲中檢索並添加到高速緩存中。
術語白話解釋:
- 讀的時候,先讀緩存,緩存沒有的話,那麼就讀數據庫,然後取出數據後放入緩存,同時返回響應
- 更新的時候,先刪除緩存,然後再更新數據庫
對於讀請求:
- 先讀cache, 再讀DB
- 如果cache hit it, 直接返回
- 如果cache miss it, 則讀取DB,並將數據set回緩存
如上圖: - 先從cache中嘗試get數據,結果miss了
- 再從db中讀取數據,從庫,讀寫分離
- 最後把數據set回cache,方便下次讀命中
對於寫請求
- 淘汰緩存,而不是更新緩存
- 先操作數據庫,再淘汰緩存
Cache Aside Pattern爲什麼建議淘汰緩存,而不是更新緩存?
如果更新緩存,在併發寫時,可能出現數據不一致。
如上圖所示,如果採用set緩存:
在1和2兩個併發寫發生時,由於無法保證時序,此時不管先操作緩存還是先操作數據庫,都可能出現:
- 請求1先操作數據庫,請求2後操作數據庫
- 請求2先set了緩存,請求1後set了緩存
導致,數據庫與緩存之間的數據不一致。
所以,Cache Aside Pattern建議,delete緩存,而不是set緩存。
爲什麼先寫數據庫,再淘汰緩存?
Cache Aside Pattern方案存在什麼問題?
答:如果先操作數據庫,再淘汰緩存,在原子性被破壞時:
1)修改數據庫成功了
2)淘汰緩存失敗了
會導致,數據庫與緩存數據不一致。
六、一致性優化
1、主庫從庫一致性問題
1.1、爲什麼會出現主從一致性問題?
在主向從同步過程中,會出現主從一致性問題。
1.2、如何優化主從不一致問題
方案1、忽略
絕大多數業務,都允許主庫和從庫短時間內不一致。
方案2、強制讀主庫
方案3、選擇性讀主庫
2、緩存一致性問題
2.1、爲什麼會出現緩存一致性問題
“寫後立即讀”問題:
"先寫DB,再刪除緩存“,只能緩解該問題,但不能根治。
2.2、如何優化緩存不一致問題
消除“主從延時”導致的不一致:
從binlog觸發一次“二次淘汰”,
也可以在service層異步觸發“二次淘汰”。
即寫數據時在寫完DB後,刪除了緩存;這時有讀請求到從庫,此時主庫還沒有完成向從庫的同步,讀請求讀到的從庫不是最新數據,而更新了緩存。那麼,當主庫同步完從庫後,會通過binlog或service層異步觸發“二次淘汰”來更新緩存。
七、擴展性
1、典型的微服務架構數據庫擴容
特點:數據量大、吞吐量達、高可用
系統架構:微服務
思考:
1)數據層如何高可用
2)數據層如何擴展
2、要解決什麼問題
- 吞吐量持續增大,如何進一步增加實例
- 數據量持續增大,如何進一步水平擴展
3、擴展性問題解決方案
方案1、停服擴容
方案2、追日誌
Step1、記錄日誌:對新的操作記錄日誌到文件
Step2、數據遷移:把原數據庫的數據遷移到新庫
Step3、數據補齊:把舊庫的日誌補齊到新庫
Step4、數據檢驗:通過工具檢驗新庫與舊庫的數據是否一致,不一致通過手動等方式補齊。
方案3、雙寫
Step1、雙寫數據(服務升級)
Step2、數據遷移(通過小工具)
Step3、數據檢驗(通過小工具)
方案4、雙倍擴容
Step1、改配置
Step2、reload配置
Step3、收尾
4、各類業務場景的水平切分實踐
問題:如何拆?按哪個屬性拆?
下面的場景幾乎涵蓋了互聯網90%的場景。
- 單key:用戶庫:user(uid, XXOO)
- 1對多:帖子庫:tiezi(tid, uid, XXOO)
- 多對多:好友庫:friend(uid, friend_uid, XXOO)
- 多key:訂單庫:order(oid, buyer_id, seller_id, XXOO)
4.1、用戶庫拆分
用戶庫:10億數據量
user(uid, uname, passwd, age, sex, create_time, … )
業務需求如下:
- 1%登陸請求:where uname=xxx and passwd=xxx
- 99%查詢請求:where uid=xxx
方案:按uid分庫
- 索引表法:根據hash分區,再查。
- 緩存映射法
- login_name生成uid
- 基因法:uid中融入login_name的“基因”:這樣利用login_name就可以定位到庫。
結論:根據uid來拆分庫,即把uid作爲負載均衡的key, 把用戶平均存到n個庫中。
4.2、帖子庫拆分
結論:“1對多”場景,使用“1”分庫,例如帖子庫中一個uid對應多個tid, 則採用uid進行分庫。
4.3、好友庫拆分
好友庫:friend(uid, friend_uid, nick, memo, XXOO)
業務需求如下:
- 查詢我的好友(50%的請求):用於頁面展示
select friend_uid from friend where uid=xxx - 查詢加我爲好友的用戶(50%的請求):用戶反向通知
select uid from friend where friend_uid=xxx
即對於各50%的查詢操作,通過數據冗餘存1份來拆分。
結論:”多對多”場景,使用數據冗餘方案,多份數據使用多種分庫手段。即把查詢分流。不同的查詢請求落到不同的庫上查詢。
4.4、訂單庫拆分
訂單庫:10億數據量
order(oid, buyer_id, seller_id, order_info, xxoo)
業務需求如下:
-
查詢訂單信息:80%請求
select * from order where oid=xxx -
查詢我買的東西:19%請求
select * from order where buyer_id=xxx -
查詢我賣出的東西:1%請求
select * from order where seller_id=xxx
結論:“多key”場景一般有兩種方案:
方案一:採用2和3綜合的方案
方案二:1%的請求採用多庫查詢
八、總結
1、數據庫工程架構,要考慮:
- 庫結構、表結構、索引結構
- 高可用、讀性能、一致性、擴展性
2、保證高可用的思路:複製冗餘
但數據冗餘會引發一致性問題
3、提升讀性能的場景方案是:
- 加索引:不同庫的索引可以不一樣
- 加從庫:會引發主從不一致
- 加緩存:會引發緩存不一致
4、旁路緩存最佳實踐,Cache Aside Pattern:
- 讀最佳實踐
- 寫最佳實踐:淘汰緩存,先寫數據庫
5、數據冗餘帶來的一致性問題優化:
- 主從不一致:忽略、強制讀主、選擇性讀主
- 緩存不一致:“寫後立即讀”問題,二次淘汰
6、增加數據庫實例、增大數據庫容量的擴展性實踐:
- 停服擴容
- 追日誌擴容(記日誌+遷移數據+追日誌+一致性對比)
- 雙寫擴容(雙寫+遷移數據+一致性對比)
- 雙倍擴容(改配置+reload+收尾)
7、用戶庫拆分實踐:
- 索引表、緩存映射、生成uid、基因法決定login_name路由
- 前臺與後臺分離,解決後臺類需求
8、帖子庫拆分實踐:
- uid分庫,基因法決定tid路由
- 索引外置,解決檢索類需求
9、好友庫拆分實踐:
- 數據冗餘,是實現多對多的常見實踐
- 數據冗餘的三類方法:服務同步冗餘、服務異步冗餘、服務線下冗餘
- 最終一致性實踐:線下掃全庫、先下掃增量、線上實時檢測
10、訂單庫拆分實踐:
- 融會貫通,綜合應用