乐趣区

关于springboot:SpringBoot整合Ehcache3

前言

公司部门老我的项目要迁徙降级 java 版本,须要进行缓存相干操作,原框架未反对这部分,通过调研 java 相干缓存计划大抵分为 ehcache 和 redis 两种,redis 的 value 最大值为 500mb 且超过 1mb 会对存取有性能影响,业务零碎须要反对列表查问缓存就不可避免的波及到大量的数据存取过滤,ehcache 反对内存 + 磁盘缓存不必放心缓存容量问题,所以框架初步版本决定集成 ehcache3,设计流程构造如下图所示

缓存配置

maven 援用

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
        </dependency>

个性化配置

  #缓存配置
  cache:
    ehcache:
      heap: 1000
      offheap: 100
      disk: 500
      diskDir: tempfiles/cache/
@Component
@ConfigurationProperties("frmae.cache.ehcache")
public class EhcacheConfiguration {
    /**
     * ehcache heap 大小
     * jvm 内存中缓存的 key 数量
     */
    private int heap;
    /**
     * ehcache offheap 大小
     * 堆外内存大小, 单位: MB
     */
    private int offheap;
    /**
     * 磁盘长久化目录
     */
    private String diskDir;
    /**
     * ehcache disk
     * 长久化到磁盘的大小, 单位: MB
     * diskDir 无效时才失效
     */
    private int disk;

    public EhcacheConfiguration(){
        heap = 1000;
        offheap = 100;
        disk = 500;
        diskDir = "tempfiles/cache/";
    }
}

代码注入配置

因为 springboot 默认缓存优先注入 redis 配置,所以须要手动申明 bean 进行注入,同时 ehcache 的 value 值必须反对序列化接口,不能应用 Object 代替,这里申明一个缓存基类,所有缓存 value 对象必须继承该类

public class BaseSystemObject implements Serializable {}
@Configuration
@EnableCaching
public class EhcacheConfig {
    @Autowired
    private EhcacheConfiguration ehcacheConfiguration;
    @Autowired
    private ApplicationContext context;

    @Bean(name = "ehCacheManager")
    public CacheManager getCacheManager() {
        // 资源池生成器配置长久化
        ResourcePoolsBuilder resourcePoolsBuilder =                   ResourcePoolsBuilder.newResourcePoolsBuilder()
                // 堆内缓存大小
                .heap(ehcacheConfiguration.getHeap(), EntryUnit.ENTRIES)
                // 堆外缓存大小
                .offheap(ehcacheConfiguration.getOffheap(), MemoryUnit.MB)
                // 文件缓存大小
                .disk(ehcacheConfiguration.getDisk(), MemoryUnit.MB);
        // 生成配置
        ExpiryPolicy expiryPolicy = ExpiryPolicyBuilder.noExpiration();
        CacheConfiguration config = CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, BaseSystemObject.class, resourcePoolsBuilder)
                // 设置永不过期
                .withExpiry(expiryPolicy)
                .build();

        CacheManagerBuilder cacheManagerBuilder = CacheManagerBuilder.newCacheManagerBuilder()
                         .with(CacheManagerBuilder.persistence(ehcacheConfiguration.getDiskDir()));
        return cacheManagerBuilder.build(true);
    }
}

缓存操作

缓存预热

针对缓存框架抉择的双写策略,即数据库和缓存同时写入,所以在系统启动时须要事后将数据库数据加载到缓存中

针对单表申明自定义注解,个性化缓存定义自定义接口

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface HPCache {

}
public interface IHPCacheInitService {String getCacheName();

    void initCache();}

零碎初始化时同步进行缓存初始化,扫描注解实体类与接口实现 Bean

@Async
    public void initCache(Class runtimeClass, List<String> extraPackageNameList) {List<Class<?>> cacheEntityList = new ArrayList<>();
        if (!runtimeClass.getPackage().getName().equals(Application.class.getPackage().getName())) {cacheEntityList.addAll(ScanUtil.getAllClassByPackageName_Annotation(runtimeClass.getPackage(), HPCache.class));
        }
        for (String packageName : extraPackageNameList) {cacheEntityList.addAll(ScanUtil.getAllClassByPackageName_Annotation(packageName, HPCache.class));
        }

        for (Class clazz : cacheEntityList) {TableName tableName = (TableName) clazz.getAnnotation(TableName.class);
            List<LinkedHashMap<String, Object>> resultList = commonDTO.selectList(tableName.value(), "*", "1=1", "", new HashMap<>(), false);
            for (LinkedHashMap<String, Object> map : resultList) {Cache cache = cacheManager.getCache(clazz.getName(), String.class, BaseSystemObject.class);
                String unitguid = ConvertOp.convert2String(map.get("UnitGuid"));
                try {Object obj = clazz.newInstance();
                    obj = ConvertOp.convertLinkHashMapToBean(map, obj);
                    cache.put(unitguid, obj);
                } catch (Exception e) {e.printStackTrace();
                }
            }
        }

        // 自定义缓存
        Map<String, IHPCacheInitService> res = context.getBeansOfType(IHPCacheInitService.class);
        for (Map.Entry en : res.entrySet()) {IHPCacheInitService service = (IHPCacheInitService) en.getValue();
            service.initCache();}

        System.out.println("缓存初始化结束");
    }

须要留神,在 EhcacheConfig 配置类中须要进行缓存名称的提前注册,否则会导致操作缓存时空指针异样

    Map<String, Object> annotatedBeans = context.getBeansWithAnnotation(SpringBootApplication.class);
        Class runtimeClass = annotatedBeans.values().toArray()[0].getClass();
        //do,dao 扫描
        List<String> extraPackageNameList = new ArrayList<String>();
        extraPackageNameList.add(Application.class.getPackage().getName());
        List<Class<?>> cacheEntityList = new ArrayList<>();
        if (!runtimeClass.getPackage().getName().equals(Application.class.getPackage().getName())) {cacheEntityList.addAll(ScanUtil.getAllClassByPackageName_Annotation(runtimeClass.getPackage(), HPCache.class));
        }
        for (String packageName : extraPackageNameList) {cacheEntityList.addAll(ScanUtil.getAllClassByPackageName_Annotation(packageName, HPCache.class));
        }

        for (Class clazz : cacheEntityList) {cacheManagerBuilder = cacheManagerBuilder.withCache(clazz.getName(), config);
        }

        // 自定义缓存
        Map<String, IHPCacheInitService> res = context.getBeansOfType(IHPCacheInitService.class);
        for (Map.Entry en :res.entrySet()) {IHPCacheInitService service = (IHPCacheInitService)en.getValue();
            cacheManagerBuilder = cacheManagerBuilder.withCache(service.getCacheName(), config);
        }

更新操作

手动获取 ehcache 的 bean 对象,调用 put,repalce,delete 办法进行操作

       private  CacheManager cacheManager = (CacheManager) SpringBootBeanUtil.getBean("ehCacheManager");
    public void executeUpdateOperation(String cacheName, String key, BaseSystemObject value) {Cache cache = cacheManager.getCache(cacheName, String.class, BaseSystemObject.class);
        if (cache.containsKey(key)) {cache.replace(key, value);
        } else {cache.put(key, value);
        }
    }

    public void executeDeleteOperation(String cacheName, String key) {Cache cache = cacheManager.getCache(cacheName, String.class, BaseSystemObject.class);
        cache.remove(key);
    }

查问操作

缓存存储单表以主键—object 模式存储,个性化缓存为 key-object 模式存储,单条记录能够通过 getCache 办法查问,列表查问须要取出整个缓存按条件进行过滤

 public Object getCache(String cacheName, String key){Cache cache = cacheManager.getCache(cacheName, String.class, BaseSystemObject.class);
        return cache.get(key);
    }

    public List<Object> getAllCache(String cacheName){List result = new ArrayList<>();
        Cache cache = cacheManager.getCache(cacheName, String.class, BaseSystemObject.class);
        Iterator iter = cache.iterator();
        while (iter.hasNext()) {Cache.Entry entry = (Cache.Entry) iter.next();
            result.add(entry.getValue());
        }
        return result;
    }

缓存与数据库数据一致性

数据库数据操作与缓存操作程序为先操作数据后操作缓存,在开启数据库事务的状况下针对单条数据单次操作是没有问题的,如果是组合操作一旦数据库操作产生异样回滚,缓存并没有回滚就会导致数据的不统一,比方执行程序为 dbop1=》cacheop1=》dbop2=》cacheop2,dbop2 异样,cacheop1 的操作曾经更改了缓存

这里抉择的计划是在数据库全副执行结束后对立操作缓存,这个计划有一个 毛病是如果缓存操作产生异样还是会呈现上述问题,理论过程中缓存只是对内存的操作异样概率较小,对缓存操作持乐观状态,同时咱们提供手动重置缓存的性能,算是一个折中计划,上面概述该计划的一个实现

申明自定义缓存事务注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheTransactional {}

申明切面监听,在标记了 CacheTransactional 注解的办法执行前进行 Redis 标识,对立执行完办法体后执行缓存操作

@Aspect
@Component
@Order(value = 101)
public class CacheExecuteAspect {
    @Autowired
    private CacheExecuteUtil cacheExecuteUtil;


    /**
     * 切面点 指定注解
     */
    @Pointcut("@annotation(com.haopan.frame.common.annotation.CacheTransactional)" +
            "|| @within(com.haopan.frame.common.annotation.CacheTransactional)")
    public void cacheExecuteAspect() {}

    /**
     * 拦挡办法指定为 repeatSubmitAspect
     */
    @Around("cacheExecuteAspect()")
    public Object around(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        CacheTransactional cacheTransactional = method.getAnnotation(CacheTransactional.class);
        if (cacheTransactional != null) {cacheExecuteUtil.putCacheIntoTransition();
               try{Object obj = point.proceed();
                   cacheExecuteUtil.executeOperation();
                   return obj;
               }catch (Exception e){e.printStackTrace();
                   throw  e;
               }
        } else {return point.proceed();
        }
    }
}

将缓存操作以线程 id 辨别放入待执行队列中序列化到 redis,提供办法对立操作

public class CacheExecuteModel implements Serializable {
    private String obejctClazzName;
    private String cacheName;
    private String key;
    private BaseSystemObject value;
    private String executeType;
}
private  CacheManager cacheManager = (CacheManager) SpringBootBeanUtil.getBean("ehCacheManager");

    @Autowired
    private RedisUtil redisUtil;

    public void putCacheIntoTransition(){String threadID = Thread.currentThread().getName();
        System.out.println("init threadid:"+threadID);
        CacheExecuteModel cacheExecuteModel = new CacheExecuteModel();
        cacheExecuteModel.setExecuteType("option");
        redisUtil.redisTemplateSetForCollection(threadID,cacheExecuteModel, GlobalEnum.RedisDBNum.Cache.get_value());
        redisUtil.setExpire(threadID,5, TimeUnit.MINUTES, GlobalEnum.RedisDBNum.Cache.get_value());
    }

    public void putCache(String cacheName, String key, BaseSystemObject value) {if(checkCacheOptinionInTransition()){String threadID = Thread.currentThread().getName();
            CacheExecuteModel cacheExecuteModel = new CacheExecuteModel("update", cacheName, key, value.getClass().getName(),value);
            redisUtil.redisTemplateSetForCollection(threadID,cacheExecuteModel, GlobalEnum.RedisDBNum.Cache.get_value());
            redisUtil.setExpire(threadID,5, TimeUnit.MINUTES, GlobalEnum.RedisDBNum.Cache.get_value());
        }else{executeUpdateOperation(cacheName,key,value);
        }

    }

    public void deleteCache(String cacheName, String key) {if(checkCacheOptinionInTransition()){String threadID = Thread.currentThread().getName();
            CacheExecuteModel cacheExecuteModel = new CacheExecuteModel("delete", cacheName, key);
            redisUtil.redisTemplateSetForCollection(threadID,cacheExecuteModel, GlobalEnum.RedisDBNum.Cache.get_value());
            redisUtil.setExpire(threadID,5, TimeUnit.MINUTES, GlobalEnum.RedisDBNum.Cache.get_value());
        }else{executeDeleteOperation(cacheName,key);
        }
    }

    public void executeOperation(){String threadID = Thread.currentThread().getName();
        if(checkCacheOptinionInTransition()){List<LinkedHashMap> executeList =  redisUtil.redisTemplateGetForCollectionAll(threadID, GlobalEnum.RedisDBNum.Cache.get_value());
            for (LinkedHashMap obj:executeList) {String executeType = ConvertOp.convert2String(obj.get("executeType"));
                if(executeType.contains("option")){continue;}
                String obejctClazzName = ConvertOp.convert2String(obj.get("obejctClazzName"));
                String cacheName = ConvertOp.convert2String(obj.get("cacheName"));
                String key = ConvertOp.convert2String(obj.get("key"));
                LinkedHashMap valueMap = (LinkedHashMap)obj.get("value");
                String valueMapJson =  JSON.toJSONString(valueMap);
                try{Object valueInstance = JSON.parseObject(valueMapJson,Class.forName(obejctClazzName));
                    if(executeType.equals("update")){executeUpdateOperation(cacheName,key,(BaseSystemObject)valueInstance);
                    }else if(executeType.equals("delete")){executeDeleteOperation(cacheName,key);
                    }
                }catch (Exception e){e.printStackTrace();
                }
            }
            redisUtil.redisTemplateRemove(threadID,GlobalEnum.RedisDBNum.Cache.get_value());
        }

    }

    public boolean checkCacheOptinionInTransition(){String threadID = Thread.currentThread().getName();
        System.out.println("check threadid:"+threadID);
        return redisUtil.isValid(threadID, GlobalEnum.RedisDBNum.Cache.get_value());
    }
退出移动版