淺析事務,鎖和索引


MySQL的默認存儲引擎爲InnoDB而不是MyISAM的一大原因就是InnoDB是支持事務的,而MyISAM不支持事務。(我覺得前者強調安全,而後者性能更好,當然在要求併發量的當下,不足以成爲被選擇的理由,所以也漸漸被InnoDB淘汰)。

事務

事務具有ACID四大特性:

  1. 原子性(Atomicity):有點像std::atomic(當然不完全一樣)是一個最小的原子單元,是一個不可分割的的一條(或多條)SQL命令集合,要麼全部完成,要麼全部失敗,執行失敗就會回滾(rollback)
  2. 一致性(Consistency):事務的一致性說的是事務要保證把數據庫從一個正確的狀態遷移到另一個正確的狀態。正確狀態就是說數據庫裏的數據滿足其約束條件。
  3. 隔離性(Isolation):一個事務不會影響其他事務的運行,在多個事務運行的情況下,隔離性保證了併發基礎,當然實際上還需要配合鎖來保證併發下的數據安全。
  4. 持久性(Durability):事務完成後,對數據庫的更改永久保存,不會因爲故障丟失。

每條SQL語句都默認封裝爲了一個事務,自動提交,影響速度,所以爲了效率可以將多個SQL命令使用begincommit封裝爲一個事務

一致性中,數據庫從一個正確的狀態遷移到另一個正確的狀態,指的是數據處於一種語義上有意義且正確的狀態,
1.原子性和一致性的的側重點不同:原子性關注狀態,要麼全部成功,要麼全部失敗,不存在部分成功的狀態。而一致性關注數據的可見性,中間狀態的數據對外部不可見,只有最初狀態和最終狀態的數據對外可見。
2.在未提交讀的隔離級別下,會造成髒讀,這就是因爲一個事務讀到了另一個事務操作內部的數據。ACID中是的一致性描述的是一個最理想的事務應該怎樣的,是一個強一致性狀態,如果要做到這點,需要使用排它鎖把事務排成一隊,即Serializable的隔離級別,這樣性能就大大降低了。現實是骨感的,所以使用隔離性的不同隔離級別來破壞一致性,來獲取更好的性能。

爲了控制多個事務對數據的併發操作,我們需要加鎖,否則有可能發生數據的丟失或者錯亂

  • 樂觀鎖和悲觀鎖是兩種應用層去實現的併發模式
    1. 悲觀鎖就是針對一切數據修改都加鎖
    2. 樂觀鎖的實現可以基於版本號或時間戳來實現
  • 共享鎖和排他鎖是InnoDB實現的兩種行鎖
  • 鎖粒度越小,衝突發生的概率越小,但代價越高,就像打掃房間,“粒度”越小,細緻的打掃整個房間,付出的精力當然更大,但相對應的打掃的也更乾淨
  • InnoDB的行鎖又有三種實現方法
    1. 記錄鎖,加在行記錄的索引上,封鎖該行的索引記錄
    2. 間隙鎖,鎖住一個區間,鎖住的的索引的間隙,確保其索引記錄之間的間隙不變
      現假設有表 table,
      在這裏插入圖片描述
      當我們執行:
Session_1 : 
START TRANSACTION;
SELECT * from table WHERE number = 4 FOR UPDATE;
…..
Session_2:
START TRANSACTION;
INSERT into table values (2, 2);		//阻塞
INSERT into table values (2, 4);		//阻塞
INSERT into table values (4, 5);		//阻塞
INSERT into table values (6, 7);		//執行成功
INSERT into table values (11, 12);	//執行成功
…
  1. Next-key鎖,記錄鎖+間隙鎖,鎖住間隙的同時鎖住改行本身
    InnoDB的行鎖算法都是基於索引實現的,鎖定的也都是索引或索引區間

隔離級別的原理

討論完事務和鎖之後,就可以探討一下隔離級別
隔離級別決定的是一個session中的事務對另一個session中事務的影響程度

  1. 讀未提交(Read Uncommitted):所有事務都能看到其他事務未提交的執行結果,據我所知該隔離級別應該在實際應用中很少使用,而且他並沒有明顯的性能優勢,還不能避免髒讀,不可重複讀,幻讀這些數據丟失和錯亂的情況
    原理:事務讀當前數據時不加鎖,在更改數據的時候,對其加行級共享鎖,結束時釋放。
    表現

    • 事務1讀取某行數據時,事務2也能讀取該行數據;
    • 事務2更新該行數據時,事務1能讀到更新後的版本,即使尚未被提交;
    • 事務1對某行數據進行更新時,事務2不能更新該行數據,直到事務1結束。
  2. 讀已提交(Read Committed):一個事務開始後,只能看見已經結束的事務的結果,正在執行的事務無法看到,
    原理:事務對當前被讀取的數據加行級共享鎖(讀到時加),讀完釋放;事務在更新數據時加行級排他鎖,事物結束後釋放
    表現

    • 事務1讀取某行數據,事務2也能同時讀取;
    • 事務2更改某行數據,事務1要麼讀取到其commit前的要麼讀取到其commit後的;
    • 事務1更新某數據時,事務2不能對其進行更新,直到事務1結束
  3. 可重複讀(Repeatable Read):該級別保證了每行數據的一致
    原理:事務在讀取某數據時,加行級共享鎖(開始讀時加),直到事務結束時釋放;
    事務在更新某數據時,加行級排他鎖
    表現

    • 事務1讀取某行數據時,事務2也能對該行數據進行讀取,更新;
    • 當事務2更新某行數據時,事務1讀取該行數據仍然是第一次讀取到的版本;
    • 事務1更新某行數據,事務2不能對其進行更新,直到事務1結束
  4. 可串行化(Serilaizable):最高隔離級別,強制事務串行執行
    原理:事務在讀取數據時,加表級共享鎖,結束時釋放;
    在更新數據時,加表級排他鎖,結束時釋放
    表現

    • 事務1正在讀取A表中的記錄時,則事務2也能讀取A表,但不能對A表做更新、新增、刪除,直到事務1結束。
    • 事務1正在更新A表中的記錄時,則事務2不能讀取A表的任意記錄,也不能更新,直到事務1結束。

而InnoDB默認工作模式爲RR(Repeatable Read)模式,默認加鎖爲next-key鎖,這樣防止了幻讀的發生(不能對該行數據進行修改,或者插入記錄)

幻讀就是說事務1讀取了數據,然後這之間事務2插入了一行或者多行的滿足事務1的選擇條件,導致事務1再次使用相同的選擇條件讀取時,得到了比第一次更多的數據(發生了幻覺)

索引

(就不一一列出B-Tree和B+Tree的實現了,只討論其特性)

索引是一種數據結構,存儲指向列值,包含指向行的指針,通過對其這些指針進行排序,達到一個能夠快速查找的目的。
在沒有索引的情況下,我們進行查找的話,自然是需要遍歷的,這就是一個O(n)的複雜度,那麼當數據量上去的情況下,耗時就會比較高了,在要求性能的時候,這是不被允許的(況且還有磁盤IO 的代價)。
MySQL的索引實現是一個樹形結構,而具體的底層實現上選用的是B+Tree

爲什麼選用B+Tree
數據庫的索引存儲在磁盤中,所以要考慮的不僅僅是查找的效率,還要考慮磁盤IO的效率,如果我們使用二叉查找樹,那麼在最壞的情況下,磁盤IO次數是樹的高度,所以選用了B+Tree這種多叉查找樹,在同等數據量的情況下,降低了樹的高度,讓他更加“矮胖”(意味着更少的磁盤 IO 次數,這也是較之B-Tree的一個優點,B+Tree中間節點不存儲指向行的指針,而都位於葉子節點,所以同等數據量下,B+Tree比B-Tree更加“矮胖”)
而且因爲B+Tree的查詢最終都要在由葉子節點組成的鏈表中,每次查詢的時間複雜度也很平均

我記得MongoDB的索引使用的B-Tree

爲什麼不用哈希索引?
哈希索引的優點就是快,O(1)的查找時間複雜度
但是缺點也很明顯:

  1. 要考慮發生哈希衝突的時候的解決,提高了成本
  2. 可能會有空出的哈希空間,造成浪費
    (記得以前討論過爲什麼epoll使用紅黑樹而不是哈希表,原因類似)
  3. 不能做範圍操作
    (Redis就是一個哈希表搭配一個跳躍表實現的,結合了兩者的優點才同時做到快速和範圍查詢,保證了效率)

MVCC

如果數據庫的事務都是串行執行的,那麼很多問題就不用考慮了,但是這樣做必然會導致性能上的瓶頸,所以我們要使用多事務併發,那麼這樣一來就要面臨數據的一致性和數據的安全性等問題,如果不應對好,可能還沒有享受併發帶來的性能提升,就已經走遠了…

而控制併發有三種途徑,分別是悲觀鎖,樂觀鎖(上篇週報中提到的)以及 MVCC,也就是——多版本併發控制
每個寫操作會創建一個新的數據版本,然後讀操作從有限多個版本中挑選一個合適的版本返回,那麼這樣一來,讀寫之間的衝突被忽略,而怎麼管理和挑選合適的數據版本纔是 MVCC 需要關注的問題。

MVCC 的特點是:讀不上鎖
這樣一來,針對讀多寫少的場景,性能大大提高,

解決了什麼?

MVCC 讀不上鎖,所以可以該機制可以代替行級鎖使用,降低系統開銷(鎖就算粒度再大,也是有不可忽視的系統代價的)

如何實現?

而 MVCC 是通過保存數據在某一個時間點的快照(是不是感覺有點像Redis的RDB持久化?)實現的

有點像每行數據都存在着有限個平行宇宙

  1. 讀取數據:
    每個版本的數據行都有着唯一的時間戳,讀取哪行就在多個版本中選擇一個最大的返回
    在這裏插入圖片描述

  2. 更新數據:
    事務讀取最新版本數據並進行sql命令操作得到數據更新後的結果,然後以此創建一個新版本的數據,其時間戳爲當前最大時間戳+1(創建出來的數據的數據行時間戳保持最大)
    在這裏插入圖片描述
    而MySQL也會定期將最低版本的數據刪除

剛纔說了,讀操作有快照讀,還有一個是當前讀,因爲快照度選擇的可見範圍內的最大時間戳,於是可能讀到的是已經過期的數據,當前讀保證的就是讀取到的是最新的數據(需要加鎖來實現)

我覺得快照讀取就像是拍照片一樣,就像是當前事務的數據來源於一張舊照片(之前某個時間點生成的數據快照),然後當前事務結束前,其他事務怎麼改變數據,都不會影響到這張照片,這就實現了可重複讀

  1. select就是快照讀(照片的生成時間是執行select的時間,不是事務開始的時間)
  2. update,delete,insert是當前讀
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章