二级缓存整合Redis

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

mybatis plus整合redis mybatis和redis整合_缓存

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

mybatis plus整合redis mybatis和redis整合_缓存_02


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

这里我们使用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和其它的序列化类差不多,负责对象的序列化和反序列化。