拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 SpringBoot整合Ehcache3

SpringBoot整合Ehcache3

白鹭 - 2022-01-24 2128 0 0

前言

公司部门老项目要迁移升级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 = https://www.cnblogs.com/yanpeng19940119/p/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());
    }
标签:

0 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *