基于MySQL的缓存方案

yljz 1年前 ⋅ 1213 阅读
ad

前言

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

  1. 使用guava等第三方工具类提供的缓存能力
  2. 自己基于集合类实现
  3. 内存缓存配合本地文件系统实现
  4. 使用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大且更新可能是几个小时一次,甚至一天一次.  所以可以使用内存二级缓存来提升性能. 这就有缓存更新的问题了.  实现原理也很简单,依然每次都去查数据库. 但是只是比对数据是否有更新, 使用版本号,或更新时间均可. 那么查询速度会非常快, 满足后台场景绰绰有余. 

核心代码

import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Objects;
 
@Slf4j
@Component
@AllArgsConstructor
public class ObjectCacheService {
    
    private final CacheMapper cacheMapper;
    private final Map<String, CachePO> localCache = Maps.newHashMapWithExpectedSize(96);
    private final Gson GSON = new Gson(); // 多线程安全
    
    public <T> void save(String key, T t) {
        CachePO entity = getEntity(key);
        String s = GSON.toJson(t);
        entity.setObjectCache(s.getBytes(StandardCharsets.UTF_8));
        cacheMapper.save(entity);
    }
    
    public <T> T get(String key, TypeToken<T> typeToken) {
        if (Objects.isNull(key)) {
            return null;
        }
        CachePO po = cacheMapper.findByKey(key);
        if (Objects.nonNull(po) && Objects.nonNull(po.getObjectCache())) {
            return GSON.fromJson(new String(po.getObjectCache(), StandardCharsets.UTF_8), typeToken.getType());
        }
        return null;
    }
    
    public <T> T getLocalCached(@NonNull String key, TypeToken<T> typeToken) {
        CachePO entity = getCachePO(key);
        if (Objects.nonNull(entity) && Objects.nonNull(entity.getObjectCache())) {
            return GSON.fromJson(
                    new String(entity.getObjectCache(),StandardCharsets.UTF_8), typeToken.getType());
        }
        log.warn("no-object-cache for {}", key);
        return null;
    }
    
    private CachePO getEntity(String key) {
        CachePO entity = cacheMapper.findByKey(key);
        if (Objects.isNull(entity)) {
            entity = new CachePO();
            entity.setKey(key);
        }
        return entity;
    }
    
    /**
     * 有最新的获取最新,没有就拿缓存里的
     */
    private CachePO getCachePO(String key) {
        boolean needUseRemote = false; // 如果需要使用MySQL 中的数据,设置为true
        CachePO CachePO = localCache.get(key);
        if (Objects.isNull(CachePO)) {
            needUseRemote = true; // 缓存为空
        } else {
            // 有新的缓存
            Date cacheTime = CachePO.getUpdatedAt();
            int count = cacheMapper.countByKeyAndUpdatedAtAfter(key, cacheTime);
            if (count > 0) {
                needUseRemote = true;
            }
        }
        if (needUseRemote){
            CachePO entity = cacheMapper.findByKey(key);
            localCache.put(key, entity);
        }
        return localCache.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='缓存表'

关于Webfunny

Webfunny专注于前端监控系统,前端埋点系统的研发。 致力于帮助开发者快速定位问题,帮助企业用数据驱动业务,实现业务数据的快速增长。支持H5/Web/PC前端、微信小程序、支付宝小程序、UniApp和Taro等跨平台框架。实时监控前端网页、前端数据分析、错误统计分析监控和BUG预警,第一时间报警,快速修复BUG!支持私有化部署,Docker容器化部署,可支持千万级PV的日活量!

  点赞 0   收藏 0
  • yljz
    共发布1篇文章 获得0个收藏
全部评论: 0