深度解析Mybatis缓存
您目前处于:技术核心竞争力  2017-09-11

本文从源码分析Mybatis一级和二级缓存的应用,进而阐述Mybatis缓存的“坑”。

在介绍Mybatis一级缓存和二级缓存之前,需要首先理解两个概念:

  • SqlSession:引用官方文档中对这个接口作用的说明—SqlSession完全包含了面向数据库执行SQL命令所需的所有方法。你可以通过SqlSession实例来直接执行已映射的SQL语句,也可以通过SqlSession得到映射和管理事务。

  • namespace:这里提到的namespace指代的是在应用中配置的mapper配置文件中的namespace。

eg:

<mapper namespace=”xxx”></mapper>

下面开始正式介绍Mybatis的一级和二级缓存。

一级缓存:SqlSession维度的缓存,也就是每个SqlSession独享的缓存,我们在使用Mybatis的时候,通常会使用SqlSession的getMapper方法获取到映射。

eg:

UserDao userDao = sqlSession.getMapper(UserDao.class);

获取到映射之后执行相关方法进行数据库操作,获取到的映射对象是通过动态代理生成的代理类MapperProxy对象,Mybatis执行SQL的过程不是本文的重点,这里不过多赘述,有兴趣的读者可以自行查阅Mybatis源码。Mybatis的数据库操作是通过Executor来执行,一级缓存也是通过Executor来维护,每个SqlSession都会持有一个Executor:

图中LocalCache就是Mybatis的一级缓存,LocalCache的查询和写入是在Executor内部完成的,BaseExecutor是一个实现了Executor接口的抽象类,通过阅读源码发现,一级缓存LocalCache就是BaseExecutor内部的一个成员变量。

public abstract class BaseExecutor implements Executor {
protected PerpetualCache localCache;
public class PerpetualCache implements Cache {
	private Map<Object, Object> cache = HashMap<Object, Object>();

一级缓存保存在PerpetualCache维护的一个Map中,下面为一级缓存执行的流程图:

从流程图中看到,在执行Executor query动作时会尝试从一级缓存中获取缓存数据,下面为相关源码:

在BaseExecutor的query方法中发现了缓存key的构建过程,Mybatis用CacheKey这个对象作为缓存的key,上文中说到一级缓存最终会保存在PerpetualCache维护的一个Map中,我们知道,Map用hashcode和equals来确定对象的唯一性,在CacheKey的update方法中,我们发现了hashcode的运算:

update方法会计算hashcode和checksum并将参数对象保存到了updateList中,而在CacheKey的equals方法中,除去hashcode、checksum和count的比较外,只要updatelist中的元素一一对应相等,那么就可以认为是相等:

所以,只要知道创建Cachekey时updateList中都存放了哪些元素,就可以知道Mybatis是通过哪些因素来确定缓存唯一性,在上文标注的createCacheKey方法中,我们找到了这些元素:

继续分析上文中提到的query方法中调用的重载query方法:

在query方法中发现了清空一级缓存的两处代码,这两处的判断在Mybatis中都是可以配置的。可以在mapper配置文件中配置flushCache来控制数据库操作是否要清空缓存,select默认为false,insert、update、delete默认为true,注意,这个配置一级缓存和二级缓存都会生效,下文会介绍二级缓存。可以使用localCacheScope选项来控制一级缓存的范围,默认为SESSION,会缓存一个SqlSession中的所有查询,如果设置为STATEMENT,则查询会清除缓存。在查询数据库queryFromDatabase方法中,Mybatis做数据库查询操作,并将返回的结果存入一级缓存:

数据库的写操作会清除缓存,同时确认的是insert、update、delete都会统一走update方法:

二级缓存:namespace维度的缓存。上文提到SqlSession中会持有一个Executor,在构建SqlSession的时候,Mybatis会根据cacheEnable选项来确定是否使用一个缓存的Executor,也就是CachingExecutor。cacheEnable可以配置,默认为true:

cacheEnable开启的状态下在mapper配置文件中配置<cache/>和<cache-ref/>可以控制缓存策略,介绍一级缓存的时候我们提到了可以使用flushCache选项来控制是否清空缓存,这个配置同样会作用于二级缓存。mybatis在解析配置文件的时候,会将<cache/>配置的参数解析成Cache对象保存到MappedStatement(可以理解为mapper配置文件的java代码实现,对象中保存了在mapper配置文件中配置的选项)中,解析配置文件的过程不是本文的内容,感兴趣的读者可以自行查看Mybatis源码。构建好SqlSession后,数据库操作就会委托给CachingExcutor进行执行:

这是一个典型的按需加载缓存的方式,注意,tcm.putObject方法执行完之后缓存并没有真正的生效,这里只是记录了这次查询将要产生缓存变更,这时候相同的sql查询缓存是不会生效的。同样的,写操作也不是马上会清除缓存:

在执行SqlSession的commit方法之后,缓存的变更会真正的被刷新到缓存中去,开始真正的发挥作用:

我们看到在保存缓存和刷新缓存时用到的delegate对象就是上文中提到的构造TransactionalCache对象时传入的Cache对象了,在构建Cache对象时,Mybatis采用装饰者模式为Cache装饰了不同的功能,有兴趣的读者可以阅读相关源码来了解这部分的内容。缓存保存在PerpetualCache中:

到这里,整个Mybatis缓存的分析就结束了,最后我们分析一下Mybatis缓存到底有什么“坑”。在介绍一级缓存时我们提到Mybatis的一级缓存是SqlSession级别的缓存,不同的SqlSession之间缓存是不共享的,如果两个SqlSession操作同一张表,这时候就可能出现其中一个SqlSession获取到的数据是过期的,我们在使用这个SqlSession查询就有可能读取过期的脏数据。在介绍二级缓存时我们提到二级缓存是namespace维度的缓存,全局共享整个namespace的缓存,当我们把针对同一张表的sql操作写到两个不同的mapper文件中或者使用表关联查询时,就很容易出现两个mapper中查询出来的同一条数据不一致的情况。所以在实际应用中,我们建议将cacheEnable设置为false、localCacheScope设置为STATEMENT,不使用mybatis的缓存,需要缓存的时候用应用程序中的缓存来控制来避免Mybatis的缓存坑。


转载请并标注: “本文转载自 linkedkeeper.com (文/张强)”  ©著作权归作者所有