Mybatis缓存
实际开发中我们都会对频繁的查询操作进行缓存,这样能有效的减少对数据的访问,减少数据库压力以及加快查询时间,Mybatis
为我们提供了一级缓存和二级缓存,接下来逐个介绍。
配置日志输出
在测试缓存效果之前,我们在上一篇博客【深入浅出Mybatis(一)】的基础上添加打印日志的相关配置。
- 在
pom.xml
中添加依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
- 在
resources
目录下创建log4j.properties
配置文件
log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
这里配置的比较简单,初衷也只是能打印sql即可。
一级缓存
缓存效果演示
这里我们在之前的项目基础上重新编写一个CacheTest.java
测试类
public class CacheTest {
private UserMapper userMapper;
private SqlSessionFactory sqlSessionFactory;
@Before
public void before() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
userMapper = sqlSession.getMapper(UserMapper.class);
}
@Test
public void firstLevelCache(){
//第一次查询
User user = userMapper.findById(1);
System.out.println(user);
//第二次查询
User user1 = userMapper.findById(1);
System.out.println(user1);
}
}
运行结果
我们一共进行了两次相同的查询操作,可是输出结果中只打印了一次查询sql语句,说明mybatis是默认存在缓存的,这就是Mybatis的一级缓存,当然这里还有一个前提,那就是两次查询必须在一个sqlSession中。
那什么是一个sqlSession呢,我们在上边测试类中再新增一个方法
@Test
public void firstLevelCache2(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
//第一次查询
User user = mapper1.findById(1);
System.out.println(user);
//第二次查询
User user1 = mapper2.findById(1);
System.out.println(user1);
}
运行结果
可以看出,我们还是执行的相同的查询操作,但是由于不是处于同一sqlSession
下,所以还是进行了两次数据库查询操作。
下边我们再编写一个测试方法,在同一sqlSession
下的两次相同查询操作的中间再进行一次更新操作
@Test
public void firstLevelCache3(){
//第一次查询
User user = userMapper.findById(1);
System.out.println(user);
//更新操作
User user2 = new User();
user2.setId(1);
user2.setUsername("test");
userMapper.update(user2);
//第二次查询
User user1 = userMapper.findById(1);
System.out.println(user1);
System.out.println(user == user1);
}
运行结果
可以看出由于进行了更新操作,所以第二次查询的时候还是进行了数据库查询而不是从缓存中读取的数据。
总结
Mybatis
的一级缓存是基于sqlSession
的并且是默认开启的,只有在同一sqlSession
下才会有缓存效果,第一次查询时,会首先去一级缓存中查询,如果有数据存在则直接返回结果,如果一级缓存中不存在则去数据库查询,然后将查询的结果返回并且放入一级缓存中- 如果在两次查询中间当前的
sqlSession
进行了commit
操作(插入,更新,删除),则会清空sqlSession
中的一级缓存,目的是保证缓存中的数据是真确的,防止脏读
图解
一级缓存源码深入解析
在这里我们首先提出几个问题,然后在再到源码中去寻找答案
- 一级缓存到底是什么
- 一级缓存什么时候被创建
- 一级缓存的工作流程是怎样的
首先,在上边我们一直强调一级缓存是基于同一SqlSession
的,所以我们直接进入SqlSession
这个接口中去寻找跟缓存cache
相关的方法或者属性,进入接口中,我们可以看到SqlSession
中定义了以下方法:
只有clearCache()
这个方法跟缓存有关系,所以问就以这个方法层层追踪,下面给出关键的追踪链:
最终在PerpetualCache.java
类中定位到clear()
方法:
@Override
public void clear() {
cache.clear();
}
发现关键代码为cache.clear()
,关键就是这个cache
是什么呢?,继续追踪到定义cache
的代码:
private Map<Object, Object> cache = new HashMap<Object, Object>();
可以看出cache
就是一个Map
,也就是说以及缓存本质就是Map
,每一个SqlSession
对象都会存放一个Map
,这就解决了我们的第一个疑问。
下面我们会思考那缓存是在什么地方什么时候创建的呢?这里不难想到,缓存是在我们执行sql
语句的前后创建,那么我们的Sql
语句执行是在哪里呢,毫无疑问是**Executor
**,而且刚刚我们清除缓存也是在Executor
中进行的,所以要解决第二个疑问,我们需要将重点放在Executor
接口中,我们来看看该接口都定义了那些方法:
能看到两个query()
方法,因为我们创建缓存肯定是在查询前后进行,所以我们直接关注重点,进入实现类BaseExecutor.java
的query()
方法中寻找答案:
但是上边的代码中我们并没有看到创建缓存的相关代码,只是了解了我们进行查询的时候以及缓存的工作流程那就是先从一级缓存中查询,如果有数据就直接返回如果没有数据就从数据库查询,解释了我们的第三个疑问,所以我们猜测创建缓存的时机就在数据库查询的方法中,我们继续查看queryFromDatabase()
这个方法的源码:
图中源码很清楚的为我们解答了一级缓存的创建时机,就是在数据库查询完成之后。 至此我们就分析完了关于一级缓存的三个疑问。
二级缓存
二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于sqlSession
的,而二级缓存是基于mapper
文件的namespace
的,也就
是说多个sqlSession
可以共享一个mapper
中的二级缓存区域,并且如果两个mapper
的namespace
相同,即使是两个mapper
,那么这两个mapper
中执行sql
查询到的数据也将存在相同的二级缓存区域中。
如何配置二级缓存
一级缓存是默认开启的,但是二级缓存需要我们手动开启。
- 首先在我们的全局配置文件
sqlMapConfig.xml
文件中添加以下代码:
<!-- 开启二级缓存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
- 在
UserMapper.xml
中开启缓存
<!--开启二级缓存-->
<cache></cache>
就这么一个空标签,其实这里可以配置,
PerpetualCache
这个类是mybatis
默认实现缓存功能的类。我们不写type
就使用mybatis
默认的缓存,也可以去实现Cache
接口来自定义缓存。
- 开启了二级缓存后,还需要将要缓存的
pojo
实现Serializable
接口,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。所以mybatis
中的pojo
都去实现Serializable
接口
public class User implements Serializable {
//省略
}
二级缓存演示
直接在我们之前的测试类CacheTest.java
中添加如下方法:
@Test
public void secondLevelCache(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
//第一次查询
User user = mapper1.findById(1);
System.out.println(user);
//关流,将数据存入二级缓存中,不然不会向二级缓存中存数据
sqlSession1.close();
//第二次查询
User user1 = mapper2.findById(1);
System.out.println(user1);
sqlSession2.close();
}
执行结果:
可以看出执行了只执行了一次查询操作,而且我们两次查询用的不是同一个SqlSession
,可以证明此处不是用的一级缓存,而是二级缓存。
我们再在两次查询操作中间新增修改操作
@Test
public void secondLevelCache(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
//第一次查询
User user = mapper1.findById(1);
System.out.println(user);
//关流,将数据存入二级缓存中,不然不会向二级缓存中存数据
sqlSession1.close();
//更新用户
User user2 = new User();
user2.setId(1);
user2.setUsername("eran");
mapper3.updateByAnnotation(user2);
sqlSession3.commit();
//第二次查询
User user1 = mapper2.findById(1);
System.out.println(user1);
sqlSession2.close();
}
执行结果:
可以看出第一次查询并没有执行数据库操作,那是因为我们之前已经将缓存的数据放在二级缓存中,然后进行了更新操作,第二次查询又进行了数据库查询操作,说明更新操作会清空二级缓存。
useCache和flushCache
mybatis
中还可以配置userCache
和flushCache
等配置项,userCache
是用来设置是否禁用二级缓存的,在statement
中设置useCache=false
可以禁用当前select
语句的二级缓存,即每次查询都会发出 sql
去查询,默认情况是true
。
<select id="findAll" resultType="User" useCache="false">
<include refid="selectUser"></include>
</select>
在mapper
的同一个namespace
中,如果有其它insert
、update
,delete
操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。设置statement
配置中的flushCache="true”
属性,默认情况下为true
,即刷新缓存,如果改成false
则 不会刷新,使用缓存时如果手动修改数据库表中的查询数据会出现脏读。
<update id="update" parameterType="User" flushCache="false">
update user
<trim prefix="set" suffixOverrides=",">
<if test="username != null">username = #{username},</if>
<if test="password != null">password = #{password},</if>
</trim>
where id = #{id}
</update>
使用redis实现二级缓存
上边介绍的是mybatis
自带的二级缓存,这种缓存机制我们使用的很少,而且只适合单体应用,不能满足分布式需求所以下面我们介绍使用redis
实现二级缓存。其实mybatis
提供了一个cache
接口,如果要实现我们自己的缓存逻辑,我们只需要实现这个接口就可以了,redis
实现二级缓存的原理就是这个。
- 首先在
pom.xml
中添加相关依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
UserMapper.xml
配置文件指定redis
缓存类
<cache type="org.mybatis.caches.redis.RedisCache"></cache>
这就替代了mybatis
自带的缓存。
- 新增
redis.properties
,配置redis
必须参数,这里我们配置最基础的就好
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=root
redis.database=0
测试和上边的二级缓存是一样的,这里就不做赘述了。
源码分析
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
的构造方法中,调用了 RedisConfigurationBuilder
来创建 RedisConfig
对象,并使用RedisConfig
来创建JedisPool
。
RedisConfig
类继承了 JedisPoolConfig
,并提供了host,port
等属性的包装,简单看一下RedisConfig
的属性,就是需要我们配置的一些基本参数。
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
文件,并将该配置文件中的内容设置到RedisConfig
对象中,并返回。
接下来,就是RedisCache
使用RedisConfig
类创建完成edisPool
;在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
和其他的序列化类差不多,负责 对象的序列化和反序列化;
说明
文章内容输出来源:拉勾教育Java高薪训练营课程归纳总结