1024创新实验室-公告

助力抖音1000个粉丝,开播写代码🎉🎉

打开【抖音APP】-点击【左上角侧边栏】-【点击扫一扫】-【进行关注】🎉🎉

和1024创新实验室一起,热爱代码,热爱生活,永远年轻,永远前行🎉🎉


Skip to content

动态加载smart-reload

一、背景与问题

可能一看到这个名字会很困惑,不知道什么意思。但是接下来想象一个场景:

比如一个电商: 如果你把一些商品信息放到java进程里的缓存里,那么当你需要更新某两个商品信息的时候怎么办?

有人说为啥不使用redis缓存? 额...并不是所有缓存都有必要使用redis 有人说不使用缓存?额...那还不如不用程序算了 有人说 重启?额...做好没年终奖的准备吧

所以系统需要有这个东西,在不重启java程序的前提下,能执行一个预留的代码。这种场景叫做Reload

二、架构与思想

2.1、轮询与订阅

其实有很多种解决方案,最经典的应该是 轮询和订阅这两种。

java
轮询:每个一段时间重新查询数据库
订阅:系统订阅消息,然后使用第三者发送消息给系统,进行一些操作

2.2、选择轮询!

SmartAdmin中的reload选择轮询+监听者模式策略。 原因:

  • 轮询比订阅简单,订阅需要依赖其他第三方:比如redis订阅,kafka等MQ订阅,Zookeeper等
  • 轮询可以专注于本应用,不需要任何第三方

2.3、如何轮询

启动线程去扫描某个表,表中存放着一些 reload项(reload item),但凡有reload项标识发生变化,就发送事件给那些监听reload项的java监听者。

2.4、设计实现

1)需要一个reload的数据,所以需要一个表定义reload项目,即表t_reload_item

sql
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

java
@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类中 增加一个 常量,如下:

java
public class ReloadConst {
    public static final String CONFIG_RELOAD = "system_config";
    public static final String CACHE_SERVICE = "cache_service";
}

第二步:在表t_reload_item,添加一条记录,其中:

java
`tag`为`system_config`,不区分大小写;
`args`为调用方法的传入参数
`identification` 为具体的标识,每当`identification`发生变化的时候,会进行reload操作

3.2、添加注解

在需要调用的方法中,添加注解@SmartReload,且指定tag,如ConfigService中: 每次reload 会重新加载 配置的缓存

java
@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、表结构

sql
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

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方法

java
    @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:

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方法,并传入参数

java
 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;
    }

联系我们

1024创新实验室-主任:卓大,混迹于各个技术圈,研究过计算机,熟悉点 java,略懂点前端。
1024创新实验室 致力于成为中原领先、国内一流的技术团队, 以AI+数字化为驱动,用技术为产业互联网提供无限可能, 业务如下:
  • 教育领域(高职院校数字化、就业创业大数据平台、继续教育平台;在线教育系统、视频直播、题库等,包含:医学、应急管理、成考、专升本等)
  • 供应链领域(网络货运平台、大宗贸易进销存ERP、物流管理TMS、B2B电商、仓储WMS、AI提效等)
  • 中医领域(诊所数字化管理、互联网医院、AI辅助诊疗、中医适宜技术、在线云问诊、空中药房等)
  • AI+软件领域(软件定制外包、开源技术、数据大屏、国产化改造、技术升级换代、人员外包、技术顾问、技术培训等)
加微信: 卓大
拉你入群,一起学习
公众号 :六边形工程师
分享:赚钱、代码、生活
请 “1024创新实验室”
烩面里加肉
咖啡配胡辣汤,提神又饱腹
抖音 : 六边形工程师
直播:赚钱、代码、中医