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

蔣老溼發表於2018-10-01

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

分散式事務介紹

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

  • 一致性
  • 可用性
  • 分割槽容錯性

是分散式事務主要是保證資料的一致性,主要有三種不同的原則

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

JTA與XA

共同點:

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

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

分散式事務之事務實現模式與技術(四)
Orderservice監聽新訂單佇列中的訊息,獲取之後新增訂單,成功則往新訂單繳費佇列中寫訊息,中間新增訂單的過程使用JTA事務管理,當新增失敗則事務回滾,不會往新訂單繳費佇列中寫訊息;
再比如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的方式,使其在一個事務裡面執行

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

分散式事務之事務實現模式與技術(四)
也就是同步到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'
複製程式碼

相關文章