基于MySQL的缓存方案

一般而言,首先能想到后台缓存有以下几种方案:

使用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的日活量!

你可能感兴趣的:(基于MySQL的缓存方案)