二级缓存整合Redis
上篇文章介绍了MyBatis自带的二级缓存,但是这个缓存是单服务器工作,无法实现分布式缓存。那么什么是分布式缓存呢?假设现在有两个服务器1和2,用户访问的时候访问了服务器1,查询后的缓存就会放在服务器1上,假设现在有个用户访问的是服务器2,那么他在服务器2上就无法获取刚刚的那个缓存,如下如所示:
为了解决这个问题,就得找一个分布式的缓存,专门用来存储缓存数据的,这样不同的服务器要缓存数据都往它那里存,取缓存数据也从它那里取,如下图所示:
如上图所示,在几个不同的服务器之间,我们使用第三方缓存框架,将缓存都放在这个第三方框架中,然后无论有多少台服务器,我们都能从缓存中获取数据。
这里我们使用MyBatis与Redis整合。
我们知道,MyBatis提供了一个cache接口,如果要实现自己的缓存逻辑,实现cache接口开发即可。MyBatis本身默认实现了一个,但是这个缓存的实现无法实现分布式缓存,所以我们要自己来实现。Redis分布式缓存就可以,Mybatis提供了一个针对cache接口的Redis实现类,该类存在mybatis-redis包中。
1)pom文件
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
2)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="cn.mybatisTest.mapper.UserMapper">
<!-- 二级缓存类地址 -->
<cache type="org.mybatis.caches.redis.RedisCache"/>
<select id="findUsers" resultType="com.test.pojo.User" useCache="true">
SELECT * FROM user
</select>
</mapper>
3)redis.properties(文件名称不能改,不然无法获取里面的数据)
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
4)测试
@Test
public void SecondLevelCache(){
//创建SqlSession1
SqlSession sqlSession1 = sqlSessionFactory.openSession();
IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
//创建SqlSession2
SqlSession sqlSession2 = sqlSessionFactory.openSession();
lUserMapper mapper2 = sqlSession2.getMapper(lUserMapper.class);
//创建SqlSession3
SqlSession sqlSession3 = sqlSessionFactory.openSession();
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);
}
5)源码分析
RedisCache和大家普遍实现Mybatis的缓存方案大同小异,无非是实现Cache接口,并使用Jedis操作缓存,不过该项目在设计细节上有一些区别。
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()
);
}
}
}
RedisCache在MyBatis启动的时候,由MyBatis的CacheBuilder创建,创建的方式很简单,就是调用RedisCache的带有String参数的构造方法,即RedisCache(String id);而在RedisCache的构造方法中,调用了RedisConfiguration来创建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;
public RedisConfig() {
}
}
RedisConfig对象是由RedisConfigurationBuilder创建的,简单看下这个类的主要方法:
final class RedisConfigurationBuilder {
private static final RedisConfigurationBuilder INSTANCE = new RedisConfigurationBuilder();
private static final String SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME = "redis.properties.filename";
private static final String REDIS_RESOURCE = "redis.properties";
private final String redisPropertiesFilename = System.getProperty("redis.properties.filename", "redis.properties");
private RedisConfigurationBuilder() {
}
public static RedisConfigurationBuilder getInstance() {
return INSTANCE;
}
public RedisConfig parseConfiguration() {
return this.parseConfiguration(this.getClass().getClassLoader());
}
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文件:
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
password= database=0 clientName=
并将该配置文件中内容设置到RedisConfig对象中,并返回;接下来,就是RedisCache使用RedisConfig类创建完成jedisPool;在RedisCache中实现了一个简单的模板方法,来操作Redis;
public final class RedisCache implements Cache {
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;
}
}
模板接口为RedisCallback,这个接口中就只需要实现一个doWithRedis方法而已;
public interface RedisCallback {
Object doWithRedis(Jedis jedis);
}
接下来看看Cache中最重要的两个方法:putObject和getObject,通过这两个方法来查看mybatis-redis储存数据的格式:
public final class RedisCache implements Cache {
@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和其它的序列化类差不多,负责对象的序列化和反序列化。