支付寶分佈式事務服務DTS

分佈式事務服務 DTS二

如何玩轉 DTS,基本上使用 DTS 對發起方的配置要求會多一點。

添加 DTS 的依賴

NOTE: 發起方和參與方都需要添加依賴。

如果使用 SOFA Lite,只需按照樣例工程裏的方式添加依賴:

<dependency>
    <groupId>com.alipay.sofa</groupId>
    <artifactId>slite-starter-xts</artifactId>
</dependency>

如果沒有使用 SOFA Lite,那麼需要在 pom 配置里加上 DTS 的依賴:

<dependency>
    <groupId>com.alipay.xts</groupId>
    <artifactId>xts-core</artifactId>
    <version>6.0.8</version>
</dependency>
<dependency>
    <groupId>com.alipay.xts</groupId>
    <artifactId>xts-adapter-sofa</artifactId>
    <version>6.0.8</version>
</dependency>

場景介紹

  1. 首先我們假想這樣一種場景:轉賬服務,從銀行 A 某個賬戶轉 100 元錢到銀行 B 的某個賬戶,銀行 A 和銀行 B 可以認爲是兩個單獨的系統,也就是兩套單獨的數據庫。
  2. 我們將賬戶系統簡化成只有賬戶和餘額 2 個字段,並且爲了適應 DTS 的兩階段設計要求,業務上又增加了一個凍結金額(凍結金額是指在一筆轉賬期間,在一階段的時候使用該字段臨時存儲轉賬金額,該轉賬額度不能被使用,只有等這筆分佈式事務全部提交成功時,纔會真正的計入可用餘額)。按這樣的設計,用戶的可用餘額等於賬戶餘額減去凍結金額。這點是理解參與者設計的關鍵,也是 DTS 保證最終一致的業務約束。
  3. 同時爲了記錄賬戶操作明細,我們設計了一張賬戶流水錶用來記錄每次賬戶的操作明細,所以領域對象簡單設計如下:
public class Account {
    /**
     * 賬戶
     */
    private String accountNo;
    /**
     * 餘額
     */
    private double amount;
    /**
     * 凍結金額
     */
    private double freezedAmount;
public class AccountTransaction {
    /**
     * 事務id
     */
    private String txId;
    /**
     * 操作賬戶
     */
    private String accountNo;
    /**
     * 操作金額
     */
    private double amount;
    /**
     * 操作類型,扣帳還是入賬
     */
    private String type;

A 銀行參與者

我們假設需要從 A 賬戶扣 100 元錢,所以 A 系統提供了一個扣帳的服務,對應扣帳的一階段接口和相應的二階段接口如下:

/**
 * A銀行參與者,執行扣帳操作
 * @version $Id: FirstAction.java, v 0.1 2014年9月22日 下午5:32:59 Exp $
 */
public interface FirstAction {
  /**
   * 一階段方法,注意要打上xts的標註哦
   * 
   * @param businessActionContext
   * @param accountNo
   * @param amount
   */
  @TwoPhaseBusinessAction(name = "firstAction", commitMethod = "commit", rollbackMethod = "rollback")
  public void prepare_minus(BusinessActionContext businessActionContext,String accountNo,double amount);
  /**
   * 二階段的提交方法
   * @param businessActionContext
   * @return
   */
  public boolean commit(BusinessActionContext businessActionContext);
  /**
   * 二階段的回滾方法
   * @param businessActionContext
   * @return
   */
  public boolean rollback(BusinessActionContext businessActionContext);
}

對應的一階段扣帳實現

public void prepare_minus(final BusinessActionContext businessActionContext,
                          final String accountNo, final double amount) {
    transactionTemplate.execute(new TransactionCallback() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
            try {
                try {
                        //鎖定賬戶
                        Account account = accountDAO.getAccount(accountNo);
                        if (account.getAmount() - amount < 0) {
                            throw new TransactionFailException("餘額不足");
                        }
                        //先記一筆賬戶操作流水
                        AccountTransaction accountTransaction = new AccountTransaction();
                        accountTransaction.setTxId(businessActionContext.getTxId());
                        accountTransaction.setAccountNo(accountNo);
                        accountTransaction.setAmount(amount);
                        accountTransaction.setType("minus");
                        //初始狀態,如果提交則更新爲C狀態,如果失敗則刪除記錄
                        accountTransaction.setStatus("I");
                        accountTransactionDAO.addTransaction(accountTransaction);
                        //再遞增凍結金額,表示這部分錢已經被凍結,不能使用
                        double freezedAmount = account.getFreezedAmount() + amount;
                        account.setFreezedAmount(freezedAmount);
                        accountDAO.updateFreezedAmount(account);
                    } catch (Exception e) {
                        System.out.println("一階段異常," + e);
                        throw new TransactionFailException("一階段操作失敗", e);
                    }
            return null;
        }
    });
}

對應的二階段提交操作

public boolean commit(final BusinessActionContext businessActionContext) {
    transactionTemplate.execute(new TransactionCallback() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
            try {
                    //找到賬戶操作流水
                    AccountTransaction accountTransaction = accountTransactionDAO
                        .findTransaction(businessActionContext.getTxId());
                    //事務數據被刪除了
                    if (accountTransaction == null) {
                        throw new TransactionFailException("事務信息被刪除");
                    }
                    //重複提交冪等保證只做一次
                    if (StringUtils.equalsIgnoreCase("C", accountTransaction.getStatus())) {
                        return true;
                    }
                    Account account = accountDAO.getAccount(accountTransaction.getAccountNo());
                    //扣錢
                    double amount = account.getAmount() - accountTransaction.getAmount();
                    if (amount < 0) {
                        throw new TransactionFailException("餘額不足");
                    }
                    account.setAmount(amount);
                    accountDAO.updateAmount(account);
                    //凍結金額相應減少
                    account.setFreezedAmount(account.getFreezedAmount()
                                             - accountTransaction.getAmount());
                    accountDAO.updateFreezedAmount(account);
                    //事務成功之後更新爲C
                    accountTransactionDAO.updateTransaction(businessActionContext.getTxId(), "C");
                } catch (Exception e) {
                    System.out.println("二階段異常," + e);
                    throw new TransactionFailException("二階段操作失敗", e);
                }
            return null;
        }
    });
    return false;
}

對應的二階段回滾操作

public boolean rollback(final BusinessActionContext businessActionContext) {
    transactionTemplate.execute(new TransactionCallback() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
            try {
                    //回滾凍結金額
                    AccountTransaction accountTransaction = accountTransactionDAO
                        .findTransaction(businessActionContext.getTxId());
                    if (accountTransaction == null) {
                        System.out.println("二階段---空回滾成功");
                        return null;
                    }
                    Account account = accountDAO.getAccount(accountTransaction.getAccountNo());
                    account.setFreezedAmount(account.getFreezedAmount()
                                             - accountTransaction.getAmount());
                    accountDAO.updateFreezedAmount(account);
                    //刪除流水
                    accountTransactionDAO.deleteTransaction(businessActionContext.getTxId());
                } catch (Exception e) {
                    System.out.println("二階段異常," + e);
                    throw new TransactionFailException("二階段操作失敗", e);
                }
              return null;
        }
   });
   return false;
}

B 銀行參與者

我們假設需要對 B 賬戶入賬 100 元錢,所以 B 系統提供了一個入賬的服務,對應入賬的一階段接口和相應的二階段接口基本和 A 銀行參與者類似,這裏不多做介紹,可以直接查看樣例工程下的 xts-sample 工程代碼。

發起方

前面介紹了參與者的實現細節,接下來看看發起方系統是如何協調這 2 個參與者,達到分佈式事務下數據的最終一致性的。相比參與者,發起方的配置要複雜一些。

  1. 在發起方自己的數據庫裏創建 DTS 的表
  2. 配置 BusinessActivityControlService

BusinessActivityControlService 是 DTS 分佈式事務的啓動類,在 SOFA 環境中,我們可以這樣使用

<!-- 分佈式事務的服務,用來發起分佈式事務 -->
<sofa:xts id="businessActivityControlService">
  <!-- 發起方自己的數據源,建議使用zdal數據源組件,這裏簡單使用dbcp數據源 -->
   <sofa:datasource ref="activityDataSource"/>
  <!-- 如果使用zdal數據源,可以不用配置這個屬性,這個dbType是用來區分目標庫的類型,以方便xts設置sqlmap -->
   <sofa:dbtype value="mysql"/>
</sofa:xts>

在其他環境中,我們也可以將它配置成一個普通 Bean,配置如下

<!-- 分佈式事務的服務,用來發起分佈式事務 -->
<bean name="businessActivityControlService" class="com.alipay.xts.client.api.impl.sofa.BusinessActivityControlServiceImplSofa">
   <!-- 發起方自己的數據源,建議使用zdal數據源組件,這裏簡單使用dbcp數據源 -->
   <property name="dataSource" ref="activityDataSource"/>
   <!-- 如果使用zdal數據源,可以不用配置這個屬性,這個dbType是用來區分目標庫的類型,以方便xts設置sqlmap -->
   <property name="dbType" value="mysql"/>
</bean>
  1. 配置參與者服務和攔截器。如果是在 SOFA 環境中,DTS 框架會自動攔截參與者方法,攔截器就不用配置了
<!-- 第一個參與者的代理 -->
<bean id="firstAction" class="org.springframework.aop.framework.ProxyFactoryBean">
   <property name="proxyInterfaces" value="com.alipay.xts.client.sample.action.FirstAction"/>
   <property name="target" ref="firstActionTarget"/>
   <property name="interceptorNames">
      <list>
          <value>businessActionInterceptor</value>
      </list>
   </property>
</bean>
<!-- 第一個參與者 -->
<bean id="firstActionTarget" class="com.alipay.xts.client.sample.action.impl.FirstActionImpl">
   <property name="accountTransactionDAO">
      <ref bean="firstActionAccountTransactionDAO" />
   </property>
   <property name="accountDAO">
      <ref bean="firstActionAccountDAO" />
   </property>
   <property name="transactionTemplate">
      <ref bean="firstActionTransactionTemplate" />
   </property>
</bean>
<!-- 第二個參與者的代理 -->
<bean id="secondAction" class="org.springframework.aop.framework.ProxyFactoryBean">
   <property name="proxyInterfaces" value="com.alipay.xts.client.sample.action.SecondAction"/>
   <property name="target" ref="secondActionTarget"/>
   <property name="interceptorNames">
     <list>
        <value>businessActionInterceptor</value>
     </list>
   </property>
</bean>
<!-- 第二個參與者 -->
<bean id="secondActionTarget" class="com.alipay.xts.client.sample.action.impl.SecondActionImpl">
   <property name="accountTransactionDAO">
      <ref bean="secondActionAccountTransactionDAO" />
   </property>
   <property name="accountDAO">
      <ref bean="secondActionAccountDAO" />
   </property>
   <property name="transactionTemplate">
      <ref bean="secondActionTransactionTemplate" />
   </property>
</bean>
<!-- 攔截器,在參與者調用前生效,插入參與者的action記錄 -->
<bean id="businessActionInterceptor"
     class="com.alipay.sofa.platform.xts.bacs.integration.BusinessActionInterceptor">
   <property name="businessActivityControlService" ref="businessActivityControlService"/>
</bean>
  1. 發起分佈式事務

啓動分佈式事務的入口方法

/**
 * 啓動一個業務活動。
 * 
 * 爲了保證業務活動的唯一性,對同樣的businessType與businessId,只能有一次成功記錄。
 * 
 * 系統允許多次調用start方式啓動業務活動,如果當前業務活動已經存在,再次啓動業務活動不會有任何效果,也不會檢查業務類型與業務號是否匹配。
 * 
 * @param businessType 業務類型,由業務系統自定義,比如'trade_pay'代表交易支付
 * @param businessId 業務號,如交易號
 * @notice 事務號的格式爲: businessType+"-"+businessId,總長度爲128
 * @return 
 */
BusinessActivityId start(String businessType, String businessId, Map<String, Object> properties);

businessType + businessId 就是最終的事務號,properties 可以讓發起方設置一些全局的事務上下文信息。

轉賬服務發起分佈式事務

/**
 * 執行轉賬操作
 * 
 * @param from
 * @param to
 * @param amount
 */
public void transfer(final String from, final String to, final double amount) {
    /**
     * 注意:開啓xts服務必須包含在發起方的本地事務模版中
     */
    transactionTemplate.execute(new TransactionCallback() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
           System.out.println("開始啓動xts分佈式事務活動");
                //啓動分佈式事務,第三個是分佈式事務的全局上下文信息
                Map<String, Object> properties = new HashMap<String, Object>();
                BusinessActivityId businessActivityId = businessActivityControlService.start("pay",
                    businessId, properties);
                System.out.println("=====啓動分佈式事務成功,事務號:" + businessActivityId.toStringForm()
                                   + "=====");
                System.out.println("=====一階段,準備從B銀行執行入賬操作=====");
                //第二個參與者入賬操作
                if (secondAction.prepare_add(null, to, amount)) {
                    System.out.println("=====一階段,從B銀行執行入賬操作成功=====");
                } else {
                    System.out.println("=====一階段,從B銀行執行入賬操作失敗,準備回滾=====");
                    status.setRollbackOnly();
                    return null;
                }
                System.out.println("=====一階段,準備從A銀行執行扣賬操作=====");
                //第一個參與者扣賬操作
                if (firstAction.prepare_minus(null, from, amount)) {
                    System.out.println("=====一階段,從A銀行執行扣賬操作成功=====");
                } else {
                    System.out.println("=====一階段,從A銀行執行扣賬操作失敗,準備回滾=====");
                    status.setRollbackOnly();
                }
            return null;
        }
    });
    System.out.println("二階段----轉賬成功,錢已到位");
}

小結

使用 DTS 開發需要關注的就是以上內容。對於參與者來說,最關鍵的是業務上如何實現兩階段處理來保證最終一致性,對於發起方來說,主要是要配置 DTS 的表。

 

http://blog.csdn.net/qq_27384769/article/details/79303942

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章