博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Mybatis 缓存实现原理——案例实践
阅读量:4180 次
发布时间:2019-05-26

本文共 11365 字,大约阅读时间需要 37 分钟。

Mybatis 缓存介绍

关于缓存,想必大家早已不陌生。第一次使用查询数据时,Mybatis 将其结果缓存起来,当下次执行相同的查询的时候直接返回(没有声明需要刷新缓存,且缓存没有超时)。

默认情况下,Mybatis 只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。

一级缓存(本地的会话缓存):只与 SqlSession 有关,不同的 SqlSession 缓存不同。
二级缓存:SqlSession 可以共享缓存。

缓存(总开关)是默认开启的,如果需要关闭缓存只需在 MyBatis 的配置文件中添加一个属性设置,

全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。

开启二级缓存的方式

要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:

还可自定义缓存参数

创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

可用的清除策略有(默认的清除策略是 LRU):

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

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 {
}

一级缓存的效果(同一个 SqlSession )

默认一级缓存,具体是什么效果呢?接下来看看下面的例子。

@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]

一级缓存的效果(不同的 SqlSession )

@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) 前后的输出结果。

直接使用默认配置会如何呢?

获取缓存报 NotSerializableException 错?

若使用默认配置 ***<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/

你可能感兴趣的文章
199. Binary Tree Right Side View(Tree)
查看>>
230. Kth Smallest Element in a BST(Tree)
查看>>
求字符串的最长回文串-----Manacher's Algorithm 马拉车算法
查看>>
回溯法常用的解题模板和常见题型
查看>>
深入分析Java I/O 的工作机制
查看>>
动态规划的套路----左神
查看>>
KMP算法简解
查看>>
左神算法课进阶版总结
查看>>
左神算法基础班总结
查看>>
Linux性能优化
查看>>
进程间的通信---UNIX高级环境编程
查看>>
基于SSH开发的城市公交管理系统 JAVA MySQL
查看>>
基于SSH开发的勤工助学管理系统 JAVA MySQL
查看>>
基于SSH开发的宠物销售商城系统 JAVA MySQL
查看>>
基于springboot的宠物领养管理系统 java
查看>>
JAVA 洗衣房管理系统 宿舍洗衣机管理系统
查看>>
基于SSM的街道办安全管理系统 JAVA
查看>>
基于SSM的论文选题管理系统 JAVA
查看>>
生成器模式
查看>>
工厂方法模式
查看>>