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和其他的序列化类差不多,负责对象的序列化和反序列化。