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

著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。

分佈式事務介紹

在分佈式系統中實現的事務就是分佈式事務,分佈式系統的CAP原則是:mysql

  • 一致性
  • 可用性
  • 分區容錯性

是分佈式事務主要是保證數據的一致性,主要有三種不一樣的原則git

  • 強一致性
  • 弱一致性
  • 最終一致性

JTA與XA

共同點:github

  • Transaction Manager(事務管理器)
  • XA Resource
  • 兩階段提交

Orderservice監聽新訂單隊列中的消息,獲取以後新增訂單,成功則往新訂單繳費隊列中寫消息,中間新增訂單的過程使用JTA事務管理,當新增失敗則事務回滾,不會往新訂單繳費隊列中寫消息;
再好比User service 扣費成功後,往新訂單轉移票隊列寫消息,這時Ticket service 正在處理中或者處理中發生了失敗,這中間的過程當中用戶查看本身的餘額已經扣費成功,但票的信息卻沒有,此時可使用事務失敗回滾的方式依次回退,這種叫弱一致性;又或者能夠把處理失敗的內容發送至一個錯誤隊列中,由人工處理等方式解決,這種叫最終一致性。

Spring JTA分佈式事務實現

  • 可使用如JBoss之類的應用服務器提供的JTA事務管理器
  • 可使用Atomikos、Bitronix等庫提供的JTA事務管理器

不使用Spring JTA的分佈式事務實現

爲何不使用JTA?

由於JTA採用兩階段提交方式,第一次是預備階段,第二次纔是正式提交。當第一次提交出現錯誤,則整個事務出現回滾,一個事務的時間可能會較長,由於它要跨越多個數據庫多個數據資源的的操做,因此在性能上可能會形成吞吐量低。spring

不適用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
複製代碼

這時候就會出現問題sql

多個資源的事務同步方法

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成功;如何避免,須要忽略重發消息,好比惟一性校驗等手段。bash

鏈式事務管理

  • 定義一個事務鏈
  • 多個事務在一個事務管理器裏依次提交
  • 可能出錯

如何選擇(根據一致性要求)

  • 強一致性事務: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
複製代碼

自定義配置類文件app

@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的方式,使其在一個事務裏面執行

也就是同步到Transaction Manager上面,可是這邊的同步不是說事務的同步,只是同步數據庫鏈接的開關

特別說明: @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

git代碼地址

分佈式系統惟一性

什麼是分佈式系統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'
複製代碼

相關文章
相關標籤/搜索