本文主要基于 TCC-Transaction 1.2.3.3 正式版
- 概述
- 2. 实体结构
2.1 商城服务
2.2 资金服务
2.3 红包服务
4.1 Try 阶段
4.2 Confirm / Cancel 阶段
友情提示:欢迎关注公众号【芋道源码】。😈关注后,拉你进【源码圈】微信群和【芋艿】搞基嗨皮。
1. 概述
本文分享 TCC 项目实战。以官方 Maven项目
tcc-transaction-http-sample
为例子(
tcc-transaction-dubbo-sample
类似 )。
建议你已经成功启动了该项目。如果不知道如何启动,可以先查看《TCC-Transaction 源码分析 —— 调试环境搭建》。如果再碰到问题,欢迎加微信公众号( 芋道源码 ),我会一一仔细回复。
OK,首先我们简单了解下这个项目。
- 首页 = 商品列表 = 确认支付页 = 支付结果页
- 使用账户余额 + 红包余额联合支付购买商品,并账户之间转账。
项目拆分三个子 Maven 项目:
tcc-transaction-http-order
:商城服务,提供商品和商品订单逻辑。tcc-transaction-http-capital
:资金服务,提供账户余额逻辑。tcc-transaction-http-redpacket
:红包服务,提供红包余额逻辑。
你行好事会因为得到赞赏而愉悦 同理,开源项目贡献者会因为 Star 而更加有动力 为 TCC-Transaction 点赞!传送门
2. 实体结构
2.1 商城服务
1 2 3 4 5 6 7 8 9 10 | public class Shop { /** * 商店编号 */ private long id; /** * 所有者用户编号 */ private long ownerUserId; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class Product implements Serializable { /** * 商品编号 */ private long productId; /** * 商店编号 */ private long shopId; /** * 商品名 */ private String productName; /** * 单价 */ private BigDecimal price; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | public class Order implements Serializable{ private static final long serialVersionUID = -5908730245224893590L; /** * 订单编号 */ private long id; /** * 支付( 下单 )用户编号 */ private long payerUserId; /** * 收款( 商店拥有者 )用户编号 */ private long payeeUserId; /** * 红包支付金额 */ private BigDecimal redPacketPayAmount; /** * 账户余额支付金额 */ private BigDecimal capitalPayAmount; /** * 订单状态 * - DRAFT :草稿 * - PAYING :支付中 * - CONFIRMED :支付成功 * - PAY_FAILED :支付失败 */ private String status = "DRAFT"; /** * 商户订单号,使用 UUID 生成 */ private String merchantOrderNo; /** * 订单明细数组 * 非存储字段 */ private List<OrderLine> orderLines = new ArrayList<OrderLine>(); } |
/**
- 订单编号
/
private long id;
/*
- 支付( 下单 )用户编号
/
private long payerUserId;
/*
- 收款( 商店拥有者 )用户编号
/
private long payeeUserId;
/*
- 红包支付金额
/
private BigDecimal redPacketPayAmount;
/*
- 账户余额支付金额
/
private BigDecimal capitalPayAmount;
/*
- 订单状态
- DRAFT :草稿
- PAYING :支付中
- CONFIRMED :支付成功
- PAY_FAILED :支付失败
/
private String status = “DRAFT”;
/*
- 商户订单号,使用 UUID 生成
*/
private String merchantOrderNo;
/**
- 订单明细数组
- 非存储字段
*/
private List<OrderLine> orderLines = new ArrayList<OrderLine>();
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class OrderLine implements Serializable { private static final long serialVersionUID = 2300754647209250837L; /** * 订单编号 */ private long id; /** * 商品编号 */ private long productId; /** * 数量 */ private int quantity; /** * 单价 */ private BigDecimal unitPrice; } |
/**
- 订单编号
/
private long id;
/*
- 商品编号
/
private long productId;
/*
- 数量
/
private int quantity;
/*
- 单价
*/
private BigDecimal unitPrice;
}
public interface CapitalAccountService {
BigDecimal getCapitalAccountByUserId(long userId);
}
public interface CapitalTradeOrderService {
String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto);
}
public interface RedPacketAccountService {
BigDecimal getRedPacketAccountByUserId(long userId);
}
public interface RedPacketTradeOrderService {
String record(TransactionContext transactionContext, RedPacketTradeOrderDto tradeOrderDto);
}
public class Product implements Serializable {
/**
- 商品编号
/
private long productId;
/*
- 商店编号
/
private long shopId;
/*
- 商品名
/
private String productName;
/*
- 单价
*/
private BigDecimal price;
}
1234567891011121314151617181920
public class OrderLine implements Serializable {private static final long serialVersionUID = 2300754647209250837L; /** * 订单编号 */private long id;/** * 商品编号 */private long productId;/** * 数量 */private int quantity;/** * 单价 */private BigDecimal unitPrice;}
public class OrderLine implements Serializable {
private static final long serialVersionUID = 2300754647209250837L;
/**
- 订单编号
/
private long id;
/*
- 商品编号
/
private long productId;
/*
- 数量
/
private int quantity;
/*
- 单价
*/
private BigDecimal unitPrice;
}
下单时,插入订单状态为
"DRAFT"
的订单( Order )记录,并插入购买的商品订单明细( OrderLine )记录。支付时,更新订单状态为
"PAYING"
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class CapitalAccount { /** * 账户编号 */ private long id; /** * 用户编号 */ private long userId; /** * 余额 */ private BigDecimal balanceAmount; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public class TradeOrder { /** * 交易订单编号 */ private long id; /** * 转出用户编号 */ private long selfUserId; /** * 转入用户编号 */ private long oppositeUserId; /** * 商户订单号 */ private String merchantOrderNo; /** * 金额 */ private BigDecimal amount; /** * 交易订单状态 * - DRAFT :草稿 * - CONFIRM :交易成功 * - CANCEL :交易取消 */ private String status = "DRAFT"; } |
public class CapitalAccount {
/**
- 账户编号
/
private long id;
/*
- 用户编号
/
private long userId;
/*
- 余额
*/
private BigDecimal balanceAmount;
}
业务逻辑:
2.3 红包服务
关系较为简单,和资金服务 99.99% 相同,有两个实体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class RedPacketAccount { /** * 账户编号 */ private long id; /** * 用户编号 */ private long userId; /** * 余额 */ private BigDecimal balanceAmount; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public class TradeOrder { /** * 交易订单编号 */ private long id; /** * 转出用户编号 */ private long selfUserId; /** * 转入用户编号 */ private long oppositeUserId; /** * 商户订单号 */ private String merchantOrderNo; /** * 金额 */ private BigDecimal amount; /** * 交易订单状态 * - DRAFT :草稿 * - CONFIRM :交易成功 * - CANCEL :交易取消 */ private String status = "DRAFT"; } |
1234567891011121314151617181920212223242526272829
public class TradeOrder {/** * 交易订单编号 */private long id;/** * 转出用户编号 */private long selfUserId;/** * 转入用户编号 */private long oppositeUserId;/** * 商户订单号 */private String merchantOrderNo;/** * 金额 */private BigDecimal amount;/** * 交易订单状态 * - DRAFT :草稿 * - CONFIRM :交易成功 * - CANCEL :交易取消 */private String status = "DRAFT";}
public class TradeOrder {
/**
- 交易订单编号
/
private long id;
/*
- 转出用户编号
/
private long selfUserId;
/*
- 转入用户编号
/
private long oppositeUserId;
/*
- 商户订单号
/
private String merchantOrderNo;
/*
- 金额
/
private BigDecimal amount;
/*
- 交易订单状态
- DRAFT :草稿
- CONFIRM :交易成功
- CANCEL :交易取消
*/
private String status = “DRAFT”;
}
订单支付支付中,插入交易订单状态为
"DRAFT"
的订单( TradeOrder )记录,并更新减少下单用户的红包账户余额。
**红包服务和资金服务为商城服务提供调用( 以资金服务为例子 )**:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // appcontext-service-provider.xml bean name="capitalAccountRepository" class="org.mengyun.tcctransaction.sample.http.capital.domain.repository.CapitalAccountRepository"/ bean name="tradeOrderRepository" class="org.mengyun.tcctransaction.sample.http.capital.domain.repository.TradeOrderRepository"/ bean name="capitalTradeOrderService" class="org.mengyun.tcctransaction.sample.http.capital.service.CapitalTradeOrderServiceImpl"/ bean name="capitalAccountService" class="org.mengyun.tcctransaction.sample.http.capital.service.CapitalAccountServiceImpl"/ bean name="capitalTradeOrderServiceExporter" class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter" property name="service" ref="capitalTradeOrderService"/ property name="serviceInterface" value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService"/ /bean bean name="capitalAccountServiceExporter" class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter" property name="service" ref="capitalAccountService"/ property name="serviceInterface" value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService"/ /bean bean id="httpServer" class="org.springframework.remoting.support.SimpleHttpServerFactoryBean" property name="contexts" util:map entry key="/remoting/CapitalTradeOrderService" value-ref="capitalTradeOrderServiceExporter"/ entry key="/remoting/CapitalAccountService" value-ref="capitalAccountServiceExporter"/ /util:map /property property name="port" value="8081"/ /bean |
bean name=”capitalAccountRepository”
class=”org.mengyun.tcctransaction.sample.http.capital.domain.repository.CapitalAccountRepository”/
bean name=”tradeOrderRepository”
class=”org.mengyun.tcctransaction.sample.http.capital.domain.repository.TradeOrderRepository”/
bean name=”capitalTradeOrderService”
class=”org.mengyun.tcctransaction.sample.http.capital.service.CapitalTradeOrderServiceImpl”/
bean name=”capitalAccountService”
class=”org.mengyun.tcctransaction.sample.http.capital.service.CapitalAccountServiceImpl”/
bean name=”capitalTradeOrderServiceExporter”
class=”org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter”
property name=”service” ref=”capitalTradeOrderService”/
property name=”serviceInterface”
value=”org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService”/
/bean
bean name=”capitalAccountServiceExporter”
class=”org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter”
property name=”service” ref=”capitalAccountService”/
property name=”serviceInterface”
value=”org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService”/
/bean
bean id=”httpServer”
class=”org.springframework.remoting.support.SimpleHttpServerFactoryBean”
property name=”contexts”
util:map
entry key=”/remoting/CapitalTradeOrderService” value-ref=”capitalTradeOrderServiceExporter”/
entry key=”/remoting/CapitalAccountService” value-ref=”capitalAccountServiceExporter”/
/util:map
/property
property name=”port” value=”8081”/
/bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class CapitalAccountServiceImpl implements CapitalAccountService { @Autowired CapitalAccountRepository capitalAccountRepository; @Override public BigDecimal getCapitalAccountByUserId(long userId) { return capitalAccountRepository.findByUserId(userId).getBalanceAmount(); } } public class CapitalAccountServiceImpl implements CapitalAccountService { @Autowired CapitalAccountRepository capitalAccountRepository; @Override public BigDecimal getCapitalAccountByUserId(long userId) { return capitalAccountRepository.findByUserId(userId).getBalanceAmount(); } } |
@Override
public BigDecimal getCapitalAccountByUserId(long userId) {
return capitalAccountRepository.findByUserId(userId).getBalanceAmount();
}
}
public class CapitalAccountServiceImpl implements CapitalAccountService {
@Autowired
CapitalAccountRepository capitalAccountRepository;
@Override
public BigDecimal getCapitalAccountByUserId(long userId) {
return capitalAccountRepository.findByUserId(userId).getBalanceAmount();
}
}
public class CapitalAccountServiceImpl implements CapitalAccountService {
@Autowired
CapitalAccountRepository capitalAccountRepository;
@Override
public BigDecimal getCapitalAccountByUserId(long userId) {
return capitalAccountRepository.findByUserId(userId).getBalanceAmount();
}
}
public class CapitalAccountServiceImpl implements CapitalAccountService {
@Autowired
CapitalAccountRepository capitalAccountRepository;
@Override
public BigDecimal getCapitalAccountByUserId(long userId) {
return capitalAccountRepository.findByUserId(userId).getBalanceAmount();
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // appcontext-service-consumer.xml bean id="httpInvokerRequestExecutor" class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor" property name="httpClient" bean class="org.apache.commons.httpclient.HttpClient" property name="httpConnectionManager" ref bean="multiThreadHttpConnectionManager"/ /property /bean /property /bean bean id="multiThreadHttpConnectionManager" class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager" property name="params" bean class="org.apache.commons.httpclient.params.HttpConnectionManagerParams" property name="connectionTimeout" value="200000"/ property name="maxTotalConnections" value="600"/ property name="defaultMaxConnectionsPerHost" value="512"/ property name="soTimeout" value="5000"/ /bean /property /bean bean id="captialTradeOrderService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean" property name="serviceUrl" value="http://localhost:8081/remoting/CapitalTradeOrderService"/ property name="serviceInterface" value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService"/ property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/ /bean bean id="capitalAccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean" property name="serviceUrl" value="http://localhost:8081/remoting/CapitalAccountService"/ property name="serviceInterface" value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService"/ property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/ /bean bean id="redPacketAccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean" property name="serviceUrl" value="http://localhost:8082/remoting/RedPacketAccountService"/ property name="serviceInterface" value="org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketAccountService"/ property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/ /bean bean id="redPacketTradeOrderService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean" property name="serviceUrl" value="http://localhost:8082/remoting/RedPacketTradeOrderService"/ property name="serviceInterface" value="org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketTradeOrderService"/ property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/ /bean |
bean id=”httpInvokerRequestExecutor”
class=”org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor”
property name=”httpClient”
bean class=”org.apache.commons.httpclient.HttpClient”
property name=”httpConnectionManager”
ref bean=”multiThreadHttpConnectionManager”/
/property
/bean
/property
/bean
bean id=”multiThreadHttpConnectionManager”
class=”org.apache.commons.httpclient.MultiThreadedHttpConnectionManager”
property name=”params”
bean class=”org.apache.commons.httpclient.params.HttpConnectionManagerParams”
property name=”connectionTimeout” value=”200000”/
property name=”maxTotalConnections” value=”600”/
property name=”defaultMaxConnectionsPerHost” value=”512”/
property name=”soTimeout” value=”5000”/
/bean
/property
/bean
bean id=”captialTradeOrderService” class=”org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean”
property name=”serviceUrl” value=”http://localhost:8081/remoting/CapitalTradeOrderService"/
property name=”serviceInterface”
value=”org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService”/
property name=”httpInvokerRequestExecutor” ref=”httpInvokerRequestExecutor”/
/bean
bean id=”capitalAccountService” class=”org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean”
property name=”serviceUrl” value=”http://localhost:8081/remoting/CapitalAccountService"/
property name=”serviceInterface”
value=”org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService”/
property name=”httpInvokerRequestExecutor” ref=”httpInvokerRequestExecutor”/
/bean
bean id=”redPacketAccountService” class=”org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean”
property name=”serviceUrl” value=”http://localhost:8082/remoting/RedPacketAccountService"/
property name=”serviceInterface”
value=”org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketAccountService”/
property name=”httpInvokerRequestExecutor” ref=”httpInvokerRequestExecutor”/
/bean
bean id=”redPacketTradeOrderService” class=”org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean”
property name=”serviceUrl” value=”http://localhost:8082/remoting/RedPacketTradeOrderService"/
property name=”serviceInterface”
value=”org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketTradeOrderService”/
property name=”httpInvokerRequestExecutor” ref=”httpInvokerRequestExecutor”/
/bean
public interface CapitalAccountService {
BigDecimal getCapitalAccountByUserId(long userId);
}
public interface CapitalTradeOrderService {
String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto);
}
public interface RedPacketAccountService {
BigDecimal getRedPacketAccountByUserId(long userId);
}
public interface RedPacketTradeOrderService {
String record(TransactionContext transactionContext, RedPacketTradeOrderDto tradeOrderDto);
}
// appcontext-service-consumer.xml
bean id=”httpInvokerRequestExecutor”
class=”org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor”
property name=”httpClient”
bean class=”org.apache.commons.httpclient.HttpClient”
property name=”httpConnectionManager”
ref bean=”multiThreadHttpConnectionManager”/
/property
/bean
/property
/bean
bean id=”multiThreadHttpConnectionManager”
class=”org.apache.commons.httpclient.MultiThreadedHttpConnectionManager”
property name=”params”
bean class=”org.apache.commons.httpclient.params.HttpConnectionManagerParams”
property name=”connectionTimeout” value=”200000”/
property name=”maxTotalConnections” value=”600”/
property name=”defaultMaxConnectionsPerHost” value=”512”/
property name=”soTimeout” value=”5000”/
/bean
/property
/bean
bean id=”captialTradeOrderService” class=”org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean”
property name=”serviceUrl” value=”http://localhost:8081/remoting/CapitalTradeOrderService"/
property name=”serviceInterface”
value=”org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService”/
property name=”httpInvokerRequestExecutor” ref=”httpInvokerRequestExecutor”/
/bean
bean id=”capitalAccountService” class=”org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean”
property name=”serviceUrl” value=”http://localhost:8081/remoting/CapitalAccountService"/
property name=”serviceInterface”
value=”org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService”/
property name=”httpInvokerRequestExecutor” ref=”httpInvokerRequestExecutor”/
/bean
bean id=”redPacketAccountService” class=”org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean”
property name=”serviceUrl” value=”http://localhost:8082/remoting/RedPacketAccountService"/
property name=”serviceInterface”
value=”org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketAccountService”/
property name=”httpInvokerRequestExecutor” ref=”httpInvokerRequestExecutor”/
/bean
bean id=”redPacketTradeOrderService” class=”org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean”
property name=”serviceUrl” value=”http://localhost:8082/remoting/RedPacketTradeOrderService"/
property name=”serviceInterface”
value=”org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketTradeOrderService”/
property name=”httpInvokerRequestExecutor” ref=”httpInvokerRequestExecutor”/
/bean
4. 下单支付流程
ps:数据访问的方法,请自己拉取代码,使用 IDE 查看。谢谢。🙂
下单支付流程,整体流程如下图( 打开大图 ):
点击【支付】按钮,下单支付流程。实现代码如下:
@Controller
@RequestMapping("")
public class OrderController {
@RequestMapping(value = "/placeorder", method = RequestMethod.POST)
public ModelAndView placeOrder(@RequestParam String redPacketPayAmount,
@RequestParam long shopId,
@RequestParam long payerUserId,
@RequestParam long productId) {
PlaceOrderRequest request = buildRequest(redPacketPayAmount, shopId, payerUserId, productId);
// 下单并支付订单
String merchantOrderNo = placeOrderService.placeOrder(request.getPayerUserId(), request.getShopId(),
request.getProductQuantities(), request.getRedPacketPayAmount());
// 返回
ModelAndView mv = new ModelAndView("pay_success");
// 查询订单状态
String status = orderService.getOrderStatusByMerchantOrderNo(merchantOrderNo);
// 支付结果提示
String payResultTip = null;
if ("CONFIRMED".equals(status)) {
payResultTip = "支付成功";
} else if ("PAY_FAILED".equals(status)) {
payResultTip = "支付失败";
}
mv.addObject("payResult", payResultTip);
// 商品信息
mv.addObject("product", productRepository.findById(productId));
// 资金账户金额 和 红包账户金额
mv.addObject("capitalAmount", accountService.getCapitalAccountByUserId(payerUserId));
mv.addObject("redPacketAmount", accountService.getRedPacketAccountByUserId(payerUserId));
return mv;
}
}
- 调用
PlaceOrderService#placeOrder(...)
方法,下单并支付订单。 - 调用
OrderService#getOrderStatusByMerchantOrderNo(...)
方法,查询订单状态。
调用
PlaceOrderService#placeOrder(...)
方法,下单并支付订单。实现代码如下:
@Service
public class PlaceOrderServiceImpl {
public String placeOrder(long payerUserId, long shopId, ListPairLong, Integer productQuantities, BigDecimal redPacketPayAmount) {
// 获取商店
Shop shop = shopRepository.findById(shopId);
// 创建订单
Order order = orderService.createOrder(payerUserId, shop.getOwnerUserId(), productQuantities);
// 发起支付
Boolean result = false;
try {
paymentService.makePayment(order, redPacketPayAmount, order.getTotalAmount().subtract(redPacketPayAmount));
} catch (ConfirmingException confirmingException) {
// exception throws with the tcc transaction status is CONFIRMING,
// when tcc transaction is confirming status,
// the tcc transaction recovery will try to confirm the whole transaction to ensure eventually consistent.
result = true;
} catch (CancellingException cancellingException) {
// exception throws with the tcc transaction status is CANCELLING,
// when tcc transaction is under CANCELLING status,
// the tcc transaction recovery will try to cancel the whole transaction to ensure eventually consistent.
} catch (Throwable e) {
// other exceptions throws at TRYING stage.
// you can retry or cancel the operation.
e.printStackTrace();
}
return order.getMerchantOrderNo();
}
}
1 2 3 4 5 6 7 8 9 | @Service public class OrderServiceImpl { @Transactional public Order createOrder(long payerUserId, long payeeUserId, List<Pair<Long, Integer>> productQuantities) { Order order = orderFactory.buildOrder(payerUserId, payeeUserId, productQuantities); orderRepository.createOrder(order); return order; } } |
调用
OrderService#createOrder(...)
方法,创建订单状态为
"DRAFT"
的商城订单。实际业务不会这么做,此处仅仅是例子,简化流程。实现代码如下:
@Compensable(confirmMethod = "confirmMakePayment", cancelMethod = "cancelMakePayment")
@Transactional
public void makePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
System.out.println("order try make payment called.time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
// 更新订单状态为支付中
order.pay(redPacketPayAmount, capitalPayAmount);
orderRepository.updateOrder(order);
// 资金账户余额支付订单
String result = tradeOrderServiceProxy.record(null, buildCapitalTradeOrderDto(order));
// 红包账户余额支付订单
String result2 = tradeOrderServiceProxy.record(null, buildRedPacketTradeOrderDto(order));
}
生产代码对于异常需要进一步处理。
调用
PaymentService#makePayment(...)
方法,发起 Try 流程,实现代码如下:
-
- 事务传播级别 Propagation.REQUIRED ( **默认值** )
- 设置 `confirmMethod` / `cancelMethod` 方法名
- 事务上下文编辑类 DefaultTransactionContextEditor ( **默认值** )
设置方法注解 @Transactional,保证方法操作原子性。
调用
OrderRepository#updateOrder(...)
方法,更新订单状态为支付中。实现代码如下:
// Order.java
public void pay(BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
this.redPacketPayAmount = redPacketPayAmount;
this.capitalPayAmount = capitalPayAmount;
this.status = "PAYING";
}
调用
TradeOrderServiceProxy#record(...)
方法,资金账户余额支付订单。实现代码如下:
// TradeOrderServiceProxy.java
@Compensable(propagation = Propagation.SUPPORTS, confirmMethod = "record", cancelMethod = "record", transactionContextEditor = Compensable.DefaultTransactionContextEditor.class)
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
return capitalTradeOrderService.record(transactionContext, tradeOrderDto);
}
// CapitalTradeOrderService.java
public interface CapitalTradeOrderService {
String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto);
}
- 本地方法调用时,参数
transactionContext
传递null
即可,TransactionContextEditor 会设置。在《TCC-Transaction 源码分析 —— TCC 实现》「6.3 资源协调者拦截器」有详细解析。 - 远程方法调用时,参数
transactionContext
需要传递。Dubbo 远程方法调用实际也进行了传递,传递方式较为特殊,通过隐式船舱,在《TCC-Transaction 源码分析 —— Dubbo 支持》「3. Dubbo 事务上下文编辑器」有详细解析。 propagation=Propagation.SUPPORTS
:支持当前事务,如果当前没有事务,就以非事务方式执行。为什么不使用 REQUIRED ?如果使用 REQUIRED 事务传播级别,事务恢复重试时,会发起新的事务。confirmMethod
、cancelMethod
使用和 try 方法相同方法名:本地发起远程服务 TCC confirm / cancel 阶段,调用相同方法进行事务的提交或回滚。远程服务的 CompensableTransactionInterceptor 会根据事务的状态是 CONFIRMING / CANCELLING 来调用对应方法。- 设置方法注解 @Compensable
- 调用 `CapitalTradeOrderService#record(...)` 方法,远程调用,发起**资金**账户余额支付订单。
调用
CapitalTradeOrderService#record(...)
方法,远程调用,发起资金账户余额支付订单。
调用
TradeOrderServiceProxy#record(...)
方法,红包账户余额支付订单。和资金账户余额支付订单 99.99% 类似,不重复“复制粘贴”。
资金服务
调用
CapitalTradeOrderServiceImpl#record(...)
方法,红包账户余额支付订单。实现代码如下:
@Override
@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = Compensable.DefaultTransactionContextEditor.class)
@Transactional
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
// 调试用
try {
Thread.sleep(1000l);
// Thread.sleep(10000000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("capital try record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
// 生成交易订单
TradeOrder tradeOrder = new TradeOrder(
tradeOrderDto.getSelfUserId(),
tradeOrderDto.getOppositeUserId(),
tradeOrderDto.getMerchantOrderNo(),
tradeOrderDto.getAmount()
);
tradeOrderRepository.insert(tradeOrder);
// 更新减少下单用户的资金账户余额
CapitalAccount transferFromAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());
transferFromAccount.transferFrom(tradeOrderDto.getAmount());
capitalAccountRepository.save(transferFromAccount);
return "success";
}
-
- 事务传播级别 Propagation.REQUIRED ( **默认值** )
- 设置 `confirmMethod` / `cancelMethod` 方法名
- 事务上下文编辑类 DefaultTransactionContextEditor ( **默认值** )
设置方法注解 @Transactional,保证方法操作原子性。
调用
TradeOrderRepository#insert(...)
方法,生成订单状态为
"DRAFT"
的交易订单。
调用
CapitalAccountRepository#save(...)
方法,更新减少下单用户的资金账户余额。Try 阶段锁定资源时,一定要先扣。TCC 是最终事务一致性,如果先添加,可能被使用。
4.2 Confirm / Cancel 阶段
当 Try 操作全部成功时,发起 Confirm 操作。
当 Try 操作存在任务失败时,发起 Cancel 操作。
4.2.1 Confirm
商城服务
调用
PaymentServiceImpl#confirmMakePayment(...)
方法,更新订单状态为支付成功。实现代码如下:
public void confirmMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
// 调试用
try {
Thread.sleep(1000l);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("order confirm make payment called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
// 更新订单状态为支付成功
order.confirm();
orderRepository.updateOrder(order);
}
// Order.java
public void confirm() {
this.status = "CONFIRMED";
}
调用
OrderRepository#updateOrder(...)
方法,更新订单状态为支付成功。实现代码如下:
资金服务
调用
CapitalTradeOrderServiceImpl#confirmRecord(...)
方法,更新交易订单状态为交易成功。
@Transactional
public void confirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
// 调试用
try {
Thread.sleep(1000l);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("capital confirm record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
// 查询交易记录
TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());
// 判断交易记录状态。因为 `#record()` 方法,可能事务回滚,记录不存在 / 状态不对
if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {
// 更新订单状态为交易成功
tradeOrder.confirm();
tradeOrderRepository.update(tradeOrder);
// 更新增加商店拥有者用户的资金账户余额
CapitalAccount transferToAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getOppositeUserId());
transferToAccount.transferTo(tradeOrderDto.getAmount());
capitalAccountRepository.save(transferToAccount);
}
}
// CapitalAccount.java
public void transferTo(BigDecimal amount) {
this.balanceAmount = this.balanceAmount.add(amount);
}
判断交易记录状态。因为
#record()
方法,可能事务回滚,记录不存在 / 状态不对。
调用
CapitalAccountRepository#save(...)
方法,更新增加商店拥有者用户的资金账户余额。实现代码如下:
红包服务
和资源服务 99.99% 相同,不重复“复制粘贴”。
4.2.2 Cancel
商城服务
调用
PaymentServiceImpl#cancelMakePayment(...)
方法,更新订单状态为支付失败。实现代码如下:
public void cancelMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
// 调试用
try {
Thread.sleep(1000l);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("order cancel make payment called.time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
// 更新订单状态为支付失败
order.cancelPayment();
orderRepository.updateOrder(order);
}
// Order.java
public void cancelPayment() {
this.status = "PAY_FAILED";
}
调用
OrderRepository#updateOrder(...)
方法,更新订单状态为支付失败。实现代码如下:
资金服务
调用
CapitalTradeOrderServiceImpl#cancelRecord(...)
方法,更新交易订单状态为交易失败。
@Transactional
public void cancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
// 调试用
try {
Thread.sleep(1000l);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("capital cancel record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
// 查询交易记录
TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());
// 判断交易记录状态。因为 `#record()` 方法,可能事务回滚,记录不存在 / 状态不对
if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {
// / 更新订单状态为交易失败
tradeOrder.cancel();
tradeOrderRepository.update(tradeOrder);
// 更新增加( 恢复 )下单用户的资金账户余额
CapitalAccount capitalAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());
capitalAccount.cancelTransfer(tradeOrderDto.getAmount());
capitalAccountRepository.save(capitalAccount);
}
}
// CapitalAccount.java
public void cancelTransfer(BigDecimal amount) {
transferTo(amount);
}
判断交易记录状态。因为
#record()
方法,可能事务回滚,记录不存在 / 状态不对。
调用
CapitalAccountRepository#save(...)
方法,更新增加( 恢复 )下单用户的资金账户余额。实现代码如下:
红包服务
和资源服务 99.99% 相同,不重复“复制粘贴”。
666. 彩蛋
嘿嘿,代码只是看起来比较多,实际不多。
蚂蚁金融云提供了银行间转账的 TCC 过程例子,有兴趣的同学可以看看:《蚂蚁金融云 —— 分布式事务服务(DTS) —— 场景介绍》。
本系列 EOF ~撒花
胖友,分享个朋友圈,可好?!