MyBatis缓存

MyBatis缓存分为一级缓存和二级缓存,我们这里分别看一下两者的区别以及如何使用。

一级缓存

一级缓存是基于sqlSession这个的,在同一个sqlSession执行两次,会在第一次执行后,缓存语句执行的结果,第二次执行的时候没有语句执行的过程,而是直接从缓存中拿结果。

//一级缓存基于sqlSession
@Test
public void testCacheLevel_1() throws IOException {
    //第一次查询,发出sql语句,并将查询出来的结果放进缓存中
    User user_1 = userMapper.findUserById(1);
    System.out.println(user_1);

    //第二次查询,由于是用一个sqlSesion,会在缓存中查询结果
    User user_2 = userMapper.findUserById(1);
    System.out.println(user_2);
}

二级缓存

二级缓存默认不是开启的,二级缓存是基于整个Mapper文件的namespace的,也就是同一个namespace,而且即使是两个文件mapper中的namespace,只要者两个namespace相同,这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中:
首先要在全局文件sqlMapConfig.xml中开启缓存,如下所示:

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

其次要在UserMapper.xml文件中开启缓存,如下所示:

<cache></cache>

这里看到UserMapper这里就是一个空标签,当它为空的时候,PerpetualCache就是myBatis默认实现缓存功能的类。当然我们也可以自定义缓存。

这里我们看一下PerpetualCache类的实现:

package org.apache.ibatis.cache.impl;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;

/**
 * 永不过期的 Cache 实现类,基于 HashMap 实现类
 *
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache {
    /**
     * 标识
     */
    private final String id;
    /**
     * 缓存容器
     */
    private Map<Object, Object> cache = new HashMap<>();

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

    @Override
    public String getId() {
        return id;
    }

    @Override
    public int getSize() {
        return cache.size();
    }

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

    @Override
    public Object getObject(Object key) {
        return cache.get(key);
    }

    @Override
    public Object removeObject(Object key) {
        return cache.remove(key);
    }

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

    @Override
    public ReadWriteLock getReadWriteLock() {
        return null;
    }

    @Override
    public boolean equals(Object o) {
        if (getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        }
        if (this == o) {
            return true;
        }
        if (!(o instanceof Cache)) {
            return false;
        }

        Cache otherCache = (Cache) o;
        return getId().equals(otherCache.getId());
    }

    @Override
    public int hashCode() {
        if (getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        }
        return getId().hashCode();
    }
}

二级缓存和Redis的整合

myBatis的二级缓存是单服务器工作,没法实现分布式缓存。解决这个问题,我们需要引入中间件对缓存数据进行集中管理,常见的有Redis,Memcached,ehcache等等,这里我们重点介绍mybatis和redis的整合。
主要有以下几步:

pom.xml加入依赖:
<dependency>
   <groupId>org.mybatis.caches</groupId>
   <artifactId>mybatis-redis</artifactId>
   <version>1.0.0-beta2</version>
</dependency>
Mapper.xml配置文件开启缓存:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lagou.mapper.IUserMapper">
    <cache type="org.mybatis.caches.redis.RedisCache" />
    <select id="findAll" resultType="com.david.pojo.User" useCache="true">
        select * from user
    </select>
</mapper>
redis.properties文件配置redis参数:
spring.redis.database=1
spring.redis.host=127.0.0.1
spring.redis.password=test123
spring.redis.pool.max-active=8
spring.redis.pool.max-idle=8
spring.redis.pool.max-wait=-1
spring.redis.pool.min-idle=0
spring.redis.port=6379
#spring.redis.sentinel.master= # Name of Redis server.
#spring.redis.sentinel.nodes= # Comma-separated list of host:port pairs.
spring.redis.timeout=5000
测试类实现:
@Test
public void SecondLevelCache(){
 SqlSession sqlSession1 = sqlSessionFactory.openSession();
 SqlSession sqlSession2 = sqlSessionFactory.openSession();
 SqlSession sqlSession3 = sqlSessionFactory.openSession();
 IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
 lUserMapper mapper2 = sqlSession2.getMapper(lUserMapper.class);
 lUserMapper mapper3 = sqlSession3.getMapper(IUserMapper.class);
 User user1 = mapper1.findUserById(1);
 sqlSession1.close(); //清空⼀级缓存
 
 User user = new User();
 user.setId(1);
 user.setUsername("lisi");
 mapper3.updateUser(user);
 sqlSession3.commit();
 User user2 = mapper2.findUserById(1);
 System.out.println(user1==user2);
}
源码分析

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());
}

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

public class RedisConfig extends JedisPoolConfig {
    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文件:

host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
password= database=0 clientName=

并将该配置文件中的内容设置到RedisConfig对象中,并返回;接下来,就是RedisCache使用RedisConfig类创建完成JedisPool;在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中最重要的两个方法:putObject和getObject,通过这两个方法来查看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结构,把cache的id作为这个hash的key (cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash的field,需要缓存的内容直接使⽤SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责对象的序列化和反序列化。

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

推荐阅读更多精彩内容