目录
前言一. 一级缓存机制展示二. 一级缓存源码分析三. 二级缓存机制展示
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中的
multiplier,
hashcode,
checksum,
count和
updateList字段用于判断
CacheKey之间是否相等,这些字段会在
CacheKey的构造函数中进行初始化,如下所示。
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLIER;
this.count = 0;
this.updateList = new ArrayList<>();
}同时
hashcode,
checksum,
count和
updateList字段会在
CacheKey的
update() 方法中被更新,如下所示。
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() 方法的入参计算并更新
hashcode,
checksum和
count的值,然后再将入参添加到
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标签上的
statementType为
CALLABLE,则会先在
handleLocallyCachedOutputParameters() 方法中处理存储过程相关逻辑然后再将命中的查询结果返回,如果未命中到查询结果,则会直接查询数据库。
上述
query() 方法中还使用到了
BaseExecutor的
queryStack字段,主要防止一级缓存作用范围是
STATEMENT并且还存在递归调用
query() 方法时,在递归尚未终止时就将一级缓存删除,如果不存在递归调用,那么一级缓存作用范围是
STATEMENT时,每次查询结束后,都会清空缓存。
下面看一下
BaseExecutor中的一级缓存
localCache,其实际是
PerpetualCache,类图如下所示。
所以
PerpetualCache的内部主要是基于一个
Map(实际为
HashMap)用于数据存储。
现在回到上面的
BaseExecutor的
query() 方法中,如果没有在一级缓存中命中查询结果,则会直接查询数据库,
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中如果是执行
增,
改和
删操作,并且在禁用二级缓存的情况下,均会调用到
BaseExecutor的
update() 方法,如下所示。
@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加载映射文件和动态代理中已经知道,
XMLMapperBuilder的
configurationElement() 方法会解析映射文件的内容并丰富到
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的实际创建是在
MapperBuilderAssistant的
useNewCache() 方法中,实现如下所示。
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;
}在
MapperBuilderAssistant的
useNewCache() 方法中会先创建
CacheBuilder,然后调用
CacheBuilder的
build() 方法构建
Cache。
CacheBuilder类图如下所示。
CacheBuilder的构造函数如下所示。
public CacheBuilder(String id) {
this.id = id;
this.decorators = new ArrayList<>();
}所以可以知道,
CacheBuilder的
id字段实际就是当前映射文件的
namespace,其实到这里已经大致可以猜到,
CacheBuilder构建出来的二级缓存
Cache在
Configuration中的唯一标识就是映射文件的
namespace。此外,
CacheBuilder中的
implementation是
PerpetualCache的
Class对象,
decorators集合中包含有
LruCache的
Class对象。下面看一下
CacheBuilder的
build() 方法,如下所示。
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;
}
CacheBuilder的
build() 方法首先会创建
PerpetualCache对象,作为基础缓存对象,然后还会为基础缓存对象根据缓存淘汰策略添加对应的装饰器,比如<
cache>标签中
eviction属性值为
LRU,那么对应的装饰器为
LruCache,根据
eviction属性值的不同,对应的装饰器就不同,下图是
MyBatis为缓存淘汰策略提供的所有装饰器。
CacheBuilder的
build() 方法中,为
PerpetualCache添加完缓存淘汰策略添装饰器后,还会继续添加标准装饰器,
MyBatis中定义的标准装饰器有
ScheduledCache,
SerializedCache,
LoggingCache,
SynchronizedCache和
BlockingCache,含义如下表所示。
| 装饰器 | 含义 |
| 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"/>那么生成的二级缓存对象如下所示。
整个装饰链如下图所示。
现在回到
MapperBuilderAssistant的
useNewCache() 方法,构建好二级缓存对象之后,会将其添加到
Configuration中,
Configuration的
addCache() 方法如下所示。
public void addCache(Cache cache) {
caches.put(cache.getId(), cache);
}这里就印证了前面的猜想,即二级缓存
Cache在
Configuration中的唯一标识就是映射文件的
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() 方法会首先在
Configuration的
cacheRefMap中将当前映射文件命名空间与引用的映射文件命名空间建立映射关系,然后会通过
CacheRefResolver将引用的映射文件的二级缓存从
Configuration中获取出来并赋值给
MapperBuilderAssistant的
currentCache,
currentCache这个字段后续会在
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命中查询结果时,并没有直接通过
Cache的
getObject() 方法,而是通过
tcm的
getObject() 方法,合理进行推测的话,应该就是
tcm持有二级缓存的引用,当需要从二级缓存中命中查询结果时,由
tcm将请求转发给二级缓存。
实际上,
tcm为
CachingExecutor持有的
TransactionalCacheManager对象,从二级缓存中命中查询结果这一请求之所以需要通过
TransactionalCacheManager转发给二级缓存,是因为需要借助
TransactionalCacheManager实现
只有当事务提交时,二级缓存才会被更新这一功能。联想到第三小节中的场景一和场景二的示例,将查询结果缓存到二级缓存中需要事务提交这一功能,其实就是借助
TransactionalCacheManager实现的,所以下面对
TransactionalCacheManager进行一个说明。首先
TransactionalCacheManager的类图如下所示。
TransactionalCacheManager中持有一个
Map,该
Map的键为
Cache,值为
TransactionalCache,即一个二级缓存对应一个
TransactionalCache。继续看
TransactionalCacheManager的
getObject() 方法,如下所示。
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中持有这个二级缓存的引用,当调用
TransactionalCacheManager的
getObject() 方法时,
TransactionalCacheManager会将调用请求转发给
TransactionalCache,下面分析一下
TransactionalCache,类图如下所示。
继续看
TransactionalCache的
getObject() 方法,如下所示。
@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() 方法中,如果
clearOnCommit为
true,则无论是否在二级缓存中命中查询结果,均返回
null,那么
clearOnCommit在什么地方会被置为
true呢,其实就是在
CachingExecutor的
flushCacheIfRequired() 方法中,这个方法在上面分析的
query() 方法中会被调用到,看一下
flushCacheIfRequired() 的实现,如下所示。
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}调用
TransactionalCacheManager的
clear() 方法时,最终会调用到
TransactionalCache的
clear() 方法,如下所示。
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}现在继续分析为什么将查询结果缓存到二级缓存中需要事务提交。从数据库中查询出来结果后,
CachingExecutor会调用
TransactionalCacheManager的
putObject() 方法试图将查询结果缓存到二级缓存中,我们已经知道,如果事务不提交,那么查询结果是无法被缓存到二级缓存中,那么在事务提交之前,查询结果肯定被
暂存到了某个地方,为了搞清楚这部分逻辑,先看一下
TransactionalCacheManager的
putObject() 方法,如下所示。
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}继续看
TransactionalCache的
putObject() 方法,如下所示。
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}到这里就搞明白了,在事务提交之前,查询结果会被
暂存到
TransactionalCache的
entriesToAddOnCommit中。
下面继续分析事务提交时如何将
entriesToAddOnCommit中
暂存的查询结果刷新到二级缓存中,
DefaultSqlSession的
commit() 方法如下所示。
@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();
}
}在
DefaultSqlSession的
commit() 方法中会调用到
CachingExecutor的
commit() 方法,如下所示。
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
// 调用TransactionalCacheManager的commit()方法
tcm.commit();
}在
CachingExecutor的
commit() 方法中,会调用
TransactionalCacheManager的
commit() 方法,如下所示。
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
// 调用TransactionalCache的commit()方法
txCache.commit();
}
}继续看
TransactionalCache的
commit() 方法,如下所示。
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);
}
}
}至此可以知道,当调用
SqlSession的
commit() 方法时,会一路传递到
TransactionalCache的
commit() 方法,最终调用
TransactionalCache的
flushPendingEntries() 方法将
暂存的查询结果全部刷到二级缓存中。
当执行
增,
删,
改操作并提交事务时,二级缓存会被清空,这是因为
增,
删,
改操作最终会调用到
CachingExecutor的
update() 方法,而
update() 方法中又会调用
flushCacheIfRequired() 方法,已经知道在
flushCacheIfRequired() 方法中如果所执行的方法对应的
MappedStatement的
flushCacheRequired字段为
true的话,则会最终将
TransactionalCache中的
clearOnCommit字段置为
true,随即在事务提交的时候,会将二级缓存清空。而加载映射文件时,解析
CURD标签为
MappedStatement时有如下一行代码。
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);即如果没有在
CURD标签中显式的设置
flushCache属性,则会给
flushCache字段一个默认值,且默认值为非查询标签下默认为
true,所以到这里就可以知道,如果是
增,
删,
改操作,那么
TransactionalCache中的
clearOnCommit字段会被置为
true,从而在提交事务时会在
TransactionalCache的
commit() 方法中将二级缓存清空。
到这里,二级缓存的源码分析结束。二级缓存的使用流程可以用下图进行概括,如下所示。
总结
关于
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一级缓存和二级缓存内容请搜索中国红客联盟以前的文章或继续浏览下面的相关文章希望大家以后多多支持中国红客联盟!