参考:
五月的仓颉:【MyBatis源码解析】MyBatis一二级缓存
Mybatis一级缓存配置:
<setting name="localCacheScope" value="SESSION"/>
value有两个值可选:
session:缓存对一次会话中所有的执行语句有效,也就是SqlSession级别的。
statement:缓存只对当前执行的这一个Statement有效。
在一级缓存中对缓存的查询和写入是在Executor中完成的,以BaseExecutor为例,查看query方法:
public abstract class BaseExecutor implements Executor { private static final Log log = LogFactory.getLog(BaseExecutor.class); protected Transaction transaction; protected Executor wrapper; protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads; protected PerpetualCache localCache; //缓存 protected PerpetualCache localOutputParameterCache; protected Configuration configuration; protected int queryStack; private boolean closed; ...... @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); //构建CacheKey CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql);//调用了下面的query方法 } @SuppressWarnings("unchecked") @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } //如果queryStack为0或者并且有必要刷新缓存 if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache();//清空本地缓存 } List<E> list; try { queryStack++; //从缓存中获取数据,key的类型为CacheKey list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { //处理本地缓存输出参数 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else {//如果获取结果为空,从数据库中查找 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); //如果是STATEMENT级别的缓存 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // 清空缓存 clearLocalCache(); } } return list; } }
(1)从BaseExecutor的成员变量中,可以看到有一个类型为PerpetualCache变量名为localCache的字段,缓存就是用它来实现的。PerpetualCache类的成员变量也很简单,包含一个id和一个HashMap,缓存数据就存储在HashMap中。
public class PerpetualCache implements Cache { private final String id; private Map<Object, Object> cache = new HashMap<Object, Object>();//使用一个map做存储 get set方法省略 ...... }
(2)在BaseExecutor的quey方法中,有一个构建CacheKey的语句,既然缓存数据存储在HashMap中,那么数据格式一定是键值对的形式,这个CacheKey就是HashMap中的key,value是数据库返回的数据。
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
(3)第二个query方法中,当执行查询时,首先通过localCache.getObject(key)从缓存中获取数据,如果获取的数据为空,再从数据库中查找。
//从缓存中获取数据,key的类型为CacheKey list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
//如果获取结果为空,从数据库中查找 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
(4)如果开启了flushcache,将会清空缓存
//如果queryStack为0或者并且有必要刷新缓存 if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache();//清空本地缓存 }
配置flushcache:
<select id="getStudent" parameterType="String" flushCache="true"> …… </select>
(5)如果一级缓存的级别为Statement,将会清空缓存,这也是如果设置一级缓存的级别为Statement时缓存只对当前执行的这一个Statement有效的原因。
//如果是STATEMENT级别的缓存 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // 清空缓存 clearLocalCache(); }
配置:
<setting name="localCacheScope" value="STATEMENT"/>
CacheKey如何生存的
(1)在query方法中,调用了createCacheKey方法生成CacheKey,然后多次调用了cachekey的update方法,将标签的ID、分页信息、SQL语句、参数等信息作为参数传入:
@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,也就是标签所在的Mapper的namespace + 标签的id cacheKey.update(ms.getId()); //偏移量,Mybatis自带分页类RowBounds中的属性 cacheKey.update(rowBounds.getOffset()); //每次查询大小,同样是Mybatis自带分页类RowBounds中的属性 cacheKey.update(rowBounds.getLimit()); //标签中定义的SQL语句 cacheKey.update(boundSql.getSql()); //获取参数 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); // 处理SQL中的参数 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; }
(2)通过源码,看一下CacheKey的update方法,update方法中记录了调用update传入参数的次数、每个传入参数的hashcode之和checksum、以及计算CacheKey的成员变量hashcode的值。
public class CacheKey implements Cloneable, Serializable { private static final long serialVersionUID = 1146682552656046210L; public static final CacheKey NULL_CACHE_KEY = new NullCacheKey(); private static final int DEFAULT_MULTIPLYER = 37; private static final int DEFAULT_HASHCODE = 17; private final int multiplier;//一个乘数 private int hashcode;//hashcode private long checksum;//update方法中传入参数的hashcode之和 private int count; //调用update方法向updatelist添加参数的的次数 private List<Object> updateList;//调用update传入的参数会被放到updateList public CacheKey() { //初始化 this.hashcode = DEFAULT_HASHCODE; this.multiplier = DEFAULT_MULTIPLYER; this.count = 0; this.updateList = new ArrayList<Object>(); } public CacheKey(Object[] objects) { this(); updateAll(objects); } public int getUpdateCount() { return updateList.size(); } public void update(Object object) { //获取参数的hash值 int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); //计数 count++; //hash值累加 checksum += baseHashCode; //更新hash,hash=hash*count baseHashCode *= count; //计算CacheKey的hashcode hashcode = multiplier * hashcode + baseHashCode; //将参数添加到updateList updateList.add(object); } ...... }
(3)CacheKey中的成员变量的作用是什么呢,接下来看一下它的equals方法,CacheKey中重写了equals方法,CacheKey中的成员变量其实就是为了判断两个CacheKey的实例是否相同:
如果满足以下条件,两个CacheKey将判为不相同:
1. 要比较的对象不是CacheKey的实例
2. CacheKey对象中的hashcode不相同、count不相同、checksum不相同(它们之间是或的关系)
3. CacheKey对象的updateList成员变量不相同
总结:
如果Statement Id + Offset + Limmit + Sql + Params 都相同将被认为是相同的SQL,第一次将CacheKey作为HashMap中的key,数据库返回的数据作为value放入到集合中,第二次查询时由于被认为是相同的SQL,HashMap中已经存在该SQL的CacheKey对象,可直接从localCache中获取数据来实现mybatis的一级缓存。
@Override public boolean equals(Object object) { //如果对象为空 if (this == object) { return true; } //如果不是CacheKey的实例 if (!(object instanceof CacheKey)) { return false; } final CacheKey cacheKey = (CacheKey) object; //如果hashcode值不相同 if (hashcode != cacheKey.hashcode) { return false; } // 如果checksum不相同 if (checksum != cacheKey.checksum) { return false; } //如果count不相同 if (count != cacheKey.count) { return false; } //对比两个对象的updatelist中的值是否相同 for (int i = 0; i < updateList.size(); i++) { Object thisObject = updateList.get(i); Object thatObject = cacheKey.updateList.get(i); //如果有不相同的,返回false if (!ArrayUtil.equals(thisObject, thatObject)) { return false; } } return true; }
总结:
(1)mybatis的一级缓存是SqlSession级别的,不同的SqlSession不共享缓存;
(2)mybatis一级缓存是通过HashMap实现的,在PerpetualCache中定义,没有容量控制;
(3)分布式环境下使用一级缓存,数据库写操作会引起脏数据问题;