深入浅出Mybatis(二)Mybatis一二级缓存机制

224 阅读10分钟

Mybatis缓存

实际开发中我们都会对频繁的查询操作进行缓存,这样能有效的减少对数据的访问,减少数据库压力以及加快查询时间,Mybatis为我们提供了一级缓存和二级缓存,接下来逐个介绍。

配置日志输出

在测试缓存效果之前,我们在上一篇博客【深入浅出Mybatis(一)】的基础上添加打印日志的相关配置。

  1. pom.xml中添加依赖
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.12</version>
</dependency>
  1. resources目录下创建log4j.properties配置文件
log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

这里配置的比较简单,初衷也只是能打印sql即可。

一级缓存

缓存效果演示

这里我们在之前的项目基础上重新编写一个CacheTest.java测试类

public class CacheTest {
    private UserMapper userMapper;
    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void before() throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        userMapper = sqlSession.getMapper(UserMapper.class);
    }

    @Test
    public void firstLevelCache(){
        //第一次查询
        User user = userMapper.findById(1);
        System.out.println(user);
        //第二次查询
        User user1 = userMapper.findById(1);
        System.out.println(user1);
    }
}

运行结果

在这里插入图片描述

我们一共进行了两次相同的查询操作,可是输出结果中只打印了一次查询sql语句,说明mybatis是默认存在缓存的,这就是Mybatis的一级缓存,当然这里还有一个前提,那就是两次查询必须在一个sqlSession中。

那什么是一个sqlSession呢,我们在上边测试类中再新增一个方法

@Test
public void firstLevelCache2(){
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();

    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
    //第一次查询
    User user = mapper1.findById(1);
    System.out.println(user);
    //第二次查询
    User user1 = mapper2.findById(1);
    System.out.println(user1);
}

运行结果

在这里插入图片描述

可以看出,我们还是执行的相同的查询操作,但是由于不是处于同一sqlSession下,所以还是进行了两次数据库查询操作。

下边我们再编写一个测试方法,在同一sqlSession下的两次相同查询操作的中间再进行一次更新操作

@Test
public void firstLevelCache3(){
    //第一次查询
    User user = userMapper.findById(1);
    System.out.println(user);
    //更新操作
    User user2 = new User();
    user2.setId(1);
    user2.setUsername("test");
    userMapper.update(user2);
    //第二次查询
    User user1 = userMapper.findById(1);
    System.out.println(user1);
    System.out.println(user == user1);
}

运行结果

在这里插入图片描述

可以看出由于进行了更新操作,所以第二次查询的时候还是进行了数据库查询而不是从缓存中读取的数据。

总结

  1. Mybatis的一级缓存是基于sqlSession的并且是默认开启的,只有在同一sqlSession下才会有缓存效果,第一次查询时,会首先去一级缓存中查询,如果有数据存在则直接返回结果,如果一级缓存中不存在则去数据库查询,然后将查询的结果返回并且放入一级缓存中
  2. 如果在两次查询中间当前的sqlSession进行了commit操作(插入,更新,删除),则会清空sqlSession中的一级缓存,目的是保证缓存中的数据是真确的,防止脏读

图解

在这里插入图片描述

一级缓存源码深入解析

在这里我们首先提出几个问题,然后在再到源码中去寻找答案

  1. 一级缓存到底是什么
  2. 一级缓存什么时候被创建
  3. 一级缓存的工作流程是怎样的

首先,在上边我们一直强调一级缓存是基于同一SqlSession的,所以我们直接进入SqlSession这个接口中去寻找跟缓存cache相关的方法或者属性,进入接口中,我们可以看到SqlSession中定义了以下方法:

在这里插入图片描述

只有clearCache()这个方法跟缓存有关系,所以问就以这个方法层层追踪,下面给出关键的追踪链: 在这里插入图片描述

最终在PerpetualCache.java类中定位到clear()方法:

 @Override
  public void clear() {
    cache.clear();
  }

发现关键代码为cache.clear(),关键就是这个cache是什么呢?,继续追踪到定义cache的代码:

private Map<Object, Object> cache = new HashMap<Object, Object>();

可以看出cache就是一个Map,也就是说以及缓存本质就是Map,每一个SqlSession对象都会存放一个Map,这就解决了我们的第一个疑问。 下面我们会思考那缓存是在什么地方什么时候创建的呢?这里不难想到,缓存是在我们执行sql语句的前后创建,那么我们的Sql语句执行是在哪里呢,毫无疑问是**Executor**,而且刚刚我们清除缓存也是在Executor中进行的,所以要解决第二个疑问,我们需要将重点放在Executor接口中,我们来看看该接口都定义了那些方法:

在这里插入图片描述

能看到两个query()方法,因为我们创建缓存肯定是在查询前后进行,所以我们直接关注重点,进入实现类BaseExecutor.javaquery()方法中寻找答案:

在这里插入图片描述

但是上边的代码中我们并没有看到创建缓存的相关代码,只是了解了我们进行查询的时候以及缓存的工作流程那就是先从一级缓存中查询,如果有数据就直接返回如果没有数据就从数据库查询,解释了我们的第三个疑问,所以我们猜测创建缓存的时机就在数据库查询的方法中,我们继续查看queryFromDatabase()这个方法的源码:

在这里插入图片描述

图中源码很清楚的为我们解答了一级缓存的创建时机,就是在数据库查询完成之后。 至此我们就分析完了关于一级缓存的三个疑问。

二级缓存

二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于sqlSession的,而二级缓存是基于mapper文件的namespace的,也就 是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mappernamespace相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。

在这里插入图片描述

如何配置二级缓存

一级缓存是默认开启的,但是二级缓存需要我们手动开启。

  1. 首先在我们的全局配置文件sqlMapConfig.xml文件中添加以下代码:
<!--  开启二级缓存  -->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>
  1. UserMapper.xml中开启缓存
<!--开启二级缓存-->
<cache></cache>

就这么一个空标签,其实这里可以配置,PerpetualCache这个类是mybatis默认实现缓存功能的类。我们不写type就使用mybatis默认的缓存,也可以去实现Cache接口来自定义缓存。

  1. 开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接口
public class User implements Serializable {
    //省略
}

二级缓存演示

直接在我们之前的测试类CacheTest.java中添加如下方法:

@Test
public void secondLevelCache(){
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    SqlSession sqlSession3 = sqlSessionFactory.openSession();

    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
    UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
    //第一次查询
    User user = mapper1.findById(1);
    System.out.println(user);
    //关流,将数据存入二级缓存中,不然不会向二级缓存中存数据
    sqlSession1.close();

    //第二次查询
    User user1 = mapper2.findById(1);
    System.out.println(user1);
    sqlSession2.close();
}

执行结果:

在这里插入图片描述

可以看出执行了只执行了一次查询操作,而且我们两次查询用的不是同一个SqlSession,可以证明此处不是用的一级缓存,而是二级缓存。

我们再在两次查询操作中间新增修改操作

@Test
public void secondLevelCache(){
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    SqlSession sqlSession3 = sqlSessionFactory.openSession();

    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
    UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
    //第一次查询
    User user = mapper1.findById(1);
    System.out.println(user);
    //关流,将数据存入二级缓存中,不然不会向二级缓存中存数据
    sqlSession1.close();
    //更新用户
    User user2 = new User();
    user2.setId(1);
    user2.setUsername("eran");
    mapper3.updateByAnnotation(user2);
    sqlSession3.commit();
    //第二次查询
    User user1 = mapper2.findById(1);
    System.out.println(user1);
    sqlSession2.close();
}

执行结果:

在这里插入图片描述

可以看出第一次查询并没有执行数据库操作,那是因为我们之前已经将缓存的数据放在二级缓存中,然后进行了更新操作,第二次查询又进行了数据库查询操作,说明更新操作会清空二级缓存。

useCache和flushCache

mybatis中还可以配置userCacheflushCache等配置项,userCache是用来设置是否禁用二级缓存的,在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出 sql去查询,默认情况是true

<select id="findAll" resultType="User" useCache="false">
    <include refid="selectUser"></include>
</select>

mapper的同一个namespace中,如果有其它insertupdate,delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则 不会刷新,使用缓存时如果手动修改数据库表中的查询数据会出现脏读。

<update id="update" parameterType="User" flushCache="false">
    update user
    <trim prefix="set" suffixOverrides=",">
        <if test="username != null">username = #{username},</if>
        <if test="password != null">password = #{password},</if>
    </trim>
    where id = #{id}
</update>

使用redis实现二级缓存

上边介绍的是mybatis自带的二级缓存,这种缓存机制我们使用的很少,而且只适合单体应用,不能满足分布式需求所以下面我们介绍使用redis实现二级缓存。其实mybatis提供了一个cache接口,如果要实现我们自己的缓存逻辑,我们只需要实现这个接口就可以了,redis实现二级缓存的原理就是这个。

  1. 首先在pom.xml中添加相关依赖
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>
  1. UserMapper.xml配置文件指定redis缓存类
<cache type="org.mybatis.caches.redis.RedisCache"></cache>

这就替代了mybatis自带的缓存。

  1. 新增redis.properties,配置redis必须参数,这里我们配置最基础的就好
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=root
redis.database=0

测试和上边的二级缓存是一样的,这里就不做赘述了。

源码分析

RedisCache和大家普遍实现Mybatis的缓存方案大同小异,无非是实现Cache接口,并使用jedis操作缓存;不过该项目在设计细节上有一些区别

public final class RedisCache implements Cache {
    public RedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require anID");
        }
        this.id = id;
        RedisConfig redisConfig =
        RedisConfigurationBuilder.getInstance().parseConfiguration();
        pool = new JedisPool(redisConfig, redisConfig.getHost(),
        redisConfig.getPort(),
        redisConfig.getConnectionTimeout(),
        redisConfig.getSoTimeout(), redisConfig.getPassword(),
        redisConfig.getDatabase(), redisConfig.getClientName());
    }
}

RedisCachemybatis启动的时候,由MyBatisCacheBuilder创建,创建的方式很简单,就是调用RedisCache的带有String参数的构造方法,即RedisCache(String id);而在RedisCache的构造方法中,调用了 RedisConfigurationBuilder 来创建 RedisConfig 对象,并使用RedisConfig 来创建JedisPool

RedisConfig类继承了 JedisPoolConfig,并提供了host,port等属性的包装,简单看一下RedisConfig的属性,就是需要我们配置的一些基本参数。

private String host = Protocol.DEFAULT_HOST;
private int port = Protocol.DEFAULT_PORT;
private int connectionTimeout = Protocol.DEFAULT_TIMEOUT;
private int soTimeout = Protocol.DEFAULT_TIMEOUT;
private String password;
private int database = Protocol.DEFAULT_DATABASE;
private String clientName;

RedisConfig对象是由RedisConfigurationBuilder创建的,简单看下这个类的主要方法:

public RedisConfig parseConfiguration(ClassLoader classLoader) {
    Properties config = new Properties();
    InputStream input = classLoader.getResourceAsStream(redisPropertiesFilename);
    if (input != null) {
        try {
            config.load(input);
        } catch (IOException e) {
            throw new RuntimeException("An error occurred while reading classpath property '"
                            + redisPropertiesFilename
                            + "', see nested exceptions", e);
        } finally {
            try {
                input.close();
            } catch (IOException e) {
                // close quietly
            }
        }
    }
    RedisConfig jedisConfig = new RedisConfig();
    setConfigProperties(config, jedisConfig);
    return jedisConfig;
}

核心的方法就是parseConfiguration方法,该方法从classpath中读取一个redis.properties文件,并将该配置文件中的内容设置到RedisConfig对象中,并返回。

接下来,就是RedisCache使用RedisConfig类创建完成edisPool;在RedisCache中实现了一个简单的模板方法,用来操作Redis

private Object execute(RedisCallback callback) {
    Jedis jedis = pool.getResource();
    try {
        return callback.doWithRedis(jedis);
    } finally {
        jedis.close();
    }
}

模板接口为RedisCallback,这个接口中就定义一个doWithRedis方法:

public interface RedisCallback {
    Object doWithRedis(Jedis jedis);
}

接下来看看Cache中最重要的两个方法:putObjectgetObject,通过这两个方法来查看mybatis-redis储存数据的格式:

@Override
public void putObject(final Object key, final Object value) {
    execute(new RedisCallback() {
        @Override
        public Object doWithRedis(Jedis jedis) {
            jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
            return null;
        }
    });
}
@Override
public Object getObject(final Object key) {
    return execute(new RedisCallback() {
           @Override
           public Object doWithRedis(Jedis jedis) {
               return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(),key.toString().getBytes()));
           }
        }
    );
}

可以很清楚的看到,mybatis-redis在存储数据的时候,是使用的hash结构,把cacheid作为这个hashkey(cacheidmybatis中就是mappernamespace);这个mapper中的查询缓存数据作为 hashfield,需要缓存的内容直接使用SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责 对象的序列化和反序列化;

说明

文章内容输出来源:拉勾教育Java高薪训练营课程归纳总结