Myth原始碼解析系列之七- 訂單下單流程原始碼解析(參與者)

有生發表於2018-01-15

前面一章我們走完了訂單下單流程發起者部分的原始碼,這次我們進入參與者部分原始碼解析~

訂單下單流程原始碼解析(參與者)

前面order服務中已經發起了對account服務的呼叫,接下來進入account服務扣款介面的實現部分

  //order服務呼叫端
  @PostMapping("/account-service/account/payment")
  @Myth(destination = "account", target = AccountService.class)
  Boolean payment(@RequestBody AccountDTO accountDO);

 //account服務介面實現 AccountServiceImpl.payment(AccountDTO accountDTO)
 @Override
    @Myth(destination = "account")
    public boolean payment(AccountDTO accountDTO) {
        LOGGER.info("============springcloud執行付款介面===============");
        final AccountDO accountDO = accountMapper.findByUserId(accountDTO.getUserId());
        if (accountDO.getBalance().compareTo(accountDTO.getAmount()) <= 0) {
            throw new MythRuntimeException("spring cloud account-service 資金不足!");
        }
        accountDO.setBalance(accountDO.getBalance().subtract(accountDTO.getAmount()));
        accountDO.setUpdateTime(new Date());
        final int update = accountMapper.update(accountDO);
        if (update != 1) {
            throw new MythRuntimeException("spring cloud account-service 資金不足!");
        }
        return Boolean.TRUE;
    }

複製程式碼

我們發現在實現類方法頭部也進行了@Myth 註解的標記,AccountServiceImpl 是一個實現類,因此這裡必然也會走aop切面,aop切面流程入口同order服務相同,區別在於order為發起方,而account,inventory為參與者,我們是否還記得角色判斷程式碼實現部分?MythTransactionFactoryServiceImpl.factoryOf 我們再來回顧下程式碼

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

複製程式碼

判斷條件要想進入參與者角色分支,這裡事務必須開啟狀態 或者 myth事務上下文必須有值 ,這兩個條件又是在哪裡進行了設值呢? 我們往回看看呼叫處,找到 SpringCloudMythTransactionInterceptor.interceptor(ProceedingJoinPoint pjp) 方法

@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);
    }
複製程式碼

因為第一次進來,顯然mythTransactionContext值為空,進入else分支,這裡我們發現是從request請求頭中獲取的事務上下文資訊的。 既然是從請求頭資訊中拿到資料, 那必然在呼叫端要先設定對不對, 我們找到myth-springcloud工程下MythRestTemplateInterceptor


//springcloud
@Configuration
public class MythRestTemplateInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        final MythTransactionContext mythTransactionContext =
                TransactionContextLocal.getInstance().get();
        requestTemplate.header(CommonConstant.MYTH_TRANSACTION_CONTEXT,
                GsonUtils.getInstance().toJson(mythTransactionContext));
    }

}

// motan
@Component
public class MotanMythTransactionInterceptor implements MythTransactionInterceptor {

    private final MythTransactionAspectService mythTransactionAspectService;

    @Autowired
    public MotanMythTransactionInterceptor(MythTransactionAspectService mythTransactionAspectService) {
        this.mythTransactionAspectService = mythTransactionAspectService;
    }

    @Override
    public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
        MythTransactionContext mythTransactionContext = null;

        final Request request = RpcContext.getContext().getRequest();
        if (Objects.nonNull(request)) {
            final Map<String, String> attachments = request.getAttachments();
            if (attachments != null && !attachments.isEmpty()) {
                String context = attachments.get(CommonConstant.MYTH_TRANSACTION_CONTEXT);
                mythTransactionContext =
                        GsonUtils.getInstance().fromJson(context, MythTransactionContext.class);
            }
        } else {
            mythTransactionContext = TransactionContextLocal.getInstance().get();
        }

        return mythTransactionAspectService.invoke(mythTransactionContext, pjp);
    }
}

//dubbo
@Component
public class DubboMythTransactionInterceptor implements MythTransactionInterceptor {

    private final MythTransactionAspectService mythTransactionAspectService;

    @Autowired
    public DubboMythTransactionInterceptor(MythTransactionAspectService mythTransactionAspectService) {
        this.mythTransactionAspectService = mythTransactionAspectService;
    }

    @Override
    public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
        final String context = RpcContext.getContext().getAttachment(CommonConstant.MYTH_TRANSACTION_CONTEXT);
        MythTransactionContext mythTransactionContext;
        if (StringUtils.isNoneBlank(context)) {
            mythTransactionContext =
                    GsonUtils.getInstance().fromJson(context, MythTransactionContext.class);
        }else{
            mythTransactionContext= TransactionContextLocal.getInstance().get();
        }
        return mythTransactionAspectService.invoke(mythTransactionContext, pjp);
    }
}
複製程式碼

我們發現是通過實現feign的RequestInterceptor介面來實現mythTransactionContext設定到頭資訊中的,這裡dubbo,motan也類似,只是實現方式不同。這裡也是實現分散式事務的最關鍵一部分,通過同一個事務上下文來關聯多子系統之間事務關係,是分散式事務實現的核心所在。

接下來我們進入參與者角色ActorMythTransactionHandler.handler

public Object handler(ProceedingJoinPoint point, MythTransactionContext mythTransactionContext) throws Throwable {

        try {
            //處理併發問題
            LOCK.lock();
            //先儲存事務日誌
            mythTransactionManager.actorTransaction(point, mythTransactionContext);

            //發起呼叫 執行try方法
            final Object proceed = point.proceed();

            //執行成功 更新狀態為commit
            mythTransactionManager.updateStatus(mythTransactionContext.getTransId(),
                    MythStatusEnum.COMMIT.getCode());

            return proceed;

        } catch (Throwable throwable) {
            LogUtil.error(LOGGER, "執行分散式事務介面失敗,事務id:{}", mythTransactionContext::getTransId);
            mythTransactionManager.updateStatus(mythTransactionContext.getTransId(),
                    MythStatusEnum.FAILURE.getCode());
            throw throwable;
        } finally {
            LOCK.unlock();
            TransactionContextLocal.getInstance().remove();
        }
    }
複製程式碼

參與者實現比較簡單, 執行業務方法前主要封裝MythTransaction訊息(狀態為:開始,角色為:參與者),然後進行持久化操作,再執行業務方法,如果成功更新MythTransaction狀態為:COMMIT,反之狀態為:FAILURE,到這裡我們參與者也是走完了 ~~ 那我們這個流程是不是完了呢? 其實還沒有,上一章最後我們留了一小塊,我們再來回顧下

/**
    * 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();
       }
   }

複製程式碼

在走account流程時,其實發起者一直在 point.proceed(); 這裡等待返回結果呢,這裡需要等待orderService.orderPay業務方法全部執行完才會返回,然而我們上面才走account一個扣款介面,還有inventory扣減庫存介面,這裡inventory介面與account介面角色都是參與者,流程上是一樣的,只是業務不一樣而已,這裡也就不做過多介紹了,童鞋們自己過一遍即可。

到這裡有童鞋可能就要說了,myth打著是一個基於訊息佇列解決分散式事務框架,但是前面講了這麼多,貌似都未涉及到訊息佇列啊, 好了,我們這就帶你們飛進mq,我們來看 mythTransactionManager.sendMessage(); 直接進入關鍵程式碼部分 CoordinatorServiceImpl.sendMessage 方法


public Boolean sendMessage(MythTransaction mythTransaction) {
        final List<MythParticipant> mythParticipants = mythTransaction.getMythParticipants();
            /*
             * 這裡的這個判斷很重要,不為空,表示本地的方法執行成功,需要執行遠端的rpc方法
             * 為什麼呢,因為我會在切面的finally裡面傳送訊息,意思是切面無論如何都需要傳送mq訊息
             * 那麼考慮問題,如果本地執行成功,呼叫rpc的時候才需要發
             * 如果本地異常,則不需要傳送mq ,此時mythParticipants為空
             */
        if (CollectionUtils.isNotEmpty(mythParticipants)) {

            for (MythParticipant mythParticipant : mythParticipants) {
                MessageEntity messageEntity =
                        new MessageEntity(mythParticipant.getTransId(),
                                mythParticipant.getMythInvocation());
                try {
                    final byte[] message = serializer.serialize(messageEntity);
                    getMythMqSendService().sendMessage(mythParticipant.getDestination(),
                            mythParticipant.getPattern(),
                            message);
                } catch (Exception e) {
                    e.printStackTrace();
                    return Boolean.FALSE;
                }
            }
            //這裡為什麼要這麼做呢? 主要是為了防止在極端情況下,發起者執行過程中,突然自身down 機
            //造成訊息未傳送,新增一個狀態標記,如果出現這種情況,通過定時任務傳送訊息
            this.updateStatus(mythTransaction.getTransId(), MythStatusEnum.COMMIT.getCode());
        }
        return Boolean.TRUE;
    }


    private synchronized MythMqSendService getMythMqSendService() {
       if (mythMqSendService == null) {
           synchronized (CoordinatorServiceImpl.class) {
               if (mythMqSendService == null) {
                   mythMqSendService = SpringBeanUtils.getInstance().getBean(MythMqSendService.class);
               }
           }
       }
       return mythMqSendService;
   }
複製程式碼

根據程式碼我們知道,這裡主要是將分散式訊息封裝至MessageEntity中,然後進行序列化傳送至mq訊息佇列,這裡有兩點要注意:

  1. serializer.serialize(messageEntity); serializer物件為服務啟動時通過spi機制載入注入
  2. mythMqSendService 為applicationContext.xml 配置的rocketmq

既然產生了訊息,必然會有消費者去消費,我們回到 myth-demo-springcloud-account工程下的RocketmqConsumer類 , account服務對應topic=“account”, Inventory服務對應的topic=“inventory”, 我們進入關鍵程式碼部分: mythMqReceiveService.processMessage(message);


public Boolean processMessage(byte[] message) {
        try {
            MessageEntity entity;
            try {
                entity = serializer.deSerialize(message, MessageEntity.class);
            } catch (MythException e) {
                e.printStackTrace();
                throw new MythRuntimeException(e.getMessage());
            }
            /*
             * 1 檢查該事務有沒被處理過,已經處理過的 則不處理
             * 2 發起發射呼叫,呼叫介面,進行處理
             * 3 記錄本地日誌
             */
            LOCK.lock();

            final String transId = entity.getTransId();
            final MythTransaction mythTransaction = findByTransId(transId);

            //如果是空或者是失敗的
            if (Objects.isNull(mythTransaction)
                    || mythTransaction.getStatus() == MythStatusEnum.FAILURE.getCode()) {
                try {

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

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

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

                    TransactionContextLocal.getInstance().set(context);
                    executeLocalTransaction(entity.getMythInvocation());

                    //會進入LocalMythTransactionHandler  那裡有儲存

                } catch (Exception e) {
                    e.printStackTrace();
                    throw new MythRuntimeException(e.getMessage());
                } finally {
                    TransactionContextLocal.getInstance().remove();
                }
            }


        } finally {
            LOCK.unlock();
        }
        return Boolean.TRUE;

    }

    @SuppressWarnings("unchecked")
    private void executeLocalTransaction(MythInvocation mythInvocation) throws Exception {
        if (Objects.nonNull(mythInvocation)) {
            final Class clazz = mythInvocation.getTargetClass();
            final String method = mythInvocation.getMethodName();
            final Object[] args = mythInvocation.getArgs();
            final Class[] parameterTypes = mythInvocation.getParameterTypes();
            final Object bean = SpringBeanUtils.getInstance().getBean(clazz);
            MethodUtils.invokeMethod(bean, method, args, parameterTypes);
            LogUtil.debug(LOGGER, "Myth執行本地協調事務:{}", () -> mythInvocation.getTargetClass()
                    + ":" + mythInvocation.getMethodName());
        }
    }
複製程式碼

消費者在接收到訊息後,進行反序列化,拿到transId查詢分散式事務訊息MythTransaction,這裡能查到資料嗎? 答案是肯定的,因為前面我們走服務呼叫時就已經對事務訊息進行了持久化操作,我們發現這裡需要進行事務狀態判斷, mythTransaction 為空或者事務狀態為FAILURE才執行本地協調事務,因為正常介面呼叫會走一次,所以這裡需要避免重複執行,導致資料不一致。

好了,到此為止我們原始碼解析部分就全部講解完畢, myth實現是沒有回滾機制的,這裡有別於tcc,也不同於2pc, 只要發起者本地事務執行成功,那麼認為這個事務就必須一直執行下去,直到成功為止,即使在呼叫其他子系統介面出現超時或者本地當機這種異常情況,待服務恢復後便會通過排程執行緒藉助mq把事務訊息傳輸給參與者,來達到最終的一致性!

異常情況處理機制介紹

  1. order服務異常(此時還未涉及呼叫account和Inventory服務),order本地事務回滾,account及Inventory服務無需處理。
  2. order服務呼叫account或Inventory服務超時,account及Inventory服務未接受到請求,此時order會通過MQ將分散式事務訊息投遞給消費者即(account及Inventory服務),account及Inventory消費訊息後查詢本地事務訊息(此時事務狀態為開始),並執行本地協調事務,以保證資料一致性。
  3. order服務呼叫account或Inventory服務超時,account及Inventory服務已接受到請求並處理,此時order還是會通過MQ將分散式事務訊息投遞給消費者即(account及Inventory服務),account及Inventory消費訊息查詢本地事務訊息,判斷事務狀態,因前面服務以接收到請求並處理,所以此時事務狀態為提交,固不會再次執行本地協調事務,因此這裡是支援冪等的。
  4. 如account及Inventory服務已接受到請求處理出現異常,此種情況會修改事務訊息狀態為:FAILURE,此時使用者可登陸管理後臺檢視到異常事務資訊,這裡需要使用者自行決定後續處理邏輯,其目的是要保證資料一致性。

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

相關文章