Myth原始碼解析系列之六- 訂單下單流程原始碼解析(發起者)

有生發表於2018-01-15

前面一章我們走完了服務啟動的原始碼,這次我們進入下單流程的原始碼解析~

訂單下單流程原始碼解析(發起者)

首先保證myth-demo-springcloud-order、myth-demo-springcloud-inventory、myth-demo-springcloud-eureka、myth-demo-springcloud-account 服務以正常啟動

進入原始碼分析前,這裡先給大家預熱下,介紹下幾個關鍵部分

  1. 事務角色
public enum MythRoleEnum {
    /**
     * Start myth role enum.
     * 這裡主要為: orderServer
     */
    START(1, "發起者"),
    /**
     * Consumer myth role enum.
     */
    LOCAL(2, "本地執行"),
    /**
     * Provider myth role enum.
     * 這裡主要為: accountServer, inventoryServer
     */
    PROVIDER(3, "提供者")
  }
複製程式碼
  1. 事務狀態

public enum MythStatusEnum {

    /**
     * Commit myth status enum.
     */
    COMMIT(1, "已經提交"),
    /**
     * Begin myth status enum.
     */
    BEGIN(2, "開始"),
    /**
     * Failure myth status enum.
     */
    FAILURE(4, "失敗")
  }
  這裡主要列了我們所使用的部分,沒用的忽略
複製程式碼

這個東西為什麼在這裡先講,主要是為了讓大家先了解下有這個東西,這樣有助於後續程式碼理解 ~~ 正所謂擒賊先擒王,抓住重點部位你就離成功不遠鳥 O(∩_∩)O

時序圖

時序圖

訂單下單介面入口:http://localhost:8884/swagger-ui.html

swagger api

輸入: 下單數量count: 1, 金額amount: 100 ,狠狠點 【Try it out!】,發起下單請求, 我們會進入OrderController.orderPay方法

@PostMapping(value = "/orderPay")
@ApiOperation(value = "訂單下單介面(注意這裡模擬的是建立訂單並進行下單扣減庫存等操作)")
public String orderPay(@RequestParam(value = "count") Integer count,
                       @RequestParam(value = "amount") BigDecimal amount) {

    return orderService.orderPay(count, amount);

}

接著進入orderServiceImpl.orderPay 方法
@Override
    public String orderPay(Integer count, BigDecimal amount) {
        final Order order = buildOrder(count, amount);
        final int rows = orderMapper.save(order);

        if (rows > 0) {
            paymentService.makePayment(order);
        }


        return "success";
    }

複製程式碼

這裡我們發現封裝了order物件然後進行了持久化操作,成功後,我們呼叫paymentService.makePayment(order); 重點來了,我們先來瞅瞅paymentService.makePayment方法體的程式碼

    @Override
    @Myth(destination = "")
    public void makePayment(Order order) {


        //檢查資料 這裡只是demo 只是demo 只是demo

        final AccountDO accountDO =
                accountClient.findByUserId(order.getUserId());

        if(accountDO.getBalance().compareTo(order.getTotalAmount())<= 0){
            throw new MythRuntimeException("餘額不足!");
        }

        final InventoryDO inventoryDO =
                inventoryClient.findByProductId(order.getProductId());

        if(inventoryDO.getTotalInventory() < order.getCount()){
            throw new MythRuntimeException("庫存不足!");
        }

        order.setStatus(OrderStatusEnum.PAY_SUCCESS.getCode());
        orderMapper.update(order);
        //扣除使用者餘額
        AccountDTO accountDTO = new AccountDTO();
        accountDTO.setAmount(order.getTotalAmount());
        accountDTO.setUserId(order.getUserId());

        accountClient.payment(accountDTO);

        //進入扣減庫存操作
        InventoryDTO inventoryDTO = new InventoryDTO();
        inventoryDTO.setCount(order.getCount());
        inventoryDTO.setProductId(order.getProductId());
        inventoryClient.decrease(inventoryDTO);
    }
複製程式碼

方法具體業務我們暫且先不看,重點我們關注方法頭部是不是有個@Myth註解,這就是實現分散式事務的關鍵,分散式事務主要通過@Myth註解來關聯實現,這是基於aop切面思想,既然這樣那麼必定會有一個切入點,下面我們找到myth-core工程AbstractMythTransactionAspect類,原來這就是定義@Myth切入點的地方啊~

@Aspect
public abstract class AbstractMythTransactionAspect {

    private MythTransactionInterceptor mythTransactionInterceptor;

    public void setMythTransactionInterceptor(MythTransactionInterceptor mythTransactionInterceptor) {
        this.mythTransactionInterceptor = mythTransactionInterceptor;
    }

    @Pointcut("@annotation(com.github.myth.annotation.Myth)")
    public void mythTransactionInterceptor() {

    }

    @Around("mythTransactionInterceptor()")
    public Object interceptCompensableMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        return mythTransactionInterceptor.interceptor(proceedingJoinPoint);
    }

    /**
     * spring Order 介面,該值的返回直接會影響springBean的載入順序
     *
     * @return int 型別
     */
    public abstract int getOrder();
}
複製程式碼

可以知道Spring實現類的方法凡是加了**@Myth**註解的,在呼叫的時候,都會進行 mythTransactionInterceptor.interceptor呼叫。也就是說一個呼叫鏈中,只要有標記該註解的業務方法,都會被加入到同一組分散式事務當中來, 其目的就是保證這麼些個業務方法,要麼全部執行成功,反之全部不執行~

AbstractMythTransactionAspect是一個抽象類,下面我們看看它的實現

圖片發自簡書App

我們發現針對每種rpc都對應著有一個實現類,下面我們來看

@Aspect
@Component
                                    public class SpringCloudMythTransactionAspect extends AbstractMythTransactionAspect implements Ordered {

    @Autowired
    public SpringCloudMythTransactionAspect(SpringCloudMythTransactionInterceptor springCloudMythTransactionInterceptor) {
        this.setMythTransactionInterceptor(springCloudMythTransactionInterceptor);
    }

    public void init() {
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
複製程式碼

我們注意到它實現了Spring的Ordered介面,並重寫了 getOrder 方法,返回了 Ordered.HIGHEST_PRECEDENCE 那麼可以知道,他是優先順序最高的切面,這裡dubbo,motan也是一樣。 我們再來看MythTransactionInterceptor 介面,也是類似,springcloud、dubbo、motan都對應一個實現類

圖片發自簡書App

看完這裡我們知道,我們知道此時程式碼不會直接進入paymentService.makePayment方法,而是先進入切面SpringCloudMythTransactionInterceptor,程式碼如下

@Override
    public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
        MythTransactionContext mythTransactionContext = TransactionContextLocal.getInstance().get();
        if (Objects.nonNull(mythTransactionContext) &&
                mythTransactionContext.getRole() == MythRoleEnum.LOCAL.getCode()) {
            mythTransactionContext = TransactionContextLocal.getInstance().get();
        } else {
            RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
            HttpServletRequest request = requestAttributes == null ? null : ((ServletRequestAttributes) requestAttributes).getRequest();
            String context = request == null ? null : request.getHeader(CommonConstant.MYTH_TRANSACTION_CONTEXT);
            if (StringUtils.isNoneBlank(context)) {
                mythTransactionContext =
                        GsonUtils.getInstance().fromJson(context, MythTransactionContext.class);
            }
        }
        return mythTransactionAspectService.invoke(mythTransactionContext, pjp);
    }
複製程式碼

走到這裡,因為第一次進來這些變數都沒有值(按理說第一次一般都記憶深刻啊~~),所以我們會直接進入mythTransactionAspectService.invoke(mythTransactionContext, pjp), 此時mythTransactionContext為null, 接下來我們進入MythTransactionAspectServiceImpl.invoke方法

public Object invoke(MythTransactionContext mythTransactionContext, ProceedingJoinPoint point) throws Throwable {
        final Class clazz = mythTransactionFactoryService.factoryOf(mythTransactionContext);
        final MythTransactionHandler mythTransactionHandler =
                (MythTransactionHandler) SpringBeanUtils.getInstance().getBean(clazz);
        return mythTransactionHandler.handler(point, mythTransactionContext);
    }
複製程式碼

然後再進入MythTransactionFactoryServiceImpl.factoryOf

/**
     * 返回 實現TxTransactionHandler類的名稱
     *
     * @param context 事務上下文
     * @return Class<T>
     * @throws Throwable 丟擲異常
     */
    @Override
    public Class factoryOf(MythTransactionContext context) throws Throwable {
        //如果事務還沒開啟或者 myth事務上下文是空, 那麼應該進入發起呼叫
        if (!mythTransactionManager.isBegin() && Objects.isNull(context)) {
            return StartMythTransactionHandler.class;
        } else {
            if (context.getRole() == MythRoleEnum.LOCAL.getCode()) {
                return LocalMythTransactionHandler.class;
            }
            return ActorMythTransactionHandler.class;
        }
    }
複製程式碼

重點來了,前面我們介紹了分散式事務角色,這裡就是判斷分散式事務角色的入口,根據判斷我們進入發起者角色,也就是StartMythTransactionHandler.handler方法

/**
     * Myth分散式事務處理介面
     *
     * @param point                  point 切點
     * @param mythTransactionContext myth事務上下文
     * @return Object
     * @throws Throwable 異常
     */
    @Override
    public Object handler(ProceedingJoinPoint point, MythTransactionContext mythTransactionContext) throws Throwable {
        try {
            //主要防止併發問題,對事務日誌的寫造成壓力,加了鎖進行處理
            try {
                LOCK.lock();
                mythTransactionManager.begin(point);
            } finally {
                LOCK.unlock();
            }
           return  point.proceed();
        } finally {
            //傳送訊息
            mythTransactionManager.sendMessage();
            mythTransactionManager.cleanThreadLocal();
            TransactionContextLocal.getInstance().remove();
        }
    }
複製程式碼

接下來進入mythTransactionManager.begin(point);

public MythTransaction begin(ProceedingJoinPoint point) {
        LogUtil.debug(LOGGER, () -> "開始執行Myth分散式事務!start");
        MythTransaction mythTransaction = getCurrentTransaction();
        if (Objects.isNull(mythTransaction)) {

            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();

            Class<?> clazz = point.getTarget().getClass();

            mythTransaction = new MythTransaction();
            mythTransaction.setStatus(MythStatusEnum.BEGIN.getCode());
            mythTransaction.setRole(MythRoleEnum.START.getCode());
            mythTransaction.setTargetClass(clazz.getName());
            mythTransaction.setTargetMethod(method.getName());
        }
        //儲存當前事務資訊
        coordinatorCommand.execute(new CoordinatorAction(CoordinatorActionEnum.SAVE, mythTransaction));

        //當前事務儲存到ThreadLocal
        CURRENT.set(mythTransaction);

        //設定myth事務上下文,這個類會傳遞給遠端
        MythTransactionContext context = new MythTransactionContext();

        //設定事務id
        context.setTransId(mythTransaction.getTransId());

        //設定為發起者角色
        context.setRole(MythRoleEnum.START.getCode());

        TransactionContextLocal.getInstance().set(context);

        return mythTransaction;

    }
複製程式碼

這個方法主要做兩件事,首先封裝分散式事務訊息(MythTransaction)進行持久化(這裡重點關注下mythTransaction.setStatus(MythStatusEnum.BEGIN.getCode事務狀態為開始, mythTransaction.setRole(MythRoleEnum.START.getCode());事務角色為發起者,後續會用到),然後再設定MythTransactionContext事務上下文,這個主要用來傳輸給遠端服務,這裡遠端服務可以理解為事務參與方。(注意這裡兩個訊息物件都分別放到了ThreadLocal變數中)

關注下持久化操作是如何做的?

//儲存當前事務資訊
        coordinatorCommand.execute(new CoordinatorAction(CoordinatorActionEnum.SAVE, mythTransaction));

//跟蹤進去最後執行的是下面這段程式碼
        @Override
           public Boolean submit(CoordinatorAction coordinatorAction) {
               try {
                   QUEUE.put(coordinatorAction);
               } catch (InterruptedException e) {
                   e.printStackTrace();
                   return Boolean.FALSE;
               }
               return Boolean.TRUE;
           }

複製程式碼

我們發現訊息是傳送給一個QUEUE佇列,大家還記得之前講服務啟動原始碼解析,專門開了一個執行緒池任務來消費QUEUE佇列做訊息持久化操作,對的,訊息就是在這裡放進去的。

好了,到此我們begin方法已經走完, 下面會呼叫point.proceed就正式進入到業務方法paymentService.makePayment中,這個方法裡主要也是做兩件事,一個呼叫Account服務進行賬戶餘額扣減,另一個呼叫Inventory服務進行庫存扣減,這兩個是通過呼叫介面來實現, 關鍵程式碼如下

accountClient.payment(accountDTO);
//AccountClient.payment方法定義
  /**
    * 使用者賬戶付款
    *
    * @param accountDO 實體類
    * @return true 成功
    */
   @PostMapping("/account-service/account/payment")
   @Myth(destination = "account", target = AccountService.class)
   Boolean payment(@RequestBody AccountDTO accountDO);

    inventoryClient.decrease(inventoryDTO);
//InventoryClient.decrease方法定義
/**
  * 庫存扣減
  *
  * @param inventoryDTO 實體物件
  * @return true 成功
  */
@Myth(destination = "inventory",target = InventoryService.class)
@RequestMapping("/inventory-service/inventory/decrease")
Boolean decrease(@RequestBody InventoryDTO inventoryDTO);
複製程式碼

注意到麼,賬戶扣款和庫存扣減方法都標記有@Myth註解,而另外兩個查詢方法是沒有的。我們知道springAop的特性,在介面上加註解,是無法進入切面的,所以我們在這裡,要採用rpc框架的某些特性來幫助我們獲取到 @Myth註解資訊, 這一步很重要。這裡我們演示的是springcloud,所以進入myth-springcloud工程的MythFeignHandler類。(其中dubbo,與motan這部分實現分別對應:DubboMythTransactionFilter 和 MotanMythTransactionFilter 類, 都是通過框架自身過濾器特性來實現, 邏輯與springcloud一樣,只是實現上有少許差別~
這裡重點提一下dubbo,dubbo是增加了動態代理方式來處理的,因為當(參與者)服務當機後,zk心跳檢測失敗,因此呼叫者不會走到filter就會返回,所以需要增加代理方式在發起遠端呼叫之前來獲取事務訊息並進行儲存,避免事務資訊丟失而導致後續不能成功向參與者發起(MQ)本地事務執行動作。dubbo代理類實現:MythInvokerInvocationHandler ,其他rpc實現童鞋們一看便知, 不再過於贅述~)

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {

            final Myth myth = method.getAnnotation(Myth.class);
            if (Objects.isNull(myth)) {
                return this.handlers.get(method).invoke(args);
            }
            try {
                final MythTransactionManager mythTransactionManager =
                        SpringBeanUtils.getInstance().getBean(MythTransactionManager.class);

                final MythParticipant participant = buildParticipant(myth, method, args);
                if (Objects.nonNull(participant)) {
                    mythTransactionManager.registerParticipant(participant);
                }
                return this.handlers.get(method).invoke(args);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                return new Object();
            }


        }
    }


    private MythParticipant buildParticipant(Myth myth, Method method, Object[] args) {

        final MythTransactionContext mythTransactionContext =
                TransactionContextLocal.getInstance().get();
        MythParticipant participant;
        if (Objects.nonNull(mythTransactionContext)) {

            final Class declaringClass = myth.target();

            MythInvocation mythInvocation = new MythInvocation(declaringClass,
                    method.getName(),
                    method.getParameterTypes(), args);

            final Integer pattern = myth.pattern().getCode();

            //封裝呼叫點
            participant = new MythParticipant(
                    mythTransactionContext.getTransId(),
                    myth.destination(),
                    pattern,
                    mythInvocation);

            return participant;
        }
        return null;
    }
複製程式碼

從原始碼得知,只有方法上打了@Myth註解的才會進入後續邏輯,否則直接執行返回,我們詳細看下buildParticipant(myth, method, args);方法,首先從TransactionContextLocal.getInstance().get獲取事務上下文物件MythTransactionContext,這裡有值嗎? 是的因為前面我們已經設定值了,所以這裡能拿到值,這裡大家注意下destination,target這兩個屬性, destination這是就是訊息中介軟體使用的佇列名稱,不同服務定義不同佇列來傳送訊息,target為目標業務class,獲取到相關值後對MythParticipant進行封裝並返回,接下來我們繼續往下走,進入MythTransactionManager.registerParticipant

public void registerParticipant(MythParticipant participant) {
       final MythTransaction transaction = this.getCurrentTransaction();
       transaction.registerParticipant(participant);
       coordinatorService.updateParticipant(transaction);
}
複製程式碼

我們發現這裡其實就是對前面MythTransaction持久化的一個更新操作,主要更新呼叫其他子系統介面需要執行的class,method等資訊,這個訊息後面會通過mq進行投遞,其他子系統通過消費來進行本地執行。

好了,到此我們已經走完了發起者大部分了,緊接著就是呼叫執行account,Inventory服務了,也就是參與者部分,這裡我們發現還有finally快部分程式碼未走完, 稍稍休息下,詳情請見下回分解~~

大家有任何問題或者建議歡迎溝通 ,歡迎加入QQ群:162614487 進行交流

相關文章