本文主要是對目前工作中使用到的DB相關知識點的總結,應用開發了解到以下深度基本足以應對日常需求,再深入下去更偏向於DB本身的理論、調優和運維實踐。
不在本文重點關注討論的內容(可能會提到一些):
- 具體的DQL、DML、DDL、DCL等語法
- 基礎性的概念,如主鍵、索引、存儲過程(注:阿里巴巴規范中禁止使用存儲過程)等
- 聯合查詢,我個人不太喜歡在應用中寫過於復雜的SQL,性能和后續維護容易出現問題
- 可能會用到的具體DB特性,如oracle的DATA GUARD
有一些屬於基礎知識或語法但是常用的信息,也會列一下,如join的用法。
一、基礎
1. ACID
DB的四大特性,這里簡單概括下不具體展開。
- 原子性(Atomicity):事務操作中的多條SQL,要么全部成功要么全部失敗,失敗后回滾不對原有數據造成任何影響。
- 一致性(Consistency):事務開始前和結束后,數據庫的完整性沒有被破壞。如觸發器、約束、級聯回滾
- 隔離性(Isolation):多個事務支持並發讀寫。具體隔離級別見后文。
- 持久性(Durability):事務結束后,修改是永久的,不丟失。
2. 范式
這里展開講比較復雜,實踐中很少用到,一般滿足1NF即可。
高一級必滿足低一級。
- 1NF:每個屬性都不可再分,即表的列是最原子的
- 2NF:在1NF基礎上,消除非主屬性對鍵的部分依賴。這里不解釋非主屬性和鍵的含義,可以簡單認為是指不存在列A可以通過列B來獲取,如“學生姓名-學號”這種y=f(x)的函數關系。
- 3NF:在2NF的基礎之上,消除了非主屬性對於碼的傳遞函數依賴
- BCNF:對於關系模式R,如果每一個函數依賴的決定因素都包含鍵,則R屬於BCNF范式
有興趣可以參考:范式通俗理解:1NF、2NF、3NF和BNCF
二、事務
3. 事務的隔離級別
3.1 讀現象
讀現象是伴生於不同的隔離級別出現的。讀現象的場景都是在多個事務並發執行的前提下可能出現的:
- 臟讀 —— 一個事務讀取了另一個未提交事務執行過程中的數據。此時另一個事務可能會由於提交失敗而回滾。
- 不可重復讀 —— 一個事務執行過程中多次查詢同一條數據但返回了不同查詢結果。這說明在事務執行過程中,數據被其他事務修改並提交了。
- 幻讀 —— 事務1先行查詢了某種數據,在修改或插入提交之前,事務2對此類數據進行了插入或刪除並提交,導致了事務1對預期結果的數量變化。
3.2 隔離級別
- 未提交讀(read uncommited):允許另外一個事務可以看到這個事務未提交的數據。
- 提交讀(read commited):保證一個事務提交后才能被另外一個事務讀取,而不能讀取未提交的數據。
- 可重復讀(repeatable read):保持讀鎖和寫鎖一直到事務提交,但不提供范圍鎖,因此不能避免幻讀。
- 可序列化(serializable):代價最高但最可靠的事務隔離級別,事務被處理為順序執行。
3.3 隔離級別與讀現象
不同的隔離級別可以防止讀現象。
隔離級別 | 臟讀 | 不可重復讀 | 幻影讀 |
---|---|---|---|
未提交讀 | 可能發生 | 可能發生 | 可能發生 |
提交讀 | - | 可能發生 | 可能發生 |
可重復讀 | - | - | 可能發生 |
可序列化 | - | - | - |
注:為什么提交讀不能避免不可重復讀?假設A事務需要讀取兩次變量a,第一次讀取時a=10,執行過程中a被事務B修改變成了20,那么A第二次讀時a與第一次的結果不同。
3.4 查看DB的隔離級別
// 查看當前會話
select @@tx_isolation;
// 查看當前系統
select @@global.tx_isolation;
MySql 5.7.14-ALISQL版默認是提交讀。
4. 事務傳播性(Spring)
在多個含有事務方法的相互調用時,事務如何在這些方法間傳播。
本節使用Spring事務注解的枚舉,而不是全稱(如PROPAGATION_REQUIRED)
spring支持7種事務傳播行為:
- REQUIRED:如果當前沒有事務,就新建一個事務;否則加入到這個已有事務中,這是最常見的選擇。
- SUPPORTS:支持當前事務,如果沒有當前事務,就以非事務方法執行。
- MANDATORY:使用當前事務,如果沒有當前事務,就拋出異常。
- REQUIRED_NEW:新建事務,如果當前存在事務,把當前事務掛起。
- NOT_SUPPORTED:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
- NEVER:以非事務方式執行操作,如果當前事務存在則拋出異常。
- NESTED:如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與propagation_required類似的操作
Spring默認是REQUIRED。
為了便於理解,將以上幾種傳播行為分類:
傳播性的類型 | 當前不在事務中 | 當前在事務中 | 備注 |
---|---|---|---|
REQUIRED | 新建一個事務 | 加入到當前事務 | 最常見的選擇 |
SUPPORTS | 非事務執行 | 加入當前事務 | |
MANDATORY | 拋異常 | 加入當前事務 | mandatory: 強制性 |
REQUIRED_NEW | 新建事務 | 掛起當前事務,新建一個事務 | 回滾與否和外部的事務脫離關系 |
NOT_SUPPORTED | 非事務執行 | 掛起當前事務,以無事務運行 | 自身的異常與外部事務沒有直接關系(事務傳播性層面上的) |
NEVER | 非事務執行 | 拋異常 | |
NESTED | 新建事務 | 嵌套事務內執行 | 見“事務嵌套”小節 |
事務掛起
當前方法不再受所屬的事務控制直到該方法結束。比如A方法起了一個事務,調用B方法時B掛起A的事務,那么B的所有DB操作都不再受A方法的事務控制,直到B執行結束。期間B如果先做了DB操作再拋異常,B的DB操作不會回滾,A視B的異常有沒有被捕獲而提交或回滾。
事務嵌套
嵌套的事務可以獨立於當前事務提交或回滾,怎么理解呢?
內部事務是外部事物的子事務,進入內部事務時,記錄一個savepoint。如果內部事務回滾,外部事物捕獲異常后不回滾。但是外部事務回滾時,兩者都不committed,一起回滾。
與REQUIRED區別:NESTED內部回滾,外部捕獲異常后不會回滾;REQUIRED即使捕獲內部異常,外部也要回滾。
與REQUIRED_NEW區別:REQUIRED_NEW的內外事務沒有直接的關系,外部回滾內部不會回滾。
事務拋異常與回滾的關系
事務掛起和事務嵌套,在有異常發生的情況下,會變得更復雜。下面根據代碼來解釋。
@Transactional(propagation = XXXX)
public outerMethod(boolean willThrowException, boolean willInnerThrowException, boolean needCatch) {
insertOrUpdateTable1Before();
try {
innerMethod(willInnerThrowException);
} catch (Throwable t) {
if(!needCatch) {
throw new RuntimeException("innerException not Catch");
}
insertOrUpdateTable1Middle();
if(willThrowException) {
throw new RuntimeException("outerException");
}
insertOrUpdateTable1After();
}
@Transactional(propagation = XXXX)
public innerMethod(boolean willThrowException) {
insertOrUpdateTable2Before()
if(willThrowException) {
throw new RuntimeException("innerException");
}
insertOrUpdateTable2After()
}
以上代碼中innerMethod()和outerMethod()可以用來表示各種代碼寫法。兩個方法的事務(如果有)是否回滾,需要判斷:
- 當前方法是否在事務中。如果在,自己的異常會導致這個事務回滾。
- innerMethod()和outerMethod()所屬的事務是否是同一個。如果是,即使innerMethod()的異常被捕獲,outerMethod()也會回滾。同理outerMethod()回滾時會導致nnerMethod()回滾
異常在事務傳播場景下,容易造成困惑的原因是,innerMethod()和outerMethod()兩個事務的關系,被innerMethod()的異常干擾:
即使innerMethod()是REQUIRED_NEW,如果拋出異常且outerMethod()沒有捕獲處理,導致的結果是兩個方法都被回滾,和預期的事務掛起不符,需要做額外的操作(捕獲並處理)才能達到預期效果。
而且,這里的范式還是最簡單的場景,即使如此分析起來也花費了大量的時間。如果有更多層的嵌套,或者事務方法A()依次調用B()、C()兩個事務方法,場景會更加復雜。
這種復雜的特性也是我工作中只用REQUIRED級別的原因,讓所有操作都在一個事務內,簡化邏輯判斷。同時,將開啟事務的方法集中在應用的同一分層中,避免嵌套。
其他代碼實例理解
再談事務回滾與Spring
Spring聲明式事務管理默認只對非檢查型異常進行事務回滾,而對檢查型異常則不進行回滾操作。
非檢查型異常即運行時異常和它的繼承類,檢查型異常則為除了非檢查型異常以外其他所有的Exception及繼承類。
這樣就很容易理解為什么平時應用開發時,推薦在內部邏輯中統一拋RuntimeException或系統封裝的BizException extends RuntimeException了。
https://www.iteye.com/blog/tommy-lu-2401180
三、性能與優化
5. 執行計划
確認SQL在實際執行時的執行情況,如是否走上索引、走了哪個索引、掃描行數、執行順序(如多個select級聯查詢)
查看方式
explain XXX
解讀
MySql: MySQL_執行計划詳細說明
6. 索引相關
6.1 聚集/非聚集索引
- 聚集索引:邏輯上和物理上都是連續的,如主鍵,一般一個表只有一個聚集索引
- 非聚集索引:邏輯上是連續的但物理上不是
以Mysql的InnoDB為例:
主鍵是聚集索引。
唯一索引、普通索引、前綴索引等都是二級索引(輔助索引)。
結合B+樹的知識,對於聚集索引,索引數據和存儲數據是在一起的,比如id-age這個記錄。
對於非聚集索引,只有索引數據,定位具體的記錄需要通過索引來找,也即通過索引找到id,再通過id找到id-age這條記錄。
6.2 覆蓋索引
查詢條件和結果全部在一個索引中,MySql不需要通過二級索引查到主鍵后再查一遍數據就可以返回查詢數據。覆蓋索引可以大大提升查詢效率,舉例
select a, b from table_x where c = XXX order by d;
其中a、b、c、d全部在索引中,那么這就是覆蓋索引。
對於做不到覆蓋索引的查詢,查到主鍵后還要回到數據表中把數據查詢出來,則稱為__回表__。
6.3 索引有序性
對於聯合索引,建立(a, b, c)相當於建立(a), (a,b), (a,b,c)。
在這個索引下,遵循”最左前綴原理“,即先按a排序,再按b排序,最后按c排序。
如果缺失了前一列,如where b = xxx,則走不上索引。
如果某一列不是等值匹配,如where a>10 and b = 1,則只能部分走上索引,b走不上索引。非等值匹配有<、>、!=、IN、LIKE等。
更完整的可以參考mysql組合索引的有序性
6.4 創建了索引但沒有走上的原因
- 使用了<、>、!=、IN、LIKE等(非最左的like,也即like 'xxx%'是可以的)
- 使用or連接查詢子句
- 預期使用聯合索引,但實際上沒有按照最左前綴原理排序(見上文7.3節)
- 字符串類型沒有使用引號
- 全表掃描比走索引快
- where子句中包含了函數或表達式
為什么你創建的數據庫索引沒有生效,索引失效的條件!
7. 行鎖和表鎖
select...for update,走上索引(含主鍵)是行鎖,沒走上就是表鎖。但是如果索引匹配過多,也會變成表鎖。
[轉載&整理&鏈接]mysql 通過測試'for update',深入了解行鎖、表鎖、索引
8. 索引的B+樹
https://www.cnblogs.com/tiancai/p/9024351.html
https://www.jianshu.com/p/9bd572b0a0d4
https://www.jianshu.com/p/23524cc57ca4
簡單概括一下:
B樹的中間節點和葉子節點都有不止一個關鍵字(key)。B樹出現的目的是減少磁盤臂移動的開銷從而,盡量減少讀寫的次數。
B+樹與B樹的不同在於,B+樹的數據都在葉子節點上,中間件節點沒有數據。
應用:由於B樹最左前綴匹配的特性,如果用左模糊查詢(like "%xxx")是走不上索引的。
四、應用開發
9. 分頁查詢
查詢第N頁(下標從1開始)數據,每頁大小PageSize
// 先獲取符合條件的總數
select count(1) from tableA where XXX
// 查詢該頁
// 偏移量,可選 offset = (pageSize-1) * N
// 行數 rows = pageSize
select row1, ..., rowN from tableA where XXX limit offset, rows
10. Join
10.1 語法
SELECT Table1.Row1, Table1.Row2, Table2.Row1
FROM Table1
INNER JOIN Table2
ON Table1.Row2 = Table2.Row2
ORDER BY Table1.Row1
10.2 種類
inner join( = join),都匹配才返回
left join,左表全返回不管右表有沒有匹配
right join,右表全返回不管左表有沒有匹配
full join,全返回,左表右表無論對方匹配都返回所有行
11. MyBatis緩存
MyBatis緩存分為兩級:一級緩存,SqlSession級別;二級緩存,SqlSessionFactory級別。和通常命名習慣相反,二級緩存的作用范圍大於一級緩存,原因是,SqlSession是由SqlSessionFactory創建的。
MyBatis默認開啟一級緩存,不開啟二級緩存。一級緩存生效於同一個SqlSession,當這個session沒有做任何update操作且查詢完全相同時,會返回一樣的數據。
此時,在並發環境下,很有可能會發生這種情況:在一台服務器A上連續查詢兩次,兩次屬於同一個SqlSession;中間另一個服務器B對表做了更新,A看到的第二次查詢結果仍然是舊的。
關於緩存的細節,如如何判斷“同一次查詢”、緩存有效期、SqlSession原理,可以自行查閱。推薦mybatis中文官網,有很多原理的介紹。
在實踐中,spring和mybatis整合以后每次查詢都會刷新sqlSession,即一級緩存是無效的。
MyBatis緩存系列
單獨提一下,二級緩存的readOnly默認為false,同一條數據在內存中每個對象都是獨立的,可修改相互不影響。可參考如何理解Mybatis二級緩存配置中的readOnly?
12. mybatis和hibernate
我在工作中絕大多數時間都用mybatis+spring/springboot寫持久層,只有一個應用因為使用SpringDataJPA才對hibernate才做了一些了解。
看了一些資料,了解到二者在寫法以外,性能的差別主要在於多表查詢這個場景,hibernate會比mybatis慢一些,原因是
hibernate為了保證POJO的數據完整性,需要將關聯的數據加載,需要額外地查詢更多的數據。
MyBatis和Hibernate相比,優勢在哪里? - 鄭沐興的回答 - 知乎
此外,JPA如果想運行原生sql,可以使用EntityManager。
13. 水平擴展與垂直擴展
13.1 水平擴展——分庫分表一般思路
- 按某一字段將一張表分片,如userId。分片方式:
- 第X位到Y位的值
- 字段hash值
- 特殊值特殊處理,如某KA(Key Account關鍵客戶)數據量較大,單獨一個分表
13.2 水平擴展——歷史庫
按日期定時同步遷移及清理線上數據
查詢需要根據日期路由到線上庫或歷史庫
13.3 水平擴展——按業務拆表
按業務,已處理數據及未處理數據拆分。如已受理未申請單和已完結申請單分開保存。
13.4 垂直擴展
提供更多、更強、容量更大的硬件資源。
13.5 FailOver
在計算機術語中,故障轉移(英語:failover),即當活動的服務或應用意外終止時,快速啟用冗余或備用的服務器、系統、硬件或者網絡接替它們工作。 故障轉移(failover)與交換轉移操作基本相同,只是故障轉移通常是自動完成的,沒有警告提醒手動完成,而交換轉移需要手動進行。 ——wiki
FailOver是從應用層面做的,不是單純DB層面。
13.5.1 背景
單庫架構,一旦庫掛掉整個服務不可用;
主備架構,切換時有時間延遲;
FailOver從分布上來看仍然是主備架構,但是增加了系統自動切換恢復能力。
13.5.2 思想
和去IOE是一致的,用大量相對廉價的硬件,拆分服務,減少單點,提升整體的可用性。
13.5.3 交互模式
僅舉兩個最典型的例子,具體場景需要結合硬件能力和應用架構綜合分析。
13.5.3.1 記賬型
特點:
- 主備准實時同步,Failover庫平時不做讀寫
- 主備庫表結構一致,Failover庫不一定和主備庫的表一致(可能會少一些不需要用到的表)
- 賬戶型數據保持最終一致性即可
方案:
- 按比列拆表拆庫,降低單個庫掛掉時影響用戶數
- 正常工作時,主備准實時同步,Failover庫不讀寫
- 主庫發生異常時,切換到備庫讀,Failover庫記錄操作信息。同時,業務操作盡量分流到不依賴相關庫到支路上。
- 主庫恢復時,不再寫入Failover,將Failover庫和主庫內容做merge,回寫主庫,主庫再同步備庫
注:可以采取雙寫、基於讀庫(上文中所述,利用oracle的data guard、mysql的replication等)、異步消息等保證主備一致。
13.5.3.2 交易流水型
特點:
- 數據保證創建,不保證推進。即交易下單失敗,重新下單
- failover庫交易號與主庫通過某些位隔離,不重復
方案:
- 和“記賬型”類似,Failover庫數據推進業務完成即可
- 可以不回寫failover期間的數據,依賴中間件讀failover庫中數據
13.6 讀寫分離
為了解決讀大於多於寫的場景下數據庫瓶頸的一種架構模式。同樣需要結合具體業務不能生搬硬套。
主要是一寫多讀的架構,在主庫掛掉的場景下有可能需要考慮使用paxos算法來決定新的主庫。
在做讀寫分離前,可以先考慮緩存是否能解決當前場景的問題。
讀寫分離和CQRS
CQRS是讀寫分離的一種形式,指的是__增刪改命令__與__查詢__的分離。
命令執行后不是實時更新到讀庫,而是異步化的,讀的內容有一定的延遲。由於需要依賴事件機制,建議只在復雜查詢場景使用。
可以參考數據庫(七),讀寫分離到CQRS
五、運維
14. binlog
記錄DB操作(不含查詢)及其他執行信息的二進制日志。
可以參考下面兩篇文章簡單了解下。
【原創】研發應該懂的binlog知識(上)
【原創】研發應該懂的binlog知識(下)
六、其他話題
15. 零碎的話題
想起來就補一些。
15.1 列的默認值
對於有默認值的非空列,如果在insert語句中指明了這一列且值為null,插入仍然會報錯,此時不會取默認值。讓該列取默認值的方式是,不讓該列出現在insert語句中。
15.2 索引下推
MySql5.6做的優化之一,可以在like查詢中提高性能。利用查詢子句中能確定的查詢條件,減少一次查詢匹配到的索引,從而減少回表查詢的數據。
16. 延伸話題
可以自行研究的話題,限於筆者接觸范圍和篇幅,不展開來寫。
- 索引建立實踐,是否越多越好,應該怎么選擇索引列
- hibernate和mybaits的區別,最大區別是mybatis需要手寫sql,用一定的工作量更大的靈活性,利於優化和多表聯合查詢
- redo log、undo log,與DB本身的分離
- 以下內容可能被濫用,我在實際工作中幾乎沒有用到,有興趣可以自行了解。
- 觸發器
- union
- 視圖
- 全表掃描時發生的filesort原理
附:“點評“ 《阿里巴巴JAVA開發手冊》之MySql規范部分
開發中遵守一些事先約定好的規范,有助於提升研發效率(無論是個人還是團隊內部或團隊之間),避免犯一些重復錯誤,也有助於后續的維護。對於《阿里巴巴JAVA開發手冊》中的規范,原版沒有寫明原因,本來想MySql規范部分這一部分補一下點評的,但是發現前兩天新出的泰山版已經補上很多說明,沒必要一一點評,直接下載來看就好:https://files.cnblogs.com/files/wuyuegb2312/《Java開發手冊(泰山版)》.pdf.zip
可以看出,前面一部分有很多規范都是和Java OOP相關聯的。對於另外的部分條目,是之前沒注意到的,單獨拉出來點評下。
count(*)和count(1)
【強制】不要使用 count(列名)或 count(常量)來替代 count(),count()是 SQL92 定義的標
准統計行數的語法,跟數據庫無關,跟 NULL 和非 NULL 無關。
說明:count(*)會統計值為 NULL 的行,而 count(列名)不會統計此列為 NULL 值的行。
官方文檔提到,InnoDB下count(*)和count(1)是沒有區別的:
InnoDB handles SELECT COUNT() and SELECT COUNT(1) operations in the same way. There is no performance difference.
但考慮到其他實現對count()有優化(如MyISAM,前提是沒有WHERE和GROUP BY子句,直接取緩存的總數),再考慮到用其他DB的情況,統一起見一直用count(*)就好了。
更詳細的分析可以看 為什么阿里巴巴禁止使用 count(列名)或 count(常量)來替代 count(*)
禁用外鍵
【強制】不得使用外鍵與級聯,一切外鍵概念必須在應用層解決。
說明:(概念解釋)學生表中的 student_id 是主鍵,那么成績表中的student_id 則為外鍵。如果更新學生表中的 student_id,同時觸發成績表中的 student_id 更新,即為級聯更新。外鍵與級聯更新適用於單機低並發,不適合分布式、高並發集群;級聯更新是強阻塞,存在數據庫更新風暴的風險;外鍵影響數據庫的插入速度。
禁止使用外鍵,在本例中並不是不允許在成績表中存放student_id字段,只是不設置成為外鍵即可,更新由應用層來做。