跳至主要內容

javaweb - Mybatis8

codejavawebmybatis约 2226 字大约 7 分钟

Mybatis详解6

缓存机制

其实缓存机制我们在之前学习IO流的时候已经提及过了,我们可以提前将一部分内容放入缓存,下次需要获取数据时,就可以直接从缓存中读取,这样的话相当于直接从内存中获取而不是再去向数据库索要数据,效率会更高,缓存的概念在我们后续的学习中还会经常遇见,它也是现在提高数据获取效率的良好解决方案。

Mybatis为了查询效率,同样内置了一个缓存机制,我们在查询时,如果Mybatis缓存中存在数据,那么我们就可以直接从缓存中获取,而不是再去向数据库进行请求,节省性能开销。

alt text
alt text

Mybatis的缓存机制有些复杂,存在一级缓存(本地缓存)二级缓存

一级缓存 (作用在SqlSession)

我们首先来看一下一级缓存,默认情况下,只启用了本地的会话缓存,也就是一级缓存,它仅仅对一个会话中的数据进行缓存(一级缓存强制启用,无法关闭,只能做调整)

也就是每一个SqlSession都有有一个对应的缓存

我们来看看下面这段代码:

TestMapper mapper = session.getMapper(TestMapper.class);
System.out.println(mapper.selectUserById(1));
System.out.println(mapper.selectUserById(1));  //再次获取

这里我们连续获取了两次ID为1的用户,我们会在日志中惊奇地发现,这里的查询操作实际上只进行了一次:

alt text
alt text

我们去掉类上的@Data注解,会发现得到的两个对象实际上就是同一个:

alt text
alt text

也就是说我们第二次查询不仅压根就没执行SQL语句,甚至直接没有重新构造对象,而是直接获取之前创建好的

可见,Mybatis确实存在着缓存机制来进行性能优化。

那么如果我修改了数据库中的内容,缓存还会生效吗:

System.out.println(mapper.selectUserById(1));
mapper.updateAgeById(1, 19);
System.out.println(mapper.selectUserById(1));

此时由于我们更新了数据库中的数据,那么之前缓存的内容也会跟着失效,第二次获取的时候会进行重新查询。

也就是说Mybatis知道我们对数据库里面的数据进行了修改,所以之前缓存的内容可能就不是当前数据库里面最新的内容了。

但是一定注意,一级缓存只针对于单个会话多个会话之间不相通

因此, 一个会话DML操作只会重置当前会话的缓存,不会重置其他会话的缓存,我们可以来试验一下:

try(SqlSession s1 = MybatisUtils.openSession(true);
    SqlSession s2 = MybatisUtils.openSession(true)) {
    TestMapper m1 = s1.getMapper(TestMapper.class);
    TestMapper m2 = s2.getMapper(TestMapper.class);
    System.out.println(m1.selectUserById(1));
    m2.updateAgeById(1, 19);
    System.out.println(m1.selectUserById(1));
}

可以看到,会话1在重复查询数据时,即使会话2已经修改了数据,但是依然没有影响会话1之中的缓存。

这导致尽管会话2更新了数据,但是会话1中的数据是从缓存得到的,没有更新。

一级缓存给我们提供了很高速的访问效率,但是它的作用范围实在是有限,如果一个会话结束,那么之前的缓存就全部失效了,但是我们希望缓存能够扩展到所有会话都能使用,无论哪个会话对于数据的查询缓存都可以直接被所有会话使用。

二级缓存 (作用在Mapper)

我们可以通过二级缓存来实现,二级缓存默认是关闭状态,要开启二级缓存,我们需要在映射器XML文件中添加:

<cache/>

二级缓存是Mapper级别的,只要是使用这个Mapper的会话,都会关联到这个二级缓存

无论哪个会话失效,它之前查询的缓存依然会存在于二级缓存中,依然可以被其他会话直接使用。

我们可以对cache标签进行一些配置:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

其中,size表示最大的缓存对象数量,当缓存达到上限时,会根据eviction配置的策略进行清理:

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

flushInterval用于控制缓存刷新时间,当到达指定时间时会自动清理所有缓存,默认情况下如果不配置此项则不会进行定时清理。

readOnly(只读)属性可以被设置为 true 或 false,只读的缓存会给所有调用者返回相同的缓存对象,且对象不能被修改。这就提供了可观的性能提升。

而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

注意: 二级缓存是事务性的,这意味着,当 SqlSession 结束并提交时,或是结束并回滚,而且没有执行 flushCache=trueinsert/delete/update 语句时,二级缓存才会被更新

即:

try(SqlSession session = MybatisUtil.getSession(true);
    SqlSession session2 = MybatisUtil.getSession(true)
){
    TestMapper mapper = session.getMapper(TestMapper.class);
    System.out.println(mapper.selectStuById(1));
    session2.commit();

    TestMapper mapper2 = session2.getMapper(TestMapper.class);
    System.out.println(mapper2.selectStuById(1));
}

只有commit了,才会保存在二级缓存,这样用mapper2时,才不会重新读取,如果没有commit,就会重新读取,因为并没有保存在缓存中

开启二级缓存后,再次执行我们之前的操作,就可以直接在二级缓存中命中了:

alt text
alt text
查找顺序

实际上,添加了二级缓存之后,Mybatis会先从二级缓存中查找数据,当二级缓存中没有时,才会从一级缓存中获取,当一级缓存中都还没有数据时,才会请求数据库

单独配置取消二级缓存 useCache='false'

当我们开启二级缓存后,默认情况下一个Mapper中所有的操作都会使用二级缓存

我们也可以单独配置其不使用二级缓存,只需要修改useCache属性即可:

<select id="selectUserById" useCache="false" resultType="com.test.User">
    select * from user where id = #{id}
</select>
操作结束,清除所有缓存 flushCache

有些操作可能比较特殊,比如我们希望某个操作执行完成后,直接清除所有缓存,无论是一级缓存还是二级缓存,那么此时就可以开启flushCache属性:

<select id="selectUserById" flushCache="true" resultType="com.test.User">
    select * from user where id = #{id}
</select>

开启此选项后,调用此操作将直接导致一级和二级缓存被清除。

缓存一致性问题

虽然缓存机制给我们提供了很大的性能提升,但是缓存存在一个问题,我们之前在计算机组成原理中可能学习过缓存一致性问题

当多个CPU在操作自己的缓存时,可能会出现各自的缓存内容不同步的问题。

alt text
alt text

Mybatis也会这样,我们来看看这个例子:

TestMapper mapper = session.getMapper(TestMapper.class);
while (true){
    Thread.sleep(3000);
    System.out.println(mapper.selectUserById(1));
}

我们现在循环地每三秒读取一次,而在这个过程中,我们使用其他软件手动修改数据库中的数据,将1号用户的ID改成100,那么理想情况下,下一次读取将直接无法获取到这行数据,因为ID已经发生变化了。

但是结果却是依然能够读取,并且sid并没有发生改变,这也证明了Mybatis的缓存在生效,因为我们是从外部进行修改,Mybatis不知道我们修改了数据,所以依然在使用缓存中的数据,但是这样很明显是不正确的

因此,如果存在多台服务器或者是多个程序都在使用Mybatis操作同一个数据库,并且都开启了缓存,需要解决这个问题

我们只能关闭所有二级缓存,并且在Mybatis每个操作都配置flushCache为true来保证刷新。

只不过这种操作实际上是治标不治本的,实现多服务器缓存共用才是最终解决方案,也就是让所有的Mybatis都使用同一个缓存进行数据存取

在后面,我们会继续学习Redis、Ehcache、Memcache等缓存框架,通过使用这些工具,就能够很好地解决缓存一致性问题。

上次编辑于: