http://www.iocoder.cn/categories/TCC-Transaction/
https://github.com/changmingxie/tcc-transaction
细读tcc,理解事物实现的本质
顾名思义,TCC - Try(完成所有业务检查,预留必须业务资源) ,Confirm(真正执行业务,不做任何业务检查,只使用Try阶段预留的业务资源,Confirm操作满足幂等性),Cancel(释放Try阶段预留的业务资源,cancel操作满足幂等性)
觉得应该先看下上面代码和文档中描述的那个买卖的例子:一个简单的购物系统,可以下单,可以选择用红包或余额支付等,从文章中摘出来一张图
基本下单流程就是这样
整个下单的起点就是一个web controller
package org.mengyun.tcctransaction.sample.dubbo.order.web.controller; import org.apache.commons.lang3.tuple.ImmutablePair; import org.mengyun.tcctransaction.sample.dubbo.order.service.AccountServiceImpl; import org.mengyun.tcctransaction.sample.order.domain.entity.Order; import org.mengyun.tcctransaction.sample.order.domain.entity.Product; import org.mengyun.tcctransaction.sample.order.domain.repository.ProductRepository; import org.mengyun.tcctransaction.sample.order.domain.service.OrderServiceImpl; import org.mengyun.tcctransaction.sample.dubbo.order.service.PlaceOrderServiceImpl; import org.mengyun.tcctransaction.sample.dubbo.order.web.controller.vo.PlaceOrderRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; import java.math.BigDecimal; import java.security.InvalidParameterException; import java.util.List; /** * Created by changming.xie on 4/1/16. */ @Controller @RequestMapping("") public class OrderController { @Autowired PlaceOrderServiceImpl placeOrderService; @Autowired ProductRepository productRepository; @Autowired AccountServiceImpl accountService; @Autowired OrderServiceImpl orderService; @RequestMapping(value = "/", method = RequestMethod.GET) public ModelAndView index() { ModelAndView mv = new ModelAndView("/index"); return mv; } @RequestMapping(value = "/user/{userId}/shop/{shopId}", method = RequestMethod.GET) public ModelAndView getProductsInShop(@PathVariable long userId, @PathVariable long shopId) { List<Product> products = productRepository.findByShopId(shopId); ModelAndView mv = new ModelAndView("/shop"); mv.addObject("products", products); mv.addObject("userId", userId); mv.addObject("shopId", shopId); return mv; } @RequestMapping(value = "/user/{userId}/shop/{shopId}/product/{productId}/confirm", method = RequestMethod.GET) public ModelAndView productDetail(@PathVariable long userId, @PathVariable long shopId, @PathVariable long productId) { ModelAndView mv = new ModelAndView("product_detail"); mv.addObject("capitalAmount", accountService.getCapitalAccountByUserId(userId)); mv.addObject("redPacketAmount", accountService.getRedPacketAccountByUserId(userId)); mv.addObject("product", productRepository.findById(productId)); mv.addObject("userId", userId); mv.addObject("shopId", shopId); return mv; } @RequestMapping(value = "/placeorder", method = RequestMethod.POST) public RedirectView 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()); return new RedirectView("/payresult/" + merchantOrderNo); } @RequestMapping(value = "/payresult/{merchantOrderNo}", method = RequestMethod.GET) public ModelAndView getPayResult(@PathVariable String merchantOrderNo) { ModelAndView mv = new ModelAndView("pay_success"); String payResultTip = null; Order foundOrder = orderService.findOrderByMerchantOrderNo(merchantOrderNo); if ("CONFIRMED".equals(foundOrder.getStatus())) payResultTip = "支付成功"; else if ("PAY_FAILED".equals(foundOrder.getStatus())) payResultTip = "支付失败"; else payResultTip = "Unknown"; mv.addObject("payResult", payResultTip); mv.addObject("capitalAmount", accountService.getCapitalAccountByUserId(foundOrder.getPayerUserId())); mv.addObject("redPacketAmount", accountService.getRedPacketAccountByUserId(foundOrder.getPayerUserId())); return mv; } private PlaceOrderRequest buildRequest(String redPacketPayAmount, long shopId, long payerUserId, long productId) { BigDecimal redPacketPayAmountInBigDecimal = new BigDecimal(redPacketPayAmount); if (redPacketPayAmountInBigDecimal.compareTo(BigDecimal.ZERO) < 0) throw new InvalidParameterException("invalid red packet amount :" + redPacketPayAmount); PlaceOrderRequest request = new PlaceOrderRequest(); request.setPayerUserId(payerUserId); request.setShopId(shopId); request.setRedPacketPayAmount(new BigDecimal(redPacketPayAmount)); request.getProductQuantities().add(new ImmutablePair<Long, Integer>(productId, 1)); return request; } }
下单
package org.mengyun.tcctransaction.sample.http.order.service; import org.apache.commons.lang3.tuple.Pair; import org.mengyun.tcctransaction.CancellingException; import org.mengyun.tcctransaction.ConfirmingException; import org.mengyun.tcctransaction.sample.order.domain.entity.Order; import org.mengyun.tcctransaction.sample.order.domain.entity.Shop; import org.mengyun.tcctransaction.sample.order.domain.repository.ShopRepository; import org.mengyun.tcctransaction.sample.order.domain.service.OrderServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.List; /** * Created by changming.xie on 4/1/16. */ @Service public class PlaceOrderServiceImpl { @Autowired ShopRepository shopRepository; @Autowired OrderServiceImpl orderService; @Autowired PaymentServiceImpl paymentService; public String placeOrder(long payerUserId, long shopId, List<Pair<Long, 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(); } }
支付服务,这里开始就用到了TCC
@Compensable(confirmMethod = "confirmMakePayment", cancelMethod = "cancelMakePayment", asyncConfirm = true)
package org.mengyun.tcctransaction.sample.dubbo.order.service; import org.apache.commons.lang3.time.DateFormatUtils; import org.mengyun.tcctransaction.api.Compensable; import org.mengyun.tcctransaction.sample.dubbo.capital.api.CapitalTradeOrderService; import org.mengyun.tcctransaction.sample.dubbo.capital.api.dto.CapitalTradeOrderDto; import org.mengyun.tcctransaction.sample.dubbo.redpacket.api.RedPacketTradeOrderService; import org.mengyun.tcctransaction.sample.dubbo.redpacket.api.dto.RedPacketTradeOrderDto; import org.mengyun.tcctransaction.sample.order.domain.entity.Order; import org.mengyun.tcctransaction.sample.order.domain.repository.OrderRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.Calendar; /** * Created by changming.xie on 4/1/16. */ @Service public class PaymentServiceImpl { @Autowired CapitalTradeOrderService capitalTradeOrderService; @Autowired RedPacketTradeOrderService redPacketTradeOrderService; @Autowired OrderRepository orderRepository; @Compensable(confirmMethod = "confirmMakePayment", cancelMethod = "cancelMakePayment", asyncConfirm = true) 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")); //check if the order status is DRAFT, if no, means that another call makePayment for the same order happened, ignore this call makePayment. if (order.getStatus().equals("DRAFT")) { order.pay(redPacketPayAmount, capitalPayAmount); try { orderRepository.updateOrder(order); } catch (OptimisticLockingFailureException e) { //ignore the concurrently update order exception, ensure idempotency. } } String result = capitalTradeOrderService.record(buildCapitalTradeOrderDto(order)); String result2 = redPacketTradeOrderService.record(buildRedPacketTradeOrderDto(order)); } 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 foundOrder = orderRepository.findByMerchantOrderNo(order.getMerchantOrderNo()); //check if the trade order status is PAYING, if no, means another call confirmMakePayment happened, return directly, ensure idempotency. if (foundOrder != null && foundOrder.getStatus().equals("PAYING")) { order.confirm(); orderRepository.updateOrder(order); } } 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 foundOrder = orderRepository.findByMerchantOrderNo(order.getMerchantOrderNo()); //check if the trade order status is PAYING, if no, means another call cancelMakePayment happened, return directly, ensure idempotency. if (foundOrder != null && foundOrder.getStatus().equals("PAYING")) { order.cancelPayment(); orderRepository.updateOrder(order); } } private CapitalTradeOrderDto buildCapitalTradeOrderDto(Order order) { CapitalTradeOrderDto tradeOrderDto = new CapitalTradeOrderDto(); tradeOrderDto.setAmount(order.getCapitalPayAmount()); tradeOrderDto.setMerchantOrderNo(order.getMerchantOrderNo()); tradeOrderDto.setSelfUserId(order.getPayerUserId()); tradeOrderDto.setOppositeUserId(order.getPayeeUserId()); tradeOrderDto.setOrderTitle(String.format("order no:%s", order.getMerchantOrderNo())); return tradeOrderDto; } private RedPacketTradeOrderDto buildRedPacketTradeOrderDto(Order order) { RedPacketTradeOrderDto tradeOrderDto = new RedPacketTradeOrderDto(); tradeOrderDto.setAmount(order.getRedPacketPayAmount()); tradeOrderDto.setMerchantOrderNo(order.getMerchantOrderNo()); tradeOrderDto.setSelfUserId(order.getPayerUserId()); tradeOrderDto.setOppositeUserId(order.getPayeeUserId()); tradeOrderDto.setOrderTitle(String.format("order no:%s", order.getMerchantOrderNo())); return tradeOrderDto; } }
继续往下,这里加了一层代理并增加了传播属性的设置,还定义了事物编辑器
@Compensable(propagation = Propagation.SUPPORTS, confirmMethod = "record", cancelMethod = "record", transactionContextEditor = MethodTransactionContextEditor.class)
package org.mengyun.tcctransaction.sample.http.order.service; import org.mengyun.tcctransaction.api.Compensable; import org.mengyun.tcctransaction.api.Propagation; import org.mengyun.tcctransaction.api.TransactionContext; import org.mengyun.tcctransaction.context.MethodTransactionContextEditor; import org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService; import org.mengyun.tcctransaction.sample.http.capital.api.dto.CapitalTradeOrderDto; import org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketTradeOrderService; import org.mengyun.tcctransaction.sample.http.redpacket.api.dto.RedPacketTradeOrderDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * Created by changming.xie on 4/19/17. */ @Component public class TradeOrderServiceProxy { @Autowired CapitalTradeOrderService capitalTradeOrderService; @Autowired RedPacketTradeOrderService redPacketTradeOrderService; /*the propagation need set Propagation.SUPPORTS,otherwise the recover doesn't work, The default value is Propagation.REQUIRED, which means will begin new transaction when recover. */ @Compensable(propagation = Propagation.SUPPORTS, confirmMethod = "record", cancelMethod = "record", transactionContextEditor = MethodTransactionContextEditor.class) public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) { return capitalTradeOrderService.record(transactionContext, tradeOrderDto); } @Compensable(propagation = Propagation.SUPPORTS, confirmMethod = "record", cancelMethod = "record", transactionContextEditor = MethodTransactionContextEditor.class) public String record(TransactionContext transactionContext, RedPacketTradeOrderDto tradeOrderDto) { return redPacketTradeOrderService.record(transactionContext, tradeOrderDto); } }
继续
package org.mengyun.tcctransaction.sample.dubbo.capital.service; import org.apache.commons.lang3.time.DateFormatUtils; import org.mengyun.tcctransaction.api.Compensable; import org.mengyun.tcctransaction.dubbo.context.DubboTransactionContextEditor; import org.mengyun.tcctransaction.sample.capital.domain.entity.CapitalAccount; import org.mengyun.tcctransaction.sample.capital.domain.entity.TradeOrder; import org.mengyun.tcctransaction.sample.capital.domain.repository.CapitalAccountRepository; import org.mengyun.tcctransaction.sample.capital.domain.repository.TradeOrderRepository; import org.mengyun.tcctransaction.sample.dubbo.capital.api.CapitalTradeOrderService; import org.mengyun.tcctransaction.sample.dubbo.capital.api.dto.CapitalTradeOrderDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Calendar; /** * Created by changming.xie on 4/2/16. */ @Service("capitalTradeOrderService") public class CapitalTradeOrderServiceImpl implements CapitalTradeOrderService { @Autowired CapitalAccountRepository capitalAccountRepository; @Autowired TradeOrderRepository tradeOrderRepository; @Override @Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = DubboTransactionContextEditor.class) @Transactional public String record(CapitalTradeOrderDto tradeOrderDto) { try { Thread.sleep(1000l); } 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 foundTradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo()); //check if trade order has been recorded, if yes, return success directly. if (foundTradeOrder == null) { TradeOrder tradeOrder = new TradeOrder( tradeOrderDto.getSelfUserId(), tradeOrderDto.getOppositeUserId(), tradeOrderDto.getMerchantOrderNo(), tradeOrderDto.getAmount() ); try { tradeOrderRepository.insert(tradeOrder); CapitalAccount transferFromAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId()); transferFromAccount.transferFrom(tradeOrderDto.getAmount()); capitalAccountRepository.save(transferFromAccount); } catch (DataIntegrityViolationException e) { //this exception may happen when insert trade order concurrently, if happened, ignore this insert operation. } } return "success"; } @Transactional public void confirmRecord(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()); //check if the trade order status is DRAFT, if yes, return directly, ensure idempotency. if (tradeOrder != null && tradeOrder.getStatus().equals("DRAFT")) { tradeOrder.confirm(); tradeOrderRepository.update(tradeOrder); CapitalAccount transferToAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getOppositeUserId()); transferToAccount.transferTo(tradeOrderDto.getAmount()); capitalAccountRepository.save(transferToAccount); } } @Transactional public void cancelRecord(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()); //check if the trade order status is DRAFT, if yes, return directly, ensure idempotency. 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); } } }
示例演示在下完订单后,使用红包帐户和资金帐户来付款,红包帐户服务和资金帐户服务在不同的系统中。示例中,有两个SOA提供方,一个是CapitalTradeOrderService,代表着资金帐户服务,另一个是RedPacketTradeOrderService,代表着红包帐户服务。
下完订单后,订单状态为DRAFT,在TCC事务中TRY阶段,订单支付服务将订单状态变成PAYING,同时远程调用红包帐户服务和资金帐户服务,将付款方的余额减掉(预留业务资源);如果在TRY阶段,任何一个服务失败,tcc-transaction将自动调用这些服务对应的cancel方法,订单支付服务将订单状态变成PAY_FAILED,同时远程调用红包帐户服务和资金帐户服务,将付款方余额减掉的部分增加回去;如果TRY阶段正常完成,则进入CONFIRM阶段,在CONFIRM阶段(tcc-transaction自动调用),订单支付服务将订单状态变成CONFIRMED,同时远程调用红包帐户服务和资金帐户服务对应的CONFIRM方法,将收款方的余额增加。