分佈式事務服務 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>
場景介紹
- 首先我們假想這樣一種場景:轉賬服務,從銀行 A 某個賬戶轉 100 元錢到銀行 B 的某個賬戶,銀行 A 和銀行 B 可以認爲是兩個單獨的系統,也就是兩套單獨的數據庫。
- 我們將賬戶系統簡化成只有賬戶和餘額 2 個字段,並且爲了適應 DTS 的兩階段設計要求,業務上又增加了一個凍結金額(凍結金額是指在一筆轉賬期間,在一階段的時候使用該字段臨時存儲轉賬金額,該轉賬額度不能被使用,只有等這筆分佈式事務全部提交成功時,纔會真正的計入可用餘額)。按這樣的設計,用戶的可用餘額等於賬戶餘額減去凍結金額。這點是理解參與者設計的關鍵,也是 DTS 保證最終一致的業務約束。
- 同時爲了記錄賬戶操作明細,我們設計了一張賬戶流水錶用來記錄每次賬戶的操作明細,所以領域對象簡單設計如下:
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 個參與者,達到分佈式事務下數據的最終一致性的。相比參與者,發起方的配置要複雜一些。
- 在發起方自己的數據庫裏創建 DTS 的表
- 配置 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>
- 配置參與者服務和攔截器。如果是在 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>
- 發起分佈式事務
啓動分佈式事務的入口方法
/**
* 啓動一個業務活動。
*
* 爲了保證業務活動的唯一性,對同樣的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