一、Mybatis 缓存的概念
缓存就是内存中的数据,常常用来对数据库查询结果的保存,使用缓存,我们可以避免频繁地与数据库进行交互,进而提高响应速度。
mybatis 也提供了对缓存的支持,分为一级缓存和二级缓存
二、一级缓存的概念
1. 概念
一级缓存是 SqlSession 级别的缓存,在操作数据库时,需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据,不同的 SqlSession 之间的缓存数据区域(HashMap)是互补影响的。
2. 执行顺序
首先去一级缓存中查询,如果有直接返回,如果没有则查询数据库同时将查询出来的结果存到一级缓存中去
3. 数据结构
HashMap
4. 缓存刷新的时机
(1)sqlSession 去执行 commit 操作(执行插⼊、更新、删除),则会清空 SqlSession 中的 ⼀级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
(2)手动刷新缓存:sqlSession.clearCache();
三、一级缓存的原理与源码分析
- 一级缓存到底是什么
- 提到⼀级缓存就绕不开 SqlSession,所以索性就直接从 SqlSession 开始找,看看有没有创建缓存或者与缓存有关的属性或者方法
- 找了一圈,发现 SqlSession 所有方法中,好像只有
clearCache()
和缓存沾点关系,那么就直接从这个方法入手public interface SqlSession extends Closeable { void clearCache(); }
- 再深入分析,流程走到 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
- 一级缓存什么时候被创建
- 大家觉得最有可能创建缓存的地方是哪里呢?
- 我觉得是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 接口来自定义缓存。
⼆级缓存底层还是HashMap结构public class PerpetualCache implements Cache { private final String id; private MapcObject, Object> cache = new HashMapC); public PerpetualCache(String id) { this.id = id; } }
- 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 二级缓存的升级。
- pom文件
org.mybatis.caches mybatis-redis 1.0.0-beta2 - 配置文件
- redis.properties
redis.host=localhost redis.port=6379 redis.connectionTimeout=5000 redis.password= redis.database=0
- 源码分析
RedisCache 和普遍实现 Mybatis 的缓存方案大同小异,无非是实现 Cache 接口,并使用 jedis 操作缓存,不过该项目在设计细节上有⼀些区别
RedisCache 在 mybatis 启动的时候,由 myBatis 的 CacheBuilder 创建,创建的方式很简单,就是调用 RedisCache 的带有 String 参数的构造方法,即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(String id);
而在 RedisCache 的构造方法中,调用了 RedisConfigu rationBuilder 来创建 RedisConfig 对象,并使用 RedisConfig 来创建JedisPool。
RedisConfig 类继承了 JedisPoolConfig,并提供了 host,port等属性的包装,简单看⼀下 RedisConfig 的
属性:
RedisConfig 对象是由 RedisConfigurationBuilder 创建的,核心的方法就是 parseConfiguration 方法,该方法从 classpath 中读取⼀个 redis.properties 文件,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 对象中,并返回
接下来,就是 RedisCache 使用 RedisConfig 类创建完成 jedisPool,在 RedisCache 中实现了⼀个简单的模板方法,用来操作Redispublic 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; }
模板接口为 RedisCallback,这个接口中就只需要实现了⼀个 doWithRedis 方法而已private Object execute(RedisCallback callback) { Jedis jedis = pool.getResource(); try { return callback.doWithRedis(jedis); } finally { jedis.close(); } }
接下来看看 Cache 中最重要的两个方法:putObject 和 getObject,通过这两个方法来查看 mybatis-redis 储存数据的格式public interface RedisCallback { Object doWithRedis(Jedis jedis); }
可以很清楚的看到,mybatis-redis 在存储数据的时候,是使用的 hash 结构,把 cache 的 id 作为这个 hash 的 key (cache的 id 在 mybatis 中就是 mapper 的namespace)。这个 mapper 中的查询缓存数据作为 hash@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())); } }); }
的 field,需要缓存的内容直接使用 SerializeUtil 存储,SerializeUtil 和其他的序列化类差不多,负责对象的序列化和反序列化