一文搞懂MyBatis一级缓存和二级缓存

目录
  • 前言
  • 一. 一级缓存机制展示
  • 二. 一级缓存源码分析
  • 三. 二级缓存机制展示
    • 1. 场景一
    • 2. 场景二
    • 3. 场景三
    • 4. 场景四
    • 5. 场景五
  • 四. 二级缓存的创建
  • 五. 二级缓存的源码分析
  • 总结

前言

在本篇文章中,将结合示例与源码,对MyBatis中的一级缓存二级缓存进行说明。

MyBatis版本:3.5.6

正文

一. 一级缓存机制展示

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 property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
    </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 property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
    </resultMap>
    <select id="selectBookById" resultMap="bookResultMap">
        SELECT
            b.id,
            b.b_name,
            b.b_price
        FROM book b
        WHERE b.id=#{id}
    </select>
    <update id="updateBookPriceById">
        UPDATE book SET b_price=#{bookPrice}
        WHERE id=#{id}
    </update>
</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源码进行讨论。

已知,禁用二级缓存的情况下,执行查询操作时,调用链如下所示。

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 property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
    </resultMap>
    <select id="selectBookById" resultMap="bookResultMap">
        SELECT
            b.id,
            b.b_name,
            b.b_price
        FROM book b
        WHERE b.id=#{id}
    </select>
    <update id="updateBookPriceById">
        UPDATE book SET b_price=#{bookPrice}
        WHERE id=#{id}
    </update>
</mapper>

二级缓存相关设置的每一项的含义,会在本小节末尾进行说明。

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));
        System.out.println(bookMapper1.selectBookById(1));
        System.out.println(bookMapper2.selectBookById(1));
    }
}

执行结果如下所示。

MyBatis中的二级缓存开启时,每次查询会先去二级缓存中命中查询结果,未命中时才会使用一级缓存以及直接去查询数据库。上述结果截图表明,场景一中,SQL语句相同时,无论是同一会话的连续两次查询还是另一会话的一次查询,均是查询的数据库,仿佛二级缓存没有生效,实际上,将查询结果缓存到二级缓存中需要事务提交,场景一中并没有事务提交,所以二级缓存中是没有内容的,最终导致三次查询均是直接查询的数据库。此外,如果是增删改操作,只要没有事务提交,那么就不会影响二级缓存。

2. 场景二

场景二:创建两个会话,会话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));
    }
}

执行结果如下所示。

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

3. 场景三

场景三:创建两个会话,会话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));
    }
}

执行结果如下所示。

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

4. 场景四

场景四:创建两个会话,创建两张表,会话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 property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
    </resultMap>
    <resultMap id="bookDetailResultMap" type="com.mybatis.learn.entity.BookDetail">
        <id property="id" column="id"/>
        <result property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
        <association property="bookStore">
            <id property="id" column="id"/>
            <result property="bookStoreName" column="bs_name"/>
        </association>
    </resultMap>
    <select id="selectBookById" resultMap="bookResultMap">
        SELECT
            b.id,
            b.b_name,
            b.b_price
        FROM book b
        WHERE b.id=#{id}
    </select>
    <update id="updateBookPriceById">
        UPDATE book SET b_price=#{bookPrice}
        WHERE id=#{id}
    </update>
    <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"/>
    <update id="updateBookPriceById">
        UPDATE bookstore SET bs_name=#{bookStoreName}
        WHERE id=#{id}
    </update>
</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第二次执行相同的多表查询时从二级缓存中命中了脏数据。

5. 场景五

场景五:执行的操作和场景四一致,但是在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"/>
    <update id="updateBookStoreById">
        UPDATE bookstore SET bs_name=#{bookStoreName}
        WHERE id=#{id}
    </update>
</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将请求发送给TransactionalCacheManager
  • TransactionalCacheManager将请求转发给二级缓存对应的TransactionalCache
  • 最后再由TransactionalCache将请求最终传递到二级缓存。

在上述getObject() 方法中,如果clearOnCommittrue,则无论是否在二级缓存中命中查询结果,均返回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>标签来引用其它映射文件的二级缓存以达到多个映射文件持有同一份二级缓存的效果。

到此这篇关于一文搞懂MyBatis一级缓存和二级缓存的文章就介绍到这了,更多相关MyBatis一级缓存和二级缓存内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Mybatis的一级缓存和二级缓存原理分析与使用

    目录 Mybatis的一级缓存和二级缓存 1 Mybatis如何判断两次查询是完全相同的查询 2 二级缓存 2.1 二级缓存配置 2.2 二级缓存特点 2.3 配置二级缓存 2.4 测试 Mybatis的一级缓存和二级缓存 Mybatis会将相同查询条件的SQL语句的查询结果存储在内存或者某种缓存介质中,当下次遇到相同的SQL时不执行该SQL,而是直接从缓存中获取结果,减少服务器的压力,尤其是在查询越多.缓存命中率越高的情况下,使用缓存对性能的提高更明显. Mybatis缓存分为一级缓存和二级缓

  • 关于mybatis的一级缓存和二级缓存的那些事儿

    一.缓存是什么 缓存其实就是存储在内存中的临时数据,这里的数据量会比较小,一般来说,服务器的内存也是有限的,不可能将所有的数据都放到服务器的内存里面,所以, 只会把关键数据放到缓存中,缓存因为速度快,使用方便而出名! 二.为什么需要缓存 BS架构里面,用户的所有操作都是对数据库的增删改查,其中查询的操作是最多的,但如果用户想要某个数据时每次都去数据库查询,这无疑会增加数据库的压力,而且获取时间效率也会降低,所以为了解决这些问题,缓存应用而生,使用了缓存之后,服务器只需要查询一次数据库,然后将数据

  • Mybatis 一级缓存和二级缓存原理区别

    目录 Mybatis缓存 Mybatis一级缓存 1.为什么需要Mybatis一级缓存 2.Mybatis一级缓存的实现 3.Mybatis一级缓存配置 Mybatis二级缓存 1.为什么需要Mybatis二级缓存? 2.Mybatis二级缓存的实现 Mybatis一级缓存与二级缓存的区别 Java面试经常问到Mybatis一级缓存和二级缓存,今天就给大家重点详解Mybatis一级缓存和二级缓存原理与区别 Mybatis缓存 缓存就是内存中的数据,常常来自对数据库查询结果的保存,使用缓存可以避免

  • MyBatis 延迟加载、一级缓存、二级缓存(详解)

    使用ORM框架我们更多的是使用其查询功能,那么查询海量数据则又离不开性能,那么这篇中我们就看下mybatis高级应用之延迟加载.一级缓存.二级缓存.使用时需要注意延迟加载必须使用resultMap,resultType不具有延迟加载功能. 一.延迟加载 延迟加载已经是老生常谈的问题,什么最大化利用数据库性能之类之类的,也懒的列举了,总是我一提到延迟加载脑子里就会想起来了Hibernate get和load的区别.OK,废话少说,直接看代码. 先来修改配置项xml. 注意,编写mybatis.xm

  • mybatis一级缓存和二级缓存的区别及说明

    目录 结论 源码 一级缓存 二级缓存 我们通常说mybatis中一级缓存是sqlSession级别的,二级缓存是namespace级别的,这篇笔记主要来记录下这么说的原理 结论 先说结论吧,一级缓存之所以说是sqlSession级别的,是因为一级缓存的数据是存放在了sqlSession的一个内部属性中,所以,每次openSession()开启一个sqlSession之后,一级缓存就会失效 二级缓存之所以可以跨sqlSession,是因为二级缓存的数据,是存放在mappedStatement对象中

  • mybatis教程之查询缓存(一级缓存二级缓存和整合ehcache)

    1 缓存的意义 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题. 2 mybatis持久层缓存 mybatis提供一级缓存和二级缓存 mybatis一级缓存是一个SqlSession级别,sqlsession只能访问自己的一级缓存的数据,二级缓存是跨sqlSession,是mapper级别的缓存,对于mapper级别的缓存不同的sqlsession是可以共享的. 3 一级缓存 3.1 原

  • Mybatis详细对比一级缓存与二级缓存

    目录 基本要点 1.缓存 2.一级缓存(默认开启,无法关闭) 3.二级缓存 4.缓存查询原理 基本要点 1.缓存 什么是缓存? 存在内存中的临时数据,我们可以把用户经常查询的数据存放到缓存中,当用户重复查询时,我们可以直接从缓存中查询,提高查询效率,可以解决高并发系统的性能问题 为什么使用缓存? 减少和数据库交互次数,减轻数据库的压力,提高系统效率 什么样的数据能使用缓存? 经常查询且不经常改变的数据 2.一级缓存(默认开启,无法关闭) 1)一级缓存的有效区间是sqlSession从创建到关闭的

  • Mybatis 一级缓存与二级缓存的实现

    mybatis缓存 mybatis作为一个流行的持久化工具,缓存必然是缺少不了的组件.通过这篇文章,就让我们来了解一下mybatis的缓存. mybatis缓存类型 说起mybatis的缓存,了解过的同学都知道,mybatis中可以有两种缓存类型: 第一种,我们通常称为以及缓存,或者sqlSession级别的缓存,这种缓存是mybatis自带的,如果mapper中的配置都是默认的话,那么一级缓存也是默认开启的. 第二种,就是非sqlSession级别的缓存了,我们通常称为二级缓存,mybatis

  • 深入理解MyBatis中的一级缓存与二级缓存

    前言 先说缓存,合理使用缓存是优化中最常见的,将从数据库中查询出来的数据放入缓存中,下次使用时不必从数据库查询,而是直接从缓存中读取,避免频繁操作数据库,减轻数据库的压力,同时提高系统性能. 一级缓存 一级缓存是SqlSession级别的缓存.在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构用于存储缓存数据.不同的sqlSession之间的缓存数据区域是互相不影响的.也就是他只能作用在同一个sqlSession中,不同的sqlSession中的缓存是互相不能读取的. 一级缓

  • MyBatis一级缓存与二级缓存原理与作用分析

    目录 缓存的作用 MyBatis 的缓存结构 一级缓存 二级缓存 缓存的作用 在 Web 系统中,最重要的操作就是查询数据库中的数据.但是有些时候查询数据的频率非常高,这是很耗费数据库资源的,往往会导致数据库查询效率极低,影响客户的操作体验.于是可以将一些变动不大且访问频率高的数据,放置在一个缓存容器中,用户下一次查询时就从缓存容器中获取结果. MyBatis 的缓存结构 MyBatis 系统中默认定义了两级缓存:一级缓存和二级缓存: MyBatis 一级缓存是一个 SqlSession 级别,

  • SpringBoot + Mybatis-plus实战之Mybatis-plus的一级缓存、二级缓存

    前言 现在的JAVA行业,貌似已经是SpringBoot + SpringCloud 的天下了,早期的SSH,SSM框架已经老去,与SpringBoot相结合的JPA框架虽然省去了很多的增删改查sql,但是比较笨拙,在面对一些复杂多变的逻辑时常常力不从心,而相对应的Mybatis由于其高度的灵活性受到广大JAVA攻城狮的欢迎.之前整合过了springboot+mybatis,前几天看到一个面试的问一个问题,Mybatis的一级缓存,二级缓存.我想这个应该也是一个重点吧,所以今天决定来详细解读一下

随机推荐