当前位置:   article > 正文

Apollo 源码解析 —— Admin Service 锁定 Namespace

apollo-adminservice

点击上方“芋道源码”,选择“设为星标

做积极的人,而不是积极废人!

源码精品专栏

 

摘要: 原创出处 http://www.iocoder.cn/Apollo/admin-service-lock-namespace/ 「芋道源码」欢迎转载,保留摘要,谢谢!

  • 1. 概述

  • 2. NamespaceLock

  • 3. 限制修改人

  • 4. 限制发布人

  • 5. 解锁

  • 666. 彩蛋


1. 概述

老艿艿:本系列假定胖友已经阅读过 《Apollo 官方 wiki 文档》  。

本文分享 Admin Service 锁定 Namespace 。可通过设置 ConfigDB 的 ServerConfig 的 "namespace.lock.switch""true" 开启。效果如下:

  • ???? 一次配置修改只能是一个人

  • ???? 一次配置发布只能是另一个人

也就是说,开启后,一次配置修改并发布,需要两个人

默认"false" ,即关闭。

2. NamespaceLock

com.ctrip.framework.apollo.biz.entity.NamespaceLock ,继承 BaseEntity 抽象类,Namespace Lock 实体。代码如下:

  1. @Entity
  2. @Table(name = "NamespaceLock")
  3. @Where(clause = "isDeleted = 0")
  4. public class NamespaceLock extends BaseEntity {
  5.     /**
  6.      * Namespace 编号 {@link Namespace}
  7.      *
  8.      * 唯一索引
  9.      */
  10.     @Column(name = "NamespaceId")
  11.     private long namespaceId;
  12. }
  • 写操作 Item 时,创建 Namespace 对应的 NamespaceLock 记录到 ConfigDB 数据库中,从而记录配置修改

  • namespaceId 字段,Namespace 编号,指向对应的 Namespace 。

    • 该字段上有唯一索引。通过该锁定,保证并发写操作时,同一个 Namespace 有且仅有创建一条 NamespaceLock 记录。

2.1 NamespaceLockService

apollo-biz 项目中,com.ctrip.framework.apollo.biz.service.NamespaceLockService ,提供 NamespaceLock  的 Service 逻辑给 Admin Service 和 Config Service 。代码如下:

  1. @Service
  2. public class NamespaceLockService {
  3.     @Autowired
  4.     private NamespaceLockRepository namespaceLockRepository;
  5.     public NamespaceLock findLock(Long namespaceId) {
  6.         return namespaceLockRepository.findByNamespaceId(namespaceId);
  7.     }
  8.     @Transactional
  9.     public NamespaceLock tryLock(NamespaceLock lock) {
  10.         return namespaceLockRepository.save(lock);
  11.     }
  12.     @Transactional
  13.     public void unlock(Long namespaceId) {
  14.         namespaceLockRepository.deleteByNamespaceId(namespaceId);
  15.     }
  16. }

2.2 NamespaceLockRepository

com.ctrip.framework.apollo.biz.repository.NamespaceLockRepository ,继承 org.springframework.data.repository.PagingAndSortingRepository 接口,提供 NamespaceLock 的数据访问 给 Admin Service 和 Config Service 。代码如下:

  1. public interface NamespaceLockRepository extends PagingAndSortingRepository<NamespaceLock, Long> {
  2.   NamespaceLock findByNamespaceId(Long namespaceId);
  3.   Long deleteByNamespaceId(Long namespaceId);
  4. }

3. 限制修改人

apollo-adminservice 项目中,在 aop 模块中,通过 Spring AOP 记录 NamespaceLock ,从而实现锁定 Namespace ,限制修改人。

3.1 @PreAcquireNamespaceLock

com.ctrip.framework.apollo.adminservice.aop.@PreAcquireNamespaceLock注解,标识方法需要获取到 Namespace 的 Lock 才能执行。

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface PreAcquireNamespaceLock {
  4. }

目前添加了 @PreAcquireNamespaceLock 注解的方法如下图:

标记 @PreAcquireNamespaceLock 注解的方法

3.2 NamespaceAcquireLockAspect

com.ctrip.framework.apollo.adminservice.aop.NamespaceAcquireLockAspect ,获得 NamespaceLock 切面。

定义切面

  1. @Aspect
  2. @Component
  3. public class NamespaceAcquireLockAspect {
  4.     private static final Logger logger = LoggerFactory.getLogger(NamespaceAcquireLockAspect.class);
  5.     @Autowired
  6.     private NamespaceLockService namespaceLockService;
  7.     @Autowired
  8.     private NamespaceService namespaceService;
  9.     @Autowired
  10.     private ItemService itemService;
  11.     @Autowired
  12.     private BizConfig bizConfig;
  13.     // create item
  14.     @Before("@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, item, ..)")
  15.     public void requireLockAdvice(String appId, String clusterName, String namespaceName, ItemDTO item) {
  16.         // 尝试锁定
  17.         acquireLock(appId, clusterName, namespaceName, item.getDataChangeLastModifiedBy());
  18.     }
  19.     // update item
  20.     @Before("@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, itemId, item, ..)")
  21.     public void requireLockAdvice(String appId, String clusterName, String namespaceName, long itemId, ItemDTO item) {
  22.         // 尝试锁定
  23.         acquireLock(appId, clusterName, namespaceName, item.getDataChangeLastModifiedBy());
  24.     }
  25.     // update by change set
  26.     @Before("@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, changeSet, ..)")
  27.     public void requireLockAdvice(String appId, String clusterName, String namespaceName, ItemChangeSets changeSet) {
  28.         // 尝试锁定
  29.         acquireLock(appId, clusterName, namespaceName, changeSet.getDataChangeLastModifiedBy());
  30.     }
  31.     // delete item
  32.     @Before("@annotation(PreAcquireNamespaceLock) && args(itemId, operator, ..)")
  33.     public void requireLockAdvice(long itemId, String operator) {
  34.         // 获得 Item 对象。若不存在,抛出 BadRequestException 异常
  35.         Item item = itemService.findOne(itemId);
  36.         if (item == null) {
  37.             throw new BadRequestException("item not exist.");
  38.         }
  39.         // 尝试锁定
  40.         acquireLock(item.getNamespaceId(), operator);
  41.     }
  42.     
  43.     // ... 省略其他方法
  44. }
  • @Aspect 注解,标记为表面类。

  • @Before 注解,标记切入执行方法

  • 调用 #acquireLock(...) 方法,尝试锁定。

acquireLock

  1. void acquireLock(String appId, String clusterName, String namespaceName, String currentUser) {
  2.     // 当关闭锁定 Namespace 开关时,直接返回
  3.     if (bizConfig.isNamespaceLockSwitchOff()) {
  4.         return;
  5.     }
  6.     // 获得 Namespace 对象
  7.     Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName);
  8.     // 尝试锁定
  9.     acquireLock(namespace, currentUser);
  10. }
  11. void acquireLock(long namespaceId, String currentUser) {
  12.     // 当关闭锁定 Namespace 开关时,直接返回
  13.     if (bizConfig.isNamespaceLockSwitchOff()) {
  14.         return;
  15.     }
  16.     // 获得 Namespace 对象
  17.     Namespace namespace = namespaceService.findOne(namespaceId);
  18.     // 尝试锁定
  19.     acquireLock(namespace, currentUser);
  20. }
  • BizConfig#isNamespaceLockSwitchOff() 方法,判断是否关闭锁定 Namespace 的开关。代码如下:

    1. public boolean isNamespaceLockSwitchOff() {
    2.     return !getBooleanProperty("namespace.lock.switch"false);
    3. }
  • #acquireLock(namespace, currentUser) 方法,尝试锁定。代码如下:

    1.   1: private void acquireLock(Namespace namespace, String currentUser) {
    2.   2:     // 当 Namespace 为空时,抛出 BadRequestException 异常
    3.   3:     if (namespace == null) {
    4.   4:         throw new BadRequestException("namespace not exist.");
    5.   5:     }
    6.   6:     long namespaceId = namespace.getId();
    7.   7:     // 获得 NamespaceLock 对象
    8.   8:     NamespaceLock namespaceLock = namespaceLockService.findLock(namespaceId);
    9.   9:     // 当 NamespaceLock 不存在时,尝试锁定
    10.  10:     if (namespaceLock == null) {
    11.  11:         try {
    12.  12:             // 锁定
    13.  13:             tryLock(namespaceId, currentUser);
    14.  14:             // lock success
    15.  15:         } catch (DataIntegrityViolationException e) {
    16.  16:             // 锁定失败,获得 NamespaceLock 对象
    17.  17:             // lock fail
    18.  18:             namespaceLock = namespaceLockService.findLock(namespaceId);
    19.  19:             // 校验锁定人是否是当前管理员
    20.  20:             checkLock(namespace, namespaceLock, currentUser);
    21.  21:         } catch (Exception e) {
    22.  22:             logger.error("try lock error", e);
    23.  23:             throw e;
    24.  24:         }
    25.  25:     } else {
    26.  26:         // check lock owner is current user
    27.  27:         // 校验锁定人是否是当前管理员
    28.  28:         checkLock(namespace, namespaceLock, currentUser);
    29.  29:     }
    30.  30: }
    • NamespaceLock.dataChangeCreatedBy 不是当前管理员时,抛出 BadRequestException 异常,从而实现限制修改人

    • 创建 NamespaceLock 对象,并调用 NamespaceLockService#tryLock(NamespaceLock) 方法,进行保存。

    • 第 8 行:调用 NamespaceLockService#findLock(namespaceId) 方法,获得 NamespaceLock 对象。

    • 第 10 至 14 行:当 NamespaceLock 不存在时,调用 #tryLock(namespaceId, currentUser) 方法,尝试锁定。代码如下:

      1. private void tryLock(long namespaceId, String user) {
      2.     // 创建 NamespaceLock 对象
      3.     NamespaceLock lock = new NamespaceLock();
      4.     lock.setNamespaceId(namespaceId);
      5.     lock.setDataChangeCreatedBy(user); // 管理员
      6.     lock.setDataChangeLastModifiedBy(user); // 管理员
      7.     // 保存 NamespaceLock 对象
      8.     namespaceLockService.tryLock(lock);
      9. }
    • 第 15 至 18 行:发生 DataIntegrityViolationException 异常,说明保存 NamespaceLock 对象失败,由于唯一索引 namespaceId 冲突,调用 NamespaceLockService#tryLock(NamespaceLock) 方法,获得最新的 NamespaceLock 对象。

    • 第 20 行 || 第 28 行:调用 #checkLock(namespace, namespaceLock, currentUser) 方法,校验锁定人是否是当前管理员。代码如下:

      1. private void checkLock(Namespace namespace, NamespaceLock namespaceLock, String currentUser) {
      2.     // 当 NamespaceLock 不存在,抛出 ServiceException 异常
      3.     if (namespaceLock == null) {
      4.         throw new ServiceException(String.format("Check lock for %s failed, please retry.", namespace.getNamespaceName()));
      5.     }
      6.     // 校验锁定人是否是当前管理员。若不是,抛出 BadRequestException 异常
      7.     String lockOwner = namespaceLock.getDataChangeCreatedBy();
      8.     if (!lockOwner.equals(currentUser)) {
      9.         throw new BadRequestException("namespace:" + namespace.getNamespaceName() + " is modified by " + lockOwner);
      10.     }
      11. }

3.3 NamespaceUnlockAspect

com.ctrip.framework.apollo.adminservice.aop.NamespaceUnlockAspect ,释放 NamespaceLock 切面。???? 在配置多次修改,恢复到原有状态( 即最后一次 Release  的配置) 。因此,NamespaceUnlockAspect 的类注释如下:

unlock namespace if is redo operation.

For example: If namespace has a item K1 = v1

  • First operate: change k1 = v2 (lock namespace)

  • Second operate: change k1 = v1 (unlock namespace)

定义切面

  1. @Aspect
  2. @Component
  3. public class NamespaceUnlockAspect {
  4.     private Gson gson = new Gson();
  5.     @Autowired
  6.     private NamespaceLockService namespaceLockService;
  7.     @Autowired
  8.     private NamespaceService namespaceService;
  9.     @Autowired
  10.     private ItemService itemService;
  11.     @Autowired
  12.     private ReleaseService releaseService;
  13.     @Autowired
  14.     private BizConfig bizConfig;
  15.     // create item
  16.     @After("@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, item, ..)")
  17.     public void requireLockAdvice(String appId, String clusterName, String namespaceName, ItemDTO item) {
  18.         // 尝试解锁
  19.         tryUnlock(namespaceService.findOne(appId, clusterName, namespaceName));
  20.     }
  21.     // update item
  22.     @After("@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, itemId, item, ..)")
  23.     public void requireLockAdvice(String appId, String clusterName, String namespaceName, long itemId, ItemDTO item) {
  24.         // 尝试解锁
  25.         tryUnlock(namespaceService.findOne(appId, clusterName, namespaceName));
  26.     }
  27.     // update by change set
  28.     @After("@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, changeSet, ..)")
  29.     public void requireLockAdvice(String appId, String clusterName, String namespaceName, ItemChangeSets changeSet) {
  30.         // 尝试解锁
  31.         tryUnlock(namespaceService.findOne(appId, clusterName, namespaceName));
  32.     }
  33.     // delete item
  34.     @After("@annotation(PreAcquireNamespaceLock) && args(itemId, operator, ..)")
  35.     public void requireLockAdvice(long itemId, String operator) {
  36.         // 获得 Item 对象。若不存在,抛出 BadRequestException 异常
  37.         Item item = itemService.findOne(itemId);
  38.         if (item == null) {
  39.             throw new BadRequestException("item not exist.");
  40.         }
  41.         // 尝试解锁
  42.         tryUnlock(namespaceService.findOne(item.getNamespaceId()));
  43.     }
  44.     
  45.     // ... 省略其他方法
  46. }
  • @Aspect 注解,标记为表面类。

  • @After 注解,标记切入执行方法

  • 调用 #tryUnlock(...) 方法,尝试解锁。

tryUnlock

  1. private void tryUnlock(Namespace namespace) {
  2.     // 当关闭锁定 Namespace 开关时,直接返回
  3.     if (bizConfig.isNamespaceLockSwitchOff()) {
  4.         return;
  5.     }
  6.     // 若当前 Namespace 的配置恢复原有状态,释放锁,即删除 NamespaceLock
  7.     if (!isModified(namespace)) {
  8.         namespaceLockService.unlock(namespace.getId());
  9.     }
  10. }
  • #isModified(Namespace) 方法,若当前 Namespace 的配置恢复原有状态

  • NamespaceLockService#unlock(namespaceId) 方法,释放锁,即删除 NamespaceLock 。

isModified

  1.   1: boolean isModified(Namespace namespace) {
  2.   2:     // 获得当前 Namespace 的最后有效的 Release 对象
  3.   3:     Release release = releaseService.findLatestActiveRelease(namespace);
  4.   4:     // 获得当前 Namespace 的 Item 集合
  5.   5:     List<Item> items = itemService.findItemsWithoutOrdered(namespace.getId());
  6.   6
  7.   7:     // 如果无 Release 对象,判断是否有普通的 Item 配置项。若有,则代表修改过。
  8.   8:     if (release == null) {
  9.   9:         return hasNormalItems(items);
  10.  10:     }
  11.  11
  12.  12:     // 获得 Release 的配置 Map
  13.  13:     Map<String, String> releasedConfiguration = gson.fromJson(release.getConfigurations(), GsonType.CONFIG);
  14.  14:     // 获得当前 Namespace 的配置 Map
  15.  15:     Map<String, String> configurationFromItems = generateConfigurationFromItems(namespace, items);
  16.  16:     // 对比两个 配置 Map ,判断是否相等。
  17.  17:     MapDifference<String, String> difference = Maps.difference(releasedConfiguration, configurationFromItems);
  18.  18:     return !difference.areEqual();
  19.  19: }
  • 第 3 行:调用 ReleaseService#findLatestActiveRelease(Namespace) 方法,获得当前 Namespace 的最后有效的 Release 对象。Release 的 configurations 字段,记录每次发布的完整配置 Map,代码如下:

    1. // Release.java
    2. @Column(name = "Configurations", nullable = false)
    3. @Lob
    4. private String configurations;
    • x

    • 例如:

      1. {
      2.     "key1""value1"
      3.     "key2""value2"
      4.     "key3""value3"
      5.     "key4""value4"
      6. }
  • 第 5 行:调用 ItemService#findItemsWithoutOrdered(namespaceId) 方法,获得当前 Namespace 的 Item 集合

  • ========== 第一种情况 ==========

  • 第 8 至 10 行:如果无 Release 对象,调用 #hasNormalItems(List<Item>) 方法,判断是否有普通的 Item 配置项。若有,则代表修改过。代码如下:

    1. private boolean hasNormalItems(List<Item> items) {
    2.     for (Item item : items) {
    3.         if (!StringUtils.isEmpty(item.getKey())) { // 非空串的 Key ,因为注释和空行的 Item 的 Key 为空串。
    4.             return true;
    5.         }
    6.     }
    7.     return false;
    8. }
  • ========== 第二种情况 ==========

  • 第 13 行:获得 Release 的配置 Map

  • 第 15 行:调用 #generateConfigurationFromItems(namespace, items) 方法,获得当前 Namespace 的配置 Map 。代码如下:

    1. private Map<String, String> generateConfigurationFromItems(Namespace namespace, List<Item> namespaceItems) {
    2.     Map<String, String> configurationFromItems = Maps.newHashMap();
    3.     // 获得父 Namespace 对象
    4.     Namespace parentNamespace = namespaceService.findParentNamespace(namespace);
    5.     // 若无父 Namespace ,使用自己的配置
    6.     // parent namespace
    7.     if (parentNamespace == null) {
    8.         generateMapFromItems(namespaceItems, configurationFromItems);
    9.         // 若有父 Namespace ,说明是灰度发布,合并父 Namespace 的配置 + 自己的配置项
    10.     } else { //child namespace
    11.         Release parentRelease = releaseService.findLatestActiveRelease(parentNamespace);
    12.         if (parentRelease != null) {
    13.             configurationFromItems = gson.fromJson(parentRelease.getConfigurations(), GsonType.CONFIG);
    14.         }
    15.         generateMapFromItems(namespaceItems, configurationFromItems);
    16.     }
    17.     return configurationFromItems;
    18. }
    19. private Map<String, String> generateMapFromItems(List<Item> items, Map<String, String> configurationFromItems) {
    20.     for (Item item : items) {
    21.         String key = item.getKey();
    22.         // 跳过注释和空行的配置项
    23.         if (StringUtils.isBlank(key)) {
    24.             continue;
    25.         }
    26.         configurationFromItems.put(key, item.getValue());
    27.     }
    28.     return configurationFromItems;
    29. }
    • 关于 Namespace 部分的代码,胖友看完灰度发布的内容,再回过头理解。

  • 第 17 至 18 行:使用 Guava MapDifference 对比两个 配置 Map ,判断是否相等。

4. 限制发布人

发布配置时,调用 ReleaseService#publish(...) 方法时,在方法内部,会调用 #checkLock(Namespace namespace, boolean isEmergencyPublish, String operator) 方法,校验锁定人是否是当前管理员。代码如下:

  1.   1: private void checkLock(Namespace namespace, boolean isEmergencyPublish, String operator) {
  2.   2:     if (!isEmergencyPublish) { // 非紧急发布
  3.   3:         // 获得 NamespaceLock 对象
  4.   4:         NamespaceLock lock = namespaceLockService.findLock(namespace.getId());
  5.   5:         // 校验锁定人是否是当前管理员。若是,抛出 BadRequestException 异常
  6.   6:         if (lock != null && lock.getDataChangeCreatedBy().equals(operator)) {
  7.   7:             throw new BadRequestException("Config can not be published by yourself.");
  8.   8:         }
  9.   9:     }
  10.  10: }
  • 第 2 行:非紧急发布,可通过设置 PortalDB 的 ServerConfig 的"emergencyPublish.supported.envs" 配置开启对应的 Env 们。例如,emergencyPublish.supported.envs = dev

  • 第 6 至 8 行:当 NamespaceLock.dataChangeCreatedBy 当前管理员时,抛出 BadRequestException 异常,从而实现限制修改人

5. 解锁

发布配置时,调用 ReleaseService#createRelease(...) 方法时,在方法内部,会调用 NamespaceLockService#unlock(namespaceId) 方法,释放 NamespaceLock 。代码如下:

  1. private Release createRelease(Namespace namespace, String name, String comment,
  2.                               Map<String, String> configurations, String operator) {
  3.     // ... 省略无关代码
  4.     // 释放 NamespaceLock
  5.     namespaceLockService.unlock(namespace.getId());
  6.     // ... 省略无关代码
  7. }

666. 彩蛋

小文一篇,睡觉~~~



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 20 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/733450
推荐阅读
相关标签
  

闽ICP备14008679号