深度剖析MyBatis的一级缓存

概述

  • MyBatis 的一级缓存是什么时候开启的?

    • 在 MyBatis 中, 一级缓存是默认开启的

参考:MyBatis缓存的概念

通过场景来理解:

场景一

1、在一个 SqlSession 中,对 User 表进行两次根据 ID 的查询,查看发出 sql 语句的情况。

/**
 * 根据ID查询用户
 *
 * @param id
 * @return
 */
@Select("select * from user where id=#{id}")
User findUserById(Integer id);

@Before
public void before() throws Exception {
    System.out.println("before...");
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    //根据 sqlSessionFactory 产生 session
    sqlSession = sqlSessionFactory.openSession();
    // 这样也是可以的,这样的话后面就不用每次都设置了
    // sqlSession = sqlSessionFactory.openSession(true);
    userMapper = sqlSession.getMapper(IUserMapper.class);
}

@Test
public void testFindUserById() {
    //第一次查询,发出sql语句,并将查询出来的结果放进缓存中
    User user = userMapper.findUserById(1);
    System.out.println(user);

    //第二次查询,由于是同一个sqlSession,会在缓存中查询结果 //如果有,则直接从缓存中取出来,不和数据库进行交互
    User user2 = userMapper.findUserById(1);
    System.out.println(user2);
}

sql 执行过程如下:

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1677d1]
==>  Preparing: select * from user where id=?
==> Parameters: 1(Integer)
<==    Columns: id, username, password, birthday
<==        Row: 1, lucy, 123, 2019-12-12
<==      Total: 1
User{id=1, username='lucy', orderList=null, roleList=null}
User{id=1, username='lucy', orderList=null, roleList=null}

Process finished with exit code 0

场景二

2、同样对 user 表进行两次查询,不同的是两次查询之间进行了一次 update 操作

@Test
public void testFindUserById2() {
    // 第一次查询
    User user = userMapper.findUserById(1);
    System.out.println(user);

    // 更新操作
    user.setUsername("tyw");
    userMapper.update(user);

    // 第二次查询
    User user2 = userMapper.findUserById(1);
    System.out.println(user2);
}

可以看到,第一次查询后,进行了更新,然后进行第二次查询,这里两次查询都输出了 sql,说明缓存没有生效。

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@48fa0f47]
==>  Preparing: select * from user where id=?
==> Parameters: 1(Integer)
<==    Columns: id, username, password, birthday
<==        Row: 1, lucy, 123, 2019-12-12
<==      Total: 1
User{id=1, username='lucy', orderList=null, roleList=null}
==>  Preparing: update user set username=? where id=?
==> Parameters: tyw(String), 1(Integer)
<==    Updates: 1
==>  Preparing: select * from user where id=?
==> Parameters: 1(Integer)
<==    Columns: id, username, password, birthday
<==        Row: 1, tyw, 123, 2019-12-12
<==      Total: 1
User{id=1, username='tyw', orderList=null, roleList=null}

总结

1、第一次查询用户 ID 为 1 的用户信息,先去缓存查找有没有 ID 为 1 的用户信息,如果没有,从数据库查询用户信息。得到用户信息,将数据保存到一级缓存中。

2、如果 SqlSession 执行了 commit 操作(执行插入、更新、删除),则会情况 SqlSession 的一级缓存。这样做的目的是保存一级缓存中额数据是最新数据,防止脏读。

3、第二次发起查询 ID 为 1 的用户信息,先去缓存中查询 ID 为 1 的用户信息,如果有,直接返回。

一级缓存查找过程

一级缓存原理探究与源码分析

问题抛出

一级缓存是什么?一级缓存什么时候被创建?一级缓存的工作流程是什么?

SqlSession 中与缓存相关的属性和方法

跟踪一下 clearCache 的子类和父类

@startuml

!include https://unpkg.com/plantuml-style-c4@latest/core.puml
' uncomment the following line and comment the first to use locally
'!include core.puml

'!theme plain
top to bottom direction
skinparam linetype ortho

class BaseExecutor
class DefaultSqlSession
class PerpetualCache
interface SqlSession << interface >>

BaseExecutor       -[#595959,dashed]->  PerpetualCache    : "«create»"
BaseExecutor      "1" *-[#595959,plain]-> "localCache\n1" PerpetualCache  
DefaultSqlSession  -[#008200,dashed]-^  SqlSession    
@enduml

简单来看

可以看到,cache 的最底层其实就是一个 HashMap

public class PerpetualCache implements Cache {

  private final String id;

  private final Map<Object, Object> cache = new HashMap<>();

  ...
}

缓存其实就是本地存放的一个 Map 对象,每一个 SqlSession 都会存放一个 map 对象的引用。

cache 的创建时机

Executor 是执行器,用来执行 SQL 请求,而且清除缓存的方法也在 Executor 中执行,所以很可能缓存的创建也很 有可能在 Executor 中。

Executor 中有一个 createCacheKey 方法,这个方法很像是创建缓存的方法,跟进去看看,发现 createCacheKey 方法是由 BaseExecutor 执行的,代码如下

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
        if (parameterMapping.getMode() != ParameterMode.OUT) {
            Object value;
            String propertyName = parameterMapping.getProperty();
            if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
                value = null;
            } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
            }
            cacheKey.update(value);
        }
    }
    if (configuration.getEnvironment() != null) {
        // issue #176
        cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
}

创建缓存 key 会经过一系列的 update 方法,update 方法由一个 CacheKey 这个对象来执行的,这个 update 方法最终由 updateList 的 list 来把五个值存进去。

public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
}

这里需要注意一下最后一个值,configuration.getEnvironmen().getId() 这是什么,这其实就是定义在 sqlMapConfig.xml 中的标签,⻅如下。

<!-- environments:运行环境 -->
<environments default="development">
    <environment id="development">
        <!-- 当前事务交给JDBC管理 -->
        <transactionManager type="JDBC"/>
        <!-- 当前使用MyBatis提供的连接池 -->
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
    <environment id="production">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
</environments>

一级缓存的使用

一级缓存更多是用于查询操作,毕竟一级缓存也叫做查询缓存。

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

@SuppressWarnings("unchecked")
@Override
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 (BaseExecutor.DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        // issue #601
        deferredLoads.clear();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482
            clearLocalCache();
        }
    }
    return list;
}

queryFromDatabase 方法

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;
}

如果查不到的话,就从数据库查,在 queryFromDatabase 中,会对 localcache 进行写入。 localcache 对象的 putObject 方法调用 PerpetualCache 类 的 put 方法,最终交给 Map 进行存放。

@Override
public void putObject(Object key, Object value) {
    cache.put(key, value);
}

文章更新历史

2024/05/15 同步文章到其他平台

posted @ 2022-09-06 22:50  灯塔下的守望者  阅读(43)  评论(0编辑  收藏  举报