十Mybatis 缓存系统解析

Posted archerLuo罗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了十Mybatis 缓存系统解析相关的知识,希望对你有一定的参考价值。

本文从以下几个方面介绍:

  • 1、如何开启 Mybatis 的缓存
  • 2、缓存的核心接口以及底层实现
  • 3、一级缓存的实现过程
  • 4、二级缓存的实现过程
  • 5、缓存的装饰器

前言

为了提高查询速度,减少数据库压力;Mybatis 提供了缓存功能,它分为一级缓存和二级缓存。

Mybatis 缓存系统的实现使用了 模板方法模式装饰器模式

1、如何开启 Mybatis 的缓存

1.1、一级缓存的作用

Mybatis 的一级缓存是会话级别的缓存,Mybatis 每创建一个 SqlSession 对象,就表示打开一次数据库会话,在一次会话中,应用程序很可能在短时间内反复执行相同的查询语句,如果不对数据进行缓存,则每查询一次就要执行一次数据库查询,这就造成数据库资源的浪费。又因为通过 SqlSession 执行的操作,实际上由 Executor 来完成数据库操作的,所以在 Executor 中会建立一个简单的缓存,即一级缓存;将每次的查询结果缓存起来,再次执行查询的时候,会先查询一级缓存,如果命中,则直接返回,否则再去查询数据库并放入缓存中。

1.2、一级缓存的开启以及生命周期

一级缓存的生命周期与 SqlSession 的生命周期相同,当调用 Executor.close 方法的时候,缓存变得不可用。一级缓存是默认开启的,一般情况下不需要特殊的配置,如果需要特殊配置,则可以通过插件的形式来实现

1.3、二级缓存的开启以及生命周期

Mybatis 提供的二级缓存是应用级别的缓存,它的生命周期和应用程序的生命周期相同,且与二级缓存相关的配置有以下 3 个

  • 1)、 mybatis-config.xml 配置文件中的 cacheEnabled 配置,它是二级缓存的总开关,只有该配置为 true ,后面的缓存配置才会生效。默认为 true,即二级缓存默认是开启的。
  <settings>
    <setting name="cacheEnabled" value="true"/>
  </settings>
  • 2)、Mapper.xml 配置文件中配置的 和 标签,如果 Mapper.xml 配置文件中配置了这两个标签中的任何一个,则表示开启了二级缓存的功能,如果配置了 标签,则在解析配置文件的时候,会为该配置文件指定的 namespace 创建相应的 Cache 对象作为其二级缓存(默认为 PerpetualCache 对象),如果配置了 节点,则通过 ref 属性的namespace值引用别的Cache对象作为其二级缓存。通过 和 标签来管理其在namespace中二级缓存功能的开启和关闭
  • 3)、 节点中的 useCache 属性也可以开启二级缓存,该属性表示查询的结果是否要存入到二级缓存中,该属性默认为 true,也就是说 标签默认会把查询结果放入到二级缓存中

img

2、缓存的核心接口

preview

2.1、Cache

Mybatis 使用 Cache 来表示缓存,它是一个接口,定义了缓存需要的一些方法

public interface Cache {
 //获取缓存的id,即 namespace
 String getId();
 // 添加缓存
 void putObject(Object key, Object value);
 //根据key来获取缓存对应的值
 Object getObject(Object key);
 // 删除key对应的缓存
 Object removeObject(Object key);
 // 清空缓存  
 void clear();
 // 获取缓存中数据的大小
 int getSize();
 //取得读写锁, 从3.2.6开始没用了
 ReadWriteLock getReadWriteLock();
}

2.2、PerpetualCache

Mybatis 为 Cache 接口提供的真正的实现类是 PerpetualCache,其他的只是应用装饰器模式,提供能额外功能,

如线程安全缓存 SynchronizedCache,它的底层存储还是 PerpetualCache。

PerpetualCache 的实现就是一个简单的 HashMap

public class PerpetualCache implements Cache {
 // id,一般对应mapper.xml 的namespace 的值
 private String id;
 
 // 用来存放数据,即缓存底层就是使用 map 来实现的
 private Map<Object, Object> cache = new HashMap<Object, Object>();
 
 public PerpetualCache(String id) {
 	this.id = id;
  }
 //......其他的getter方法.....
 // 添加缓存
 @Override
 public void putObject(Object key, Object value) {
    cache.put(key, value);
  }
 // 获取缓存
 @Override
 public Object getObject(Object key) {
 	return cache.get(key);
  }
 // 删除缓存
 @Override
 public Object removeObject(Object key) {
 	return cache.remove(key);
  }
 // 清空缓存
 @Override
 public void clear() {
    cache.clear();
  }
}

2.3、CacheKey

Mybatis 的缓存使用了 key-value 的形式存入到 HashMap 中,而 key 的话,Mybatis 使用了 CacheKey 来表示 key,

它的生成规则为:mappedStementId + offset + limit + SQL + queryParams + environment生成一个哈希码.

public class CacheKey implements Cloneable, Serializable {

    private static final int DEFAULT_MULTIPLYER = 37;
    private static final int DEFAULT_HASHCODE = 17;

    // 参与计算hashcode,默认值为37
    private int multiplier;
    // CacheKey 对象的 hashcode ,默认值 17
    private int hashcode;
    // 检验和 
    private long checksum;
    // updateList 集合的个数
    private int count;
    // 由该集合中的所有对象来共同决定两个 CacheKey 是否相等
    private List<Object> updateList;

    public int getUpdateCount() {
        return updateList.size();
    }

    // 调用该方法,向 updateList 集合添加对应的对象
    public void update(Object object) {
        if (object != null && object.getClass().isArray()) {
            // 如果是数组,则循环处理每一项
            int length = Array.getLength(object);
            for (int i = 0; i < length; i++) {
                Object element = Array.get(object, i);
                doUpdate(element);
            }
        } else {
            doUpdate(object);
        }
    }

    // 计算 count checksum hashcode 和把对象添加到 updateList 集合中
    private void doUpdate(Object object) {
        int baseHashCode = object == null ? 1 : object.hashCode();
        count++;
        checksum += baseHashCode;
        baseHashCode *= count;
        hashcode = multiplier * hashcode + baseHashCode;

        updateList.add(object);
    }

    // 判断两个 CacheKey 是否相等
    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }
        if (!(object instanceof CacheKey)) {
            return false;
        }

        final CacheKey cacheKey = (CacheKey) object;

        if (hashcode != cacheKey.hashcode) {
            return false;
        }
        if (checksum != cacheKey.checksum) {
            return false;
        }
        if (count != cacheKey.count) {
            return false;
        }
        // 如果前几项都不满足,则循环遍历 updateList 集合,判断每一项是否相等,如果有一项不相等则这两个CacheKey不相等
        for (int i = 0; i < updateList.size(); i++) {
            Object thisObject = updateList.get(i);
            Object thatObject = cacheKey.updateList.get(i);
            if (thisObject == null) {
                if (thatObject != null) {
                    return false;
                }
            } else {
                if (!thisObject.equals(thatObject)) {
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public int hashCode() {
        return hashcode;
    }
}

CacheKey 的创建

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        //cacheKey 对象 
        CacheKey cacheKey = new CacheKey();
        // 向 updateList 存入id
        cacheKey.update(ms.getId());
        // 存入offset
        cacheKey.update(rowBounds.getOffset());
        // 存入limit
        cacheKey.update(rowBounds.getLimit());
        // 存入sql
        cacheKey.update(boundSql.getSql());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
        for (ParameterMapping parameterMapping : parameterMappings) {
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                String propertyName = parameterMapping.getProperty();
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                Object value = metaObject.getValue(propertyName);
                // 存入每一个参数
                cacheKey.update(value);
            }
        }
        if (configuration.getEnvironment() != null) {
            // 存入 environmentId
            cacheKey.update(configuration.getEnvironment().getId());
        }
        return cacheKey;
    }

3、一级缓存的实现

Mybatis 的一级缓存是在 BaseExecutor 中实现的

3.1、Executor

Executor 接口定义了操作数据库的基本方法,我们用 SqlSession 来执行 sql时,其实是操作了 Executor 的相关方法

public interface Executor {

    ResultHandler NO_RESULT_HANDLER = null;

    // insert | update | delete 的操作方法
    int update(MappedStatement ms, Object parameter) throws SQLException;

    // 查询,带分页,带缓存  
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

    // 查询,带分页 
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

    // 查询存储过程
    <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

    //刷新批处理语句
    List<BatchResult> flushStatements() throws SQLException;

    // 事务提交
    void commit(boolean required) throws SQLException;

    // 事务回滚
    void rollback(boolean required) throws SQLException;

    // 创建缓存的key
    CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

    // 是否缓存
    boolean isCached(MappedStatement ms, CacheKey key);

    // 清空缓存
    void clearLocalCache();

    // 延迟加载
    void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

    // 获取事务
    Transaction getTransaction();
}

3.2、BaseExecutor

BaseExecutor 是一个抽象类,实现了 Executor 接口的所有方法,使用了模板模式策略模式,抽象了 (doUpdate, doQuery, doQueryCursor, doFlushStatement)4个核心方法用来执行sql,它们由不同的子类实现

Mybatis 的一级缓存就是在该类中实现的。

具体代码实现如下

	@Override
    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) {
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                // 缓存中获取不到,则调用queryFromDatabase()方法从数据库中查询
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        if (queryStack == 0) {
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            deferredLoads.clear();
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }

 	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 {
            // 调用doQuery()方法查询
            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;
    }

4、二级缓存的实现

Mybatis 的二级缓存是用 CachingExecutor 来实现的,它是 Executor 的一个装饰器类。为 Executor 对象添加了缓存的功能。

在介绍 CachingExecutor 之前,先来看看 CachingExecutor 依赖的两个类,TransactionalCacheManager 和 TransactionalCache。

4.1、TransactionalCache

TransactionalCache 实现了 Cache 接口,主要用于保存在某个 SqlSession 的某个事务中需要向某个二级缓存中添加的数据,代码如下:

public class TransactionalCache implements Cache {
    // 底层封装的二级缓存对应的Cache对象
    private Cache delegate;
    // 为true时,表示当前的 TransactionalCache 不可查询,且提交事务时会清空缓存
    private boolean clearOnCommit;
    // 存放需要添加到二级缓存中的数据
    private Map<Object, Object> entriesToAddOnCommit;
    // 存放未命中缓存的 CacheKey 对象
    private Set<Object> entriesMissedInCache;

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

    // 添加缓存数据的时候,先暂时放到 entriesToAddOnCommit 集合中,在事务提交的时候,再把数据放入到二级缓存中,避免脏数据
    @Override
    public void putObject(Object key, Object object) {
        entriesToAddOnCommit.put(key, object);
    }
    // 提交事务,
    public void commit() {
        if (clearOnCommit) {
            delegate.clear();
        }
        // 把 entriesToAddOnCommit  集合中的数据放入到二级缓存中
        flushPendingEntries();
        reset();
    }
    // 把 entriesToAddOnCommit  集合中的数据放入到二级缓存中
    private void flushPendingEntries() {
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
            // 放入到二级缓存中
            delegate.putObject(entry.getKey(), entry.getValue()Mybatis源码解析MyBatis的二级缓存源码解析

mybatis系统性详解(学习笔记)

mybatis的缓存机制源码分析之二级缓存解析

mybatis的缓存机制源码分析之一级缓存解析

干货分享 | MyBatis实战缓存机制设计与原理解析

《深入理解mybatis原理6》 MyBatis的一级缓存实现详解 及使用注意事项