Mybatis源码分析系列(二)-mybatis中的二级缓存

目录

1.二级缓存总体流程

2.二级缓存为何可以线程共享或会话共享

2.1 原理分析        

2.2 put过程

2.query过程

3.二级缓存实现细节                              


上一篇:mybatis一级缓存源码分析


        相较于一级缓存,二级缓存要完成的功能就要复杂的多,实现也复杂的多。不过捏也不要担心,一步步跟着来也很容易滴。

1.二级缓存总体流程

        我们首先来看下二级缓存的总体流程。其实天下所有缓存流程都一样,先查缓存,没有的话查询数据库然后放到缓存中,下次直接从缓存中获取。二级缓存也一样,接下来我们看看源码:

public class Application {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //进入这行
        String res = sqlSession.selectOne("com.mybatis.demo.CronMapper.selectOne",1);
        System.out.println(res);
    }
}

上边sqlSession的默认实现是DefaultSqlSession。所以我们进入DefaultSqlSession的selectOne方法:

@Override
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    // 进入这行
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }
@Override
  public <E> List<E> selectList(String statement, Object parameter) {
    //进入这行
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }
@Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      //进入这行
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

上边executor的默认实现是CachingExecutor,所以我们进入CachingExecutor的query方法:

@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就不为空 所以会进入下边的if判断
    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);
  }

通过上述的简单分析,我们知道了二级缓存的总体流程。接下来我们详细分析下为什么说二级缓存可以做到线程共享。

2.二级缓存为何可以线程共享或会话共享

2.1 原理分析        

        二级缓存为什么可以跨线程/跨会话访问呢?我们先来看一个原理图:

 如图所示:

        1. 一个会话对应一个CachingExecutor,一个CachingExecutor对应一个TransactionalCacheManager事务管理器。

        2. 在TransactionalCacheManager中维护了一个Map,key就是真正的二级缓存对象,value是一个TransactionalCache对象,我们称其为暂存区。

        3.TransactionalCache这个暂存区中维护了一个Map,这个map里边就是当前将要放到二级缓存中的真实的数据。

        4.当通过sqlSession进行增删改时,会首先同步到暂存区中,注意此时并没有放到二级缓存里边,其他的会话是查询不到的。

        5.只有当前这个sqlSession的事务提交或者关闭的时候才会把暂存区里边的数据刷新到二级缓存中。真正的二级缓存对象并不是维护在SqlSession里边,所以对于不同的会话来讲二级缓存是数据共享的。这也就解释了为什么二级缓存是线程共享的。

        可能有同学有疑问了,那这个二级缓存是在哪儿维护的呢?其实二级缓存对象是维护在MappedStatement里边,通俗点说,二级缓存是和我们的Mapper.java或者Mapper.xml绑定在一起的。

        光说不练都是假把式,接下来直接看源码:

2.2 put过程

        首先来看下二级缓存的修改流程。我们直接进入CachingExecutor的query方法:

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //这个获取的就是真正的二级缓存对象,也验证了上边我们所说的二级缓存其实维护在MappedStatement里边
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        //这个是获取 我们先不看  看下边的put过程
        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);
  }
public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
 }

我们首先来看第一个方法getTransactionalCache(cache):

private TransactionalCache getTransactionalCache(Cache cache) {
    //很明显就是从map中获取暂存区对象 如果不存在直接new一个
    //transactionalCaches这货就是个map
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }

有一点需要特别注意,第一次从transactionalCaches这个map中获取的时候肯定是拿不到的。那么就会new一个缓冲区对象TransactionalCache。我们看下它的构造方法:

public TransactionalCache(Cache delegate) {
    //注意! 此时给缓存区对象的delegate属性赋值为真正的二级缓存对象
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }

后续缓存区对象会通过这个delegate也就是真正的二级缓存对象将数据进行存放。请牢记此处!

接下来我们看第二个方法putObject(key, value):

@Override
public void putObject(Object key, Object object) {
  //entriesToAddOnCommit这货就是暂存区对象中的一个map
  //就是把真正的数据放到这个map中
  entriesToAddOnCommit.put(key, object);
}

到此我们将数据放到了暂存区的map里边,注意,此时还没有把数据放到真正的二级缓存中。其他的会话此时还查询不到。

我们继续看sqlSession的commit方法:

public class Application {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        String res = sqlSession.selectOne("com.mybatis.demo.CronMapper.selectOne",1);
        //进入这个方法
        sqlSession.commit();
        System.out.println(res);
    }
}
@Override
  public void commit() {
    //进入这个方法
    commit(false);
  }
@Override
  public void commit(boolean force) {
    try {
      //进入这个方法
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

注意此时executor默认实现是CachingExecutor,所以我们进入CachingExecutor的commit方法:

@Override
  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    //进入这个方法
    tcm.commit();
  }
public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

这个就比较关键了,我们知道transactionalCaches这个map里边存放了所有二级缓存对象以及对应的暂存区。这个for循环就是遍历所有二级缓存对象,然后将对应的暂存区里边的数据放到对应的二级缓存对象里边。我们接着往下看:

public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    //进入这个方法
    flushPendingEntries();
    reset();
  }
private void flushPendingEntries() {
    //循环暂存区中的map 拿到所有真实数据的key value 然后放到二级缓存中
    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);
      }
    }
  }

我们看,entriesToAddOnCommit这个map是暂存区中存放真实数据的。现在for循环遍历它拿到所有数据然后放到二级缓存中。这个delegate就是真正的二级缓存对象。上边我们已经通过缓存区的构造方法已经证实了这点。

        我们已经了解了更新以及事务提交的过程,接下来我们再看下二级缓存的查询过程。

2.query过程

        二级缓存的查询过程就容易多了,我们直接从CachingExecutor的query方法看起:

@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);
  }
public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }

这个方法我们在跟update的时候已经看过了,getTransactionalCache(cache)这个方法会返回一个和当前二级缓存对应的暂存区对象。接下来直接看暂存区TransactionalCache的getObject方法:

@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;
    } else {
      return object;
    }
  }

我们应该还记得,这个delegate就是真正的二级缓存对象吧。那么行棋至此,我们看过了暂存区、二级缓存的update、query、commit等操作的整体流程。

3.二级缓存实现细节            

        经过上述分析,我们已经知道了二级缓存的存取流程及线程共享的实现原理。但是我们还没有深入到二级缓存对象的内部,接下来我们看看二级缓存对象内部究竟是如何完成的。

        我们知道一个缓存绝不是简单存取那么简单,我们还要考虑同步锁、淘汰策略、日志记录、命中统计、序列化等等等等。那这么多功能我们要放到一个类里边岂不是成了屎山。mybatis使用策略+责任链的模式巧妙的解决了这个问题。我们先来看一张图:

 这张图其实很形象的描绘了二级缓存的实现机制。图中每个节点都会维护一个delegate属性,而这个属性的默认实现就是下一个节点。每个节点只完成自己负责的工作然后调用delegate的方法,让下一个节点继续执行。如此执行下去到最后一个节点时,最后一个节点维护了一个Map,这个map就是真正的二级缓存了。最后把数据放进去或者取出来即可。多说无益,咱们直接上代码,首先来看SynchronizedCache的getObject方法(SynchronizedCache这个对象是头结点,也是我们上边一直说的二级缓存对象):

@Override
  public synchronized Object getObject(Object key) {
    return delegate.getObject(key);
  }

可以看到这个方法只是加了个同步锁,其他什么都没干就交给了下个节点。下个节点也就是delegate的默认实现是LoggingCache,我们继续:

@Override
  public Object getObject(Object key) {
    requests++;
    //这个方法就是让下一个节点继续执行
    final Object value = delegate.getObject(key);
    if (value != null) {
      hits++;
    }
    if (log.isDebugEnabled()) {
      log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
    }
    return value;
  }

上边这个方法其实只是打印了日志统计了一下缓存命中率,我们接着进下一个节点SerializedCache:

@Override
  public Object getObject(Object key) {
    Object object = delegate.getObject(key);
    return object == null ? null : deserialize((byte[]) object);
  }

上边方法只是给查询结果做了个序列化操作,别的什么没干,我们接着进下一个节点LruCache:

@Override
  public Object getObject(Object key) {
    keyMap.get(key); //touch
    return delegate.getObject(key);
  }

  所谓lru呢指的是最近最少使用,和Redis里边也是一样的。感兴趣的同学可以自己去看下Java如何实现lru,我们不做详细介绍。我们继续进最后一个节点 PerpetualCache:    

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

这就很明显了,直接从cache里边get了。那cache是啥呢?就是个map 哈哈:

public class PerpetualCache implements Cache {

  private final String id;

  //cache就是个map 用来存放数据的
  private final Map<Object, Object> cache = new HashMap<>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

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

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

行棋至此我们已经详细分析了二级缓存具体实现细节。至于put的过程各位小伙伴自己去看看吧,也是简单的很呐!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值