Mybatis 缓存

1. 一级缓存

1.1 一级缓存生效测试

  • 在一个 sqlSession 中,对 User 表根据 id 进行两次查询,查看他们发出 sql 语句的情况
package com.study.mapper;

import com.study.pojo.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 org.junit.Before;
import org.junit.Test;

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

/**
 * 缓存的测试
 * @author xiaosong
 * @since 2021/4/12
 */
public class CacheTest {

    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void before() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("sqlMapperConfig.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void testSelect(){
        //根据 sqlSessionFactory 产生 session
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        //第一次查询,发出sql语句,并将查询出来的结果放进缓存中
        User userFrist = userMapper.findUserById(1);
        System.out.println(userFrist);
        //第二次查询,由于是同一个sqlSession,会在缓存中查询结果
        //如果有,则直接从缓存中取出来,不和数据库进行交互
        User userSecond = userMapper.findUserById(1);
        System.out.println(userSecond);
    }
   
}

查看控制台打印情况:


  • 同样是对 user 表进行两次查询,只不过两次查询之间进行了一次 update 操作。
package com.study.mapper;

import com.study.pojo.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 org.junit.Before;
import org.junit.Test;

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

/**
 * 缓存的测试
 * @author xiaosong
 * @since 2021/4/12
 */
public class CacheTest {

    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void before() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("sqlMapperConfig.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void testSelectAndUpdate(){
        //根据 sqlSessionFactory 产生 session
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        //第一次查询,发出sql语句,并将查询出来的结果放进缓存中
        User userFrist = userMapper.findUserById(1);
        System.out.println(userFrist);
        //第二步进行了一次更新操作,sqlSession.commit()
        User user = new User();
        user.setId(1);
        user.setUsername("jack");
        userMapper.updateUser(user);
        //第二次查询,由于是同一个sqlSession.commit(),会清空缓存信息
        //则此次查询也会发出sql语句
        User userSecond = userMapper.findUserById(1);
        System.out.println(userSecond);
    }

}

查看控制台打印情况:


1.2 一级缓存总结

  1. 第一次发起查询用户 id1 的用户信息,先去找缓存中是否有 id1 的用户信息,如果没有,从 数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
  2. 如果中间 sqlSession 去执行 commit 操作(执行插入、更新、删除)或者手动刷新一级缓存 clearCache(),则会清空 sqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
  3. 第二次发起查询用户 id1 的用户信息,先去找缓存中是否有 id1 的用户信息,如果缓存中有,直接从缓存中获取用户信息

说明:一级缓存默认开启

1.3 一级缓存原理探究与源码分析

问题:一级缓存到底是什么?一级缓存什么时候被创建、一级缓存的工作流程是怎样的?

  • 上面我们一直提到一级缓存,那么提到一级缓存就绕不开 SqlSession,所以索性我们就直接从 SqlSession,看看有没有创建缓存或者与缓存有关的属性或者方法。

  • 调研了一圈,发现上述所有方法中,好像只有 clearCache() 和缓存沾点关系,那么就直接从这个方 法入手吧,分析源码时,我们要看它(此类)是谁,它的父类和子类分别又是谁,对如上关系了解了,你才会对这个类有更深的认识,分析了一圈,你可能会得到如下这个流程图。

  • 再深入分析,流程走到 Perpetualcache 中的 clear()方法之后,会调用其 cache.clear()方法,那么这个cache是什么东西呢?点进去发现,cache 其实就是 private Map cache = new HashMap();也就是一个Map,所以说 cache.clear() 其实就是 map.clear(),也就是说,缓存其实就是本地存放的一个 map 对象,每一个 SqISession 都会存放一个map 对象的引用,那么这个 cache 是何时创建的呢?

  • 你觉得最有可能创建缓存的地方是哪里呢?我觉得是 Executor,为什么这么认为?因为 Executor 是执行器,用来执行 SQL 请求,并且清除缓存的方法也在 Executor 中执行,所以很可能缓存的创建也很有可能在Executor 中,看了一圈发现 Executor 中有一个 createCacheKey⽅法,这个方法很像是创建缓存的方法啊,跟进去看看,会发现createCacheKey 方法是由 BaseExecutor 执行的,代码如下:

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    // 创建 CacheKey 对象
    CacheKey cacheKey = new CacheKey();
    // 设置 id、offset、limit、sql 到 CacheKey 对象中
    //MappedStatement 的 id
    // id 就是 Sql 语句的所在位置包名+类名+ SQL名称
    cacheKey.update(ms.getId());
    // offset 就是 0
    cacheKey.update(rowBounds.getOffset());
    // limit 就是 Integer.MAXVALUE
    cacheKey.update(rowBounds.getLimit());
    //具体的SQL语句
    cacheKey.update(boundSql.getSql());
    // 设置 ParameterMapping 数组的元素对应的每个 value 到 CacheKey 对象中
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic 这块逻辑,和 DefaultParameterHandler 获取 value 是一致的。
    for (ParameterMapping parameterMapping : parameterMappings) {
        if (parameterMapping.getMode() != ParameterMode.OUT) {
            Object value;
            String propertyName = parameterMapping.getProperty();
            if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
                value = null;
            } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
            }
            //后⾯是update 了 sql中带的参数
            cacheKey.update(value);
        }
    }
    // 设置 Environment.id 到 CacheKey 对象中
    if (configuration.getEnvironment() != null) {
        // issue #176
        cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
}
  • 创建缓存 key 会经过一系列的 update方法,update 方法由一个CacheKey 这个对象来执行的,这个 update 方法最终由 updateListlist 来把五个值存进去,对照上面的代码和下面的图示,你应该能理解
    这五个值都是什么了
  • 这里需要注意一下最后一个值,configuration.getEnvironment().getId()这是什么,这其实就是 定义在 mybatis-config.xml 中的标签,⻅如下。
<!--environments:运行环境-->
<environments default="dev">
    <environment id="dev">
        <!--当前事务交由JDBC进行管理-->
        <transactionManager type="JDBC"/>
        <!--当前使用mybatis提供的连接池-->
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
</environments>
  • 那么我们回归正题,那么创建完缓存之后该用在何处呢?总不会凭空创建一个缓存不使用吧?绝对不会的,经过我们对一级缓存的探究之后,我们发现一级缓存更多是用于查询操作,毕竟一级缓存也叫做查询缓存吧,为什么叫查询缓存我们一会儿说。我们先来看一下这个缓存到底用在哪了,我们跟踪到 query 方法如下:
//此方法在SimpleExecutor的父类BaseExecutor中实现
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //根据传入的参数动态获得SQL语句,最后返回用BoundSql对象表示
    BoundSql boundSql = ms.getBoundSql(parameter);
    //为本次查询创建缓存的Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 查询
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

调用重载的 query方法

@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    // 已经关闭,则抛出 ExecutorException 异常
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    // 清空本地缓存,如果 queryStack 为零,并且要求清空本地缓存。
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        // queryStack + 1
        queryStack++;
        // 从一级缓存中,获取查询结果
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        // 获取到,则进行处理
        if (list != null) {
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        // 获得不到,则从数据库中查询
        } else {
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        // queryStack - 1
        queryStack--;
    }
    if (queryStack == 0) {
        // 执行延迟加载
        for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        // issue #601
        // 清空 deferredLoads
        deferredLoads.clear();
        // 如果缓存级别是 LocalCacheScope.STATEMENT ,则进行清理
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482
            clearLocalCache();
        }
    }
    return list;
}

调用 queryFromDatabase 方法

// 从数据库中读取操作
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 在缓存中,添加占位对象。此处的占位符,和延迟加载有关,可见 `DeferredLoad#canLoad()` 方法
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 执行读操作
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        // 从缓存中,移除占位对象
        localCache.removeObject(key);
    }
    // 添加到缓存中
    localCache.putObject(key, list);
    // 暂时忽略,存储过程相关
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}
  • 如果查不到的话,就从数据库查,在 queryFromDatabase 中,会对 localcache 进行写入。 localcache对象的 put 方法最终交给Map 进行存放
/**
 * 缓存容器
 */
private Map<Object, Object> cache = new HashMap<>();

@Override
public void putObject(Object key, Object value) {
    cache.put(key, value);
}

2. 二级缓存

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

2.1 开启二级缓存

和一级缓存默认开启不一样,二级缓存需要我们手动开启,首先在全局配置文件 sqlMapConfig.xml 文件中加入如下代码:

<!--开启二级缓存-->
<settings>
    <setting name="cacheEnable" value="true"/>
</settings>

其次在 UserMapper.xml 文件中开启缓存,如果是注解的方式,在mapper 接口上添加 @CacheNamespace

<cache/>

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

public class PerpetualCache implements Cache {

  private final String id;

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

  public PerpetualCache(String id) {
    this.id = id;
  }
  ....

我们可以看到二级缓存底层还是 HashMap 结构

public class User implements Serializable(

    //用户ID
    private int id;
    
    //用户姓名
    private String username;
     
}

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

2.2 测试二级缓存

  • 测试二级缓存和 sqlSession 无关
@Test
public void testTwoCache(){
    //根据 sqlSessionFactory 产生 session
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();

    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

    //第一次查询,发出sql语句,并将查询的结过放入缓存中
    User user1 = userMapper1.selectUserById(1);
    System.out.println(user1);
    sqlSession1.close();//第一次查询完后关闭 sqlSession

    //第二次查询,即使sqlSession1已经关闭了,这次查询依然不发出sql语句
    User user2 = userMapper2.selectUserById(1);
    System.out.println(user2);
    sqlSession2.close();
}


可以看出上面两个不同的 sqlSession ,第一个关闭了,第二次查询依然不发出 sql 查询语句。

  • 测试执行 commit() 操作,二级缓存数据清空
@Test
public void testTwoCacheCommit(){
    //根据 sqlSessionFactory 产生 session
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    SqlSession sqlSession3 = sqlSessionFactory.openSession();

    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);

    //第一次查询,发出sql语句,并将查询的结过放入缓存中
    User user1 = userMapper1.selectUserById(1);
    System.out.println(user1);
    sqlSession1.close();//第一次查询完后关闭 sqlSession

    //执⾏更新操作,commit()
    user1.setUsername("Eric");
    int count = userMapper3.updateUser(user1);
    System.out.println(count);
    sqlSession3.clearCache();

    //第二次查询,由于上次更新操作,缓存数据已经清空(防⽌数据脏读),这里必须再次发出sql语句
    User user2 = userMapper2.selectUserById(1);
    System.out.println(user2);
    sqlSession2.close();
}

查看控制台情况:


2.3 useCache 和 flushCache

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

<select id="selectUserById" resultType="com.study.pojo.User" useCache="false">
    SELECT * FROM user WHERE id = #{id}
</select>
@Select("SELECT * FROM user")
@Options(useCache = true)
List<User> findUserList();

这种情况是针对每次查询都需要最新的数据 sql ,要设置成useCache=false ,禁用二级缓存,直接从数据库中获取。

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

<select id="selectUserById" resultType="com.study.pojo.User" flushCache="true" useCache="false">
    SELECT * FROM user WHERE id = #{id}
</select>

⼀般情况下,执行完 commit 操作都需要刷新缓存,flushCache=true 表示刷新缓存,这样可以避免数据库脏读。所以我们不用设置,默认即可。

3. 二级缓存整合redis

上面我们介绍了 mybatis 自带的二级缓存,但是这个缓存是单服务器工作,无法实现分布式缓存。 那么什么是分布式缓存呢?假设现在有两个服务器1和2,用户访问的时候访问了 1服务器,查询后的缓 存就会放在1服务器上,假设现在有个用户访问的是2服务器,那么他在2服务器上就无法获取刚刚那个缓存,如下图所示:

为了解决这个问题,就得找一个分布式的缓存,专门用来存储缓存数据的,这样不同的服务器要缓存数据都往它那里存,取缓存数据也从它那里取,如下图所示:


如上图所示,在几个不同的服务器之间,我们使用第三方缓存框架,将缓存都放在这个第三方框架中,然后无论有多少台服务器,我们都能从缓存中获取数据。

这里我们介绍 mybatisredis 的整合。

刚刚提到过,mybatis 提供了⼀个 cache 接口,如果要实现自己的缓存逻辑,实现 cache 接口开发即可。

mybatis本身默认实现了⼀个,但是这个缓存的实现无法实现分布式缓存,所以我们要自己来实现。redis 分布式缓存就可以,mybatis 提供了⼀个针对 cache 接口的 redis 实现类,该类存在 mybatis-redis包中。

3.1 实现

  • pom 文件
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>
  • 映射配置文件或者注解
@CacheNamespace(implementation = RedisCache.class)
<cache type="org.mybatis.caches.redis.RedisCache"/>
  • redis.properties
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
  • 测试
@Test
public void testTwoCacheCommit(){
    //根据 sqlSessionFactory 产生 session
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    SqlSession sqlSession3 = sqlSessionFactory.openSession();

    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);

    //第一次查询,发出sql语句,并将查询的结过放入缓存中
    User user1 = userMapper1.selectUserById(1);
    System.out.println(user1);
    sqlSession1.close();//第一次查询完后关闭 sqlSession

    //执⾏更新操作,commit()
    user1.setUsername("Eric");
    int count = userMapper3.updateUser(user1);
    System.out.println(count);
    sqlSession3.clearCache();

    //第二次查询,由于上次更新操作,缓存数据已经清空(防⽌数据脏读),这里必须再次发出sql语句
    User user2 = userMapper2.selectUserById(1);
    System.out.println(user2);
    sqlSession2.close();

    System.out.println(user1 == user2);
}

说明:二级缓存存储的不是对象,而是数据,所以user1 == user2false

3.2 源码分析

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

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.mybatis.caches.redis;

import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public final class RedisCache implements Cache {
    private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
    private String id;
    private static JedisPool pool;

    public RedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        } else {
            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());
        }
    }

    private Object execute(RedisCallback callback) {
        Jedis jedis = pool.getResource();

        Object var3;
        try {
            var3 = callback.doWithRedis(jedis);
        } finally {
            jedis.close();
        }

        return var3;
    }

    public String getId() {
        return this.id;
    }

    public int getSize() {
        return (Integer)this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                Map<byte[], byte[]> result = jedis.hgetAll(RedisCache.this.id.toString().getBytes());
                return result.size();
            }
        });
    }

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

    public Object getObject(final Object key) {
        return this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                return SerializeUtil.unserialize(jedis.hget(RedisCache.this.id.toString().getBytes(), key.toString().getBytes()));
            }
        });
    }

    public Object removeObject(final Object key) {
        return this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                return jedis.hdel(RedisCache.this.id.toString(), new String[]{key.toString()});
            }
        });
    }

    public void clear() {
        this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                jedis.del(RedisCache.this.id.toString());
                return null;
            }
        });
    }

    public ReadWriteLock getReadWriteLock() {
        return this.readWriteLock;
    }

    public String toString() {
        return "Redis {" + this.id + "}";
    }
}

RedisCachemybatis 启动的时候,由 MyBatisCacheBuilder 创建,创建的方式很简单,就是调用RedisCache 的带有 String 参数的构造方法,即RedisCache(String id);而在 RedisCache 的构造方法中,调用了 RedisConfigurationBuilder 来创建 RedisConfig 对象,并使用 RedisConfig 来创建JedisPool
RedisConfig 类继承了 JedisPoolConfig,并提供了 host,port 等属性的包装,简单看⼀下 RedisConfig 的属性:

public class RedisConfig extends JedisPoolConfig {
    private String host = "localhost";
    private int port = 6379;
    private int connectionTimeout = 2000;
    private int soTimeout = 2000;
    private String password;
    private int database = 0;
    private String clientName;

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

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

        }
    }

    RedisConfig jedisConfig = new RedisConfig();
    this.setConfigProperties(config, jedisConfig);
    return jedisConfig;
}

核心的方法就是 parseConfiguration 方法,该方法从 classpath 中读取⼀个 redis.properties 文件:

redis.host=127.0.0.1
redis.port=6379
redis.password=
redis.database=0
redis.connectionTimeout=5000

并将该配置文件中的内容设置到 RedisConfig 对象中,并返回;接下来,就是 RedisCache 使用 RedisConfig 类创建完成JedisPool;在 RedisCache 中实现了一个简单的模板方法,用来操作 Redis

private Object execute(RedisCallback callback) {
    Jedis jedis = pool.getResource();

    Object var3;
    try {
        var3 = callback.doWithRedis(jedis);
    } finally {
        jedis.close();
    }

    return var3;
}

模板接口为RedisCallback,这个接口中就只需要实现了⼀个doWithRedis方法而已:

package org.mybatis.caches.redis;

import redis.clients.jedis.Jedis;

public interface RedisCallback {

    Object doWithRedis(Jedis jedis);
}

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

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

public Object getObject(final Object key) {
    return this.execute(new RedisCallback() {
        public Object doWithRedis(Jedis jedis) {
            return SerializeUtil.unserialize(jedis.hget(RedisCache.this.id.toString().getBytes(), key.toString().getBytes()));
        }
    });
}

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

4. 案例代码

详情参考https://gitee.com/xiaosonglab/mybatis-cache.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 关于 Mybatis 缓存的那点事儿,你知道吗? 缓存实现的方式 一级缓存 二级缓存 案例实操 1. 一级缓存 基...
    冰岛暖男的春天阅读 98评论 0 0
  • 像大多数的持久化框架一样,Mybatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。 缓...
    飞扬code阅读 125评论 0 1
  • mybatis缓存 像大多数的持久化框架一样,Mybatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数...
    哈哈大圣阅读 534评论 0 6
  • mybatis缓存机制 简介: mybatis提供查询缓存,用于减轻数据库压力,提高数据库性能 mybatis提供...
    蓝色Hippie阅读 145评论 0 0
  • Mybatis缓存 像大多数的持久化框架一样,Mybatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数...
    一只程序汪阅读 200评论 0 1