本文共 11365 字,大约阅读时间需要 37 分钟。
关于缓存,想必大家早已不陌生。第一次使用查询数据时,Mybatis 将其结果缓存起来,当下次执行相同的查询的时候直接返回(没有声明需要刷新缓存,且缓存没有超时)。
默认情况下,Mybatis 只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。
一级缓存(本地的会话缓存):只与 SqlSession 有关,不同的 SqlSession 缓存不同。 二级缓存:SqlSession 可以共享缓存。缓存(总开关)是默认开启的,如果需要关闭缓存只需在 MyBatis 的配置文件中添加一个属性设置,
全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
还可自定义缓存参数
创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
可用的清除策略有(默认的清除策略是 LRU):
flushInterval(刷新间隔):属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size(引用数目):属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
readOnly(只读):属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
另一种开启方式,在 Mapper 接口上添加 @CacheNamespace 注解。
@CacheNamespacepublic 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 typepublic 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 Connection2020-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: 12020-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 Connection2020-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: 12020-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 Connection2020-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: 12020-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]
接下来,我们测试一下开启二级缓存,配置如下:
执行会出现什么结果呢
@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.02020-05-10 20:48:38 DEBUG [main] - Opening JDBC Connection2020-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: 12020-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.52020-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) 前后的输出结果。直接使用默认配置会如何呢?
若使用默认配置 ***<cache/>***,可能会报如下错误:
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); }}
@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.02020-05-10 20:49:40 DEBUG [main] - Opening JDBC Connection2020-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: 12020-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.52020-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 进行本地存储,不能满足分布式系统的要求。
可以通过实现你自己的缓存,来完全覆盖缓存行为。type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口。
若需对缓存进行配置,只需要简单地在你的缓存实现中添加公有的 JavaBean 属性,然后通过 cache 元素传递属性值。
Mybatis 默认只启用了本地的会话缓存,如果要开启二级缓存则另外需要增加配置,也可使用自定义的二级缓存实现。
源码分析请看下篇
转载地址:http://pmrai.baihongyu.com/