乐趣区

关于缓存:Mybatis源码缓存机制

前言

在 Mybatis 源码 -Executor 的执行过程中对 Mybatis 的一次理论执行进行了阐明,在整个执行过程中,没有对缓存相干逻辑进行剖析,这本篇文章中,将联合示例与源码,对 Mybatis 中的 一级缓存 二级缓存 进行阐明。

注释

一. 一级缓存机制展现

Mybatis 中如果屡次执行完全相同的 SQL 语句时,Mybatis提供了 一级缓存 机制用于进步查问效率。一级缓存是默认开启的,如果想要手动配置,须要在 Mybatis 配置文件中退出如下配置。

<settings>
    <setting name="localCacheScope" value="SESSION"/>
</settings>

其中 localCacheScope 能够配置为 SESSION(默认) 或者STATEMENT,含意如下所示。

属性值 含意
SESSION 一级缓存在一个会话中失效。即在一个会话中的所有查问语句,均会共享同一份一级缓存,不同会话中的一级缓存不共享。
STATEMENT 一级缓存仅针对以后执行的 SQL 语句失效。以后执行的 SQL 语句执行结束后,对应的一级缓存会被清空。

上面以一个例子对 Mybatis 的一级缓存机制进行演示和阐明。首先开启日志打印,而后敞开二级缓存,并将一级缓存作用范畴设置为SESSION,配置如下。

<settings>
    <setting name="logImpl" value="STDOUT_LOGGING" />
    <setting name="cacheEnabled" value="false"/>
    <setting name="localCacheScope" value="SESSION"/>
</settings>

映射接口如下所示。

public interface BookMapper {Book selectBookById(int id);

}

映射文件如下所示。

<?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.learn.dao.BookMapper">
    <resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
        <result column="b_name" property="bookName"/>
        <result column="b_price" property="bookPrice"/>
    </resultMap>

    <select id="selectBookById" resultMap="bookResultMap">
        SELECT
        b.id, b.b_name, b.b_price
        FROM
        book b
        WHERE
        b.id=#{id}
    </select>
</mapper>

Mybatis的执行代码如下所示。

public class MybatisTest {public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        SqlSession sqlSession = sqlSessionFactory.openSession(false);
        BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);

        System.out.println(bookMapper.selectBookById(1));
        System.out.println(bookMapper.selectBookById(1));
        System.out.println(bookMapper.selectBookById(1));
    }

}

在执行代码中,间断执行了三次查问操作,看一下日志打印,如下所示。

能够晓得,只有第一次查问时和数据库进行了交互,前面两次查问均是从一级缓存中查问的数据。当初往映射接口和映射文件中退出更改数据的逻辑,如下所示。

public interface BookMapper {Book selectBookById(int id);
    // 依据 id 更改图书价格
    void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice);

}
<?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.learn.dao.BookMapper">
    <resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
        <result column="b_name" property="bookName"/>
        <result column="b_price" property="bookPrice"/>
    </resultMap>

    <select id="selectBookById" resultMap="bookResultMap">
        SELECT
        b.id, b.b_name, b.b_price
        FROM
        book b
        WHERE
        b.id=#{id}
    </select>

    <insert id="updateBookPriceById">
        UPDATE
        book
        SET
        b_price=#{bookPrice}
        WHERE
        id=#{id}
    </insert>
</mapper>

执行的操作为先执行一次查问操作,而后执行一次更新操作并提交事务,最初再执行一次查问操作,执行代码如下所示。

public class MybatisTest {public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        SqlSession sqlSession = sqlSessionFactory.openSession(false);
        BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);

        System.out.println(bookMapper.selectBookById(1));

        System.out.println("Change database.");
        bookMapper.updateBookPriceById(1, 22.5f);
        sqlSession.commit();

        System.out.println(bookMapper.selectBookById(1));
    }

}

执行后果如下所示。

通过上述后果能够晓得,在执行更新操作之后,再执行查问操作时,是间接从数据库查问的数据,并未应用一级缓存,即在一个会话中,对数据库的 操作,均会使一级缓存生效。

当初在执行代码中创立两个会话,先让会话 1 执行一次查问操作,而后让会话 2 执行一次更新操作并提交事务,最初让会话 1 再执行一次雷同的查问。执行代码如下所示。

public class MybatisTest {public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        SqlSession sqlSession1 = sqlSessionFactory.openSession(false);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(false);
        BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
        BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);

        System.out.println(bookMapper1.selectBookById(1));

        System.out.println("Change database.");
        bookMapper2.updateBookPriceById(1, 22.5f);
        sqlSession2.commit();

        System.out.println(bookMapper1.selectBookById(1));
    }

}

执行后果如下所示。

上述结果表明,会话 1 的第一次查问是间接查问的数据库,而后会话 2 执行了一次更新操作并提交了事务,此时数据库中 id 为 1 的图书的价格曾经变更为了 22.5,紧接着会话 1 又做了一次查问,但查问后果中的图书价格为 20.5,说明会话 1 的第二次查问是从缓存获取的查问后果。所以在这里能够晓得,Mybatis中每个会话均会保护一份一级缓存,不同会话之间的一级缓存各不影响。

在本大节最初,对 Mybatis 的一级缓存机制做一个总结,如下所示。

  • Mybatis的一级缓存默认开启,且默认作用范畴为 SESSION,即一级缓存在一个会话中失效,也能够通过配置将作用范畴设置为STATEMENT,让一级缓存仅针对以后执行的SQL 语句失效;
  • 在同一个会话中,执行 操作会使本会话中的一级缓存生效;
  • 不同会话持有不同的一级缓存,本会话内的操作不会影响其它会话内的一级缓存。

二. 一级缓存源码剖析

本大节将对一级缓存对应的 Mybatis 源码进行探讨。在 Mybatis 源码 -Executor 的执行过程中曾经晓得,禁用二级缓存的状况下,执行查问操作时,调用链如下所示。

BaseExecutor 中有两个重载的 query() 办法,上面先看第一个 query() 办法的实现,如下所示。

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                       ResultHandler resultHandler) throws SQLException {
    // 获取 Sql 语句
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 生成 CacheKey
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 调用重载的 query()办法
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

在上述 query() 办法中,先会在 MappedStatement 中获取 SQL 语句,而后生成 CacheKey,这个CacheKey 理论就是本会话一级缓存中缓存的惟一标识,CacheKey类图如下所示。

CacheKey中的 multiplierhashcodechecksumcountupdateList字段用于判断 CacheKey 之间是否相等,这些字段会在 CacheKey 的构造函数中进行初始化,如下所示。

public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLIER;
    this.count = 0;
    this.updateList = new ArrayList<>();}

同时 hashcodechecksumcountupdateList字段会在 CacheKeyupdate()办法中被更新,如下所示。

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

次要逻辑就是基于 update() 办法的入参计算并更新 hashcodechecksumcount的值,而后再将入参增加到 updateList 汇合中。同时,在 CacheKey 重写的 equals() 办法中,只有当 hashcode 相等,checksum相等,count相等,以及 updateList 汇合中的元素也全都相等时,才算做两个 CacheKey 是相等。

回到上述的 BaseExecutor 中的 query() 办法,在其中会调用 createCacheKey() 办法生成CacheKey,其局部源码如下所示。

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, 
                               RowBounds rowBounds, BoundSql boundSql) {
    ......
    
    // 创立 CacheKey
    CacheKey cacheKey = new CacheKey();
    
    // 基于 MappedStatement 的 id 更新 CacheKey
    cacheKey.update(ms.getId());
    // 基于 RowBounds 的 offset 更新 CacheKey
    cacheKey.update(rowBounds.getOffset());
    // 基于 RowBounds 的 limit 更新 CacheKey
    cacheKey.update(rowBounds.getLimit());
    // 基于 Sql 语句更新 CacheKey
    cacheKey.update(boundSql.getSql());
    
    ......
    
    // 基于查问参数更新 CacheKey
    cacheKey.update(value);
    
    ......
    
    // 基于 Environment 的 id 更新 CacheKey
    cacheKey.update(configuration.getEnvironment().getId());
    
    return cacheKey; 
}

所以能够得出结论,判断 CacheKey 是否相等的根据就是 MappedStatement id + RowBounds offset + RowBounds limit + SQL + Parameter + Environment id 相等。

获取到 CacheKey 后,会调用 BaseExecutor 中重载的 query() 办法,如下所示。

@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.");
    }
    //queryStack 是 BaseExecutor 的成员变量
    //queryStack 次要用于递归调用 query()办法时避免一级缓存被清空
    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) {for (BaseExecutor.DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();
        }
        deferredLoads.clear();
        // 如果一级缓存作用范畴是 STATEMENT 时,每次 query()执行结束就须要清空一级缓存
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {clearLocalCache();
        }
    }
    return list;
}

上述 query() 办法中,会先依据 CacheKey 去缓存中命中查问后果,如果命中到查问后果并且映射文件中 CURD 标签上的 statementTypeCALLABLE,则会先在 handleLocallyCachedOutputParameters() 办法中解决存储过程相干逻辑而后再将命中的查问后果返回,如果未命中到查问后果,则会间接查询数据库。上述 query() 办法中还应用到了 BaseExecutorqueryStack字段,次要避免一级缓存作用范畴是 STATEMENT 并且还存在递归调用 query() 办法时,在递归尚未终止时就将一级缓存删除,如果不存在递归调用,那么一级缓存作用范畴是 STATEMENT 时,每次查问完结后,都会清空缓存。上面看一下 BaseExecutor 中的一级缓存localCache,其理论是PerpetualCache,类图如下所示。

所以 PerpetualCache 的外部次要是基于一个 Map(理论为HashMap)用于数据存储。当初回到下面的BaseExecutorquery()办法中,如果没有在一级缓存中命中查问后果,则会间接查询数据库,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 {// 调用 doQuery()进行查问操作
        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()办法中和一级缓存相干的逻辑就是在查问完数据库后,会将查问后果以 CacheKey 作为惟一标识缓存到一级缓存中。

Mybatis中如果是执行 操作,并且在禁用二级缓存的状况下,均会调用到 BaseExecutorupdate()办法,如下所示。

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

所以 Mybatis 中的一级缓存在执行了 操作后,会被清空即生效。

最初,一级缓存的应用流程能够用下图进行概括。

三. 二级缓存机制展现

Mybatis的一级缓存仅在一个会话中被共享,会话之间的一级缓存互不影响,而 Mybatis 的二级缓存能够被多个会话共享,本大节将联合例子,对 Mybatis 中的二级缓存的应用机制进行剖析。要应用二级缓存,须要对 Mybatis 配置文件进行更改以开启二级缓存,如下所示。

<settings>
    <setting name="logImpl" value="STDOUT_LOGGING" />
    <setting name="cacheEnabled" value="true"/>
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

上述配置文件中还将一级缓存的作用范畴设置为了STATEMENT,目标是为了在例子中屏蔽一级缓存对查问后果的烦扰。映射接口如下所示。

public interface BookMapper {Book selectBookById(int id);
    void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice);

}

要应用二级缓存,还须要在映射文件中退出二级缓存相干的设置,如下所示。

<?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.learn.dao.BookMapper">
    <!-- 二级缓存相干设置 -->
    <cache eviction="LRU"
           type="org.apache.ibatis.cache.impl.PerpetualCache"
           flushInterval="600000"
           size="1024"
           readOnly="true"
           blocking="false"/>

    <resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
        <result column="b_name" property="bookName"/>
        <result column="b_price" property="bookPrice"/>
    </resultMap>

    <select id="selectBookById" resultMap="bookResultMap">
        SELECT
        b.id, b.b_name, b.b_price
        FROM
        book b
        WHERE
        b.id=#{id}
    </select>

    <insert id="updateBookPriceById">
        UPDATE
        book
        SET
        b_price=#{bookPrice}
        WHERE
        id=#{id}
    </insert>
</mapper>

二级缓存相干设置的每一项的含意,会在本大节开端进行阐明。

场景一 :创立两个会话,会话 1 以雷同SQL 语句间断执行两次查问,会话 2 以雷同 SQL 语句执行一次查问。执行代码如下所示。

public class MybatisTest {public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        SqlSession sqlSession1 = sqlSessionFactory.openSession(false);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(false);
        BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
        BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);

        System.out.println(bookMapper1.selectBookById(1));
        System.out.println(bookMapper1.selectBookById(1));

        System.out.println(bookMapper2.selectBookById(1));
    }

}

执行后果如下所示。

Mybatis中的二级缓存开启时,每次查问会先去二级缓存中命中查问后果,未命中时才会应用一级缓存以及间接去查询数据库。上述后果截图表明,场景一中,SQL语句雷同时,无论是同一会话的间断两次查问还是另一会话的一次查问,均是查问的数据库,好像二级缓存没有失效,实际上,将查问后果缓存到二级缓存中须要事务提交,场景一中并没有事务提交,所以二级缓存中是没有内容的,最终导致三次查问均是间接查问的数据库。此外,如果是增删改操作,只有没有事务提交,那么就不会影响二级缓存。

场景二 :创立两个会话,会话 1 执行一次查问并提交事务,而后会话 1 以雷同SQL 语句再执行一次查问,接着会话 2 以雷同 SQL 语句执行一次查问。执行代码如下所示。

public class MybatisTest {public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        SqlSession sqlSession1 = sqlSessionFactory.openSession(false);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(false);
        BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
        BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);

        System.out.println(bookMapper1.selectBookById(1));
        sqlSession1.commit();
        System.out.println(bookMapper1.selectBookById(1));

        System.out.println(bookMapper2.selectBookById(1));
    }

}

执行后果如下所示。

场景二中第一次查问后提交了事务,此时将查问后果缓存到了二级缓存,所以后续的查问全副在二级缓存中命中了查问后果。

场景三:创立两个会话,会话 1 执行一次查问并提交事务,而后会话 2 执行一次更新并提交事务,接着会话 1 再执行一次雷同的查问。执行代码如下所示。

public class MybatisTest {public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        // 将事务隔离级别设置为读已提交
        SqlSession sqlSession1 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
        BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
        BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);

        System.out.println(bookMapper1.selectBookById(1));
        sqlSession1.commit();

        System.out.println("Change database.");
        bookMapper2.updateBookPriceById(1, 20.5f);
        sqlSession2.commit();

        System.out.println(bookMapper1.selectBookById(1));
    }

}

执行后果如下所示。

场景三的执行结果表明,执行更新操作并且提交事务后,会清空二级缓存,执行新增和删除操作也是同理。

场景四:创立两个会话,创立两张表,会话 1 首先执行一次多表查问并提交事务,而后会话 2 执行一次更新操作以更新表 2 的数据并提交事务,接着会话 1 再执行一次雷同的多表查问。创表语句如下所示。

CREATE TABLE book(id INT(11) PRIMARY KEY AUTO_INCREMENT,
    b_name VARCHAR(255) NOT NULL,
    b_price FLOAT NOT NULL,
    bs_id INT(11) NOT NULL,
    FOREIGN KEY book(bs_id) REFERENCES bookstore(id)
);

CREATE TABLE bookstore(id INT(11) PRIMARY KEY AUTO_INCREMENT,
    bs_name VARCHAR(255) NOT NULL
)

book 表和 bookstore 表中增加如下数据。

INSERT INTO book (b_name, b_price, bs_id) VALUES ("Math", 20.5, 1);
INSERT INTO book (b_name, b_price, bs_id) VALUES ("English", 21.5, 1);
INSERT INTO book (b_name, b_price, bs_id) VALUES ("Water Margin", 30.5, 2);

INSERT INTO bookstore (bs_name) VALUES ("XinHua");
INSERT INTO bookstore (bs_name) VALUES ("SanYou")

创立 BookStore 类,如下所示。

@Data
public class BookStore {

    private String id;
    private String bookStoreName;

}

创立 BookDetail 类,如下所示。

@Data
public class BookDetail {

    private long id;
    private String bookName;
    private float bookPrice;

    private BookStore bookStore;

}

BookMapper映射接口增加 selectBookDetailById() 办法,如下所示。

public interface BookMapper {Book selectBookById(int id);
    void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice);
    BookDetail selectBookDetailById(int id);

}

BookMapper.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.learn.dao.BookMapper">
    <cache eviction="LRU"
           type="org.apache.ibatis.cache.impl.PerpetualCache"
           flushInterval="600000"
           size="1024"
           readOnly="true"
           blocking="false"/>

    <resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
        <result column="b_name" property="bookName"/>
        <result column="b_price" property="bookPrice"/>
    </resultMap>
    
    <resultMap id="bookDetailResultMap" type="com.mybatis.learn.entity.BookDetail">
        <id column="id" property="id"/>
        <result column="b_name" property="bookName"/>
        <result column="b_price" property="bookPrice"/>
        <association property="bookStore">
            <id column="id" property="id"/>
            <result column="bs_name" property="bookStoreName"/>
        </association>
    </resultMap>

    <select id="selectBookById" resultMap="bookResultMap">
        SELECT
        b.id, b.b_name, b.b_price
        FROM
        book b
        WHERE
        b.id=#{id}
    </select>

    <insert id="updateBookPriceById">
        UPDATE
        book
        SET
        b_price=#{bookPrice}
        WHERE
        id=#{id}
    </insert>
    
    <select id="selectBookDetailById" resultMap="bookDetailResultMap">
        SELECT
        b.id, b.b_name, b.b_price, bs.id, bs.bs_name
        FROM
        book b, bookstore bs
        WHERE
        b.id=#{id}
        AND
        b.bs_id = bs.id
    </select>
</mapper>

还须要增加 BookStoreMapper 映射接口,如下所示。

public interface BookStoreMapper {void updateBookPriceById(@Param("id") int id, @Param("bookStoreName") String bookStoreName);

}

还须要增加 BookStoreMapper.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.learn.dao.BookStoreMapper">
    <cache eviction="LRU"
           type="org.apache.ibatis.cache.impl.PerpetualCache"
           flushInterval="600000"
           size="1024"
           readOnly="true"
           blocking="false"/>

    <insert id="updateBookPriceById">
        UPDATE
        bookstore
        SET
        bs_name=#{bookStoreName}
        WHERE
        id=#{id}
    </insert>

</mapper>

进行完上述更改之后,进行场景四的测试,执行代码如下所示。

public class MybatisTest {public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        // 将事务隔离级别设置为读已提交
        SqlSession sqlSession1 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
        BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
        BookStoreMapper bookStoreMapper = sqlSession2.getMapper(BookStoreMapper.class);

        System.out.println(bookMapper1.selectBookDetailById(1));
        sqlSession1.commit();

        System.out.println("Change database.");
        bookStoreMapper.updateBookStoreById(1, "ShuXiang");
        sqlSession2.commit();

        System.out.println(bookMapper1.selectBookDetailById(1));
    }

}

执行后果如下所示。

会话 1 第一次执行多表查问并提交事务时,将查问后果缓存到了二级缓存中,而后会话 2 对 bookstore 表执行了更新操作并提交了事务,然而最初会话 1 第二次执行雷同的多表查问时,却从二级缓存中命中了查问后果,最终导致查问进去了脏数据。实际上,二级缓存的作用范畴是同一命名空间下的多个会话共享,这里的命名空间就是映射文件的 namespace,能够了解为每一个映射文件持有一份二级缓存,所有会话在这个映射文件中的所有操作,都会共享这个二级缓存。所以场景四的例子中,会话 2 对bookstore 表执行更新操作并提交事务时,清空的是 BookStoreMapper.xml 持有的二级缓存,BookMapper.xml持有的二级缓存没有感知到 bookstore 表的数据产生了变动,最终导致会话 1 第二次执行雷同的多表查问时从二级缓存中命中了脏数据。

场景五 :执行的操作和 场景四 统一,然而在 BookStoreMapper.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.learn.dao.BookStoreMapper">
    <cache-ref namespace="com.mybatis.learn.dao.BookMapper"/>

    <insert id="updateBookStoreById">
        UPDATE
        bookstore
        SET
        bs_name=#{bookStoreName}
        WHERE
        id=#{id}
    </insert>

</mapper>

执行代码如下所示。

public class MybatisTest {public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        // 将事务隔离级别设置为读已提交
        SqlSession sqlSession1 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
        BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
        BookStoreMapper bookStoreMapper = sqlSession2.getMapper(BookStoreMapper.class);

        System.out.println(bookMapper1.selectBookDetailById(1));
        sqlSession1.commit();

        System.out.println("Change database.");
        bookStoreMapper.updateBookStoreById(1, "ShuXiang");
        sqlSession2.commit();

        System.out.println(bookMapper1.selectBookDetailById(1));
    }

}

执行后果如下所示。

BookStoreMapper.xml 中应用 <cache-ref> 标签援用了命名空间为 com.mybatis.learn.dao.BookMapper 的映射文件应用的二级缓存,因而相当于 BookMapper.xml 映射文件与 BookStoreMapper.xml 映射文件持有同一份二级缓存,会话 2 在 BookStoreMapper.xml 映射文件中执行更新操作并提交事务后,会导致二级缓存被清空,从而会话 1 第二次执行雷同的多表查问时会从数据库查问数据。

当初对 Mybatis 的二级缓存机制进行一个总结,如下所示。

  • Mybatis中的二级缓存默认开启,能够在 Mybatis 配置文件中的 <settings> 中增加 <setting name="cacheEnabled" value="false"/> 将二级缓存敞开;
  • Mybatis中的二级缓存作用范畴是同一命名空间下的多个会话共享,这里的命名空间就是映射文件的 namespace,即不同会话应用同一映射文件中的SQL 语句对数据库执行操作并提交事务后,均会影响这个映射文件持有的二级缓存;
  • Mybatis中执行查问操作后,须要提交事务能力将查问后果缓存到二级缓存中;
  • Mybatis中执行增,删或改操作并提交事务后,会清空对应的二级缓存;
  • Mybatis中须要在映射文件中增加 <cache> 标签来为映射文件配置二级缓存,也能够在映射文件中增加 <cache-ref> 标签来援用其它映射文件的二级缓存以达到多个映射文件持有同一份二级缓存的成果。

最初,对 <cache> 标签和 <cache-ref> 标签进行阐明。

<cache>标签如下所示。

属性 含意 默认值
eviction 缓存淘汰策略。LRU示意最近应用频次起码的优先被淘汰;FIFO示意先被缓存的会先被淘汰;SOFT示意基于软援用规定来淘汰;WEAK示意基于弱援用规定来淘汰 LRU
flushInterval 缓存刷新距离。单位毫秒 空,示意永不过期
type 缓存的类型 PerpetualCache
size 最多缓存的对象个数 1024
blocking 缓存未命中时是否阻塞 false
readOnly 缓存中的对象是否只读。配置为 true 时,示意缓存对象只读,命中缓存时会间接将缓存的对象返回,性能更快,然而线程不平安;配置为 false 时,示意缓存对象可读写,命中缓存时会将缓存的对象克隆而后返回克隆的对象,性能更慢,然而线程平安 false

<cache-ref>标签如下所示。

属性 含意
namespace 其它映射文件的命名空间,设置之后则以后映射文件将和其它映射文件将持有同一份二级缓存

四. 二级缓存的创立

在 Mybatis 源码 - 加载映射文件与动静代理中曾经晓得,XMLMapperBuilderconfigurationElement() 办法会解析映射文件的内容并丰盛到 Configuration 中,但在 Mybatis 源码 - 加载映射文件与动静代理中并未对解析映射文件的 <cache> 标签和 <cache-ref> 标签进行阐明,因而本大节将对这部分内容进行补充。configurationElement()办法如下所示。

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);
        // 解析 <cache-ref> 标签
        cacheRefElement(context.evalNode("cache-ref"));
        // 解析 <cache> 标签
        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);
    }
}

configurationElement() 办法中会先解析 <cache-ref> 标签,而后再解析 <cache> 标签,因而在这里先进行一个揣测:如果映射文件中同时存在 <cache-ref><cache>标签,那么 <cache> 标签配置的二级缓存会笼罩 <cache-ref> 援用的二级缓存。上面先剖析 <cache> 标签的解析,cacheElement()办法如下所示。

private void cacheElement(XNode context) {if (context != null) {
        // 获取 <cache> 标签的 type 属性值
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        // 获取 <cache> 标签的 eviction 属性值
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        // 获取 <cache> 标签的 flushInterval 属性值
        Long flushInterval = context.getLongAttribute("flushInterval");
        // 获取 <cache> 标签的 size 属性值
        Integer size = context.getIntAttribute("size");
        // 获取 <cache> 标签的 readOnly 属性值并取反
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        // 获取 <cache> 标签的 blocking 属性值
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

单步跟踪 cacheElement() 办法,每个属性解析进去的内容能够参照下图。

Cache的理论创立是在 MapperBuilderAssistantuseNewCache()办法中,实现如下所示。

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

MapperBuilderAssistantuseNewCache()办法中会先创立 CacheBuilder,而后调用CacheBuilderbuild()办法构建 CacheCacheBuilder 类图如下所示。

CacheBuilder的构造函数如下所示。

public CacheBuilder(String id) {
    this.id = id;
    this.decorators = new ArrayList<>();}

所以能够晓得,CacheBuilderid 字段理论就是以后映射文件的 namespace,其实到这里曾经大抵能够猜到,CacheBuilder 构建进去的二级缓存 CacheConfiguration中的惟一标识就是映射文件的 namespace。此外,CacheBuilder 中的 implementationPerpetualCacheClass 对象,decorators汇合中蕴含有 LruCacheClass对象。上面看一下 CacheBuilderbuild()办法,如下所示。

    public Cache build() {setDefaultImplementations();
        // 创立 PerpetualCache,作为根底 Cache 对象
        Cache cache = newBaseCacheInstance(implementation, id);
        setCacheProperties(cache);
        if (PerpetualCache.class.equals(cache.getClass())) {
            // 为根底 Cache 对象增加缓存淘汰策略相干的装璜器
            for (Class<? extends Cache> decorator : decorators) {cache = newCacheDecoratorInstance(decorator, cache);
                setCacheProperties(cache);
            }
            // 持续增加装璜器
            cache = setStandardDecorators(cache);
        } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {cache = new LoggingCache(cache);
        }
        return cache;
    }

CacheBuilderbuild() 办法首先会创立 PerpetualCache 对象,作为根底缓存对象,而后还会为根底缓存对象依据缓存淘汰策略增加对应的装璜器,比方 <cache> 标签中 eviction 属性值为 LRU,那么对应的装璜器为LruCache,依据eviction 属性值的不同,对应的装璜器就不同,下图是 Mybatis 为缓存淘汰策略提供的所有装璜器。

CacheBuilderbuild() 办法中,为 PerpetualCache 增加完缓存淘汰策略添装璜器后,还会持续增加规范装璜器,Mybatis中定义的规范装璜器有 ScheduledCacheSerializedCacheLoggingCacheSynchronizedCacheBlockingCache,含意如下表所示。

装璜器 含意
ScheduledCache 提供缓存定时刷新性能,<cache>标签设置了 flushInterval 属性值时会增加该装璜器
SerializedCache 提供缓存序列化性能,<cache>标签的 readOnly 属性设置为 false 时会增加该装璜器
LoggingCache 提供日志性能,默认会增加该装璜器
SynchronizedCache 提供同步性能,默认会增加该装璜器
BlockingCache 提供阻塞性能,<cache>标签的 blocking 属性设置为 true 时会增加该装璜器

如下是一个 <cache> 标签的示例。

<cache eviction="LRU"
       type="org.apache.ibatis.cache.impl.PerpetualCache"
       flushInterval="600000"
       size="1024"
       readOnly="false"
       blocking="true"/>

那么生成的二级缓存对象如下所示。

整个装璜链如下图所示。

当初回到 MapperBuilderAssistantuseNewCache()办法,构建好二级缓存对象之后,会将其增加到 Configuration 中,ConfigurationaddCache() 办法如下所示。

public void addCache(Cache cache) {caches.put(cache.getId(), cache);
}

这里就印证了后面的猜测,即二级缓存 CacheConfiguration中的惟一标识就是映射文件的namespace

当初再剖析一下 XMLMapperBuilder 中的 configurationElement() 办法对 <cache-ref> 标签的解析。cacheRefElement()办法如下所示。

private void cacheRefElement(XNode context) {if (context != null) {
        // 在 Configuration 的 cacheRefMap 中将以后映射文件命名空间与援用的映射文件命名空间建设映射关系
        configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
        CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
        try {
            //CacheRefResolver 会将援用的映射文件的二级缓存从 Configuration 中获取进去并赋值给 `MapperBuilderAssistant` 的 currentCache
            cacheRefResolver.resolveCacheRef();} catch (IncompleteElementException e) {configuration.addIncompleteCacheRef(cacheRefResolver);
        }
    }
}

cacheRefElement()办法会首先在 ConfigurationcacheRefMap中将以后映射文件命名空间与援用的映射文件命名空间建设映射关系,而后会通过 CacheRefResolver 将援用的映射文件的二级缓存从 Configuration 中获取进去并赋值给 MapperBuilderAssistantcurrentCachecurrentCache这个字段后续会在 MapperBuilderAssistant 构建 MappedStatement 时传递给 MappedStatement,以及如果映射文件中还存在<cache> 标签,那么 MapperBuilderAssistant 会将 <cache> 标签配置的二级缓存从新赋值给 currentCache 以笼罩 <cache-ref> 标签援用的二级缓存,所以映射文件中同时有 <cache-ref> 标签和 <cache> 标签时,只有 <cache> 标签配置的二级缓存会失效。

五. 二级缓存的源码剖析

本大节将对二级缓存对应的 Mybatis 源码进行探讨。Mybatis中开启二级缓存之后,执行查问操作时,调用链如下所示。

CachingExecutor 中有两个重载的 query() 办法,上面先看第一个 query() 办法,如下所示。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, 
        RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取 Sql 语句
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 创立 CacheKey
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

持续看重载的 query() 办法,如下所示。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
              ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 从 MappedStatement 中将二级缓存获取进去
    Cache cache = ms.getCache();
    if (cache != null) {
        // 清空二级缓存(如果需要的话)flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            // 解决存储过程相干逻辑
            ensureNoOutParams(ms, boundSql);
            // 从二级缓存中依据 CacheKey 命中查问后果
            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);
            }
            // 返回查问后果
            return list;
        }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

上述 query() 办法整体执行流程比较简单,概括下来就是:先从缓存中命中查问后果,命中到查问后果则返回,未命中到查问后果则间接查询数据库并把查问后果缓存到二级缓存中。然而从二级缓存中依据 CacheKey 命中查问后果时,并没有间接通过 CachegetObject()办法,而是通过 tcmgetObject()办法,正当进行揣测的话,应该就是 tcm 持有二级缓存的援用,当须要从二级缓存中命中查问后果时,由 tcm 将申请转发给二级缓存。实际上,tcmCachingExecutor 持有的 TransactionalCacheManager 对象,从二级缓存中命中查问后果这一申请之所以须要通过 TransactionalCacheManager 转发给二级缓存,是因为须要借助 TransactionalCacheManager 实现 只有当事务提交时,二级缓存才会被更新 这一性能。联想到第三大节中的场景一和场景二的示例,将查问后果缓存到二级缓存中须要事务提交这一性能,其实就是借助 TransactionalCacheManager 实现的,所以上面对 TransactionalCacheManager 进行一个阐明。首先 TransactionalCacheManager 的类图如下所示。

TransactionalCacheManager中持有一个 Map,该Map 的键为 Cache,值为TransactionalCache,即一个二级缓存对应一个TransactionalCache。持续看TransactionalCacheManagergetObject()办法,如下所示。

public Object getObject(Cache cache, CacheKey key) {return getTransactionalCache(cache).getObject(key);
}

private TransactionalCache getTransactionalCache(Cache cache) {return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}

通过上述代码能够晓得,一个二级缓存对应一个 TransactionalCache,且TransactionalCache 中持有这个二级缓存的援用,当调用 TransactionalCacheManagergetObject()办法时,TransactionalCacheManager会将调用申请转发给TransactionalCache,上面剖析一下TransactionalCache,类图如下所示。

持续看 TransactionalCachegetObject()办法,如下所示。

@Override
public Object getObject(Object key) {
    // 在二级缓存中命中查问后果
    Object object = delegate.getObject(key);
    if (object == null) {
        // 未命中则将 CacheKey 增加到 entriesMissedInCache 中
        // 用于统计命中率
        entriesMissedInCache.add(key);
    }
    if (clearOnCommit) {return null;} else {return object;}
}

到这里就能够晓得了,在 CachingExecutor 中通过 CacheKey 命中查问后果时,其实就是 CachingExecutor 将申请发送给 TransactionalCacheManagerTransactionalCacheManager 将申请转发给二级缓存对应的 TransactionalCache,最初再由TransactionalCache 将申请最终传递到二级缓存。在上述 getObject() 办法中,如果 clearOnCommit 为 true,则无论是否在二级缓存中命中查问后果,均返回 null,那么clearOnCommit 在什么中央会被置为 true 呢,其实就是在 CachingExecutorflushCacheIfRequired()办法中,这个办法在下面剖析的 query() 办法中会被调用到,看一下 flushCacheIfRequired() 的实现,如下所示。

private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {tcm.clear(cache);
    }
}

调用 TransactionalCacheManagerclear()办法时,最终会调用到 TransactionalCacheclear()办法,如下所示。

@Override
public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();}

当初持续剖析为什么将查问后果缓存到二级缓存中须要事务提交。从数据库中查问进去后果后,CachingExecutor会调用 TransactionalCacheManagerputObject()办法试图将查问后果缓存到二级缓存中,咱们曾经晓得,如果事务不提交,那么查问后果是无奈被缓存到二级缓存中,那么在事务提交之前,查问后果必定被 暂存 到了某个中央,为了搞清楚这部分逻辑,先看一下 TransactionalCacheManagerputObject()办法,如下所示。

public void putObject(Cache cache, CacheKey key, Object value) {getTransactionalCache(cache).putObject(key, value);
}

持续看 TransactionalCacheputObject()办法,如下所示。

@Override
public void putObject(Object key, Object object) {entriesToAddOnCommit.put(key, object);
}

到这里就搞明确了,在事务提交之前,查问后果会被 暂存 TransactionalCacheentriesToAddOnCommit 中。上面持续剖析事务提交时如何将 entriesToAddOnCommit暂存 的查问后果刷新到二级缓存中,DefaultSqlSessioncommit() 办法如下所示。

@Override
public void commit() {commit(false);
}

@Override
public void commit(boolean force) {
    try {executor.commit(isCommitOrRollbackRequired(force));
        dirty = false;
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error committing transaction. Cause:" + e, e);
    } finally {ErrorContext.instance().reset();}
}

DefaultSqlSessioncommit()办法中会调用到 CachingExecutorcommit()办法,如下所示。

@Override
public void commit(boolean required) throws SQLException {delegate.commit(required);
    // 调用 TransactionalCacheManager 的 commit()办法
    tcm.commit();}

CachingExecutorcommit()办法中,会调用 TransactionalCacheManagercommit()办法,如下所示。

public void commit() {for (TransactionalCache txCache : transactionalCaches.values()) {// 调用 TransactionalCache 的 commit()办法
        txCache.commit();}
}

持续看 TransactionalCachecommit()办法,如下所示。

public void commit() {if (clearOnCommit) {delegate.clear();
    }
    flushPendingEntries();
    reset();}

private void flushPendingEntries() {
    // 将 entriesToAddOnCommit 中暂存的查问后果全副缓存到二级缓存中
    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);
        }
    }
}

至此能够晓得,当调用 SqlSessioncommit()办法时,会一路传递到 TransactionalCachecommit()办法,最终调用 TransactionalCacheflushPendingEntries()办法将 暂存 的查问后果全副刷到二级缓存中。

当执行 操作并提交事务时,二级缓存会被清空,这是因为 操作最终会调用到 CachingExecutorupdate()办法,而 update() 办法中又会调用 flushCacheIfRequired() 办法,曾经晓得在 flushCacheIfRequired() 办法中如果所执行的办法对应的 MappedStatementflushCacheRequired字段为 true 的话,则会最终将 TransactionalCache 中的 clearOnCommit 字段置为 true,随即在事务提交的时候,会将二级缓存清空。而加载映射文件时,解析CURD 标签为 MappedStatement 时有如下一行代码。

boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);

即如果没有在 CURD 标签中显式的设置 flushCache 属性,则会给 flushCache 字段一个默认值,且默认值为非查问标签下默认为 true,所以到这里就能够晓得,如果是操作,那么 TransactionalCache 中的 clearOnCommit 字段会被置为 true,从而在提交事务时会在TransactionalCachecommit()办法中将二级缓存清空。

到这里,二级缓存的源码剖析完结。二级缓存的应用流程能够用下图进行概括,如下所示。

总结

对于 Mybatis 的一级缓存,总结如下。

  • Mybatis的一级缓存默认开启,且默认作用范畴为 SESSION,即一级缓存在一个会话中失效,也能够通过配置将作用范畴设置为STATEMENT,让一级缓存仅针对以后执行的SQL 语句失效;
  • 在同一个会话中,执行 操作会使本会话中的一级缓存生效;
  • 不同会话持有不同的一级缓存,本会话内的操作不会影响其它会话内的一级缓存。

对于 Mybatis 的二级缓存,总结如下。

  • Mybatis中的二级缓存默认开启,能够在 Mybatis 配置文件中的 <settings> 中增加 <setting name="cacheEnabled" value="false"/> 将二级缓存敞开;
  • Mybatis中的二级缓存作用范畴是同一命名空间下的多个会话共享,这里的命名空间就是映射文件的 namespace,即不同会话应用同一映射文件中的SQL 语句对数据库执行操作并提交事务后,均会影响这个映射文件持有的二级缓存;
  • Mybatis中执行查问操作后,须要提交事务能力将查问后果缓存到二级缓存中;
  • Mybatis中执行增,删或改操作并提交事务后,会清空对应的二级缓存;
  • Mybatis中须要在映射文件中增加 <cache> 标签来为映射文件配置二级缓存,也能够在映射文件中增加 <cache-ref> 标签来援用其它映射文件的二级缓存以达到多个映射文件持有同一份二级缓存的成果。
退出移动版