事務特性原理及其原理、隔離級別和傳播屬性

一、前言

2020.1.8
對博客內容進行擴展和修改時,寫了半天的內容在點擊保存的時候突然讓我重新登錄,重新登錄後對博客內容的修改都沒了,只好重新寫(新寫的感覺沒有第一遍寫的順暢 ),好氣啊!!!!!!!!

二、事務的四大特性

事務(Transaction)是併發控制單位,是用戶定義的一個操作序列,這些操作要麼都做,要麼都不做,是一個不可分割的工作單位。

1. 介紹

1.1. 原子性

一個事務中所有對數據庫的操作是一個不可分割的操作序列,要麼全做要麼全不做

即事務是執行的最小單元,執行結果只有兩個,要麼成功,要麼失敗回滾。

1.2. 一致性

一致性是指事務必須使數據庫從一個一致性狀態變換到另一個一致性狀態,也就是說一個事務執行之前和執行之後都必須處於一致性狀態。數據不會因爲事務的執行而遭到破壞。

比如,當數據庫只包含成功事務提交的結果時,就說數據庫處於一致性狀態。如果數據庫系統在運行中發生故障,有些事務尚未完成就被迫中斷,這些未完成事務對數據庫所做的修改有一部分已寫入物理數據庫,這時數據庫就處於一種不正確的狀態,或者說是不一致的狀態。

1.3. 持久性

持久性是指一個事務一旦被提交了,那麼對數據庫中的數據的改變就是永久性的,接下來的任何操作和故障都不應該影響到已提交的事務執行結果。

例如我們在使用JDBC操作數據庫時,在提交事務方法後,提示用戶事務操作完成,當我們程序執行完成直到看到提示後,就可以認定事務以及正確提交,即使這時候數據庫出現了問題,也必須要將我們的事務完全執行完成,否則就會造成我們看到提示事務處理完畢,但是數據庫因爲故障而沒有執行事務的重大錯誤。

1.4. 隔離性

一個事務的執行不能其它事務干擾。即一個事務內部的操作及使用的數據對其它併發事務是隔離的,併發執行的各個事務之間不能互相干擾。

隔離性是當多個用戶併發訪問數據庫時,比如操作同一張表時,數據庫爲每一個用戶開啓的事務,不能被其他事務的操作所幹擾,多個併發事務之間要相互隔離。
 關於事務的隔離性數據庫提供了多種隔離級別,稍後會介紹到。

2. MySql 中四大特性實現原理

Mysql 在5.7版本後默認使用 InnoDB 作爲存儲引擎,所以下面介紹 InnoDB中的實現方式。

2.1 原子性

InnoDB 引擎使用 undo log(歸滾日誌)來保證原子性操作

事務對數據庫的每一條數據的改動(INSERT、DELETE、UPDATE)都會被記錄到 undo log 中,

  • insert : undo log 中將插入記錄的主鍵記錄下來,事務回滾時將這條主鍵記錄刪除即可。
  • delete :undo log 中將這條記錄的所有字段內容記錄下來,事務回滾時將由這些內容組成的記錄插入到表中即可。
  • update:undo log 中將這條記錄的舊值都記錄下來,事務回滾時將這條記錄更新爲舊值即可。

當事務執行失敗或者調用了 rollback 方法時,就會觸發回滾事件,利用 undo log 中記錄將數據回滾到修改之前的樣子。

關於undo log 的具體工作流程,詳參: https://blog.51cto.com/qiangmzsx/1768263

2.2 持久性

InnoDB 引擎使用 redo log(歸檔日誌)來保證原子性操作。

由於CPU和磁盤速度不一致問題,Mysql是將磁盤上的數據加載到內存,對內存進行操作,然後再回寫磁盤。假設此時宕機了,在內存中修改的數據全部丟失了,持久性就無法保證。
可能有人會提出將數據寫回磁盤後再將事務提交即可。如果在寫回磁盤前機器宕機,則事務未提交,下次啓動會回滾操作。

但實際上,根據局部性原理和磁盤預讀的特性,磁盤每次IO並不是嚴格按需讀取,即使只需要一個字節,磁盤也會從這個位置開始,順序向後讀取一定長度的數據放入內存。磁盤的預讀一般是頁的整數倍(頁是計算機管理存儲器的邏輯塊,硬件及操作系統往往將主存和磁盤存儲區分割爲連續的大小相等的塊,每個存儲塊稱爲一頁。不同操作系統的頁大小可能不一樣,一般爲8k)。簡單來說,一次IO讀取,不光把當前磁盤地址的數據,而是把相鄰的數據也都讀取到內存緩衝區內。
而上述方案則會出現如下問題:

  • 每次更新可能只是更新一兩個字節,但是卻需要IO整個頁的大小
  • 一個事務中的SQL可能牽扯多個磁盤頁的數據修改,而這些數據物理上可能無限遠,即會出現隨機IO,速度比較慢。

所以,InnoDB 引擎引入了一箇中間層來解決這個持久性的問題,我們把這個叫做 redo log(歸檔日誌)。

當事務中對數據修改時,InnoDB會先將記錄寫入到redo log中,隨後更新內存,這時候更新就算結束了,當事務提交的時候,會將redo log日誌進行刷盤(redo log一部分在內存中,一部分在磁盤上)。這時候即使數據庫宕機重啓,也可以從 redo log中讀取未寫入磁盤中的數據,再根據 undo log 和 bin log 內容決定是回滾數據還是提交數據。

使用 redo log 有以下兩個優勢:

redo log只記錄了修改哪一頁修改的內容,因此體積小,刷盤快。
redo log使用末尾進行追加,屬於順序IO。相較於隨機IO效率更高。

關於undo log 的具體工作流程,詳參: https://mp.weixin.qq.com/s/vmB7Gsr9N3ZfF805wy6eHw

2.3 隔離性

InnnDB利用鎖和 MVCC 機制來保證隔離性。

根據 https://www.cnblogs.com/moershiwei/p/9766916.html 的介紹,我們可以理解爲鎖的實現方式是悲觀鎖的方式(即每次操作先加鎖,再操作,提交後釋放鎖),MVCC是一種樂觀鎖的方式(即每次操作不加鎖,最後提交前對比一下版本號或者其他標誌,如果沒有過程中沒有被修改,就提交)。

2.3.1 數據庫鎖(悲觀鎖)

1. 悲觀鎖按照使用性質劃分:

  • 共享鎖(Share locks簡記爲S鎖):也稱讀鎖,事務A對對象T加s鎖,其他事務也只能對T加S,多個事務可以同時讀,但不能有寫操作,直到A釋放S鎖。

  • 排它鎖(Exclusive locks簡記爲X鎖):也稱寫鎖,事務A對對象T加X鎖以後,其他事務不能對T加任何鎖,只有事務A可以讀寫對象T直到A釋放X鎖。

  • 更新鎖(簡記爲U鎖):用來預定要對此對象施加X鎖,它允許其他事務讀,但不允許再施加U鎖或X鎖;當被讀取的對象將要被更新時,則升級爲X鎖,主要是用來防止死鎖的。因爲使用共享鎖時,修改數據的操作分爲兩步,首先獲得一個共享鎖,讀取數據,然後將共享鎖升級爲排它鎖,然後再執行修改操作。這樣如果同時有兩個或多個事務同時對一個對象申請了共享鎖,在修改數據的時候,這些事務都要將共享鎖升級爲排它鎖。這些事務都不會釋放共享鎖而是一直等待對方釋放,這樣就造成了死鎖。如果一個數據在修改前直接申請更新鎖,在數據修改的時候再升級爲排它鎖,就可以避免死鎖。

2. 悲觀鎖按照作用範圍劃分

  • Record Locks(行鎖) : 行鎖,顧名思義,是加在索引行(對!是索引行!不是數據行!)上的鎖。比如select * from user where id=1 and id=10 for update,就會在id=1和id=10的索引行上加Record Lock。

  • Gap Locks(間隙鎖) : 間隙鎖,它會鎖住兩個索引之間的區域。比如select * from user where id>1 and id<10 for update,就會在id爲(1,10)的索引區間上加Gap Lock。

  • Next-Key Locks(間隙鎖) : 也叫間隙鎖,它是Record Lock + Gap Lock形成的一個閉區間鎖。比如select * from user where id>=1 and id<=10 for update,就會在id爲[1,10]的索引閉區間上加Next-Key Lock。

  • 表鎖: 鎖住整張表

組合起來就會有:行級共享鎖,表級共享鎖,行級排它鎖,表級排它鎖

InnoDB 默認支持表級鎖和行級鎖,但是需要注意的是,如果檢索項(可以簡單理解是where後面的過濾項)如果是索引的話則使用行級鎖,否則使用表級鎖。

2.3.2 MVCC : 多版本併發控制 (樂觀鎖)

InnoDB 默認的隔離級別REPEATABLE READ(可重複讀)需要兩個不同的事務相互之間不能影響,而且還能支持併發,這點悲觀鎖是達不到的,所以REPEATABLE READ(可重複讀)採用的就是樂觀鎖,而樂觀鎖的實現採用的就是MVCC。

雖然使用鎖機制也可以控制併發,但是其系統開銷較大,而MVCC可以在大多數情況下代替行級鎖,使用MVCC,能降低其系統開銷。

InnoDB 通過在每條記錄上創建兩個隱藏的列來實現,這兩列分別是數據版本號db_trx_id(最後更新數據的事務id, 默認是1) 刪除版本號 db_roll_pt (數據刪除的事務id,默認null。 事務id由mysql數據庫自動生成,且遞增。db_trx_id記錄着最近更新這條記錄的事務的id,db_roll_pt 記錄着刪除這條記錄的事務的id。

當查詢時需要同時滿足以下兩個條件
  1、查找數據版本號 db_trx_id,早於(小於等於)當前事務id的數據行。 這樣可以確保事務讀取的數據是事務之前已經存在的。或者是當前事務插入或修改的。
  2、查找刪除版本號 db_roll_pt 爲null 或者大於當前事務版本號的記錄。 這樣確保取出來的數據在當前事務開啓之前沒有被刪除。

更新時: 會生成新的一行。先複製數據,新數據數據版本號爲當前事務id,刪除版本號爲 null 。然後更新 原來數據的刪除版本號爲 當前事務id


下面的a(1, null) 代表a(db_trx_id, db_roll_pt ) 的值:

比如:

  1. 事務A,id 爲1。 插入了記錄a(1, null),b(1, null),並提交。
  2. 事務B,id爲 2 。查詢三遍表記錄(即三遍 select 語句),當查詢結束第一遍時,返回 a(1, null),b(1, null)。此時事務C開始執行。
  3. 事務C,id爲 3。刪除了記錄a(1, 3)並提交。此時表中記錄爲a(1, 3),b(1, null)
  4. 此時事務B執行第二次查詢,a記錄仍滿足查詢條件,仍會被查詢出來,此時查詢結果和第一次查詢結果相同 : a(1, 3),b(1, null)
  5. 事務D,id爲4 。更新操作,更新b記錄,尚未提交。過程是會先複製一條b記錄,此時表中記錄爲a(1, 3), b(1, null), b副本(4, null)。
  6. 此時事務B執行第三次查詢。根據查詢條件,結果仍爲a(1, 3), b(1, null)。 – 此時可以看到,事務B的三次查詢返回結果相同,並未受到事務C、D的影響。
  7. 事務D繼續操作,更新數據b,原來數據的刪除版本號爲 當前事務id。即這時庫中記錄數據爲a(1, 3), b(1, 4), b副本(4, null), 然後提交。
  8. 假設步驟1中事務A,插入記錄a(1, null) 後未插入b便提交,事務B進行了第一次查找,會查找出來a(1, null)。之後事務C插入了數據b(1, null)並提交。事務B進行第二次查找則會查找出來a(1, null), b(1, null)。出現了幻讀。

可以看到MVCC實現了可重複讀的隔離級別,避免了髒讀、不可重複讀,但是可能會出現幻讀。

上面的一個流程只是簡單的文字敘述,詳細內容參看: https://www.cnblogs.com/luchangyou/p/11321607.html


2.4 一致性

數據庫是無法保證一致性的,一致性的保證需要業務代碼來確保業務的一致性。
比如轉賬業務,A扣錢,B加錢,代碼中沒有寫B加錢的邏輯,數據庫自然無法保證,一致性自然無法保證。

二、事務的七種傳播屬性

1. 介紹

在這裏插入圖片描述

事務 解釋
PROPAGATION_REQUIRED 支持當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇。即如果上級具有事務,則使用上級的事務,不具備則自己新建一個事務
PROPAGATION_REQUIRES_NEW 新建事務,如果當前存在事務,把當前事務掛起。即如果上級存在事務,則掛起上級事務,使用自己新創建的事務
PROPAGATION_MANDATORY 支持當前事務,如果當前沒有事務,就拋出異常。即如果上級具有事務,則使用上級的事務,上級沒有事務,則拋出異常
PROPAGATION_SUPPORTS 支持當前事務,如果當前沒有事務,就以非事務方式執行。即如果上級具有事務,則使用上級的事務,如果上級沒有事務,則不開啓事務
PROPAGATION_NOT_SUPPORTED 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。即如果上級具有事務,則使用掛起上級事務,使用非事務方式。
PROPAGATION_NEVER 以非事務方式執行,如果當前存在事務,則拋出異常
PROPAGATION_NESTED 如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則進行與PROPAGATION_REQUIRED類似的操作。

2. 解釋

上面的解釋比較官方,什麼意思呢?具體以下面的一個小例子來解釋。

Spring 中 @Transactional 註解默認的隔離級別是 PROPAGATION_REQUIRED

  1. 首先程序結構如下(其實很好理解,所以寫的比較簡單)
    UserService 調用 insertUser() 方法。 insertUser() 調用 RoleService .insertRole() 中來向數據庫中添加一個角色。我們的目的就是當用戶調用insertUser() 方法時會調用RoleService.insertRole方法.

  2. 首先驗證一個 @Transactional 默認的傳播屬性 PROPAGATION_SUPPORTS
    代碼如下,insertUser 開啓 傳播屬性爲 REQUIRED 的事務,insertRole 開始傳播屬性爲SUPPORTS 的事務。我們需要驗證 inserRole 方法上的事務是否會生效?

    	******************** UserService *************************
        /**
         * 加入兩個角色
         */
        @Override
        @Transactional(propagation = REQUIRED)   // 開啓事務,傳播屬性爲 REQUIRED
        public void insertUser() {
            User user = new User();
            user.setName("陳七");
            user.setPwd("999999");
            user.setParentId(3);
            userMapper.insertUser(user);
            roleService.insertRole();
            int i = 10 / 0;
        }
    
    	******************** UserService *************************
    	
    	******************** RoleService *************************
    	@Override
    	@Transactional(propagation = SUPPORTS)	 // 開啓事務,傳播屬性爲 SUPPORTS
        public void insertRole() {
            roleMapper.insertRole();	// 如果有一條數據,則事務未生效
            int i = 10 / 0;
        }
        ******************** RoleService *************************
    
  3. 執行結果是User表和Role表中都沒有數據,說明insertRole 方法上的事務生效了。

  4. 再回頭 看一下 PROPAGATION_SUPPORTS 傳播屬性的描述:支持當前事務,如果當前沒有事務,就以非事務方式執行。。也就是說本例中 insertRole 方法的事務是否生效是看他的上級inserUser方法是否具有事務的,所以我們這裏去掉了insertUser 方法的事務註解。重新運行。

    	******************** UserService *************************
        /**
         * 加入兩個角色
         */
        @Override
        public void insertUser() {
            User user = new User();
            user.setName("陳七");
            user.setPwd("999999");
            user.setParentId(3);
            userMapper.insertUser(user);
            roleService.insertRole();
            int i = 10 / 0;
        }
    
    	******************** UserService *************************
    	
    	******************** RoleService *************************
    	@Override
    	@Transactional(propagation = SUPPORTS)	 // 開啓事務,傳播屬性爲 SUPPORTS
        public void insertRole() {
            roleMapper.insertRole();	// 如果有一條數據,則事務未生效
            int i = 10 / 0;
        }
        ******************** RoleService *************************
    
  5. 運行結果如下, User 表和Role表中都插入了數據。因爲inserUser 方法沒有事務,所以User表中插入數據應該的,但是role表中也插入了數據,說明insertRole 方法上面的事務未生效。因爲其調用者並沒有開始事務,而insertRole 方法在開始事務的時候定義了傳播屬性是PROPAGATION_SUPPORTS。上級沒有事務則不會自動生成事務。所以造成了如下結果 :
    . 在這裏插入圖片描述
    在這裏插入圖片描述

  6. 爲了更好的驗證,我們將 insertRole 方法上的事務傳播屬性改爲 PROPAGATION_MANDATORYPROPAGATION_MANDATORY 的描述支持當前事務,如果當前沒有事務,就拋出異常。即如果上級具有事務,則使用上級的事務,上級沒有事務,則拋出異常 。再次運行。可以看到因爲insertUser方法沒有開啓事務,所以拋出了異常。
    在這裏插入圖片描述

三、事務的五種隔離級別

1. 什麼是髒讀、不可重複讀、幻讀。

  • 髒讀:髒讀又稱無效數據讀出。一個事務讀取另外一個事務還沒有提交的數據叫髒讀。

    例如:事務T1修改了一行數據,但是還沒有提交,這時候事務T2讀取了被事務T1修改後的數據,之後事務T1因爲某種原因Rollback了,那麼事務T2讀取的數據就是髒的。

  • 不可重複讀:不可重複讀是指在同一個事務內,兩個相同的查詢返回了不同的結果。

    例如:事務T1讀取某一數據,事務T2讀取並修改了該數據,T1爲了對讀取值進行檢驗而再次讀取該數據,便得到了不同的結果。

    解決:使用行級鎖,鎖定該行,事務A多次讀取操作完成後才釋放該鎖,這個時候才允許其他事務更改剛纔的數據

  • 幻讀:在同一事務內,兩次相同的查詢返回的數據條目數量不同

    例如:事務T1讀取一次表中數據總量,事務T2修改了表中數據總量(插入或者刪除)。這是事務T1再次讀取表中數據總量,發現和第一次讀取的總量不同,好像產生了幻覺。

    解決:使用表級鎖,鎖定整張表,事務A多次讀取數據總量之後才釋放該鎖,這個時候才允許其他事務新增數據。

注意:
1. 髒讀、不可重複讀、幻讀的級別高低是:髒讀 < 不可重複讀 < 幻讀
2. 不可重複讀針對的是多次讀取內容不同,幻讀針對的是多次讀取,內容條數不同

2. 事務的隔離級別

這裏說的五種隔離級別,是指在Spring中,而在數據庫中,並不包含Sping默認這一級別

隔離級別 解釋
Spring默認(DEFAULT) 這是一個PlatfromTransactionManager默認的隔離級別,使用數據庫默認的事務隔離級別.另外四個與JDBC的隔離級別相對應;
讀未提交(READ_UNCOMMITTED) 這是事務最低的隔離級別,它允許事務可以看到這個事務未提交的數據。這種隔離級別會產生髒讀,不可重複讀和幻讀。
讀已提交 (READ_ COMITTED) 保證一個事務修改的數據提交後才能被另外一個事務讀取。 另外一個事務不能讀取該事務未提交的數據。這種事務隔離級別可以避免髒讀出現,但是可能會出現不可重複讀和幻讀。
可重複讀(REPEATABLE READ) 這種事務隔離級別可以防止髒讀,不可重複讀。但是可能出現幻讀。它除了保證-一個事務不能讀取另一個事務未提交的數據外,還保證了避免下面的情況產生(不可重複讀) (MySql 默認就是這個級別)
可串行化 (SERIALIZABLE) 這是花費最高代價但是最可靠的事務隔離級別。事務被處理爲順序執行。除了防止髒讀,不可重複讀外,還避免了幻像讀。但是不建議使用,他將事務完全按照串行處理。

以上:內容部分參考網絡
https://www.cnblogs.com/daofaziran/p/10933302.html
https://blog.csdn.net/star1210644725/article/details/96829608
https://mp.weixin.qq.com/s/uMBPfM7zx0Rp_brP4cnKaA
https://www.cnblogs.com/luchangyou/p/11321607.html
https://www.cnblogs.com/moershiwei/p/9766916.html
https://www.jianshu.com/p/563612576e6e
如有侵擾,聯繫刪除。 內容僅用於自我記錄學習使用。如有錯誤,歡迎指正

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