前言
分布式配置中心还未普及的时候,通常我们对于项目的配置会放在配置文件中,例如 SpringBoot
项目放在 application.yml
中,以往的 Spring
项目,有些配置可能会存放在一个 properties
文件中。
对于 application.yml
的配置似乎不能动态化,于是后面出现了一些分布式配置中心,例如 nacos
等。我们需要在 application.yml
中配置命名空间、文件扩展名等。然后会把项目配置都放在分布式配置中心的文件中。
然而有一些业务场景,项目的配置并不适合放在分布式配置中心,但是又需要动态配置的功能。
不适合分布式配置中心的场景
随着公司规模的扩大,组织架构划分明确,分布式配置中心由专门的架构组负责管理,普通业务开发者无权限直接访问,仅相关业务部门的负责人具备查看和修改配置文件的权限。这样的话每次业务开发者都需要让负责人去配置中心添加和修改配置,比较麻烦。
而且,通常情况下,分布式配置中心存储的配置内容多为全局性、公共性的配置项,例如 ES、MySQL、MQ、Redis、线程池
等基础组件的配置信息。因为很多配置它并不适合放在配置中心
需要版本、开关控制的配置
如实验性功能开关、A/B 测试配置等。这类配置,尤其是开关和业务紧密相连,不适合放在远程分布式配置中心
配置值是大 JSON
有些配置的参数值会以一个大 JSON
的形式存在,若将其直接放入分布式配置中心,则需要额外维护一个独立的 JSON
配置文件,增加了管理和使用的复杂性。
用于用户交互的配置
部分业务配置涉及对客话术或展示内容,需要向产品或运营人员提供便捷的配置页面。如果让他们直接操作分布式配置中心,不仅不符合其使用习惯,还可能带来不必要的风险和操作复杂性。因此,针对这类需求,需要一种更友好、更贴合业务场景的配置管理方式。
高频动态更新的配置
需要频繁更新的配置,如实时促销活动规则、动态定价策略等。分布式配置中心的设计更适合低频更新的配置,频繁更新可能导致性能问题,活动期间客户端可能需要频繁拉取配置,增加网络开销和延迟。
项目参数配置介绍
针对分布式配置中心不适合的场景,我们需要一个功能,存储针对于业务应用的业务配置参数,这个功能很简单,只需要存储 key-value
键值对即可。让每一个微服务都有自己的专属项目参数配置。示例页面如下
配置列表页面
添加配置
更新配置
看到这里我们就自然而然的想到了实现方式非常简单。在数据库建个表存储,然后查询用 Redis
来做个缓存即可。但是我们仔细思考,似乎使用本地缓存就可以,没有必要使用 Redis
。毕竟多引入一个技术组件就多一点复杂性,本地缓存我们可以考虑使用 caffeine
。
我们日常开发中也应该尽可能多的让自己的代码逻辑处于可配置化,这样当产品有一些相关的需求改动,可以让我们尽可能少的去生产发版
例如延迟消息发送的时间、授信失败 N 天后不允许再次授信等等
caffeine 常用 API 介绍
引入 Maven 依赖
xml
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.2.0</version>
</dependency>
如果是 SpringBoot
项目,可以不标版本号,父依赖有引入
基本用法
Caffeine
的核心类是 Cache
和 LoadingCache
,分别用于手动加载和自动加载缓存。
创建缓存
java
// 创建一个简单的缓存
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100) // 最大缓存条目数
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
.build();
添加和获取缓存
java
// 添加缓存
cache.put("key1", "value1");
// 获取缓存
String value = cache.getIfPresent("key1");
System.out.println(value); // 输出: value1
使用 get
方法(自动加载)
java
// 如果缓存不存在,则通过提供的函数加载
String value = cache.get("key2", k -> "defaultValue");
System.out.println(value); // 输出: defaultValue
LoadingCache
java
LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> loadFromDatabase(key)); // 自动加载函数
// 模拟从数据库加载
private static String loadFromDatabase(String key) {
//...从数据库加载
return "value";
}
缓存淘汰策略
是不是想到了 Redis
的八股文面试中淘汰策略?同为缓存,都是大同小异的
基于容量淘汰
java
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100) // 最大缓存条目数
.build();
基于时间淘汰
java
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
.expireAfterAccess(5, TimeUnit.MINUTES) // 访问后5分钟过期
.build();
基于引用淘汰
这个没用过,有点高端的感觉
java
Cache<String, String> cache = Caffeine.newBuilder()
.weakKeys() // 弱引用键
.weakValues() // 弱引用值
.softValues() // 软引用值
.build();
异步缓存
Caffeine
支持异步加载缓存。
创建异步缓存
java
AsyncCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(100)
.buildAsync();
// 异步获取缓存
CompletableFuture<String> future = asyncCache.get("key4", k -> "asyncValue");
future.thenAccept(value -> System.out.println(value)); // 输出: asyncValue
异步加载缓存
java
AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder()
.maximumSize(100)
.buildAsync(key -> loadFromDatabase(key));
// 异步获取缓存
CompletableFuture<String> future = asyncLoadingCache.get("key5");
future.thenAccept(value -> System.out.println(value)); // 输出: value_for_key5
缓存统计
Caffeine
还提供了缓存命中率、加载次数等统计信息。
启用统计
java
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.recordStats() // 启用统计
.build();
获取统计信息
java
com.github.benmanes.caffeine.cache.stats.CacheStats stats = cache.stats();
System.out.println("命中率: " + stats.hitRate());
System.out.println("加载次数: " + stats.loadCount());
System.out.println("命中次数: " + stats.hitCount());
System.out.println("未命中次数: " + stats.missCount());
监听器
Caffeine
支持缓存事件的监听。
添加监听器
java
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.removalListener((key, value, cause) ->
System.out.println("Key " + key + " was removed, cause: " + cause))
.build();
其他功能
- 手动移除缓存
java
cache.invalidate("key1"); // 失效单个键
cache.invalidateAll(); // 失效所有键
- 批量操作
java
Map<String, String> data = Map.of("key1", "value1", "key2", "value2");
cache.putAll(data);
- 缓存清理
java
cache.cleanUp(); // 手动触发缓存清理
项目参数配置的实现
流程图
建表语句
sql
CREATE TABLE `project_config` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`system_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '微服务名',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '参数名(system_id + name 要唯一)',
`value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '参数值',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '配置描述',
`need_check` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否需要审核',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标志',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE COMMENT '参数名索引'
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
创建缓存
java
// 创建一个缓存实例
public static final Cache<String, String> PROJECT_CONFIG_CACHE = Caffeine.newBuilder()
.expireAfterWrite(600, TimeUnit.MINUTES) // 写入后 600 分钟过期
.maximumSize(100000) // 最大缓存项数 10 w
.build();
获取配置
java
/**
* 获取项目参数配置
* */
public String getValue(String systemId, String name, String defaultValue) {
return PROJECT_CONFIG_CACHE.get(systemId + SEPARATOR + name, k -> projectConfigService.getValue(systemId, name, defaultValue));
}
缓存不命中就查询数据库
java
@Transactional(rollbackFor = Exception.class)
public String getValue(String systemId, String name, String defaultValue) {
log.info("ProjectConfigService#getValue systemId-{},name-{}", systemId, name);
//排他锁
ProjectConfig projectConfig = projectConfigMapper.selectOne(Wrappers.<ProjectConfig>lambdaQuery().eq(ProjectConfig::getSystemId, systemId).eq(ProjectConfig::getName, name).last(" for update"));
if (projectConfig != null) {
return projectConfig.getValue();
}
projectConfig = new ProjectConfig();
projectConfig.setSystemId(systemId);
projectConfig.setName(name);
projectConfig.setValue(defaultValue);
projectConfig.setCreateTime(LocalDateTime.now());
projectConfig.setUpdateTime(LocalDateTime.now());
projectConfig.setNeedCheck(0);
projectConfigMapper.insert(projectConfig);
return projectConfig.getValue();
}
获取集合类型配置,有时业务配置是一个 List
java
/**
* 获取集合配置
* */
public <T> List<T> getList(String systemId, String name, List<T> defaultValue) {
String value = getValue(systemId, name, JSON.toJSONString(defaultValue));
return JSON.parseObject(value, new TypeReference<>() {
});
}
获取 Map
类型配置,有时业务配置是一个 Map
java
/**
* 获取 Map 配置
* */
public <K,V> Map<K, V> getMap(String systemId, String name, Map<K, V> defaultValue) {
String value = getValue(systemId, name, JSON.toJSONString(defaultValue));
return JSON.parseObject(value, new TypeReference<>() {
});
}
获取开关类型配置,有时我们需要根据开关来走不同的业务逻辑
java
/**
* 获取开关配置
* */
public Boolean getSwitch(String systemId,String name,Boolean defaultValue) {
String value = getValue(systemId, name, String.valueOf(defaultValue));
return "1".equals(value) || "true".equals(value);
}
源码分享
源码已分享到 github project-param-config
结语
本篇文章的内容很简单,主要是针对一些和业务强耦合的配置,不适合放在分布式配置中心的场景处理,从而使用本地项目参数配置。为了防止过度访问数据库,在数据库前面再加一层缓存处理。