前言
本文章将介绍Mybatis中的一级缓存和二级缓存。主要是围绕着下列问题去展开
- 什么是一级缓存?
- 为什么使用一级缓存?
- 什么是二级缓存?
- 二级缓存的作用?
- 二级缓存是怎么实现的?
一级缓存
Mybatis会在会话对象SqlSession对象中建议一个简单的缓存,将查询到的结果缓存起来,当下次查询的时候,会判断有没有一个完全一样的查询,如果有直接从缓存中返回。否则查询数据库并存入缓存,最后返回给用户。
这样做主要是避免我们在短时间内,反复地执行相同的查询语句,而得到的结果也是相同的。如果不使用缓存会造成很大的资源浪费。
一级缓存的原理及源码分析
- 一级缓存默认就开启
- 一级缓存底层是使用HashMap的数据结构存储
- 缓存是在执行器Executor中创建,添加和删除缓存都在这里执行
- 默认的缓存Key有五个部分组成(具体可以查看类:BaseExecutor):
- statementId: MapStatement的id
- offset:分页设置,默认为0,从RowBound分页对象中获取
- limit:分页最大页数,从RowBound分页对象中获取
- boundSql:查询的sql语句,从BoundSql对象中获取
- value:sql的参数,parameter中获取
- env: 当前的环境,如果sqlMapConfig.xml有配置<environment id="development">, 也会设置到key中
缓存相关的部分源码展示:
创建缓存KEY的源码
CacheKey cacheKey = new CacheKey();
//MappedStatement 的 id 。id就是Sql语句的所在位置包名+类名+ SQL名称
cacheKey.update(ms.getId());
// offset 就是 0
cacheKey.update(rowBounds.getOffset());
// limit 就是 Integer.MAXVALUE
cacheKey.update(rowBounds.getLimit());
//具体的SQL语句
cacheKey.update(boundSql.getSql());
//后面是update 了 sql中带的参数
cacheKey.update(value);
//...省略一千万行代码
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
BaseExecutor部分query源码:
@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);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException{
//... 省略一千万行代码
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//处理存储过程的作用
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//如果查不到的话,就从数据库查 queryFromDatabase()
list = queryFromDatabase(ms, parameter,rowBounds,resultHandler,key,boundSql);
}
//... 省略一千万行代码
}
/**
* queryFromDatabase中,会对localcache进行写入。
* localcache对象的put方法最终交给Map进行存放
*/
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//执行查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//清除缓存中的数据
localCache.removeObject(key);
}
//把查询到的数据,重新放入一级缓存中
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
二级缓存
二级缓存的原理和一级缓存的原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取;
- 二级缓存需要手工开启;
- 二级缓存存储的不是对象,而是对象的数据;
- 因为二级缓存的存放介质很多,可以是内存,磁盘等。因为在取数据的时候需要反序列化操作,所以对象需要实现Serializable接口;
- 二级缓存是基于mapper文件的namespace。多个sqlSession可以共享一个mapper中的二级缓存区域。
开启默认二级缓存的配置
开启的方式有以下三种:
1. 在全局配置文件sqlMapConfig.xml添加以下配置开启
<!--开启二级缓存-->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
2. 在Mapper.xml中添加配置开启
<!--开启二级缓存-->
<cache></cache>
3. 在Mapper类中添加注解开启
//使用二级缓存,等用于<cache>属性
@CacheNamespace
public interface UserMapper{
}
使用Redis实现二级缓存
- 使用默认的二级缓存实现有哪些弊端?
- 使用缓存中间件实现二级缓存解决了哪些问题?
使用默认的二级缓存实现,只能在单机系统中使用,不能实现分布式缓存。
引入缓存中间件redis作为二级缓存的存储介质,可以实现多机分布式缓存数据存储,对缓存数据进行集中管理。
- 添加依赖。在pom.xml中添加以下依赖:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
- 在resources目录中添加配置 redis.properties
默认情况下,如果项目已经有使用了redis,且没有其他特殊配置,不配置redis.properties也是可以的。
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
- 指定使用RedisCache作为二级缓存
xml中配置
<cache type="org.mybatis.caches.redis.RedisCache" />
注解中配置
@CacheNamespace(implementation=RedisCache.class)
public interface UserMapper{
}
RedisCache源码分析
- 实现了Mybatis的Cache接口
- 内部使用jedis封装了对redis的CURD操作
- 使用了redis的Hash数据结构,系列化后存储数据
- RedisCache在mybatis启动的时候,由MyBatis的CacheBuilder创建
部分核心源码:
//实现了Mybatis的Cache接口
public final class RedisCache implements Cache {
public RedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require anID");
}
this.id = id;
//调用RedisConfigurationBuilder创建RedisConfig
RedisConfig redisConfig = RedisConfigurationBuilder.getInstance()
//RedisConfig核心方法,这个是加载解析redis配置文件的
.parseConfiguration();
pool = new JedisPool(redisConfig,redisConfig.getHost(),redisConfig.getPort(),redisConfig.getConnectionTimeout(),redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName());
}
...
}
/**
* 通过类加载器加载redis配置资源,
*/
public RedisConfig parseConfiguration(ClassLoader classLoader) {
Properties config = new Properties();
InputStream input = classLoader.getResourceAsStream(redisPropertiesFilename);
if (input != null) {
try {
//加载配置信息到Properties容器中
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继承了 JedisPoolConfig
RedisConfig jedisConfig = new RedisConfig();
//开始设置redis配置
setConfigProperties(config, jedisConfig);
return jedisConfig;
}
结语
实际情况中,我个人是比较少直接用到mybatis的二级缓存的。都是自己使用redis缓存在业务上做单独的缓存处理(可能是我项目经验不够吧[奸笑])。但是总体来说,其实也是根据系统开发者的想法+业务本身的各种因素去决定的。合适才是最重要的。
前言中的问题,不知道大家是否已经能知道心里的答案呢?可以评论中写出来巩固自己的理解,也能帮助其他小伙伴哦[憨笑]
[玫瑰][玫瑰]?你的“关注”“收藏”是我最大的动力。非常感谢。[玫瑰][玫瑰]