为什么要使用缓存
当我们需要使用DB数据库的数据时,需要使用statement对象去操作DB数据库,如果同时又多个请求一样的statement的时候就需要去查询多次,如果使用了缓存,就不会出现这种情况,这样又多个相同的statement对象请求来的时候只会执行一次DB数据库,因为会先去缓存中获取数据,确认是否又缓存的数据,有就返回数据,如果没有才去执行DB数据库。
mybatis二级缓存的使用测试
1.同一个命名空间同一个sqlsession对象执行相同语句 只会操作一次数据库,第二次会从数据库拿
MybatisMapper mapper = sqlSessionFactory.openSession().getMapper(MybatisMapper.class);
System.out.println(mapper.selectById(“2”, “测试数据操作”));
System.out.println(mapper.selectById(“2”, “测试数据操作”));
控制台数据记录:
2.同一个命名空间不同sqlsession对象执行相同语句
SqlSession sqlSession = sqlSessionFactory.openSession();
MybatisMapper mapper = sqlSession.getMapper(MybatisMapper.class);
System.out.println(mapper.selectById("2", "测试数据操作"));
sqlSession.commit();
SqlSession sqlSession1 = sqlSessionFactory.openSession();
MybatisMapper mapper1 = sqlSession1.getMapper(MybatisMapper.class);
System.out.println(mapper1.selectById("2", "测试数据操作"));
控制台数据记录:
mybatis一级缓存的使用测试
1.同一个sqlsession对象执行同一个语句
SqlSession sqlSession = sqlSessionFactory.openSession();
MybatisMapper mapper = sqlSession.getMapper(MybatisMapper.class);
System.out.println(mapper.selectById("2", "测试数据操作"));
System.out.println(mapper.selectById("2", "测试数据操作"));
控制台数据记录:
2.不同sqlsession对象执行同一个语句
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession = sqlSessionFactory.openSession();
MybatisMapper mapper1 = sqlSession1.getMapper(MybatisMapper.class);
MybatisMapper mapper = sqlSession.getMapper(MybatisMapper.class);
System.out.println(mapper.selectById("2", "测试数据操作"));
System.out.println(mapper1.selectById("2", "测试数据操作"));
控制台数据记录:
如果有事务的提交的话就会清楚缓存,这样同样的语句就会执行多次
SqlSession sqlSession = sqlSessionFactory.openSession();
MybatisMapper mapper = sqlSession.getMapper(MybatisMapper.class);
System.out.println(mapper.selectById("2", "测试数据操作"));
sqlSession.commit();
System.out.println(mapper.selectById("2", "测试数据操作"));
控制台数据记录:
mybatis缓存的实现机制
一般需要实现缓存的时候会在莫个作用域里面创建一个Map对象key-value的形式来保存数据,当然mybatis缓存的实现也不例外,分别是二级缓存使用Map<String, Cache> caches = new StrictMap<>(“Caches collection”)来存储二级缓存的数据,而一级缓存是使用PerpetualCache 类的Map<Object, Object> cache = new HashMap<>()属性来存储一级缓存的数据,从上面可以看出来不管是二级缓存还是一级缓存mybatis缓存都是使用的cache接口对象保存。
mybatis二级缓存的初始化
mybatis规定如果需要使用二级缓存,需要在mybatis的配置文件一般是xxxMapper.xml中配置,具体配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.ibatis.mybatistestcustom.MybatisMapper">
<!-- 这里是配置二级缓存方式1,具体参数设置可以参考官网的配置 -->
<cache />
<!-- 这里是配置二级缓存方式2,具体参数设置可以参考官网的配置 -->
<cache-ref/>
<sql id="common">
select name from user where
</sql>
<resultMap id="dsa" type="org.apache.ibatis.mybatistestcustom.TestDomain">
<result property="name" column="name"/>
</resultMap>
<select id="selectById" parameterType="map" resultMap="dsa">
<include refid="common"/> id = ${id} and name = #{name}
</select>
</mapper>
mybatis二级缓存的初始话需要先解析/Mapper
的文件,在根据该文件去解析cache
,cache-ref
获得配置的缓存数据,然后在把获取的缓存数据保存到指定Map对象中
如果是通过<cache-ref/>
获取的配置缓存数据如下:
1.根据命名空间去获取对应的缓存对象
2.把这个缓存对象设置成当前的缓存对象
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
如果是通过<cache/>
获取的配置缓存数据如下:
1.通过建筑类CacheBuilder(currentNamespace)
传入当前的命名空间的名字来新建一个cache
缓存对象
2.把获取的cache缓存对象保存到Configuration
类的Map<String, Cache> caches = new StrictMap<>("Caches collection")
属性当中,且把当前缓存对象设置成刚刚通过命名空间获取的cache
缓存对象。
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;
}
从上面两种获取二级缓存的方式中可以看出来,两种方式都是通过命名空间去获取的,所以二级缓存的作用域应该就是命名空间的作用域了。
把获取的二级缓存的数据保存到指定Map对象如下:
这个保存操作是在解析/Mapper的select|insert|update|delete的节点时候保存的。
1.先解析select|insert|update|delete 节点
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
2.通过XMLStatementBuilder建筑类的parseStatementNode()方法去解析该节点的所有属性设置,这里会有很多的属性的解析,我们这里不用去关系这些属性的解析,只需要留意下面代码最后一个方法builderAssistant.addMappedStatement()
这个方法即可
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
3.初始话二级缓存的操作就在builderAssistant.addMappedStatement()
这个方法中执行.cache(currentCache)设置的,具体代码如下,会把上面获取到的currentCache当前缓存对象设置到MapperStatementBuilder建筑类的二级缓存属性中,在后续需要使用二级缓存的时候就通过MapperStatement对象获取cache即可,到此二级缓存初始话完成。
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
mybatis一级缓存的初始化
mybatis一级缓存根据二级缓存不一样,二级缓存如果需要使用的话需要在xxxMapper.xml文件中设置或者来配置二级缓存,而一级缓存mybatis默认是开启的,不需要手动注册,当时初始化也是要初始话的。
一级缓存默认是开启的
一级缓存的作用域的默认设置
1.这个setting的属性设置有很多设置操作,这里忽略了其他的设置操作,只显示了设置一级缓存作用域的设置。
2.可以看到默认的一级缓存作用域是session,如果想要一级缓存失效可以把这个一级缓存的作用域设置成STATEMENT
3.官方对这个作用域的设置解析如下:
localCacheScope MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。可选值 SESSION | STATEMENT 默认值是 SESSION
private void settingsElement(Properties props) {
。。。。
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
。。。。
}
初始话一级缓存
1.首先需要获取opsession。
SqlSession sqlSession = sqlSessionFactory.openSession()
2.然后在使用DefaultSqlSessionFactory新建sqlSession对象的时候新建一个执行器newExecutor
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
。。。
final Executor executor = configuration.newExecutor(tx, execType);
。。。
}
3.新建newExecutor的内部逻辑中的simpleExcetor初始话的时候会对父类构造函数BaseExcetor()初始化,会把一级缓存设置为 PerpetualCache类,对于一级缓存的操作都在PerpetualCache 类的Map<Object, Object> cache Map属性中操作。
protected BaseExecutor(Configuration configuration, Transaction transaction) {
。。。
this.localCache = new PerpetualCache("LocalCache");
。。。
}
mybatis使用二级缓存的代码
如果在xxxMapper.xml文件中配置了二级缓存,就会在opsession的时候获取完simpleExcetor后还会再对这个执行器包装一个CacheExcetor类,然后在使用CacheExcetor操作数据库,在操作数据库的时候就会去先查询一次二级缓存是否有需要的数据,接着再去查询一级缓存是否有需要的数据,最后两个缓存都没有数据就去DB查询数据
- 根据配置是否需要CacheExcetor装饰
可以看到是根据cacheEnabled来判断是否需要装饰为CacheExcetor类,cacheEnabled这个属性默认就是true的,在Configuration类初始化的时候就初始话为true了。
官方对这个cacheEnabled的配置解析为:
cacheEnabled 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 可选值true | false 默认值true
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
。。。
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
。。。
}
- 使用二级缓存的代码,如果上面配置了二级缓存,那么下面这个executor.query就会去调用CacheExcetor的方法
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
。。。
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
。。。
}
- 在CacheExcetor执行器中的处理逻辑如下,可以看出首先会去MappedStatement对象中获取已经设置好的二级缓存对象,如果不为空,就执行二级缓存的逻辑,会去当前的二级缓存中获取需要的数据,如果获取不到就去执行DB数据库获取数据,最后会把数据保存到缓存当中。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
mybatis使用一级缓存的代码
如果没有在xxxMapper.xml文件中配置二级缓存,就会在opsession的时候获取完simpleExcetor后不会再对这个执行器包装一个CacheExcetor类,然后在使用simpleExcetor操作数据库,在操作数据库的时候就会去先查询一次一级缓存是否有需要的数据,如果一级缓存都没有数据就去DB查询数据
1.使用一级缓存的代码,如果上面没有配置二级缓存,那么下面这个executor.query就会去调用CacheExcetor的方法
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
。。。
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
。。。
}
- 在simpleExcetor执行器中的处理逻辑如下,可以看出首先会去localCache对象中获取已经设置好的一级缓存对象,如果不为空,就执行一级缓存的逻辑,会去当前的一级缓存中获取需要的数据,如果获取不到就去执行DB数据库获取数据,最后会把数据保存到缓存当中。
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.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
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();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}