Mybatis之一级缓存源码分析

准备

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更新的最新数据。

建议在开发中,在全局配置文件中将其禁用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值