引言
在现代微服务架构中,配置管理是一个看似简单实则复杂的关键问题。随着服务数量的增加,传统的配置文件方式暴露出诸多问题:配置分散、修改困难、缺乏版本控制、无法实时生效等。本文将深入探讨如何从零开始构建一个高可用的微服务配置中心,分享我们在实际项目中积累的技术实践和经验。
为什么需要配置中心?
在微服务架构中,每个服务都需要配置信息,包括数据库连接、第三方API密钥、业务参数等。传统的做法是将这些配置写在每个服务的配置文件中,但这种方式存在明显缺陷:
- 配置分散:修改一个配置需要到多个服务中分别修改
- 环境差异:开发、测试、生产环境配置难以统一管理
- 重启成本:每次修改配置都需要重启服务
- 安全性:敏感信息(如密码)以明文形式存储在代码仓库中
配置中心通过集中化管理配置,提供统一的配置访问接口,解决了上述问题。
技术选型与架构设计
核心需求分析
在构建配置中心之前,我们需要明确核心需求:
- 高可用性:配置中心作为基础设施,必须保证高可用
- 实时性:配置变更能够快速推送到所有服务实例
- 版本管理:支持配置的历史版本回溯
- 权限控制:不同环境、不同服务的配置访问权限控制
- 多环境支持:支持开发、测试、生产等多环境
- 客户端兼容性:支持多种语言和框架
架构设计
我们采用分层架构设计:
┌─────────────────────────────────────────┐
│ 客户端SDK │
│ (Java/Go/Python/Node.js等) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 配置中心服务端 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ API层 │ │ 业务层 │ │ 数据层 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 存储层 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ MySQL │ │ Redis │ │ 文件 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────┘
核心实现
1. 数据模型设计
首先设计配置数据的核心模型:
-- 应用表
CREATE TABLE applications (
id INT PRIMARY KEY AUTO_INCREMENT,
app_id VARCHAR(64) NOT NULL UNIQUE COMMENT '应用唯一标识',
app_name VARCHAR(128) NOT NULL COMMENT '应用名称',
description TEXT COMMENT '应用描述',
owner VARCHAR(64) COMMENT '负责人',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_app_id (app_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 配置表
CREATE TABLE configurations (
id INT PRIMARY KEY AUTO_INCREMENT,
app_id VARCHAR(64) NOT NULL COMMENT '应用ID',
environment VARCHAR(32) NOT NULL COMMENT '环境: dev/test/prod',
namespace VARCHAR(64) NOT NULL COMMENT '命名空间',
config_key VARCHAR(256) NOT NULL COMMENT '配置键',
config_value LONGTEXT NOT NULL COMMENT '配置值',
config_type VARCHAR(32) DEFAULT 'text' COMMENT '配置类型: text/json/yaml/properties',
version INT DEFAULT 1 COMMENT '版本号',
description TEXT COMMENT '配置描述',
is_encrypted BOOLEAN DEFAULT FALSE COMMENT '是否加密',
created_by VARCHAR(64) COMMENT '创建人',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_app_env_ns_key (app_id, environment, namespace, config_key),
INDEX idx_app_env (app_id, environment)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 配置变更历史表
CREATE TABLE config_history (
id INT PRIMARY KEY AUTO_INCREMENT,
config_id INT NOT NULL COMMENT '配置ID',
old_value LONGTEXT COMMENT '旧值',
new_value LONGTEXT NOT NULL COMMENT '新值',
operation VARCHAR(32) COMMENT '操作类型: CREATE/UPDATE/DELETE',
operator VARCHAR(64) COMMENT '操作人',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_config_id (config_id),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 服务端实现
2.1 配置管理API
使用Spring Boot实现RESTful API:
@RestController
@RequestMapping("/api/v1/config")
@Slf4j
public class ConfigController {
@Autowired
private ConfigService configService;
/**
* 获取配置
*/
@GetMapping("/{appId}/{environment}/{namespace}")
public ResponseEntity<Map<String, Object>> getConfig(
@PathVariable String appId,
@PathVariable String environment,
@PathVariable String namespace,
@RequestParam(required = false) String version) {
try {
Map<String, Object> configs = configService.getConfigs(
appId, environment, namespace, version);
return ResponseEntity.ok(configs);
} catch (ConfigNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Collections.singletonMap("error", e.getMessage()));
}
}
/**
* 更新配置
*/
@PutMapping("/{appId}/{environment}/{namespace}/{key}")
public ResponseEntity<ApiResponse> updateConfig(
@PathVariable String appId,
@PathVariable String environment,
@PathVariable String namespace,
@PathVariable String key,
@RequestBody ConfigUpdateRequest request,
@RequestHeader("X-User-Id") String operator) {
try {
configService.updateConfig(
appId, environment, namespace, key,
request.getValue(), request.getDescription(),
operator);
return ResponseEntity.ok(ApiResponse.success("配置更新成功"));
} catch (Exception e) {
log.error("配置更新失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("配置更新失败: " + e.getMessage()));
}
}
/**
* 监听配置变更
*/
@GetMapping("/watch/{appId}/{environment}/{namespace}")
public SseEmitter watchConfig(
@PathVariable String appId,
@PathVariable String environment,
@PathVariable String namespace,
@RequestParam Long version) {
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L); // 30分钟超时
configService.addWatcher(appId, environment, namespace, version, emitter);
emitter.onCompletion(() ->
configService.removeWatcher(appId, environment, namespace, version, emitter));
emitter.onTimeout(() ->
configService.removeWatcher(appId, environment, namespace, version, emitter));
return emitter;
}
}
/**
* 配置服务实现
*/
@Service
@Slf4j
public class ConfigServiceImpl implements ConfigService {
@Autowired
private ConfigRepository configRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ConfigChangeNotifier configChangeNotifier;
private final Map<String, List<SseEmitter>> watchers = new ConcurrentHashMap<>();
@Override
public Map<String, Object> getConfigs(String appId, String environment,
String namespace, String version) {
// 1. 尝试从缓存获取
String cacheKey = buildCacheKey(appId, environment, namespace);
Map<String, Object> cachedConfigs = (Map<String, Object>)
redisTemplate.opsForValue().get(cacheKey);
if (cachedConfigs != null && !cachedConfigs.isEmpty()) {
log.debug("从缓存获取配置: {}", cacheKey);
return cachedConfigs;
}
// 2. 从数据库获取
List<Configuration> configs = configRepository.findByAppIdAndEnvironmentAndNamespace(
appId, environment, namespace);
if (configs.isEmpty()) {
throw new ConfigNotFoundException("配置不存在");
}
// 3. 构建配置映射
Map<String, Object> configMap = configs.stream()
.collect(Collectors.toMap(
Configuration::getConfigKey,
config -> parseConfigValue(config.getConfigValue(), config.getConfigType())
));
// 4. 写入缓存
redisTemplate.opsForValue