关于缓存,想必大家早已不陌生。第一次使用查询数据时,Mybatis 将其结果缓存起来,当下次执行相同的查询的时候直接返回(没有声明需要刷新缓存,且缓存没有超时)。
默认情况下,Mybatis 只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。
一级缓存(本地的会话缓存):只与 SqlSession 有关,不同的 SqlSession 缓存不同。
二级缓存:SqlSession 可以共享缓存。
缓存(总开关)是默认开启的,如果需要关闭缓存只需在 MyBatis 的配置文件中添加一个属性设置,
全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。
<settings>
<setting name="cacheEnabled" value="false"/>
settings>
要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
还可自定义缓存参数
创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
可用的清除策略有(默认的清除策略是 LRU):
flushInterval(刷新间隔):属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size(引用数目):属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
readOnly(只读):属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
另一种开启方式,在 Mapper 接口上添加 @CacheNamespace 注解。
@CacheNamespace
public interface AutoConstructorMapper {
}
注解配置和 xml 配置只能选择一个,否者报错如下:
Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.IllegalArgumentException: Caches collection already contains value for org.apache.ibatis
另一种开启方式,还可和其他命名空间共享相同的缓存配置和实例,在 Mapper 接口上添加 @CacheNamespaceRef注解。
@CacheNamespaceRef(name = "org.apache.ibatis.submitted.cache.PersonMapper") // by name
// 或者 @CacheNamespaceRef(PersonMapper.class) // by type
public interface AutoConstructorMapper {
}
默认一级缓存,具体是什么效果呢?接下来看看下面的例子。
@Test
void localCacheTest() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
PrimitiveSubject subject1 = mapper.getSubject(1);
PrimitiveSubject subject2 = mapper.getSubject(1);
PrimitiveSubject subject3 = mapper.getSubject(1);
assertTrue(subject1 == subject2);
assertTrue(subject1 == subject3);
}
}
单元测试成功,(subject1 == subject2,且 subject1 == subject3)说明查询返回是的是相同的对象。
日志也只打印了一个查询语句,也说明 SQL 只执行了一次。
2020-05-10 18:07:35 DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2020-05-10 18:07:38 DEBUG [main] - Opening JDBC Connection
2020-05-10 18:07:38 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:38 DEBUG [main] - ==> Preparing: SELECT * FROM subject WHERE id = ?
2020-05-10 18:07:38 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 18:07:38 DEBUG [main] - <== Total: 1
2020-05-10 18:07:38 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:38 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
@Test
void localCacheDifferentSqlSession() {
PrimitiveSubject subject1;
PrimitiveSubject subject2;
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
subject1 = mapper.getSubject(1);
}
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
subject2 = mapper.getSubject(1);
}
assertTrue(subject1 != subject2);
}
单元测试成功,(subject1 != subject2)说明查询返回是的是不同的对象。
日志打印了两次查询语句,说明 SQL 执行了两次,一级缓存对不同的 SqlSession 无效。
2020-05-10 18:07:54 DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2020-05-10 18:07:56 DEBUG [main] - Opening JDBC Connection
2020-05-10 18:07:56 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:56 DEBUG [main] - ==> Preparing: SELECT * FROM subject WHERE id = ?
2020-05-10 18:07:56 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 18:07:56 DEBUG [main] - <== Total: 1
2020-05-10 18:07:56 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:56 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:56 DEBUG [main] - Opening JDBC Connection
2020-05-10 18:07:56 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@51bd8b5c]
2020-05-10 18:07:56 DEBUG [main] - ==> Preparing: SELECT * FROM subject WHERE id = ?
2020-05-10 18:07:56 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 18:07:56 DEBUG [main] - <== Total: 1
2020-05-10 18:07:56 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@51bd8b5c]
2020-05-10 18:07:56 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@51bd8b5c]
接下来,我们测试一下开启二级缓存,配置如下:
<mapper namespace="org.apache.ibatis.autoconstructor.AutoConstructorMapper">
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
<select id="getSubject" resultType="org.apache.ibatis.autoconstructor.PrimitiveSubject">
SELECT * FROM subject WHERE id = #{id}
select>
mapper>
执行会出现什么结果呢
@Test
void twoLevelCacheDifferentSqlSession() {
PrimitiveSubject subject1;
PrimitiveSubject subject2;
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
subject1 = mapper.getSubject(1);
}
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
subject2 = mapper.getSubject(1);
}
log.debug(subject1.toString());
log.debug(subject2.toString());
subject1.setAge(27);
log.debug(subject1.toString());
log.debug(subject2.toString());
assertTrue(subject1 == subject2);
}
2020-05-10 20:48:36 DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2020-05-10 20:48:38 DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.0
2020-05-10 20:48:38 DEBUG [main] - Opening JDBC Connection
2020-05-10 20:48:38 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:48:38 DEBUG [main] - ==> Preparing: SELECT * FROM subject WHERE id = ?
2020-05-10 20:48:38 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 20:48:38 DEBUG [main] - <== Total: 1
2020-05-10 20:48:38 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:48:38 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:48:38 DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.5
2020-05-10 20:48:38 DEBUG [main] - PrimitiveSubject{
id=1, name='a', age=10, height=100, weight=45, active=true, dt=Sun May 10 20:48:38 CST 2020}
2020-05-10 20:48:38 DEBUG [main] - PrimitiveSubject{
id=1, name='a', age=10, height=100, weight=45, active=true, dt=Sun May 10 20:48:38 CST 2020}
2020-05-10 20:48:38 DEBUG [main] - PrimitiveSubject{
id=1, name='a', age=27, height=100, weight=45, active=true, dt=Sun May 10 20:48:38 CST 2020}
2020-05-10 20:48:38 DEBUG [main] - PrimitiveSubject{
id=1, name='a', age=27, height=100, weight=45, active=true, dt=Sun May 10 20:48:38 CST 2020}
SQL 只执行了一次,且多了一行输出 Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.5 ,说明二次缓存命中。
这里需要注意的是 Cache 的配置 ***readOnly=“true”***,如果修改缓存,是会影响其他调用者的。
可以看 subject1.setAge(27) 前后的输出结果。
直接使用默认配置会如何呢?
若使用默认配置 ***
org.apache.ibatis.cache.CacheException: Error serializing object. Cause: java.io.NotSerializableException: org.apache.ibatis.autoconstructor.PrimitiveSubject
此时需要给 PrimitiveSubject 类实现 Serializable 接口,因为默认 ***readOnly=“false”***,是通过 SerializedCache 类来实现的,序列化和反序列化需要实现 Serializable 接口,序列化可保证业务更改获取到的值不影响实际的缓存。具体实现代码在:
##org.apache.ibatis.mapping.CacheBuilder#setStandardDecorators
if (readWrite) {
cache = new SerializedCache(cache);
}
public class SerializedCache implements Cache {
public void putObject(Object key, Object object) {
if (object == null || object instanceof Serializable) {
delegate.putObject(key, serialize((Serializable) object));
} else {
throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
}
}
public Object getObject(Object key) {
Object object = delegate.getObject(key);
return object == null ? null : deserialize((byte[]) object);
}
}
<mapper namespace="org.apache.ibatis.autoconstructor.AutoConstructorMapper">
<cache/>
<select id="getSubject" resultType="org.apache.ibatis.autoconstructor.PrimitiveSubject">
SELECT * FROM subject WHERE id = #{id}
select>
@Test
void twoLevelCacheDifferentSqlSessionWithDefaultCache() {
...
log.debug(subject1.toString());
log.debug(subject2.toString());
assertTrue(subject1 != subject2);
}
单元测试成功,且 subject1 != subject2(不相等,是因为反序列化后返回的结果),但是两者的内容是一样的,SQL 只执行一次,缓存命中。
2020-05-10 20:49:38 DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2020-05-10 20:49:40 DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.0
2020-05-10 20:49:40 DEBUG [main] - Opening JDBC Connection
2020-05-10 20:49:40 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:49:40 DEBUG [main] - ==> Preparing: SELECT * FROM subject WHERE id = ?
2020-05-10 20:49:40 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 20:49:40 DEBUG [main] - <== Total: 1
2020-05-10 20:49:40 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:49:40 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:49:40 DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.5
2020-05-10 20:49:40 DEBUG [main] - PrimitiveSubject{
id=1, name='a', age=10, height=100, weight=45, active=true, dt=Sun May 10 20:49:40 CST 2020}
2020-05-10 20:49:40 DEBUG [main] - PrimitiveSubject{
id=1, name='a', age=10, height=100, weight=45, active=true, dt=Sun May 10 20:49:40 CST 2020}
开启一个不自动提交的事务1,事务1先查询数据总数为3条,删除一条记录之后执行查询总数变为2条,回滚之后再次查询总数为3条。
开启一个自动提交的事务2,事务2执行查询,总数还是为3条,删除一条记录。
开启一个自动提交的事务3,事务3执行查询,总数变为2条。
@Test
void transactionCache() {
try (SqlSession sqlSession = sqlSessionFactory.openSession(false)) {
AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
assertEquals(3, mapper.getSubjects().size());
mapper.deleteSubject(1);
assertEquals(2, mapper.getSubjects().size());
sqlSession.rollback();
assertEquals(3, mapper.getSubjects().size());
}
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
assertEquals(3, mapper.getSubjects().size());
mapper.deleteSubject(1);
}
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
assertEquals(2, mapper.getSubjects().size());
}
}
执行无报错,缓存事务测试通过。
Mybatis 二级缓存默认使用 PerpetualCache 进行本地存储,不能满足分布式系统的要求。
可以通过实现你自己的缓存,来完全覆盖缓存行为。
<cache type="com.domain.something.MyCustomCache">
<property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
cache>
type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口。
若需对缓存进行配置,只需要简单地在你的缓存实现中添加公有的 JavaBean 属性,然后通过 cache 元素传递属性值。
Mybatis 默认只启用了本地的会话缓存,如果要开启二级缓存则另外需要增加配置,也可使用自定义的二级缓存实现。
源码分析请看下篇 Mybatis 缓存实现原理