手把手带你阅读Mybatis源码(三)缓存篇

前言

大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读Mybatis源码(一)构造篇 和 手把手带你阅读Mybatis源码(二)执行篇,主要说明了MyBatis是如何将我们的xml配置文件构建为其内部的Configuration对象和MappedStatement对象的,然后在第二篇我们说了构建完成后MyBatis是如何一步一步地执行我们的SQL语句并且对结果集进行封装的。

那么这篇作为MyBatis系列的最后一篇,自然是要来聊聊MyBatis中的一个不可忽视的功能,一级缓存和二级缓存

何谓缓存?

虽然这篇说的是MyBatis的缓存,但是我希望正在学习计算机的小伙伴即使还没有使用过MyBatis框架也能看明白今天这篇文章。

缓存是什么?我来说说个人的理解,最后再上比较官方的概念。

缓存(Cache),顾名思义,有临时存储的意思。计算机中的缓存,我们可以直接理解为,存储在内存中的数据的容器,这与物理存储是有差别的,由于内存的读写速度比物理存储高出几个数量级,所以程序直接从内存中取数据和从物理硬盘中取数据的效率是不同的,所以有一些经常需要读取的数据,设计师们通常会将其放在缓存中,以便于程序对其进行读取。

但是,缓存是有代价的,刚才我们说过,缓存就是在内存中的数据的容器,一条64G的内存条,通常可以买3-4块1T-2T的机械硬盘了,所以缓存不能无节制地使用,这样成本会剧增,所以一般缓存中的数据都是需要频繁查询,但是又不常修改的数据

而在一般业务中,查询通常会经过如下步骤。

读操作 --> 查询缓存中已经存在数据 -->如果不存在则查询数据库,如果存在则直接查询缓存-->数据库查询返回数据的同时,写入缓存中。

写操作 --> 清空缓存数据 -->写入数据库

缓存流程

比较官方的概念

? 缓存就是数据交换的缓冲区(称作:Cache),当某一硬件要读取数据时,会首先从缓存汇总查询数据,有则直接执行,不存在时从内存中获取。由于缓存的数据比内存快的多,所以缓存的作用就是帮助硬件更快的运行。

? 缓存往往使用的是RAM(断电既掉的非永久存储),所以在用完后还是会把文件送到硬盘等存储器中永久存储。电脑中最大缓存就是内存条,硬盘上也有16M或者32M的缓存。

? 高速缓存是用来协调CPU与主存之间存取速度的差异而设置的。一般CPU工作速度高,但内存的工作速度相对较低,为了解决这个问题,通常使用高速缓存,高速缓存的存取速度介于CPU与主存之间。系统将一些CPU在最近几个时间段经常访问的内容存在高速缓存,这样就在一定程度上缓解了由于主存速度低造成的CPU“停工待料”的情况。

? 缓存就是把一些外存上的数据保存在内存上而已,为什么保存在内存上,我们运行的所有程序里面的变量都是存放在内存中的,所以如果想将值放入内存上,可以通过变量的方式存储。在JAVA中一些缓存一般都是通过Map集合来实现的。

MyBatis的缓存

在说MyBatis的缓存之前,先了解一下Java中的缓存一般都是怎么实现的,我们通常会使用Java中的Map,来实现缓存,所以在之后的缓存这个概念,就可以把它直接理解为一个Map,存的就是键值对。

一级缓存简介

MyBatis中的一级缓存,是默认开启且无法关闭的一级缓存默认的作用域是一个SqlSession,解释一下,就是当SqlSession被构建了之后,缓存就存在了,只要这个SqlSession不关闭,这个缓存就会一直存在,换言之,只要SqlSession不关闭,那么这个SqlSession处理的同一条SQL就不会被调用两次,只有当会话结束了之后,这个缓存才会一并被释放。

虽说我们不能关闭一级缓存,但是作用域是可以修改的,比如可以修改为某个Mapper。

一级缓存的生命周期:

1、如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。

2、如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。

3、SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用。

节选自:https://www.cnblogs.com/happyflyingpig/p/7739749.html

MyBatis一级缓存简单示意图

二级缓存简介

MyBatis的二级缓存是默认关闭的,如果要开启有两种方式:

1.在mybatis-config.xml中加入如下配置片段

     <!-- 全局配置参数,需要时再设置 -->     <settings>            <!-- 开启二级缓存  默认值为true -->         <setting name="cacheEnabled" value="true"/>

     </settings>

2.在mapper.xml中开启

     <!--开启本mapper的namespace下的二级缓存-->     <!--             eviction:代表的是缓存回收策略,目前MyBatis提供以下策略。             (1) LRU,最近最少使用的,一处最长时间不用的对象             (2) FIFO,先进先出,按对象进入缓存的顺序来移除他们             (3) SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象             (4) WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。                 这里采用的是LRU,  移除最长时间不用的对形象

             flushInterval:刷新间隔时间,单位为毫秒,如果你不配置它,那么当             SQL被执行的时候才会去刷新缓存。

             size:引用数目,一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。             这里配置的是1024个对象

             readOnly:只读,意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有             办法修改缓存,他的默认值是false,不允许我们修改      -->     <cache eviction="回收策略" type="缓存类"/>

二级缓存的作用域与一级缓存不同,一级缓存的作用域是一个SqlSession,但是二级缓存的作用域是一个namespace,什么意思呢,你可以把它理解为一个mapper,在这个mapper中操作的所有SqlSession都可以共享这个二级缓存。但是假设有两条相同的SQL,写在不同的namespace下,那这个SQL就会被执行两次,并且产生两份value相同的缓存。

MyBatis缓存的执行流程

依旧是用前两篇的测试用例,我们从源码的角度看看缓存是如何执行的。

public static void main(String[] args) throws Exception {    String resource = "mybatis.xml";    InputStream inputStream = Resources.getResourceAsStream(resource);    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);    SqlSession sqlSession = sqlSessionFactory.openSession();    //从调用者角度来讲 与数据库打交道的对象 SqlSession    DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);    Map<String,Object> map = new HashMap<>();    map.put("id","2121");    //执行这个方法实际上会走到invoke    System.out.println(mapper.selectAll(map));    sqlSession.close();    sqlSession.commit();  }

这里会执行到query()方法:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)      throws SQLException {    //二级缓存的Cache,通过MappedStatement获取    Cache cache = ms.getCache();    if (cache != null) {      //是否需要刷新缓存      //在<select>标签中也可以配置flushCache属性来设置是否查询前要刷新缓存,默认增删改刷新缓存查询不刷新      flushCacheIfRequired(ms);      //判断这个mapper是否开启了二级缓存      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;      }    }    //如果二级缓存为null,那么直接查询一级缓存    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}

可以看到首先MyBatis在查询数据时会先看看这个mapper是否开启了二级缓存,如果开启了,会先查询二级缓存,如果缓存中存在我们需要的数据,那么直接就从缓存返回数据,如果不存在,则继续往下走查询逻辑。

接着往下走,如果二级缓存不存在,那么就直接查询数据了吗?答案是否定的,二级缓存如果不存在,MyBatis会再查询一次一级缓存,接着往下看。

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++;      //查询一级缓存(localCache)      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;      if (list != null) {          //对于存储过程有输出资源的处理        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);      } else {          //如果缓存为空,则从数据库拿        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);         /**这个是queryFromDatabase的逻辑         * //先往缓存中put一个占位符            localCache.putObject(key, EXECUTION_PLACEHOLDER);            try {              list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);            } finally {              localCache.removeObject(key);            }            //往一级缓存中put真实数据            localCache.putObject(key, list);            if (ms.getStatementType() == StatementType.CALLABLE) {              localOutputParameterCache.putObject(key, parameter);            }            return list;         */      }    } 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;  }

一级缓存和二级缓存的查询逻辑其实差不多,都是先查询缓存,如果没有则进行下一步查询,只不过一级缓存中如果没有结果,那么就直接查询数据库,然后回写一级缓存。

讲到这里其实一级缓存和二级缓存的执行流程就说完了,缓存的逻辑其实都差不多,MyBatis的缓存是先查询一级缓存再查询二级缓存

但是文章到这里并没有结束,还有一些缓存相关的问题可以聊。

缓存事务问题

不知道这个问题大家有没有想过,假设有这么一个场景,这里用二级缓存举例,因为二级缓存是跨事务的。

假设我们在查询之前开启了事务,并且进行数据库操作:

1.往数据库中插入一条数据(INSERT)

2.在同一个事务内查询数据(SELECT)

3.提交事务(COMMIT)

4.提交事务失败(ROLLBACK)

我们来分析一下这个场景,首先SqlSession先执行了一个INSERT操作,很显然,在我们刚才分析的逻辑基础上,此时缓存一定会被清空,然后在同一个事务下查询数据,数据又从数据库中被加载到了缓存中,此时提交事务,然后事务提交失败了。

考虑一下此时会出现什么情况,相信已经有人想到了,事务提交失败之后,事务会进行回滚,那么执行INSERT插入的这条数据就被回滚了,但是我们在插入之后进行了一次查询,这个数据已经放到了缓存中,下一次查询必然是直接查询缓存而不会再去查询数据库了,可是此时缓存和数据库之间已经存在了数据不一致的问题。

问题的根本原因就在于,数据库提交事务失败了可以进行回滚,但是缓存不能进行回滚

我们来看看MyBatis是如何解决这个问题的。

TransactionalCacheManager

这个类是MyBatis用于缓存事务管理的类,我们可以看看其数据结构。

  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);    }

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

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

    private TransactionalCache getTransactionalCache(Cache cache) {      return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);    }

  }

TransactionalCacheManager中封装了一个Map,用于将事务缓存对象缓存起来,这个Map的Key是我们的二级缓存对象,而Value是一个叫做TransactionalCache,顾名思义,这个缓存就是事务缓存,我们来看看其内部的实现。

  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;    //未命中的缓存集合,防止击穿缓存,并且如果查询到的数据为null,说明要通过数据库查询,有可能存在数据不一致,都记录到这个地方    private final Set<Object> entriesMissedInCache;

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

    @Override    public String getId() {      return delegate.getId();    }

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

    @Override    public Object getObject(Object key) {      // issue #116      Object object = delegate.getObject(key);      if (object == null) {          //如果取出的是空,那么放到未命中缓存,并且在查询数据库之后putObject中将本应该放到真实缓存中的键值对放到待提交事务缓存        entriesMissedInCache.add(key);      }      //如果不为空      // issue #146      //查看缓存清空标识是否为false,如果事务提交了就为true,事务提交了会更新缓存,所以返回null。      if (clearOnCommit) {        return null;      } else {          //如果事务没有提交,那么返回原先缓存中的数据,        return object;      }    }

    @Override    public void putObject(Object key, Object object) {        //如果返回的数据为null,那么有可能到数据库查询,查询到的数据先放置到待提交事务的缓存中        //本来应该put到缓存中,现在put到待提交事务的缓存中去。      entriesToAddOnCommit.put(key, object);    }

    @Override    public Object removeObject(Object key) {      return null;    }

    @Override    public void clear() {        //如果事务提交了,那么将清空缓存提交标识设置为true      clearOnCommit = true;      //清空entriesToAddOnCommit      entriesToAddOnCommit.clear();    }

    public void commit() {      if (clearOnCommit) {          //如果为true,那么就清空缓存。        delegate.clear();      }      //把本地缓存刷新到真实缓存。      flushPendingEntries();      //然后将所有值复位。      reset();    }

    public void rollback() {        //事务回滚      unlockMissedEntries();      reset();    }

    private void reset() {        //复位操作。      clearOnCommit = false;      entriesToAddOnCommit.clear();      entriesMissedInCache.clear();    }

    private void flushPendingEntries() {        //遍历事务管理器中待提交的缓存      for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {          //写入到真实的缓存中。        delegate.putObject(entry.getKey(), entry.getValue());      }      for (Object entry : entriesMissedInCache) {          //把未命中的一起put        if (!entriesToAddOnCommit.containsKey(entry)) {          delegate.putObject(entry, null);        }      }    }

    private void unlockMissedEntries() {      for (Object entry : entriesMissedInCache) {          //清空真实缓存区中未命中的缓存。      try {          delegate.removeObject(entry);        } catch (Exception e) {          log.warn("Unexpected exception while notifiying a rollback to the cache adapter."              + "Consider upgrading your cache adapter to the latest version.  Cause: " + e);        }      }    }

  }

在TransactionalCache中有一个真实缓存对象Cache,这个真实缓存对象就是我们真正的二级缓存,还有一个 entriesToAddOnCommit,这个Map对象中存放的是所有待提交事务的缓存

我们在二级缓存执行的代码中,看到在缓存中get或者put结果时,都是叫tcm的对象调用了getObject()方法和putObject()方法,这个对象实际上就是TransactionalCacheManager的实体对象,而这个对象实际上是调用了TransactionalCache的方法,我们来看看这两个方法是如何实现的。

  @Override  public Object getObject(Object key) {      // issue #116      Object object = delegate.getObject(key);      if (object == null) {          //如果取出的是空,那么放到未命中缓存,并且在查询数据库之后putObject中将本应该放到真实缓存中的键值对放到待提交事务缓存        entriesMissedInCache.add(key);      }      //如果不为空      // issue #146      //查看缓存清空标识是否为false,如果事务提交了就为true,事务提交了会更新缓存,所以返回null。      if (clearOnCommit) {        return null;      } else {          //如果事务没有提交,那么返回原先缓存中的数据,        return object;      }  }  @Override  public void putObject(Object key, Object object) {        //如果返回的数据为null,那么有可能到数据库查询,查询到的数据先放置到待提交事务的缓存中        //本来应该put到缓存中,现在put到待提交事务的缓存中去。      entriesToAddOnCommit.put(key, object);  }

在getObject()方法中存在两个分支:

如果发现缓存中取出的数据为null,那么会把这个key放到entriesMissedInCache中,这个对象的主要作用就是将我们未命中的key全都保存下来,防止缓存被击穿,并且当我们在缓存中无法查询到数据,那么就有可能到一级缓存和数据库中查询,那么查询过后会调用putObject()方法,这个方法本应该将我们查询到的数据put到真是缓存中,但是现在由于存在事务,所以暂时先放到entriesToAddOnCommit中。

如果发现缓存中取出的数据不为null,那么会查看事务提交标识(clearOnCommit)是否为true,如果为true,代表事务已经提交了,之后缓存会被清空,所以返回null,如果为false,那么由于事务还没有被提交,所以返回当前缓存中存的数据。

那么当事务提交成功或提交失败,又会是什么状况呢?不妨看看commit和rollback方法。

  public void commit() {      if (clearOnCommit) {          //如果为true,那么就清空缓存。        delegate.clear();      }      //把本地缓存刷新到真实缓存。      flushPendingEntries();      //然后将所有值复位。      reset();  }

  public void rollback() {        //事务回滚      unlockMissedEntries();      reset();  }

先分析事务提交成功的情况,如果事务正常提交了,那么会有这么几步操作:

  1. 清空真实缓存。
  2. 将本地缓存(未提交的事务缓存 entriesToAddOnCommit)刷新到真实缓存。
  3. 将所有值复位。

我们来看看代码是如何实现的:

  private void flushPendingEntries() {        //遍历事务管理器中待提交的缓存      for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {          //写入到真实的缓存中。        delegate.putObject(entry.getKey(), entry.getValue());      }      for (Object entry : entriesMissedInCache) {          //把未命中的一起put        if (!entriesToAddOnCommit.containsKey(entry)) {          delegate.putObject(entry, null);        }      }  }  private void reset() {        //复位操作。      clearOnCommit = false;      entriesToAddOnCommit.clear();      entriesMissedInCache.clear();  }  public void clear() {      //如果事务提交了,那么将清空缓存提交标识设置为true      clearOnCommit = true;      //清空事务提交缓存      entriesToAddOnCommit.clear();  }

清空真实缓存就不说了,就是Map调用clear方法,清空所有的键值对。

将未提交事务缓存刷新到真实缓存,首先会遍历entriesToAddOnCommit,然后调用真实缓存的putObject方法,将entriesToAddOnCommit中的键值对put到真实缓存中,这步完成后,还会将未命中缓存中的数据一起put进去,值设置为null。

最后进行复位,将提交事务标识设为false,未命中缓存、未提交事务缓存中的所有数据全都清空。

如果事务没有正常提交,那么就会发生回滚,再来看看回滚是什么流程:

  1. 清空真实缓存中未命中的缓存。
  2. 将所有值复位
  public void rollback() {        //事务回滚      unlockMissedEntries();      reset();  }

  private void unlockMissedEntries() {      for (Object entry : entriesMissedInCache) {          //清空真实缓存区中未命中的缓存。      try {          delegate.removeObject(entry);        } catch (Exception e) {          log.warn("Unexpected exception while notifiying a rollback to the cache adapter."              + "Consider upgrading your cache adapter to the latest version.  Cause: " + e);        }      }  }

由于凡是在缓存中未命中的key,都会被记录到entriesMissedInCache这个缓存中,所以这个缓存中包含了所有查询数据库的key,所以最终只需要在真实缓存中把这部分key和对应的value给删除即可。

缓存事务总结

简而言之,缓存事务的控制主要是通过TransactionalCacheManager控制TransactionCache完成的,关键就在于TransactionCache中的entriesToAddCommit和entriesMissedInCache这两个对象,entriesToAddCommit在事务开启到提交期间作为真实缓存的替代品,将从数据库中查询到的数据先放到这个Map中,待事务提交后,再将这个对象中的数据刷新到真实缓存中,如果事务提交失败了,则清空这个缓存中的数据即可,并不会影响到真实的缓存。

entriesMissedInCache主要是用来保存在查询过程中在缓存中没有命中的key,由于没有命中,说明需要到数据库中查询,那么查询过后会保存到entriesToAddCommit中,那么假设在事务提交过程中失败了,而此时entriesToAddCommit的数据又都刷新到缓存中了,那么此时调用rollback就会通过entriesMissedInCache中保存的key,来清理真实缓存,这样就可以保证在事务中缓存数据与数据库的数据保持一致。

缓存事务

一些使用缓存的经验

二级缓存不能存在一直增多的数据

由于二级缓存的影响范围不是SqlSession而是namespace,所以二级缓存会在你的应用启动时一直存在直到应用关闭,所以二级缓存中不能存在随着时间数据量越来越大的数据,这样有可能会造成内存空间被占满。

二级缓存有可能存在脏读的问题(可避免)

由于二级缓存的作用域为namespace,那么就可以假设这么一个场景,有两个namespace操作一张表,第一个namespace查询该表并回写到内存中,第二个namespace往表中插一条数据,那么第一个namespace的二级缓存是不会清空这个缓存的内容的,在下一次查询中,还会通过缓存去查询,这样会造成数据的不一致。

所以当项目里有多个命名空间操作同一张表的时候,最好不要用二级缓存,或者使用二级缓存时避免用两个namespace操作一张表。

Spring整合MyBatis缓存失效问题

一级缓存的作用域是SqlSession,而使用者可以自定义SqlSession什么时候出现什么时候销毁,在这段期间一级缓存都是存在的。
当使用者调用close()方法之后,就会销毁一级缓存。

但是,我们在和Spring整合之后,Spring帮我们跳过了SqlSessionFactory这一步,我们可以直接调用Mapper,导致在操作完数据库之后,Spring就将SqlSession就销毁了,一级缓存就随之销毁了,所以一级缓存就失效了。

那么怎么能让缓存生效呢

  1. 开启事务,因为一旦开启事务,Spring就不会在执行完SQL之后就销毁SqlSession,因为SqlSession一旦关闭,事务就没了,一旦我们开启事务,在事务期间内,缓存会一直存在。
  2. 使用二级缓存。

结语

Hello world.

原文地址:https://www.cnblogs.com/javazhiyin/p/12357397.html

时间: 2024-09-27 04:27:45

手把手带你阅读Mybatis源码(三)缓存篇的相关文章

手把手带你阅读Mybatis源码(二)执行篇

前言 上一篇文章提到了MyBatis是如何构建配置类的,也说了MyBatis在运行过程中主要分为两个阶段,第一是构建,第二就是执行,所以这篇文章会带大家来了解一下MyBatis是如何从构建完毕,到执行我们的第一条SQL语句的.之后这部分内容会归置到公众号菜单栏:连载中…-框架分析中,欢迎探讨! 入口(代理对象的生成) public static void main(String[] args) throws Exception { /******************************构

spring事务源码分析结合mybatis源码(三)

下面将结合mybatis源码来分析下,这种持久化框架是如何对connection使用,来达到spring事务的控制. 想要在把mybatis跟spring整合都需要这样一个jar包:mybatis-spring-x.x.x.jar,这里面定义了一些主要的整合信息. 在spring配置文件中需要配置如下两个bean: <!-- mybatis配置 --> <bean id="sqlSessionFactory" class="org.mybatis.sprin

问答形式阅读jQuery源码(三)

通过艾伦的博客,我们能看出,jQuery的promise和其他回调都是通过jQuery.Callbacks实现的.所以我们一起简单看看jQuery.Deferred和jQuery.Callbacks.来看看关于他们的一些提问. 提问:jQuery.Callbacks的配置为什么是用字符串参数? jQuery.Callbacks有四种配置,分别是once.memory.unique.stopOnFalse.而jQuery.Callbacks的配置形式却和以往我们熟悉的不同,不是使用json,而是使

jQuery2.x源码解析(缓存篇)

缓存是jQuery中的又一核心设计,jQuery自身的很多东西都依赖于缓存,比如事件.一些中间变量.动画等.同时他还为用户提供接口了使用缓存的接口,方便用户在元素节点上保存自己的数据,并且帮助用户解决直接把数据保存到DOM元素是可能引起的内存泄漏.命名冲突等问题. 同时,html5提出了一种通过属性缓存元素数据的功能,就是data-*属性,他可以以字符串的形式保存数据,并且不会和元素固有属性冲突.jQuery的缓存提供了访问data-*的接口,与html5标准结合更加紧密,更加规范. 提问:jQ

深入浅出Mybatis系列(三)---配置详解之properties与environments(mybatis源码篇)[转]

上篇文章<深入浅出Mybatis系列(二)---配置简介(mybatis源码篇)>我们通过对mybatis源码的简单分析,可看出,在mybatis配置文件中,在configuration根节点下面,可配置properties.typeAliases.plugins.objectFactory.objectWrapperFactory.settings.environments.databaseIdProvider.typeHandlers.mappers这些节点.那么本次,就会先介绍prope

MyBatis 源码分析 - 插件机制

1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 MyBatis 为例,我们可基于 MyBatis 插件机制实现分页.分表,监控等功能.由于插件和业务无关,业务也无法感知插件的存在.因此可以无感植入插件,在无形中增强功能. 开发 MyBatis 插件需要对 MyBatis 比较深了解才行,一般来说最好能够掌握 MyBatis 的源码,门槛相对较高.本篇

MyBatis 源码分析系列文章合集

1.简介 我从七月份开始阅读MyBatis源码,并在随后的40天内陆续更新了7篇文章.起初,我只是打算通过博客的形式进行分享.但在写作的过程中,发现要分析的代码太多,以至于文章篇幅特别大.在这7篇文章中,有4篇文章字数超过了1万,最长的一篇文章约有2.7万字(含代码).考虑到超长文章对读者不太友好,以及拆分文章工作量也不小等问题.遂决定将博文整理成电子书,方便大家阅读. 经过两周紧张的排版,<一本小小的MyBatis源码分析书>诞生了.本书共7章,约300页.本书以电子书的形式发布,大家可自由

对vue源码之缓存的研究--------------引用

探索vue源码之缓存篇 一.从链表说起 首先我们来看一下链表的定义: 链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer) 其中的双向链表是我们今天的主角: 双向链表也叫双链表.双向链表中不仅有指向后一个节点的指针,还有指向前一个节点的指针.这样可以从任何一个节点访问前一个节点,当然也可以访问后一个节点,以至整个链表.一般是在需要大批量的另外储存数据在链表中的位置的时候用. 图示如下 想象一

带你逐行阅读redux源码

带你逐行阅读redux源码 redux版本:2019-7-17最新版:v4.0.4 git 地址:https://github.com/reduxjs/redux/tree/v4.0.4 redux目录结构 +-- src // redux的核心内容目录 | +-- utils // redux的核心工具库 | | +-- actionTypes.js // 一些默认的随机actionTypes | | +-- isPlainObject.js // 判断是否是字面变量或者new出来的objec