QPS这么高,那就来写个多级缓存吧

查询mysql数据库时,同样的输入需要不止一次获取值或者一个查询需要做大量运算时,很容易会想到使用redis缓存。但是如果查询并发量特别大的话,请求redis服务也会特别耗时,这种场景下,将redis迁移到本地减少查询耗时是一种常见的解决方法

多级缓存基本架构

基本架构.png
说明:存储选择了 mysqlredisguava cache mysql作为持久化, redis作为服务器缓存, guava cache作为本地缓存。二级缓存其实就是在 redis上面在架了一层 guava cahe
二级缓存.png

guava cache简单介绍

guava cacheconcurrent hashmap类似,都是k-v型存储,但是concurrent hashmap只能显示的移除元素,而guava cache当内存不够用时或者存储超时时会自动移除,具有缓存的基本功能

封装guava cache

  • 抽象类:SuperBaseGuavaCache.java
@Slf4j
public abstract class SuperBaseGuavaCache<K, V> {
    /**
     * 缓存对象
     * */
    private LoadingCache<K, V> cache;

    /**
     * 缓存最大容量,默认为10
     * */
    protected Integer maximumSize = 10;

    /**
     * 缓存失效时长
     * */
    protected Long duration = 10L;

    /**
     * 缓存失效单位,默认为5s
     */
    protected TimeUnit timeUnit = TimeUnit.SECONDS;

    /**
     * 返回Loading cache(单例模式的)
     *
     * @return LoadingCache<K, V>
     * */
    private LoadingCache<K, V> getCache() {
        if (cache == null) {
            synchronized (SuperBaseGuavaCache.class) {
                if (cache == null) {
                    CacheBuilder<Object, Object> tempCache = null;

                    if (duration > 0 && timeUnit != null) {
                        tempCache = CacheBuilder.newBuilder()
                            .expireAfterWrite(duration, timeUnit);
                    }

                    //设置最大缓存大小
                    if (maximumSize > 0) {
                        tempCache.maximumSize(maximumSize);
                    }

                    //加载缓存
                    cache = tempCache.build( new CacheLoader<K, V>() {
                        //缓存不存在或过期时调用
                        @Override
                        public V load(K key) throws Exception {
                            //不允许返回null值
                            V target = getLoadData(key) != null ? getLoadData(key) : getLoadDataIfNull(key);
                            return target;
                        }
                    });
                }


            }
        }

        return cache;
    }

    /**
     * 返回加载到内存中的数据,一般从数据库中加载
     *
     * @param key key值
     * @return V
     * */
    abstract V getLoadData(K key);

    /**
     * 调用getLoadData返回null值时自定义加载到内存的值
     *
     * @param key
     * @return V
     * */
    abstract V getLoadDataIfNull(K key);

    /**
     * 清除缓存(可以批量清除,也可以清除全部)
     *
     * @param keys 需要清除缓存的key值
     * */
    public void batchInvalidate(List<K> keys) {
        if (keys != null ) {
            getCache().invalidateAll(keys);
            log.info("批量清除缓存, keys为:{}", keys);
        } else {
            getCache().invalidateAll();
            log.info("清除了所有缓存");
        }
    }

    /**
     * 清除某个key的缓存
     * */
    public void invalidateOne(K key) {
        getCache().invalidate(key);
        log.info("清除了guava cache中的缓存, key为:{}", key);
    }

    /**
     * 写入缓存
     *
     * @param key 键
     * @param value 键对应的值
     * */
    public void putIntoCache(K key, V value) {
        getCache().put(key, value);
    }

    /**
     * 获取某个key对应的缓存
     *
     * @param key
     * @return V
     * */
    public V getCacheValue(K key) {
        V cacheValue = null;
        try {
            cacheValue = getCache().get(key);
        } catch (ExecutionException e) {
            log.error("获取guava cache中的缓存值出错, {}");
        }

        return cacheValue;
    }
}
复制代码

抽象类说明:

  • 1.双重锁检查并发安全的获取LoadingCache的单例对象

  • expireAfterWrite()方法指定guava cache中键值对的过期时间,默认缓存时长为10s

  • maximumSize()方法指定内存中最多可以存储的键值对数量,超过这个数量,guava cache将采用LRU算法淘汰键值对

  • 这里采用CacheLoader的方式加载缓存值,需要实现load()方法。当调用guava cacheget()方法时,如果guava cache中存在将会直接返回值,否则调用load()方法将值加载到guava cache中。在该类中,load方法中是两个抽象方法,需要子类去实现,一个是getLoadData() 方法,这个方法一般是从数据库中查找数据,另外一个是getLoadDataIfNull()方法,当getLoadData()方法返回null值时调用,guava cache通过返回值是否为null判断是否需要进行加载,load()方法中返回null值将会抛出InvalidCacheLoadException异常:

  • invalidateOne()方法主动失效某个key的缓存

  • batchInvalidate()方法批量清除缓存或清空所有缓存,由传入的参数决定

  • putIntoCache()方法显示的将键值对存入缓存

  • getCacheValue()方法返回缓存中的值

  • 抽象类的实现类:StudentGuavaCache.java

@Component
@Slf4j
public class StudentGuavaCache extends SuperBaseGuavaCache<Long, Student> {
    @Resource
    private StudentDAO studentDao;

    @Resource
    private RedisService<Long, Student> redisService;

    /**
     * 返回加载到内存中的数据,从redis中查找
     *
     * @param key key值
     * @return V
     * */
    @Override
    Student getLoadData(Long key) {
        Student student = redisService.get(key);
        if (student != null) {
            log.info("根据key:{} 从redis加载数据到guava cache", key);
        }
        return student;
    }

    /**
     * 调用getLoadData返回null值时自定义加载到内存的值
     *
     * @param key
     * @return
     * */
    @Override
    Student getLoadDataIfNull(Long key) {
        Student student = null;
        if (key != null) {
            Student studentTemp = studentDao.findStudent(key);
            student = studentTemp != null ? studentTemp : new Student();
        }

        log.info("从mysql中加载数据到guava cache中, key:{}", key);

        //此时在缓存一份到redis中
        redisService.set(key, student);
        return student;
    }
}
复制代码

实现父类的getLoadData()getLoadDataIfNull()方法

  • getLoadData()方法返回redis中的值
  • getLoadDataIfNull()方法如果redis缓存中不存在,则从mysql查找,如果在mysql中也查找不到,则返回一个空对象

查询

  • 流程图:
    查询.png
    • 1.查询本地缓存是否命中
    • 2.本地缓存不命中查询redis缓存
    • 3.redis缓存不命中查询mysql
    • 4.查询到的结果都会被load到本地缓存中在返回
  • 代码实现:
public Student findStudent(Long id) {
        if (id == null) {
            throw new ErrorException("传参为null");
        }

        return studentGuavaCache.getCacheValue(id);
    }
复制代码

删除

  • 流程图:

    删除.png

  • 代码实现:

@Transactional(rollbackFor = Exception.class)
    public int removeStudent(Long id) {
        //1.清除guava cache缓存
        studentGuavaCache.invalidateOne(id);
        //2.清除redis缓存
        redisService.delete(id);
        //3.删除mysql中的数据
        return studentDao.removeStudent(id);
    }
复制代码

更新

  • 流程图:

    更新.png

  • 代码实现:

 @Transactional(rollbackFor = Exception.class)
    public int updateStudent(Student student) {
        //1.清除guava cache缓存
        studentGuavaCache.invalidateOne(student.getId());
        //2.清除redis缓存
        redisService.delete(student.getId());
        //3.更新mysql中的数据
        return studentDao.updateStudent(student);
    }
复制代码

更新和删除就最后一步对mysql的操作不一样,两层缓存都是删除的


天太冷了,更新完毕要学罗文姬女士躺床上玩手机了



最后: 附:[完整项目地址](https://github.com/TiantianUpup/double-cache)

猜你喜欢

转载自juejin.im/post/5c224cd3f265da610e801e8a