1. 程式人生 > 其它 >資料庫-資料庫系統原理

資料庫-資料庫系統原理

資料庫-資料庫系統原理

    落花人獨立,微雨燕雙飛。

簡介:資料庫-資料庫系統原理。

一、事務

概念

事務指的是滿足 ACID 特性的一組操作,可以通過 Commit 提交一個事務,也可以使用 Rollback 進行回滾。

ACID

1. 原子性(Atomicity)

事務被視為不可分割的最小單元,事務的所有操作要麼全部提交成功,要麼全部失敗回滾。

回滾可以用回滾日誌(Undo Log)來實現,回滾日誌記錄著事務所執行的修改操作,在回滾時反向執行這些修改操作即可。

2. 一致性(Consistency)

資料庫在事務執行前後都保持一致性狀態。在一致性狀態下,所有事務對同一個資料的讀取結果都是相同的。

3. 隔離性(Isolation)

一個事務所做的修改在最終提交以前,對其它事務是不可見的。

4. 永續性(Durability)

一旦事務提交,則其所做的修改將會永遠儲存到資料庫中。即使系統發生崩潰,事務執行的結果也不能丟失。

系統發生崩潰可以用重做日誌(Redo Log)進行恢復,從而實現永續性。與回滾日誌記錄資料的邏輯修改不同,重做日誌記錄的是資料頁的物理修改。

事務的 ACID 特性概念簡單,但不是很好理解,主要是因為這幾個特性不是一種平級關係:

  • 只有滿足一致性,事務的執行結果才是正確的。
  • 在無併發的情況下,事務序列執行,隔離性一定能夠滿足。此時只要能滿足原子性,就一定能滿足一致性。
  • 在併發的情況下,多個事務並行執行,事務不僅要滿足原子性,還需要滿足隔離性,才能滿足一致性。
  • 事務滿足持久化是為了能應對系統崩潰的情況。

AUTOCOMMIT

MySQL 預設採用自動提交模式。也就是說,如果不顯式使用START TRANSACTION語句來開始一個事務,那麼每個查詢操作都會被當做一個事務並自動提交。

二、併發一致性問題

在併發環境下,事務的隔離性很難保證,因此會出現很多併發一致性問題。

丟失修改

丟失修改指一個事務的更新操作被另外一個事務的更新操作替換。一般在現實生活中常會遇到,例如:T1和 T2兩個事務都對一個數據進行修改,T1先修改並提交生效,T2隨後修改,T2的修改覆蓋了 T1

的修改。

讀髒資料

讀髒資料指在不同的事務下,當前事務可以讀到另外事務未提交的資料。例如:T1修改一個數據但未提交,T2隨後讀取這個資料。如果 T1撤銷了這次修改,那麼 T2讀取的資料是髒資料。

不可重複讀

不可重複讀指在一個事務內多次讀取同一資料集合。在這一事務還未結束前,另一事務也訪問了該同一資料集合並做了修改,由於第二個事務的修改,第一次事務的兩次讀取的資料可能不一致。例如:T2讀取一個數據,T1對該資料做了修改。如果 T2再次讀取這個資料,此時讀取的結果和第一次讀取的結果不同。

幻影讀

幻讀本質上也屬於不可重複讀的情況,T1讀取某個範圍的資料,T2在這個範圍內插入新的資料,T1再次讀取這個範圍的資料,此時讀取的結果和和第一次讀取的結果不同。

產生併發不一致性問題的主要原因是破壞了事務的隔離性,解決方法是通過併發控制來保證隔離性。併發控制可以通過封鎖來實現,但是封鎖操作需要使用者自己控制,相當複雜。資料庫管理系統提供了事務的隔離級別,讓使用者以一種更輕鬆的方式處理併發一致性問題。

三、封鎖

封鎖粒度

MySQL 中提供了兩種封鎖粒度:行級鎖以及表級鎖。

應該儘量只鎖定需要修改的那部分資料,而不是所有的資源。鎖定的資料量越少,發生鎖爭用的可能就越小,系統的併發程度就越高。

但是加鎖需要消耗資源,鎖的各種操作(包括獲取鎖、釋放鎖、以及檢查鎖狀態)都會增加系統開銷。因此封鎖粒度越小,系統開銷就越大。

在選擇封鎖粒度時,需要在鎖開銷和併發程度之間做一個權衡。

封鎖型別

1. 讀寫鎖

  • 互斥鎖(Exclusive),簡寫為 X 鎖,又稱寫鎖。
  • 共享鎖(Shared),簡寫為 S 鎖,又稱讀鎖。

有以下兩個規定:

  • 一個事務對資料物件 A 加了 X 鎖,就可以對 A 進行讀取和更新。加鎖期間其它事務不能對 A 加任何鎖。
  • 一個事務對資料物件 A 加了 S 鎖,可以對 A 進行讀取操作,但是不能進行更新操作。加鎖期間其它事務能對 A 加 S 鎖,但是不能加 X 鎖。

2. 意向鎖

使用意向鎖(Intention Locks)可以更容易地支援多粒度封鎖。

在存在行級鎖和表級鎖的情況下,事務 T 想要對錶 A 加 X 鎖,就需要先檢測是否有其它事務對錶 A 或者表 A 中的任意一行加了鎖,那麼就需要對錶 A 的每一行都檢測一次,這是非常耗時的。

意向鎖在原來的 X/S 鎖之上引入了 IX/IS,IX/IS 都是表鎖,用來表示一個事務想要在表中的某個資料行上加 X 鎖或 S 鎖。有以下兩個規定:

  • 一個事務在獲得某個資料行物件的 S 鎖之前,必須先獲得表的 IS 鎖或者更強的鎖;
  • 一個事務在獲得某個資料行物件的 X 鎖之前,必須先獲得表的 IX 鎖。

通過引入意向鎖,事務 T 想要對錶 A 加 X 鎖,只需要先檢測是否有其它事務對錶 A 加了 X/IX/S/IS 鎖,如果加了就表示有其它事務正在使用這個表或者表中某一行的鎖,因此事務 T 加 X 鎖失敗。

封鎖協議

1. 三級封鎖協議

一級封鎖協議

事務 T 要修改資料 A 時必須加 X 鎖,直到 T 結束才釋放鎖。

可以解決丟失修改問題,因為不能同時有兩個事務對同一個資料進行修改,那麼事務的修改就不會被覆蓋。

二級封鎖協議

在一級的基礎上,要求讀取資料 A 時必須加 S 鎖,讀取完馬上釋放 S 鎖。

可以解決讀髒資料問題,因為如果一個事務在對資料 A 進行修改,根據 1 級封鎖協議,會加 X 鎖,那麼就不能再加 S 鎖了,也就是不會讀入資料。

三級封鎖協議

在二級的基礎上,要求讀取資料 A 時必須加 S 鎖,直到事務結束了才能釋放 S 鎖。

可以解決不可重複讀的問題,因為讀 A 時,其它事務不能對 A 加 X 鎖,從而避免了在讀的期間資料發生改變。

2. 兩段鎖協議

加鎖和解鎖分為兩個階段進行。

可序列化排程是指,通過併發控制,使得併發執行的事務結果與某個序列執行的事務結果相同。序列執行的事務互不干擾,不會出現併發一致性問題。

事務遵循兩段鎖協議是保證可序列化排程的充分條件。例如以下操作滿足兩段鎖協議,它是可序列化排程。

lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B)

但不是必要條件,例如以下操作不滿足兩段鎖協議,但它還是可序列化排程。

lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C)

MySQL 隱式與顯式鎖定

MySQL 的 InnoDB 儲存引擎採用兩段鎖協議,會根據隔離級別在需要的時候自動加鎖,並且所有的鎖都是在同一時刻被釋放,這被稱為隱式鎖定。

InnoDB 也可以使用特定的語句進行顯示鎖定:

1 SELECT ... LOCK In SHARE MODE;
2 SELECT ... FOR UPDATE;

四、隔離級別

未提交讀(READ UNCOMMITTED)

事務中的修改,即使沒有提交,對其它事務也是可見的。

提交讀(READ COMMITTED)

一個事務只能讀取已經提交的事務所做的修改。換句話說,一個事務所做的修改在提交之前對其它事務是不可見的。

可重複讀(REPEATABLE READ)

保證在同一個事務中多次讀取同一資料的結果是一樣的,可重複讀為 Mysql 預設隔離級別。

可序列化(SERIALIZABLE)

強制事務序列執行,這樣多個事務互不干擾,不會出現併發一致性問題。

該隔離級別需要加鎖實現,因為要使用加鎖機制保證同一時間只有一個事務執行,也就是保證事務序列執行。

五、多版本併發控制

多版本併發控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 儲存引擎實現隔離級別的一種具體方式,用於實現提交讀可重複讀這兩種隔離級別。

提交讀隔離級別總是讀取最新的資料行,要求很低,無需使用 MVCC。

可序列化隔離級別需要對所有讀取的行都加鎖,單純使用 MVCC 無法實現。

基本思想

在封鎖一節中提到,加鎖能解決多個事務同時執行時出現的併發一致性問題。在實際場景中讀操作往往多於寫操作,因此又引入了讀寫鎖來避免不必要的加鎖操作,例如讀和讀沒有互斥關係。讀寫鎖中讀和寫操作仍然是互斥的,而 MVCC 利用了多版本的思想,寫操作更新最新的版本快照,而讀操作去讀舊版本快照,沒有互斥關係,這一點和 CopyOnWrite 類似。

在 MVCC 中事務的修改操作(DELETE、INSERT、UPDATE)會為資料行新增一個版本快照。

髒讀和不可重複讀最根本的原因是事務讀取到其它事務未提交的修改。在事務進行讀取操作時,為了解決髒讀和不可重複讀問題,MVCC 規定只能讀取已經提交的快照。當然一個事務可以讀取自身未提交的快照,這不算是髒讀。

版本號

  • 系統版本號 SYS_ID:是一個遞增的數字,每開始一個新的事務,系統版本號就會自動遞增。
  • 事務版本號 TRX_ID :事務開始時的系統版本號。

Undo 日誌

MVCC 的多版本指的是多個版本的快照,快照儲存在 Undo 日誌中,該日誌通過回滾指標 ROLL_PTR 把一個數據行的所有快照連線起來。

例如在 MySQL 建立一個表 t,包含主鍵 id 和一個欄位 x。我們先插入一個數據行,然後對該資料行執行兩次更新操作。

1 INSERT INTO t(id, x) VALUES(1, "a");
2 UPDATE t SET x="b" WHERE id=1;
3 UPDATE t SET x="c" WHERE id=1;

因為沒有使用START TRANSACTION將上面的操作當成一個事務來執行,根據 MySQL 的 AUTOCOMMIT 機制,每個操作都會被當成一個事務來執行,所以上面的操作總共涉及到三個事務。快照中除了記錄事務版本號 TRX_ID 和操作之外,還記錄了一個 bit 的 DEL 欄位,用於標記是否被刪除。

INSERT、UPDATE、DELETE 操作會建立一個日誌,並將事務版本號 TRX_ID 寫入。DELETE 可以看成是一個特殊的 UPDATE,還會額外將 DEL 欄位設定為 1。

ReadView

MVCC 維護了一個 ReadView 結構,主要包含了當前系統未提交的事務列表 TRX_IDs {TRX_ID_1, TRX_ID_2, ...},還有該列表的最小值 TRX_ID_MIN 和 最大值 TRX_ID_MAX。

在進行 SELECT 操作時,根據資料行快照的 TRX_ID 與 TRX_ID_MIN 和 TRX_ID_MAX 之間的關係,從而判斷資料行快照是否可以使用:

  • TRX_ID < TRX_ID_MIN,表示該資料行快照時在當前所有未提交事務之前進行更改的,因此可以使用。

  • TRX_ID > TRX_ID_MAX,表示該資料行快照是在事務啟動之後被更改的,因此不可使用。

  • TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根據隔離級別再進行判斷:

    • 提交讀:如果 TRX_ID 在 TRX_IDs 列表中,表示該資料行快照對應的事務還未提交,則該快照不可使用。否則表示已經提交,可以使用。
    • 可重複讀:都不可以使用。因為如果可以使用的話,那麼其它事務也可以讀到這個資料行快照並進行修改,那麼當前事務再去讀這個資料行得到的值就會發生改變,也就是出現了不可重複讀問題。

在資料行快照不可使用的情況下,需要沿著 Undo Log 的回滾指標 ROLL_PTR 找到下一個快照,再進行上面的判斷。

快照讀與當前讀

1. 快照讀

MVCC 的 SELECT 操作是快照中的資料,不需要進行加鎖操作。

SELECT * FROM table ...;

2. 當前讀

MVCC 其它會對資料庫進行修改的操作(INSERT、UPDATE、DELETE)需要進行加鎖操作,從而讀取最新的資料。可以看到 MVCC 並不是完全不用加鎖,而只是避免了 SELECT 的加鎖操作。

1 INSERT;
2 UPDATE;
3 DELETE;

在進行 SELECT 操作時,可以強制指定進行加鎖操作。以下第一個語句需要加 S 鎖,第二個需要加 X 鎖。

1 SELECT * FROM table WHERE ? lock in share mode;
2 SELECT * FROM table WHERE ? for update;

六、Next-Key Locks

Next-Key Locks 是 MySQL 的 InnoDB 儲存引擎的一種鎖實現。

MVCC 不能解決幻影讀問題,Next-Key Locks 就是為了解決這個問題而存在的。在可重複讀(REPEATABLE READ)隔離級別下,使用 MVCC + Next-Key Locks 可以解決幻讀問題。

Record Locks

鎖定一個記錄上的索引,而不是記錄本身。

如果表沒有設定索引,InnoDB 會自動在主鍵上建立隱藏的聚簇索引,因此 Record Locks 依然可以使用。

Gap Locks

鎖定索引之間的間隙,但是不包含索引本身。例如當一個事務執行以下語句,其它事務就不能在 t.c 中插入 15。

SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;

Next-Key Locks

它是 Record Locks 和 Gap Locks 的結合,不僅鎖定一個記錄上的索引,也鎖定索引之間的間隙。它鎖定一個前開後閉區間,例如一個索引包含以下值:10, 11, 13, and 20,那麼就需要鎖定以下區間:

1 (-∞, 10]
2 (10, 11]
3 (11, 13]
4 (13, 20]
5 (20, +∞)
View Code

七、關係資料庫設計理論

函式依賴

記 A->B 表示 A 函式決定 B,也可以說 B 函式依賴於 A。

如果 {A1,A2,... ,An} 是關係的一個或多個屬性的集合,該集合函式決定了關係的其它所有屬性並且是最小的,那麼該集合就稱為鍵碼。

對於 A->B,如果能找到 A 的真子集 A',使得 A'-> B,那麼 A->B 就是部分函式依賴,否則就是完全函式依賴。

對於 A->B,B->C,則 A->C 是一個傳遞函式依賴。

異常

以下的學生課程關係的函式依賴為 {Sno, Cname} -> {Sname, Sdept, Mname, Grade},鍵碼為 {Sno, Cname}。也就是說,確定學生和課程之後,就能確定其它資訊。

SnoSnameSdeptMnameCnameGrade
1 學生-1 學院-1 院長-1 課程-1 90
2 學生-2 學院-2 院長-2 課程-2 80
2 學生-2 學院-2 院長-2 課程-1 100
3 學生-3 學院-2 院長-2 課程-2 95

不符合正規化的關係,會產生很多異常,主要有以下四種異常:

  • 冗餘資料:例如學生-2出現了兩次。
  • 修改異常:修改了一個記錄中的資訊,但是另一個記錄中相同的資訊卻沒有被修改。
  • 刪除異常:刪除一個資訊,那麼也會丟失其它資訊。例如刪除了課程-1需要刪除第一行和第三行,那麼學生-1的資訊就會丟失。
  • 插入異常:例如想要插入一個學生的資訊,如果這個學生還沒選課,那麼就無法插入。

正規化

正規化理論是為了解決以上提到四種異常。

高級別正規化的依賴於低級別的正規化,1NF 是最低級別的正規化。

1. 第一正規化 (1NF)

屬性不可分,即要求資料庫表的每一列都是不可分割的原子資料項。

2. 第二正規化 (2NF)

每個非主屬性完全函式依賴於鍵碼,即確保資料庫表中的每一列都和主鍵相關,而不能只與主鍵的某一部分相關。

可以通過分解來滿足。

分解前

SnoSnameSdeptMnameCnameGrade
1 學生-1 學院-1 院長-1 課程-1 90
2 學生-2 學院-2 院長-2 課程-2 80
2 學生-2 學院-2 院長-2 課程-1 100
3 學生-3 學院-2 院長-2 課程-2 95

以上學生課程關係中,{Sno, Cname} 為鍵碼,有如下函式依賴:

  • Sno -> Sname, Sdept
  • Sdept -> Mname
  • Sno, Cname-> Grade

Grade 完全函式依賴於鍵碼,它沒有任何冗餘資料,每個學生的每門課都有特定的成績。

Sname, Sdept 和 Mname 都部分依賴於鍵碼,當一個學生選修了多門課時,這些資料就會出現多次,造成大量冗餘資料。

分解後

關係-1

SnoSnameSdeptMname
1 學生-1 學院-1 院長-1
2 學生-2 學院-2 院長-2
3 學生-3 學院-2 院長-2

有以下函式依賴:

  • Sno -> Sname, Sdept
  • Sdept -> Mname

關係-2

SnoCnameGrade
1 課程-1 90
2 課程-2 80
2 課程-1 100
3 課程-2 95

有以下函式依賴:

  • Sno, Cname -> Grade

3. 第三正規化 (3NF)

非主屬性不傳遞函式依賴於鍵碼,即確保資料表中的每一列資料都和主鍵直接相關,而不能間接相關。

上面的 關係-1 中存在以下傳遞函式依賴:

  • Sno -> Sdept -> Mname

可以進行以下分解:

關係-11

SnoSnameSdept
1 學生-1 學院-1
2 學生-2 學院-2
3 學生-3 學院-2

關係-12

SdeptMname
學院-1 院長-1
學院-2 院長-2

八、ER 圖

Entity-Relationship,有三個組成部分:實體、屬性、聯絡。

用來進行關係型資料庫系統的概念設計。

落花人獨立

微雨燕雙飛