事務,是操做數據庫中的數據的邏輯單元。在處理一個業務過程當中,事務保證多個數據修改的操做,要麼都修改爲功,要麼都失敗。同時,幾個事務之間又相互獨立,不會相互影響。html
在這篇文章中,咱們會先帶你們理解事務,以及Spring中的事務,經過Spring的事務抽象引出JTA事務,以及JTA的分佈式事務。理解了事務之後,再介紹分佈式系統、以及分佈式系統的原則,和分佈式系統中實現事務的原則。java
在介紹分佈式事務以前,先來來回顧一下事務的ACID原則:git
那麼,在分佈式系統中,這個原則是否可以保證呢?答案是不能,Not even close! 以原子性爲例,在有多個系統的分佈式系統中,一個分佈式事務是在不一樣的系統內部執行的,咱們沒有辦法保證它們可以同時完成,或者都不作。至於分佈式事務的原則,咱們過一會再說,咱們先把事務搞清楚。github
Spring是一個偉大的框架,從一開始只是一個容器框架,到如今已經發展成爲了一個包含企業開發中的方方面面的不少框架的總稱。它不但從複雜度上,發展出了用於各個方面的子框架。它還從易用性出發,推出了像Spring-Boot這樣的框架,使得搭建環境變得異常的簡單。web
很早以前Spring就已經有了一套本身的事務規範。(在org.springframework.transaction包中),並且用起來也很是的簡單:redis
1spring 2數據庫 3編程 4緩存 5 6 7 8 9 10 |
public Class OrderService { @Transactional public TicketOrder buyTicket(OrderDTO orderDTO) { TicketOrder tkOrder = new TicketOrder(); jdbcTemplate.execute(createOrderSQL); return tkOrder; } } |
咱們只須要在方法上加一個Transactional
標籤,那個這個方法就會在一個事務裏面執行。這是用代理模式實現的。Spring容器在初始化這個service實例的時候,其實是建立一個代理類,而後在調用這個方法的時候,包裝一個事務的處理。上面的方式使用代理模式展開,大體以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public Class OrderServiceProxy { // 經過代理實現的僞代碼,在原先的代碼外再包一層事務的建立、commit、rollback public Order buyTicket(OrderDTO orderDTO) { // get transaction from entityManager theTransaction.begin(); try { orderServiceImpl.buyTicket(orderDTO) theTransaction.commit(); } catch(Exception e) { theTransaction.rollback(); throw e; } } } |
從這個流程能夠看出,在更改數據的時候jdbcTemplate.save(order),事務並無提交,用戶查看最新的數據的時候,也看不到這條數據(隔離性),只有commit之後,全部的數據修改纔會同時起效(原子性)。若是期間發生任何錯誤,事務就會回退rollback,全部的數據修改又回到未修改狀態。
可能不少Java開發人員對事務的瞭解,就到這一步,沒有再往下了解過。咱們如今就來了解一下Spring的事務管理,瞭解了Spring的事務,才能知道如何在分佈式系統中使用Spring的事務管理實現分佈式事務。
因爲歷史緣由,從很早以前,Spring就已經有了一套本身的事務規範(在org.springframework.transaction包中)。可是針對JTA規範,Spring也作了不少工做,使得咱們在實現事務的時候不須要關心具體用的是哪個。可是,這也有一些問題:
第一個問題就是,不少作Java開發的人,都不知道JTA和Spring Transaction的區別。
第二,這兩種規範在某些地方仍是有些區別,使用不當也會出現問題。
咱們在上面介紹本地事務的時候說,使用Spring框架的標籤@Transactional,來方便的實現事務,可是須要說明的是,這個標籤類,在Spring的事務以及JavaEE事務規範中都有定義,分別是:
咱們在使用的時候就須要注意,若是你用Spring boot,那麼大部分狀況下這兩種標籤都能使用。spring boot提供了不少自動配置,可以根據你是否包含了JTA的依賴,來判斷是否要使用JTA的事務。若是沒有,即便你用的javax.transaction.Transactional
標籤,也會使用spring的事務機制來處理。
可是,若是你的spring應用比較複雜,有不少自定義的配置,就須要注意這兩種標籤。最好是根據須要明確的使用一種。
Spring之因此可以實現對兩種事務的支持,是由於在spring的Transaction規範中,定義了一個統一的PlatformTransactionManager
事務管理器。即便你沒有使用某個JPA框架,而是直接用JDBTemplate,Spring也可以使用默認的DataSourceTransactionManager來使用JDBC的事務來實現事務。並且也能夠直接經過標籤org.springframework.transaction.annotation.Transactional
來實現事務。也就是說,你不須要任何JPA的實現框架,只是使用Spring-Transaction就能實現數據庫的事務。
Spring的PlatformTransactionManager,也有JTA的實現JtaTransactionManager
。也就是說,你可使用Spring的事務規範,卻使用JTA的實現,並且也幾乎不須要任何配置,只要在具體的運行環境中包含包含JTA的實現能夠。好比你用JBoss的應用服務器,系統就會使用Jboss的JTA實現;若是你的class path裏面有Atomikos的庫,系統就會使用Atomikos的JTA實現。若是你使用spring-boot,你只須要在你的依賴裏面、或運行環境裏面,提供你所須要的JTA實現,它就會自動使用。
除了數據庫,spring的事務還支持JMS的事務,也就是在經過JMS使用某個消息中間件時,也能用spring的事務來實現讀寫消息的事務。
再總結一下Spring的事務抽象,它定義了抽象的事務管理,能夠管理任何支持事務操做(也就是commit和rollback)的資源,如:
EntityManager
中得到EntityTransaction
,經過它實現。session
:jms的session
提供commit()和rollback()操做。若是查看Spring事務相關的文檔,常常會看到’local transactions’和’external transactions’,本地事務和全局事務(或叫外部事務)。上面咱們說了,對任何資源,只要它提供了事務的操做,咱們就能使用spring的事務管理來提供事務。因爲spring提供了一個事務管理的抽象接口,而事務的控制,能夠是在spring容器來控制,也能夠由外部的事務管理模塊來控制,這就是本地事務和全局事務的區別。
本地事務就是指的是由Spring容器建立和維護的事務。例如在使用JDBC事務操做數據庫的時候,spring容器會在須要的時候建立事務的上下文,開啓一個JDBC的事務,而後調用業務方法,執行完成後,調用commit方法;而後在出錯的時候調用資源的rollback方法。還有事務的傳播、隔離等也都是由Spring容器來提供。本地事務只能針對一個資源實現徹底的事務控制。若是要在一個本地事務中操做兩個資源(例如兩個數據庫),實際上前後在兩個數據庫的Connection上調用commit()
方法去提交。
而外部事務,就是spring只負責經過事務的接口來開始事務、提交事務、回滾事務,而具體的操做仍是得有外部提供的事務管理的模塊或組件來執行和維護。例如咱們使用JBoss來運行咱們的web應用,而後在JBoss上配置了JTA的事務。那麼事務的具體管理和維護就是由JBoss提供的事務管理模塊來進行。
本地事務和外部事務的一個主要區別就是,是否能對多個資源實現事務控制。咱們來經過一個例子來講明它們的區別:使用JDBCTemplate和JMSTemplate對一個數據庫和一個MQ進行操做。使用Spring的代碼大體以下:
1 2 3 4 5 6 7 8 9 10 |
public Class OrderService { @Transactional public TicketOrder buyTicket(OrderDTO orderDTO) { orderRepository.save(order); jmsTemplate.convertAndSend("order:need_to_pay", order); return tkOrder; } } |
也就是在@Transactional
標記的方法裏,經過jdbcTemplate操做數據庫,使用JMSTemplate操做MQ。這個方法雖然是在一個事務裏,可是,若是咱們使用本地事務,那麼這兩個資源(數據庫和MQ)其實是在各自的事務裏面分別操做。把這段代碼展開成它實際的樣子,大體以下:
1 2 3 4 5 6 7 8 9 10 11 |
jmsTransaction.begin(); // get transactions from jms session dbTransaction.begin(); // get transactions from JDBC connection try { orderRepository.save(order); jmsTemplate.convertAndSend("order:need_to_pay", dto); dbTransaction.commit(); jmsTransaction.commit(); } catch(Exception e) { dbTransaction.rollback(); jmsTransaction.rollback(); } |
這樣,若是上述代碼在jmsTransaction.commit();
的時候出錯,這時候數據庫的事務已經提交,就沒法回滾。若是這時候這個方法被從新執行,數據庫的操做就會被重複執行。
若是咱們使用外部事務,那麼這裏就不會針對兩個資源出現兩個事務,而是隻有一個事務,來統一管理多個資源。若是在多個資源上的事務出錯了,外部的事務也可以保證回滾,這是經過事務的兩階段提交(2PC)來實現。使用JTA實現的事務正是這種外部事務。
因爲JTA使用兩階段提交來實現多個資源之間的事務,這就會帶來很大的性能問題。由於它要同步多個資源的事務,對每一個資源使用兩階段提交,這就使得這個事務所花的時間比本地事務多不少。並且在這個時間段內,因爲事務的隔離性,可能會形成長時間的資源佔用,使得其它的事務沒法同步訪問該資源上的一些數據。
在上面已經屢次提到JTA事務,那麼JTA究竟是什麼呢?介紹JTA以前,先看看XA。
XA是由X/Open組織提出的分佈式事務的架構(或者叫協議)。XA架構主要定義了(全局)事務管理器(Transaction Manager)和(局部)資源管理器(Resource Manager)之間的接口。XA接口是雙向的系統接口,在事務管理器(Transaction Manager)以及一個或多個資源管理器(Resource Manager)之間造成通訊橋樑。也就是說,在基於XA的一個事務中,咱們能夠針對多個資源進行事務管理,例如一個系統訪問多個數據庫,或即訪問數據庫、又訪問像消息中間件這樣的資源。這樣咱們就可以實如今多個數據庫和消息中間件直接實現所有提交、或所有取消的事務。XA規範不是java的規範,而是一種通用的規範,
目前各類數據庫、以及不少消息中間件都支持XA規範。
JTA是知足XA規範的、用於Java開發的規範。因此,當咱們說,使用JTA實現分佈式事務的時候,其實就是說,使用JTA規範,實現系統內多個數據庫、消息中間件等資源的事務。
JTA(Java Transaction API),是J2EE的編程接口規範,它是XA協議的JAVA實現。它主要定義了:
javax.transaction.TransactionManager
,定義了有關事務的開始、提交、撤回等操做。javax.transaction.xa.XAResource
,一種資源若是要支持JTA事務,就須要讓它的資源實現該XAResource
接口,並實現該接口定義的兩階段提交相關的接口。若是咱們有一個應用,它使用JTA接口實現事務,應用在運行的時候,就須要一個實現JTA的容器,通常狀況下,這是一個J2EE容器,像JBoss,Websphere等應用服務器。可是,也有一些獨立的框架實現了JTA,例如Atomikos, bitronix都提供了jar包方式的JTA實現框架。這樣咱們就可以在Tomcat或者Jetty之類的服務器上運行使用JTA實現事務的應用系統。
在上面的本地事務和外部事務的區別中說到,JTA事務是外部事務,能夠用來實現對多個資源的事務性。它正是經過每一個資源實現的XAResource
來進行兩階段提交的控制。感興趣的同窗能夠看看這個接口的方法,除了commit, rollback等方法之外,還有end()
, forget()
, isSameRM()
, prepare()
等等。光從這些接口就可以想象JTA在實現兩階段事務的複雜性。
在上面介紹Spring的事務抽象的時候,說過Spring事務支持JPA的事務,這裏再針對這個作一下說明。
可能不少作Java開發的,都沒弄清楚JTA和JPA的區別,即便我一開始也覺得JTA和JPA就是一回事,其實否則。JPA是Java Persistence API,也就是Java持久化編程接口。它定義了Java對象和它的持久化之間的聯繫,也就是Java的Object和Relation之間的Mapping,也就是一般說的ORM。而這個對象的持久化,不只限於數據庫,也多是NoSQL,多是文件,也多是其它可以序列化後保存的地方。JPA使用@Entity
標記一個Java對象,並將這個Java對象和數據庫的某一個表關聯。經過ID
將一個實例映射到表中的一條記錄。
像Hibernate
就是一個實現JPA的框架。
使用JPA
實現事務,用的是EntityManager
來獲取一個事務EntityTransaction
,而在JTA中,用的是TransactionManager
。
使用JTA事務,能夠實現對多個資源實現事務,這也是一說到分佈式事務,就會說JTA的緣由。
若是你的分佈式系統只是把數據庫按照功能地區等進行分區分片的劃分,再使用MQ等資源,那你就徹底能夠經過使用JTA來實現不一樣資源的分佈式事務。
可是,如今流行的微服務框架,每每是部署多個服務,一個事務可能須要調用多個服務,調用多個數據庫、MQ,對於這種微服務架構的分佈式事務,又須要使用其它的方式來實現。
分佈式系統從一開始到如今,有多種形式,從應用的個數和使用的數據庫角度來講,簡單列舉了以下幾種:
這幾年,微服務的概念愈來愈火,通常來講,用微服務架構實現的分佈式系統,有兩種方式(這裏簡化了不少東西,像緩存、監控、消息中間件、日誌等等支持系統,只是僅僅考慮通常的業務系統):
按照功能劃分,每一個應用提供某種功能,而每一個應用又部署多個實例來實現高可用。
這種實現的好處是能夠每一個功能一個應用,那個每一個應用模塊都比較簡單;可是,這就會有不少服務間調用,有時候爲了完成一個業務請求,要在好幾個服務之間調用好幾回。這就須要在拆分模塊、設計服務間調用的接口、設計業務的流程的時候都須要綜合考慮,儘可能減小服務間調用。
還有一種是用一樣的應用部署多個實例,各個服務經過分區分片等方式,使用各自的數據庫或其它數據源。
這種方式的好處就是,全部的功能都在一個應用裏,一個應用部署任意多個,只須要經過合理的數據庫的分區分片,讓不一樣的節點訪問不一樣的數據庫。可是,一旦你的數據愈來愈複雜,數據庫的分區分片會很是複雜。有時候,也能夠把數據庫按功能分開,一個節點訪問多個數據庫。
在介紹如何實現分佈式系統的事務以前,咱們先看看分佈式系統的原則。
對於分佈式系統來講,很難有一個相似ACID這樣的標準,和知足這個標準的開發規範,及其實現的框架,來爲咱們方便的實現分佈式系統的事務。要實現分佈式系統的事務,咱們每每須要根據實際須要,在可用性(包括性能、系統吞吐量等)、事務性(相似本地事務的ACID)、可操做性(開發和維護的難易程度)之間作出權衡。
那麼,分佈式事務的原則是什麼呢?咱們怎麼能肯定一個分佈式事務的實現,知足了它的事務性的要求呢?首先,咱們來看看分佈式系統的一個原則,或者叫定理,CAP定理。
CAP定理,包括如下幾個方面:
爲了便於咱們理解,在介紹這個CAP定理的時候,咱們結合一個業務實例來看看這個CAP定理是什麼意思。這個實例是一個簡單的訂票系統的購票流程,大體以下:
這是一個微服務架構的分佈式系統,有一個網關統一接受用戶請求,而後將請求轉發到相應的服務上。
總共有3個服務:Order,User,Ticket分別用於處理交易、用戶、票相關的業務。
每一個服務都使用本身的數據庫。
一個購票流程大體以下:
再來看看CAP定理:
因爲分佈式系統形式的多樣性和複雜性,若是想徹底知足上述的原則設計一個分佈式系統,幾乎是不可能的。首先,分佈式服系統就是要把系統的各個部分部署到不一樣的服務器上,那咱們就必需要經過分區容錯來避免因爲網絡、機器故障等緣由形成的問題。因此分區容錯性是必不可少的,不然可用性都沒法保證。
對於可用性來講,若是咱們要嚴格保證可用性,即便是在分區容錯性獲得保障的前提下,全部的服務都是可用的,有時候,咱們也須要經過異步的方式來處理一些業務,這就會形成數據的不一致。如已經從用戶帳戶上扣費,可是票尚未轉移完成等。
再來看一致性,是否有辦法可以實現呢?那咱們就須要先來看看幾種一致性:
在通常的分佈式系統的設計中,咱們大都以最終一致性爲目標,來設計咱們的分佈式事務。這既能保證系統的可用性和容錯性,也能在絕大多數狀況下保證數據的弱一致性,而且在少數出錯或網絡高延遲的狀況下,也能保證數據的最終一致性。
上面說了分佈式事務的原則,以最終一致性爲目標,那爲了實現這個目標,我就須要不少異常狀況的處理,包括數據庫失敗、業務代碼失敗、網絡錯誤等。舉例來講,在一個接口調用,若是發生超時,我就須要重試。可是也有可能對方的服務已經處理完這個請求,只是在完成返回結果的時候,網絡傳輸的問題致使超時。那麼服務調用端再重試,實際上就是發了兩次請求。因此,我就須要對於分佈式服務的事務處理,對於一樣的消息,只會處理一次。
分佈式系統的冪等性,就是對於一個處理接口,若是它會對系統形成反作用,也就是修改數據,那就須要保證對於一樣的請求,無論請求了多少次,結果都是一致的。
那麼,如何保證這個冪等性呢?一種比較通用的方法就是,對每個請求,生成一個token
,並且須要惟一,而後將這個token放在請求的參數裏面。服務在處理這個請求以前,先拿到token,檢查這個token是否已經處理過,只有沒有處理過的纔去處理。這個token能夠保存在數據庫、redis、甚至內存等地方。因爲它只是用來記錄已經處理的請求的token,因此大可沒必要保存在內存中。因爲在分佈式系統中,一個服務會部署多個,一個請求失敗後從新發送,有多是被髮送到另外一臺機器上。因此這個token應該是服務範圍共享的,咱們須要在同一個服務的多個部署都能共享訪問的地方,來保存已經處理過的token。
因此,使用redis來保存token是一個不錯的選擇。
分佈式事務的實現,也有不少種的實現方式,通常稱做模式。咱們在一個分佈式系統的一個業務方法裏,每每須要調用外部的數據庫、MQ,也有可能調用其它服務。若是你把其它的服務也看做一種資源,那麼一個業務方法實際上就是操做了好幾個資源。而這,正是XA所作的事情。可是,不是全部的資源都支持XA,像咱們的服務間調用,通常就只有一個處理方法,不會提供什麼commit()
、rollback()
之類的方法,更別說兩階段提交須要的其它方式。
可是,咱們卻能用事務的思想爲咱們實現分佈式事務提供一些啓發。從本地事務的處理過程,咱們能夠看出,它是經過:1.嘗試修改 2.提交(完成) 3.取消(出錯)的方式來實現的 。根據這個思想,咱們能夠想到分佈式事務的幾種實現方式:
有關TCC模式的詳細內容,請參考做者的原文。TCC模式的事務實現,咱們會在另外一篇文章中再介紹。
有關在spring中實現分佈式事務,有一篇文章。這篇文章介紹了使用XA和不使用XA實現分佈式事務的幾種方式。可是都是說的同時使用2種或以上的資源(如數據庫和MQ等支持XA的數據源)的狀況。雖然不能適用於微服務架構的服務間調用的狀況,可是也能有一些借鑑意義。
在上面的文章中介紹的其中一種方式是:最大努力一階段提交。仍是用以前的例子說明,也就是在一個事務方法中操做DB和MQ:
1 2 3 4 5 6 7 8 9 10 |
@Service public Class OrderService { @Transactional public TicketOrder buyTicket(OrderDTO orderDTO) { jdbcTemplate.doQuery(sth); jmsTemplate.send(sth); return tkOrder; } } |
若是這裏不用JTA事務,而是使用Spring的本地事務,那麼這個方法內的操做執行完之後,spring事務管理器會前後提交DB Connection的事務和JMS Session的事務。在這種方式下,絕大部分狀況下都不會有問題。可是有可能出現的一種錯誤就是,在提交完一個事務後,提交另外一個事務的時候出錯了。在以最終一致性爲目標的分佈式事務中,每每就容許這種狀況的出現,可是須要採用另外一些措施來補救。
至此,咱們介紹了Spring事物、JTA事物,還有跨多個資源的事物,也介紹了一下分佈式系統和分佈式事務,特別是分佈式事務的強一致性原則。在以後的幾篇文章中,將繼續介紹實現分佈式事務的幾種具體的方法。