Springboot資料庫事務處理——Spring宣告式事務

謎一樣的Coder發表於2018-11-18

前言

Spring事務部分之前一直理解的模稜兩可,在實際開發中這些都由架構師完成,底層程式設計師更多的只是編寫業務,並沒有實際接觸這個事務的配置,在資料庫中事務的配置卻是非常重要的一環。在springboot學習過程中,正好遇到了spring事務管理這個章節,就借這個機會總結一下事務的處理部分。

在spring資料庫事務中,可以使用程式設計式事務和宣告式事務,但是程式設計式事務需要手動編寫事務的提交和回滾,這樣相關程式碼就會與業務程式碼耦合在一起,這種方式目前也已經看不到使用,所以這裡主要總結宣告式事務的編寫方式。

本篇部落格重點會梳理隔離級別和傳播行為的概念,並附上相關例項。

spring宣告式事務的使用

首先,需要明確的是宣告式事務底層是通過AOP的方式實現的,往深處說應該是採用了動態代理模式。

宣告式事務字面意思就是告訴spring在什麼地方啟用事務,spring就會通過AOP自動進行資料庫的事務操作。@Transactional既是spring中事務申明的標籤,這個標籤可以放在類上,也可以放在方法上;放在類上,表示這個類中所有的公共非靜態方法都將採用事務機制;放在方法上,表示這個方法將採用事務處理機制。

其實spring的事務使用,只是給@Transactional這個註解配置屬性而已......這個已經easy爆了

spring事務管理的核心內容是隔離級別和傳播行為,隔離級別本身是資料庫的概念,不同層度的隔離級別可以從不同層度上壓制資料丟失更新的問題。傳播行為則是規定了方法之間呼叫事務採取的策略問題。這兩個概念後面會詳細討論。

spring事務管理器

事務的開啟,回滾和提交都是交由spring的事務管理器來完成,spring中事務管理器的頂層介面PlatformTransactionManager,提供了一個事務管理器的實現標準,其中有一個方法——getTransaction(TransactionDefinition definition),其中的TransactionDefinition這個變數依賴於我們配置的@Transactional的配置項生成。

隔離級別

資料庫事務

這個概念其實已經爛熟了,提到資料庫事務就會想到它的四個屬性——A(Atomic 原子性)、C(Consistency 一致性)、I(Isolation 隔離性)、D(Durability 永續性)。

除了隔離性,其他三個屬性非常好理解,A——原子性:事務要麼全部成功,要麼全部不成功。C——一致性:資料庫事務操作前後資料必須不能出錯。D——永續性,事務結束後,資料必須固化到一個地方(磁碟等儲存介質)。

唯獨這個隔離性,是在為了解決資料更新丟失的問題而提出的。所以這裡先介紹資料更新丟失的兩個場景

資料丟失更新

多個事務(或者執行緒,其實感覺多執行緒情況下也會有這類問題,思想都差不多)在同時訪問或修改相關資料的時候會產生兩類資料更新丟失的情況

第一類丟失更新

上述的圖片其實就是第一類更新丟失的問題,在T5時刻,事務1回滾了庫存資料,而事務2的操作已經賣了一件商品出去,庫存總數依然是100,這個資料明顯是有問題的。 這種由於一個事務回滾,另一個事務提交而引發的資料不一致的情況,就是第一類更新丟失。現在的資料庫系統基本都已經通過版本控制解決了第一類丟失更新問題。

第二類丟失更新

 上述資料表示的是第二類資料更新丟失,在T5時刻,事務1提交資料,庫存總數為99。但是事務2也買了一件商品,事務1也成功賣了一件商品,庫存總數應該是98,而不是99,資料依舊不正確。這種由於多個事務都提交資料引發的丟失更新問題稱為第二類丟失更新。為了克服這個問題,於是就有了事務隔離級別。

隔離級別詳解

隔離級別有四種,這個也是爛熟的概念。1、讀未提交,2、讀寫提交,3、可重複讀,4、序列化

讀未提交

這個是最低的隔離級別,從字面不難理解,其實就是允許一個事務讀取另外一個事務沒有提交的資料。

問題同樣出現在T5時刻, 事務2將庫存扣減,之後提交事務這個過程沒有問題。問題在於事務2讀取的是事務1未提交的資料,即讀取的庫存數為1。事務1在回滾的時候,這種其實就是第一類丟失更新問題,但是由於採用CAS演算法解決了該問題,事務1發現自己的資料並不是最新的,因此會採用事務2提交的資料,最終庫存數為0,這明顯就不正確,只買了一件商品,庫存卻少了2。

這個就是髒讀現象,讀取了一個事務未提交的資料,這個資料並不一定會持久化到資料庫中,稱為髒資料。可見這個隔離級別對資料一致性影響非常大,因此實際中真正用到事務的地方並不會用這種隔離級別。

讀寫提交

在讀未提交的基礎上,更進一步,一個事務只能讀取另外一個事務已經提交的資料,不能讀取未提交的資料,避免了髒讀。

T4時刻,在事務2提交事務的時候,發現本身自己讀取不到事務1提交的資料,因此不會將庫存扣減,而是儲存讀取到的資料。這個結果是正確的。但是......如果事務2扣減庫存發生在T5之後,就會產生以下問題:

在T5時刻,事務2提交資料的時候會出錯,畢竟資料庫如果有限制的話,商品庫存這個欄位的資料是不能小於0的。這個的根本原因就是事務2沒有再次讀取到事務1更新的資料,為了解決這個問題,資料庫的隔離級別還提出了可重複讀的隔離級別。

可重複讀

事務2在T5時刻更新資料庫之前重新讀取了事務1提交的資料,因此這裡直接會返回庫存為0的資料。 

但是這樣依舊會產生新的問題——幻讀

針對多條資料的時候,會出現查詢的記錄與最終得到的記錄不一致,上面的例項非常清楚,這個就不詳細解釋了。

序列化

序列化是資料隔離的終極級別,讓所有的事務都排隊處理,這樣並不會產生資料問題,但是效能就非常低了。

合理使用隔離級別

上面這張表估計也是見過多次了,這次應該理解更加深刻了,在一定場景下選用不同級別的隔離模式也是一個綜合考量,需要綜合效能和資料需求考慮。

在實際中,選擇隔離級別會以讀寫提交為主,能夠防止髒讀,但是不能避免不可重複讀和幻讀的問題,有時候為了解決資料庫資料不一致和效能問題,會用到Redis等NoSQL資料庫。同時對於隔離級別,不同的資料庫的支援也是不同的。例如Oracle只支援讀寫提交和序列化,而MySQL全部支援,但是MySQL預設是可重複讀,Oracle預設是讀寫提交。

傳播行為

傳播行為理解起來相對簡單。

回到開頭的前言部分,說過,傳播行為是方法之間呼叫事務採取的策略問題。在一個批量任務執行的過程中,呼叫多個交易時,如果有一些交易發生異常,我們是回滾這些發生異常的交易,還是全部批量回滾所有任務。這個就是傳播行為來定義的。當然,更多的時候,我們只希望回滾出現異常的交易。

這個圖只是一個簡易的例項圖,用大白話來解釋就是,如果批量任務函式配置了事務,批量任務在呼叫 單個交易的時候,單個交易也配置了自己的事務,這兩者的關係該如何處理,單個交易是用自己的事務還是用批量任務函式的事務?

上述表中的呼叫者就是對應前面例項中的批量任務,事務型別是配置在單個交易的邏輯程式碼上。

總結

寫到這裡,這篇文章算是結束了,本來想一併總結springboot中整合資料庫訪問層的操作,但是發現其實這個比較簡單,網上一些大牛部落格總結的已經很到位了,一些簡單的例項一看就能明白,這裡就不再這篇部落格中總結,會單獨在springboot的資料訪問部落格中去總結。

參考資料:

《深入淺出Spring Boot 2.X》 一本比較通俗的書,例項豐富,本文中的大部分圖例都來自這本書,文字部分也只是在這本書中提取出主幹內容。

例項程式碼(還不是很完善)地址:https://github.com/liman657/springbootlearn

相關文章