apollo--NamespaceLock

可通过设置 ConfigDB 的 ServerConfig 的 "namespace.lock.switch""true" 开启。效果如下:

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

也就是说,开启后,一次配置修改并发布,需要两个人默认"false" ,即关闭。

NamespaceLock

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@Table(name = "NamespaceLock")
@Where(clause = "isDeleted = 0")
public class NamespaceLock extends BaseEntity {

/**
* Namespace 编号 {@link Namespace}
*
* 唯一索引
*/
@Column(name = "NamespaceId")
private long namespaceId;

}
  • 写操作 Item 时,创建 Namespace 对应的 NamespaceLock 记录到 ConfigDB 数据库中,从而记录配置修改
  • namespaceId字段,Namespace 编号,指向对应的 Namespace 。
    • 该字段上有唯一索引。通过该锁定,保证并发写操作时,同一个 Namespace 有且仅有创建一条 NamespaceLock 记录。

加锁与解锁的过程

image-20200911171424449

"

限制修改人

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

1. @PreAcquireNamespaceLock

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

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

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

image-20200831102043253

"

image-20200831102210434

"

2. NamespaceAcquireLockAspect

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

1. 定义切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@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(...) 方法,尝试锁定。

2. acquireLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
void acquireLock(String appId, String clusterName, String namespaceName, String currentUser) {
// 当关闭锁定 Namespace 开关时,直接返回;
// 配置:namespace.lock.switch,默认false
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);
}

private void acquireLock(Namespace namespace, String currentUser) {
// 当 Namespace 为空时,抛出 BadRequestException 异常
if (namespace == null) {
throw new BadRequestException("namespace not exist.");
}
long namespaceId = namespace.getId();
// 获得 NamespaceLock 对象
NamespaceLock namespaceLock = namespaceLockService.findLock(namespaceId);
// 当 NamespaceLock 不存在时,尝试锁定
if (namespaceLock == null) {
try {
// 锁定
tryLock(namespaceId, currentUser);
// lock success
} catch (DataIntegrityViolationException e) {
// 锁定失败,获得 NamespaceLock 对象
// lock fail
namespaceLock = namespaceLockService.findLock(namespaceId);
// 校验锁定人是否是当前操作人
checkLock(namespace, namespaceLock, currentUser);
} catch (Exception e) {
logger.error("try lock error", e);
throw e;
}
} else {
// check lock owner is current user
// 校验锁定人是否是当前操作人
checkLock(namespace, namespaceLock, 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);
}

private void checkLock(Namespace namespace, NamespaceLock namespaceLock, String currentUser) {
...
// 校验锁定人是否是当前操作人。若不是,抛出 BadRequestException 异常
String lockOwner = namespaceLock.getDataChangeCreatedBy();
if (!lockOwner.equals(currentUser)) {
throw new BadRequestException("namespace:" + namespace.getNamespaceName() + " is modified by " + lockOwner);
}
}
  • 发生 DataIntegrityViolationException 异常,说明保存 NamespaceLock 对象失败,由于唯一索引 namespaceId 冲突,调用 NamespaceLockService#findLock(NamespaceLock) 方法,获得最新的 NamespaceLock 对象。
  • 调用 #checkLock(namespace, namespaceLock, currentUser) 方法,校验锁定人是否是当前管理员。
  • NamespaceLock.dataChangeCreatedBy 不是当前管理员时,抛出 BadRequestException 异常,从而实现限制修改人

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. 定义切面

1
2
3
4
5
6
7
8
9
10
11
@Aspect
@Component
public class NamespaceUnlockAspect {
// 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));
}
...
}
  • @Aspect 注解,标记为表面类。
  • @After 注解,标记切入执行方法
  • 调用 #tryUnlock(...) 方法,尝试解锁。

2. tryUnlock

1
2
3
4
5
6
7
8
9
10
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 。

限制发布人

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

1
2
3
4
5
6
7
8
9
10
 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: }
  • 第 2 行:非紧急发布,可通过设置 PortalDB 的 ServerConfig 的"emergencyPublish.supported.envs" 配置开启对应的 Env 们。例如,emergencyPublish.supported.envs = dev
  • 第 6 至 8 行:当 NamespaceLock.dataChangeCreatedBy 当前管理员时,抛出 BadRequestException 异常,从而实现限制修改人

解锁

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

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

参考