一般而言,首先能想到后台缓存有以下几种方案:
使用guava等第三方工具类提供的缓存能力
自己基于集合类实现
内存缓存配合本地文件系统实现
使用Redis缓存中间件
使用本地内存实现缓存都优点是缓存数据更靠近用户端,以空间换时间. 但是由于数据是分散存储的,如果数据有变更则必须同时更新所有应用实例的缓存数据,否则会出现数据不一致的情况。
而使用缓存中间件可以利用Nosql数据库进行集中式管理缓存数据,一般数据变更后删除缓存,下次查询数据再更新进缓存. 优点是引进中间件提供通用缓存功能,各应用无需自己实现. 缺点需要维护额外的中间件,如果中间件是多应用共用,一个应用缓存使用不当会影响到其他应用.当然我们也可以采取一些措施来减少这种影响. 另外一个缺点就是如果有比较多的大Key再会影响Redis的缓存性能。
基于MySQL实现的缓存方案
为什么要这么做?
我们实际后台中经常会出现比较大的数据集,比如XXX排行榜,XXX结构体之类的.这些数据的特点是不经常更新,数据比较大.缓存Key数量也就百数量级以内了.
针对这种场景,我们一般不太想使用Redis等缓存中间件来增加系统复杂性. 但是使用本地缓存,又必须在应用启动时把数据加载到内存中. 增加了应用启动的负担,降低开发效率. 假如我们的数据又是基于大数据, 我们知道大数据查询的API响应时间一般比较长. 此时我们也常常会考虑使用文件系统来缓冲数据, 启动直接读本地缓存. 然后定时更新数据,更新文件.
这样做在物理机部署时问题不大,但是一旦我们系统上云了. 则可能面对每次启动服务都需要创建一次缓存文件. 这会使情况变得更为糟糕
需要解决什么问题
既然是缓存,那么就必须要解决缓存都几个问题即:
缓存数据存储
缓存更新
我的方案是如何做的呢?
关于数据存储: 使用Gson等Json工具将Collection Map Object转换成字符串, 字符串通过getBytes(StandardCharsets.UTF_8)转换成byte[] 存储到MySQL到 BLOB字段里. 为什么要转换成byte[]. 我们知道二进制用来传输数据,没有中间转换环节,是非常安全的,这里说的不是网络安全,了解中文乱码的同学应该会深有感触
为什么使用Gson, 第一是API简单,第二是增减对象字段不会反序列化失败 (这点很重要). 笔者曾考虑使用SerializationUtils, 但是要求model实现Serializable接口. 但List Map等没有实现啊.这也不难,使用一个实现了序列化接口的对象包装List Map也可以, 但是增减字段那就没办法了.
2022-04-10更新:实际使用时,我碰到过比较大的集合对象,达到几十M。像List Map互相嵌套那种,这个时候如果实时用Gson去反序列化,效率会非常低,可能要去到10秒以上。而使用Serializable方式效率高很多。我看到有的博主说Json反序列化一般性能优于JDK序列化。这很有可能是基于简单POJO对象测试的,使用场景是跨进程调用的序列化(http rpc等),这个时候确实对性能要求较高,很明显,这篇文章不是这个场景
关于缓存更新: 如果直接查mysql,其实也不存在缓存数据更新问题. 但是因为我们缓存Value大且更新可能是几个小时一次,甚至一天一次. 所以可以使用内存二级缓存来提升性能. 这就有缓存更新的问题了. 实现原理也很简单,依然每次都去查数据库. 但是只是比对数据是否有更新, 使用版本号,或更新时间均可. 那么查询速度会非常快, 满足后台场景绰绰有余.
核心代码
importcom.google.common.collect.Maps;
importcom.google.gson.Gson;
importcom.google.gson.reflect.TypeToken;
importlombok.AllArgsConstructor;
importlombok.NonNull;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.stereotype.Component;
importjava.nio.charset.StandardCharsets;
importjava.util.Date;
importjava.util.Objects;
@Slf4j
@Component
@AllArgsConstructor
publicclassObjectCacheService{
privatefinalCacheMapper cacheMapper;
privatefinalMap localCache = Maps.newHashMapWithExpectedSize(96);
privatefinalGson GSON =newGson();// 多线程安全
publicvoidsave(String key, T t){
CachePO entity = getEntity(key);
String s = GSON.toJson(t);
entity.setObjectCache(s.getBytes(StandardCharsets.UTF_8));
cacheMapper.save(entity);
}
publicT get(String key, TypeToken
typeToken){ if(Objects.isNull(key)) {
returnnull;
}
CachePO po = cacheMapper.findByKey(key);
if(Objects.nonNull(po) && Objects.nonNull(po.getObjectCache())) {
returnGSON.fromJson(newString(po.getObjectCache(), StandardCharsets.UTF_8), typeToken.getType());
}
returnnull;
}
publicT getLocalCached(@NonNull String key, TypeToken
typeToken){ CachePO entity = getCachePO(key);
if(Objects.nonNull(entity) && Objects.nonNull(entity.getObjectCache())) {
returnGSON.fromJson(
newString(entity.getObjectCache(),StandardCharsets.UTF_8), typeToken.getType());
}
log.warn("no-object-cache for {}", key);
returnnull;
}
private CachePO getEntity(String key){
CachePO entity = cacheMapper.findByKey(key);
if(Objects.isNull(entity)) {
entity =newCachePO();
entity.setKey(key);
}
returnentity;
}
/**
* 有最新的获取最新,没有就拿缓存里的
*/
private CachePO getCachePO(String key){
booleanneedUseRemote =false;// 如果需要使用MySQL 中的数据,设置为true
CachePO CachePO = localCache.get(key);
if(Objects.isNull(CachePO)) {
needUseRemote =true;// 缓存为空
}else{
// 有新的缓存
Date cacheTime = CachePO.getUpdatedAt();
intcount = cacheMapper.countByKeyAndUpdatedAtAfter(key, cacheTime);
if(count >0) {
needUseRemote =true;
}
}
if(needUseRemote){
CachePO entity = cacheMapper.findByKey(key);
localCache.put(key, entity);
}
returnlocalCache.get(key);
}
}
CREATE TABLE`object_cache`(
`cache_key`varchar(50) NOT NULL COMMENT'key值',
`cache_value`mediumblob COMMENT'value值', -- 请关注blob mediumblob longblob大小
`created_at`datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT'创建时间',
`updated_at`datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT'更新时间',
PRIMARY KEY (`cache_key`)
)DEFAULT CHARSET=utf8 COMMENT='缓存表'
以上是<基于MySQL的缓存方案>的方法分享, PS:如果你是前端工程师同学,欢迎试用体验【webfunny监控系统】。
关于Webfunny
Webfunny专注于微信小程序、H5前端、PC前端线上应用实时监控,实时监控前端网页、前端数据分析、错误统计分析监控和BUG预警,第一时间报警,快速修复BUG!支持私有化部署,容器化部署,可支持千万级PV的日活量!