Apollo 源碼解析 —— Portal 灰度全量發佈

點擊上方“芋道源碼”,選擇“設爲星標

管她前浪,還是後浪?

能浪的浪,纔是好浪!

每天 8:55 更新文章,每天掉億點點頭髮...

源碼精品專欄

 

摘要: 原創出處 http://www.iocoder.cn/Apollo/portal-publish-namespace-branch-to-master/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!

  • 1. 概述

  • 2. Portal 側

    • 2.1 NamespaceBranchController

    • 2.2 NamespaceBranchService

    • 2.3 ReleaseAPI

  • 3. Admin Service 側

    • 3.1 ReleaseController

    • 3.2 ReleaseService

    • 3.3  NamespaceBranchService

    • 3.4 ClusterService

  • 666. 彩蛋


1. 概述

老艿艿:本系列假定胖友已經閱讀過 《Apollo 官方 wiki 文檔》  ,特別是 《Apollo 官方 wiki 文檔 —— 灰度發佈使用指南》。

本文接 《Apollo 源碼解析 —— Portal 灰度發佈》 ,分享灰度全量發佈。

我們先來看看官方文檔對灰度全量發佈的使用指南,來理解下它的定義和流程。

如果灰度的配置測試下來比較理想,符合預期,那麼就可以操作【全量發佈】。

全量發佈的效果是:

  1. 灰度版本的配置會合並回主版本,在這個例子中,就是主版本的 timeout 會被更新成 3000

  2. 主版本的配置會自動進行一次發佈

  3. 在全量發佈頁面,可以選擇是否保留當前灰度版本,默認爲不保留。

我選擇了不保留灰度版本,所以發佈完的效果就是主版本的配置更新、灰度版本刪除。點擊主版本的實例列表,可以看到10.32.21.22和10.32.21.19都使用了主版本最新的配置。

灰度發佈2

灰度全量發佈,和 《Apollo 源碼解析 —— Portal 發佈配置》 ,差異點在於,多了一步配置合併,所以代碼實現上,有很多相似度。整體系統流程如下:

流程

2. Portal 側

2.1 NamespaceBranchController

apollo-portal 項目中,com.ctrip.framework.apollo.portal.controller.NamespaceBranchController ,提供 Namespace 分支API

#merge(...) 方法,灰度全量發佈,合併子 Namespace 變更的配置 Map 到父 Namespace ,並進行一次 Release 。代碼如下:

  1: @PreAuthorize(value = "@permissionValidator.hasReleaseNamespacePermission(#appId, #namespaceName)")
  2: @RequestMapping(value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/merge", method = RequestMethod.POST)
  3: public ReleaseDTO merge(@PathVariable String appId, @PathVariable String env,
  4:                         @PathVariable String clusterName, @PathVariable String namespaceName,
  5:                         @PathVariable String branchName, @RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch,
  6:                         @RequestBody NamespaceReleaseModel model) {
  7:     // 若是緊急發佈,但是當前環境未允許該操作,拋出 BadRequestException 異常
  8:     if (model.isEmergencyPublish() && !portalConfig.isEmergencyPublishAllowed(Env.fromString(env))) {
  9:         throw new BadRequestException(String.format("Env: %s is not supported emergency publish now", env));
 10:     }
 11:     // 合併子 Namespace 變更的配置 Map 到父 Namespace ,並進行一次 Release
 12:     ReleaseDTO createdRelease = namespaceBranchService.merge(appId, Env.valueOf(env), clusterName, namespaceName, branchName,
 13:             model.getReleaseTitle(), model.getReleaseComment(),
 14:             model.isEmergencyPublish(), deleteBranch);
 15: 
 16:     // 創建 ConfigPublishEvent 對象
 17:     ConfigPublishEvent event = ConfigPublishEvent.instance();
 18:     event.withAppId(appId)
 19:             .withCluster(clusterName)
 20:             .withNamespace(namespaceName)
 21:             .withReleaseId(createdRelease.getId())
 22:             .setMergeEvent(true)
 23:             .setEnv(Env.valueOf(env));
 24:     // 發佈 ConfigPublishEvent 事件
 25:     publisher.publishEvent(event);
 26:     return createdRelease;
 27: }
  • POST /apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/merge 接口,Request Body 傳遞 JSON 對象。

  • @PreAuthorize(...) 註解,調用 PermissionValidator#hasReleaseNamespacePermissio(appId, namespaceName) 方法,校驗是否有發佈配置的權限。後續文章,詳細分享。

  • 第 7 至 10 行:校驗若是緊急發佈,但是當前環境未允許該操作,拋出 BadRequestException 異常。

  • 第 11 至 14 行:調用 NamespaceBranchService#merge(...) 方法,合併子 Namespace 變更的配置 Map 到父 Namespace ,並進行一次 Release 。

  • 第 16 至 25 行:創建 ConfigPublishEvent 對象,並調用 ApplicationEventPublisher#publishEvent(event) 方法,發佈 ConfigPublishEvent 事件。這部分,我們在後續文章分享。

  • 第 26 行:返回 ReleaseDTO 對象。

2.2 NamespaceBranchService

apollo-portal 項目中,com.ctrip.framework.apollo.portal.service.NamespaceBranchService ,提供 Namespace 分支Service 邏輯。

#merge(...) 方法,調用 Admin Service API ,合併子 Namespace 變更的配置 Map 到父 Namespace ,並進行一次 Release 。代碼如下:

  1: @Autowired
  2: private AdminServiceAPI.NamespaceBranchAPI namespaceBranchAPI;
  3: @Autowired
  4: private ReleaseService releaseService;
  5: 
  6: public ReleaseDTO merge(String appId, Env env, String clusterName, String namespaceName,
  7:                         String branchName, String title, String comment,
  8:                         boolean isEmergencyPublish, boolean deleteBranch) {
  9:     // 計算變化的 Item 集合
 10:     ItemChangeSets changeSets = calculateBranchChangeSet(appId, env, clusterName, namespaceName, branchName);
 11:     // 合併子 Namespace 變更的配置 Map 到父 Namespace ,並進行一次 Release
 12:     ReleaseDTO mergedResult = releaseService.updateAndPublish(appId, env, clusterName, namespaceName, title, comment,
 13:                     branchName, isEmergencyPublish, deleteBranch, changeSets);
 14:     // 【TODO 6001】Tracer 日誌
 15:     Tracer.logEvent(TracerEventType.MERGE_GRAY_RELEASE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName));
 16:     return mergedResult;
 17: }
  • 第 10 行:調用 #calculateBranchChangeSet(appId, env, clusterName, namespaceName, branchName) 方法,計算變化的 Item 集合。詳細解析,見 「2.2.1 calculateBranchChangeSet」 。

  • 第12 至 13 行:調用 ReleaseService#updateAndPublish(...) 方法,調用 Admin Service API ,合併子 Namespace 變更的配置 Map 到父 Namespace ,並進行一次 Release 。代碼如下:

    @Autowired
    private AdminServiceAPI.ReleaseAPI releaseAPI;
    
    public ReleaseDTO updateAndPublish(String appId, Env env, String clusterName, String namespaceName,
                                       String releaseTitle, String releaseComment, String branchName,
                                       boolean isEmergencyPublish, boolean deleteBranch, ItemChangeSets changeSets) {
        return releaseAPI.updateAndPublish(appId, env, clusterName, namespaceName, releaseTitle, releaseComment, branchName,
                isEmergencyPublish, deleteBranch, changeSets);
    }
    
    • 方法內部,調用 ReleaseAPI#updateAndPublish(...) 方法,調用 Admin Service API ,合併子 Namespace 變更的配置 Map 到父 Namespace ,並進行一次 Release 。???? 可能會有胖友會問,爲什麼不 NamespaceBranchService 直接調用 ReleaseAPI 呢?ReleaseAPI 屬於 ReleaseService 模塊,對外透明屏蔽該細節。這樣,未來 ReleaseService 想要改實現,可能不是調用 ReleaseAPI 的方法,而是別的方法,也是非常方便的。

  • 第 15 行:【TODO 6001】Tracer 日誌

2.2.1 calculateBranchChangeSet

  1: @Autowired
  2: private ItemsComparator itemsComparator;
  3: @Autowired
  4: private UserInfoHolder userInfoHolder;
  5: @Autowired
  6: private NamespaceService namespaceService;
  7: @Autowired
  8: private ItemService itemService;
  9: 
 10: private ItemChangeSets calculateBranchChangeSet(String appId, Env env, String clusterName, String namespaceName, String branchName) {
 11:     // 獲得父 NamespaceBO 對象
 12:     NamespaceBO parentNamespace = namespaceService.loadNamespaceBO(appId, env, clusterName, namespaceName);
 13:     // 若父 Namespace 不存在,拋出 BadRequestException 異常。
 14:     if (parentNamespace == null) {
 15:         throw new BadRequestException("base namespace not existed");
 16:     }
 17:     // 若父 Namespace 有配置項的變更,不允許合併。因爲,可能存在衝突。
 18:     if (parentNamespace.getItemModifiedCnt() > 0) {
 19:         throw new BadRequestException("Merge operation failed. Because master has modified items");
 20:     }
 21:     // 獲得父 Namespace 的 Item 數組
 22:     List<ItemDTO> masterItems = itemService.findItems(appId, env, clusterName, namespaceName);
 23:     // 獲得子 Namespace 的 Item 數組
 24:     List<ItemDTO> branchItems = itemService.findItems(appId, env, branchName, namespaceName);
 25:     // 計算變化的 Item 集合
 26:     ItemChangeSets changeSets = itemsComparator.compareIgnoreBlankAndCommentItem(parentNamespace.getBaseInfo().getId(), masterItems, branchItems);
 27:     // 設置 `ItemChangeSets.deleteItem` 爲空。因爲子 Namespace 從父 Namespace 繼承配置,但是實際自己沒有那些配置項,所以如果不清空,會導致這些配置項被刪除。
 28:     changeSets.setDeleteItems(Collections.emptyList());
 29:     // 設置 `ItemChangeSets.dataChangeLastModifiedBy` 爲當前管理員
 30:     changeSets.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId());
 31:     return changeSets;
 32: }
  • 第 11 至 20 行,父 Namespace 相關

    • 第 12 行:調用 namespaceService#loadNamespaceBO(appId, env, clusterName, namespaceName) 方法,獲得父 NamespaceBO 對象。該對象,包含了 Namespace 的詳細數據,包括 Namespace 的基本信息、配置集合。詳細解析,點擊方法鏈接查看,筆者已經添加詳細註釋。方法比較冗長,胖友耐心閱讀,其目的是爲了【第 17 至 20 行】的判斷,是否有未發佈的配置變更。

    • 第 13 至 16 行:若 Namespace 不存在,拋出 BadRequestException 異常。

    • 第 17 至 20 行:若 Namespace 有未發佈的配置變更,不允許合併。因爲,可能存在衝突,無法自動解決。此時,需要在 Portal 上將 Namespace 的配置進行一次發佈,或者回退回歷史版本。

  • 第 21 至 30 行:獲得配置變更集合 ItemChangeSets 對象。該對象,我們在 《Apollo 源碼解析 —— Portal 批量變更 Item》 。

    • 第 22 行:調用 ItemService#findItems(appId, env, clusterName, namespaceName)  方法,獲得 Namespace 的 ItemDTO 數組。

    • 第 24 行:調用 ItemService#findItems(appId, env, branchName, namespaceName)  方法,獲得 Namespace 的 ItemDTO 數組。

    • 第 26 行:調用 ItemsComparator#compareIgnoreBlankAndCommentItem(baseNamespaceId, baseItems, targetItems) 方法,計算變化的 Item 集合。詳細解析,點擊方法鏈接查看,筆者已經添加詳細註釋。

    • 第 28 行:設置 ItemChangeSets.deleteItem。因爲 Namespace 從 Namespace 繼承配置,但是實際自己沒有那些配置項,所以如果不設置爲空,會導致合併時,這些配置項被刪除。

2.3 ReleaseAPI

com.ctrip.framework.apollo.portal.api.ReleaseAPI ,實現 API 抽象類,封裝對 Admin Service 的 Release 模塊的 API 調用。代碼如下:

ReleaseAPI

3. Admin Service 側

3.1 ReleaseController

apollo-adminservice 項目中, com.ctrip.framework.apollo.adminservice.controller.ReleaseController ,提供 Release 的 API

#updateAndPublish(...) 方法,合併子 Namespace 變更的配置 Map 到父 Namespace ,並進行一次 Release 。代碼如下:

  1: /**
  2:  * merge branch items to master and publish master
  3:  *
  4:  * @return published result
  5:  */
  6: @Transactional
  7: @RequestMapping(path = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/updateAndPublish", method = RequestMethod.POST)
  8: public ReleaseDTO updateAndPublish(@PathVariable("appId") String appId,
  9:                                    @PathVariable("clusterName") String clusterName,
 10:                                    @PathVariable("namespaceName") String namespaceName,
 11:                                    @RequestParam("releaseName") String releaseName,
 12:                                    @RequestParam("branchName") String branchName,
 13:                                    @RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch, // 是否刪除 Namespace 分支
 14:                                    @RequestParam(name = "releaseComment", required = false) String releaseComment,
 15:                                    @RequestParam(name = "isEmergencyPublish", defaultValue = "false") boolean isEmergencyPublish,
 16:                                    @RequestBody ItemChangeSets changeSets) {
 17:     // 獲得 Namespace
 18:     Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName);
 19:     if (namespace == null) {
 20:         throw new NotFoundException(String.format("Could not find namespace for %s %s %s", appId, clusterName, namespaceName));
 21:     }
 22:     // 合併子 Namespace 變更的配置 Map 到父 Namespace ,並進行一次 Release
 23:     Release release = releaseService.mergeBranchChangeSetsAndRelease(namespace, branchName, releaseName, releaseComment, isEmergencyPublish, changeSets);
 24:     // 若需要刪除子 Namespace ,則進行刪除
 25:     if (deleteBranch) {
 26:         namespaceBranchService.deleteBranch(appId, clusterName, namespaceName, branchName, NamespaceBranchStatus.MERGED, changeSets.getDataChangeLastModifiedBy());
 27:     }
 28:     // 發送 Release 消息
 29:     messageSender.sendMessage(ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName), Topics.APOLLO_RELEASE_TOPIC);
 30:     // 將 Release 轉換成 ReleaseDTO 對象
 31:     return BeanUtils.transfrom(ReleaseDTO.class, release);
 32: }
  • 第 17 至 21 行:調用 NamespaceService#findOne(ppId, clusterName, namespaceName) 方法,獲得 Namespace 對象。

    • 若校驗到不存在,拋出  NotFoundException 異常。

  • 第 23 行:調用 ReleaseService#mergeBranchChangeSetsAndRelease(...) 方法,合併 Namespace 變更的配置 Map 到 Namespace ,並進行一次 Release 。詳細解析,見 「3.2 ReleaseService」 。

  • 第 25 至 27 行:若需要刪除子 Namespace ,即 Portal 中選擇【刪除灰度版本】,調用 NamespaceBranchService#deleteBranch(...) 方法,刪除 Namespace 相關的記錄。詳細解析,見 「3.3  NamespaceBranchService」 。

  • 第 29 行:調用 MessageSender#sendMessage(String message, String channel) 方法,發送發佈消息。

  • 第 31 行:調用 BeanUtils#transfrom(Class<T> clazz, Object src) 方法,將 Release 轉換成 ReleaseDTO 對象。

3.2 ReleaseService

apollo-biz 項目中,com.ctrip.framework.apollo.biz.service.ReleaseService ,提供 Release  的 Service 邏輯給 Admin Service 和 Config Service 。

3.2.1 mergeBranchChangeSetsAndRelease

ReleaseService#mergeBranchChangeSetsAndRelease(...) 方法,合併 Namespace 變更的配置 Map 到 Namespace ,並進行一次 Release 。代碼如下:

  1: // 合併子 Namespace 變更的配置 Map 到父 Namespace ,並進行一次 Release
  2: @Transactional
  3: public Release mergeBranchChangeSetsAndRelease(Namespace namespace, String branchName, String releaseName,
  4:                                                String releaseComment, boolean isEmergencyPublish,
  5:                                                ItemChangeSets changeSets) {
  6:     // 校驗鎖定
  7:     checkLock(namespace, isEmergencyPublish, changeSets.getDataChangeLastModifiedBy());
  8:     // 變更的配置集 合 ItemChangeSets 對象,更新到父 Namespace 中。
  9:     itemSetService.updateSet(namespace, changeSets);
 10: 
 11:     // 獲得子 Namespace 的最新且有效的 Release 對象
 12:     Release branchRelease = findLatestActiveRelease(namespace.getAppId(), branchName, namespace.getNamespaceName());
 13:     // 獲得子 Namespace 的最新且有效的 Release 編號
 14:     long branchReleaseId = branchRelease == null ? 0 : branchRelease.getId();
 15: 
 16:     // 獲得父 Namespace 的配置 Map
 17:     Map<String, String> operateNamespaceItems = getNamespaceItems(namespace);
 18: 
 19:     // 創建 Map ,用於 ReleaseHistory 對象的 `operationContext` 屬性。
 20:     Map<String, Object> operationContext = Maps.newHashMap();
 21:     operationContext.put(ReleaseOperationContext.SOURCE_BRANCH, branchName);
 22:     operationContext.put(ReleaseOperationContext.BASE_RELEASE_ID, branchReleaseId);
 23:     operationContext.put(ReleaseOperationContext.IS_EMERGENCY_PUBLISH, isEmergencyPublish);
 24: 
 25:     // 父 Namespace 進行發佈
 26:     return masterRelease(namespace, releaseName, releaseComment, operateNamespaceItems,
 27:             changeSets.getDataChangeLastModifiedBy(),
 28:             ReleaseOperation.GRAY_RELEASE_MERGE_TO_MASTER, operationContext);
 29: }
  • 第 7 行:調用 #checkLock(...) 方法,校驗鎖定。

  • 第 9 行:調用 ItemService#updateSet(namespace, changeSets) 方法,將變更的配置集 合 ItemChangeSets 對象,更新到 Namespace 中。詳細解析,在 《Apollo 源碼解析 —— Portal 批量變更 Item》 中。

    • 第 17 行:調用 #getNamespaceItems(namespace) 方法,獲得 Namespace 的配置 Map 。因爲上面已經更新過,所以獲得到的是合併後的結果。

  • 第 11 至 23 行:創建 Map ,並設置需要的 KV ,用於 ReleaseHistory 對象的 operationContext 屬性。

    • 第 12 行:調用 #findLatestActiveRelease(...) 方法,獲得 Namespace 的最新有效的 Release 對象。

    • 第 14 行:獲得 Namespace 的最新有效的 Release 編號。

    • 第 21 至 23 行:設置 KV 到 Map 中。

  • 第 26 至 28 行:調用 #masterRelease(...) 方法, Namespace 進行發佈。這塊,和 《Apollo 源碼解析 —— Portal 發佈配置》 的邏輯就統一了,所以詳細解析,見該文。

3.3  NamespaceBranchService

apollo-biz 項目中,com.ctrip.framework.apollo.biz.service.NamespaceBranchService ,提供 Namespace 分支Service 邏輯給 Admin Service 和 Config Service 。

3.3.1 deleteBranch

#deleteBranch(...) 方法,刪除 Namespace 相關的記錄。代碼如下:

  1: @Transactional
  2: public void deleteBranch(String appId, String clusterName, String namespaceName,
  3:                          String branchName, int branchStatus, String operator) {
  4:     // 獲得子 Cluster 對象
  5:     Cluster toDeleteCluster = clusterService.findOne(appId, branchName);
  6:     if (toDeleteCluster == null) {
  7:         return;
  8:     }
  9:     // 獲得子 Namespace 的最後有效的 Release 對象
 10:     Release latestBranchRelease = releaseService.findLatestActiveRelease(appId, branchName, namespaceName);
 11:     // 獲得子 Namespace 的最後有效的 Release 對象的編號
 12:     long latestBranchReleaseId = latestBranchRelease != null ? latestBranchRelease.getId() : 0;
 13: 
 14:     // 創建新的,用於表示刪除的 GrayReleaseRule 的對象
 15:     // update branch rules
 16:     GrayReleaseRule deleteRule = new GrayReleaseRule();
 17:     deleteRule.setRules("[]");
 18:     deleteRule.setAppId(appId);
 19:     deleteRule.setClusterName(clusterName);
 20:     deleteRule.setNamespaceName(namespaceName);
 21:     deleteRule.setBranchName(branchName);
 22:     deleteRule.setBranchStatus(branchStatus); // Namespace 分支狀態
 23:     deleteRule.setDataChangeLastModifiedBy(operator);
 24:     deleteRule.setDataChangeCreatedBy(operator);
 25:     // 更新 GrayReleaseRule
 26:     doUpdateBranchGrayRules(appId, clusterName, namespaceName, branchName, deleteRule, false, -1);
 27: 
 28:     // 刪除子 Cluster
 29:     // delete branch cluster
 30:     clusterService.delete(toDeleteCluster.getId(), operator);
 31: 
 32:     // 創建 ReleaseHistory 對象,並保存
 33:     int releaseOperation = branchStatus == NamespaceBranchStatus.MERGED ? ReleaseOperation.GRAY_RELEASE_DELETED_AFTER_MERGE : ReleaseOperation.ABANDON_GRAY_RELEASE;
 34:     releaseHistoryService.createReleaseHistory(appId, clusterName, namespaceName, branchName, latestBranchReleaseId, latestBranchReleaseId,
 35:             releaseOperation, null, operator);
 36:     // 記錄 Audit 到數據庫中
 37:     auditService.audit("Branch", toDeleteCluster.getId(), Audit.OP.DELETE, operator);
 38: }
  • 第 4 至 8 行:調用 ClusterService#findOne(appId, branchName) 方法,獲得 Cluster 對象。

  • 第 10 行:調用 ReleaseService#findLatestActiveRelease(namespace) 方法,獲得最後有效的 Release 對象。

    • 第 12 行:獲得最後有效的 Release 對象的編號。

  • 第 14 至 24 行:創建新的,用於表示刪除的 GrayReleaseRule 的對象。並且,當前場景,該 GrayReleaseRule 的 branchStatusMERGED

    • 第 26 行:調用 #doUpdateBranchGrayRules(...) 方法,更新 GrayReleaseRule 。詳細解析,見 《Apollo 源碼解析 —— Portal 配置灰度規則》 中。

  • 第 30 行:調用 ClusterService#delte(id, operator) 方法,刪除子 Cluster 相關。詳細解析,見 「3.4 ClusterService」 。

  • 第 32 至 35 行:調用 ReleaseHistoryService#createReleaseHistory(...) 方法,創建 ReleaseHistory 對象,並保存。

  • 第 37 行:記錄 Audit 到數據庫中。

3.4 ClusterService

apollo-biz 項目中,com.ctrip.framework.apollo.biz.service.ClusterService ,提供 Cluster 的 Service 邏輯給 Admin Service 和 Config Service 。

3.4.1 delete

#delete(...) 方法,刪除 Cluster 相關。代碼如下:

@Transactional
public void delete(long id, String operator) {
    // 獲得 Cluster 對象
    Cluster cluster = clusterRepository.findOne(id);
    if (cluster == null) {
        throw new BadRequestException("cluster not exist");
    }
    // 刪除 Namespace
    // delete linked namespaces
    namespaceService.deleteByAppIdAndClusterName(cluster.getAppId(), cluster.getName(), operator);

    // 標記刪除 Cluster
    cluster.setDeleted(true);
    cluster.setDataChangeLastModifiedBy(operator);
    clusterRepository.save(cluster);

    // 記錄 Audit 到數據庫中
    auditService.audit(Cluster.class.getSimpleName(), id, Audit.OP.DELETE, operator);
}
  • 標記刪除 Cluster 和其相關的 Namespace 。代碼比較簡單,胖友自己看看哈。

666. 彩蛋

灰度發佈結束~還有一些其他流程,胖友可以自己看看,例如放棄灰度



歡迎加入我的知識星球,一起探討架構,交流源碼。加入方式,長按下方二維碼噢

已在知識星球更新源碼解析如下:

最近更新《芋道 SpringBoot 2.X 入門》系列,已經 20 餘篇,覆蓋了 MyBatis、Redis、MongoDB、ES、分庫分表、讀寫分離、SpringMVC、Webflux、權限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能測試等等內容。

提供近 3W 行代碼的 SpringBoot 示例,以及超 4W 行代碼的電商微服務項目。

獲取方式:點“在看”,關注公衆號並回復 666 領取,更多內容陸續奉上。

兄弟,一口,點個????

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章