數據庫架構設計一次搞定

學自:(沈劍,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、訂單庫拆分實踐:

  • 融會貫通,綜合應用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章