MyBatis 缓存

1. 前言

频繁地查询必然会给数据库带来巨大的压力,为此 MyBatis 提供了丰富的缓存功能。缓存可以有效的提升查询效率、缓解数据库压力,提高应用的稳健性。

MyBatis 的缓存有两层,默认情况下会开启一级缓存,并提供了开启二级缓存的配置。本小节我们将一起学习 MyBatis 的缓存,充分地了解和使用它。

2. 一级缓存

MyBatis 一级缓存是默认开启的,缓存的有效范围是一个会话内。一个会话内的 select 查询语句的结果会被缓存起来,当在该会话内调用 update、delete 和 insert 时,会话缓存会被刷新,以前的缓存会失效。

2.1 使用一级缓存

下面,我们以一个简单的例子来看看 MyBatis 的一级缓存是如何工作的。

package com.imooc.mybatis.cache;

import com.imooc.mybatis.mapper.UserMapper;
import com.imooc.mybatis.model.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;

@SuppressWarnings({"Duplicates"})
public class CacheTest1 {
  public static void main(String[] args) throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession session = sqlSessionFactory.openSession();
    // 得到 mapper
    UserMapper userMapper = session.getMapper(UserMapper.class);
    // 查询得到 user1
    User user1 = userMapper.selectUserById(1);
    System.out.println(user1);
    // 查询得到 user2
    User user2 = userMapper.selectUserById(1);
    // 通过 == 判断 user1 和 user2 是否指向同一内存区间
    System.out.println(user1 == user2);
    session.commit();
    session.close();
  }
}

结果:

User{id=1, username='peter-gao', age=180, score=1000}
true

在这个例子中,我们连续两次调用了 userMapper 的 selectUserById 方法,但是在程序输出中,user1 和 user2 却指向了同一块内存区域。这就是 MyBatis 缓存的作用,当第二次调用查询时,MyBatis 没有查询数据库而是直接从缓存中拿到了数据。

2.2 弃用一级缓存

2.2.1 select 配置关闭缓存

select 默认会启用一级缓存,我们也可通过配置来关闭掉 select 缓存。

如下,我们通过 flushCache 属性来关闭 select 查询的缓存。

<select id="selectUserById" flushCache="true" parameterType="java.lang.Integer"
        resultType="com.imooc.mybatis.model.User">
  SELECT * FROM imooc_user WHERE id = #{id}
</select>

再次运行程序,结果如下:

User{id=1, username='peter-gao', age=180, score=1000}
false

此时 user1 与 user2 不再指向同一内存区,缓存失效了。

2.2.2 调用 insert、update、delete 刷新缓存

一般情况下,我们都推荐开启 select 的缓存,因为这会节省查询时间。当然在一个会话中,调用 insert、update、delete 语句时,会话中的缓存也会被刷新。

如下:

UserMapper userMapper = session.getMapper(UserMapper.class);
User user1 = userMapper.selectUserById(1);
System.out.println(user1);
User user = new User();
user.setUsername("cache test");
user.setAge(10);
user.setScore(100);
userMapper.insertUser(user);
User user2 = userMapper.selectUserById(1);
System.out.println(user1 == user2);
session.commit();
session.close();
User{id=1, username='peter', age=18, score=100}
false

在第一个查询调用前,我们先进行了一次 insert 操作,此时会刷新缓存,user1 和 user2 又没有指向同一处内存。

3. 二级缓存

MyBatis 二级缓存默认关闭,我们可以通过简单的设置来开启二级缓存。二级缓存的有效范围为一个 SqlSessionFactory 生命周期,绝大多数情况下,应用都会只有一个 SqlSessionFactory,因此我们可以把二级缓存理解为全局缓存。

3.1 全局可用

在 MyBatis 全局配置文件中,即 mybatis-config.xml 文件,二级缓存可由 settings 下的 cacheEnabled 属性开启。如下:

<settings>
  <setting name="cacheEnabled" value="true"/>
</settings>

当打开 cacheEnabled 属性后,二级缓存全局可用。

TIPS:注意,这里是可用,cacheEnabled 的默认值其实也是 true,即全局可用,由于二级缓存需要对 mapper 配置后才真正生效,简单来说就是双层开关。当将其设置为 false 后,则全局关闭,mapper 中即使配置了,二级缓存也会失效。

3.2 mapper 中开启

3.2.1 xml 开启

在二级缓存全局可用的情况下,mapper 才可通过 cache 配置开启二级缓存。如,在 UserMapper.xml 文件中开启二级缓存:

<cache/>

这种情况下,缓存的行为如下:

  • mapper 下的所有 select 语句会被缓存;
  • mapper 下的 update,insert,delete 语句会刷新缓存;
  • 使用 LRU 算法来回收对象;
  • 最大缓存 1024 个对象;
  • 缓存可读、可写。
  • 缓存不会根据时间来刷新。

cache 提供了诸多属性来修改缓存行为,示例如下:

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

这个例子下的缓存使用 FIFO 算法来回收对象,并每隔 60 秒刷新一次,最多缓存 512 个对象,且缓存只可读。

cache 有 4 个属性可配置,从而改变缓存的行为。

属性 描述
eviction 回收策略,默认 LRU,可选择的有 FIFO(先进先出),SOFT(软引用),WEAK(弱引用)
flushInterval 刷新时间
size 最多缓存对象数
readOnly 是否只读

3.2.2 注解开启

如果你不使用 mapper.xml 文件,也可以使用注解来开启。

如下:

package com.imooc.mybatis.mapper;

import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.cache.decorators.FifoCache;

@Mapper
@CacheNamespace(
  eviction = FifoCache.class,
  flushInterval = 60000,
  size = 512,
  readWrite = false
)
public interface BlogMapper {
}

注解 CacheNamespace 的配置与 xml 配置保持一致,唯一区别在于若使用注解,那么 eviction 属性需直接给出缓存实现类。

3.3 缓存共享

3.3.1 xml 共享

有时候,我们想在不同的 mapper 中共享缓存,为了解决这类问题,MyBatis 提供了 cache-ref 配置。

使用也很简单,如下:

<cache-ref namespace="com.imooc.mybatis.mapper.UserMapper"/>

mapper 由 namespace 来唯一标识,因此只需在另一个 mapper 文件中添加上 cache-ref 配置,并加上相应的 namespace 即可。

这样当前的 mapper 可以共享来自 UserMapper 的缓存。

3.3.2 注解共享

同样的,我们也可以使用注解来共享缓存。

如下:

@CacheNamespaceRef(UserMapper.class)
public interface BlogMapper {
}

这里,BlogMapper 共享了 UserMapper 的缓存。

TIPS: 注意,CacheNamespaceRef 与 CacheNamespace 不能共存,既然选择了共享就不能再独立开辟缓存区了。

4. 小结

  • MyBatis 的一级缓存默认可用,有效范围小,不会影响到其它会话,因此无特殊情况,不推荐丢弃一级缓存。
  • MyBatis 二级缓存默认使用程序内存缓存,但这显然不够安全,一般情况下我们都推荐使用 Redis 等专业的缓存。