老艿艿:本系列假定胖友已经阅读过 《Apollo 官方 wiki 文档》 。
本文分享 Admin Service 锁定 Namespace 。可通过设置 ConfigDB 的 ServerConfig 的 "namespace.lock.switch"
为 "true"
开启。效果如下:
也就是说,开启后,一次配置修改并发布,需要两个人。
默认为 "false"
,即关闭。
com.ctrip.framework.apollo.biz.entity.NamespaceLock
,继承 BaseEntity 抽象类,Namespace Lock 实体。代码如下:
@Entity
@Table(name = "NamespaceLock")
@Where(clause = "isDeleted = 0")
public class NamespaceLock extends BaseEntity {
/**
* Namespace 编号 {@link Namespace}
*
* 唯一索引
*/
@Column(name = "NamespaceId")
private long namespaceId;
}
namespaceId
字段,Namespace 编号,指向对应的 Namespace 。在 apollo-biz
项目中,com.ctrip.framework.apollo.biz.service.NamespaceLockService
,提供 NamespaceLock 的 Service 逻辑给 Admin Service 和 Config Service 。代码如下:
@Service
public class NamespaceLockService {
@Autowired
private NamespaceLockRepository namespaceLockRepository;
public NamespaceLock findLock(Long namespaceId) {
return namespaceLockRepository.findByNamespaceId(namespaceId);
}
@Transactional
public NamespaceLock tryLock(NamespaceLock lock) {
return namespaceLockRepository.save(lock);
}
@Transactional
public void unlock(Long namespaceId) {
namespaceLockRepository.deleteByNamespaceId(namespaceId);
}
}
com.ctrip.framework.apollo.biz.repository.NamespaceLockRepository
,继承 org.springframework.data.repository.PagingAndSortingRepository
接口,提供 NamespaceLock 的数据访问 给 Admin Service 和 Config Service 。代码如下:
public interface NamespaceLockRepository extends PagingAndSortingRepository<NamespaceLock, Long> {
NamespaceLock findByNamespaceId(Long namespaceId);
Long deleteByNamespaceId(Long namespaceId);
}
在 apollo-adminservice
项目中,在 aop
模块中,通过 Spring AOP 记录 NamespaceLock ,从而实现锁定 Namespace ,限制修改人。
com.ctrip.framework.apollo.adminservice.aop.@PreAcquireNamespaceLock
,注解,标识方法需要获取到 Namespace 的 Lock 才能执行。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAcquireNamespaceLock {
}
目前添加了 @PreAcquireNamespaceLock
注解的方法如下图:
标记 @PreAcquireNamespaceLock
注解的方法
com.ctrip.framework.apollo.adminservice.aop.NamespaceAcquireLockAspect
,获得 NamespaceLock 切面。
定义切面
@Aspect
@Component
public class NamespaceAcquireLockAspect {
private static final Logger logger = LoggerFactory.getLogger(NamespaceAcquireLockAspect.class);
@Autowired
private NamespaceLockService namespaceLockService;
@Autowired
private NamespaceService namespaceService;
@Autowired
private ItemService itemService;
@Autowired
private BizConfig bizConfig;
// create item
@Before("@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, item, ..)")
public void requireLockAdvice(String appId, String clusterName, String namespaceName, ItemDTO item) {
// 尝试锁定
acquireLock(appId, clusterName, namespaceName, item.getDataChangeLastModifiedBy());
}
// update item
@Before("@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, itemId, item, ..)")
public void requireLockAdvice(String appId, String clusterName, String namespaceName, long itemId, ItemDTO item) {
// 尝试锁定
acquireLock(appId, clusterName, namespaceName, item.getDataChangeLastModifiedBy());
}
// update by change set
@Before("@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, changeSet, ..)")
public void requireLockAdvice(String appId, String clusterName, String namespaceName, ItemChangeSets changeSet) {
// 尝试锁定
acquireLock(appId, clusterName, namespaceName, changeSet.getDataChangeLastModifiedBy());
}
// delete item
@Before("@annotation(PreAcquireNamespaceLock) && args(itemId, operator, ..)")
public void requireLockAdvice(long itemId, String operator) {
// 获得 Item 对象。若不存在,抛出 BadRequestException 异常
Item item = itemService.findOne(itemId);
if (item == null) {
throw new BadRequestException("item not exist.");
}
// 尝试锁定
acquireLock(item.getNamespaceId(), operator);
}
// ... 省略其他方法
}
@Aspect
注解,标记为表面类。@Before
注解,标记切入执行方法前。#acquireLock(...)
方法,尝试锁定。acquireLock
void acquireLock(String appId, String clusterName, String namespaceName, String currentUser) {
// 当关闭锁定 Namespace 开关时,直接返回
if (bizConfig.isNamespaceLockSwitchOff()) {
return;
}
// 获得 Namespace 对象
Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName);
// 尝试锁定
acquireLock(namespace, currentUser);
}
void acquireLock(long namespaceId, String currentUser) {
// 当关闭锁定 Namespace 开关时,直接返回
if (bizConfig.isNamespaceLockSwitchOff()) {
return;
}
// 获得 Namespace 对象
Namespace namespace = namespaceService.findOne(namespaceId);
// 尝试锁定
acquireLock(namespace, currentUser);
}
BizConfig#isNamespaceLockSwitchOff()
方法,判断是否关闭锁定 Namespace 的开关。代码如下:
public boolean isNamespaceLockSwitchOff() { return !getBooleanProperty("namespace.lock.switch", false); }#acquireLock(namespace, currentUser)
方法,尝试锁定。代码如下:
1: private void acquireLock(Namespace namespace, String currentUser) { 2: // 当 Namespace 为空时,抛出 BadRequestException 异常 3: if (namespace == null) { 4: throw new BadRequestException("namespace not exist."); 5: } 6: long namespaceId = namespace.getId(); 7: // 获得 NamespaceLock 对象 8: NamespaceLock namespaceLock = namespaceLockService.findLock(namespaceId); 9: // 当 NamespaceLock 不存在时,尝试锁定 10: if (namespaceLock == null) { 11: try { 12: // 锁定 13: tryLock(namespaceId, currentUser); 14: // lock success 15: } catch (DataIntegrityViolationException e) { 16: // 锁定失败,获得 NamespaceLock 对象 17: // lock fail 18: namespaceLock = namespaceLockService.findLock(namespaceId); 19: // 校验锁定人是否是当前管理员 20: checkLock(namespace, namespaceLock, currentUser); 21: } catch (Exception e) { 22: logger.error("try lock error", e); 23: throw e; 24: } 25: } else { 26: // check lock owner is current user 27: // 校验锁定人是否是当前管理员 28: checkLock(namespace, namespaceLock, currentUser); 29: } 30: }NamespaceLock.dataChangeCreatedBy
不是当前管理员时,抛出 BadRequestException 异常,从而实现限制修改人。NamespaceLockService#tryLock(NamespaceLock)
方法,进行保存。NamespaceLockService#findLock(namespaceId)
方法,获得 NamespaceLock 对象。#tryLock(namespaceId, currentUser)
方法,尝试锁定。代码如下:
private void tryLock(long namespaceId, String user) { // 创建 NamespaceLock 对象 NamespaceLock lock = new NamespaceLock(); lock.setNamespaceId(namespaceId); lock.setDataChangeCreatedBy(user); // 管理员 lock.setDataChangeLastModifiedBy(user); // 管理员 // 保存 NamespaceLock 对象 namespaceLockService.tryLock(lock); }namespaceId
冲突,调用 NamespaceLockService#tryLock(NamespaceLock)
方法,获得最新的 NamespaceLock 对象。#checkLock(namespace, namespaceLock, currentUser)
方法,校验锁定人是否是当前管理员。代码如下:
private void checkLock(Namespace namespace, NamespaceLock namespaceLock, String currentUser) { // 当 NamespaceLock 不存在,抛出 ServiceException 异常 if (namespaceLock == null) { throw new ServiceException(String.format("Check lock for %s failed, please retry.", namespace.getNamespaceName())); } // 校验锁定人是否是当前管理员。若不是,抛出 BadRequestException 异常 String lockOwner = namespaceLock.getDataChangeCreatedBy(); if (!lockOwner.equals(currentUser)) { throw new BadRequestException("namespace:" + namespace.getNamespaceName() + " is modified by " + lockOwner); } }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
定义切面
@Aspect
@Component
public class NamespaceUnlockAspect {
private Gson gson = new Gson();
@Autowired
private NamespaceLockService namespaceLockService;
@Autowired
private NamespaceService namespaceService;
@Autowired
private ItemService itemService;
@Autowired
private ReleaseService releaseService;
@Autowired
private BizConfig bizConfig;
// create item
@After("@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, item, ..)")
public void requireLockAdvice(String appId, String clusterName, String namespaceName, ItemDTO item) {
// 尝试解锁
tryUnlock(namespaceService.findOne(appId, clusterName, namespaceName));
}
// update item
@After("@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, itemId, item, ..)")
public void requireLockAdvice(String appId, String clusterName, String namespaceName, long itemId, ItemDTO item) {
// 尝试解锁
tryUnlock(namespaceService.findOne(appId, clusterName, namespaceName));
}
// update by change set
@After("@annotation(PreAcquireNamespaceLock) && args(appId, clusterName, namespaceName, changeSet, ..)")
public void requireLockAdvice(String appId, String clusterName, String namespaceName, ItemChangeSets changeSet) {
// 尝试解锁
tryUnlock(namespaceService.findOne(appId, clusterName, namespaceName));
}
// delete item
@After("@annotation(PreAcquireNamespaceLock) && args(itemId, operator, ..)")
public void requireLockAdvice(long itemId, String operator) {
// 获得 Item 对象。若不存在,抛出 BadRequestException 异常
Item item = itemService.findOne(itemId);
if (item == null) {
throw new BadRequestException("item not exist.");
}
// 尝试解锁
tryUnlock(namespaceService.findOne(item.getNamespaceId()));
}
// ... 省略其他方法
}
@Aspect
注解,标记为表面类。@After
注解,标记切入执行方法后。#tryUnlock(...)
方法,尝试解锁。tryUnlock
private void tryUnlock(Namespace namespace) {
// 当关闭锁定 Namespace 开关时,直接返回
if (bizConfig.isNamespaceLockSwitchOff()) {
return;
}
// 若当前 Namespace 的配置恢复原有状态,释放锁,即删除 NamespaceLock
if (!isModified(namespace)) {
namespaceLockService.unlock(namespace.getId());
}
}
#isModified(Namespace)
方法,若当前 Namespace 的配置恢复原有状态。NamespaceLockService#unlock(namespaceId)
方法,释放锁,即删除 NamespaceLock 。isModified
1: boolean isModified(Namespace namespace) {
2: // 获得当前 Namespace 的最后有效的 Release 对象
3: Release release = releaseService.findLatestActiveRelease(namespace);
4: // 获得当前 Namespace 的 Item 集合
5: List<Item> items = itemService.findItemsWithoutOrdered(namespace.getId());
6:
7: // 如果无 Release 对象,判断是否有普通的 Item 配置项。若有,则代表修改过。
8: if (release == null) {
9: return hasNormalItems(items);
10: }
11:
12: // 获得 Release 的配置 Map
13: Map<String, String> releasedConfiguration = gson.fromJson(release.getConfigurations(), GsonType.CONFIG);
14: // 获得当前 Namespace 的配置 Map
15: Map<String, String> configurationFromItems = generateConfigurationFromItems(namespace, items);
16: // 对比两个 配置 Map ,判断是否相等。
17: MapDifference<String, String> difference = Maps.difference(releasedConfiguration, configurationFromItems);
18: return !difference.areEqual();
19: }
ReleaseService#findLatestActiveRelease(Namespace)
方法,获得当前 Namespace 的最后有效的 Release 对象。Release 的 configurations
字段,记录每次发布的完整配置 Map,代码如下:
// Release.java @Column(name = "Configurations", nullable = false) @Lob private String configurations;ItemService#findItemsWithoutOrdered(namespaceId)
方法,获得当前 Namespace 的 Item 集合。#hasNormalItems(List<Item>)
方法,判断是否有普通的 Item 配置项。若有,则代表修改过。代码如下:
private boolean hasNormalItems(List<Item> items) { for (Item item : items) { if (!StringUtils.isEmpty(item.getKey())) { // 非空串的 Key ,因为注释和空行的 Item 的 Key 为空串。 return true; } } return false; }#generateConfigurationFromItems(namespace, items)
方法,获得当前 Namespace 的配置 Map 。代码如下:
private Map<String, String> generateConfigurationFromItems(Namespace namespace, List<Item> namespaceItems) { Map<String, String> configurationFromItems = Maps.newHashMap(); // 获得父 Namespace 对象 Namespace parentNamespace = namespaceService.findParentNamespace(namespace); // 若无父 Namespace ,使用自己的配置 // parent namespace if (parentNamespace == null) { generateMapFromItems(namespaceItems, configurationFromItems); // 若有父 Namespace ,说明是灰度发布,合并父 Namespace 的配置 + 自己的配置项 } else { //child namespace Release parentRelease = releaseService.findLatestActiveRelease(parentNamespace); if (parentRelease != null) { configurationFromItems = gson.fromJson(parentRelease.getConfigurations(), GsonType.CONFIG); } generateMapFromItems(namespaceItems, configurationFromItems); } return configurationFromItems; } private Map<String, String> generateMapFromItems(List<Item> items, Map<String, String> configurationFromItems) { for (Item item : items) { String key = item.getKey(); // 跳过注释和空行的配置项 if (StringUtils.isBlank(key)) { continue; } configurationFromItems.put(key, item.getValue()); } return configurationFromItems; }发布配置时,调用 ReleaseService#publish(...)
方法时,在方法内部,会调用 #checkLock(Namespace namespace, boolean isEmergencyPublish, String operator)
方法,校验锁定人是否是当前管理员。代码如下:
1: private void checkLock(Namespace namespace, boolean isEmergencyPublish, String operator) {
2: if (!isEmergencyPublish) { // 非紧急发布
3: // 获得 NamespaceLock 对象
4: NamespaceLock lock = namespaceLockService.findLock(namespace.getId());
5: // 校验锁定人是否是当前管理员。若是,抛出 BadRequestException 异常
6: if (lock != null && lock.getDataChangeCreatedBy().equals(operator)) {
7: throw new BadRequestException("Config can not be published by yourself.");
8: }
9: }
10: }
"emergencyPublish.supported.envs"
配置开启对应的 Env 们。例如,emergencyPublish.supported.envs = dev
。NamespaceLock.dataChangeCreatedBy
是当前管理员时,抛出 BadRequestException 异常,从而实现限制修改人。发布配置时,调用 ReleaseService#createRelease(...)
方法时,在方法内部,会调用 NamespaceLockService#unlock(namespaceId)
方法,释放 NamespaceLock 。代码如下:
private Release createRelease(Namespace namespace, String name, String comment,
Map<String, String> configurations, String operator) {
// ... 省略无关代码
// 释放 NamespaceLock
namespaceLockService.unlock(namespace.getId());
// ... 省略无关代码
}
小文一篇,睡觉~~~