• TCC细读


    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方法,将收款方的余额增加。

  • 相关阅读:
    Java的自动拆箱和装箱
    记录一次买阿里云服务器、建站的经验
    java中的位运算符
    java String拼接时候的一个小问题
    java获取各类容器和数组的长度
    java多线程:循环屏障
    Spring框架10:spring编程式事务控制
    Spring框架9:spring实现声明式事务控制
    Spring框架8:spring使用AOP实现事务控制
    C++ 中的bind
  • 原文地址:https://www.cnblogs.com/it-worker365/p/10065781.html
Copyright © 2020-2023  润新知