准备
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis01" />
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.mybatis.mapper"/>
</mappers>
</configuration>
mapper.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="com.mybatis.mapper.DeptMapper">
<select id="selectList" resultType="com.mybatis.bean.Dept">
select
id,
dept_name deptName
from
dept
</select>
</mapper>
SQL
CREATE TABLE `dept` (
`id` int(11) NOT NULL,
`dept_name` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into `dept` values(1,'aaa');
insert into `dept` values(2,'bbb');
insert into `dept` values(3,'ccc');
POJO
@Data
public class Dept{
private Integer id;
private String deptName;
}
测试
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class);
List<Dept> deptList = deptMapper.selectList();
System.out.println(deptList.size());
List<Dept> deptList02 = deptMapper.selectList();
System.out.println(deptList02.size());
日志输出结果:
DEBUG 02-09 00:35:41,994 ==> Preparing: select id, dept_name deptName from dept (BaseJdbcLogger.java:181)
DEBUG 02-09 00:35:42,021 ==> Parameters: (BaseJdbcLogger.java:181)
DEBUG 02-09 00:35:42,038 <== Total: 3 (BaseJdbcLogger.java:181)
3
3
从日志输出结果可以知道,SQL只执行了一次。明明调用了两次userMapper.selectList(),为什么呢?
一级缓存源码分析
我们知道,一个SqlSession代表一个Connection,所以对于一级缓存源码分析,其实就是从SqlSession创建到执行过程的分析。
// DefaultSqlSession#getMapper(Class<T>)
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory =
(MapperProxyFactory<T>) knownMappers.get(type);
return mapperProxyFactory.newInstance(sqlSession);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy =
new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
new Class[] { mapperInterface }, mapperProxy);
}
从上面的代码中,是Mybatis通过动态代理创建Mapper接口的代理类的代码逻辑。在MapperProxy实现了InvocationHandler接口,所以Mybatis的执行核心逻辑在于MapperProxy#invoke方法中。
// MapperProxy#invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// ...
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
// 此方法是对MapperMethod进行缓存,即在同一个Mapper代理对象中,重复执行同一个方法时,只会创建一个MapperMethod
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod =
new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
// 因为查询接口返回的是List,所以直接简化其他代码
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
//...
case SELECT:
// ...
result = executeForMany(sqlSession, args);
//...
}
return result;
}
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
// ...
result = sqlSession.<E>selectList(command.getName(), param);
return result;
}
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds,
Executor.NO_RESULT_HANDLER);
}
// BaseExecutor#query(MappedStatement, Object, RowBounds, ResultHandler)
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
// 此处是对于查询条件创建一个缓存的key,这就是一级缓存从map中取出数据的key值
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 如果在<select>标签中设置flushCache="true",则会进行清除一级缓存。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 尝试根据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) {
// 如果在全局配置文件中设置了<setting name="localCacheScope" value="STATEMENT"/>,则不会进行一级缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
// 从数据库中查询数据
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> 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;
}
从上面的代码分析,可以清楚地知道,一级缓存实现的大致流程。
一级缓存的CacheKey
Mybatis的一级缓存存放于PerpetualCache#cache中,其实一个HashMap。对于cache的get()操作,HashMap会对CacheKey进行hashCode和equals的判断。CacheKey重写这两个方法:
// CacheKey#hashCode
public int hashCode() {
// hashcode其值为常量值:private static final int DEFAULT_HASHCODE = 17;
return hashcode;
}
// CacheKey#equals
// equals方法会对key中的各种参数值(涉及sql)进行对比,如果匹配成功,则将其对应的value值返回
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
if (count != cacheKey.count) {
return false;
}
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (thisObject == null) {
if (thatObject != null) {
return false;
}
} else {
if (!thisObject.equals(thatObject)) {
return false;
}
}
}
return true;
}
关闭一级缓存
方法一
在<select>标签上设置flushCache=“true”,即<select id=“selectList” flushCache=“true”>,从设置会使得Mybatis对该SQL的一级缓存进行清除。
代码演示:
<select id="selectList" resultType="com.mybatis.bean.Dept" flushCache="true">
select
id,
dept_name deptName
from
dept
</select>
测试:
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
DeptMapper userMapper = sqlSession.getMapper(DeptMapper.class);
List<Dept> deptList = userMapper.selectList();
System.out.println(deptList.size());
List<Dept> deptList02 = userMapper.selectList();
System.out.println(deptList02.size());
日志输入:
DEBUG 02-09 02:06:20,985 ==> Preparing: select id, dept_name deptName from dept (BaseJdbcLogger.java:181)
DEBUG 02-09 02:06:21,015 ==> Parameters: (BaseJdbcLogger.java:181)
DEBUG 02-09 02:06:21,030 <== Total: 3 (BaseJdbcLogger.java:181)
3
DEBUG 02-09 02:06:21,031 ==> Preparing: select id, dept_name deptName from dept (BaseJdbcLogger.java:181)
DEBUG 02-09 02:06:21,032 ==> Parameters: (BaseJdbcLogger.java:181)
DEBUG 02-09 02:06:21,033 <== Total: 3 (BaseJdbcLogger.java:181)
3
从日志数据结果可以看出,SQL执行了两次,说明一级缓存失效了。
方法二
在全局配置文件的<settings>标签中,Mybatis提供了一个属性:localCacheScope,此属性用于控制本地缓存的生命周期,其属性值有两个SESSION和STATEMENT,其中SESSION是默认值。
(1)SESSION:代表和SqlSession的生命周期一致,在SqlSession的生命周期里,如果不进行SQL修改操作,其缓存会一直存在。
(2)STATEMENT:代表一次SQL的请求生命周期。即本次SQL查询结果会在查询得到接口后清除,不对其进行一级缓存。
下面进行代码演示:
对<select>标签还原为没有flushCache="true"的状态。
在全局配置文件中的<settings>标签进行配置:
<setting name="localCacheScope" value="STATEMENT"/>
测试:
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
DeptMapper userMapper = sqlSession.getMapper(DeptMapper.class);
List<Dept> deptList = userMapper.selectList();
System.out.println(deptList.size());
List<Dept> deptList02 = userMapper.selectList();
System.out.println(deptList02.size());
日志输入:
DEBUG 02-09 02:16:23,495 ==> Preparing: select id, dept_name deptName from dept (BaseJdbcLogger.java:181)
DEBUG 02-09 02:16:23,526 ==> Parameters: (BaseJdbcLogger.java:181)
DEBUG 02-09 02:16:23,545 <== Total: 3 (BaseJdbcLogger.java:181)
3
DEBUG 02-09 02:16:23,546 ==> Preparing: select id, dept_name deptName from dept (BaseJdbcLogger.java:181)
DEBUG 02-09 02:16:23,546 ==> Parameters: (BaseJdbcLogger.java:181)
DEBUG 02-09 02:16:23,547 <== Total: 3 (BaseJdbcLogger.java:181)
3
从日志数据结果可以看出,SQL执行了两次,说明一级缓存失效了。
方式三
在每次查询后,对SqlSession调用clearCache方法,可以进行清空缓存。
或者在一个事务中执行了增删改操作,也可以进行清空缓存。
Mapper配置文件和全局配置文件都置为初始状态。
测试代码:
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class);
List<Dept> deptList = deptMapper.selectList();
System.out.println(deptList.size());
// 清空缓存
sqlSession.clearCache();
List<Dept> deptList02 = deptMapper.selectList();
System.out.println(deptList02.size());
日志输入:
DEBUG 02-09 02:24:19,285 ==> Preparing: select id, dept_name deptName from dept (BaseJdbcLogger.java:181)
DEBUG 02-09 02:24:19,312 ==> Parameters: (BaseJdbcLogger.java:181)
DEBUG 02-09 02:24:19,328 <== Total: 3 (BaseJdbcLogger.java:181)
3
DEBUG 02-09 02:24:19,329 ==> Preparing: select id, dept_name deptName from dept (BaseJdbcLogger.java:181)
DEBUG 02-09 02:24:19,330 ==> Parameters: (BaseJdbcLogger.java:181)
DEBUG 02-09 02:24:19,331 <== Total: 3 (BaseJdbcLogger.java:181)
3
从日志数据结果可以看出,SQL执行了两次,说明一级缓存失效了。
总结
对于Mybatis的一级缓存,其实有点鸡肋,一般来说对于SQL查询来说,查询一次即可,不会在一个事务中重复查询多次。
Mybatis一级缓存(或二级本地缓存)会导致在分布式环境下出现脏数据。在分布式环境下,Mybatis的一级缓存无法集中统一管理,即在进程A中获取不到进程B更新的最新数据。
建议在开发中,在全局配置文件中将其禁用。