美文网首页springboot程序员Spring Boot
Mybatis原理--缓存机制(一级缓存)

Mybatis原理--缓存机制(一级缓存)

作者: ChinaXieShuai | 来源:发表于2017-09-27 00:39 被阅读150次

    MyBatis将数据缓存设计成两级结构,分为一级缓存、二级缓存:
    一级缓存是Session会话级别的缓存:表示一次数据库会话的SqlSession对象之中,又被称之为本地缓存。一级缓存是MyBatis内部实现的一个特性,用户不能配置,默认情况下自动支持的缓存,用户没有定制它的权利(不过这也不是绝对的,可以通过开发插件对它进行修改);
    二级缓存是Application应用级别的缓存:它的是生命周期很长,跟Application的声明周期一样,也就是说它的作用范围是整个Application应用。

    由于MyBatis使用SqlSession对象表示一次数据库的会话,为了减少资源的浪费,MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。

    一级缓存
    实际上, SqlSession只是一个MyBatis对外的接口,SqlSession将它的工作交给了Executor执行器这个角色来完成,负责完成对数据库的各种操作。当创建了一个SqlSession对象时,MyBatis会为这个SqlSession对象创建一个新的Executor执行器,而缓存信息就被维护在这个Executor执行器中,MyBatis将缓存和对缓存相关的操作封装成了Cache接口中,
    Executor接口的实现类BaseExecutor中拥有一个Cache接口的实现类PerpetualCache,则对于BaseExecutor对象而言,它将使用PerpetualCache对象维护缓存
    public class PerpetualCache implements Cache {
        private String id;
        private Map<Object, Object> cache = new HashMap();
    
        public PerpetualCache(String id) {
            this.id = id;
        }
    
        public String getId() {
            return this.id;
        }
    
        public int getSize() {
            return this.cache.size();
        }
    
        public void putObject(Object key, Object value) {
            this.cache.put(key, value);
        }
    
        public Object getObject(Object key) {
            return this.cache.get(key);
        }
    
        public Object removeObject(Object key) {
            return this.cache.remove(key);
        }
    
        public void clear() {
            this.cache.clear();
        }
    
        public ReadWriteLock getReadWriteLock() {
            return null;
        }
    
        public boolean equals(Object o) {
            if(this.getId() == null) {
                throw new CacheException("Cache instances require an ID.");
            } else if(this == o) {
                return true;
            } else if(!(o instanceof Cache)) {
                return false;
            } else {
                Cache otherCache = (Cache)o;
                return this.getId().equals(otherCache.getId());
            }
        }
    
        public int hashCode() {
            if(this.getId() == null) {
                throw new CacheException("Cache instances require an ID.");
            } else {
                return this.getId().hashCode();
            }
        }
    }
    
    一级缓存的生命周期
    1. MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
    2. 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
    3. 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;
      4.SqlSession中执行了任何一个update操作(update()、delete()、insert()),都会清空PerpetualCache对象的数据,但是该对象可以继续使用;
    一级缓存的实现
    • 对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;
    • 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
    • 如果命中,则直接将缓存结果返回;
    • 如果没命中:
      1. 去数据库中查询数据,得到查询结果;
      2. 将key和查询到的结果分别作为key,value对存储到Cache中;
      3. 将查询结果返回;
    • 结束。
    Cache接口的设计以及CacheKey的定义
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
            BoundSql boundSql = ms.getBoundSql(parameter);
            //构建缓存需要的key值
            CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
            return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    
    //构建缓存需要的key值
    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
            if(this.closed) {
                throw new ExecutorException("Executor was closed.");
            } else {
                CacheKey cacheKey = new CacheKey();
                cacheKey.update(ms.getId());
                cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
                cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
                cacheKey.update(boundSql.getSql());
                List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
                TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    
                for(int i = 0; i < parameterMappings.size(); ++i) {
                    ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
                    if(parameterMapping.getMode() != ParameterMode.OUT) {
                        String propertyName = parameterMapping.getProperty();
                        Object value;
                        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 = this.configuration.newMetaObject(parameterObject);
                            value = metaObject.getValue(propertyName);
                        }
    
                        cacheKey.update(value);
                    }
                }
    
                return cacheKey;
            }
        }
    

    调用如下方法时,debug源码的截图
    //mapper接口的方法 schoolCustomerDao.selectBySome(1l, "2017-09-17","120706049");

    Debug调试截图

    CacheKey构建好了之后,就可以存储查询后的结果了,源码如下所示:

    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
            this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
    
            List list;
            try {
                list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
            } finally {
                this.localCache.removeObject(key);
            }
            //缓存查询的结果
            this.localCache.putObject(key, list);
            if(ms.getStatementType() == StatementType.CALLABLE) {
                this.localOutputParameterCache.putObject(key, parameter);
            }
    
            return list;
        }
    
    

    得出CacheKey由以下条件可以得到:statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值

    一级缓存的性能分析

    我将从两个 一级缓存的特性来讨论SqlSession的一级缓存性能问题:

    • MyBatis对会话(Session)级别的一级缓存设计的比较简单,就简单地使用了HashMap来维护,并没有对HashMap的容量和大小进行限制。
      读者有可能就觉得不妥了:如果我一直使用某一个SqlSession对象查询数据,这样会不会导致HashMap太大,而导致 java.lang.OutOfMemoryError错误啊? 读者这么考虑也不无道理,不过MyBatis的确是这样设计的。
      MyBatis这样设计也有它自己的理由:
    1. 一般而言SqlSession的生存时间很短。一般情况下使用一个SqlSession对象执行的操作不会太多,执行完就会消亡;
    2. 对于某一个SqlSession对象而言,只要执行update操作(update、insert、delete),都会将这个SqlSession对象中对应的一级缓存清空掉,所以一般情况下不会出现缓存过大,影响JVM内存空间的问题;
    3. 可以手动地释放掉SqlSession对象中的缓存。
    • 一级缓存是一个粗粒度的缓存,没有更新缓存和缓存过期的概念
      MyBatis的一级缓存就是使用了简单的HashMap,MyBatis只负责将查询数据库的结果存储到缓存中去, 不会去判断缓存存放的时间是否过长、是否过期,因此也就没有对缓存的结果进行更新这一说了。

    根据一级缓存的特性,在使用的过程中,我认为应该注意:

    1. 对于数据变化频率很大,并且需要高时效准确性的数据要求,我们使用SqlSession查询的时候,要控制好SqlSession的生存时间,SqlSession的生存时间越长,它其中缓存的数据有可能就越旧,从而造成和真实数据库的误差;同时对于这种情况,用户也可以手动地适时清空SqlSession中的缓存;
    2. 对于只执行、并且频繁执行大范围的select操作的SqlSession对象,SqlSession对象的生存时间不应过长。

    相关文章

      网友评论

        本文标题:Mybatis原理--缓存机制(一级缓存)

        本文链接:https://www.haomeiwen.com/subject/yifxsxtx.html