關(guān)系型數(shù)據(jù)庫的瓶頸 與 優(yōu)化
1. 數(shù)據(jù)庫的分類
數(shù)據(jù)庫大致可以分為兩部分:
傳統(tǒng)的關(guān)系型數(shù)據(jù)庫, 如: MySQL, Oracle, SQLServer 以及 PostgreSQL; MySQL 是國內(nèi)使用最廣泛的數(shù)據(jù)庫, Oracle 在傳統(tǒng)行業(yè)應(yīng)用最為廣泛, PostgreSQL 性能和功能都比較完善, 但目前文檔和社區(qū)還有待成長.
非關(guān)系型數(shù)據(jù)庫, 如 HBase(列式數(shù)據(jù)庫), MongoDB(文檔型數(shù)據(jù)庫), Redis(高性能 KV 存儲), Lucene(搜索引擎) 等等.
2. 關(guān)系型數(shù)據(jù)庫的瓶頸與優(yōu)化
2.1 為什么數(shù)據(jù)庫的架構(gòu)需要調(diào)整
互聯(lián)網(wǎng)的數(shù)據(jù)增長往往是指數(shù)型的;
讀寫分離, 分布式: 單機(jī)性能上存在瓶頸;
NoSQL, 搜索引擎: 特殊場景的需求無法滿足;
分析系統(tǒng): 無法滿足大數(shù)據(jù)的分析需求;
部署要求: 同城容災(zāi)/異地容災(zāi).
2.2 數(shù)據(jù)庫會遇到什么問題
2.2.1 性能
查詢性能
寫入更新
并發(fā), 數(shù)據(jù)量等
2.2.2 功能
新功能: LBS/JSON/特殊業(yè)務(wù)場景
數(shù)據(jù)安全性: 強(qiáng)一致性/非強(qiáng)一致性
大數(shù)據(jù)分析
搜索等
3. 不同業(yè)務(wù)場景的存儲選型
3.1 一個簡單的問題
MySQL 已經(jīng)有 cache 了, 為何還需要加一層 Redis
3.2 數(shù)據(jù)庫查詢開銷

其中比較耗時的步驟有:
建立 TCP 連接
生成執(zhí)行計劃
開表
從磁盤掃描數(shù)據(jù)
關(guān)閉連接
3.2.1 SQL 解析
假設(shè)有如下三條語句, 均是根據(jù)主鍵的查詢.
1 | # 1 |
造成第三條語句執(zhí)行時間如此長的主要原因就是大量的 OR 語句會導(dǎo)致 SQL 解析非常耗時.
3.2.2 以 MySQL 的 InnoDB 存儲引擎主鍵查詢?yōu)槔?/span>
1 | SELECT * FROM t WHERE id = ?; |

常規(guī)配置的服務(wù)器基本可以達(dá)到 400000 QPS.
3.2.3 如果查詢條件不是主鍵
1 | SELECT * FROM t WHERE name = ?; |

對于非主鍵的查詢, MySQL 會根據(jù)二級索引查詢到主索引對應(yīng)節(jié)點(diǎn)的位置. 按照圖中的情況, 會首先通過三次 IO 找到對應(yīng)主鍵, 在二級索引的葉子節(jié)點(diǎn)會同時保存索引字段的值以及主鍵的值, 再回到主索引通過主鍵查詢到整條記錄.
在 MySQL 中, 主鍵查詢是最為高效的一類查詢.
DBA 往往希望所有的 SQL 語句都是 KV 查詢, 但是往往是不現(xiàn)實(shí)的.
主鍵查詢有限, 有些主鍵沒有業(yè)務(wù)含義;
設(shè)計表結(jié)構(gòu)時, 并沒有考慮過主鍵問題.
SQL 語句允許開發(fā)人員用各種方式從表中獲取數(shù)據(jù), 但 DBA 卻不會希望我們這么做.
3.2.3 數(shù)據(jù)庫的大字段
1 | content varchar(2046) NOT NULL COMMENT '原始消息'; |
以 InnoDB 存儲引擎為例:
TinyText/Text/Mediumtext
varchar(256)/varchar(500)/varchar(20000)
tinyBlob/blob/mediumBlob
text 類型本質(zhì)上和 varchar 類型沒有區(qū)別.

MySQL 中, 數(shù)據(jù)是以頁的方式來組織的, 每個數(shù)據(jù)頁默認(rèn)大小 16 KB, 其中包括頁頭, 頁尾, 中間是一行一行的記錄.
圖中的每條記錄包括 ID, NAME, AGE 和 DETAIL. 假設(shè) DETAIL 是一個大字段, 達(dá)到超過了單頁的大小, 此時 DB 會新開一個數(shù)據(jù)頁, 當(dāng)前頁通過指針指向該頁. 如果一頁依然不夠, MySQL 就會不斷新加數(shù)據(jù)頁直到能夠存下為止.
一旦存在這樣的大字段, 會帶來如下問題:
查詢開銷大;
查詢影響大, 嚴(yán)重時會觸發(fā)熱頁換出, 引起系統(tǒng)抖動. MySQL 將記錄從磁盤讀取出來的時候, 可能會有很多數(shù)據(jù)頁, MySQL 自帶緩存時非常寶貴的, 會導(dǎo)致真正使用頻率高的數(shù)據(jù)頁被替換成大字段的數(shù)據(jù)頁. 此外, 對 MySQL 來說, 即便只查記錄中的某幾個字段, 數(shù)據(jù)庫依然會把整條記錄取出, 讀進(jìn)內(nèi)存, 再進(jìn)行指定字段的篩選
對于大字段場景可以嘗試的優(yōu)化方案:
是否適合存儲關(guān)系型數(shù)據(jù)庫;
是否所有數(shù)據(jù)都需要存數(shù)據(jù)庫;
是否可以新建一張表存儲大字段.
3.2.4 數(shù)據(jù)庫緩存利用率
以 InnoDB 存儲引擎為例:
MySQL 默認(rèn)數(shù)據(jù)頁為
16KB, 哪怕只讀一行記錄, 也需要從磁盤中取出16KB數(shù)據(jù)取出;MySQL 是以頁為最小的緩存單位;
如果每行數(shù)據(jù) 1kb, 256kb 內(nèi)存空間能緩存多少行有效數(shù)據(jù), 最好的情況是每條數(shù)據(jù)整齊排列在一個數(shù)據(jù)頁中, 那么可以緩存256條記錄, 最壞的情況下每一頁只存在一條數(shù)據(jù), 那么就只能緩存16條;
在 256KB 的 Buffer Pool 中, 并不是所有空間都用來做數(shù)據(jù)頁緩存, 有很大的一塊在 Write Buffer(MySQL 為了優(yōu)化寫操作, 會將一段時間內(nèi)的寫操作先放在 Write Buffer, 再由后臺線程定時異步刷新到磁盤上). 然而剩下的
128KB中還存在一部分臟頁.

緩存為什么如此重要:
互聯(lián)網(wǎng)產(chǎn)品往往讀多寫少;
擴(kuò)展緩存遠(yuǎn)比擴(kuò)展 DB 簡單;
數(shù)據(jù)庫緩存利用率很低;
互聯(lián)網(wǎng)應(yīng)用對 DB 響應(yīng)時間比較敏感, 緩存系統(tǒng)一般性能比較好
只要符合條件的數(shù)據(jù)都應(yīng)該走緩存:
修改不頻繁的數(shù)據(jù);
非實(shí)時的數(shù)據(jù), 一致性要求不嚴(yán)的數(shù)據(jù);
查詢頻率較高, 帶有明顯熱點(diǎn)請求的數(shù)據(jù);
3.2.5 緩存帶來的問題
用了緩存并不一定代表沒有問題
緩存命中
緩存穿透
緩存失效
緩存一致
3.2.6 選擇正確的索引
降低掃描數(shù)據(jù)量還是降低排序代價
大多數(shù)查詢只能使用一個索引, 因此在需要對多個列進(jìn)行操作的 SQL 語句中, 我們需要準(zhǔn)確評估每個索引的開銷.
key idx_create_time(createTime)
key idx_price(price)
1SELECT * FROM tb_order WHERE createTime > xxx AND createTime < xxx ORDER BY price DESC;
3.2.7 索引的使用
3.2.7.1 索引字段過長, 超過索引支持
1 | # name varchar(512) |
上面的例子在實(shí)際場景中執(zhí)行非常慢, 使用 EXPLAIN 打印查詢計劃:
select_type: SIMPLE
table: comment
type: range
possible_keys: id_name
key: uk_sess
key_len: 403
ref: NULL
rows: 462642
Extra:Using where; Using filesort
1 row in set(0.00sec)
其中需要重點(diǎn)關(guān)注的是: Extra:Using where; Using filesort
Using where: 表用到了索引
Using filesort: MySQL 自帶的磁盤排序, 并沒有用到索引的排序
問題是為什么使用了索引, 查詢效率依然非常慢?
真正的原因是字段太長, 而索引的長度只能覆蓋 256 字節(jié), 導(dǎo)致 ORDER BY 無法在內(nèi)存中完成排序
3.2.7.2
查詢某個用戶 id 的分值總和
1 | -- uid varchar(190) NOT NULL DEFAULT '' COMMENT '用戶 id', |

這條 SQL 的執(zhí)行順序:
根據(jù)二級索引 uid 找到所有主鍵 id
再根據(jù)主鍵逐行找到 score
對 score 進(jìn)行聚合
這個 SQL 的問題在于需要進(jìn)行大量的回表操作(從二級索引回到一級索引), 然后將全部符合過濾條件的記錄放在內(nèi)存中完成聚合操作.
改進(jìn)的方法其實(shí)很簡單, 可以嘗試使用 (uid, score) 建立聯(lián)合索引, 這樣只需要查詢二級索引就可以獲得全部數(shù)據(jù).

隨機(jī)插入 100W 條數(shù)據(jù), 現(xiàn)在對比下兩條索引的開銷.

3.3 數(shù)據(jù)庫寫開銷
對持久化要求嚴(yán)格, 寫操作代價大
日志文件需要 fsync, 硬件存在瓶頸
數(shù)據(jù)庫寫操作很難擴(kuò)展
主從要求一致場景下還要算上網(wǎng)絡(luò)開銷

將 3 所在的數(shù)據(jù)頁讀到緩存中;
在內(nèi)存中將 3 改成 5, 提交事務(wù), 觸發(fā) Redo Log 的刷新;
向用戶返回操作成功;
3.4 業(yè)務(wù)場景觸發(fā)的高并發(fā)寫入
3.4.1 秒殺
高并發(fā)寫入的極端情況
業(yè)務(wù)優(yōu)化(緩存/令牌通/排隊(duì)/Java 信號量/樂觀鎖)
熱點(diǎn)資源隔離
引入數(shù)據(jù)庫線程池
InnoDB 內(nèi)核層優(yōu)化: AliSQL
3.4.2 私信/站內(nèi)信消息推送
高并發(fā)寫入
伴隨大量的讀請求
系統(tǒng)消息/個人消息區(qū)分對待
消息內(nèi)容單獨(dú)對待
延遲寫入, 通過隊(duì)列/緩存達(dá)到限流目的
3.4.3 聽歌量

業(yè)務(wù)原因?qū)е聦懭肓糠浅4?/span>
插入更新比不確定, 更新能力強(qiáng)
數(shù)據(jù)庫需要具備自動擴(kuò)展的能力
數(shù)據(jù)非強(qiáng)一致
3.5 死鎖和超時
InnoDB 鎖超時默認(rèn)需要 5s 等待
死鎖馬上就能被發(fā)現(xiàn), 然后被 DB 自動回滾
鎖超時一般是索引不對, 或者 SQL 語句執(zhí)行性能較差
死鎖一般是業(yè)務(wù)實(shí)現(xiàn)有問題
鎖超時一般影響較為可控
死鎖情況比較嚴(yán)重, 會導(dǎo)致全站崩潰
3.6 數(shù)據(jù)庫并發(fā)事務(wù), 鎖
業(yè)務(wù)流程中的鎖: 減庫存, 發(fā)優(yōu)惠券
悲觀鎖實(shí)現(xiàn):
1 | BEGIN; |
樂觀鎖實(shí)現(xiàn):
1 | BEGIN; |
4. 數(shù)據(jù)庫的模塊化拆分
4.1 單機(jī)服務(wù)器的局限
雖然硬件配置越來越高, 但是總有瓶頸(e.g. CPU/內(nèi)存/網(wǎng)絡(luò)/IO/容量)
為了后續(xù)業(yè)務(wù)的可擴(kuò)展性
單機(jī)系統(tǒng)崩潰風(fēng)險較高
優(yōu)化性能
讀寫分離
冷熱分離, 創(chuàng)建歸檔庫
關(guān)鍵鏈路和非關(guān)鍵鏈路隔離
系統(tǒng)層面做好降級
4.2 常見拆分方案
4.2.1 讀寫分離
讀寫分離的原理就是將數(shù)據(jù)庫讀寫操作分散到不同的節(jié)點(diǎn)上

讀寫分離的基本原理就是:
數(shù)據(jù)庫服務(wù)器搭建主從集群;
數(shù)據(jù)庫主機(jī)負(fù)責(zé)寫操作, 從機(jī)只負(fù)責(zé)讀操作;
數(shù)據(jù)庫主機(jī)通過復(fù)制將數(shù)據(jù)同步到從機(jī), 每臺數(shù)據(jù)庫服務(wù)器都存儲了所有業(yè)務(wù)數(shù)據(jù).
業(yè)務(wù)服務(wù)器將寫操作發(fā)給數(shù)據(jù)庫主機(jī), 將讀操作發(fā)給數(shù)據(jù)庫從機(jī).
使用讀寫分離之后, 可能會引入兩個問題:
主從復(fù)制延遲
分配機(jī)制
4.2.1.1 復(fù)制延遲
主從復(fù)制的延遲可能達(dá)到秒級, 如果有大量數(shù)據(jù)短時間需要完成同步, 延遲甚至可能達(dá)到分鐘.
主從復(fù)制所帶來的問題:
如果業(yè)務(wù)服務(wù)器將數(shù)據(jù)寫入到主庫后進(jìn)行讀取, 此時讀操作訪問從庫, 而主庫的數(shù)據(jù)沒有完全復(fù)制過來, 從庫是無法讀取到最新數(shù)據(jù)的.
解決方案:
寫操作后的讀操作指定發(fā)給主庫, 邏輯會和業(yè)務(wù)強(qiáng)綁定, 對業(yè)務(wù)侵入較大.
讀從庫失敗后再讀一次主庫, 如果有大量沒有命中從庫的讀請求, 會給主庫帶來較大壓力.
關(guān)鍵業(yè)務(wù)讀寫操作全部走主庫, 非關(guān)鍵業(yè)務(wù)采用讀寫分離.
4.2.1.2 分配機(jī)制
將讀寫操作區(qū)分開來, 然后訪問不同的數(shù)據(jù)庫服務(wù)器, 一般有兩種方式: 程序代碼封裝和中間件封裝
1. 程序代碼封裝
在代碼中抽象一個數(shù)據(jù)訪問層, 實(shí)現(xiàn)讀寫操作分離和數(shù)據(jù)庫服務(wù)器連接的管理.

特點(diǎn):
實(shí)現(xiàn)簡單, 可以根據(jù)業(yè)務(wù)定制化;
無法做到多語言通用, 容易重復(fù)開發(fā);
故障情況下, 如果主從發(fā)生切換, 需要將系統(tǒng)配置手動修改.
2. 中間件封裝
獨(dú)立一套系統(tǒng)出來, 實(shí)現(xiàn)讀寫分離和數(shù)據(jù)庫服務(wù)器連接的管理, 中間件對業(yè)務(wù)服務(wù)器提供 SQL 兼容的協(xié)議, 業(yè)務(wù)服務(wù)器無需自己進(jìn)行讀寫分離, 對于業(yè)務(wù)服務(wù)器來說, 訪問中間件和訪問數(shù)據(jù)庫沒有區(qū)別

特點(diǎn):
能夠支持多種編程語言, 因?yàn)閿?shù)據(jù)庫中間件對業(yè)務(wù)提供的是標(biāo)準(zhǔn)的 SQL 接口.
實(shí)現(xiàn)較為復(fù)雜, 需要完整支持 SQL 語法和數(shù)據(jù)庫服務(wù)器的協(xié)議.
性能要求很高, 容易成為瓶頸.
數(shù)據(jù)庫主從切換對業(yè)務(wù)服務(wù)器無感知, 數(shù)據(jù)庫中間件可以探測數(shù)據(jù)庫服務(wù)器的主從狀態(tài)(e.g. 向某個測試庫寫入一條數(shù)據(jù), 成功的是主機(jī), 失敗的是從機(jī))
4.2.2 分布式
讀寫分離分散了讀寫操作的壓力, 但沒有分散存儲的壓力, 當(dāng)數(shù)據(jù)量達(dá)到千萬級以上的時候, 單臺數(shù)據(jù)庫服務(wù)器的存儲能力就會成為瓶頸:
數(shù)據(jù)量太大, 讀寫的性能會大幅下降.
數(shù)據(jù)文件備份和恢復(fù)都會很困難.

垂直分表: 適合將某些表中不常用且占用大量空間的列拆分出去. 代價是操作表的數(shù)量增加.
水平拆分: 適合行數(shù)較大的表, 會引入更多的復(fù)雜度:
路由,join 操作,count 操作等
https://destinywang.github.io/blog/2019/01/19/關(guān)系型數(shù)據(jù)庫的瓶頸與優(yōu)化

喜歡,在看
