目录
- 初始化流程 加载解析配置文件
- createSqlSource()方法解析 占位符替换 动态sql标签解析
- sqlSessionFactory.openSession()方法解析 创建事务对象 装饰器模式创建执行器
- SqlSession.selectOne(selectList)执行流程解析 一二级缓存 参数设置 结果集封装
- Mapper代理查询过程解析 <package>标签解析 JDK动态代理原理 代理对象创建 invoke方法
- 详解插件机制 自定义插件 初始化流程 拦截原理
- 详解缓存机制 一二级缓存配置方式 源码解析
前言
在Mybatis中,存在缓存机制。分为一级缓存和二级缓存,一级缓存是SqlSession级别的,二级缓存则是namespace级别、即跨SqlSession级别的。一级缓存默认开启,二级缓存开启则需要进行配置。今天通过本文章分享一下Mybatis的缓存机制,希望可以帮到大家,谢谢~
一级缓存
效果演示
在Mybatis中,一级缓存默认开启。下面可以进行一个简单测试。
java
public interface UserMapper {
User findUserById(int id);
void updateUser(User user);
}
xml
<mapper namespace="com.sxb.mapper.UserMapper">
<select id="findUserById" parameterType="int" resultType="com.sxb.pojo.User">
select id,username from user where id = #{id}
</select>
<update id="updateUser" parameterType="com.sxb.pojo.User">
update user set username = #{username} where id = #{id}
</update>
</mapper>
下面这个测试方法中,同一个SqlSession下,相同的查询条件,查出来的User对象将会是同一个对象,user1==user2 会是true。
也就是说,第二次相同条件的查询,没有去查询数据库,而是命中了缓存。
如果两次查询中间,进行一次更新操作,则一级缓存会被清除。
java
public void firstLevelCacheTest() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user1 = userMapper.findUserById(1);
User user2 = userMapper.findUserById(1);
User user = new User();
user.setId(1);
user.setUsername("zhangsan");
userMapper.updateUser(user);
User user3 = userMapper.findUserById(1);
System.out.println(user1 == user2);//true
System.out.println(user2 == user3);//false
}
源码解析
Mybaits中的一级缓存,类型为PerpetualCache
,这个类很简单,内部封装了一个Map集合cache
,key为CacheKey
对象,值为查询出来的结果。
java
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
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 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();
}
}
一级缓存的应用位置,在执行器(Executor)
这个组件中,整个查询的流程,包括涉及到一级缓存的使用,以前的文章中介绍的比较清楚了,可以参考:Mybatis源码 - SqlSession.selectOne(selectList)执行流程解析 一二级缓存 参数设置 结果集封装。
那么,更新操作时,一级缓存的清除是在什么位置呢?当然也是在SqlSession中,在Mybatis中,只要涉及到更新的方法,无论是增、删、改,其实最后执行到SqlSession中,都是走的update方法,可以看一下方法源码:
java
public class DefaultSqlSession implements SqlSession {
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
}
第6行:委派给了执行器进行更新操作。看一下执行器中的源码
java
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
可以看到,调用了clearLocalCache
方法,方法中,进行了一级缓存的清除。
java
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
注意:在xml编写sql语句时,更新操作最好不要因为懒使用<select>标签,因为这样的话,会被框架识别为查询操作,执行完毕之后,
不会去清除一级缓存
,可能会造成,查不到最新数据的情况。
二级缓存
开启配置
想要开启二级缓存,需要三步
-
核心配置文件中,配置
<settings>
标签,将cacheEnabled
的值设置为true
。(默认为true)xml<configuration> <!--开启二级缓存(本身默认开启)--> <settings> <setting name="cacheEnabled" value="true"/> </settings> <!--省略其它配置项...--> </configuration>
-
在想要开启二级缓存的mapper.xml中配置
<cache></cache>
标签。xml<mapper namespace="com.sxb.mapper.UserMapper"> <!--开启二级缓存--> <cache></cache> <!--省略sql语句...--> </mapper>
-
mapper.xml中sql语句标签上,配置
useCache
的值为true
。(默认为true)xml<mapper namespace="com.sxb.mapper.UserMapper"> <!--开启二级缓存--> <cache></cache> <select id="findUserById" parameterType="int" resultType="com.xxx.pojo.User" useCache="true"> select id,username from user where id = #{id} </select> </mapper>
完成以上步骤之后,就针对某个Mapper,开启了二级缓存。多个SqlSession会共享缓存。
效果演示
java
public void secondLevelCacheTest() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
//这次查询会去查询数据库
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = userMapper1.findUserById(1);
//只有调用了sqlSession的commit或close方法,才会令对二级缓存的操作生效
sqlSession1.commit();
//这次查询不会去查询数据库,而是会命中缓存
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.findUserById(1);
System.out.println(user1 == user2);//尽管命中了缓存,还是false
}
以上测试方法中,有几个需要注意的点:
- 针对二级缓存的操作,不管是查询完数据库,将结果设置到二级缓存;还是执行了update操作,清空二级缓存。都需要执行sqlSession的commit或close方法,才会生效。
- 最后对于两个对象的比较是false,是因为二级缓存的实现方式,是把数据持久化到磁盘,读取时再从磁盘读取,所以这里是false。
这两点,我们将在下面的源码解析中进行讲解。
源码解析
<settings>标签解析
对于核心配置文件中,<configuration>
标签中的<settings>
标签的解析工作,是由XmlConfigBuilder
的parse
方法处理的。在这个方法中,调用了本类中的parseConfiguration
方法,解析了每个子标签,其中就包括<settings>
标签。
java
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
settingsElement方法中,进行了非常多配置项的赋值。
java
private void settingsElement(Properties props) {
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
//这里将cacheEnabled的值,设置到了configuration中,默认值为true
configuration.setCacheEnabled(
booleanValueOf(props.getProperty("cacheEnabled"), true));
//省略其它项的设置...
}
<cache>标签解析
<cache>
标签是映射配置文件中的标签,是<mapper>
的子标签,所以它的解析是交由XMLMapperBuilder
中的parse
方法来处理的。这个方法中,会调用本类方法configurationElement
来处理<mapper>
标签下的所有子标签,其中就包括了<cache>
标签。
java
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);
}
}
第9行:调用了cacheElement
方法,解析了<cache>
标签。
java
private void cacheElement(XNode context) {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
- 第3-11行:获取封装了
<cache>
标签中的所有属性值,第3行中,缓存的类型为PERPETUAL
,底层数据结构为PerpetualCache
,与一级缓存一致。 - 第12行:使用上一步封装好的各个属性,调用
MapperBuilderAssistant
对象的useNewCache
方法,创建了Cache
对象。
java
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;
}
- 创建Cache对象之后,这里会将Cache对象封装到
configuration
对象中,同时也会将cache的值赋值给本类中的成员变量currentCache
。 - 后续在解析
select|insert|update|delete
四类标签、构建MappedStatement
对象时,会调用MapperBuilderAssistant
对象的addMappedStatement
方法,这里会把currentCache
属性值赋值到 MappedStatement 对象的cache
属性中,所以说,一个Mapper下的多个sql,共享同一个Cache对象。
缓存 设置&获取
Mybatis中,对于数据库的操作,最后会委派到执行器来执行,而因为Mybatis默认开启一级缓存,所以在创建执行器时会进行一层包装,创建出CachingExecutor
对象。
SqlSession中的查询方法,无论是selectList还是selectOne,都会执行到执行器对象的query方法,下面就直接从执行器的query方法看起,看一下二级缓存是怎么使用的:
java
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
- 第4行:调用了
createCacheKey
方法创建了CacheKey
对象,这个是缓存中的key,一二级缓存中都会使用。 - 第5行:调用了重载的
query
方法,执行查询操作。看一下源码:
java
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//获取mapper.xml中解析<cache>标签后封装的对象
Cache cache = ms.getCache();
//如果不为空,说明开启了二级缓存。进到查询二级缓存的逻辑中
if (cache != null) {
//判断如果sql标签上,标注了`flushCache=true`,就清除二级缓存。
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);
}
- 第3行:从
MappedStatement
对象中获取了Cache
对象,这里就是在解析mapper.xml映射配置文件时,解析出来的<cache>
标签。 - 第4行:对
Cache
对象进行判空,如果不为null,说明mapper.xml中配置了二级缓存,这里就走到查询二级缓存的逻辑中。 - 第7行:判断如果sql标签上,标注了
flushCache=true
,就清除二级缓存。 - 第12行:调用了
tcm
对象的getObject
方法,传入了Cache
对象和CacheKey
对象,查询了二级缓存。 - 第17行:调用了
tcm
对象的putObject
方法,将查询出来的结果,添加到了二级缓存(此处只是存到了一个Map集合,并未真的放到二级缓存。只有SqlSession在调用commit或close方法时,才会生效)。
tcm对象就是TransactionalCacheManager
,事务缓存管理器。里面封装了对二级缓存的操作方法,这里看一下这个类的结构。
TransactionalCacheManager
java
public class TransactionalCacheManager {
//封装了每一个mapper对应的TransactionalCache对象
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
//查询二级缓存,这里是从真正的二级缓存中查询
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
//设置二级缓存:只会将值设置到entriesToAddOnCommit中,不会真正的设置到二级缓存中
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
/**
* 1、将entriesMissedInCache的元素从delegate中删除
* 2、清空两个Map集合,设置clearOnCommit为false
*/
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
}
-
首先,这个类中,维护了一个Map集合:
transactionalCaches
,key 为解析 xml文件 中的 <cache> 标签后封装的 cache 对象,value 是一个TransactionalCache
对象。这里就可以解释为什么PerpetualCache
中重写了equals和hashcode方法,因为这里被用作了HashMap中的key。 -
可以看到,类中其他的方法,其实都是获取到Map集合中的
TransactionalCache
对象,然后再调用它的方法,我们看一下这个类的结构:
TransactionalCache
java
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
private final Cache delegate;
private boolean clearOnCommit;
private final Map<Object, Object> entriesToAddOnCommit;
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifying a rollback to the cache adapter. "
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
}
-
成员变量解释
- Cache delegate:真正的二级缓存存放对象。数据结构默认为
PerpetualCache
- boolean clearOnCommit:是否提交事务的时候清除二级缓存
- Map<Object, Object> entriesToAddOnCommit:
未提交事务
时的缓存数据,提交事务才会生效,回滚事务时会清空。 - Set<Object> entriesMissedInCache:未命中的缓存key的集合,commit时也存入了二级缓存。可以理解为是对null值也做了缓存。
- Cache delegate:真正的二级缓存存放对象。数据结构默认为
-
commit方法:
- 如果之前调用过clear方法,会设置一个标识
clearOnCommit
为true,本方法中会判断这个标识,为true就会清空二级缓存 - 遍历
entriesToAddOnCommit
集合,将里面的值赋值到delegate,真正的二级缓存。 - 遍历
entriesMissedInCache
集合,将未命中的缓存key存储到到delegate,value为null。 - 清空两个Map集合,设置clearOnCommit为false
- 如果之前调用过clear方法,会设置一个标识
-
rollback方法:
- 将
entriesMissedInCache
的元素从delegate中删除 - 清空两个Map集合,设置clearOnCommit为false
- 将
这也就解释了,为什么只有调用了commit方法,二级缓存才会生效。原因就在于,在设置二级缓存时,首先是设置到了entriesToAddOnCommit
Map集合中,只有提交事务
,这些数据才会生效,如果回滚事务
,这些数据会被清空。
到这里,整个解析流程就结束了。感谢你的阅读,如果有不对的地方,欢迎在评论区指正!谢谢~