zoukankan      html  css  js  c++  java
  • 在Spring Boot中使用Mybatis RedisCache的笔记

    MyBatis RedisCache 的项目地址

    http://mybatis.org/redis-cache/

    https://github.com/mybatis/redis-cache

    这是MyBatis官方的二级缓存的Redis实现, 因为其依赖于Jedis和固定的redis.properties, 和Spring Boot集成较为麻烦, 在Spring Boot 2.1.x中使用还会报RedisConfig初始化错误.

    实际项目使用中, 经过一些修改使其能正常使用, 记录如下

    使其正常运行

    首先不要用pom的jar包引入, 直接到github项目地址上下载源代码, 需要的只是 src/main/java/org/mybatis/caches/redis/ 目录下的文件, 将其放到自己的项目里.

    其次, 现在的源码中, 对redis.properties要求其中各项配置名称要以redis.为前缀, 和jar包引用时的要求不一样.

    这样基本就能启动运行了

    集成到Spring Boot的配置

    如果不希望单独做一个redis.properties的配置文件, 可以加上一个静态引用, 例如

    /**
     * Cons:
     * 1. Memory issues: if you redeploy the WAR without restarting the VM, you end up with 2 application contexts in the
     * same VM: the one attached to the static field of ApplicationContextHolder and the new one that is stored in the
     * ServletContext. This is just the same issue as the commons-logging memory issue.
     * 2. Tests: if you use spring tests, you will have multiple application contexts in the same VM when running a suite,
     * but only the one loaded from the first test is stored in the static field.
     * 3. Application context hierarchy: It is quite common to have a "services application context" and a "web application
     * context" (and a DispatcherServlet application context), each one being a child of the previous one. Only the root
     * (services) application context will be stored in the static variable, and thus you have a lot of beans that are not
     * accessible.
     *
     * Though, it's safe to use this in a java -jar application.
     */
    @Component
    public class ApplicationContextHolder implements ApplicationContextAware {
    
        private static ApplicationContext context;
    
        /**
         * Returns the Spring managed bean instance of the given class type if it exists.
         * Returns null otherwise.
         */
        public static <T> T getBean(Class<T> beanClass) {
            return context.getBean(beanClass);
        }
    
        @SuppressWarnings("unchecked")
        public static <T> T getBean(String name) {
            return (T) context.getBean(name);
        }
    
        @Override
        public void setApplicationContext(ApplicationContext context) throws BeansException {
            // store ApplicationContext reference to access required beans later on
            synchronized (this) {
                if (ApplicationContextHolder.context == null) {
                    ApplicationContextHolder.context = context;
                }
            }
        }
    }
    

    然后, 就可以在RedisCache.java中, 静态引用SysConfig了, 将其中初始化那一步修改为

        public MyBatisCache(final String id) {
            if (id == null) {
                throw new IllegalArgumentException("Cache instances require an ID");
            }
            this.id = id;
            redisConfig = new MyBatisCacheConfig();
            SysConfig sysConfig = ApplicationContextHolder.getBean(SysConfig.class);
            redisConfig.setHost(sysConfig.getRedisMybatis().getHost());
            redisConfig.setPort(sysConfig.getRedisMybatis().getPort());
            redisConfig.setPassword(sysConfig.getRedisMybatis().getPassword());
            redisConfig.setDatabase(sysConfig.getRedisMybatis().getDatabase());
            ...
    

    这样就可以在mapper初始化的时候拿到已经赋值的配置信息, 完成mapper对应的RedisCache实例的初始化.

    MyBatis的缓存过期机制, flushInterval参数

    在实际测试中, 发现Redis中的缓存数据TTL为-1, 在Hash中的key也无过期时间信息, 怀疑RedisCache的实现是否能正常处理缓存过期, 因此一路追查到了MyBatis的代码.

    MyBatis在每个Mapper中, 可以设置参数 flushInterval 用来控制缓存的过期时间, 这个参数, 在 MapperBuilderAssistant 中, 被设置为Cache的clearInternal

      public Cache useNewCache(Class<? extends Cache> typeClass,
          Class<? extends Cache> evictionClass,
          Long flushInterval,
          Integer size,
          boolean readWrite,
          boolean blocking,
          Properties props) {
        Cache cache = new CacheBuilder(currentNamespace)
            .implementation(valueOrDefault(typeClass, PerpetualCache.class))
            .addDecorator(valueOrDefault(evictionClass, LruCache.class))
            .clearInterval(flushInterval)
            .size(size)
            .readWrite(readWrite)
            .blocking(blocking)
            .properties(props)
            .build();
        configuration.addCache(cache);
        currentCache = cache;
        return cache;
      }
    

    而后在CacheBuilder中, 会根据这个参数, 判断是否生成代理类ScheduledCache

      private Cache setStandardDecorators(Cache cache) {
        try {
          MetaObject metaCache = SystemMetaObject.forObject(cache);
          if (size != null && metaCache.hasSetter("size")) {
            metaCache.setValue("size", size);
          }
          if (clearInterval != null) {
            cache = new ScheduledCache(cache);
            ((ScheduledCache) cache).setClearInterval(clearInterval);
          }
          if (readWrite) {
            cache = new SerializedCache(cache);
          }
          cache = new LoggingCache(cache);
          cache = new SynchronizedCache(cache);
          if (blocking) {
            cache = new BlockingCache(cache);
          }
          return cache;
        } catch (Exception e) {
          throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
        }
      }
    

    ScheduledCache内部存储了一个变量lastClear, 用来记录最后一次清空缓存的时间, 在get, put, remove等各个操作前, 会判断是否需要清空, 注意是整个namespace的缓存清空.

      private boolean clearWhenStale() {
        if (System.currentTimeMillis() - lastClear > clearInterval) {
          clear();
          return true;
        }
        return false;
      }
    
      @Override
      public void putObject(Object key, Object object) {
        clearWhenStale();
        delegate.putObject(key, object);
      }
    
      @Override
      public Object getObject(Object key) {
        return clearWhenStale() ? null : delegate.getObject(key);
      }
    

    由此可以看出, MyBatis的缓存过期管理机制还是比较粗糙的, 并且依赖本地的变量, 同样的LRU机制也是依赖本地.

    在分布式系统中使用MyBatis时如果开启缓存, 需要注意这个问题, 

    1. 各个节点对于缓存的清空时间分别计划, 实际上是叠加的, 如果设置的缓存时间为10分钟, 运行着三个节点, 并且节点都不断有查询请求, 那么在10分钟之间至少会被清空三次.

    2. 缓存的过期, 是整体进行的, 无论期间产生的数据从何时开始, 在何时被访问, 有可能一个缓存刚刚创建就被清空了.

    建议的解决方案

    1. 关闭 MyBatis 的 flushInterval , 这样就不存在缓存频繁清空的问题, 使用全局的时间任务来触发缓存清空操作

    2. 将decorators下的机制, 也改为使用集中的存储

    3. 对namespace下的缓存数据, 是否可以在值中增加过期时间, 将过期时间粒度细化到单个结果.

  • 相关阅读:
    鸿蒙的js开发模式19:鸿蒙手机下载python服务器端文件的实现
    【资源下载】Linux下的Hi3861一站式鸿蒙开发烧录(附工具)
    并发编程大扫盲:带你了解何为线程上下文切换
    内存溢出 MAT 排查工具,它真香香香
    面试官问:如何排除GC引起的CPU飙高?我脱口而出5个步骤
    小学妹问我:如何利用可视化工具排查问题?
    你不得不知的6个JDK自带JVM调优工具
    那个小白还没搞懂内存溢出,只能用案例说给他听了
    听说同学你搞不懂Spring Boot集成Mybatis的玩法,田哥来教你
    手把手教你设置JVM调优参数
  • 原文地址:https://www.cnblogs.com/milton/p/12341312.html
Copyright © 2011-2022 走看看