Mybatis 一/二级缓存关系及访问顺序

第一章 Mybatis 一/二级缓存

一级缓存(Session 级别缓存)

  • 作用范围:一级缓存是 SqlSession 级别的缓存,仅在当前 SqlSession 的生命周期内有效。

  • 存储位置:存储在 SqlSession 的内存中。

  • 特点

    • 自动管理:MyBatis 自动维护一级缓存,无需手动配置。

    • 生命周期:与 SqlSession 的生命周期一致,SqlSession 关闭后,一级缓存失效。

    • 作用:减少对数据库的访问次数,提高查询效率。

  • 访问顺序

1.首先访问本地缓存。

2.本地缓存没有时,从数据库中获取。

二级缓存(Mapper 级别缓存)
  • 作用范围:二级缓存是 Mapper 级别的缓存,作用于整个应用级别,多个 SqlSession 可以共享二级缓存。

  • 存储位置:存储在 Cache 接口的实现类中,可以是内存缓存、文件缓存或其他存储方式。

  • 特点

    • 需要手动配置:需要在 Mapper 文件中配置 <cache> 标签。

    • 生命周期:与应用的生命周期一致,应用关闭后,二级缓存失效。

    • 作用:进一步减少对数据库的访问次数,提高查询效率,适用于多用户共享的场景

  • 访问顺序

1.在commit的时候,把二级缓存中的数据刷新到正在代理的一级缓存中。

2.然后直接访问一级缓存中的数据。

3.若一级缓存中没有,则访问数据库获取。

第二章 当只有一级缓存时

当只有一级缓存时,local cache 自动开启,先从cache里取数据,没有再从数据库里取数据,并放入缓存中。

BaseExecutor 类

BaseExecutor 类是 MyBatis 中执行 SQL 的基础类,它包含了缓存相关的逻辑。

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) {
            handleLocallyCachedOutputParameter(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();
    }
    return list;
}
  • localCache.getObject(key):首先检查一级缓存。
  • queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql):如果一级缓存中没有数据,则执行数据库查询。

第三章 当二级缓存开启后

CachingExecutor 类

CachingExecutor 类是 MyBatis 中处理二级缓存的类。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
        }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
  • tcm.getObject(cache, key):检查二级缓存。
  • delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql):如果二级缓存中没有数据,则执行数据库查询,并将结果存入二级缓存。

那我们接着看下tcm.getObject(cache, key)方法。

TransactionalCacheManager 类

tcm.getObject(cache,key) 是访问的TranscationalCacheManager类里的方法。

public class CachingExecutor implements Executor {

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public class TransactionalCacheManager {

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

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

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
   
  ...
}

从上面代码可以看到, tcm.getObject(cache, key) 其实是获取了一个TransactionalCache对象,然后调用了它的getObject(key)方法来返回值。

 TransactionalCache类

/**
 * The 2nd level cache transactional buffer.
 * <p>
 * This class holds all cache entries that are to be added to the 2nd level cache during a Session. Entries are sent to
 * the cache when commit is called or discarded if the Session is rolled back. Blocking cache support has been added.
 * Therefore any get() that returns a cache miss will be followed by a put() so any lock associated with the key can be
 * released.
 *
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  private final Cache delegate;
  private boolean clearOnCommit;
  private final Map<Object, Object> entriesToAddOnCommit;
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }

从上面注释可以看到,这是二级缓存真正存放数据的地方。从构造函数可以看到传入了一个 Cache delegate 对象进行初始化,这里的delegate cache就是我们传入的一级缓存。

private final Map<Object, Object> entriesToAddOnCommit;

这个类就是二级缓存具体放全局数据的地方。

它的getObject(key)方法如下:

@Override
  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    }
    return object;
  }

可以看到,从二级缓存里获取对象,直接委托给了delegate(一级缓存)来获取。这里有点懵了,那二级缓存里的数据 entriesToAddOnCommit 是否有被用到呢?

让我们看entriesToAddOnCommit的完整数据链路:

1)putObject

public class TransactionalCacheManager {

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

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

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }

  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

TranscationalCache类中的真实方法: 

@Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

当调用CachingExecutor.putObject()的时候,最终会调用TranscationCache.putObject()方法,把需要cache的对象存放到 entriesToAddOnCommit Map中。

2)关键链路  flushPendingEntries 

TransactionalCache类中有一个私有方法,flushPendingEntries 它就是把二级缓entriesToAddOnCommit  Map中的数据全部flush到一级缓存中。

private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

那这个 flushPendingEntries 操作会在什么时候运行呢?
我们可以看到TransactionalCache类的commit方法,会调用flushPendingEntries 把二级缓存中保存的所有数据刷新到一级缓存中。【这也是为什么我们二级缓存生效条件中有一项:必须等到sqlSession.commit()方法调用后才生效】

public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

到此刻,我们明白了。当我们调用CachingExecutor类的commit方法后,最终会传导到TransactionalCache类中的commit方法,把二级缓存中的所有数据刷新到一级缓存。

CachingExecutor.commit() --> TransactionalCacheManager.commit() -->TransactionalCache.commit() 

CachingExecutor类的commit

@Override
  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }

TransactionCacheManager的commit

public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

然后我们前面 调用二级缓存的方法,它最终委托给了一级缓存的getObject方法能够拿到数据了。

### MyBatis 一级缓存二级缓存的区别 #### 一级缓存 一级缓存也被称为本地会话缓存,存在于 SqlSession 生命周期内。当同个 `SqlSession` 实例中多次执行相同的 SQL 查询语句MyBatis 将只发送次请求给数据库并把结果保存到该级别的缓存里;对于后续相同条件下的查询操作,则直接返回之前已有的数据而不再访问实际的数据源[^1]。 ```java try (SqlSession session = sqlSessionFactory.openSession()) { User user1 = session.selectOne("findUser", id); // 同session下再次查询同记录不会触发SQL执行 User user2 = session.selectOne("findUser", id); } ``` #### 二级缓存 相比之下,二级缓存作用范围更广,它跨越了不同 `SqlSession` 的界限,在整个应用程序级别上共享。这意味着如果两个不同的 `SqlSession` 对象分别进行了同样的查询调用,并且这些对象都启用了二级缓存功能的话,那么第二次查询可以直接利用第次的结果而不必重新向数据库发起请求。需要注意的是,为了使某个映射文件能够参与全局性的缓存管理,必须显式配置 `<cache>` 或者通过 Java API 设置相应的属性来开启此特性[^3]。 ```xml <mapper namespace="com.example.UserMapper"> <!-- 开启当前命名空间下的二级缓存 --> <cache/> </mapper> ``` ### 执行顺序 在默认情况下,MyBatis尝试从最近的一级缓存获取所需数据。如果没有命中,则继续向上查找至更高层次的缓存——即二级缓存。只有在这两级缓存均未找到匹配项之后才会真正去连接底层数据库读取原始资料。此外,旦发生任何写入操作(如 insert, update 或 delete),不仅会使涉及的对象被标记为脏状态从而强制刷新关联的一级缓存条目,还会清空对应的二级缓存区域以保持致性[^2]。 ```sql -- 插入新纪录后,相关联的一级二级缓存都会受到影响 INSERT INTO users VALUES (...); -- 更新现有记录同样如此处理 UPDATE users SET ... WHERE ... -- 删除也会引起相应变化 DELETE FROM users WHERE ... ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值