动态加载smart-reload

9/7/2021

# 一、背景与问题

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

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

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

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

# 二、架构与思想

# 2.1、轮询与订阅

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

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

# 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
1
2
3
4
5
6
7
8
9
10
11
12

# 2)需要在代码中找到reload的地方,准备使用一个注解解决@SmartReload

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SmartReload {
    String value(); //reload的 tag,对应 t_reload_item表中的 tag
}
1
2
3
4
5

# 三、具体使用

# 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";
}
1
2
3
4

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

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

# 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));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 3.3、进行reload

需要reload的时候,打开表t_reload_item,找到记录 tag = system_config;
修改:identification,只要和上次的identification不一样就可以;
修改:args,即需要传入java方法的参数;
等待一段时间(sa-common中配置文件配置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项目';
1
2
3
4
5
6
7
8
9
10
11
12

# 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);
    }
1
2
3
4
5
6
7
8
9
10
11

# 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;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 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);
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

执行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;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 联系我们

1024创新实验室-主任:卓大 (opens new window),混迹于各个技术圈,研究过计算机,熟悉点 java,略懂点前端。
1024创新实验室(河南·洛阳) (opens new window) 致力于成为中原领先、国内一流的技术团队,以技术创新为驱动,合作各类项目。

加 主任 “卓大” 微信
拉你入群,一起学习
关注 “小镇程序员”
分享代码与生活、技术与赚钱
请 “1024创新实验室” 喝咖啡
支持我们的开源与分享

告白气球 (钢琴版)
JESSE T