本文主要基于 TCC-Transaction 1.2.3.3 正式版
本文分享 TCC 项目实战。以官方 Maven项目 tcc-transaction-http-sample 为例子( tcc-transaction-dubbo-sample 类似 )。
首先我们简单了解下这个项目。
首页 => 商品列表 => 确认支付页 => 支付结果页
使用账户余额 + 红包余额联合支付购买商品,并账户之间转账。
项目拆分三个子 Maven 项目:
public class Shop {
/**
* 商店编号
*/
private long id;
/**
* 所有者用户编号
*/
private long ownerUserId;
}
public class Product implements Serializable {
/**
* 商品编号
*/
private long productId;
/**
* 商店编号
*/
private long shopId;
/**
* 商品名
*/
private String productName;
/**
* 单价
*/
private BigDecimal price;
}
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 orderLines = new ArrayList();
}
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”。
关系较为简单,有两个实体:
public class CapitalAccount {
/**
* 账户编号
*/
private long id;
/**
* 用户编号
*/
private long userId;
/**
* 余额
*/
private BigDecimal balanceAmount;
}
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 )记录,并更新减少下单用户的资金账户余额。
关系较为简单,和资金服务 99.99% 相同,有两个实体:
public class RedPacketAccount {
/**
* 账户编号
*/
private long id;
/**
* 用户编号
*/
private long userId;
/**
* 余额
*/
private BigDecimal balanceAmount;
}
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 )记录,并更新减少下单用户的红包账户余额。
服务之间,通过 HTTP 进行调用。
红包服务和资金服务为商城服务提供调用( 以资金服务为例子 ):
// appcontext-service-provider.xml
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();
}
}
商城服务调用
// appcontext-service-consumer.xml
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);
}
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(…) 方法,下单并支付订单。实现代码如下:
@Service
public class PlaceOrderServiceImpl {
public String placeOrder(long payerUserId, long shopId, List> 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();
}
}
@Service
public class OrderServiceImpl {
@Transactional
public Order createOrder(long payerUserId, long payeeUserId, List> productQuantities) {
Order order = orderFactory.buildOrder(payerUserId, payeeUserId, productQuantities);
orderRepository.createOrder(order);
return order;
}
}
商城服务
调用 PaymentService#makePayment(…) 方法,发起 Try 流程,实现代码如下:
@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));
}
设置方法注解 @Compensable
设置方法注解 @Transactional,保证方法操作原子性。
调用 OrderRepository#updateOrder(…) 方法,更新订单状态为支付中。实现代码如下:
// Order.java
public void pay(BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
this.redPacketPayAmount = redPacketPayAmount;
this.capitalPayAmount = capitalPayAmount;
this.status = "PAYING";
}
// 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);
}
设置方法注解 @Compensable
调用 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";
}
设置方法注解 @Compensable
设置方法注解 @Transactional,保证方法操作原子性。
调用 TradeOrderRepository#insert(…) 方法,生成订单状态为 “DRAFT” 的交易订单。
调用 CapitalAccountRepository#save(…) 方法,更新减少下单用户的资金账户余额。Try 阶段锁定资源时,一定要先扣。TCC 是最终事务一致性,如果先添加,可能被使用。
当 Try 操作全部成功时,发起 Confirm 操作。
当 Try 操作存在任务失败时,发起 Cancel 操作。
商城服务
调用 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";
}
资金服务
调用 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);
}
红包服务
和资源服务 99.99% 相同,不重复“复制粘贴”。
商城服务
调用 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";
}
资金服务
调用 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);
}
红包服务
和资源服务 99.99% 相同,不重复“复制粘贴”。
GitHub源码
“随着微服务架构的发展,Spring Cloud 使用得越来越广泛。驰狼课堂 Spring Boot 快速入门,Spring Boot 与Spring Cloud 整合,docker+k8s,大型电商商城等多套免费实战教程可以帮您真正做到快速上手,将技术点切实运用到微服务项目中。”