feat: 实现配置删除通知机制(deletedKeys)

- 服务端 fetchChangedConfigs 接口新增 deletedKeys 字段,返回已删除配置的 key 列表
- Go/Node.js/Python/Spring Boot 客户端支持解析 deletedKeys 并删除本地配置
- Spring Boot 客户端新增 removeConfigs 方法,同步更新 Spring Environment
This commit is contained in:
2026-06-17 21:07:51 +08:00
parent 82523b9236
commit 639475b730
6 changed files with 104 additions and 0 deletions
+12
View File
@@ -223,6 +223,18 @@ func (c *Client) fetchChanged() error {
delete(wrapper.Data, "latest.fetch.timestamp") delete(wrapper.Data, "latest.fetch.timestamp")
} }
// 处理删除的配置
if deletedKeysStr, ok := wrapper.Data["deletedKeys"]; ok && deletedKeysStr != "" {
deletedKeys := strings.Split(deletedKeysStr, ",")
c.mu.Lock()
for _, key := range deletedKeys {
delete(c.configs, key)
}
c.mu.Unlock()
log.Printf("[ConfigClient] %d configs deleted", len(deletedKeys))
delete(wrapper.Data, "deletedKeys")
}
if len(wrapper.Data) > 0 { if len(wrapper.Data) > 0 {
changed := make(map[string]string) changed := make(map[string]string)
for k, v := range wrapper.Data { for k, v := range wrapper.Data {
+11
View File
@@ -127,6 +127,17 @@ export class ConfigClient {
} }
delete changedData['latest.fetch.timestamp']; delete changedData['latest.fetch.timestamp'];
// 处理删除的配置
const deletedKeysStr = changedData['deletedKeys'];
if (deletedKeysStr) {
const deletedKeys = deletedKeysStr.split(',');
for (const key of deletedKeys) {
delete this.configs[key];
}
console.info('[ConfigClient]', deletedKeys.length, 'configs deleted');
delete changedData['deletedKeys'];
}
const changed = {}; const changed = {};
for (const [k, v] of Object.entries(changedData)) { for (const [k, v] of Object.entries(changedData)) {
changed[k] = v; changed[k] = v;
@@ -103,6 +103,15 @@ class ConfigClient:
if ts: if ts:
self._last_time = int(ts) self._last_time = int(ts)
# 处理删除的配置
deleted_keys_str = changed_data.pop("deletedKeys", None)
if deleted_keys_str:
deleted_keys = deleted_keys_str.split(",")
with self._lock:
for key in deleted_keys:
self._configs.pop(key, None)
logger.info("[ConfigClient] %d configs deleted", len(deleted_keys))
if changed_data: if changed_data:
changed = {} changed = {}
for k, v in changed_data.items(): for k, v in changed_data.items():
@@ -415,6 +415,42 @@ public class ConfigcenterContext {
} }
} }
public synchronized void removeConfigs(List<String> keys) {
for (String key : keys) {
localProps.remove(key);
logger.info("Config [{}] removed", key);
}
// Update Spring Environment property source
if (applicationContext != null) {
StandardEnvironment environment = (StandardEnvironment) applicationContext.getEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
synchronized (propertySources) {
if (propertySources.contains(MANAGER_PROPERTY_SOURCE)) {
Properties properties = new Properties();
Object source = propertySources.get(MANAGER_PROPERTY_SOURCE);
if (source instanceof MapPropertySource) {
Map<String, Object> map = ((MapPropertySource) source).getSource();
for (Entry<String, Object> entry : map.entrySet()) {
if (!keys.contains(entry.getKey())) {
properties.put(entry.getKey(), entry.getValue());
}
}
} else if (source instanceof PropertiesPropertySource) {
Map<String, Object> src = ((PropertiesPropertySource) source).getSource();
for (Entry<String, Object> entry : src.entrySet()) {
if (!keys.contains(entry.getKey())) {
properties.put(entry.getKey(), entry.getValue().toString());
}
}
}
propertySources.replace(MANAGER_PROPERTY_SOURCE, new PropertiesPropertySource(MANAGER_PROPERTY_SOURCE, properties));
}
}
}
// Refresh @Value and @ConfigurationProperties fields
refreshDynamicConfigFields();
}
private void refreshDynamicConfigFields() { private void refreshDynamicConfigFields() {
if (applicationContext != null) { if (applicationContext != null) {
try { try {
@@ -63,6 +63,15 @@ public class InternalConfigChangeListener {
if (data.containsKey(LATEST_FETCH_TIMESTAMP)) { if (data.containsKey(LATEST_FETCH_TIMESTAMP)) {
params.put("lastTime", Long.parseLong(data.remove(LATEST_FETCH_TIMESTAMP).toString())); params.put("lastTime", Long.parseLong(data.remove(LATEST_FETCH_TIMESTAMP).toString()));
} }
// 处理删除的配置
if (data.containsKey("deletedKeys")) {
String deletedKeysStr = (String) data.remove("deletedKeys");
if (deletedKeysStr != null && !deletedKeysStr.isEmpty()) {
String[] deletedKeys = deletedKeysStr.split(",");
context.removeConfigs(java.util.Arrays.asList(deletedKeys));
logger.info("[ConfigClient] {} configs deleted", deletedKeys.length);
}
}
if (!data.isEmpty()) context.updateConfig(data); if (!data.isEmpty()) context.updateConfig(data);
} }
} }
+27
View File
@@ -9,6 +9,7 @@ import (
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -172,6 +173,32 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
result := make(map[string]string) result := make(map[string]string)
var configIds []int var configIds []int
// 查询已删除的配置(enabled=0 且 updated_at > lastTime),返回 deletedKeys
var deletedKeys []string
if !req.IgnoreGlobal && app.GroupId != nil {
var deletedGlobalConfigs []model.AppConfig
dao.Engine.Where("env = ? AND version = ? AND enabled = 0 AND group_id = ? AND (app_id is null OR app_id = 0) AND updated_at > ?",
req.Env, version, *app.GroupId, lastUpdateTime).Find(&deletedGlobalConfigs)
for _, cfg := range deletedGlobalConfigs {
kvMap := h.parseService.ParseConfigToKVMap(&cfg)
for k := range kvMap {
deletedKeys = append(deletedKeys, k)
}
}
}
var deletedAppConfigs []model.AppConfig
dao.Engine.Where("env = ? AND version = ? AND enabled = 0 AND app_id = ? AND updated_at > ?",
req.Env, version, app.Id, lastUpdateTime).Find(&deletedAppConfigs)
for _, cfg := range deletedAppConfigs {
kvMap := h.parseService.ParseConfigToKVMap(&cfg)
for k := range kvMap {
deletedKeys = append(deletedKeys, k)
}
}
if len(deletedKeys) > 0 {
result["deletedKeys"] = strings.Join(deletedKeys, ",")
}
// load global changed configs // load global changed configs
if !req.IgnoreGlobal && app.GroupId != nil { if !req.IgnoreGlobal && app.GroupId != nil {
var globalConfigs []model.AppConfig var globalConfigs []model.AppConfig