缓存是指内存中的数据,常常来自于对数据库查询结果的缓存,为什么需要缓存?因为频繁的直接对数据库操作是很消耗系统资源的。我们将频繁需要获取的数据进行缓存,可以避免频繁的数据库交互,可以提高系统的响应速度。
mybatis对缓存的支持有一级缓存和二级缓存:
- 一级缓存是SqlSession级别,也就是在操作数据库的时候要构造SqlSeesion对象,对象中有一个数据结构(HashMap)用于存储数据,不同的SqlSession之间的缓存区域是互不影响的。
- 二级缓存为mapper级别缓存,多个SqlSession去操作同一个Mapper的sql语句,这个时候多个SqlSession是共享二级缓存的,二级缓存是跨SqlSession的。
先附上代码链接:https://gitee.com/wzlove521/lagou_study/tree/master/mybatis/mybatis_multitable
一级缓存
一级缓存的测试以及缓存失效,首先要确认一级缓存的存在,我们先配置mybatis的打印sql,之后进行查看结果会更清晰明了。在config的配置文件中添加
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
编写测试类进行测试:
public class CacheTest {
private UserMapper userMapper;
private SqlSession sqlSession;
@Before
public void createUserMapper() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapperConfig.xml");
sqlSession = new SqlSessionFactoryBuilder().build(resourceAsStream).openSession();
userMapper = sqlSession.getMapper(UserMapper.class);
}
@Test
public void testFirstLevelCache() {
User user = userMapper.selectById(2);
User user1 = userMapper.selectById(2);
System.out.println(user == user1);
}
}
debug测试可以看到在 testFirstLevelCache 中,执行第一个查询的时候会打开连接,然后查询数据库;第二次查询的时候是直接返回结果的,并且最后的输出为true。
缓存的生成就是这么一个过程,第一次查询,会去一级缓存中查询有没有缓存,没有去数据库查询,将查询结果进行缓存;如果存在缓存,则直接返回。
缓存失效
缓存失效或者说手动清除缓存,手动清除缓存直接使用 sqlSession.cleanCache(); 手动清除缓存。那什么情况下会自动清除缓存呢,那就是出现了增删改操作并进行了事务提交。如下
@Test
public void testFirstLevelCacheClean() {
User user = userMapper.selectById(2);
// sqlSession.clearCache();
User user2 = new User();
user.setId(1);
user.setName("王智");
userMapper.updateUser(user2);
sqlSession.commit();
User user1 = userMapper.selectById(2);
System.out.println(user == user1);
}
可以看到只要有增删改操作就会清除一级缓存,无论你的操作是否影响了当前缓存的数据,只要发生,就会清除,这样做的目的是避免脏读。
额外说一点,SqlSession中保存一级缓存使用的数据结构是HashMap,存在的键值对的值肯定是查询的结果,那键呢?键的组成比较复杂,包括下面几个部分:
- statementId:唯一标识:namespace + id
- params:参数
- boundSql:封装了当前执行的sql
- rowBounds:分页对象
源码分析
一级缓存是基于SqlSession的,所以源码的分析也从SqlSession开始。
这就是源码查找的整个流程图,下面展示一下比较重要大代码:
// 首先SqlSeesion的接口就不用看了,从实现类的代码看起
public class DefaultSqlSession implements SqlSession {
private final Executor executor;
@Override
public void clearCache() {
executor.clearLocalCache();
}
}
// Executor是执行器,实现类看BaseExecutor,该类实现了clearLocalCache方法
public abstract class BaseExecutor implements Executor {
protected PerpetualCache localCache;
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
}
// PerpetualCache就是最后一层了,可以看到具体的数据结构
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
@Override
public void clear() {
// 执行的就是HashMap的清空方法
cache.clear();
}
}
根据clean方法找到了整个线路,那具体的缓存的实现在哪一步呢?我们在执行查询的时候才会放缓存,所以缓存的实现肯定是放在执行器的步骤,接下来肯定看执行器的查询方法:
// 查看query方法
public abstract class BaseExecutor implements Executor {
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
}
// 看到生成了CacheKey,接下来看看CacheKey是如何生成的,包含了哪几个部分
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
// statementId = namespace.id
cacheKey.update(ms.getId());
// 分页数据
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// 执行的sql
cacheKey.update(boundSql.getSql());
// 存放参数
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176 当前使用的数据库的id
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
// 最后再看query方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
// query方法的操作就很熟悉,先查询缓存,缓存为空,查询数据库,在 queryFromDatabase 方法中查询库并进行了返回。
以上就是缓存的使用,至于清除缓存我们可以看到commit方法中调用了 clearLocalCache() 方法,所以只要进行了增删改触发了事务的提交,那就一定会清空缓存。
二级缓存
二级缓存是跨sqlSession的,是基于mapper接口的,准确的来说是基于namespace的,只要namespace相同,就共用一个二级缓存。二级缓存需要手动开启,开启的方式根据注解开发和xml开发是不同的。
注解开发,需要在接口上添加注解: @CacheNamespace
xml开发,需要在xml文件中添加标签cache:
其次,需要在配置文件Configuration中添加setting:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
成功开启二级缓存之后,就可以正常使用。原理同一级缓存相同,首先查询缓存,没有命中,查库,保存到缓存。
一级缓存和二级缓存的执行顺序
对于这里只能说我自己验证的结果,如果存在误差还请和我一起交流。
经过验证,实际的执行顺序是:二级缓存 - 一级缓存 - 数据库。操作流程是先查询二级缓存,没有查询到结果,会查询一级缓存,没有查询到结果会查询数据库。从数据库中查询到结果会放到一级缓存,二级缓存中是没有的,只有当前的sqlSession关闭或者commit之后,才会放到二级缓存,并且当数据存在在二级缓存中时,cleanCache是无法清空二级缓存的。
这里需要注意,也正是我开始了解缓存的时候所疑惑的地方。验证代码如下:
@Test
public void testSecondLevelCache() {
SqlSession sqlSession1 = build.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user = mapper1.findById(2);
// sqlSession1不关闭,sqlSession2无法命中缓存,
sqlSession1.close();
SqlSession sqlSession2 = build.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user1 = mapper2.findById(2);
System.out.println(user == user1);
// sqlSession2清空缓存只能清空自己的一级缓存
sqlSession2.clearCache();
SqlSession sqlSession3 = build.openSession();
UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
User user2 = mapper3.findById(2);
}
既然cleanCache无法清除二级缓存,那怎样清除呢?两个办法
- 配置入手,flushCache设置为true
- 进行增删改操作并进行了commit
测试
需要注意的是在进行二级缓存操作的时候,需要是pojo类实现Serializable接口,因为缓存可能存在在硬盘上。
准备工作做好之后编写测试类: ```java @Test public void testSecondLevelCache() { SqlSession sqlSession1 = build.openSession(); UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class); User user = mapper1.findById(2); // sqlSession1不关闭,sqlSession2无法命中缓存,那一级缓存和二级缓存的顺序是咋样的呢?是不是意味着一级缓存在关闭的时候会将数据放入二级缓存呢? // 研究一下 sqlSession1.close(); SqlSession sqlSession2 = build.openSession(); UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class); User user1 = mapper2.findById(2); System.out.println(user == user1); }
// 需要说明的是,运行完成之后可以看到 Cache Hit Ratio [com.wangzhi.mapper.UserMapper]: 0.5,说明二级缓存生效。 // 但是会发现最后的输出结果是false,也就说明了二级缓存存入的是数据,而不是对象,这也是为什么pojo要实现Serializable。
至于缓存失效也会有所不同,使用clearCache方法并不能清除二级缓存,需要SqlSession进行了增删改并提交了事务,或者通过配置 flushCache = true。
<a name="EaYDj"></a>
#### 其他配置
useCache和flushCache,这两个配置是用来给单独的方法或者标签使用的。
```java
// xml中使用
<select id="selectById" useCache="true" flushCache="true" resultMap="userMap" parameterType="int">
SELECT
user.id AS userId,
user.name AS userName,
orders.id AS orderId,
orders.name AS orderName,
orders.user_id AS orderUserId,
orders.create_time AS orderCreateTime
FROM user LEFT JOIN orders ON user.id = orders.user_id WHERE user.id = #{id}
</select>
// 注解使用
@Options(useCache = true, flushCache = Options.FlushCachePolicy.DEFAULT)
@Select("SELECT id, name FROM user WHERE id = #{id} ")
User findById(int id);
useCache = true表示是否从缓存中读取? true为是,false会直接查询数据库,跳过一级缓存和二级缓存。
flushCache为是否刷新缓存,一定要设置为true,不能设置为false,不然会出现脏读。注解使用的时候flushCache有三个可选值
- DEFAULT:默认,在进行查询的时候不会刷新缓存,进行增删改操作会刷新缓存。
- TRUE:进行任何操作都会户刷新缓存
- FALSE:不会刷新缓存
redis实现二级缓存
既然mybatis本身支持二级缓存,为什么还需要使用redis来进行实现呢?原因就在于分布式环境,mybatis自身的二级缓存并不能支持分布式环境,所以就需要使用支持分布式环境的数据库redis或者其它实现。mybatis提供了redis的实现,我们只需要使用对应的jar包就好。
引入依赖:
<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-redis -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
// 确认实现类,接口上注解实现
@CacheNamespace(implementation = RedisCache.class)
// xml标签实现
<cache type="org.mybatis.caches.redis.RedisCache"/>
源码查看
// 首先,如果不进行redis的配置文件的编写会默认连接本地的redis,默认的配置如下:
public final class Protocol {
private static final String ASK_RESPONSE = "ASK";
private static final String MOVED_RESPONSE = "MOVED";
private static final String CLUSTERDOWN_RESPONSE = "CLUSTERDOWN";
public static final String DEFAULT_HOST = "localhost";
public static final int DEFAULT_PORT = 6379;
public static final int DEFAULT_SENTINEL_PORT = 26379;
public static final int DEFAULT_TIMEOUT = 2000;
public static final int DEFAULT_DATABASE = 0;
......
}
// 其次,如果需要配置远端redis,就需要配置文件,为什么配置文件的名字一定要是redis.properties呢
final class RedisConfigurationBuilder {
/**
* This class instance.
*/
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";
......
}
// 接下来就看具体的实现
public final class RedisCache implements Cache {
// 读写锁
private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
// hset表示使用的数据结构是哈希
@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()));
}
});
}
}
基本上实现就是这样,如果需要自定义实现缓存,操作的流程就是实现Cache接口,完成实现方法的编写就好。