关于缓存: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>标签来援用其它映射文件的二级缓存以达到多个映射文件持有同一份二级缓存的成果。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理