MyBatis源码通~缓存Cache

Cache缓存原理

Mybatis提供一级缓存和二级缓存,一级缓存即缓存在内存中的,二级缓存则是利用第三方缓存工具来缓存数据。对应类包括BaseExecutorCachingExecutorCache接口实现类

1、缓存实现类 implement Cache

在这里插入图片描述

  1. Mybatis提供了非常多的缓存实现类,有最基本的PerpetualCache实现类、实现LRU策略的LruCache、可保证线程安全的缓存 SynchronizedCache 和具备阻塞功能的缓存 BlockingCache等。
  2. Cache的实现类中,除PerpetualCache算作是具体的缓存实现类外,其他的都算是缓存实现类的装饰类。因为每个类中都有一个Cache delegate来实现装饰增强功能。

1.1、PerpetualCache

内部用一个Map来实现数据的缓存。Mybatis的一级缓存则是用该实现类来缓存数据的。

private Map<Object, Object> cache = new HashMap<>();

public void putObject(Object key, Object value) {
    cache.put(key, value);
}

public Object getObject(Object key) {
    return cache.get(key);
}

public void clear() {
    cache.clear();
}

1.2、LruCache

顾名思义,是一种具有 LRU 策略的缓存实现类,即最近最少使用的缓存会在当缓存空间满的时候,将最历史缓存中去掉。

//--☆☆-- LruCache
public class LruCache implements Cache {
  //NOTE: 被装饰缓存类
  private final Cache delegate;

  //NOTE: 记录缓存的key,作用是:在调用getObject时触发调整LinkedHashMap节点的顺序。
  private Map<Object, Object> keyMap;

  //NOTE: 应该被剔除的缓存key
  private Object eldestKey;

  /**
   * 默认缓存大小为1024
   * @param delegate
   */
  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }
  public void setSize(final int size) {
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      //NOTE: 该方法会在调用Map的get方法时被调用
      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        //NOTE: 若当前put数据后,大小大于默认值,则记录需要被删除的key,在下次put数据时会删掉这个数据
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    //NOTE: 删除需要删除的缓存节点
    cycleKeyList(key);
  }

  public Object getObject(Object key) {
    //NOTE: keyMap.get(key),目的是刷新 key 对应的键值对在 LinkedHashMap 的位置
    keyMap.get(key);
    return delegate.getObject(key);
  }
  //
  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }
}
  1. LruCache实现的关键在于 keyMap,其使用LinkedHashMap,并覆盖removeEldestEntry 方法。LinkedHashMap可以保证插入的键值对的顺序,当插入一个新的键值对时,LinkedHashMap内部会调整尾节点tail,head则是第一个插入的键值对,也就是最久没有被访问的节点。但默认情况下LinkedHashMap只会按照插入的顺序来维护键值对的顺序,所以为了实现最近使用的顺序来维护键值对,则需要设置accessOrder=true并且需覆盖 removeEldestEntry方法。
  2. LinkedHashMap 在插入新的键值对时会调用removeEldestEntry方法,以决定是否在插入新的键值对后,移除老的键值对。在代码中,当被装饰类的容量超出了 keyMap 的所规定的容量后,keyMap 会移除最长时间未被访问的键,并保存到 eldestKey 中,然后由 cycleKeyList 方法将 eldestKey 传给被装饰类的 removeObject 方法,移除相应的缓存项目。

1.3、BlockingCache

BlockingCache 基于 Java 重入锁实现了阻塞特性。同一时刻仅允许一个线程访问指定 key 的缓存项,其他线程将会被阻塞。

  1. 查询缓存时,会先获得对应key的锁并加锁,若命中缓存则会释放锁,否则一直锁定。getObject 方法若返回 null,表示缓存未命中。此时 MyBatis 会进行数据库查询,并调用 putObject方法存储查询结果,对指定 key 对应的锁进行解锁。
  2. 当指定的key对应元素不在缓存中,BlockingCache会根据Lock进行加锁。此时其他的线程处于等待状态,直到key对应的数据被填充到缓存中,而不是让所有线程都去访问数据库。
  3. 但在removeObject时只仅调用了 releaseLock 方法释放锁,却没有调用被装饰类的 removeObject 方法移除指定缓存项。答案将在分析二级缓存的相关逻辑时分析。

2、缓存Key:CacheKey

为了是缓存hash更均匀,在CacheKey对象中包含了很多SQL操作的信息。且特别重要的是:一个CacheKey是由:statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值 四部分构成,并用一个特定的hash方法来构建hashcode。

2.1、构建CacheKey

一个CacheKey构成:
statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
CacheKey cacheKey = new CacheKey();
//NOTE: 记录MappedStatement id
cacheKey.update(ms.getId());
//NOTE: 记录指定查询结果集的范围
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
//NOTE: 查询所使用的SQL语句
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
 //NOTE: 记录用户传入的实参值-start
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);
    }
    //NOTE: 记录用户传入的实参值-end
    cacheKey.update(value);
  }
}
return cacheKey;
}

2.2、怎样判断某两次查询是完全相同的查询?

从上述createCacheKey方法看出,一个CacheKey对象只要以下4点完全相同,则表示是同一个Sql查询。

  1. 传入的 statementId ;
  2. 查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offset和rowBounds.limit表示);
  3. 每次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() 即sql语句);
  4. 传递给java.sql.Statement要设置的参数值,即传入的实参数值。

3、Mybatis缓存

3.1、一级缓存

  1. 一级缓存是Session会话级别的缓存,表示一次数据库会话的SqlSession对象之中,又被称之为本地缓存。Mybaits默认支持一级缓存,无需配置。
  2. MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。

###3.1.1、缓存构建:BaseExecutor
**初始化入口:**创建SqlSession过程中。SqlSession将它的工作交给了Executor执行器这个角色来完成,负责完成对数据库的各种操作。每个Executor继承至BaseExecutor抽象类,创建对应Executor时会调用父类(BaseExecutor)的构造函数,其中包含了一级缓存的构建,而真正的缓存信息包含在其中的localCache(PerpetualCache)对象中。

 protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    //NOTE: 延迟加载队列
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    //NOTE: 一级缓存默认使用PerpetualCache缓存类
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }

在这里插入图片描述

3.1.2、一级缓存的生命周期

一级缓存是SqlSession级别的缓存,MyBatis在开启一个数据库会话时,SqlSession就会创建一个一级缓存PerpetualCache对象,当会话结束时,SqlSession对象以及其内部的Executor对象和PerpetualCache对象会自动释放掉。

释放PerpetualCache对象的情况
  1. 当SqlSession调用close()方法时,会释放掉一级缓存PerpetualCache对象,即该对象将不可用。
  2. 当SqlSession调用clearCache()方法时,会清空PerpetualCache对象中的数据,但该对象仍可使用。(和2的区别就是PerpetualCache对象没有被置为NULL)
  3. SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但该对象可以继续使用。

3.1.3 工程流程

在执行SQL查询语句时,调用BaseExecutor的query,

  1. 根据statementId、params、rowBounds、boundSql构建一个CacheKey对象,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;
  2. 判断从Cache(PerpetualCache)中据特定的key值取的数据数据是否为空,即是否命中;
  3. 若命中则返回;
  4. 若未命中缓存:
    1. queryFromDatabase从数据库中查询到结果
    2. 将key和查询到的结果分别作为key-value对存储到Cache中;
    3. 返回结果。

3.2、二级缓存:CachingExecutor

  1. 二级缓存是应用级别的缓存,它的生命周期和应用的生命周期一样,其作用范围即整个应用。
  2. 一个SqlSession对象会将数据库操作交给Executor来完成,Mybatis的二级缓存就会在整个Executor上来搞事情,若设置cacheEnabled=true则会在创建Executor对象时增加一个装饰器CachingExecutor,后续SqlSession的操作将交给CachingExecutor来完成。
  3. CachingExecutor对于查询操作,先判断该查询请求在二级缓存中是否有缓存结果,若命中,则直接返回缓存结果;若为命中缓存,则调用真正的Executor对象来完成查询操作,得到查询结果后,CachingExecutor会将真正Executor返回的查询结果放置到缓存中,然后在返回给用户。
    在这里插入图片描述

3.2.1、二级缓存配置

二级缓存是可以有用户自定义配置的,且二级缓存不是简单地对整个应用就只有一个Cache缓存对象,而是细化到Mapper级别,即每个Mapper都可以配置一个Cache缓存。主要有两种方式配置

  1. 每个Mapper分配一个Cache缓存对象(使用节点配置)
  2. 多个Mapper共用一个Cache缓存对象(使用节点配置)

3.2.2、如何开启二级缓存?

MyBatis对二级缓存的支持粒度很细,它会指定某一条查询语句是否使用二级缓存。

虽然在Mapper中配置了,并且为此Mapper分配了Cache对象,这并不表示我们使用Mapper中定义的查询语句查到的结果都会放置到Cache对象之中,必须指定Mapper中的某条选择语句是否支持缓存

在<select> 节点中配置useCache="true",Mapper才会对此Select的查询支持缓存特性,否则,不会对此Select查询经过Cache缓存。

开启二级缓存的必要条件
  • 全局缓存配置开关开启cacheEnabled=true
  • Mapper映射文件配置<cache/><cached-ref>节点
  • 每个sql 语句配置节点开启useCache=true
一级缓存与二级缓存的优先级

如果开启且配置了二级缓存,那么在执行select查询语句的时候,Mybatis会先从二级缓存中获取结果,其次才是一级缓存。
二级缓存(CachingExecutor) ——> 一级缓存(BaseExecutor) ——> 数据库

  • 从源代码的角度看:
//--☆☆--CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {
    //NOTE: 是否配置了二级缓存
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      //NOTE: 当前执行Statement语句是否开启了缓存useCache=true
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          //NOTE: 未命中缓存则通过被装饰executor对象delegate发起查询--BaseExecutor
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); 
        }
        return list;
      }
    }
    //NOTE: 没有配置二级缓存,则通过被装饰executor对象delegate发起查询--BaseExecutor
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  
  //--☆☆--BaseExecutor
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    //NOTE: 缓存刷新
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      //NOTE: 一级缓存中是否缓存了对应sql?
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        //NOTE: 一级缓存中有数据,则返回
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        //NOTE: 一级缓存中没有则查询数据库
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    ....//懒加载逻辑
    return list;
  }

3.2.3、二级缓存实现选择

  1. Mybatis默认提供了很多的Cache实现类,只需要给Mapper文件中的节点<cache type=""/>加上属性type=具体实现的Cache即可(默认是LRUCache)。另外用户可通过实现Cache接口使用自定义的缓存。当然MyBatis还支持跟第三方内存缓存库如Memecached的集成。
  2. CachingExecutor调用ms.getCache获取的Cache本质上是一系列的装饰器模式,具体结构:
    在这里插入图片描述
  • SynchronizedCache:同步Cache,实现比较简单,直接使用synchronized修饰方法。
  • LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
  • SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
  • LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。
  • PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。

3.2.4、二级缓存事务管理

因为二级缓存时应用级别的,支持多Session共享。所以需要考虑事务,必然需要做到事务提交时,才将当前事务中查询时产生的缓存,同步到二级缓存中。

  • TransactionalCacheManager:缓存事务管理
  • TransactionalCache:支持事务的Cache
3.2.4.1 TransactionalCacheManager

缓存事务管理器,内部用一个Map来记录Cahe对象和对应事务TransactionalCache缓存对象。内部的事务提交、回滚等操作实际都是调用TransactionalCache。

3.2.4.2 TransactionalCache

事务缓存实现类,实现接口Cache。只有事务提交了缓存才生效。如果事务回滚或者不提交事务,则不对缓存产生影响。

  1. SQL执行完拿到结果后:
//-☆☆- CachingExecutor
if (list == null) {
	list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
	//将结果缓存
	tcm.putObject(cache, key, list); 
}

//-☆☆-TransactionalCache
//被装饰缓存对象
private final Cache delegate;
private boolean clearOnCommit;
/**
* 事务提交前,所有数据库查询的结果将缓存在该集合中
*/
private final Map<Object, Object> entriesToAddOnCommit;
/**
* 事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中
*/
private final Set<Object> entriesMissedInCache;

//1-☆☆-TransactionalCache-事务提交前
public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
}

//2-☆☆-TransactionalCache-事务提交
public void commit() {
if (clearOnCommit) {
  delegate.clear();
}
//事务提交将数据缓存到二级缓存中并重置当前缓存的数据
flushPendingEntries();
reset();
}

//3-☆☆-数据添加到二级缓存
private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    //NOTE: 事务提交后,若未命中缓存则添加
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

3.2.5、注意:最好不要使用Mybatis的二级缓存

  • 原因:二级缓存时应用级别的,若在查询与更新操作交替进行的情境下,会出现脏数据。比如:

    1. 先查询记录R的查询操作A,并将结果缓存;
    2. 接着对记录R进行更新操作B;
    3. 接着再进行查询操作A,由于第一步已经将数据存入缓存,那么僵直接获取到结果,但在第二部时已经更新R记录,导致这次获取的数据不正确。
  • 解决办法:可以通过插件的方式,在每次更新时,主动清除二级缓存数据。但每次执行数据库操作多需要经过插件的判断,影响性能。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: MyBatis 是一个开源的持久层框架,可以方便地将 SQL 语句和 Java 对象进行映射。如果您想要学习 MyBatis 源码,可以按照以下步骤进行: 1. 了解 MyBatis 的架构和设计原理。可以阅读官方文档和相关书籍,例如《MyBatis 技术内幕》。 2. 下载 MyBatis 的源代码,并导入到 IDE 中。MyBatis 使用 Maven 进行构建,您可以使用 IDE 的 Maven 插件来下载依赖项。 3. 查看 MyBatis 的源代码结构。MyBatis 的主要代码在 `mybatis-3` 模块中,包括 `src/main/java` 和 `src/main/resources` 目录。其中,`src/main/java` 目录包含了 MyBatis 的核心代码,例如 `org.apache.ibatis.session.SqlSession` 类;`src/main/resources` 目录包含了 MyBatis 的配置文件和映射文件。 4. 阅读 MyBatis 的源代码。可以从 MyBatis 的入口处 `org.apache.ibatis.session.SqlSessionFactoryBuilder` 开始,深入了解 MyBatis 的初始化流程、SQL 语句的执行流程、映射文件的解析和缓存等。 5. 调试 MyBatis 的源代码。可以使用 IDE 的调试功能,对 MyBatis 进行单步调试,观察代码的执行流程,加深对 MyBatis 的理解。 6. 学习 MyBatis 的单元测试。MyBatis 的单元测试位于 `src/test/java` 目录中,可以过单元测试来了解 MyBatis 的各个功能点的使用方法和测试用例。 7. 参与 MyBatis 的开发。如果您对 MyBatis 源码有深入的了解,并希望为 MyBatis 做出贡献,可以参与 MyBatis 的开发,贡献代码和文档,提交 issue 和 PR。MyBatis 的开发社区非常活跃,可以在官方网站和 GitHub 上找到相关信息。 希望这些步骤对您学习 MyBatis 源码有所帮助。 ### 回答2: MyBatis是一个开源的Java持久层框架,过操作对象与数据库关系映射来提供数据持久化的功能。了解MyBatis源码是学习和使用该框架的重要一步。 首先,MyBatis源码结构比较清晰,主要分为核心模块和附属模块。核心模块包括XML配置解析、SQL语句解析、参数处理、数据库连接管理等功能的实现,是实现MyBatis基本功能的核心部分。附属模块包括缓存、事务、插件等额外功能的实现,可以根据需要进行扩展和配置。 学习MyBatis源码可以从以下几个方面入手: 1. 配置文件解析:MyBatis过XML配置文件来进行相关的配置,了解配置文件的解析过程可以帮助理解MyBatis的初始化过程和各项配置的作用。 2. SQL语句解析与执行:MyBatis将SQL语句封装成MappedStatement对象进行管理,了解MappedStatement的生成过程,以及SQL语句的解析、参数处理和执行过程,可以深入了解MyBatis的SQL执行原理。 3. 会话管理和事务处理:MyBatis采用SqlSessionFactory和SqlSession来管理数据库连接和事务,在MyBatis源码中可以学习到如何管理数据库连接池、事务的提交和回滚等核心功能的实现。 4. 缓存机制:MyBatis提供了一级缓存和二级缓存的功能,了解缓存的生成和更新过程,以及缓存的命中和失效原理,可以提高数据库查询性能。 总之,过学习MyBatis源码,可以加深对该框架的理解,掌握其内部实现原理,有助于在使用时更加灵活和高效地进行开发。同时,也为以后解决一些特殊问题提供了更多的思路和方法。 ### 回答3: MyBatis是一个优秀的持久层框架,学习其源码有助于理解其底层原理和设计思想。 首先,可以从MyBatis的入口开始学习,即SqlSessionFactoryBuilder类。该类负责解析配置文件、创建Configuration对象,并过Configuration对象创建SqlSessionFactory实例。 接下来,可以学习Configuration类,该类负责管理整个MyBatis的配置信息。其中包括了数据库连接信息、映射文件信息、缓存信息等。在该类内部,会调用XMLMapperBuilder类解析映射文件,在解析映射文件过程中,会创建MappedStatement对象,该对象表示一条SQL语句的映射信息。 学习MappedStatement对象可以了解MyBatis的SQL语句解析过程。该对象包含了SQL语句的相关信息,包括参数映射关系、返回结果映射关系等。在执行SQL语句时,会使用ParameterHandler类处理参数,过ResultSetHandler类处理查询结果。 同时,学习到Executor接口及其实现类,可以了解MyBatis的执行过程。Executor负责执行SQL语句,其中包括了写操作的update方法和读操作的query方法。在执行过程中,会过StatementHandler类创建PreparedStatement对象,并过ResultSetHandler类处理执行结果。 最后,还可以学习到MyBatis的事务处理和缓存机制。Transaction接口及其实现类负责事务管理,过JDBC的事务机制实现了事务的提交和回滚。而Cache接口及其实现类负责缓存查询结果,在查询时会先从缓存中查找结果。 总结来说,过学习MyBatis源码可以深入理解其底层原理和设计思想。从SqlSessionFactory的创建开始,到Configuration的配置解析、MappedStatement的创建,再到Executor的执行过程和Transaction的事务管理,以及Cache缓存机制,逐步掌握MyBatis的各个组件和它们之间的交互关系。这对于我们使用MyBatis开发项目,解决问题和优化性能都具有积极的意义。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只打杂的码农

你的鼓励是对我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值