分佈式事務上節中討論的數據庫事務是解決“單個數據庫數據不一致”的問題,而在一些具有規模的網站系統當中,數據庫往往不止一個,一旦出現多個數據庫,則會出現多數據庫的數據不一致問題。多個數據庫的數據不一致問題一般有兩種場景,如圖4.76所示。圖4.76 多數據庫的數據不一致場景場景一,由於“應用拆分”和“數據庫分離”造成的多數據庫數據不一致問題,例如,用戶餘額存放在“資金”數據庫中,優惠券存放在“優惠券”數據庫,當發生交易時,會出現扣減餘額成功,但扣減優惠券失敗的情況;場景二,當數據庫中的數據個數超過千萬級時,一般都需要進行分庫,這也會造成多數據庫的數據不一致問題,例如,用戶1的餘額存在數據庫A中,而用戶2的餘額存在數據庫B中,當用戶1向用戶2轉賬時,會出現用戶1扣除金額成功,而用戶2增加金額失敗的情況。解決“多數據庫的數據不一致問題”的方式被稱為分佈式事務,確切地說,分佈式事務不是某種具體的方式,而是解決“多數據庫的數據不一致問題”的方式的統稱,分佈式事務的實現方式本身是開放的。說明:關於分佈式事務的相關理論有CAP原則和BASE理論;關於分佈式事務的相關算法有2PC、3PC、TCC及本地消息表等。分佈式事務是行業內一大難題,在選取分佈式事務具體方案之前,需要分析清楚是否真的需要分佈式事務。很多時候,造成場景一的原因,是由於過度設計造成的,即在系統內部劃分出很多沒必要的獨立模塊。例如,圖4.76中所舉的例子,優惠券本身可以算是資金的一部分,如果把資金和優惠券合並成一個模塊,則不需要分佈式事務。註意:以下介紹的分佈式事務的解決方法都不是唯一方法,具體實施需要根據實際情況而定。1.XA事務XA(eXtended Architecture)是由X/Open組織提出的分佈式事務的規范,XA事務能較好地解決“多數據庫的數據不一致”問題。目前,比較流行的數據庫(如Oracle、MySQL等)都支持XA事務。註意:Oracle執行XA事務的性能要優於MySQL,另外,MySQL最好選用5.7或之後的版本,因為MySQL 5.7之前的版本對XA事務的支持都是有缺陷的。XA事務可以簡單地理解為多個數據庫同時執行數據庫事務,但與執行普通數據庫事務不同的是,最後的提交階段需要先檢查所有事務的狀態(是否允許提交)後才能提交。第三方軟件使用數據庫XA事務的流程如圖4.77所示。以Java為例,通過JDBC使用XA事務的代碼如代碼4.55所示。圖4.77 第三方軟件使用數據庫XA事務的流程代碼4.55 通過JDBC使用XA事務//連接數據庫1,並獲取資源管理器對象rm_1Connection conn_1 = DriverManager.getConnection("jdbc:mysql://ip:port/xxx","userName","password");XAConnection xaConn_1 = new MysqlXAConnection((com.mysql.jdbc.Connection)conn_1, false);XAResource rm_1 = xaConn_1.getXAResource();//連接數據庫2,並獲取資源管理器對象rm_2Connection conn_2 = DriverManager.getConnection("jdbc:mysql://ip:port/xxx","userName","password");XAConnection xaConn_2 = new MysqlXAConnection((com.mysql.jdbc.Connection)conn_2, false);XAResource rm_2 = xaConn_2.getXAResource();//設置全局事務ID//xxx可以用UUID.randomUUID().toString()生成byte[] gtrid = "xxx".getBytes();try{//操作數據庫1//生成數據庫1的事務ID,gtrid為全局事務ID,bqual_1為分支限定符byte[] bqual_1 = "db_1".getBytes();Xid xid_1 = new MysqlXid(gtrid, bqual_1, 1);//啟動數據庫1的事務rm_1.start(xid_1, XAResource.TMNOFLAGS);//使用conn_1執行SQL語句(數據庫1執行SQL語句)…//SQL語句執行結束,遷移事務狀態rm_1.end(xid_1, XAResource.TMSUCCESS);//操作數據庫2//生成數據庫2的事務ID,bqual_2需要與數據庫1的值有所區別byte[] bqual_2 = "db_2".getBytes();Xid xid_2 = new MysqlXid(gtrid, bqual_2, 1);//啟動數據庫2的事務rm_2.start(xid_2, XAResource.TMNOFLAGS);//使用conn_2執行SQL語句(數據庫2執行SQL語句)…//SQL語句執行結束,遷移事務狀態rm_2.end(xid_2, XAResource.TMSUCCESS);//兩段提交//準備階段,獲取兩個數據庫的事務狀態int prepare_1 = rm_1.prepare(xid_1);int prepare_2 = rm_2.prepare(xid_2);//提交階段,根據兩個事務的狀態決定提交還是回滾if (prepare_1 == XAResource.XA_OK && prepare_2 == XAResource.XA_OK) {rm_1.commit(xid_1, false);rm_2.commit(xid_2, false);} else { //一個數據庫事務存在問題,則回滾rm_1.rollback(xid_1);rm_2.rollback(xid_2);}} catch(XAException e) {//發生錯誤,回滾事務rm_1.rollback(xid_1);rm_2.rollback(xid_2);}XA事務在使用上是簡單的,但是由於XA事務是同時對多個數據庫執行數據庫事務,因此會同時浪費多個數據庫的性能。在大型網站系統當中,XA事務一般是不被提倡的,因為大型網站系統需要應對高並發場景,在高並發壓力下,XA事務往往會大量阻塞任務,從而引發超時等異常。不過,在一個大型網站系統中,並發壓力的分佈往往是不均等的,也就是說,存在並發壓力不大的模塊,而在這些模塊中使用XA事務也是可以的。因此,XA事務的好處是使用簡單,但不適合用於並發壓力大的功能模塊。2.JTAJTA(Java Transaction API)是Java的事務管理框架。通過使用JTA,開發者可以更簡單地實現XA事務。值得一提的是,JTA隻是簡化瞭編碼,並沒有改變XA事務性能差的狀況。下面以Spring Boot為例,介紹使用JTA實現XA事務的過程。說明:JTA實際上隻是提供瞭事務管理的統一接口,它本身不負責具體的實現,具體的實現交由Atomikos或JOTM等事務管理器完成。(1)引入JTA依賴包。需要在工程配置文件(build.gradle)中添加JTA的依賴包,如代碼4.56所示,其中,選用Atomikos作用具體的事務管理器,數據庫操作框架選用JDBC Template。代碼4.56 在build.gradle中添加JTA依賴包…dependencies {…//在dependencies中添加JTA的依賴包,選用Atomikos作為具體的事務管理器implementation 'org.springframework.boot:spring-boot-starter-jtaatomikos'//添加數據庫操作框架依賴包,這裡選用JDBC Templateimplementation 'org.springframework.boot:spring-boot-starter-jdbc'implementation 'com.alibaba:druid:1.0.26' //數據庫連接池依賴runtimeOnly 'mysql:mysql-connector-java' //MySQL驅動依賴…}…添加完依賴包後,需要同步工程配置。JTA的依賴包在同步工程配置後才會被下載和引入。在IntelliJ IDEA中,隻需要單擊“同步”按鈕即可同步工程配置,如圖4.78所示。圖4.78 在IntelliJ IDEA中同步build.gradle配置(2)配置數據庫信息。配置數據庫連接信息需要在後端應用程序的配置文件(默認是application.properties)中設置。與平常配置數據庫不同的是,這裡需要配置兩個數據庫連接信息,如代碼4.57所示,其中,數據庫1通過Spring Boot提供的默認字段配置,數據庫2通過自定義字段配置。說明:默認情況下,Spring Boot隻提供一個數據庫的連接配置,如果需要連接多個數據庫,則需要通過額外代碼完成多個數據庫的連接。代碼4.57 在配置文件中添加數據庫連接信息…#配置說明可參考4.4.4小節中的代碼4.50#數據庫1的連接信息,通過Spring Boot的默認字段配置,也可通過自定義字段配置spring.datasource.jdbc-url=jdbc:mysql://ip:port/xxxspring.datasource.username=rootspring.datasource.password=passwordspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.type=com.alibaba.druid.pool.DruidDataSourcespring.datasource.max-active=20spring.datasource.max-idle=8spring.datasource.initial-size=10#數據庫2的連接信息,通過自定義字段配置,dao.extradb為自定義字段dao.extradb.jdbc-url=jdbc:mysql://ip:port/xxxdao.extradb.username=rootdao.extradb.password=passworddao.extradb.driver-class-name=com.mysql.cj.jdbc.Driverdao.extradb.type=com.alibaba.druid.pool.DruidDataSourcedao.extradb.max-active=20dao.extradb.max-idle=8dao.extradb.initial-size=10…配置文件雖然配置瞭多個數據庫連接信息,但默認情況下Spring Boot隻接受一個數據庫的設置,因此需要添加額外代碼(新建一個Java文件)關聯這些數據庫連接信息,如代碼4.58所示。說明:代碼4.58是全局設置,不需要被其他文件引用,存放位置最好在Dao層內,以方便開發者查閱。代碼4.58 在配置文件中添加數據庫連接信息package com.example.demo.dao.config;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.boot.jdbc.DataSourceBuilder;import org.springframework.context.annotation.Primary;import org.springframework.jdbc.core.JdbcTemplate;import javax.sql.DataSource;@Configurationpublic class DataBaseConfig {//關聯數據庫1的連接信息,並設置為默認連接//設置標識,關聯函數DataBaseTemplate1()中的參數@Bean(name="dataBaseConfig_1")@Primary //默認數據庫標識//spring.datasource為數據庫1的連接信息字段@ConfigurationProperties(prefix="spring.datasource")DataSource DataBaseConfig1(){return DataSourceBuilder.create().build();}@Bean(name="databaseTemplate_1")@Primary //默認數據庫標識public JdbcTemplate DataBaseTemplate1(@Qualifier("dataBaseConfig_1")DataSource data){return new JdbcTemplate(data);}//關聯數據庫2的連接信息//設置標識,關聯函數DataBaseTemplate2()中的參數@Bean(name="dataBaseConfig_2")//dao.extradb為數據庫2的自定義連接信息字段@ConfigurationProperties(prefix = "dao.extradb")DataSource DataBaseConfig2(){return DataSourceBuilder.create().build();}@Bean(name="databaseTemplate_2")public JdbcTemplate DataBaseTemplate2(@Qualifier("dataBaseConfig_2")DataSource data){return new JdbcTemplate(data);}}(3)在Dao層中操作多個數據庫,如代碼4.59所示。在實際編碼中,對不同數據庫的操作最好分為不同的文件。代碼4.59 在Dao層中操作多個數據庫package com.example.demo.dao;//引用JDBCTemplate的類import org.springframework.jdbc.core.JdbcTemplate;import … //省略其他引用的類@Repository("TestDao")public class TestDao {//獲取數據庫1(默認數據庫)的JdbcTemplate對象,此對象會被自動註入@Autowiredprivate JdbcTemplate jdbcTemplate_1;//獲取數據庫2的JdbcTemplate對象,databaseTemplate_2為代碼4.58中的標識,此對象會被自動註入@Autowired@Qualifier("databaseTemplate_2")private JdbcTemplate jdbcTemplate_2;//在數據庫1增加一條記錄public String Create_1(String value1, String value2){try{//SQL語句,?為占位符,後續通過參數替換String sqlString = "INSERT INTO formName VALUES (?, ?)";//執行SQL語句int result = jdbcTemplate.update(sqlString, value1, value2);if(result > 0){return "success";}else {return "fail";}}catch(Exception e){return "fail";}}//在數據庫2增加一條記錄public String Create_2(String value1, String value2){try{String sqlString = "INSERT INTO formName VALUES (?, ?)";int result = jdbcTemplate.update(sqlString, value1, value2);if(result > 0){return "success";}else {return "fail";}}catch(Exception e){return "fail";}}(4)在業務層(Service層)中標記需要使用數據庫事務的方法,如代碼4.60所示,其中,@Transactional為XA事務標記。說明:代碼4.60中的@Transactional標識與數據庫事務的標識相同,但其本質上是XA事務。代碼4.60 在Service層中添加事務標記…@Service("XXX")public class XXXService {@Resource(name = "TestDao")private TestDao _testDao;…//標記數據庫事務,當異常發生時,會自動撤銷所有數據庫操作。函數中不能用catch捕獲異常@Transactionalpublic JSONObject ServiceFunction(JSONObject requestParam, JSONObjectreturnParam){//調用代碼4.59中的函數1(操作數據庫1)_testDao.Create_1(…);//調用代碼4.59中的函數2(操作數據庫2)_testDao.Create_1 (…);return returnParam;}}3.本地消息表當多個數據庫操作分別處在不同的程序中卻又需要保持數據一致性時(圖4.76中的場景一),一般采用“本地消息表”實現分佈式事務。“本地消息表”這個方案的核心是將分佈式事務拆分成多個數據庫事務進行處理,並通過額外的檢查機制確保多個數據庫事務都正常完成,以達到數據最終一致的效果。“本地消息表”方案的工作原理如圖4.79所示。其中,檢查程序可以是程序1本身,本地消息表的實體可以是本地文件、數據庫中的表等。圖4.79 “本地消息表”方案的工作原理“本地消息表”方案是解決分佈式事務的一種思路,而其具體的實現是開放的,針對不同的應用場景或軟件形態會有不同的實現方式。針對後端應用程序而言,可參照圖4.80所示的工作流程,其中,檢查程序最好是獨立的一個程序,本地消息表一般是數據庫中的表(可以為數據庫1或數據庫2中的表,也可以是獨立數據庫中的表)。圖4.80 後端應用程序采用“本地消息表”實現分佈式事務的工作流程說明:圖4.80中的檢查程序也可以是後端應用程序1中的定時任務,但是後端應用程序1可能被部署在多個服務器上,如果是這樣的話,則需要使用Quartz等分佈式定時器框架,以避免發生定時任務被多次執行的情況。“本地消息表”方案的實現在實際編碼中比較麻煩,而且會增加後端結構的復雜性。而性能方面,一般來說“本地消息表”方案更勝一籌。但是,因為“本地消息表”方案的具體實現是五花八門的,而且其內部可能會用到XA事務,所以很難評定與單純使用XA事務的性能對比。4.其他除瞭“XA事務”和“本地消息表”這兩種分佈式事務解決方案以外,還有很多其他解決思路或方案,如2PC(兩段式提交,XA事務是其中一種實現)、3PC(三段式提交)、基於消息隊列的解決方案及TCC(事務補償)方案等。但是,無論采用哪種方案,分佈式事務都會增加系統復雜度和限制數據庫性能,所以架構設計應該盡量避開分佈式事務。如果不能完全避開分佈式事務,則需要對系統內的分佈式事務提供統一的規范,以防止“五花八門”的分佈式事務解決方案出現在網站系統當中(限制系統混亂度)。本文給大傢講解的內容是大型網站架構的技術細節:後端架構數據庫–分佈式事務下篇文章給大傢講解的內容是大型網站架構的技術細節:後端架構非關系型數據庫感謝大傢的支持
本文出自快速备案,转载时请注明出处及相应链接。