文章目录
- 引言
- 案例
- A项目
- B项目
- 分析
- 我的实现
- 总结
引言
缓存,这是一个经久不衰的话题,它通过“空间换时间”的战术不仅能够极大提升处理查询性能还能很好的保护底层资源。最近针对系统数据缓存的优化后,由于这是一个通用的场景并且有了一点心得因此在这里分享出来。
案例
无论是传统Web后端应用还是大数据平台服务应用,本质上都是一个Java进程并且应用相关数据一般都是存在Mysql。从抽象的角度来看,所有请求基本都要经过以下几个流程:参数校验、鉴权、请求处理、请求响应。这几个流程特别是前几个往往需要依赖存在MySQL中的系统数据,例如判断请求中携带的的服务ID在系统中是否有对应的服务、获取服务对应的认证信息进行身份校验、请求处理时常常依赖的一些数据如某个变动非常低频的小表或者一些配置在MySQL中的系统配置等,此时如果是你,会怎么去设计的这个缓存?我先举两个负责过的项目之前是如何设计的
A项目
服务启动的时候读取MySQL中的系统数据并以HashMap的格式存在Java进程中,不会自动更新,在有配置变动时循环调用所有服务器的ip接口触发重新加载数据。具体流程如下
这种设计的优点如下
- 实现简单,不依赖外部组件
- 有数据变更立马更新所有缓存数据,基本不会在缓存中读到旧数据的逻辑
以上是这种设计的优点,可能也是设计者这么设计的原因。但是随着维护会出现以下问题
- 所有服务器的ip是配置在apollo中的,因此每次扩容机器时都要将新的ip增加到apollo中,否则可能会出现缓存不更新现象
- 在服务从ECS迁移到K8s后,服务的ip是动态分配的并且每次重启后都不一样,因此当有配置变动时只能手动重启服务或者执行curl调用接口来触发各个节点进行配置更新
B项目
服务启动的时候读取MySQL中的系统数据并以HashMap的格式存在Java进程中,并记录当前更新时间,在查询缓存时会判断此时距离上次更新时间是否已经超过30秒(硬编码),如果超过则重新触发查询MySQL更新缓存。具体流程如下
这种设计的优点如下
- 服务之间无需通信,相比A服务来说是会自己主动更新
除了上面这一个优点貌似旧没有了,值得吐槽的点很多,大致列举一下它实现的问题
- 针对每一个需要更新的缓存都新建一个自己独立的类来实现CommandLineRunner接口
- 硬编码30s过期的逻辑以及查询时再去做更新动作(类似第一次惩罚)
- 更新缓存时的逻辑是,先全量查询Mysql对应的表,然后循环对比缓存中的数据,有新增的就追加到缓存,再去跟缓存比较是否有删除的,有的话再去缓存中删除
分析
这两个服务在更新系统数据缓存上都存在不少问题,现在再回过头来思考🤔下,我们理想的系统数据缓存的实现应该是什么样子的?我大概整理了一下大致如下
- 服务启动时要全量读取对应的数据以Map格式存在Java进程的内存中
- 服务应该是定时自行去Mysql同步最新的数据,并且定时周期是允许可配置的
- 一些重要配置变更时触发各个服务立马去Mysql同步数据(可选,需要参考具体业务场景)
基于这个场景我对比了下当前比较热门的缓存工具Guava和Caffeine,最终发现它们都不适合咱们的这个场景。它们的设计更偏向于解决缓存热点数据的问题,简单来说就是咱们的场景是Java内存有10G,而存在Mysql中的数据有100M,我们需要将这100M的数据存在Java内存中进行请求加速,需要解决的是如何全量加载到内存的问题。而Guava和Caffeine的设计要解决的问题时,如何更有效的合理的利用内存,例如Java内存只有10G,而存在MySQL的数据有100G,因此需要将这100G中的热点数据存在Java内存中来提升性能和降低高频访问Mysql,它具体要考虑的问题大致有以下几点
- 通过LRU算法或者变种来保留热点数据
- 通过大小限制以及时间限制来剔除掉缓存数据从而保证Java内存不会被撑爆
- 在有新数据写入或者数据更新时,同步更新缓存中的数据
…
通过这些你会发现其实这个场景真不适合用Guava和Caffeine,这个过程中也翻阅了一些大佬的实现,但是发现都有点跑偏了,有种为了用Caffeine而强行用的味道。例如 https://www.vincentli.top/2020/09/01/jvm-local-cache-case-caffeine/,这里本质上还是跟B项目的实现一样,只不过用Caffeine替换了Java的HashMap罢了。在这里补充说下,Caffeine是非常优秀的缓存工具,现在很多优秀的开源组件例如 Pulsar 中也会用它来加速查询,但盲目的使用是不可取的,并且这肯定也不是Caffeine设计者想看到的。
我的实现
上面说了这么多,我分享下自己的实现供大家参考以及批判。在这里通过一个定时线程池实现即可,在启动时触发一次并在后续周期性的刷新配置即可,核心代码也就两三行。先看下流程
代码实现如下
public class CacheManager implements CommandLineRunner {
private static final Long DEFAULT_INTERVAL = 36000L;
private final ScheduledExecutorService apiSystemConfigExecutorService = Executors.newSingleThreadScheduledExecutor();
private static Map<String, String> apiSysConfig = new HashMap<>();
@Override
public void run(String... args) throws Exception {
apiSystemConfigExecutorService.scheduleAtFixedRate(this::loadApiSysConfigDataFromDB,
0, Optional.ofNullable(PropertyConfigurer.getLong("SYSTEM_CONFIG_UPDATE_PERIOD")).orElse(DEFAULT_INTERVAL), TimeUnit.SECONDS);
}
public void loadApiSysConfigDataFromDB(){
ApisDao apisDao = SpringContextHolder.getBean(ApisDao.class);
List<ApiSysConfigEntity> sysConfigEntityList = apisDao.selectSystemConfig();
if (null == sysConfigEntityList || sysConfigEntityList.size() == 0){
return;
}
logger.info("load ApiSysConfigEntity from api_sys_config in mysql ,the size of api in cache is " + sysConfigEntityList.size());
apiSysConfig = sysConfigEntityList.stream()
.collect(Collectors.toMap(ApiSysConfigEntity::getName, ApiSysConfigEntity::getValue));
}
}
总结
在工作中只要我们观察和思考,就会发现其实是存在不少值得完善的地方,此时应该考虑对它们进行完善,否则如果长期维护一个“丑陋”的系统,你的思维、以及审美也会随之跟着降低,以至于久而久之就觉得这种设计也挺好的,甚至后续再有类似的场景时你还是会选择这种设计。工作的本质也是一场修行,在对系统进行改进完善的过程也是自我完善的过程,简称“借物得道”,同时如果读者针对这个场景有更合适的设计也欢迎在下方一起讨论。