前言
本文章将介绍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缓存在业务上做单独的缓存处理(可能是我项目经验不够吧[奸笑])。但是总体来说,其实也是根据系统开发者的想法+业务本身的各种因素去决定的。合适才是最重要的。
前言中的问题,不知道大家是否已经能知道心里的答案呢?可以评论中写出来巩固自己的理解,也能帮助其他小伙伴哦[憨笑]
[玫瑰][玫瑰]?你的“关注”“收藏”是我最大的动力。非常感谢。[玫瑰][玫瑰]