• Mybatis自定义分布式二级缓存实现与遇到的一些问题解决方案!


    先说两句:

      我们都知道Mybatis缓存分两类: 一级缓存(同一个Session会话内) & 二级缓存(基于HashMap实现的以 namespace为范围的缓存)

      今天呢, 我们不谈一级缓存, 我们来谈一谈 二级缓存, 通过查看Mybatis源码发现, 他的二级缓存实现真的十分简单, 默认的实现类是 org.apache.ibatis.cache.impl.PerpetualCache 这里贴一下他的源码吧:

      
    /**
     *    Copyright 2009-2015 the original author or authors.
     *
     *    Licensed under the Apache License, Version 2.0 (the "License");
     *    you may not use this file except in compliance with the License.
     *    You may obtain a copy of the License at
     *
     *       http://www.apache.org/licenses/LICENSE-2.0
     *
     *    Unless required by applicable law or agreed to in writing, software
     *    distributed under the License is distributed on an "AS IS" BASIS,
     *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     *    See the License for the specific language governing permissions and
     *    limitations under the License.
     */
    package org.apache.ibatis.cache.impl;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.locks.ReadWriteLock;
    
    import org.apache.ibatis.cache.Cache;
    import org.apache.ibatis.cache.CacheException;
    
    /**
     * @author Clinton Begin
     */
    public class PerpetualCache implements Cache {
    
      private String id;
    
      private Map<Object, Object> cache = new HashMap<Object, Object>();
    
      public PerpetualCache(String id) {
        this.id = id;
      }
    
      @Override
      public String getId() {
        return id;
      }
    
      @Override
      public int getSize() {
        return cache.size();
      }
    
      @Override
      public void putObject(Object key, Object value) {
        cache.put(key, value);
      }
    
      @Override
      public Object getObject(Object key) {
        return cache.get(key);
      }
    
      @Override
      public Object removeObject(Object key) {
        return cache.remove(key);
      }
    
      @Override
      public void clear() {
        cache.clear();
      }
    
      @Override
      public ReadWriteLock getReadWriteLock() {
        return null;
      }
    
      @Override
      public boolean equals(Object o) {
        if (getId() == null) {
          throw new CacheException("Cache instances require an ID.");
        }
        if (this == o) {
          return true;
        }
        if (!(o instanceof Cache)) {
          return false;
        }
    
        Cache otherCache = (Cache) o;
        return getId().equals(otherCache.getId());
      }
    
      @Override
      public int hashCode() {
        if (getId() == null) {
          throw new CacheException("Cache instances require an ID.");
        }
        return getId().hashCode();
      }
    
    }
    PerpetualCache.java

      那么既然他都已经有一个实现了, 我们为什么还要自定义实现呢? 

      原因很简单, 默认实现是(HashMap)本地缓存, 不支持分布式缓存, 而我们现在大多数项目都是以集群的方式部署, 这种情况下, 使用本地缓存会出现很严重的脏读问题, 特定情况下更新可直接导致数据不一致的问题.

      如果让缓存实现支持分布式呢? 方案有很多, 基本围绕着NoSQL数据库实现, 最常用的就是Redis了, 接下来我们就来用Redis实现自定义缓存类

      说干就干吧, 开始我们的自定义缓存实现之路

    实现过程:

      首先: Mybatis自定义实现缓存类的配置方式十分简单, 我们只需要开启二级缓存, 并在 mapper.xml 中修改如下配置:

      

      其中 type 属性值就是我们自定义的缓存实现辣!

      接下来我们来看一看实现类内部是怎么写的:

      
    package com.cardgame.demo.game.component.db;
    
    import com.cardgame.demo.component.redis.RedisUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.cache.Cache;
    
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    import static com.cardgame.demo.game.component.core.config.Constants.REDIS_KEY_GAME;
    
    /**
     * @author yjy
     * 2018-08-06 15:46
     */
    @Slf4j
    public class MybatisRedisCache implements Cache {
    
        private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    
        private String id;
    
        public MybatisRedisCache(final String id) {
            if (id == null) {
                throw new IllegalArgumentException("id can not be null");
            }
            log.debug("MyBatisRedisCache:id=" + id);
            this.id = id;
        }
    
        @Override
        public String getId() {
            return this.id;
        }
    
        @Override
        public int getSize() {
            int size = (int) RedisUtils.getInstance().getHashSizeObj(getKey());
            log.debug("MybatisCache getSize > {}", size);
            return size;
        }
    
        @Override
        public void putObject(Object key, Object value) {
            RedisUtils.getInstance().setHashObj(getKey(), key, value);
            log.debug("MybatisCache put > key: {}, val: {}", key, value);
        }
        @Override
        public Object getObject(Object key) {
            Object val = RedisUtils.getInstance().getHashObj(getKey(), key);
            log.debug("MybatisCache get > key: {}, val: {}", key, val);
            return val;
        }
    
        @Override
        public Object removeObject(Object key) {
            // 移除指定缓存
            Object obj = RedisUtils.getInstance().getHashObj(getKey(), key);
            RedisUtils.getInstance().delHashObj(getKey(), key);
            log.debug("MybatisCache remove > key: {}, val: {}", key, obj);
            return obj;
        }
    
        @Override
        public void clear() {
            // 清除所有缓存
            RedisUtils.getInstance().delObj(getKey());
            log.debug("MybatisCache clear > hashKey : {}", getKey());
        }
    
        @Override
        public ReadWriteLock getReadWriteLock() {
            return readWriteLock;
        }
        
        protected String getKey() {
            return REDIS_KEY_GAME + "mybatis_cache_" + id;
        }
    
    }
    MybatisRedisCache

      自定义缓存实现的要求很低, 只需要 实现 org.apache.ibatis.cache.Cache 就可以了. 这里我们用到了 RedisUtils 工具类, 还请同学们 对应到自己的项目中的工具类.

      至此: 我们的Mybatis二级缓存就支持分布式啦!

    问题来临:

      今天我偶然想起, 是否需要给二级缓存 设置一个过期时间?

      我们来想一下不设置过期时间会有什么问题:

        当有一天我们不得不手动修改数据库的数据时(别问为啥要动数据库, 因为我在本地测试需要修改), 如果相应的 namespace 没有 插入和更新操作, 那么他的缓存将一直有有效, 然后查出来的数据一直是修改前的数据, 而且我们使用的Redis做的缓存, 即使重启了系统缓存依然还是在, 只能从redis中找到指定缓存并清除, 实在是头疼

      有没有办法解决呢? 

      你可能已经想到了, cache标签不是支持 flushInterval 属性的吗? 设置一个 flushInterval = "10000", 这样不就行了吗?

      我开始也是这么认为的, 直到有一天我发现这个设置完全没有起作用, 缓存一直是有效的, 那问题就来了, 为什么呢?

      百思不得其解的我决定去瞄一瞄Mybatis的源码, 最终让我发现了其中的奥秘, 我们来看一下下面的代码:

      

      这是Mybatis初始化二级缓存中的一段代码, 我们可以看到, flushInterval 属性对于自定义实现类是不起作用的, 而 Mybatis 实现的缓存过期时间的原理则是利用 设计模式(装饰器模式) 在默认的 缓存实现类上封装了一层 ScheduleCache, 也正是此类实现了缓存的有效期设置.

      完了完了, 那不能设置咋办啊, 这不是坑爹吗?

      冷静下来, 先别忙. 既然 不能通过设置解决, 那我们就自己想办法解决吧

      既然你Mybatis不帮我封装 ScheduleCache, 那我就自己封装一个 ScheduleRedisCache, 原有的MybatisRedisCache不变. 我们看一下 ScheduleRedisCache的代码:

      
    package com.cardgame.demo.game.component.db;
    
    import com.cardgame.demo.component.redis.RedisUtils;
    import org.apache.ibatis.cache.Cache;
    
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    import static com.cardgame.demo.game.component.core.config.Constants.REDIS_KEY_GAME;
    
    /**
     *
     * 二级缓存 过期时间 实现
     *
     * @author yjy
     * 2018-08-13 13:21
     */
    public class ScheduleRedisCache implements Cache {
    
        private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    
        private String id;
        private long flushInterval = 10000; // 缓存刷新时间, 单位毫秒
        private Cache delegate; // 委派缓存类
    
        public ScheduleRedisCache(String id) {
            if (id == null) {
                throw new IllegalArgumentException("id can not be null");
            }
            this.id = id;
            this.delegate = new MybatisRedisCache(id);
        }
    
        @Override
        public String getId() {
            return id;
        }
    
        @Override
        public void putObject(Object key, Object value) {
            // 记录过期时间
            long timeout = System.currentTimeMillis() + flushInterval;
            RedisUtils.getInstance().setHashObj(getKey(), key, timeout);
            delegate.putObject(key, value);
        }
    
        @Override
        public Object getObject(Object key) {
            Object timeout = RedisUtils.getInstance().getHashObj(getKey(), key);
            // if 未过期
            if (timeout != null && (long) timeout > System.currentTimeMillis()) {
                // 更新过期时间
                RedisUtils.getInstance().setHashObj(getKey(), key, System.currentTimeMillis() + flushInterval);
                return delegate.getObject(key);
            }
            return null;
        }
    
        @Override
        public Object removeObject(Object key) {
            RedisUtils.getInstance().delHashObj(getKey(), key);
            return delegate.removeObject(key);
        }
    
        @Override
        public void clear() {
            RedisUtils.getInstance().delObj(getKey());
            delegate.clear();
        }
    
        @Override
        public int getSize() {
            return (int) RedisUtils.getInstance().getHashSizeObj(getKey());
        }
    
        @Override
        public ReadWriteLock getReadWriteLock() {
            return readWriteLock;
        }
    
        protected String getKey() {
            return REDIS_KEY_GAME + "schedule_cache_" + id;
        }
    
    }
    ScheduleRedisCache.java

      代码简单易懂, 说白了就是增加一个 缓存管理, 专门 缓存二级缓存的 过期时间. 在适当的时候 更新过期时间 / 清除过期时间

      需要注意的是, 既然我们封装了 MybatisRedisCache, 那么 mapper.xml 中就需要 改一改了, 如下: 

      

      

      这样, 我们就实现了 二级缓存的超时自动过期功能了!!! 

    结论: 谢谢大家!666

      

      

  • 相关阅读:
    HttpServletRequest对象(一)
    HttpServletResponse对象(二)
    HttpServletResponse对象(一)
    Servlet路径跳转问题
    sevlet的url-pattern设置
    java中使用相对路径读取文件的写法总结 ,以及getResourceAsStream() (转)
    创建第一个servlet程序--HelloServlet
    Eclipse创建一个JAVA WEB项目
    Servlet学习(二):ServletConfig获取参数;ServletContext应用:请求转发,参数获取,资源读取;类装载器读取文件
    Centos7默认安装的docker版本说明
  • 原文地址:https://www.cnblogs.com/imyjy/p/9468186.html
Copyright © 2020-2023  润新知