Myth原始碼解析系列之五- 服務啟動原始碼解析

有生發表於2018-01-15

通過前面幾篇文章,我們搭建了環境,也進行了分散式事務服務的體驗,相信大家對myth也有了一個大體直觀的瞭解,接下來我們將正式步入原始碼解析之旅~~

order服務啟動原始碼解析(myth-demo-springcloud , 事務訊息持久化:db方式, mq: rocketmq )

我們首先找到訂單demo( myth-demo-springcloud-order),第一步先找到程式入口,我們框架是基於spring,很自然我們會想到applicationContext.xml,請看一下配置,相信大家還有印象~

     <!--開啟掃描Myth分散式框架包-->
    <context:component-scan base-package="com.github.myth.*"/>
    <!--開啟動態代理-->
    <aop:aspectj-autoproxy expose-proxy="true"/>

    <!--配置啟動類-->
    <bean id="mythTransactionBootstrap" class="com.github.myth.core.bootstrap.MythTransactionBootstrap">
        <property name="repositorySuffix" value="order-service"/>
        <property name="serializer" value="kryo"/>
        <property name="coordinatorQueueMax" value="5000"/>
        <property name="coordinatorThreadMax" value="8"/>
        <property name="rejectPolicy" value="Abort"/>
        <property name="blockingQueueType" value="Linked"/>
        <property name="needRecover" value="true"/>
        <property name="scheduledDelay" value="120"/>
        <property name="scheduledThreadMax" value="4"/>
        <property name="recoverDelayTime" value="120"/>
        <property name="retryMax" value="30"/>
        <property name="repositorySupport" value="db"/>
        <property name="mythDbConfig">
            <bean class="com.github.myth.common.config.MythDbConfig">
                <property name="url"
                          value="jdbc:mysql://127.0.0.1:3306/myth?useUnicode=true&amp;characterEncoding=utf8"/>
                <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
                <property name="password" value="123456"/>
                <property name="username" value="root"/>
            </bean>
        </property>
    </bean>
複製程式碼

通過以上配置我們知道首先需要開啟Aop切面,再掃描框架的包,重點我們來關注 MythTransactionBootstrap

MythTransactionBootstrap 原始碼解析

我們先來看看序列圖,從圖中我們得知主要涉及MythTransactionBootstrap 、MythInitServiceImpl、CoordinatorServiceImpl三個類,後面我們逐步來走

序列圖

MythTransactionBootstrap 簡單類圖

MythTransactionBootstrap 類關係圖

廢話不多說,直接上菜(精品四菜一湯O(∩_∩)O哈哈~):

@Component
public class MythTransactionBootstrap extends MythConfig implements ApplicationContextAware {


    private final MythInitService mythInitService;

    @Autowired
    public MythTransactionBootstrap(MythInitService mythInitService) {
        this.mythInitService = mythInitService;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringBeanUtils.getInstance().setCfgContext((ConfigurableApplicationContext) applicationContext);
        start(this);
    }


    private void start(MythConfig tccConfig) {
        mythInitService.initialization(tccConfig);
    }
}
複製程式碼

我們發現MythTransactionBootstrap繼承 MythConfig, 所以能獲取在xml配置的屬性資訊,它還實現了 ApplicationContextAware介面, 因此當spring容器初始化的時候,會自動的將ApplicationContext注入進來

我們繼續跟蹤,進入mythInitService.initialization方法

    /**
     * Myth分散式事務初始化方法
     *
     * @param mythConfig TCC配置
     */
    @Override
    public void initialization(MythConfig mythConfig) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> LOGGER.error("系統關閉")));
        try {
            loadSpiSupport(mythConfig);
            coordinatorService.start(mythConfig);
        } catch (Exception ex) {
            LogUtil.error(LOGGER, "Myth事務初始化異常:{}", ex::getMessage);
            //非正常關閉
            System.exit(1);
        }
        LogUtil.info(LOGGER, () -> "Myth事務初始化成功!");
    }
複製程式碼

根據註釋我們知道,這裡主要進行相關初始化操作,程式碼比較簡潔,裡面主要包含兩個方法,我們先來看第一個loadSpiSupport(mythConfig), LoadSpiSupport 中主要採用jdk自帶的spi載入機制,spi機制很常見,比如開源框架spring, 阿里的HSF,dubbo等開源框架都有使用該機制,如果有不明白的小夥伴,可以自行google

spi 實現的相關類

spi

在LoadSpiSupport方法裡其實主要通過spi機制做了幾件事:

  1. 載入注入分散式事務訊息的序列化方式
<property name="serializer" value="kryo"/><!-- 這裡預設是 kryo -->
//關鍵程式碼如下
//載入完後的serializer,進行設定併入住到spring上下文中
serializer.ifPresent(coordinatorService::setSerializer);
serializer.ifPresent(s-> SpringBeanUtils.getInstance().registerBean(ObjectSerializer.class.getName(), s));
複製程式碼
  1. 載入注入分散式事務訊息的持久化方式
<property name="repositorySupport" value="db"/><!-- 這裡預設是 db -->
//關鍵程式碼如下
//將CoordinatorRepository實現注入到spring容器中
repositoryOptional.ifPresent(repository -> {
            serializer.ifPresent(repository::setSerializer);
            SpringBeanUtils.getInstance().registerBean(CoordinatorRepository.class.getName(), repository);
        });
複製程式碼

在這裡我們就可以發現spi的好處,可以在不改任何程式碼的情況下,只需做少許配置就可以靈活設定自己想要的序列化及持久化方式~~ 有木有~~

接下來我們來看第二個方法,coordinatorService.start(mythConfig)

/**
    * 儲存本地事務日誌
    *
    * @param mythConfig 配置資訊
    * @throws MythException 異常
    */
   @Override
   public void start(MythConfig mythConfig) throws MythException {
       this.mythConfig = mythConfig;

      //在前面我們已經做了注入操作,注入物件為JdbcCoordinatorRepository
       coordinatorRepository = SpringBeanUtils.getInstance().getBean(CoordinatorRepository.class);

      // 根據配置我們這裡的值為:order-service
       final String repositorySuffix = buildRepositorySuffix(mythConfig.getRepositorySuffix());
       //初始化spi 協調資源儲存
       coordinatorRepository.init(repositorySuffix, mythConfig);
       //初始化 協調資源執行緒池
       initCoordinatorPool();

       //如果需要自動恢復 開啟執行緒 排程執行緒池,進行恢復
       if (mythConfig.getNeedRecover()) {
           scheduledAutoRecover();
       }
   }
複製程式碼

緊接著我們進入,JdbcCoordinatorRepository.init(repositorySuffix, mythConfig), 詳見程式碼

/**
     * 初始化操作
     *
     * @param modelName  模組名稱
     * @param mythConfig 配置資訊
     */
    @Override
    public void init(String modelName, MythConfig mythConfig) {
        dataSource = new DruidDataSource();
        final MythDbConfig tccDbConfig = mythConfig.getMythDbConfig();
        dataSource.setUrl(tccDbConfig.getUrl());
        dataSource.setDriverClassName(tccDbConfig.getDriverClassName());
        dataSource.setUsername(tccDbConfig.getUsername());
        dataSource.setPassword(tccDbConfig.getPassword());
        dataSource.setInitialSize(tccDbConfig.getInitialSize());
        dataSource.setMaxActive(tccDbConfig.getMaxActive());
        dataSource.setMinIdle(tccDbConfig.getMinIdle());
        dataSource.setMaxWait(tccDbConfig.getMaxWait());
        dataSource.setValidationQuery(tccDbConfig.getValidationQuery());
        dataSource.setTestOnBorrow(tccDbConfig.getTestOnBorrow());
        dataSource.setTestOnReturn(tccDbConfig.getTestOnReturn());
        dataSource.setTestWhileIdle(tccDbConfig.getTestWhileIdle());
        dataSource.setPoolPreparedStatements(tccDbConfig.getPoolPreparedStatements());
        dataSource.setMaxPoolPreparedStatementPerConnectionSize(tccDbConfig.getMaxPoolPreparedStatementPerConnectionSize());
        this.tableName = RepositoryPathUtils.buildDbTableName(modelName);
        executeUpdate(SqlHelper.buildCreateTableSql(tccDbConfig.getDriverClassName(), tableName));
    }
複製程式碼

這裡主要初始化資料來源,然後建立order服務對應的一張分散式事務訊息表,用來儲存分散式事務訊息,走完程式碼我們會建立一張表:myth_order_service。 到此init程式碼已走完, 接下來我們來看 initCoordinatorPool()

private void initCoordinatorPool() {
        synchronized (LOGGER) {
            QUEUE = new LinkedBlockingQueue<>(mythConfig.getCoordinatorQueueMax());
            final int coordinatorThreadMax = mythConfig.getCoordinatorThreadMax();
            final MythTransactionThreadPool threadPool = SpringBeanUtils.getInstance().getBean(MythTransactionThreadPool.class);
            final ExecutorService executorService = threadPool.newCustomFixedThreadPool(coordinatorThreadMax);
            LogUtil.info(LOGGER, "啟動協調資源操作執行緒數量為:{}", () -> coordinatorThreadMax);
            for (int i = 0; i < coordinatorThreadMax; i++) {
                executorService.execute(new Worker());
            }

        }
    }

    /**
    * 執行緒執行器
    */
   class Worker implements Runnable {

       @Override
       public void run() {
           execute();
       }

       private void execute() {
           while (true) {
               try {
                   final CoordinatorAction coordinatorAction = QUEUE.take();
                   if (coordinatorAction != null) {
                       final int code = coordinatorAction.getAction().getCode();
                       if (CoordinatorActionEnum.SAVE.getCode() == code) {
                           save(coordinatorAction.getMythTransaction());
                       } else if (CoordinatorActionEnum.DELETE.getCode() == code) {
                           remove(coordinatorAction.getMythTransaction().getTransId());
                       } else if (CoordinatorActionEnum.UPDATE.getCode() == code) {
                           update(coordinatorAction.getMythTransaction());
                       }
                   }
               } catch (Exception e) {
                   e.printStackTrace();
                   LogUtil.error(LOGGER, "執行協調命令失敗:{}", e::getMessage);
               }
           }

       }
   }
複製程式碼

這個方法裡首先初始化一個LinkedBlockingQueue佇列QUEUE,該佇列作用主要用於存放分散式訊息內容,其次建立了一個執行緒池,執行緒池中執行的任務Worker,主要消費QUEUE佇列訊息進行分散式訊息的持久化操作,細心的童鞋發現這裡用到了命令模式,我們這裡的持久化為mysql。

下一步我們來看 scheduledAutoRecover方法

new ScheduledThreadPoolExecutor(1,
               MythTransactionThreadFactory.create("MythAutoRecoverService",
                       true))
               .scheduleWithFixedDelay(() -> {
                   LogUtil.debug(LOGGER, "auto recover execute delayTime:{}",
                           () -> mythConfig.getScheduledDelay());
                   try {
                       final List<MythTransaction> mythTransactionList =
                               coordinatorRepository.listAllByDelay(acquireData());
                       if (CollectionUtils.isNotEmpty(mythTransactionList)) {
                           mythTransactionList
                                   .forEach(mythTransaction -> {
                                       final Boolean success = sendMessage(mythTransaction);
                                       //傳送成功 ,更改狀態
                                       if (success) {
                                           coordinatorRepository.updateStatus(mythTransaction.getTransId(),
                                                   MythStatusEnum.COMMIT.getCode());
                                       }
                                   });
                       }

                   } catch (Exception e) {
                       e.printStackTrace();
                   }
               }, 30, mythConfig.getScheduledDelay(), TimeUnit.SECONDS);


複製程式碼

前面我們建立了一個執行緒池進行分散式訊息的持久化操作,這裡就是如何使用這些資料,建立一個排程執行緒,定時取出指定有效時間範圍內且訊息狀態為開始的資料,然後再往mq中投遞訊息,注意這裡有個開關needRecover, 根據註釋得知只需要在事務發起方我們才需要開啟,預設關閉狀態,我們這裡是order服務,即為事務發起方,所以需要開啟

  <property name="needRecover" value="true"/>
複製程式碼

最後我們再來看下mq訊息傳送部分,通過applicationContext.xml,我們發現我們只放開了rocketmq的宣告,固我們訊息傳送使用的是rocketmq

spi

到此我們order服務啟動原始碼部分已走完,啟動成功後控制檯輸出內容:

swagger api

account ,inventory服務啟動原始碼解析

account ,inventory 啟動流程與order服務大體相似,主要有以下區別

  1. account 建立分散式訊息表名為: myth_account_service;inventory 建立分散式訊息表名為: myth_inventory_service
  2. needRecover開關這裡為關閉,不需要開啟排程執行緒

好了,這一章我們完成了服務啟動的原始碼解析,後面我們將進入下單流程部分,感覺怎麼樣 很簡單有木有 是不是你的菜 O(∩_∩)O~

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

相關文章