一个简单实用的函数式缓存工具类

一个简单实用的函数式缓存工具类:封装了基本的缓存增删查操作,提供了热点数据集中失效和缓存穿透的统一解决方案,以及在此基础上的开发模型。

背景介绍

日常开发中缓存使用越来越普遍而问题也接憧而至,首先是开发方式的繁琐:查询缓存->有就直接返回,没有就查询数据库或者调用其他服务获取,再保存至缓存中...如:

    /**
     * 获取渠道下,字段值
     * 
  • * @param channelNo: 渠道 * @param field: 字段 * @return: java.lang.String 值 */ private String getChannelConfigFieldValue(String channelNo,String field){ .... //从缓存获取 String key = StringUtils.join(CHANNEL_CONFIG_KEY_PREFIX,channelNo); String result = RedisUtils.get(key); JSONObject jsonObject; if(StringUtils.isNotBlank(result)){ jsonObject = JSON.parseObject(result); }else{ //加载并缓存数据 jsonObject = loadAndCacheChannelConfigInfo(channelNo); } .... } /** * 加载并缓存渠道配置信息 *
  • * @param channelNo: 渠道号 * @return: com.alibaba.fastjson.JSONObject */ private JSONObject loadAndCacheChannelConfigInfo(String channelNo){ ..... //发起远程调用/访问数据库 加载数据 Map queryParam = new HashMap<>(); ... log.info("加载并缓存渠道配置信息 param => {}", JSON.toJSONString(queryParam)); String result = HttpClientUtils.sendHttpPostJson(getChannelInfoUrl, JSON.toJSONString(queryParam)); ... JSONObject jsonObject = JSON.parseObject(result); ... //设置缓存 String key = StringUtils.join(CHANNEL_CONFIG_KEY_PREFIX,channelNo); RedisUtils.set(key, result,CACHE_EXPIRE_TIME); log.info("加载并缓存渠道配置信息成功 key=> {}", key); return jsonObject; ... }

    此类模板式的代码出现在使用缓存的任何地方,且大多是CV大法而来,既不美观也不优雅,还极易出错。其次,在使用缓存的过程中由于使用方法或者对问题的处理不当,可能给数据库或者依赖的服务造成严重的影响,常见的问题如:热点数据集中失效,缓存穿透等。所以需要將缓存开发使用过程中的共性抽离出来,抽象并封装使之模板化,规范化,并最终形成一个可扩展,易维护,使用方便的工具或者工具集。

    设计思路

    • 统一缓存开发使用模式,提供/约定一套开发方法/模型。

      动静分离:抽象公共部分,函数化变化部分。

    • 封装常见问题处理流程默认实现,并提供扩展机制。

      SPI机制:内部默认实现,外部可扩展。

    • 资源使用细粒度保护,减少资源竞争。

      分布式锁:redisson分布式锁

    知识准备

    一,函数式编程:

    1. 什么是函数式编程

      函数式编程并不是Java新提出的概念,属于编程范式中的一种,它起源于一个数学问题,我们并不需要过多的了解函数式编程的历史,要追究它的历史以及函数式编程,关于范畴论、柯里化早就让人立马放弃学习函数式编程了,对于函数式编程我们所要知道的是,它能将一个行为传递作为参数进行传递。

    2. 函数式编程的目的

      Java8出现之前,我们关注的往往是某一类对象应该具有什么样的属性,当然这也是面向对象的核心--对数据进行抽象。但是java8出现以后,这一点开始出现变化,似乎在某种场景下,更加关注某一类共有的行为(这似乎与之前的接口有些类似),这也就是java8提出函数式编程的目的。

    对象-函数.png
    1. Lambda表达式

      不得不提增加Lambda的目的,其实就是为了支持函数式编程,而为了支持Lambda表达式,才有了函数式接口。另外,为了在面对大型数据集合时,为了能够更加高效的开发,编写的代码更加易于维护,更加容易运行在多核CPU上,java在语言层面增加了Lambda表达式。

    2. 函数式接口

      在Java中有一个接口中只有一个方法表示某特定方法并反复使用,例如Runnable接口中只有run方法就表示执行的线程任务。Java8中对于这样的接口有了一个特定的名称——函数式接口。Java8中即使是支持函数式编程,也并没有再标新立异另外一种语法表达。所以只要是只有一个方法的接口,都可以改写成Lambda表达式。在Java8中新增了java.util.function用来支持Java的函数式编程,其中的接口均是只包含一个方法,与其他接口的区别:

      • 函数式接口中只能有一个抽象方法(我们在这里不包括与Object的方法重名的方法);
      • 可以有从Object继承过来的抽象方法,因为所有类的最终父类都是Object;
      • 接口中唯一抽象方法的命名并不重要,因为函数式接口就是对某一行为进行抽象,主要目的就是支持Lambda表达式。

      Java8还提供了@FunctionalInterface注解来帮助我们标识函数式接口。另外需要注意的是函数式接口的目的是对某一个行为进行封装,某些接口可能只是巧合符合函数式接口的定义。java8的Function包下的类是为了支持基本类型而添加的接口,部分类图如下:

    Supplier.png
    1. 进一步学习

      推荐大家阅读《java8 in action》 以及《Java函数式编程》,前者是对java8新特性全面的阐述,深入浅出,注重实战,非常适合入门。后者则注重函数式编程背后的故事,教你如何构建函数式数据结构,以及函数式编程范式如何帮助你编写更好的程序。

    二,SPI机制

    1. java SPI 机制

      SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。

    java-spi.png
    1. SPI机制的约定:

      • 在META-INF/services/目录中创建以接口全限定名命名的文件该文件内容为Api具体实现类的全限定名
      • 使用ServiceLoader类动态加载META-INF中的实现类
      • 如SPI的实现类为Jar则需要放在主程序classPath中
      • Api具体实现类必须有一个不带参数的构造方法
    2. 不足

      • 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
      • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
      • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
    3. 改造

      基于 xkernel

    三,分布式锁

    这里用到分布式锁 starter,详见:分布式锁 starter

    实施步骤

    一,缓存模块设计

    整个模块围绕2个接口和1个工具类展开:缓存管理接口,缓存函数接口,缓存整合工具类。

    • 缓存管理接口主要定义常用缓存基本操作接口,如设置,获取,删除缓存等,默认实现基于Redis。

    • 缓存函数接口则是基于Suppliper和Function2个函数式接口,分别定义了获取单个对象和集合对象的4个常用接口,后续可根据实际需要扩展,默认实现基于缓存管理接口+分布式锁 starter 。

    • 缓存整合工具类则组合了2个接口,对外统一提供缓存相关操作方法。

    • 缓存管理接口和缓存函数接口是模块的扩展点,基于xkernel 提供的SPI机制,结合SpringBoot注解 ConditionalOnBean,ConditionalOnProperty实现。

      总体类结构图如下所示:

    CacheManager.png

    包结构如下:

    xService
     └── src
        ├── main  
        │   ├── java  
        │   │   └── com.javacoo 
        │   │   ├────── xservice
        │   │   │         ├──────cache
        │   │   │         │         ├── CacheFunction 缓存函数接口
        │   │   │         │         ├── CacheManager 缓存管理接口
        │   │   │         │         ├── CacheFactory 缓存工厂
        │   │   │         │         ├── CacheHolder 缓存对象持有者
        │   │   │         │         ├── config
        │   │   │         │         │     └── CacheConfig 缓存配置
        │   │   │         │         └── internal 接口内部实现
        │   │   │         │               ├── redis
        │   │   │         │               └──   ├── CombinatorialFunction 函数组合对象
        │   │   │         │                     ├── RedisCacheFunction 缓存函数接口实现类
        │   │   │         │                     └── RedisCacheManager 缓存管理接口实现类
        │   │   │         └──────utils
        │   │   │                   └── CacheUtil 缓存工具类
        │   └── resource  
        │       ├── META-INF
        │             └── ext
        │                  └── internal
        │                          ├── com.javacoo.xservice.base.support.cache.CacheFunction
        │                          └── com.javacoo.xservice.base.support.cache.CacheManager
        └── test  测试
    

    二,核心逻辑概述

    模块核心是围绕数据加载方案及热点数据集中失效和缓存穿透问题解决方案展开:数据加载方案主要是采取多重检查机制,确保分布式,高并发条件下数据不多加载,不漏加载。热点数据集中失效和缓存穿透问题解决方案主要采用如下策略:

    • 热点数据集中失效解决方案:redisson分布式锁+随机过期时间
    • 缓存穿透的解决方案:设置空数据特定值(根据业务场景特性:空数据的key数量有限、key重复请求概率较高),缺点:需要存储所有空数据的key,对于一些恶意攻击,KEY不相同的情况,也起不了保护数据库的作用。
    • 缓存穿透的解决备选方案:空数据的key各不相同、key重复请求概率低的场景而言,可使用BloomFilter。

    加载数据并缓存流程图:

    加载数据并设置缓存.png

    三,如何扩展

    基于xkernel 提供的SPI机制,扩展非常方便,大致步骤如下:

    1. 实现缓存函数接口:如 com.xxxx.xxxx.MyCacheFunction
    2. 配置缓存函数接口:
      • 在项目resource目录新建包->META-INF->services
      • 创建com.javacoo.xservice.base.support.cache.CacheFunction文件,文件内容:实现类的全局限定名,如:
    myCacheFunction=com.xxxx.xxxx.MyCacheFunction
    
    • 修改配置文件,添加如下内容:
    #缓存函数接口实现
    app.config.cache.functionImpl = myCacheFunction
    

    四,如何使用

    由于过于简单,直接上代码,如下所示:

        /**
         * 查询缓存KEY:id
         */
        private static final String QUERY_CACHE_KEY = "query:%1$s";
        /**
         * 查询缓存超时时间:1 分钟
         */
        private static final int QUERY_CACHE_TIMEOUT = 60 * 1;
        /**
         * 缓存工具类
         */
        @Autowired
        protected CacheUtil cacheUtil;
        @Autowired
        private ExampleDao exampleDao;
    
        @Override
        public Optional getExampleInfo(String id) {
            AbstractAssert.isNotBlank(id, ErrorCodeConstants.SERVICE_GET_EXAMPLE_INFO_ID);
            // 从缓存中获取数据
            String cacheKey = String.format(QUERY_CACHE_KEY,id);
            return Optional.ofNullable(cacheUtil.getCacheValueFunction(cacheKey,QUERY_CACHE_TIMEOUT,ExampleDto.class, getExampleInfoFunction,id));
        }
    
        /**
         * 获取示例信息函数
         * 
  • * @author [email protected] * @param id: id * @return: ExampleDto 示例信息 */ private Function getExampleInfoFunction = (String id)-> exampleDao.getExampleInfo(id);

    五,代码实现

    1. 缓存管理接口

      /**
       * 缓存管理接口
       * 
    2. * * @author: [email protected] */ @Spi(CacheConfig.DEFAULT_IMPL) public interface CacheManager { /** * 设置单个值 *
    3. * @author [email protected] * @param key: 键 * @param value: 值 * @return: boolean true->成功 */ boolean set(String key, Object value); /** * 设置单个值并设置失效时间 *
    4. * @author [email protected] * @param key: 键 * @param value: 值 * @param expireTime:缓存时间,单位秒 * @return: boolean true->成功 */ boolean setAndExpire(String key, Object value, int expireTime); /** * 获取单个值 *
    5. * @author [email protected] * @param key: 键 * @return: T 返回对象 */ T get(String key); /** * 批量设置hash *
    6. * @author [email protected] * @param key: 键 * @param hash: Map对象 * @return: boolean true->成功 */ boolean hmSet(String key, Map hash); /** * 给hash字段设置值 *
    7. * @author [email protected] * @param key: 键 * @param field: 字段 * @param value: 值 * @return: boolean true->成功 */ boolean hSet(String key, String field, Object value); /** * 获取hash值 *
    8. * @author [email protected] * @param key: 键 * @param field: 字段 * @return: T 返回对象 */ T hGet(String key, String field); /** * 如果缓存key不存在则设置缓存并设置失效时间,否则不做操作 *
    9. * @author [email protected] * @param key: 键 * @param value: 值 * @param time: 缓存时间,单位秒 * @return: boolean true->成功 */ boolean setIfAbsent(final String key, Object value, int time); /** * 删除缓存 *
    10. * @author [email protected] * @date 2021/7/14 9:13 * @param key: 键 * @return: boolean true->成功 */ boolean del(String key); /** * 删除hash缓存 *
    11. * @author [email protected] * @param key: 键 * @param fields: 字段 * @return: long 值 */ long hashDel(String key, String... fields); /** * 自增 *
    12. * @author [email protected] * @param key: 键 * @param liveTime: 天数 这个计数器的有效存留时间 * @param delta: 自增量 * @return: java.lang.Long */ Long incr(String key, long liveTime, long delta); }
    13. 缓存管理接口实现类:

      /**
       * 缓存管理接口实现类
       * 
    14. 基于redis实现
    15. * * @author: [email protected] */ @Slf4j public class RedisCacheManager implements CacheManager { /** * RedisTemplate */ @Autowired private RedisTemplate redisTemplate; /** * 设置单个值 *
    16. * * @param key : 键 * @param value : 值 * @author [email protected] * @return: boolean true->成功 */ @Override public boolean set(String key, Object value) { Objects.requireNonNull(key, "缓存Key不能为空!"); try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { log.error("[Redis缓存]设置缓存 key:{},value:{},失败:{}", key, value, e); return false; } } /** * 设置单个值并设置失效时间 *
    17. * * @param key : 键 * @param value : 值 * @param expireTime :缓存时间,单位秒 * @author [email protected] * @return: boolean true->成功 */ @Override public boolean setAndExpire(String key, Object value, int expireTime) { Objects.requireNonNull(key, "缓存Key不能为空!"); try { //设置默认时间24H expireTime = expireTime <= 0 ? 1 : expireTime; redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS); return true; } catch (Exception e) { log.error("[Redis缓存]设置缓存 key:{},value:{},失败:{}", key, value, e); return false; } } /** * 获取单个值 *
    18. * * @param key : 键 * @author [email protected] * @return: T 返回对象 */ @Override public T get(String key) { Objects.requireNonNull(key, "缓存Key不能为空!"); try { return (T) redisTemplate.opsForValue().get(key); } catch (Exception e) { log.error("[Redis缓存]获取缓存 key:{},失败:{}", key, e); return null; } } /** * 批量设置hash *
    19. * * @param key : 键 * @param hash : Map对象 * @author [email protected] * @return: boolean true->成功 */ @Override public boolean hmSet(String key, Map hash) { Objects.requireNonNull(key, "缓存Key不能为空!"); try { redisTemplate.opsForHash().putAll(key, hash); return true; } catch (Exception e) { log.error("[Redis缓存]设置缓存hash key:{},value:{},失败:{}", key, hash, e); return false; } } /** * 给hash字段设置值 *
    20. * * @param key : 键 * @param field : 字段 * @param value : 值 * @author [email protected] * @return: boolean true->成功 */ @Override public boolean hSet(String key, String field, Object value) { Objects.requireNonNull(key, "缓存Key不能为空!"); Objects.requireNonNull(field, "缓存哈希字段不能为空!"); try { redisTemplate.opsForHash().put(key, field, value); return true; } catch (Exception e) { log.error("[Redis缓存]设置hash值, key:{}, value:{} 失败. {}", key, field, e); return false; } } /** * 获取hash值 *
    21. * * @param key : 键 * @param field : 字段 * @author [email protected] * @return: T 返回对象 */ @Override public T hGet(String key, String field) { Objects.requireNonNull(key, "缓存Key不能为空!"); Objects.requireNonNull(field, "缓存哈希字段不能为空!"); try { return (T) redisTemplate.opsForHash().get(key, field); } catch (Exception e) { log.error("[Redis缓存]获取缓存hash key:{},field:{} 失败,{}", key, field, e); return null; } } /** * 如果缓存key不存在则设置缓存并设置失效时间,否则不做操作 *
    22. * * @param key : 键 * @param value : 值 * @param time : 缓存时间,单位秒 * @author [email protected] * @return: boolean true->成功 */ @Override public boolean setIfAbsent(String key, Object value, int time) { Objects.requireNonNull(key, "缓存Key不能为空!"); try { time = time <= 0 ? 1 : time; return redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS); } catch (Exception e) { log.error("[Redis缓存]设置缓存 key:{},value:{}失败,{}", key, value, e); return false; } } /** * 删除缓存 *
    23. * * @param key : 键 * @author [email protected] * @return: boolean true->成功 */ @Override public boolean del(String key) { Objects.requireNonNull(key, "缓存Key不能为空!"); try { return redisTemplate.delete(key); } catch (Exception e) { log.error("[Redis缓存]删除缓存 key:{} 失败.{}", key, e); return false; } } /** * 删除hash缓存 *
    24. * * @param key : 键 * @param fields : 字段 * @author [email protected] * @return: long 值 */ @Override public long hashDel(String key, String... fields) { Objects.requireNonNull(key, "缓存Key不能为空!"); Objects.requireNonNull(fields, "缓存哈希字段不能为空!"); try { return redisTemplate.opsForHash().delete(key, fields); } catch (Exception e) { log.error("[Redis缓存]删除缓存 key:{},fields:{} 失败,{}", key, fields, e); } return -1; } /** * 自增 *
    25. * * @param key : 键 * @param liveTime : 天数 这个计数器的有效存留时间 * @param delta : 自增量 * @author [email protected] * @return: java.lang.Long */ @Override public Long incr(String key, long liveTime, long delta) { RedisAtomicLong entityIdCounter = new RedisAtomicLong("INCSEQ_"+key, redisTemplate.getConnectionFactory()); Long increment = entityIdCounter.addAndGet(delta); //初始设置过期时间 if ((null == increment || increment.longValue() == 0) && liveTime > 0) { entityIdCounter.expire(liveTime, TimeUnit.DAYS); } return increment; } }
    26. 缓存函数接口:

      /**
       * 缓存函数接口
       * @author: [email protected]
       * @since: 2021/7/14 11:08
       */
      @Spi(CacheConfig.DEFAULT_IMPL)
      public interface CacheFunction {
          /**
           * 根据参数获取缓存
           * 
    27. Function版
    28. * @author [email protected] * @param key: 缓存KEY * @param expireTime: 超时时间,单位 秒 * @param clazz: 目标对象类型 * @param function: 执行函数 * @param p: 附加给function的参数 * @return: java.util.Optional 对象数据 */ Optional getCacheValueFunction(String key,int expireTime, Class clazz, Function function,P p); /** * 获取缓存 *
    29. Supplier版
    30. * @author [email protected] * @param key:缓存KEY * @param expireTime:超时时间,单位 秒 * @param clazz:目标对象类型 * @param function:执行函数 * @return: java.util.Optional 对象数据 */ Optional getCacheValueFunction(String key,int expireTime, Class clazz, Supplier function); /** * 根据参数获取集合缓存 *
    31. Function版
    32. * @author [email protected] * @param key: 缓存KEY * @param expireTime: 超时时间,单位 秒 * @param clazz: 目标对象类型 * @param function: 执行函数 * @param p: 附加给function的参数 * @return: java.util.Optional> 对象集合数据 */ Optional> getCacheListValueFunction(String key,int expireTime, Class clazz, Function> function,P p); /** * 获取集合缓存 *
    33. Supplier版本
    34. * @author [email protected] * @param key: 缓存KEY * @param expireTime: 超时时间,单位 秒 * @param clazz: 目标对象类型 * @param function: 执行函数 * @return: java.util.Optional> 对象集合数据 */ Optional> getCacheListValueFunction(String key,int expireTime, Class clazz,Supplier> function); }
    35. 缓存函数接口实现类:

      /**
       * 缓存函数接口实现类
       * 
    36. *
    37. 1,热点数据集中失效解决方案:redisson分布式锁+随机过期时间
    38. *
    39. 2,缓存穿透的解决方案:设置空数据特定值(根据业务场景特性:空数据的key数量有限、key重复请求概率较高),缺点:需要存储所有空数据的key,对于一些恶意攻击,KEY不相同的情况,也起不了保护数据库的作用
    40. *
    41. 3,缓存穿透的解决备选方案:空数据的key各不相同、key重复请求概率低的场景而言,可使用BloomFilter
    42. * @author: [email protected] */ @Slf4j public class RedisCacheFunction implements CacheFunction { /** * 根据参数获取缓存 *
    43. Function版
    44. * * @param key : 缓存KEY * @param expireTime : 超时时间,单位 秒 * @param clazz : 目标对象类型 * @param function : 执行函数 * @param p : 附加给function的参数 * @author [email protected] * @return: java.util.Optional 对象数据 */ @Override public Optional getCacheValueFunction(String key, int expireTime, Class clazz, Function function, P p) { return getCacheValue(key,expireTime,clazz,CombinatorialFunction.builder().function(function).p(p).build()); } /** * 获取缓存 *
    45. Supplier版
    46. * * @param key :缓存KEY * @param expireTime :超时时间,单位 秒 * @param clazz :目标对象类型 * @param function :执行函数 * @author [email protected] * @return: java.util.Optional 对象数据 */ @Override public Optional getCacheValueFunction(String key, int expireTime, Class clazz, Supplier function) { return getCacheValue(key,expireTime,clazz,CombinatorialFunction.builder().supplier(function).build()); } /** * 根据参数获取集合缓存 *
    47. Function版
    48. * * @param key : 缓存KEY * @param expireTime : 超时时间,单位 秒 * @param clazz : 目标对象类型 * @param function : 执行函数 * @param p : 附加给function的参数 * @author [email protected] * @return: java.util.Optional> 对象集合数据 */ @Override public Optional> getCacheListValueFunction(String key, int expireTime, Class clazz, Function> function, P p) { return getCacheListValue(key,expireTime,clazz,CombinatorialFunction.builder().listFunction(function).p(p).build()); } /** * 获取集合缓存 *
    49. Supplier版本
    50. * * @param key : 缓存KEY * @param expireTime : 超时时间,单位 秒 * @param clazz : 目标对象类型 * @param function : 执行函数 * @author [email protected] * @return: java.util.Optional> 对象集合数据 */ @Override public Optional> getCacheListValueFunction(String key, int expireTime, Class clazz, Supplier> function) { return getCacheListValue(key,expireTime,clazz,CombinatorialFunction.builder().listSupplier(function).build()); } /** * 获取集合缓存 *
    51. * @author [email protected] * @param key: 缓存KEY * @param expireTime: 超时时间,单位 秒 * @param clazz: 目标对象类型 * @param combinatorialFunction:函数组合对象 * @return: java.util.Optional> 对象集合数据 */ public Optional> getCacheListValue(String key, int expireTime, Class clazz,CombinatorialFunction combinatorialFunction) { //获取缓存 List records = getList(key, clazz); if (records != null && !records.isEmpty()) { return Optional.of(records); } //检查是否是特定值-empty Object o = get(key); if(Constants.CACHE_EMPTY_VALUE.equals(o)){ return Optional.empty(); } //获取锁失败 if (!tryLock(key)) { log.error("获取锁失败:key->{},直接返回.",key); return Optional.empty(); } //获取锁成功 try { //再检查一次:当其他等待线程获取到锁时,缓存一般已经有值,所以需要再次确认,以免重复查库 records = getList(key, clazz); if (records != null && !records.isEmpty()) { return Optional.of(records); } //执行目标函数 if(combinatorialFunction.getListSupplier() != null){ records = combinatorialFunction.getListSupplier().get(); }else if(combinatorialFunction.getListFunction() != null && combinatorialFunction.getP() != null){ records = combinatorialFunction.getListFunction().apply(combinatorialFunction.getP()); } if (records != null && !records.isEmpty()) { //设置缓存 setObject(key, records,expireTime); //再次获取缓存,确保缓存成功 records = getList(key, clazz); if (records != null && !records.isEmpty()) { log.info("线程{},执行目标函数获取数据并缓存,key={}",Thread.currentThread().getName(), key); return Optional.of(records); } } //缓存空字符串数据,防止重复请求 setAndExpire(key, Constants.CACHE_EMPTY_VALUE,expireTime); log.info("线程{},数据不存在,缓存空字符串数据,防止重复请求,key={}",Thread.currentThread().getName(), key); } catch(Exception e){ log.error("获取集合缓存失败:key->{}.",key,e); throw e; }finally { unlock(key); } return Optional.empty(); } /** * 获取单值缓存 *
    52. * @author [email protected] * @param key: 缓存KEY * @param expireTime: 超时时间,单位 秒 * @param clazz: 目标对象类型 * @param combinatorialFunction: 函数组合对象 * @return: java.util.Optional 对象数据 */ public Optional getCacheValue(String key, int expireTime, Class clazz, CombinatorialFunction combinatorialFunction) { //获取缓存 T record = getObject(key, clazz); if (record != null) { return Optional.of(record); } //检查是否是特定值-empty Object o = get(key); if(Constants.CACHE_EMPTY_VALUE.equals(o)){ return Optional.empty(); } //获取锁失败 if (!tryLock(key)) { log.error("获取锁失败:key->{},直接返回.",key); return Optional.empty(); } try { //再检查一次:当其他等待线程获取到锁时,缓存一般已经有值,所以需要再次确认,以免重复查库 record = getObject(key, clazz); if(record != null){ return Optional.of(record); } //执行目标函数 if(combinatorialFunction.getSupplier() != null){ record = combinatorialFunction.getSupplier().get(); }else if(combinatorialFunction.getFunction() != null && combinatorialFunction.getP() != null){ record = combinatorialFunction.getFunction().apply(combinatorialFunction.getP()); } if(record != null){ //设置缓存 setObject(key, record,expireTime); //再次获取缓存,确保缓存成功 record = getObject(key, clazz); if(record != null){ log.info("线程{},执行目标函数获取数据并缓存,key={}",Thread.currentThread().getName(), key); return Optional.of(record); } } //缓存空字符串数据,防止重复请求 setAndExpire(key, Constants.CACHE_EMPTY_VALUE,expireTime); log.info("线程{},数据不存在,缓存空字符串数据,防止重复请求,key={}",Thread.currentThread().getName(), key); } catch(Exception e){ log.error("根据参数获取缓存失败:key->{}.",key,e); throw e; } finally { unlock(key); } return Optional.empty(); } /** * 加锁 *
    53. * @author [email protected] * @param cacheKey: 缓存key * @return: boolean 是否成功 */ private boolean tryLock(String cacheKey){ int timeout = Lock.TIMEOUT_SECOND; if(!LockHolder.getLock().isPresent()){ log.error("不支持加锁"); return false; } boolean isLocked = LockHolder.getLock().get().tryLock(cacheKey, TimeUnit.SECONDS,0,timeout); if(isLocked){ SwapAreaUtils.getSwapAreaData().setCacheKey(cacheKey); log.info("加锁成功,KEY:{},自动失效时间:{}秒",cacheKey,timeout); } return isLocked; } /** * 解锁 *
    54. * @author [email protected] * @param key: 缓存key * @return: void */ private void unlock(String key){ if(!LockHolder.getLock().isPresent()){ return; } LockHolder.getLock().get().unlock(key); } /** * 获取对象集合 *
    55. * @author [email protected] * @param key:键 * @param clazz:对象类型 * @return: java.util.List对象集合 */ private final List getList(final String key,final Class clazz) { try{ return FastJsonUtil.toList(get(key), clazz); }catch(Exception ex){ log.error("获取对象集合->JSON转集合对象失败",ex); //如果不是空值,说明数据异常,需要删除此数据 if(!Constants.CACHE_EMPTY_VALUE.equals(get(key))){ del(key); } } return Collections.emptyList(); } /** * 获取对象 *
    56. * @author [email protected] * @param key: 键 * @param clazz: 对象类型 * @return: T 目标对象 */ private final T getObject(final String key,final Class clazz) { try{ return FastJsonUtil.toBean(get(key), clazz); }catch(Exception ex){ //如果不是空值 if(!Constants.CACHE_EMPTY_VALUE.equals(get(key))){ del(key); } } return null; } /** * 删除缓存 *
    57. * @author [email protected] * @param key: 键 * @return: boolean true->成功 */ private boolean del(String key) { if(getCacheManager() == null){ return false; } return getCacheManager().del(key); } /** * 设置对象 *
    58. * @author [email protected] * @param key: 键 * @param value: 对象 * @param expireTime 超时时间,单位 秒 */ private void setObject(final String key, final Object value,final int expireTime) { setAndExpire(key, FastJsonUtil.toJSONString(value),expireTime); } /** * 设置单个值并设置失效时间 *
    59. * @author [email protected] * @param key: 键 * @param value: 值 * @param expireTime:缓存时间,单位秒 * @return: boolean true->成功 */ private boolean setAndExpire(String key, Object value, int expireTime) { if(getCacheManager() == null){ return false; } return getCacheManager().setAndExpire(key,value,expireTime); } /** * 获取单个值 *
    60. * @author [email protected] * @param key: 键 * @return: T 返回对象 */ private T get(String key) { if(getCacheManager() == null){ return null; } return getCacheManager().get(key); } /** * 获取缓存管理器 *
    61. * @author [email protected] * @return: com.javacoo.xservice.base.support.cache.CacheManager */ private CacheManager getCacheManager(){ if(!CacheHolder.getCacheManager().isPresent()){ log.error("不支持缓存"); return null; } return CacheHolder.getCacheManager().get(); } }
    62. 函数组合对象:

      /**
       * 函数组合对象
       * 
    63. * * @author: [email protected] */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CombinatorialFunction { /** * 单值,提供者函数 */ private Supplier supplier; /** * 单值,带参数函数 */ private Function function; /** * 集合,提供者函数 */ private Supplier> listSupplier; /** * 集合,带参数函数 */ private Function> listFunction; /** * 参数 */ private P p; }
    64. 缓存管理对象持有者:

      /**
       * 缓存管理对象持有者
       * 
    65. * @author [email protected] */ public class CacheHolder { /** 缓存管理接口对象*/ static CacheManager cacheManager; /** 缓存函数接口对象*/ static CacheFunction cacheFunction; public static Optional getCacheManager() { return Optional.ofNullable(cacheManager); } public static Optional getCacheFunction() { return Optional.ofNullable(cacheFunction); } }
    66. 缓存管理接口工厂:

      /**
       * 缓存管理接口工厂
       * 
    67. * @author [email protected] */ @Slf4j @Component @ConditionalOnBean(CacheConfig.class) public class CacheFactory { /** 缓存配置 */ @Autowired private CacheConfig cacheConfig; @Bean public CacheManager createCacheManager() { log.info("初始化缓存管理,实现类名称:{}",cacheConfig.getImpl()); CacheHolder.cacheManager = ExtensionLoader.getExtensionLoader(CacheManager.class).getExtension(cacheConfig.getImpl()); log.info("初始化缓存管理,缓存管理接口实现类:{}", CacheHolder.cacheManager); return CacheHolder.cacheManager; } @Bean public CacheFunction createCacheFunction() { log.info("初始化缓存函数接口,实现类名称:{}",cacheConfig.getFunctionImpl()); CacheHolder.cacheFunction = ExtensionLoader.getExtensionLoader(CacheFunction.class).getExtension(cacheConfig.getFunctionImpl()); log.info("初始化缓存函数接口实现类:{}", CacheHolder.cacheFunction); return CacheHolder.cacheFunction; } }
    68. 工具类实现:

      /**
       * 缓存工具类
       * 
    69. 提供缓存基本操作
    70. *
    71. 获取单个对象缓存:Function版
    72. *
    73. 获取单个对象缓存:Supplier版
    74. *
    75. 获取集合类型缓存:Function版
    76. *
    77. 获取集合类型缓存:Supplier版
    78. * @author [email protected] */ @Component public class CacheUtil { /** * 设置单个值 * @param key * @param value * @return */ public boolean set(String key, Object value) { return getCacheManager() == null ? false : getCacheManager().set(key,value); } /** * 设置单个值并设置失效时间 * @param key * @param value * @param expireTime 缓存时间,单位秒 * @return */ public boolean setAndExpire(String key, Object value, int expireTime) { return getCacheManager() == null ? false : getCacheManager().setAndExpire(key,value,expireTime); } /** * 获取单个值 * @param key * @return */ public T get(String key) { return getCacheManager() == null ? null : getCacheManager().get(key); } /** * 批量设置hash * @param key * @param hash * @return */ public boolean hmSet(String key, Map hash) { return getCacheManager() == null ? false : getCacheManager().hmSet(key,hash); } /** * 给hash字段设置值 * @param key * @param field * @param value * @return */ public boolean hSet(String key, String field, Object value) { return getCacheManager() == null ? false : getCacheManager().hSet(key,field,value); } /** * 获取hash值 * 引用见{@link RedisTemplate} * @param key * @param field * @return */ public T hGet(String key, String field) { return getCacheManager() == null ? null : getCacheManager().hGet(key,field); } /** * 如果缓存key不存在则设置缓存并设置失效时间,否则不做操作 * @param key * @param value * @param time 缓存时间,单位秒 * @return */ public boolean setIfAbsent(final String key, Object value, int time) { return getCacheManager() == null ? false : getCacheManager().setIfAbsent(key,value,time); } /** * 删除缓存 * @param key * @return */ public boolean del(String key) { return getCacheManager() == null ? false : getCacheManager().del(key); } /** * 删除hash缓存 * @param key * @param fields * @return */ public long hashDel(String key, String... fields) { return getCacheManager() == null ? -1 : getCacheManager().hashDel(key,fields); } /** * redis 自增 * @param key * @param liveTime 天数 这个计数器的有效存留时间 * @param delta 自增量 * @return */ public long incr(String key, long liveTime, long delta) { return getCacheManager() == null ? -1 : getCacheManager().incr(key,liveTime,delta); } /** * 获取缓存 *

      * 说明:Function版 *

      * @author DuanYong * @param key 缓存KEY * @param expireTime 超时时间,单位 秒 * @param clazz 目标对象类型 * @param function 执行函数 * @param p 附加给function的参数 * @return 目标对象 */ public T getCacheValueFunction(String key,int expireTime, Class clazz, Function function,P p) { return getCacheFunction() == null ? null : getCacheFunction().getCacheValueFunction(key,expireTime,clazz,function,p).orElse(null); } /** * 获取缓存 *

      * 说明:Supplier版 *

      * @author DuanYong * @param key 缓存KEY * @param clazz 目标对象类型 * @param function 执行函数 * @return 目标对象 */ public T getCacheValueFunction(String key,int expireTime, Class clazz, Supplier function) { return getCacheFunction() == null ? null : getCacheFunction().getCacheValueFunction(key,expireTime,clazz,function).orElse(null); } /** * 获取集合缓存 *

      说明:Function版本

      * @author DuanYong * @param key 缓存KEY * @param clazz 目标对象类型 * @param function 执行函数 * @param p 附加给function的参数 * @return 目标对象 */ public List getCacheListValueFunction(String key,int expireTime, Class clazz, Function> function,P p) { return getCacheFunction() == null ? Collections.emptyList() : getCacheFunction().getCacheListValueFunction(key,expireTime,clazz,function,p).orElse(Collections.emptyList()); } /** * 获取集合缓存 *

      说明:Supplier版本

      * @author DuanYong * @param key 缓存KEY * @param clazz 目标对象类型 * @param function 执行函数 * @return 目标对象 */ public List getCacheListValueFunction(String key,int expireTime, Class clazz,Supplier> function) { return getCacheFunction() == null ? Collections.emptyList() : getCacheFunction().getCacheListValueFunction(key,expireTime,clazz,function).orElse(Collections.emptyList()); } /** * 获取缓存管理器 *
    79. * @author [email protected] * @return: com.javacoo.xservice.base.support.cache.CacheManager */ private CacheManager getCacheManager(){ if(!CacheHolder.getCacheManager().isPresent()){ log.error("不支持缓存"); return null; } return CacheHolder.getCacheManager().get(); } /** * 获取缓存函数接口 *
    80. * @author [email protected] * @return: com.javacoo.xservice.base.support.cache.CacheFunction */ private CacheFunction getCacheFunction(){ if(!CacheHolder.getCacheManager().isPresent()){ log.error("不支持缓存"); return null; } if(!CacheHolder.getCacheFunction().isPresent()){ log.error("不支持缓存函数"); return null; } return CacheHolder.getCacheFunction().get(); }

    应用场景

    • 适用于大多数并发较少,数据一致性要求不高的场景。

    结果验证及局限性

    • 统一了缓存使用模式,简化了开发。
    • 此方案在并发较少,数据一致性要求不高的场景效果较好。
    • 高并发,数据一致性要求高的场景其缓存设计方案可参考: OpenResty+Lua+Redis+Canal实现多级缓存架构

    后续规划

    • 完善缓存相关问题解决方案。
    一些信息
    路漫漫其修远兮,吾将上下而求索
    码云:https://gitee.com/javacoo
    QQ群:164863067
    作者/微信:javacoo
    邮箱:[email protected]
    

    你可能感兴趣的:(一个简单实用的函数式缓存工具类)