基于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<T> 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<T> 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的日活量!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352

推荐阅读更多精彩内容