有段時間沒更新博客了,最近在學習一些有關事務的知識點,今天來總結一下,本文會涉及到如下幾個知識點:mysql
MySQL事務
Spring事務
分佈式事務
事務是一系列操做組成的工做單元,該工做單元內的操做是不可分割的,即要麼全部操做都作,要麼全部操做都不作,這就是事務。
舉個例子:算法
張三要給李四轉帳100元,那麼咱們會有這樣的一段SQL:spring
begin transaction; update account set money = money-100 where name = '張三'; update account set money = money+100 where name = '李四'; commit transaction;
事務的體現:這兩個SQL要麼所有成功,要麼所有失敗。sql
事務可否生效數據庫引擎是否支持事務是關鍵。好比經常使用的 MySQL 數據庫默認使用支持事務的innodb
引擎。可是,若是把數據庫引擎變爲 myisam
,那麼程序也就再也不支持事務了!數據庫
事務具備如下四個特性:編程
原子性
一致性
隔離性
持久性
通常來講,原子是指不能分解成小部分的東西。例如,在多線程編程中,若是一個線程執行一個原子操做,這意味着另外一個線程沒法看到該操做的一半結果。系統只能處於操做以前或操做以後的狀態,而不是介於二者之間的狀態。服務器
事務一致性是指數據庫中的數據在事務操做先後都必須知足業務規則約束。
好比A轉帳給B,那麼轉帳先後,AB的帳戶總金額應該是一致的。網絡
一個事務的執行不能被其它事務干擾。即一個事務內部的操做及使用的數據對其它併發事務是隔離的,併發執行的各個事務之間不能互相干擾。
(設置不一樣的隔離級別,互相干擾的程度會不一樣)多線程
事務一旦提交,結果即是永久性的。即便發生宕機,仍然能夠依靠事務日誌完成數據的持久化。併發
日誌包括回滾日誌(undo)和重作日誌(redo),當咱們經過事務修改數據時,首先會將數據庫變化的信息記錄到重作日誌中,而後再對數據庫中的數據進行修改。這樣即便數據庫系統發生奔潰,咱們還能夠經過重作日誌進行數據恢復。
MySQL有如下四個事務隔離級別:
未提交讀(READ UNCOMMITTED)
已提交讀(READ COMMITTED)
可重複讀(REPEATABLE READ)
串行化(SERIALIZABLE)
各個隔離級別可能會存在如下的問題:
那麼什麼是髒讀、不可重複讀和幻讀?
髒讀:指一個事務能夠看到另外一個事務未提交的數據
好比說事務A修改了一個值可是還未提交,這時事務B能夠看到A修改的值,這就是髒讀。
不可重複讀:一個事務執行兩次一樣的查詢語句,先後得出的數據卻不一致
好比說事務A執行了select
語句,事務B修改了某個值,事務A再次執行select
語句時發現結果和上次不一致,所以叫作不可重複讀。
幻讀:在同一個事務中,同一個查詢屢次返回的記錄行數不一致(這裏的結果特指查詢到的記錄行數,幻讀能夠看作不可重複讀的一種特殊狀況)
好比說事務A執行了select
語句,事務B插入數據,事務A再次執行select
語句時發現多了幾條記錄,好像出現了幻覺同樣,所以叫作幻讀。
先說結論:經過改變鎖的釋放時機來解決髒讀問題。
首先先了解一下爲何會出現髒讀?緣由就是在未提交讀
這個級別下,當事務A修改了數據以後就立馬釋放了鎖,所以事務B能夠讀取到這個未提交的數據。
在已提交讀
級別下寫操做加的鎖會到事務提交後釋放,因此事務B不會讀到事務A未提交的數據,經過改變鎖的釋放時機解決了髒讀的問題。
結論:可重複讀
級別就是經過MVCC
機制來解決不可重複讀問題的
多版本併發控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存儲引擎實現隔離級別的一種具體方式,用於實現提交讀和可重複讀這兩種隔離級別。而未提交讀隔離級別老是讀取最新的數據行,無需使用 MVCC。可串行化隔離級別須要對全部讀取的行都加鎖,單純使用 MVCC 沒法實現。
MVCC機制(多版本併發控制)
就我我的理解來講其實就是給每行數據都添加了幾個隱藏字段,用來表示數據的版本號,即一個數據在mysql中會有多個不一樣的版本。
在講 MVCC 的實現原理以前,我覺頗有必要先去了解一下 MVCC 的兩種讀形式。
有了MVCC以後咱們能夠把SQL操做分爲兩類:
讀取當前事務可見的數據,默認的select
操做就是快照讀,讀的是歷史版本的數據。
讀取最新的數據,除了默認select操做外的select..for update
、update
、insert
、delete
等操做都是當前讀,讀取的都是最新的數據。
如今咱們有了MVCC,當事務A執行一個普通的select操做(快照讀)
,MySQL會把此次讀取的數據保存起來,在這期間無論事務B執行update或是insert操做,事務A再次執行select操做讀取到的數據是不會變的,所以經過可重複讀級別經過MVCC解決了不可重複讀問題,順便解決了部分的幻讀問題,沒錯MVCC並無解決全部的幻讀問題,只是解決了一部分。
當事務A執行的是當前讀,也就是加鎖的select操做時如select * from Employee for update
,會去讀取最新的數據,這樣的話仍是能夠看到事務B提交的數據,所以MySQL提供了Next-Key Lock
算法來幫助咱們對數據加鎖。
InnoDB有三種行鎖的算法:
3. Next-Key Lock:1+2,鎖定一個範圍,而且鎖定記錄自己。對於行的查詢,都是採用該方法,主要目的是解決幻讀的問題。
Next-Key Lock 是 MySQL 的 InnoDB 存儲引擎的一種鎖實現。
MVCC 不能解決幻讀的問題,Next-Key Lock 就是爲了解決這個問題而存在的。在可重複讀(REPEATABLE READ)隔離級別下,使用 MVCC + Next-Key Lock
能夠解決幻讀問題。
當查詢的索引含有惟一屬性的時候,Next-Key Lock
會進行優化,將其降級爲Record Lock
,即僅鎖住索引自己,不是範圍。
它是 Record Lock
和 Gap Lock
的結合,不只鎖定一個記錄上的索引,也鎖定索引之間的間隙。
這樣的話當事務A執行了select * from Employee for update
以後,事務B插入數據會被阻塞,這樣的話Repeatable Read(可重複讀)
級別使用 MVCC + Next-Key Lock
能夠解決了不可重複讀和幻讀的問題。
注意:Repeatable Read(可重複讀)是MySQL的默認隔離級別。
這是最高的隔離級別,它經過強制事務排序,使之不可能相互衝突,從而解決幻讀問題。簡言之,它是在每一個讀的數據行上加上共享鎖。在這個級別,可能致使大量的超時現象和鎖競爭。
在項目開發中咱們有時候會遇到Spring事務失效的場景,那麼什麼場景會致使事務失效呢?
這種失效是因爲配置錯誤,如果錯誤的配置如下三種 propagation,事務將不會發生回滾。
TransactionDefinition.PROPAGATION_SUPPORTS:若是當前存在事務,則加入該事務;若是當前沒有事務,則以非事務的方式繼續運行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式運行,若是當前存在事務,則把當前事務掛起。
TransactionDefinition.PROPAGATION_NEVER:以非事務方式運行,若是當前存在事務,則拋出異常。
如下來自 Spring 官方文檔:
When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.
大概意思就是 @Transactional
只能用於 public 的方法上,不然事務不會失效,若是要用在非 public 方法上,能夠開啓 AspectJ
代理模式。
rollbackFor 能夠指定可以觸發事務回滾的異常類型。Spring默認拋出了未檢查unchecked異常(繼承自 RuntimeException 的異常)或者 Error纔回滾事務;其餘異常不會觸發回滾事務。若是在事務中拋出其餘類型的異常,但卻指望 Spring 可以回滾事務,就須要指定 rollbackFor屬性。
// 但願自定義的異常能夠進行回滾 @Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
來看兩個示例:
@Service public class OrderServiceImpl implements OrderService { public void update(Order order) { updateOrder(order); } @Transactional public void updateOrder(Order order) { // update order; } }
update方法上面沒有加 @Transactional
註解,調用有 @Transactional
註解的 updateOrder 方法,updateOrder 方法上的事務管用嗎?
@Service public class OrderServiceImpl implements OrderService { @Transactional public void update(Order order) { updateOrder(order); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateOrder(Order order) { // update order; } }
此次在 update 方法上加了 @Transactional
,updateOrder 加了 REQUIRES_NEW
新開啓一個事務,那麼新開的事務管用麼?
這兩個例子的答案是:無論用!
由於@Transactional
註解底層實際上是Spring幫咱們生成了一個代理對象,當其它對象調用帶有@Transactional
的方法時,其實調的是代理對象,Spring會在代理對象中幫咱們加上一系列的事務操做。
在上面的例子中它們發生了自身調用,就調用該類本身的方法,而沒有通過 Spring 的代理類,默認只有在外部調用事務纔會生效,這也是老生常談的經典問題了。
這種狀況是最多見的一種@Transactional註解失效場景
@Autowired private B b; @Service public class OrderServiceImpl implements OrderService { @Transactional public void A(Order order) { try { b.insert(); }catch (Exception e){ //do something; } } }
若是b.insert()方法內部拋了異常,而A方法此時try catch了B方法的異常,那這個事務還能正常回滾嗎?
答案:不能!而是會拋出下面異常:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
由於當ServiceB中拋出了一個異常之後,ServiceB標識當前事務須要rollback。可是ServiceA中因爲你手動的捕獲這個異常並進行處理,ServiceA認爲當前事務應該正常commit。此時就出現了先後不一致,也就是由於這樣,拋出了前面的UnexpectedRollbackException異常。
spring的事務是在調用業務方法以前開始的,業務方法執行完畢以後才執行commit or rollback,事務是否執行取決因而否拋出Runtime異常。若是拋出runtime exception 並在你的業務方法中沒有catch到的話,事務會回滾。
在業務方法中通常不須要catch異常,若是非要catch必定要拋出throw new RuntimeException()
,或者註解中指定拋異常類型@Transactional(rollbackFor=Exception.class)
,不然會致使事務失效,數據commit形成數據不一致,因此有些時候try catch反倒會多此一舉。
首先看下什麼是分佈式事務:
分佈式事務就是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位於不一樣的分佈式系統的不一樣節點之上。簡單的說,就是一次大的操做由不一樣的小操做組成,這些小的操做分佈在不一樣的服務器上,且屬於不一樣的應用,分佈式事務須要保證這些小操做要麼所有成功,要麼所有失敗。本質上來講,分佈式事務就是爲了保證不一樣數據庫的數據一致性。
那麼爲何須要分佈式事務?直接用Spring提供的@Transaction註解
不行嗎?
這裏極其重要的一點:單塊系統是運行在同一個 JVM 進程中的,可是分佈式系統中的各個系統運行在各自的 JVM 進程中。所以你直接加@Transactional
註解是不行的,由於它只能控制同一個 JVM 進程中的事務,可是對於這種跨多個 JVM 進程的事務無能無力。
咱們來解釋一下這個方案的大概流程:
適用場景: 這個方案的使用仍是比較廣,目前國內互聯網公司大都是基於這種思路玩兒的。
整個流程圖以下所示:
這個方案的大體流程:
這套方案和上面的可靠消息最終一致性方案的區別:
可靠消息最終一致性方案能夠保證的是隻要系統 A 的事務完成,經過不停(無限次)重試來保證系統 B 的事務總會完成。
可是最大努力方案就不一樣,若是系統 B 本地事務執行失敗了,那麼它會重試 N 次後就再也不重試,系統 B 的本地事務可能就不會完成了。
至於你想控制它究竟有「多努力」,這個須要結合本身的業務來配置。
好比對於電商系統,在下完訂單後發短信通知用戶下單成功的業務場景中,下單正常完成,可是到了發短信的這個環節因爲短信服務暫時有點問題,致使重試了 3 次仍是失敗。
那麼此時就再也不嘗試發送短信,由於在這個場景中咱們認爲 3 次就已經算是盡了「最大努力」了。
簡單總結:就是在指定的重試次數內,若是能執行成功那麼皆大歡喜,若是超過了最大重試次數就放棄,再也不進行重試。
適用場景: 通常用在不過重要的業務操做中,就是那種完成的話是錦上添花,但失敗的話對我也沒有什麼壞影響的場景。
好比上邊提到的電商中的部分通知短信,就比較適合使用這種最大努力通知方案來作分佈式事務的保證。
TCC 的全稱是:
這個實際上是用到了補償的概念,分爲了三個階段:
仍是給你們舉個例子:
好比跨銀行轉帳的時候,要涉及到兩個銀行的分佈式事務,若是用 TCC 方案來實現,思路是這樣的:
適用場景:這種方案說實話幾乎不多有人使用,可是也有使用的場景。
由於這個事務回滾其實是嚴重依賴於你本身寫代碼來回滾和補償了,會形成補償代碼巨大,很是之噁心。
好比說咱們,通常來講跟錢相關的,跟錢打交道的,支付、交易相關的場景,咱們會用 TCC,嚴格保證分佈式事務要麼所有成功,要麼所有自動回滾,嚴格保證資金的正確性,在資金上不容許出現問題。
比較適合的場景:除非你是真的一致性要求過高,是你係統中核心之核心的場景,好比常見的就是資金類的場景,那你能夠用 TCC 方案了。 你須要本身編寫大量的業務邏輯,本身判斷一個事務中的各個環節是否 ok,不 ok 就執行補償/回滾代碼。
並且最好是你的各個業務執行的時間都比較短。
可是說實話,通常儘可能別這麼搞,本身手寫回滾邏輯,或者是補償邏輯,實在太噁心了,那個業務代碼很難維護。
今天簡單對事務作了一個總結,有什麼不對的地方請多多指教!