Mybatis一级缓存和二级缓存原理与使用(mybatis的一级缓存)

前言

本文章将介绍Mybatis中的一级缓存和二级缓存。主要是围绕着下列问题去展开

  • 什么是一级缓存?
  • 为什么使用一级缓存?
  • 什么是二级缓存?
  • 二级缓存的作用?
  • 二级缓存是怎么实现的?

#java##数据库##开发#

一级缓存

Mybatis会在会话对象SqlSession对象中建议一个简单的缓存,将查询到的结果缓存起来,当下次查询的时候,会判断有没有一个完全一样的查询,如果有直接从缓存中返回。否则查询数据库并存入缓存,最后返回给用户。

这样做主要是避免我们在短时间内,反复地执行相同的查询语句,而得到的结果也是相同的。如果不使用缓存会造成很大的资源浪费。

一级缓存的原理及源码分析

  • 一级缓存默认就开启
  • 一级缓存底层是使用HashMap的数据结构存储
  • 缓存是在执行器Executor中创建,添加和删除缓存都在这里执行
  • 默认的缓存Key有五个部分组成(具体可以查看类:BaseExecutor):
  1. statementId: MapStatement的id
  2. offset:分页设置,默认为0,从RowBound分页对象中获取
  3. limit:分页最大页数,从RowBound分页对象中获取
  4. boundSql:查询的sql语句,从BoundSql对象中获取
  5. value:sql的参数,parameter中获取
  6. 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实现二级缓存

  1. 使用默认的二级缓存实现有哪些弊端?
  2. 使用缓存中间件实现二级缓存解决了哪些问题?

使用默认的二级缓存实现,只能在单机系统中使用,不能实现分布式缓存。

引入缓存中间件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源码分析

  1. 实现了Mybatis的Cache接口
  2. 内部使用jedis封装了对redis的CURD操作
  3. 使用了redis的Hash数据结构,系列化后存储数据
  4. 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缓存在业务上做单独的缓存处理(可能是我项目经验不够吧[奸笑])。但是总体来说,其实也是根据系统开发者的想法+业务本身的各种因素去决定的。合适才是最重要的。

前言中的问题,不知道大家是否已经能知道心里的答案呢?可以评论中写出来巩固自己的理解,也能帮助其他小伙伴哦[憨笑]

[玫瑰][玫瑰]?你的“关注”“收藏”是我最大的动力。非常感谢。[玫瑰][玫瑰]

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注