分散式事務之事務實現模式與技術(四)

分散式事務介紹
在分散式系統中實現的事務就是分散式事務,分散式系統的CAP原則是:
- 一致性
- 可用性
- 分割槽容錯性
是分散式事務主要是保證資料的一致性,主要有三種不同的原則
- 強一致性
- 弱一致性
- 最終一致性
JTA與XA
共同點:
- Transaction Manager(事務管理器)
- XA Resource
- 兩階段提交


Orderservice監聽新訂單佇列中的訊息,獲取之後新增訂單,成功則往新訂單繳費佇列中寫訊息,中間新增訂單的過程使用JTA事務管理,當新增失敗則事務回滾,不會往新訂單繳費佇列中寫訊息;
再比如User service 扣費成功後,往新訂單轉移票佇列寫訊息,這時Ticket service 正在處理中或者處理中發生了失敗,這中間的過程中使用者檢視自己的餘額已經扣費成功,但票的資訊卻沒有,此時可以使用事務失敗回滾的方式依次回退,這種叫弱一致性;又或者可以把處理失敗的內容傳送至一個錯誤佇列中,由人工處理等方式解決,這種叫最終一致性。
Spring JTA分散式事務實現
- 可以使用如JBoss之類的應用伺服器提供的JTA事務管理器
- 可以使用Atomikos、Bitronix等庫提供的JTA事務管理器
不使用Spring JTA的分散式事務實現
為什麼不使用JTA?
因為JTA採用兩階段提交方式,第一次是預備階段,第二次才是正式提交。當第一次提交出現錯誤,則整個事務出現回滾,一個事務的時間可能會較長,因為它要跨越多個數據庫多個數據資源的的操作,所以在效能上可能會造成吞吐量低。
不適用JTA,依次提交兩事務
1.start message transaction 2.receive message 3.start database transaction 4.update database 5.commit database transaction 6.commit message transaction##當這一步出現錯誤時,上面的因為已經commit,所以不會rollback 複製程式碼
這時候就會出現問題
多個資源的事務同步方法
XA與最後資源博弈
1.start message transaction 2.receive message 3.start JTA transaction on DB 4.update database 5.phase-1 commit on DB transaction 6.commit message transaction##當這一步出現錯誤時,上面的因為是XA的第一次提交預備狀態,所以可以rollback 7.phase-2 commit on DB transaction##當這一步出現錯誤時,因為message不是XA方式,commit後無法rollback 複製程式碼
但這種相比不使用JTA,已經很大程度上避免了事務發生錯誤的可能性。
共享資源
- 兩個資料來源共享同一個底層資源
- 比如ActiveMQ使用DB作為底層資源儲存
- 使用資料庫的database transaction Manager事務管理器來控制事務提交
- 需要資料來源支援指定底層資源儲存方式
最大努力一次提交
- 依次提交事務
- 可能出錯
- 通過AOP或Listener實現事務直接的同步
JMS最大努力一次提交+重試
- 適用於其中一個數據源是MQ,並且事務由讀MQ訊息開始
- 利用MQ訊息的重試機制
- 重試的時候需要考慮重複訊息
1.start message transaction 2.receive message 3.start database transaction 4.update database#資料庫操作出錯,訊息被放回MQ佇列,重試重新觸發該方法 5.commit database transaction 6.commit message transaction 複製程式碼
上面這種時候沒有問題
1.start message transaction 2.receive message 3.start database transaction 4.update database 5.commit database transaction 6.commit message transaction#提交MQ事務出錯,訊息放回至MQ佇列,重試重新觸發該方法 複製程式碼
可能存在問題:會重複資料庫操作,因為database transaction不是使用JTA事務管理,所以database已經commit成功;如何避免,需要忽略重發訊息,比如唯一性校驗等手段。

鏈式事務管理
- 定義一個事務鏈
- 多個事務在一個事務管理器裡依次提交
- 可能出錯
如何選擇(根據一致性要求)
- 強一致性事務:JTA(效能最差、只適用於單個服務內)
- 弱、最終一致性事務:最大努力一次提交、鏈式事務(設計相應的錯誤處理機制)
如何選擇(根據場景)
- MQ-DB:最大努力一次提交+重試
- 多個DB:鏈式事務管理
- 多個數據源:鏈式事務、或其他事務同步方式
例項
例項1-DB-DB
application.properties中配置了兩個資料來源
# 預設的Datasource配置 # spring.datasource.url = jdbc:mysql://localhost:3307/user # spring.datasource.username = root # spring.datasource.password = 123456 # spring.datasource.driverClassName = com.mysql.jdbc.Driver spring.ds_user.url = jdbc:mysql://localhost:3307/js_user spring.ds_user.username = root spring.ds_user.password = 123456 spring.ds_user.driver-class-name = com.mysql.jdbc.Driver spring.ds_order.url = jdbc:mysql://localhost:3307/js_order spring.ds_order.username = root spring.ds_order.password = 123456 spring.ds_order.driver-class-name = com.mysql.jdbc.Driver 複製程式碼
自定義配置類檔案
@Configuration public class DBConfiguration{ @Bean @Primary @ConfigurationProperties(prefix="spring.ds_user") #設定讀取在properties檔案內容的字首 public DataSourceProperties userDataSourceProperties() { return new DataSourceProperties(); } @Bean @Primary public DataSource userDataSource(){ return userDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build(); } @Bean public JdbcTemplate userJdbcTemplate(@Qualifier("userDataSource") DataSource userDataSource){ return new JdbcTemplate(userDataSource); } @Bean @ConfigurationProperties(prefix="spring.ds_order") #設定讀取在properties檔案內容的字首 public DataSourceProperties orderDataSourceProperties() { return new DataSourceProperties(); } @Bean public DataSource orderDataSource(){ return userDataSourceProperties().initializeDataSourceBuilder().type(HikariDAtaSource.class).build(); } @Bean public JdbcTemplate orderJdbcTemplate(@Qualifier("orderDataSource") DataSource orderDataSource){ return new JdbcTemplate(orderDataSource); } } 複製程式碼
ofollow,noindex">Spring註解解釋(@Primary、@Qualifier)
實際呼叫類
public class CustomerService{ @Autowired @Qualifier("userJdbcTemplate") private jdbcTemplate userJdbcTemplate; @Autowired @Qualifier("orderJdbcTemplate") private jdbcTemplate orderJdbcTemplate; private static final String UPDATE_CUSTOMER_SQL; private static final String INSERT_ORDER_SQL; @Transactional#事務管理註解 public void createOrder(Order order){ userJdbcTemplate.update(UPDATE_CUSTOMER_SQL, order) if(order.getTitle().contains("error1")){#模擬異常出現 throw new RuntimeException("error1") } orderJdbcTemplate.update(INSERT_ORDER_SQL, order)#沒有使用事務,直接提交 if(order.getTitle().contains("error2")){#模擬異常出現 throw new RuntimeException("error2") } } } 複製程式碼
關於上述過程的詳細說明:
因為使用了標籤 @Transactional的方式,使其在一個事務裡面執行


也就是同步到Transaction Manager上面,但是這邊的同步不是說事務的同步,只是同步資料庫連線的開關

特別說明: @Transactional 如果沒有做任何配置的情況下,則會使用DBConfiguration類中@Primart註解下的DataSource,用它去做datasource connection
spring DataSourceUtils 使用已有的connection,只是控制資料庫連線的釋放,不是事務。
例項2-DB-DB.鏈式事務管理器
鏈式事務管理器在 這個庫裡面

DBConfiguration類中新增一段
@Bean public PlatformTransactionManager transactionManager(){ DataSourceTransactionManager userTM = new DataSourceTransactionManager(userDataSource()) #看似方法呼叫,實則從spring容器中獲取 DataSourceTransactionManager orderTM = new DataSourceTransactionManager(orderDataSource()) # orderTM.setDataSource(orderDataSource())如果使用這種方式則不是從容器中去獲取了,因為orderTM不是spring容器管理 ChainedTransactionManager tm = new ChainedTransactionManager(userTM, orderTM)## order先執行,user後執行 return tm; } 複製程式碼
連結事務管理器(Chaining transaction managers)
出現異常是否會有問題呢?
- 使用debug方式模擬執行,第一個order事務提交以後,第二user個事務執行的時候把mysql服務給停掉,出現如下異常
例項3-JPA-DB.鏈式事務管理器
- mysql + mysql
- 鏈式事務:JpaTransactionManager + DataSourceTransactionMananger
- 不處理重試
基於例項1的核心程式碼繼續做修改演示:
例項4-JMS-DB.最大努力一次提交
- JMS-DB
- ActiveMQ + Mysql
- 最大努力一次提交:TransactionAwareConnectionFactoryProxy
分散式系統唯一性
什麼是分散式系統ID?
- 分散式系統的全域性唯一標識
- UUID:生成唯一ID的規範
- 用於唯一標識,處理重複訊息
分散式系統唯一性ID生成策略:
- 資料庫自增序列
- UUID:唯一ID標準,128位,幾種生成方式(時間+版本等方式)
- MongDB的ObjectID:時間戳+機器ID+程序ID+序號
- Redis的INCR操作、Zookeeper節點的版本號
使用何種方式?
- 自增的ID:需要考慮安全性、部署
- 時間有序:便於通過ID判斷建立時間
- 長度、是否數字型別:是否建立索引
分散式系統分散式物件
- Redis:Redisson庫:RLock,RMap,RQueue等物件
- Zookeeper:Netflix Curator庫:Lock,Queue等物件
分散式事務實現模式
- 訊息驅動模式:Message Driven
- 事件溯源模式:Event Sourcing
- TCC模式:Try-Confirm-Cancel
冪等性
- 冪等操作:任意多次執行所產生的影響,與一次執行的影響相同
- 方法的冪等性:使用同樣的引數呼叫一次方法多次,與呼叫一次結果相同
- 介面的冪等性:介面被重複呼叫,結果一致
微服務介面的冪等性
- 重要性:經常需要通過重試實現分散式事務的最終一致性
- GET方法不會對系統產生副作用,具有冪等性
- POST、PUT、DELETE方法的實現需要滿足冪等性
Service方法實現冪等性
public OrderService{ Map disMap;# 用於存放已經處理的id @Transactional void ticketOrder(BuyTickerDTO dto){ String uid = createUUID(dto);# 建立並獲取資料的唯一id if(!diMap.contains(uuid){#disMap還沒有處理過這個資料唯一id,則進入建立 Order order = createOrder(dto); disMap.append(uid)## 追加Map } } userService.charge(dto);#呼叫user微服務 } 複製程式碼
SQL實現冪等性
#通過調節限定,只有第一次支付的時候才會扣餘額,被重複呼叫的時候就不會重複扣費用,通過paystatus判斷 UPDATE customer SET deposit = deposit - ${value}, paystatus = 'PAID' WHERE orderId = ${id} and paystatus = 'UNPAID' 複製程式碼