动态加载smart-reload
一、背景与问题
可能一看到这个名字会很困惑,不知道什么意思。但是接下来想象一个场景:
比如一个电商: 如果你把一些商品信息放到java进程里的缓存里,那么当你需要更新某两个商品信息的时候怎么办?
有人说为啥不使用redis缓存? 额...并不是所有缓存都有必要使用redis 有人说不使用缓存?额...那还不如不用程序算了 有人说 重启?额...做好没年终奖的准备吧
所以系统需要有这个东西,在不重启java程序的前提下,能执行一个预留的代码。这种场景叫做Reload
二、架构与思想
2.1、轮询与订阅
其实有很多种解决方案,最经典的应该是 轮询和订阅这两种。
轮询:每个一段时间重新查询数据库
订阅:系统订阅消息,然后使用第三者发送消息给系统,进行一些操作
2.2、选择轮询!
SmartAdmin中的reload选择轮询+监听者模式
策略。 原因:
- 轮询比订阅简单,订阅需要依赖其他第三方:比如redis订阅,kafka等MQ订阅,Zookeeper等
- 轮询可以专注于本应用,不需要任何第三方
2.3、如何轮询
启动线程去扫描某个表,表中存放着一些 reload项(reload item),但凡有reload项标识发生变化,就发送事件给那些监听reload项的java监听者。
2.4、设计实现
1)需要一个reload的数据,所以需要一个表定义reload项目,即表t_reload_item
CREATE TABLE `t_reload_item` (
`tag` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '项名称',
`args` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '参数 可选',
`identification` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '运行标识',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`tag`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='reload项目';
tag: reload项
args:listener执行之后的参数
identification:标识,identification与上次比较发生变化才进行reload
2)需要在代码中找到reload的地方,准备使用一个注解解决@SmartReload
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SmartReload {
String value(); //reload的 tag,对应 t_reload_item表中的 tag
}
三、具体使用
3.1、定义tag
先定义一个tag名称,比如system_config
,目的是为了 进行重新加载 config
的缓存。
第一步:在ReloadConst.java
类中 增加一个 常量,如下:
public class ReloadConst {
public static final String CONFIG_RELOAD = "system_config";
public static final String CACHE_SERVICE = "cache_service";
}
第二步:在表t_reload_item
,添加一条记录,其中:
`tag`为`system_config`,不区分大小写;
`args`为调用方法的传入参数
`identification` 为具体的标识,每当`identification`发生变化的时候,会进行reload操作
3.2、添加注解
在需要调用的方法中,添加注解@SmartReload
,且指定tag,如ConfigService
中: 每次reload 会重新加载 配置的缓存
@Slf4j
@Service
public class ConfigService {
//系统配置缓存
private final ConcurrentHashMap<String, ConfigEntity> configCache = new ConcurrentHashMap<>();
@Autowired
private ConfigDao configDao;
/**
* 此处为 reload方法,每当`identification`发生变化的时候,会执行此方法;
* 此处 param 为 数据库t_reload_item 表中的args字段
*/
@SmartReload(ReloadConst.CONFIG_RELOAD)
public void configReload(String param) {
this.loadConfigCache();
}
//初始化系统设置缓存
@PostConstruct
private void loadConfigCache() {
List<ConfigEntity> entityList = configDao.selectList(null);
entityList.forEach(entity -> this.configCache.put(entity.getConfigKey().toLowerCase(), entity));
}
}
3.3、进行reload
需要reload的时候,打开表t_reload_item
,找到记录 tag = system_config
;
修改:identification
,只要和上次的identification
不一样就可以;
修改:args
,即需要传入java方法的参数;
等待一段时间(sa-base
中配置文件配置reload.interval-seconds
参数,秒)后,会自动执行ConfigService.configReload(param)
方法;
四、实现原理
4.1、原理概述
就是有守护线程 去每隔一段时间扫码数据库t_reload_item
,如果发现 identification
和之前的不一样,就找到对应tag
的reload方法去执行。几个特殊点:
- 项目启动的时候,就去加载
t_reload_item
作为初始参照物 - 项目启动的时候扫描所有的
@SmartReload
注解,并找到tag -> reload方法
的对应关系 - 使用守护daemon线程去轮训
4.2、表结构
tag: reload项
args:执行之后的参数
identification:标识,identification与上次比较发生变化才进行reload
CREATE TABLE `t_reload_item` (
`tag` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '项名称',
`args` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '参数 可选',
`identification` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '运行标识',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`tag`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='reload项目';
4.3、守护线程
SmartReloadManager.java
中
public SmartReloadManager(AbstractSmartReloadCommand reloadCommand, int intervalSeconds) {
this.threadPoolExecutor = new ScheduledThreadPoolExecutor(THREAD_COUNT, r -> {
Thread t = new Thread(r, THREAD_NAME_PREFIX);
if (!t.isDaemon()) {
t.setDaemon(true);
}
return t;
});
this.threadPoolExecutor.scheduleWithFixedDelay(new SmartReloadRunnable(reloadCommand), 10, intervalSeconds, TimeUnit.SECONDS);
reloadCommand.setReloadManager(this);
}
4.4、查找reload方法
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(bean.getClass());
if (methods == null) {
return bean;
}
for (Method method : methods) {
SmartReload smartReload = method.getAnnotation(SmartReload.class);
if (smartReload == null) {
continue;
}
int paramCount = method.getParameterCount();
if (paramCount > 1) {
log.error("<<SmartReloadManager>> register tag reload : " + smartReload.value() + " , param count cannot greater than one !");
continue;
}
String reloadTag = smartReload.value();
this.register(reloadTag, new SmartReloadObject(bean, method));
}
return bean;
}
4.5、比较 identification
在reload线程中去比较 SmartReloadRunnable.java
:
private void doTask() {
List<SmartReloadItem> smartReloadItemList = this.abstractSmartReloadCommand.readReloadItem();
ConcurrentHashMap<String, String> tagIdentifierMap = this.abstractSmartReloadCommand.getTagIdentifierMap();
for (SmartReloadItem smartReloadItem : smartReloadItemList) {
String tag = smartReloadItem.getTag();
String tagIdentifier = smartReloadItem.getIdentification();
String preTagChangeIdentifier = tagIdentifierMap.get(tag);
// 数据不一致
if (preTagChangeIdentifier == null || !preTagChangeIdentifier.equals(tagIdentifier)) {
this.abstractSmartReloadCommand.putIdentifierMap(tag, tagIdentifier);
// 执行重新加载此项的动作
SmartReloadResult smartReloadResult = this.doReload(smartReloadItem);
this.abstractSmartReloadCommand.handleReloadResult(smartReloadResult);
}
}
}
执行reload方法,并传入参数
private SmartReloadResult doReload(SmartReloadItem smartReloadItem) {
SmartReloadResult result = new SmartReloadResult();
SmartReloadObject smartReloadObject = this.abstractSmartReloadCommand.reloadObject(smartReloadItem.getTag());
try {
if (smartReloadObject == null) {
result.setResult(false);
result.setException("不能从系统中找到对应的tag:" + smartReloadItem.getTag());
return result;
}
Method method = smartReloadObject.getMethod();
if (method == null) {
result.setResult(false);
result.setException("reload方法不存在");
return result;
}
result.setTag(smartReloadItem.getTag());
result.setArgs(smartReloadItem.getArgs());
result.setIdentification(smartReloadItem.getIdentification());
result.setResult(true);
int paramCount = method.getParameterCount();
if (paramCount > 1) {
result.setResult(false);
result.setException("reload方法" + method.getName() + "参数太多");
return result;
}
if (paramCount == 0) {
method.invoke(smartReloadObject.getReloadObject());
} else {
method.invoke(smartReloadObject.getReloadObject(), smartReloadItem.getArgs());
}
} catch (Throwable throwable) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
result.setResult(false);
result.setException(throwable.toString());
}
return result;
}
联系我们
- 教育领域(高职院校数字化、就业创业大数据平台、继续教育平台;在线教育系统、视频直播、题库等,包含:医学、应急管理、成考、专升本等)
- 供应链领域(网络货运平台、大宗贸易进销存ERP、物流管理TMS、B2B电商、仓储WMS、AI提效等)
- 中医领域(诊所数字化管理、互联网医院、AI辅助诊疗、中医适宜技术、在线云问诊、空中药房等)
- AI+软件领域(软件定制外包、开源技术、数据大屏、国产化改造、技术升级换代、人员外包、技术顾问、技术培训等)
加微信: 卓大 拉你入群,一起学习 | 公众号 :六边形工程师 分享:赚钱、代码、生活 | 请 “1024创新实验室” 烩面里加肉 咖啡配胡辣汤,提神又饱腹 | 抖音 : 六边形工程师 直播:赚钱、代码、中医 |