事務是邏輯上的一組操做,要麼都執行,要麼都不執行,一榮俱榮,一損俱損。數據庫事務有嚴格的定義,必須知足4個特性。java
原子性:事務是最小的執行單位,不容許分割。事務的原子性確保動做要麼所有完成,要麼徹底不起做用;
一致性:執行事務先後,數據保持一致,多個事務對同一個數據讀取的結果是相同的,事務操做成功先後,數據庫所處的狀態和它的業務規則是一致的。在這些事務特性中,數據一致性是最終目標,其它特性都是爲達到這個目標採起的措施、要求或者手段。mysql
ACID中的一致性和CAP中的一致性有什麼區別?面試
兩者徹底不是一個事情,數據庫對於 ACID 中的一致性的定義是這樣的:若是一個事務原子地在一個一致地數據庫中獨立運行,那麼在它執行以後,數據庫的狀態必定是一致的。對於這個概念,它的第一層意思就是對於數據完整性的約束,包括主鍵約束、引用約束以及一些約束檢查等等,在事務的執行的先後以及過程當中不會違背對數據完整性的約束,全部對數據庫寫入的操做都應該是合法的,並不能產生不合法的數據狀態。而第二層意思實際上是指邏輯上的對於開發者的要求,咱們要在代碼中寫出正確的事務邏輯,好比銀行轉帳,事務中的邏輯不可能只扣錢或者只加錢,這是應用層面上對於數據庫一致性的要求。即,數據庫 ACID 中的一致性對事務的要求不止包含對數據完整性以及合法性的檢查,還包含應用層面邏輯的正確。算法
CAP 定理中的數據一致性,實際上是說分佈式系統中的各個節點中對於同一數據的拷貝有着相同的值。spring
隔離性:併發訪問數據庫時,一個用戶的事務不被其餘事務所幹擾,各併發事務之間數據庫是獨立的,採用數據庫鎖機制來保證事務的隔離性;
持久性:一個事務被提交以後。它對數據庫中數據的改變是持久的,即便數據庫發生故障也不該該對其有任何影響。sql
在典型的應用程序中,多個事務併發運行,常常會操做相同的數據來完成各自的任務(多個用戶對統一數據進行操做)。併發雖然是必須的,但可能會致使如下的問題:
髒讀(Dirtyread):當一個事務正在訪問數據而且對數據進行了修改,而這種修改尚未提交到數據庫中,這時另一個事務也訪問了這個數據,而後使用了這個數據。由於這個數據是尚未提交的數據,那麼另一個事務讀到的這個數據是「髒數據」,依據「髒數據」所作的操做多是不正確的。
丟失修改(Losttomodify):指在一個事務讀取一個數據時,另一個事務也訪問了該數據,那麼在第一個事務中修改了這個數據後,第二個事務也修改了這個數據。這樣第一個事務內的修改結果就被丟失,所以稱爲丟失修改。例如:事務1讀取某表中的數據A=20,事務2也讀取A=20,事務1修改A=A-1,事務2也修改A=A-1,最終結果A=19,事務1的修改被丟失。
不可重複讀(Unrepeatableread):指在一個事務內屢次讀同一數據。在這個事務尚未結束時,另外一個事務也訪問該數據。那麼,在第一個事務中的兩次讀數據之間,因爲第二個事務的修改致使第一個事務兩次讀取的數據可能不太同樣。這就發生了在一個事務內兩次讀到的數據是不同的狀況,所以稱爲不可重複讀。
幻讀(Phantomread):幻讀與不可重複讀相似。它發生在一個事務(T1)讀取了幾行數據,接着另外一個併發事務(T2)插入了一些數據時。在隨後的查詢中,第一個事務(T1)就會發現多了一些本來不存在的記錄,就好像發生了幻覺同樣,因此稱爲幻讀。數據庫
不可重複度和幻讀區別:不可重複讀的重點是修改,幻讀的重點在於新增或者刪除。例1(一樣的條件,你讀取過的數據,再次讀取出來發現值不同了):事務1中的A先生讀取本身的工資爲1000的操做還沒完成,事務2中的B先生就修改了A的工資爲2000,致使A再讀本身的工資時工資變爲2000;這就是不可重複讀。
例2(一樣的條件,第1次和第2次讀出來的記錄數不同):假某工資單表中工資大於3000的有4人,事務1讀取了全部工資大於3000的人,共查到4條記錄,這時事務2又插入了一條工資大於3000的記錄,事務1再次讀取時查到的記錄就變爲了5條,這樣就致使了幻讀。編程
SQL標準定義了四個隔離級別,隔離性和一致性實際上是一個須要開發者去權衡的問題,爲數據庫提供什麼樣的隔離性層級也就決定了數據庫的性能以及能夠達到什麼樣的一致性。
READ-UNCOMMITTED(讀取未提交):最低的隔離級別,容許讀取還沒有提交的數據變動,可能會致使髒讀、幻讀或不可重複讀。
READ-COMMITTED(讀取已提交):容許讀取併發事務已經提交的數據,能夠阻止髒讀,可是幻讀或不可重複讀仍有可能發生。
REPEATABLE-READ(可重複讀):對同一字段的屢次讀取結果都是一致的,除非數據是被自己事務本身所修改,能夠阻止髒讀和不可重複讀,但幻讀仍有可能發生。
SERIALIZABLE(可串行化):最高的隔離級別,徹底服從ACID的隔離級別。全部的事務依次逐個執行,這樣事務之間就徹底不可能產生干擾,也就是說,該級別能夠防止髒讀、不可重複讀以及幻讀。安全
隔離級別 | 髒讀 | 不可重複讀 | 幻影讀 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
表-數據併發問題在不一樣隔離級別的出現多線程
這裏須要注意的是:與SQL標準不一樣的地方在於InnoDB存儲引擎在REPEATABLE-READ(可重讀)事務隔離級別下使用的是Next-KeyLock鎖算法,所以能夠避免幻讀的產生,這與其餘數據庫系統(如SQLServer)是不一樣的。因此說InnoDB存儲引擎的默認支持的隔離級別是REPEATABLE-READ(可重讀)已經能夠徹底保證事務的隔離性要求,即達到了SQL標準的SERIALIZABLE(可串行化)隔離級別。由於隔離級別越低,事務請求的鎖越少,因此大部分數據庫系統的隔離級別都是READ-COMMITTED(讀取提交內容):,可是你要知道的是InnoDB存儲引擎默認使用REPEATABLE-READ(可重讀)並不會有任何性能損失。
InnoDB存儲引擎在分佈式事務的狀況下通常會用到SERIALIZABLE(可串行化)隔離級別。
事務的原子性和持久性是由事務日誌(transaction log)保證的,回滾日誌用於對事務的影響進行撤銷,重作日誌在錯誤處理時對已經提交的事務進行重作。它們能保證兩點:
UndoLog的原理很簡單,爲了知足事務的原子性,在操做任何數據以前,首先將數據備份到一個地方(這個存儲數據備份的地方稱爲UndoLog)。而後進行數據的修改。若是出現了錯誤或者用戶執行了ROLLBACK語句,系統能夠利用Undo Log中的備份將數據恢復到事務開始以前的狀態,UndoLog並不能將數據庫物理地恢復到執行語句或者事務以前的樣子;它是邏輯日誌,當回滾日誌被使用時,它只會按照日誌邏輯地將數據庫中的修改撤銷掉看,能夠理解爲咱們在事務中使用的每一條INSERT都對應了一條DELETE,每一條UPDATE也都對應一條相反的UPDATE語句。 和UndoLog相反,RedoLog記錄的是新數據的備份。在事務提交前,只要將RedoLog持久化便可,不須要將數據持久化。當系統崩潰時,雖然數據沒有持久化,可是RedoLog已經持久化。系統能夠根據RedoLog的內容,將全部數據恢復到最新的狀態。 (深刻分析能夠參考面向信仰編程-淺入深出MySQL中事務的實現-回滾日誌、重作日誌)
數據庫對於隔離級別的實現就是使用併發控制機制對在同一時間執行的事務進行控制,限制不一樣的事務對於同一資源的訪問和更新,而最重要也最多見的併發控制機制,主要有鎖、時間戳(即樂觀鎖,並非真正的鎖機制,而是一種思想)、多版本MVCC。
Spring爲事務管理提供了一致的編程模板,在高層創建了統一的事務抽象。也就是說,無論是選擇Spring JDBC、Hibernate、JPA仍是選擇MyBatis,Spring均可以讓用戶使用統一的編程模型進行事務管理。
Spring爲事務管理提供了一致的編程模板,在高層次創建了統一的事務抽象。像Spring DAO爲不一樣的持久化技術實現提供模板類同樣,Spring事務管理繼承了這一風格,也提供了事務模板類TransactionTemplate。經過TransactionTemplate並配合使用事務回調TransactionCallback指定具體的持久化操做就能夠經過編程方式實現事務管理,而無須關注資源獲取、複用、釋放、事務同步和異常處理的操做。
在Spring事務管理SPI的抽象層主要包括3個接口,分別是PlatformTransactionManager、TransactionDefinition和TransactionStatus,它們位於org.springframework.transaction包中。3者關係如圖:
圖-Spring事務管理SPI抽象
其中,TransactionDefinition用於描述事務的隔離級別,超時時間、是否爲只讀事務和事務傳播規則等控制事務具體行爲的事務屬性。這些事務屬性能夠經過XML配置、註解描述或手工編程的方式設置。PlatformTransactionManager根據TransactionDefinition提供的事務屬性配置信息建立事務,並用TransactionStatus描述這個激活事務的狀態。
事務隔離:TransactionDefinition使用了java.sql.Connection接口中同名的4個隔離級別,此外,TransactionDefinition還定義了一個默認的隔離級別,它表示使用底層數據庫的默認隔離級別。
事務傳播:一般在一個事務中執行的全部代碼都會同一事務的上下文中。可是Spring也提供了幾個可選的事務傳播類型,例如簡單地參與到現有的事務中,或者掛起當前的事務,建立一個新事務。
事務超時:事務在超時前能運行多久,超過期間後,事務被回滾。有些事務管理器不支持事務過時的功能,這時若是設置TIMEOUT_DEFAULT等值時將拋出異常。
只讀狀態:只讀事務不修改任何數據,主要用於優化,若是更改數據就會拋出異常。
spring容許經過XML或者註解元數據的方式爲一個有事務要求的服務類方法配置事務屬性,這些信息做爲Spring事務管理框架的輸入,Spring將自動按照事務屬性信息的指示,爲目標方法提供相應的事務支持。
TransactionStatus表明一個事務的具體運行狀態,事務管理器經過該接口獲取事務的運行期狀態信息,也能夠經過該接口間接地回滾事務,它相比於在拋出異常時回滾事務的方式更具備可控性。
PlatformTransactionManager是事務的最高層抽象,它提供了3個接口方法:
TransactionStatus getTransaction(TransactionDefinition definition):該方法根據事務定義信息從事務環境中返回一個已存在的事務,或者建立一個新的事務,並用TransactionStatus描述這個事務的狀態。
commit(TransactionStatus status):根據事務的狀態提交事務,若是事務狀態已經被標識爲rollback-only,該方法將執行一個回滾事務的操做。
rollback(TransactionStatus status):回滾事務,當提交事務拋出異常時,回滾會被隱式執行。
Spring將事務管理委託給底層具體的持久化實現框架完成,所以Spring爲不一樣的持久化框架提供了PlatformTransactionManager接口的實現類,以下圖:
圖-不一樣持久化技術對應的事務管理器實現類
這些事務管理器都是對特定事務實現框架的代理,這樣咱們就能夠經過spring的高級抽象,對不一樣種類的事務實現使用相同的方式進行管理,而不用關心具體的實現。要實現事務管理,首先要在Spring中配置好相應的事務管理器,爲事務管理器指定數據資源及一些其它事務管理控制屬性。
Spring將JDBC的Connection、Hibernate的Session等訪問數據庫的鏈接或會話對象統稱爲資源。這些資源在同一時刻是不能多線程共享的,爲了讓DAO、Service能作到singleton,Spring的事務同步管理器類org.springframework.transaction.support.TransactionSynchronizationManager使用ThreadLocal爲不一樣事務線程提供了獨立的資源副本,同時維護事務配置的屬性和運行狀態信息。
在一個service接口中可能會調用另外一個service接口的方法,以共同完成一個完整的業務操做,Spring經過事務傳播行爲控制當前的事務如何傳播到被嵌套調用的目標服務接口方法中。Spring在TransactionDefinition接口中規定了7種類型的事務傳播行爲,以下圖
圖-事務傳播行爲類型
Spring爲編程式事務管理提供了模板類org.springframework.transaction.support.TransactionTemplate,和那些持久化模板類同樣,TransactionTemplate也是線程安全的。TransactionTemplate有2個重要的方法:
// 設置事務管理器 void setTransactionManager(PlatformTransactionManager transactionManager) // 在TransactionCallback回調接口中定義須要以事務的方式組織的數據訪問邏輯 Object execute(TransactionCallback action) TransactionCallback接口只有一個方法:Object doInTransaction(TransactionStatus status)。若是操做不會返回結果,可使用TransactionCallback的子接口TransactionCallbackWithoutResult。
Spring Boot中使用@Transactional註解配置事務管理
是否用了Spring,就必定要用Spring事務管理器,不然就沒法進行數據的持久化操做呢?答案是否認的,脫離了事務性,DAO照樣能夠順利地進行數據操做。對於強調速度的應用,數據庫自己可能就不支持事務,如MyISAM引擎的數據庫,這是無需配置事務管理器,即便配置了,也是沒有實際用處的。
將面向接口編程奉爲圭臬,過度強制面向接口編程除了會帶來更多的的類文件,並不會有什麼好處。Spring事務管理支持在Controller層直接添加事務註解並生效,所以事務管理並不必定強制應用必須嚴格分層,能夠根據實際應用出發,根據實際須要進行編程。
除了事務的傳播行爲,對於事務的其它特性,Spring是藉助底層資源的功能來完成的,無非充當了一個代理的角色。可是Spring事務的傳播行爲倒是Spring憑藉自身的框架提供的功能。
例如對於調用鏈Service1#method1()->Service2#method2()->Service3#method3(),那麼這三個方法經過Spring的事務傳播機制均可以工做在同一個事務中。
首先調用的是AOP代理對象而不是目標對象,首先執行事務切面,事務切面內部經過TransactionInterceptor環繞加強進行事務的加強,即進入目標方法以前開啓事務,退出目標方法時提交/回滾事務。目標對象內部的自我調用將沒法實施切面中的加強。
public interface AService { public void a(); public void b(); } @Service() public class AServiceImpl1 implements AService{ @Transactional(propagation = Propagation.REQUIRED) public void a() { // 此處的this指向目標對象,所以調用this.b()將不會執行b事務切面,即不會執行事務加強(經過日誌能夠觀察到) this.b(); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void b() { } }
如何解決?
方法一:調用AOP代理對象的b方法便可執行事務切面進行事務加強
須要開啓暴露Aop代理到ThreadLocal支持,並將this.b()修改成((AService) AopContext.currentProxy()).b();
這種經過ThreadLocal暴露Aop代理對象適合解決全部場景(無論是singleton Bean仍是prototype Bean)的AOP代理獲取問題(即能解決目標對象的自我調用問題);
方法二:經過初始化方法在目標對象中注入代理對象
這種方式不是很靈活,全部須要自我調用的實現類必須重複實現代碼
@Service public class AServiceImpl3 implements AService{ @Autowired //① 注入上下文 private ApplicationContext context; private AService proxySelf; //② 表示代理對象,不是目標對象 @PostConstruct //③ 初始化方法 private void setSelf() { //從上下文獲取代理對象(若是經過proxtSelf=this是不對的,this是目標對象) //此種方法不適合於prototype Bean,由於每次getBean返回一個新的Bean proxySelf = context.getBean(AService.class); } @Transactional(propagation = Propagation.REQUIRED) public void a() { proxySelf.b(); //④ 調用代理對象的方法 這樣能夠執行事務切面 } @Transactional(propagation = Propagation.REQUIRES_NEW) public void b() { } }
方法三:經過BeanPostProcessor在目標對象中注入代理對象
須要注意到循環依賴和非singleton bean的影響,如下方式能解決singleton之間的循環依賴問題,可是不能解決循環依賴中包含prototype Bean的自我調用問題。
// 即咱們自定義的BeanPostProcessor (InjectBeanSelfProcessor) // 若是發現咱們的Bean是實現了該標識接口就調用setSelf注入代理對象。 public interface BeanSelfAware { void setSelf(Object proxyBean); } @Component public class InjectBeanSelfProcessor implements BeanPostProcessor, ApplicationContextAware { private ApplicationContext context; //① 注入ApplicationContext public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; } public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if(!(bean instanceof BeanSelfAware)) { //② 若是Bean沒有實現BeanSelfAware標識接口 跳過 return bean; } if(AopUtils.isAopProxy(bean)) { //③ 若是當前對象是AOP代理對象,直接注入 ((BeanSelfAware) bean).setSelf(bean); } else { //④ 若是當前對象不是AOP代理,則經過context.getBean(beanName)獲取代理對象並注入 //此種方式不適合解決prototype Bean的代理對象注入 ((BeanSelfAware)bean).setSelf(context.getBean(beanName)); } return bean; } public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } }
擴展:靈活使用事務註解並不是必定須要接口
若是在一個業務方法中包含查詢、修改等邏輯,但其實只有這些修改操做須要實施業務加強,這是應該怎麼處理?可讓改service實現BeanSelfAware接口,而後在service注入代理對象,經過代理對象
@Service public class AServiceImpl1 implements AService,BeanSelfAware{ private AServiceImpl1 self; @Override public void setProxy(AServiceImpl1 proxy) { this.self = proxy; } @Override public void a() { // ... 其它查詢等無需事務邏輯 // 須要事務加強的修改邏輯 self.m(); } @Transactional public void m() { } }
在相同線程中進行相互的嵌套調用的事務方法工做在相同的事務中,若是這些相互嵌套調用的方法工做在不一樣線程中,則不一樣的線程下的事務方法工做在獨立的事務中。
若是用戶採用了一種高端的ORM技術(Hibernate、JPA、JDO),同時還採用了一種JDBC技術(Spring JDBC、Mybatis),因爲前者的會話Session是對後者鏈接Connection的封裝,Spring會足夠智能地在同一個事務線程中讓前者的會話封裝後者的鏈接,因此只要只要直接採用前者前者的事務管理器就能夠了。
圖-混合數據訪問技術所對應的事務管理器
因爲Spring事務管理是基於接口代理或者動態字節碼技術,經過AOP實施事務加強的,雖然Spring依然支持AspectJ在類的加載期間實施加強,但這種方法不多使用,這裏不作討論。
對於基於接口動態代理的AOP事務加強來講,因爲接口的方法都必須是public的,這就要求實現類的實現方法也必須是public的(不能是protected、private)的,同時不能使用static修飾符。因此,能夠實施接口動態代理的方法只能是public或public final修飾符的方法,其它方法都不能被動態代理,相應地也就不能實施AOP加強,換句話說即不能進行Spring事務的加強。
基於CGLib字節碼動態代理的方案是經過擴展被加強類,動態建立起子類的方式進行AOP加強植入的,因爲使用final、static、private修飾符的方法都不能被子類覆蓋,相應地這些方法沒法實施AOP加強。因此方法簽名必須特別注意這些修飾符的使用,以避免成爲事務管理的漏網之魚。
須要注意的是,咱們說這些方法不能被Spring進行AOP事務加強,是指這些方法不能啓動事務,可是外層方法的事務上下文依舊能夠順利地傳播到這些方法中。這些不能被事務加強的方法和可被事務加強的方法的惟一區別在於「是否能夠主動開啓一個新事務」,前者能夠然後者不能夠,對於事務傳播行爲來講,兩者是徹底相同的。
圖-Spring事務管理漏網之魚
參考資料