分散式事務介紹
在分散式系統中實現的事務就是分散式事務,分散式系統的CAP原則是:
- 一致性
- 可用性
- 分割槽容錯性
是分散式事務主要是保證資料的一致性,主要有三種不同的原則
- 強一致性
- 弱一致性
- 最終一致性
JTA與XA
共同點:
- Transaction Manager(事務管理器)
- XA Resource
- 兩階段提交
再比如User service 扣費成功後,往新訂單轉移票佇列寫訊息,這時Ticket service 正在處理中或者處理中發生了失敗,這中間的過程中使用者檢視自己的餘額已經扣費成功,但票的資訊卻沒有,此時可以使用事務失敗回滾的方式依次回退,這種叫弱一致性;又或者可以把處理失敗的內容傳送至一個錯誤佇列中,由人工處理等方式解決,這種叫最終一致性。
Spring JTA分散式事務實現
- 可以使用如JBoss之類的應用伺服器提供的JTA事務管理器
- 可以使用Atomikos、Bitronix等庫提供的JTA事務管理器
不使用Spring J他的分散式事務實現
為什麼不使用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);
}
}
複製程式碼
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的方式,使其在一個事務裡面執行
特別說明: @Transactional 如果沒有做任何配置的情況下,則會使用DBConfiguration類中@Primart註解下的DataSource,用它去做datasource connection
spring DataSourceUtils原始碼
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服務給停掉,出現如下異常 重啟啟動msyql服務,程式繼續執行,此時來看資料庫order表中多了一條記錄,而user表沒有變化;第一個order事務並沒有回滾;那如果是rollback的時候停掉mysql服務,其實是沒有影響的,因為本身就沒有commit, 執不執行rollback本身是沒有影響的。
git程式碼地址 ☚
例項3-JPA-DB.鏈式事務管理器
- mysql + mysql
- 鏈式事務:JpaTransactionManager + DataSourceTransactionMananger
- 不處理重試
基於例項1的核心程式碼繼續做修改演示:
git程式碼地址 ☚
例項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'
複製程式碼