【来自拉钩java高薪训练营学习笔记】
什么是二级缓存?
二级缓存和一级缓存的原理是一样的,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于的sqlSession,而二级缓存是基于mapper文件的namespace的,也就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如何两个mapper的namespace相同,即使两个mapper,那这两个mapper中执行SQL查询到的数据也将存在相同的二级缓存区域中
- 如上图
sqlSession1
在查询时会从UserMapper的二级缓存
中取,如果没有则执行数据库查询操作。 - 然后写入到二级缓存中
-
sqlSession2
则执行同样的UserMapper
查询时,会从UserMapper的二级缓存
中取,此时的二级缓存中已经有内容了,所以就可以直接取到,不再与数据库交互。 -
sqlSession3
在执行事务操作(插入、更新、删除)时,会清空UserMapper
的二级缓存
1. 开启二级缓存
如何使用二级缓存:
mybatis中,一级缓存是默认开启的,但是二级缓存需要配置才可以使用
-
在全局配置文件
sqlMapConfig.xml
中加入如下代码:<!--开启二级缓存--> <settings> <setting name="cacheEnabled" value="true"/> </settings>
-
其次在哪个namespace中开启二级就在哪里配置,因为mybatis有注解和xml两种方式所以:
-
注解
注解扩展://我们默认使用的是mybatis自带的二级缓存,它的实现在PerpetualCache类中,所以可以写成 @CacheNamespace(implementation = PerpetualCache.class) //如果是使用redis作为二级缓存的话,下面第二部分会讲到
-
xml
这样就开启了UserMapper
的二级缓存
-
测试一:
注意:将缓存的pojo实现
Serializable
接口,为了将缓存数据取出执行反序列化操作,因为二级缓存的存储介质多种多样,不一定只在内存中,也可能在硬盘中,如果我们要再取出这个缓存的话,就需要反序列化了。所以mybatis的pojo都去实现Serializable
接口
最后执行看到打印日志:
为什么System.out.println(user1==user2)
为false ?二级缓存和一级缓存不同,二级缓存缓存的不是对象,而是数据,在第二次查询时底层重新创建了一个User对象,并且把二级缓存中的数据重新封装成了对象并返回。所以user1和user2不是一个对象。
-
测试二:
我们在测试二中进行一下事务操作,看看是否能清空二级缓存:
增加了一个修改操作,发现执行了两个select
,说明提交事务会刷新二级缓存
userCache和flushCache
还可以配置userCache
和flushCache
-
userCache : 是用来设置是否禁用二级缓存的,在statement中设置可以禁用当前select语句的二级缓存,即每次查询都会发出sql。默认情况为true.
-
flushCache : 在mapper的同一个namespace中,如果有其它的增删改操作后需要刷新缓存,如果部执行刷新缓存会出现脏读。
设置statement配置中的flushCache="true",即刷新缓存,如果改成false则不会刷新,有可能出现脏读。所以一般情况下没必要改
Mybatis二级缓存和一级缓存一样也是使用到了
org.apache.ibatis.cache.impl.PerpetualCache
这个类是mybatis的默认缓存类,同时,想要自定义缓存必须实现
cache
接口
2. 使用Redis实现二级缓存
Mybatis自带的二级缓存是有缺点的,就是这个缓存是单服务器进行工作的,无法实现分布式缓存。
所以为了解决这个问题,必须找一个分布式缓存专门存放缓存数据。
如何使用
mybatis提供了一个针对cache接口的redis实现类,在mybatis-redis
包中
-
首先我们引入jar包
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency>
-
//**********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"> //表示针对于当前的namespace开启二级缓存 <cache type="org.mybatis.caches.redis.RedisCache" /> <select id="findAll" resultType="com.lagou.pojo.User" useCache="true"> select * from user </select>
//*******注解方式********** @CacheNamespace(implementation = RedisCache .class) public interface UserMapper { //根据id查询用户 注解使用 @Select("select * from user where id=#{id}") public User findById(Integer id);
这个类同样实现了
Cache
接口 -
配置redis的配置文件
redis.host=localhost redis.port=6379 redis.connectionTimeout=5000 redis.password= redis.database=0
3. Redis二级缓存源码分析
RedisCache和Mybatis二级缓存的方案都差不多,无非是实现Cache接口,并使用jedis操作缓存,不过在设计细节上有点区别。
我们带着问题分析源码:
- 在RedisCache类中如何向redis中进行缓存值的存取 ?
- 使用了哪种数据结构 ?
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;
//首先其实现了Cache接口,被mybatis初始化的时候的CacheBuilder创建
//创建方式就是调用了下面的有参构造
public final class RedisCache implements Cache {
private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
private String id;
private static JedisPool pool;
//有参构造
public RedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
//RedisConfigurationBuilder调用parseConfiguration()方法创建RedisConfig对象
RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
//构建Jedis池
pool = new JedisPool(redisConfig,redisConfig.getHost(),redisConfig.getPort(),redisConfig.getConnectionTimeout(),redisConfig.getSoTimeout(),redisConfig.getPassword(),redisConfig.getDatabase(),redisConfig.getClientName());
}
//模板方法,下面的putObject和getObject、removeObject都会用到这个方法
private Object execute(RedisCallback callback) {
Jedis jedis = pool.getResource();
try {
return callback.doWithRedis(jedis);
} finally {
jedis.close();
}
}
//。。。。。。。。省略部分代码
@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()));
}
});
}
@Override
public Object removeObject(final Object key) {
return execute(new RedisCallback() {
@Override
public Object doWithRedis(Jedis jedis) {
return jedis.hdel(id.toString(),key.toString());
}
});
}
}
-
RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
RedisConfig
中封装了默认的Redis配置信息
这个方法读取了我们配置在/resource/redis.properties
这个文件
RedisConfig后构建了Jedis池 -
put方法
private Object execute(RedisCallback callback) { Jedis jedis = pool.getResource(); try { return callback.doWithRedis(jedis); } finally { jedis.close(); } } public void putObject(final Object key,SerializeUtil.serialize(value)); return null; } }); }
我们可以看到,put方法调用了模板方法得到 一个jedis链接,然后调用doWithRedis()方法
jedis.hset(id.toString().getBytes(),SerializeUtil.serialize(value));
可以很清楚的看到,mybatis-redis在存储数据的时候,是使用的hash结构,把cache的id作为这个hash的key (cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash的field,需要缓存的内容直接使用SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责对象的序列化和反序列化;