Mybatis基础 -- 缓存

一、Mybatis 缓存的概念

缓存就是内存中的数据,常常用来对数据库查询结果的保存,使用缓存,我们可以避免频繁地与数据库进行交互,进而提高响应速度。
mybatis 也提供了对缓存的支持,分为一级缓存和二级缓存

二、一级缓存的概念

1. 概念
一级缓存是 SqlSession 级别的缓存,在操作数据库时,需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据,不同的 SqlSession 之间的缓存数据区域(HashMap)是互补影响的。
2. 执行顺序
首先去一级缓存中查询,如果有直接返回,如果没有则查询数据库同时将查询出来的结果存到一级缓存中去
3. 数据结构
HashMap
4. 缓存刷新的时机
(1)sqlSession 去执行 commit 操作(执行插⼊、更新、删除),则会清空 SqlSession 中的 ⼀级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
(2)手动刷新缓存:sqlSession.clearCache();

三、一级缓存的原理与源码分析

  • 一级缓存到底是什么
    1. 提到⼀级缓存就绕不开 SqlSession,所以索性就直接从 SqlSession 开始找,看看有没有创建缓存或者与缓存有关的属性或者方法
    2. 找了一圈,发现 SqlSession 所有方法中,好像只有clearCache()和缓存沾点关系,那么就直接从这个方法入手
      public interface SqlSession extends Closeable {
          void clearCache();
      }
      
    3. 再深入分析,流程走到 Perpetualcache 中的clear()方法之后,会调用 cache.clear()方法,那 么这个 cache 是什么东西呢?点进去发现,cache 其实就是private Map cache = new HashMap();也就是⼀个Map,所以说 cache.clear() 其实就是 map.clear(),也就是说,缓存其实就是本地存放的⼀个map对象,每⼀个SqISession 都会存放⼀个map对象的引用
      public class PerpetualCache implements Cache {
          private Map cache = new HashMap();
          public void clear() {
              this.cache.clear();
          }
      }
      
  • 一级缓存什么时候被创建
    1. 大家觉得最有可能创建缓存的地方是哪里呢?
    2. 我觉得是Executor,为什么这么认为?因为Executor是执行器,用来执行SQL请求(自定义持久层框架中就已经做过类似的事情),而且清除缓存的方法也在Executor中执行,所以很可能缓存的创建也很有可能在Executor中,看了⼀圈发现Executor中有⼀个 createCacheKey 方法,这个方法很像是创建缓存的方法,跟进去看看,发现 createCacheKey 方法是由 BaseExecutor 执行的,代码如下
      @Override
      public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
          if (closed) {
              throw new ExecutorException("Executor was closed.");
          }
          CacheKey cacheKey = new CacheKey();
          //  id就是Sql语句的所在位置包名+类名+ SQL名称
          cacheKey.update(ms.getId());
          // rowBounds就是分页对象,offset默认0
          cacheKey.update(rowBounds.getOffset());
          // rowBounds就是分页对象,limit默认Integer.MAX_VALUE
          cacheKey.update(rowBounds.getLimit());
          // 具体的sql语句
          cacheKey.update(boundSql.getSql());
          List parameterMappings = boundSql.getParameterMappings();
          TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
          // mimic DefaultParameterHandler logic
          for (ParameterMapping parameterMapping : parameterMappings) {
            if (parameterMapping.getMode() != ParameterMode.OUT) {
              Object value;
              String propertyName = parameterMapping.getProperty();
              if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
              } else if (parameterObject == null) {
                value = null;
              } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
              } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
              }
              cacheKey.update(value);
            }
          }
          if (configuration.getEnvironment() != null) {
            // issue #176
            cacheKey.update(configuration.getEnvironment().getId());
          }
          return cacheKey;
      }
      
      // 调用 cacheKey.update(configuration.getEnvironment().getId());
      // 核心配置文件中的这个id作为基准,add进updateList中
      public void update(Object object) {
          int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 
          count++;
          checksum += baseHashCode;
          baseHashCode *= count;
          hashcode = multiplier * hashcode + baseHashCode;
          updateList.add(object);
      }
      
  • 一级缓存的工作流程是怎样的
    ⼀级缓存更多是用于查询操作,我们先来看⼀下这个缓存到底用在哪了,跟踪到
    query方法如下:
    Override
    public  List query(MappedStatement ms, Object parameter, RowBounds
                      rowBounds, ResultHandler resultHandler) throws SQLException {
       BoundSql boundSql = ms.getBoundSql(parameter);
       //创建缓存
       CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
       return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    
    @SuppressWarnings("unchecked")
    Override
    public  List query(MappedStatement ms, Object parameter, RowBounds
               rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
                                                            throws SQLException {
       ...
       list = resultHandler == null ? (List) localCache.getObject(key) : null;
       if (list != null) {
           //这个主要是处理存储过程⽤的。
           handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
       } else {
           list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key,
                                           boundSql);
       }
       ...
    }
    
    private  List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List list;
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
          list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
          localCache.removeObject(key);
        }
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
          localOutputParameterCache.putObject(key, parameter);
        }
        return list;
    }
    

四、二级缓存的原理与源码分析

二级缓存的原理和一级缓存原理一样,第⼀次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是⼀级缓存是基于 sqlSession 的,而二级缓存是基于mapper文件的 namespace 的,也 就是说多个 sqlSession 可以共享⼀个mapper中的二级缓存区域,并且如果两个mapper的 namespace 相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。


  • 开启二级缓存
    
    
     
         
     
    
    
    
    
    
    mapper.xml 文件中就这么⼀个空标签,其实这里可以配置,PerpetualCache这个类是 mybatis 默认实现缓存功能的类。不写 type 就使用 mybatis 默认的缓存,也可以去实现 Cache 接口来自定义缓存。
    public class PerpetualCache implements Cache {
        private final String id;
        private MapcObject, Object> cache = new HashMapC);
    
        public PerpetualCache(String id) { this.id = id; }
    }
    
    ⼆级缓存底层还是HashMap结构
  • useCache
    mybatis 中还可以配置 userCache 和 flushCache 等配置项,userCache 是用来设置是否禁用二级缓存的,在 statement 中设置 useCache = false 可以禁用当前 select 语句的二级缓存,即每次查询都会发出 sql去查询,默认情况是 true,即该 sql 使用二级缓存。
    
    
  • flushCache
    在 mapper 的同⼀个 namespace 中,如果有其它 insert、update, delete 操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。设置 statement 配置中的 flushCache = "true" 属性,默认情况下为 true,即刷新缓存,如果改成 false 则不会刷新,使用缓存时如果手动修改数据库表中的查询数据会出现脏读。
    
    

五、二级缓存整合Redis

mybatis 自带的二级缓存是单服务器工作,无法实现分布式缓存。所以需要引入 redis 进行对 mybatis 二级缓存的升级。

  1. pom文件
    
        org.mybatis.caches
        mybatis-redis
        1.0.0-beta2
    
    
  2. 配置文件
    
    
     
       
       
    
    
  3. redis.properties
    redis.host=localhost
    redis.port=6379
    redis.connectionTimeout=5000
    redis.password=
    redis.database=0
    
  4. 源码分析
    RedisCache 和普遍实现 Mybatis 的缓存方案大同小异,无非是实现 Cache 接口,并使用 jedis 操作缓存,不过该项目在设计细节上有⼀些区别
    public final class RedisCache implements Cache {
        public RedisCache(final String id) {
            if (id == null) {
                throw new IllegalArgumentException("Cache instances require anID");
            }
            this.id = id;
            RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
            pool = new JedisPool(redisConfig,
                                 redisConfig.getHost(), 
                                 redisConfig.getPort(),
                                 redisConfig.getConnectionTimeout(),
                                 redisConfig.getSoTimeout(), 
                                 redisConfig.getPassword(),
                                 redisConfig.getDatabase(), 
                                 redisConfig.getClientName()
                   );
        }
    }
    
    RedisCache 在 mybatis 启动的时候,由 myBatis 的 CacheBuilder 创建,创建的方式很简单,就是调用 RedisCache 的带有 String 参数的构造方法,即RedisCache(String id);而在 RedisCache 的构造方法中,调用了 RedisConfigu rationBuilder 来创建 RedisConfig 对象,并使用 RedisConfig 来创建JedisPool。
    RedisConfig 类继承了 JedisPoolConfig,并提供了 host,port等属性的包装,简单看⼀下 RedisConfig 的
    属性:
    public class RedisConfig extends JedisPoolConfig {
        private String host = Protocol.DEFAULT_HOST;
        private int port = Protocol.DEFAULT_PORT;
        private int connectionTimeout = Protocol.DEFAULT_TIMEOUT;
        private int soTimeout = Protocol.DEFAULT_TIMEOUT;
        private String password;
        private int database = Protocol.DEFAULT_DATABASE;
        private String clientName;
    }
    
    RedisConfig 对象是由 RedisConfigurationBuilder 创建的,核心的方法就是 parseConfiguration 方法,该方法从 classpath 中读取⼀个 redis.properties 文件,
    并将该配置文件中的内容设置到 RedisConfig 对象中,并返回
    public RedisConfig parseConfiguration(ClassLoader classLoader) {
        Properties config = new Properties();
        InputStream input = classLoader.getResourceAsStream(redisPropertiesFilename);
        if (input != null) {
            try {
                config.load(input);
            } catch (IOException e) {
                throw new RuntimeException("An error occurred while reading classpath property '" + redisPropertiesFilename + "', see nested exceptions", e);
            } finally {
                try {
                    input.close();
                } catch (IOException e) {
                    // close quietly
                }
            }
        }
        RedisConfig jedisConfig = new RedisConfig();
        setConfigProperties(config, jedisConfig);
        return jedisConfig; 
    }
    
    接下来,就是 RedisCache 使用 RedisConfig 类创建完成 jedisPool,在 RedisCache 中实现了⼀个简单的模板方法,用来操作Redis
    private Object execute(RedisCallback callback) {
        Jedis jedis = pool.getResource();
        try {
            return callback.doWithRedis(jedis);
        } finally {
            jedis.close();
        } 
    }
    
    模板接口为 RedisCallback,这个接口中就只需要实现了⼀个 doWithRedis 方法而已
    public interface RedisCallback {
         Object doWithRedis(Jedis jedis);
    }
    
    接下来看看 Cache 中最重要的两个方法:putObject 和 getObject,通过这两个方法来查看 mybatis-redis 储存数据的格式
    @Override
    public void putObject(final Object key, final Object value) {
         execute(new RedisCallback() {
             @Override
             public Object doWithRedis(Jedis jedis) {
                 jedis.hset(id.toString().getBytes(), 
                            key.toString().getBytes(), 
                            SerializeUtil.serialize(value));
                 return null;
             }
         });
    }
    
    @Override
    public Object getObject(final Object key) {
        return execute(new RedisCallback() {
            @Override
            public Object doWithRedis(Jedis jedis) {
                return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(), key.toString().getBytes()));
            }
        });
    }
    
    可以很清楚的看到,mybatis-redis 在存储数据的时候,是使用的 hash 结构,把 cache 的 id 作为这个 hash 的 key (cache的 id 在 mybatis 中就是 mapper 的namespace)。这个 mapper 中的查询缓存数据作为 hash
    的 field,需要缓存的内容直接使用 SerializeUtil 存储,SerializeUtil 和其他的序列化类差不多,负责对象的序列化和反序列化

你可能感兴趣的:(Mybatis基础 -- 缓存)