Dubbo支持分布式事务吗?

2020-07-03 07:51发布

1条回答
无需指教
2楼 · 2020-07-03 08:38

一、背景

       目前开发的项目是分布式架构的,数据库也是分开的,各个子工程之间是通过dubbo调用,由于没有考虑分布式事务的问题,导致接口出错回滚时,调用端正常回滚了但是被调用端却不能回滚,产生了很多垃圾数据。

二、分布式事务(事务补偿机制

 事务补偿即在事务链中的任何一个正向事务操作,都必须存在一个完全符合回滚规则的可逆事务。如果是一个完整的事务链,则必须事务链中的每一个业务服务或操作都有对应的可逆服务。在上面方式中可以看到需要手工编写大量的代码来处理以保证事务的完整性,我们可以考虑实现一个通用的事务管理器,实现事务链和事务上下文的管理。对于事务链上的任何一个服务正向和逆向操作均在事务管理和协同器上注册,由事务管理器接管所有的事务补偿和回滚操作。

三、tcc-transaction框架介绍

 介绍:tcc-transaction是开源的TCC补偿性分布式事务框架,Git地址:https://github.com/changmingxie/tcc-transaction
 TCC-Transaction 通过 Dubbo 隐式传参的功能,避免自己对业务代码的入侵。

四、例子

首先我们简单了解下这个项目。

  • 首页 => 商品列表 => 确认支付页 => 支付结果页

  • 使用账户余额 + 红包余额联合支付购买商品,并账户之间转账

项目拆分三个子 Maven 项目:

  • tcc-transaction-http-order :商城服务,提供商品和商品订单逻辑。

  • tcc-transaction-http-capital :资金服务,提供账户余额逻辑。

  • tcc-transaction-http-redpacket :红包服务,提供红包余额逻辑。

2. 实体结构

2.1 商城服务

  • Shop,商店表。实体代码如下:


    public class Shop {

    /**

    * 商店编号

    */

    private long id;

    /**

    * 所有者用户编号

    */

    private long ownerUserId;

    }

  • Product,商品表。实体代码如下:


    public class Product implements Serializable {

    /**

    * 商品编号

    */

    private long productId;

    /**

    * 商店编号

    */

    private long shopId;

    /**

    * 商品名

    */

    private String productName;

    /**

    * 单价

    */

    private BigDecimal price;

    }

  • Order,订单表。实现代码如下:


    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();

    }

  • OrderLine,订单明细。实体代码如下:


    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"

  • 订单支付成功,更新订单状态为 "CONFIRMED"

  • 订单支付失败,更新订单状体为 "PAY_FAILED"

2.2 资金服务

关系较为简单,有两个实体:

  • CapitalAccount,资金账户余额。实体代码如下:


    public class CapitalAccount {

    /**

    * 账户编号

    */

    private long id;

    /**

    * 用户编号

    */

    private long userId;

    /**

    * 余额

    */

    private BigDecimal balanceAmount;

    }

  • TradeOrder,交易订单表。实体代码如下:


    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 )记录,并更新减少下单用户的资金账户余额。

  • 订单支付成功,更新交易订单状态为 "CONFIRM",并更新增加商店拥有用户的资金账户余额。

  • 订单支付失败,更新交易订单状态为 "CANCEL",并更新增加( 恢复 )下单用户的资金账户余额。

2.3 红包服务

关系较为简单,和资金服务 99.99% 相同,有两个实体:

  • RedPacketAccount,红包账户余额。实体代码如下:


    public class RedPacketAccount {

    /**

    * 账户编号

    */

    private long id;

    /**

    * 用户编号

    */

    private long userId;

    /**

    * 余额

    */

    private BigDecimal balanceAmount;

    }

  • TradeOrder,交易订单表。实体代码如下:


    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 )记录,并更新减少下单用户的红包账户余额。

  • 订单支付成功,更新交易订单状态为 "CONFIRM",并更新增加商店拥有用户的红包账户余额。

  • 订单支付失败,更新交易订单状态为 "CANCEL",并更新增加( 恢复 )下单用户的红包账户余额。

3. 服务调用

服务之间,通过 HTTP 进行调用。

红包服务和资金服务为商城服务提供调用( 以资金服务为例子 )

  • XML 配置如下 :


    // appcontext-service-provider.xml

    <beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"

    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <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>

    beans>

  • Java 代码实现如下 :


    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();

    }

    }


商城服务调用

  • XML 配置如下:


    // appcontext-service-consumer.xml

    <beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <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>

    beans>

  • Java 接口接口如下:


    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);

    }

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, 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();

}

}

  • 调用 ShopRepository#findById(...) 方法,查询商店。

  • 调用 OrderService#createOrder(...) 方法,创建订单状态为 "DRAFT" 的商城订单。实际业务不会这么做,此处仅仅是例子,简化流程。实现代码如下:


    @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(...) 方法,发起支付,TCC 流程

4.1 Try 阶段

商城服务

调用 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

    • 事务传播级别 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 事务传播级别,事务恢复重试时,会发起新的事务。

    • confirmMethodcancelMethod 使用和 try 方法相同方法名本地发起远程服务 TCC confirm / cancel 阶段,调用相同方法进行事务的提交或回滚。远程服务的 CompensableTransactionInterceptor 会根据事务的状态是 CONFIRMING / CANCELLING 来调用对应方法。

    • 设置方法注解 @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

    • 事务传播级别 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);

}

  • 生产代码该方法需要加下 @Transactional 注解,保证原子性

  • 调用 OrderRepository#updateOrder(...) 方法,更新订单状态为支付成功。实现代码如下:


    // 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);

}

}

  • 设置方法注解 @Transactional,保证方法操作原子性。

  • 判断交易记录状态。因为 #record() 方法,可能事务回滚,记录不存在 / 状态不对。

  • 调用 TradeOrderRepository#update(...) 方法,更新交易订单状态为交易成功

  • 调用 CapitalAccountRepository#save(...) 方法,更新增加商店拥有者用户的资金账户余额。实现代码如下:


    // CapitalAccount.java

    public void transferTo(BigDecimal amount) {

    this.balanceAmount = this.balanceAmount.add(amount);

    }


红包服务

资源服务 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);

}

  • 生产代码该方法需要加下 @Transactional 注解,保证原子性

  • 调用 OrderRepository#updateOrder(...) 方法,更新订单状态为支付失败。实现代码如下:


    // 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);

}

}

  • 设置方法注解 @Transactional,保证方法操作原子性。

  • 判断交易记录状态。因为 #record() 方法,可能事务回滚,记录不存在 / 状态不对。

  • 调用 TradeOrderRepository#update(...) 方法,更新交易订单状态为交易失败

  • 调用 CapitalAccountRepository#save(...) 方法,更新增加( 恢复 )下单用户的资金账户余额。实现代码如下:


    // CapitalAccount.java

    public void cancelTransfer(BigDecimal amount) {

    transferTo(amount);

    }


一、背景

       目前开发的项目是分布式架构的,数据库也是分开的,各个子工程之间是通过dubbo调用,由于没有考虑分布式事务的问题,导致接口出错回滚时,调用端正常回滚了但是被调用端却不能回滚,产生了很多垃圾数据。

二、分布式事务(事务补偿机制

 事务补偿即在事务链中的任何一个正向事务操作,都必须存在一个完全符合回滚规则的可逆事务。如果是一个完整的事务链,则必须事务链中的每一个业务服务或操作都有对应的可逆服务。在上面方式中可以看到需要手工编写大量的代码来处理以保证事务的完整性,我们可以考虑实现一个通用的事务管理器,实现事务链和事务上下文的管理。对于事务链上的任何一个服务正向和逆向操作均在事务管理和协同器上注册,由事务管理器接管所有的事务补偿和回滚操作。

三、tcc-transaction框架介绍

 介绍:tcc-transaction是开源的TCC补偿性分布式事务框架,Git地址:https://github.com/changmingxie/tcc-transaction
 TCC-Transaction 通过 Dubbo 隐式传参的功能,避免自己对业务代码的入侵。

四、例子

首先我们简单了解下这个项目。

  • 首页 => 商品列表 => 确认支付页 => 支付结果页

  • 使用账户余额 + 红包余额联合支付购买商品,并账户之间转账

项目拆分三个子 Maven 项目:

  • tcc-transaction-http-order :商城服务,提供商品和商品订单逻辑。

  • tcc-transaction-http-capital :资金服务,提供账户余额逻辑。

  • tcc-transaction-http-redpacket :红包服务,提供红包余额逻辑。

2. 实体结构

2.1 商城服务

  • Shop,商店表。实体代码如下:


    public class Shop {

    /**

    * 商店编号

    */

    private long id;

    /**

    * 所有者用户编号

    */

    private long ownerUserId;

    }

  • Product,商品表。实体代码如下:


    public class Product implements Serializable {

    /**

    * 商品编号

    */

    private long productId;

    /**

    * 商店编号

    */

    private long shopId;

    /**

    * 商品名

    */

    private String productName;

    /**

    * 单价

    */

    private BigDecimal price;

    }

  • Order,订单表。实现代码如下:


    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();

    }

  • OrderLine,订单明细。实体代码如下:


    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"

  • 订单支付成功,更新订单状态为 "CONFIRMED"

  • 订单支付失败,更新订单状体为 "PAY_FAILED"

2.2 资金服务

关系较为简单,有两个实体:

  • CapitalAccount,资金账户余额。实体代码如下:


    public class CapitalAccount {

    /**

    * 账户编号

    */

    private long id;

    /**

    * 用户编号

    */

    private long userId;

    /**

    * 余额

    */

    private BigDecimal balanceAmount;

    }

  • TradeOrder,交易订单表。实体代码如下:


    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 )记录,并更新减少下单用户的资金账户余额。

  • 订单支付成功,更新交易订单状态为 "CONFIRM",并更新增加商店拥有用户的资金账户余额。

  • 订单支付失败,更新交易订单状态为 "CANCEL",并更新增加( 恢复 )下单用户的资金账户余额。

2.3 红包服务

关系较为简单,和资金服务 99.99% 相同,有两个实体:

  • RedPacketAccount,红包账户余额。实体代码如下:


    public class RedPacketAccount {

    /**

    * 账户编号

    */

    private long id;

    /**

    * 用户编号

    */

    private long userId;

    /**

    * 余额

    */

    private BigDecimal balanceAmount;

    }

  • TradeOrder,交易订单表。实体代码如下:


    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 )记录,并更新减少下单用户的红包账户余额。

  • 订单支付成功,更新交易订单状态为 "CONFIRM",并更新增加商店拥有用户的红包账户余额。

  • 订单支付失败,更新交易订单状态为 "CANCEL",并更新增加( 恢复 )下单用户的红包账户余额。

3. 服务调用

服务之间,通过 HTTP 进行调用。

红包服务和资金服务为商城服务提供调用( 以资金服务为例子 )

  • XML 配置如下 :


    // appcontext-service-provider.xml

    <beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"

    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <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>

    beans>

  • Java 代码实现如下 :


    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();

    }

    }


商城服务调用

  • XML 配置如下:


    // appcontext-service-consumer.xml

    <beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <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>

    beans>

  • Java 接口接口如下:


    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);

    }

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, 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();

}

}

  • 调用 ShopRepository#findById(...) 方法,查询商店。

  • 调用 OrderService#createOrder(...) 方法,创建订单状态为 "DRAFT" 的商城订单。实际业务不会这么做,此处仅仅是例子,简化流程。实现代码如下:


    @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(...) 方法,发起支付,TCC 流程

4.1 Try 阶段

商城服务

调用 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

    • 事务传播级别 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 事务传播级别,事务恢复重试时,会发起新的事务。

    • confirmMethodcancelMethod 使用和 try 方法相同方法名本地发起远程服务 TCC confirm / cancel 阶段,调用相同方法进行事务的提交或回滚。远程服务的 CompensableTransactionInterceptor 会根据事务的状态是 CONFIRMING / CANCELLING 来调用对应方法。

    • 设置方法注解 @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

      • 事务传播级别 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);

    }

    • 生产代码该方法需要加下 @Transactional 注解,保证原子性

    • 调用 OrderRepository#updateOrder(...) 方法,更新订单状态为支付成功。实现代码如下:


      // 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);

    }

    }

    • 设置方法注解 @Transactional,保证方法操作原子性。

    • 判断交易记录状态。因为 #record() 方法,可能事务回滚,记录不存在 / 状态不对。

    • 调用 TradeOrderRepository#update(...) 方法,更新交易订单状态为交易成功

    • 调用 CapitalAccountRepository#save(...) 方法,更新增加商店拥有者用户的资金账户余额。实现代码如下:


      // CapitalAccount.java

      public void transferTo(BigDecimal amount) {

      this.balanceAmount = this.balanceAmount.add(amount);

      }


    红包服务

    资源服务 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);

    }

    • 生产代码该方法需要加下 @Transactional 注解,保证原子性

    • 调用 OrderRepository#updateOrder(...) 方法,更新订单状态为支付失败。实现代码如下:


      // 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);

    }

    }

    • 设置方法注解 @Transactional,保证方法操作原子性。

    • 判断交易记录状态。因为 #record() 方法,可能事务回滚,记录不存在 / 状态不对。

    • 调用 TradeOrderRepository#update(...) 方法,更新交易订单状态为交易失败

    • 调用 CapitalAccountRepository#save(...) 方法,更新增加( 恢复 )下单用户的资金账户余额。实现代码如下:


      // CapitalAccount.java

      public void cancelTransfer(BigDecimal amount) {

      transferTo(amount);

      }



    相关问题推荐

    • 回答 2

      Statement的execute(String query)方法用来执行任意的SQL查询,如果查询的结果是一个ResultSet,这个方法就返回true。如果结果不是ResultSet,比如insert或者update查询,它就会返回false。我们可以通过它的getResultSet方法来获取ResultSet,或者通过getUpda...

    • 回答 22

      忙的时候项目期肯定要加班 但是每天加班应该还不至于

    • 回答 108
      已采纳

      虽然Java人才越来越多,但是人才缺口也是很大的,我国对JAVA工程师的需求是所有软件工程师当中需求大的,达到全部需求量的60%-70%,所以Java市场在短时间内不可能饱和。其次,Java市场不断变化,人才需求也会不断增加。马云说过,未来的制造业要的不是石油,...

    • 回答 5
      已采纳

      工信部证书含金量较高。工信部是国务院的下属结构,具有发放资质、证书的资格。其所发放的证书具有较强的权威性,在全国范围内收到认可,含金量通常都比较高。 工信部证书,其含义也就是工信部颁发并承认的某项技能证书,是具有法律效力的,并且是国家认可的...

    • 回答 70
      已采纳

      学Java好不好找工作?看学完Java后能做些什么吧。一、大数据技术Hadoop以及其他大数据处理技术都是用Java或者其他,例如Apache的基于Java 的 HBase和Accumulo以及ElasticSearchas。但是Java在此领域并未占太大空间,但只要Hadoop和ElasticSearchas能够成长壮...

    • 回答 16
      已采纳

      就是java的基础知识啊,比如Java 集合框架;Java 多线程;线程的五种状态;Java 虚拟机;MySQL (InnoDB);Spring 相关;计算机网络;MQ 消息队列诸如此类

    • 回答 12

      #{}和${}这两个语法是为了动态传递参数而存在的,是Mybatis实现动态SQL的基础,总体上他们的作用是一致的(为了动态传参),但是在编译过程、是否自动加单引号、安全性、使用场景等方面有很多不同,下面详细比较两者间的区别:1.#{} 是 占位符 :动态解析 ...

    • 回答 62

      没问题的,专科学历也能学习Java开发的,主要看自己感不感兴趣,只要认真学,市面上的培训机构不少都是零基础课程,能跟得上,或是自己先找些资料学习一下。

    • 回答 4

      1、反射对单例模式的破坏采用反射的方式另辟蹊径实例了该类,导致程序中会存在不止一个实例。解决方案其思想就是采用一个全局变量,来标记是否已经实例化过了,如果已经实例化过了,第 二次实例化的时候,抛出异常2、clone()对单例模式的破坏当需要实现单例的...

    • 回答 5

       优点: 一、实例控制  单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。 二、灵活性  因为类控制了实例化过程,所以类可以灵活更改实例化过程。 缺点: 一、开销  虽然数量很少,但如果每次对象请求引用时都要...

    • 回答 4

      这个主要是看你数组的长度是多少, 比如之前写过的一个程序有个数组存的是各个客户端的ip地址:string clientIp[4]={XXX, xxx, xxx, xxx};这个时候如果想把hash值对应到上面四个地址的话,就应该对4取余,这个时候p就应该为4...

    • 回答 6

       哈希表的大小 · 关键字的分布情况 · 记录的查找频率 1.直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)。...

    • 回答 6

      哈希表的大小取决于一组质数,原因是在hash函数中,你要用这些质数来做模运算(%)。而分析发现,如果不是用质数来做模运算的话,很多生活中的数据分布,会集中在某些点上。所以这里最后采用了质数做模的除数。 因为用质数做了模的除数,自然存储空间的大小也用质数了...

    • 回答 2

      是啊,哈希函数的设计至关重要,好的哈希函数会尽可能地保证计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间

    • 回答 3

       解码查表优化算法,seo优化

    • 回答 5

      1.对对象元素中的关键字(对象中的特有数据),进行哈希算法的运算,并得出一个具体的算法值,这个值 称为哈希值。2.哈希值就是这个元素的位置。3.如果哈希值出现冲突,再次判断这个关键字对应的对象是否相同。如果对象相同,就不存储,因为元素重复。如果对象不同,就...

    没有解决我的问题,去提问