前言

  mybatis的缓存大家都知道分为一级和二级缓存,一级缓存系统默认使用,二级缓存默认开启,但具体用的时候需要我们自己手动配置。我们依旧还是先看一个demo。这儿只贴出关键代码

  1. public interface AssetInfoMapper {
  2.  
  3. List<AssetInfo> get(@Param("name") String name, @Param("id")String id);
  4. }
  1. <mapper namespace="com.test.mapper.AssetInfoMapper">
  2. <cache></cache>
  3.  
  4. <select id="get" resultType="com.megalith.entity.AssetInfo" useCache="true">
  5. select * from asset_info where id =#{id} and `name` = #{name}
  6. </select>
  7. </mapper>
  8.  
  1. @Autowired
  2. private SqlSessionFactory sqlSessionFactory;
  3.  
  4. @GetMapping("/get")
  5. public List<AssetInfo> get() {
  6. SqlSession sqlSession = sqlSessionFactory.openSession();
  7. AssetInfoMapper mapper = sqlSession.getMapper(AssetInfoMapper.class);
  8. List<AssetInfo> test = mapper.get("测试删除" , "123123123");
  9. System.out.println("获取test0的值:"+test.get(0).getName());
  10. test.get(0).setName("测试bug");
  11. List<AssetInfo> test2 = mapper.get("测试删除" , "123123123");
  12. System.out.println("获取test0的值:"+test2.get(0).getName());
  13.  
  14. System.out.println(test == test2);
  15. System.out.println("---------------------------------");
  16. sqlSession.close();
  17. sqlSession = sqlSessionFactory.openSession();
  18. mapper = sqlSession.getMapper(AssetInfoMapper.class);
  19. List<AssetInfo> test3 = mapper.get("测试删除" , "123123123");
  20. System.out.println(test == test3);
  21. System.out.println("获取test0的值:"+test3.get(0).getName());
  22. return test;
  23. }
  1. 另外在初始化的SqlSessionFactoryBean中配置Conguration时配置上日志打印,方便观察

  

我们执行上面的方法后结果如下

  我们会发现

  通过控制台发现以下情况,至于原因则看完了源代码之后相信会理解为何会有这个结果

  1.只会执行一次sql

  2.第一次和第二次获取的结果相同,第一次和第三次的结果不同。

  3.第一次获取到值后修改其属性,影响了第二次及第三次查询出来的结果。

正文

1 二级缓存

  说起二级缓存,得先说下二级缓存的设置,我们初始化mybatis在配置configuration的时候有个属性叫做cacheEnabled,这个属性可以手动设置,不设置的话默认为true,说明系统默认是支持二级缓存的,那二级缓存有没有初始化的地方呢,这个又要回到我们解析xxMapper.xml文件中的代码了。我们先来回顾下XmlCongiBuilder.parse()方法中

  1. public void parse() {
  2. if (!configuration.isResourceLoaded(resource)) {
  3. configurationElement(parser.evalNode("/mapper"));
  4. ...
  5. }
  6. }
  1. private void configurationElement(XNode context) {
  2. try {
  3. ...
  4. cacheElement(context.evalNode("cache"));
  5. ...
  6. } catch (Exception e) {
  7. throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  8. }
  9. }

  可以看到 在解析mapper.xml文件时,会找到mapper节点下的cache节点,

  1. private void cacheElement(XNode context) {
  2. //如果不为空则执行下面的逻辑
  3. if (context != null) {
  4. //获取缓存type 没有则默认为PERPETUAL
  5. String type = context.getStringAttribute("type", "PERPETUAL");
  6. //获取缓存type 看下类型是否是别名已注册了的
  7. Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
  8. //获取缓存算法 默认为LRU 淘汰最近最小使用
  9. String eviction = context.getStringAttribute("eviction", "LRU");
  10. //获取其别名类型
  11. Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
  12. //获取刷新间隔
  13. Long flushInterval = context.getLongAttribute("flushInterval");
  14. //获取缓存大小
  15. Integer size = context.getIntAttribute("size");
  16. //是否只读 默认false
  17. boolean readWrite = !context.getBooleanAttribute("readOnly", false);
  18. //是否有锁 默认false
  19. boolean blocking = context.getBooleanAttribute("blocking", false);
  20. //获取里面值
  21. Properties props = context.getChildrenAsProperties();
  22. //创建缓存
  23. builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  24. }
  25. }

  注意这儿总体的逻辑大致是判断mapper节点下是否有cache节点,如果有则获取其值并构建缓存。所以我们可以得出二级缓存是为每个mapper构建了一个缓存,并且需要我们在mapper中设置cache节点才能生效,二级缓存是mapper级别的缓存。我们可以再看下具体的构建逻辑

  

  1. public Cache useNewCache(Class<? extends Cache> typeClass,
  2. Class<? extends Cache> evictionClass,
  3. Long flushInterval,
  4. Integer size,
  5. boolean readWrite,
  6. boolean blocking,
  7. Properties props) {
  8. //构建cache
  9. Cache cache = new CacheBuilder(currentNamespace) //currentNamespace 即缓存的id
  10. .implementation(valueOrDefault(typeClass, PerpetualCache.class)) //缓存实现
  11. .addDecorator(valueOrDefault(evictionClass, LruCache.class)) //缓存算法
  12. .clearInterval(flushInterval) //刷新间隔
  13. .size(size) //缓存大小
  14. .readWrite(readWrite) //是否只读
  15. .blocking(blocking) //是否有锁
  16. .properties(props) //cache节点中的值
  17. .build(); //最终构建
  18. //在configuration中添加cache
  19. configuration.addCache(cache);
  20. //设置当前Builder的cache
  21. currentCache = cache;
  22. return cache;
  23. }
  24. /*********configuration中********/
  25. protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
  26. public void addCache(Cache cache) {
  27. caches.put(cache.getId(), cache);
  28. }

  这儿可以看出主要是根据我们传入的值采用构建模式构建一个缓存,缓存id为nameSpace。最终存到了configuration中caches。我们注意这儿设置了一个currentCache,这个有什么用呢,其实在解析xml构建每个方法的MappedStatment时会设置到cache中,我们可以看下解析每个sql方法时其中有一个步骤

  1. MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
  2. .resource(resource)
  3. .fetchSize(fetchSize)
  4. .timeout(timeout)
  5. .statementType(statementType)
  6. .keyGenerator(keyGenerator)
  7. .keyProperty(keyProperty)
  8. .keyColumn(keyColumn)
  9. .databaseId(databaseId)
  10. .lang(lang)
  11. .resultOrdered(resultOrdered)
  12. .resultSets(resultSets)
  13. .resultMaps(getStatementResultMaps(resultMap, resultType, id))
  14. .resultSetType(resultSetType)
  15. .flushCacheRequired(valueOrDefault(flushCache, !isSelect)) //flushCache即该方法是否刷新缓存,如果没设置那么默认 select不刷新,其他更新操作默认刷新
  16. .useCache(valueOrDefault(useCache, isSelect)) //useCache即是否使用cache,如果没设置 那么默认 select使用 其他更新操作不使用
  17. .cache(currentCache);//设置当前的缓存
  18.  
  19. private <T> T valueOrDefault(T value, T defaultValue) {
  20. return value == null ? defaultValue : value;
  21. }

  通过这个方法构建的最后一步可以知道,mapper中的方法默认使用的都是一个缓存管理器。还有两个构建步骤需要注意即flushCacheRequired & useCache。这个注释已经写的比较明确,在后面我们使用二级缓存的时候,这两个属性非常重要,所以还请大家牢记

  到此,二级缓存的初始化已经解析完毕,可以发现mybatis在解析mapper.xml的时候就已经为每个mapper生成了一个缓存管理器(如果配置了cache节点),那程序中是如何使用的呢?我们接着往下看

  本文假设读者已经了解了mybatis的查询流程,如果不了解的建议先看我的前三篇博文

  1.1 二级缓存使用的前置CachingExecutor

  我们在查询时使用SqlSessionFactory.openSession方法时会同时构建一个Executor作为执行器,我们再简要看下创建Executor的逻辑

  1. public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  2. executorType = executorType == null ? defaultExecutorType : executorType;
  3. executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  4. Executor executor;
  5. if (ExecutorType.BATCH == executorType) {
  6. executor = new BatchExecutor(this, transaction);
  7. } else if (ExecutorType.REUSE == executorType) {
  8. executor = new ReuseExecutor(this, transaction);
  9. } else {
  10. executor = new SimpleExecutor(this, transaction);
  11. }
  12. if (cacheEnabled) { //判断是否使用缓存
  13. executor = new CachingExecutor(executor);
  14. }
  15. executor = (Executor) interceptorChain.pluginAll(executor);
  16. return executor;
  17. }

  其中有一步需要注意if (cacheEnabled)  ,如果条件成立  那么就会用静态代理的方法创建一个CachingExecutor,这个cacheEnabled则是我们的configuration的cacheEnabled属性,是我们一开始设置的。也可以不设置 ,默认为true,即默认开启二级缓存。所以我们会使用CachingExecutor来进行sql操作,那么具体的存储逻辑呢?  我们则来看一下CachingExecutor的查询逻辑

  1.2构造cacheKey即缓存的key

    我们随便找到其中查询方法

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

  可以看到这儿根据我们的参数构造的了一个缓存的key,我们来具体看下创建缓存key的过程,最终调用的是BaseExecutor的方法

  1. @Override
  2. public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  3. if (closed) {
  4. throw new ExecutorException("Executor was closed.");
  5. }
  6. //创建一个缓存key
  7. CacheKey cacheKey = new CacheKey();
  8. //散列方法的类名+方法名
  9. cacheKey.update(ms.getId());
  10. //散列分页的开始数据位置
  11. cacheKey.update(rowBounds.getOffset());
  12. //散列分页的数据长度
  13. cacheKey.update(rowBounds.getLimit());
  14. //散列要执行的sql
  15. cacheKey.update(boundSql.getSql());
  16. //获取参数信息
  17. List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  18. //获取类型转换器
  19. TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
  20. // mimic DefaultParameterHandler logic
  21. for (ParameterMapping parameterMapping : parameterMappings) {
  22. if (parameterMapping.getMode() != ParameterMode.OUT) {
  23. Object value;
  24. String propertyName = parameterMapping.getProperty();
  25. if (boundSql.hasAdditionalParameter(propertyName)) {
  26. value = boundSql.getAdditionalParameter(propertyName);
  27. } else if (parameterObject == null) {
  28. value = null;
  29. } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
  30. value = parameterObject;
  31. } else {
  32. MetaObject metaObject = configuration.newMetaObject(parameterObject);
  33. value = metaObject.getValue(propertyName);
  34. }
  35. //参数值进行散列
  36. cacheKey.update(value);
  37. }
  38. }
  39. if (configuration.getEnvironment() != null) {
  40. // 如果指定了环境 那么对环境id进行散列
  41. cacheKey.update(configuration.getEnvironment().getId());
  42. }
  43. return cacheKey;
  44.  
  45. }
  46. /******************cacheKey*********************/
  47. public void update(Object object) {
  48. //每次都对hashcode新处理 并将传入的值加入updateList
  49.  
  50. int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
  51.   //参数数量+1
  52. count++;
    //核实码 即每个参数值的hashcode相加
  53. checksum += baseHashCode;
  54. baseHashCode *= count;
  55.  
  56. hashcode = multiplier * hashcode + baseHashCode;
  57.   //将传入的数据加入updateList
  58. updateList.add(object);
  59. }

  可以看到 这个核心的缓存key主要对方法的全限定名,分页信息,执行的sql,参数类型以及参数值,环境id 进行散列并且存储信息。所以我们可以得知 mybatis对两次查询是否完全一致的判定标准就是前面的这几项是否完全一致。那么是否是这样呢,我们可以看下其equals方法

  1. @Override
  2. public boolean equals(Object object) {
  3. //如果内存地址一致那肯定一致
  4. if (this == object) {
  5. return true;
  6. }
  7. //类型不同肯定不一致
  8. if (!(object instanceof CacheKey)) {
  9. return false;
  10. }
  11.  
  12. final CacheKey cacheKey = (CacheKey) object;
  13. //如果hashcode不同那肯定不同
  14. if (hashcode != cacheKey.hashcode) {
  15. return false;
  16. }
  17. //判断核实码
  18. if (checksum != cacheKey.checksum) {
  19. return false;
  20. }
  21. //判断参数数量
  22. if (count != cacheKey.count) {
  23. return false;
  24. }
  25. //判断每个传入的参数 即全限定名,分页信息,执行的sql,参数类型以及参数值,环境id 的equals是否满足
  26. for (int i = 0; i < updateList.size(); i++) {
  27. Object thisObject = updateList.get(i);
  28. Object thatObject = cacheKey.updateList.get(i);
  29. if (!ArrayUtil.equals(thisObject, thatObject)) {
  30. return false;
  31. }
  32. }
  33. return true;
  34. }

  通过这个方法的最后代码可以得知最后还是对每个参数进行了equals判断,所以满足上面的说法

  缓存key构造出来之后相信后面的逻辑大家也大概能理出来了,就是查询缓存,如果没有则查询数据库,然后把结果放入缓存,这是经典的缓存规则,不过mybatis的二级缓存并不是存储了就马上生效的。我们接着看

  1.3 缓存逻辑

    缓存key构造完成后 我们接着看查询逻辑,即1.2第一个代码块中的最后一句return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

  1. private final TransactionalCacheManager tcm = new TransactionalCacheManager();
  2.  
  3. @Override
  4. public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
  5. throws SQLException {
  6. //获取到方法对应mapper的cache
  7. Cache cache = ms.getCache();
  8. if (cache != null) {
  9. //如果该方法是更新操作 或者设置了刷新缓存 即flushCache 则刷新缓存
  10. flushCacheIfRequired(ms);
  11. //注意如果方法有resultHandler则不走缓存
  12. if (ms.isUseCache() && resultHandler == null) {
  13. //检查如果方法为存储过程 且有输出参数 那么会抛异常
  14. ensureNoOutParams(ms, boundSql);
  15. @SuppressWarnings("unchecked")
  16. //通过缓存管理器查询值
  17. List<E> list = (List<E>) tcm.getObject(cache, key);
  18. if (list == null) {
  19. //没查询到值或者之前的值为null,则从库查询
  20. list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  21. //将结果放入缓存管理器,注意此时还没有放入真正的缓存
  22. tcm.putObject(cache, key, list); // issue #578 and #116
  23. }
  24. //返回结果
  25. return list;
  26. }
  27. }
  28. //查库
  29. return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  30. }
  31.  
  32. //刷新缓存
  33. private void flushCacheIfRequired(MappedStatement ms) {
  34. Cache cache = ms.getCache();
  35. if (cache != null && ms.isFlushCacheRequired()) {
  36. tcm.clear(cache);
  37. }
  38. }

  这里面就用到了之前让大家注意看的构建MappedStatment时设置的那三个缓存有关的属性了。这儿则先获取到该方法对应的cache,即上面我们设置的currentCache.如果不为空则判断是否需要刷新刷新缓存,如果需要则刷新缓存,然后检查是否设置了usingCahce为true且该方法没有设置resultHandler。然后检查该方法为存储过程且有输出参数,如果是则抛异常。满足这些再从缓存中判断值,这儿的逻辑比较简单,都是经典的缓存处理原理。我们这儿可以得知  mapper中的设置了缓存节点,但select依旧不能用二级缓存的情况有 1.useCache设置为false,2.方法设置了ResultHandler,3.该方法sql为存储过程且有输出参数。同时注意如果flushCache设置为true那么每次调用都会刷新之前的缓存。

  但是到这儿缓存依旧没有完全生效,我们可以看到这儿缓存操作的都tcm  即名为TransactionalCacheManager 的类,我们看下我们用到的clear和putObject方法具体的逻辑是什么

  1.4 TransactionalCacheManager  缓存缓冲区管理器

 

  1. private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
  1.  
  2. private TransactionalCache getTransactionalCache(Cache cache) {
  3. return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  4. }
  5.  
  6. public void putObject(Cache cache, CacheKey key, Object value) {
  7. getTransactionalCache(cache).putObject(key, value);
  8. }
  1. public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
    }
  1.  

  可以看到这个类对我们传入的每个缓存key都会新建一个TransactionalCache对象,然后将这个存到map中 ,注意缓存的key为我们的cache管理器,传入TransactionalCache的是缓存key和缓存value,computeIfAbsent方法代表的意义是没有则新建一个,所以这儿会通过有参构造传入cache管理器创建一个TransactionalCache。所以我们看下这个类TransactionalCache的属性,以及我们调用的putObject逻辑

  1.5TransactionalCache 缓存缓冲区

  1. public class TransactionalCache implements Cache {
  2.  
  3. private static final Log log = LogFactory.getLog(TransactionalCache.class);
  4.  
  5. //缓存处理器
  6. private final Cache delegate;
  7. //默认false 如果执行 clear为true同时会清除缓存
  8. private boolean clearOnCommit;
  9. //待生效的缓存
  10. private final Map<Object, Object> entriesToAddOnCommit;
  11. //未命中的缓存
  12. private final Set<Object> entriesMissedInCache;
  13.  
  14. //构造方法
  15. public TransactionalCache(Cache delegate) {
  16. this.delegate = delegate;
  17. this.clearOnCommit = false;
  18. this.entriesToAddOnCommit = new HashMap<>();
  19. this.entriesMissedInCache = new HashSet<>();
  20. }
  21.  
  22. @Override
  23. public String getId() {
  24. return delegate.getId();
  25. }
  26.  
  27. @Override
  28. public int getSize() {
  29. return delegate.getSize();
  30. }
  31.  
  32. @Override
  33. public Object getObject(Object key) {
  34. // 缓存管理器拿到缓存
  35. Object object = delegate.getObject(key);
  36. if (object == null) {
  37. //没拿到则添加到未命中缓存
  38. entriesMissedInCache.add(key);
  39. }
  40. // 如果此时清空缓存的标识为ture 直接返回null
  41. if (clearOnCommit) {
  42. return null;
  43. } else {
  44. return object;
  45. }
  46. }
  47.  
  48. @Override
  49. public void putObject(Object key, Object object) {
  50. //添加到待提交的缓存
  51. entriesToAddOnCommit.put(key, object);
  52. }
  53.  
  54. @Override
  55. public Object removeObject(Object key) {
  56. return null;
  57. }
  58.  
  59. @Override
  60. public void clear() {
  61. //清除缓存
  62. clearOnCommit = true;
    //等待队列也被清空
  63. entriesToAddOnCommit.clear();
  64. }
  65. //执行提交
  66. public void commit() {
  67. //执行过clear方法,那么再次清除缓存管理器中的所有缓存
  68. if (clearOnCommit) {
  69. delegate.clear();
  70. }
  71. //将等待提交的缓存提交
  72. flushPendingEntries();
  73. //所有数据置为初始化状态
  74. reset();
  75. }
  76. //执行rollback时清除掉所有未命中的缓存 并且还会清空等待的缓存
  77. public void rollback() {
  78. unlockMissedEntries();
  79. reset();
  80. }
  81.  
  82. private void reset() {
  83. clearOnCommit = false;
  84. entriesToAddOnCommit.clear();
  85. entriesMissedInCache.clear();
  86. }
  87.  
  88. private void flushPendingEntries() {
  89. for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
  90. //存在真实缓存处理器中
  91. delegate.putObject(entry.getKey(), entry.getValue());
  92. }
  93. for (Object entry : entriesMissedInCache) {
  94. //将直到现在也为命中的缓存以值为null的形式存入
  95. if (!entriesToAddOnCommit.containsKey(entry)) {
  96. delegate.putObject(entry, null);
  97. }
  98. }
  99. }
  100.  
  101. private void unlockMissedEntries() {
  102. for (Object entry : entriesMissedInCache) {
  103. try {
  104. delegate.removeObject(entry);
  105. } catch (Exception e) {
  106. log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
  107. + "Consider upgrading your cache adapter to the latest version. Cause: " + e);
  108. }
  109. }
  110. }
  111.  
  112. }

   可以看到该类最终的才是真正把缓存存到缓存管理器中的地方,其中的方法也是操作缓存的方法,我们执行的putObject只是将缓存加入了等待map而已,而commit方法中的flushPendingEntries()方法才是真正刷新缓存的地方,由于其时private方法,所以我们要主要看到底哪个地方调用了commit方法。并且如果执行了其clear方法,,缓存管理器中现有缓存将会被清空,等待队列也会被清空,获取缓存也会返回null。执行了rollback方法则会将所有的现有缓存清空且未命中缓存也会清空,clear方法我们上面判断flushCache的值的时候就调用过一次。 我们接着看commit,clear,rollback方法哪些地方会调用

  1.6commit,clear,rollback  缓存实际操作

    我们看下在CachingExecutor中哪些地方调用了这三个方法

  1. @Override
  2. public void close(boolean forceRollback) {
  3. try {
  4. //是否强制回滚
  5. if (forceRollback) {
  6. //回滚 即清空未命中缓存 且等待提交的缓存也会被清空
  7. tcm.rollback();
  8. } else {
  9. //提交 即将缓存存储真正的缓存管理器
  10. tcm.commit();
  11. }
  12. } finally {
  13. delegate.close(forceRollback);
  14. }
  15. }
  1. @Override
  2. public void commit(boolean required) throws SQLException {
  3. delegate.commit(required);
  4. //执行缓存管理器的commit 即将等待提交的缓存存入真正的缓存管理器中
  5. tcm.commit();
  6. }
  1. private void flushCacheIfRequired(MappedStatement ms) {
  2. Cache cache = ms.getCache();
  3. if (cache != null && ms.isFlushCacheRequired()) {
    //清除缓存
  4. tcm.clear(cache);
  5. }
  6. }
  1. @Override
  2. public void rollback(boolean required) throws SQLException {
  3. try {
  4. delegate.rollback(required);
  5. } finally {
  6. if (required) {
  7. tcm.rollback();
  8. }
  9. }
  10. }

  其实这三个方法通过分析代码可以得知,close方法在我们执行SqlSession的close方法会执行,commit方法在我们执行SqlSession.commit方法会执行。而flushCacheIfRequired则会在执行update操作或者select操作时判断该方法的Mappedstatment的flushRequired决定执行。当然了 根据默认情况  如果mapper开启了二级缓存 那么update操作(默认flushCache为true)会执行而select操作不会执行默认flushCache为false)。

  这儿我们还得注意DefualSqlSession的操作执行close或者commit或者rollback操作时会做一个判断,即会判断是否强制回滚或者数据已经被更改 (dirty属性为true)。满足其一缓存都会被清除

  缓存也必须提交了才能生效

  1. executor.commit(isCommitOrRollbackRequired(force));
  2.  
  3. private boolean isCommitOrRollbackRequired(boolean force) {
  4. return (!autoCommit && dirty) || force;
  5. }

  所以我们可以得出结论

    1.执行SqlSession.close操作且传入参数是否强制回滚(默认false)且系统没有数据被更改,则会提交缓存。如果强制回滚或者没有更新操作即dirty为fasle则清除等待缓存

    2.执行SqlSession.commit操作会提交缓存

    3.执行SqlSession.rollback且传入参数为true或者没有更新操作即dirty为fasle则不会清除缓存,否则会清除等待缓存

    4.sql执行时如果方法的MappedStatment的flushRequied为true时则会执行clear方法清除等待缓存以及缓存管理器中的缓存

    5.缓存是由具体的缓存管理器实现的,所以我们每次得到的对象都会经过序列化或者反序列化,缓存的对象是不相等的

    6.二级缓存默认开启,且范围为mapper范围,但使用需要在mapper.xml中添加cache节点

    7.在sql方法中,查询操作usecCache默认为true,flushCache默认为false。更新操作中useCache,默认false,flushCache默认为true。

2. 一级缓存

  一级缓存的逻辑比二级缓存简单的多。同时一级缓存的优先级比二级缓存低

  一级缓存的操作主要在BaseExecutor中,即二级缓存没有查找到之后使用CachingExecutor中封存的SimpleExecutor的query方法,由于SimpleExecutor没有实现这个方法,所以直接看父类BaseExecutor的方法

  

  1. protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads = new ConcurrentLinkedQueue<>();;
      //一级缓存
  2. protected PerpetualCache localCache = new PerpetualCache("LocalCache");
    //存储过程有关的一级缓存
  3. protected PerpetualCache localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");;
  4.  
  5. @SuppressWarnings("unchecked")
  6. @Override
  7. public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  8. ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  9. if (closed) {
  10. throw new ExecutorException("Executor was closed.");
  11. }
  12. //当前查询操作在进行且刷新标志位true
  13. if (queryStack == 0 && ms.isFlushCacheRequired()) {
  14. //刷新一级cache
  15. clearLocalCache();
  16. }
  17. List<E> list;
  18. try {
  19. //查询次数+1
  20. queryStack++;
  21. //从缓存拿
  22. list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
  23. if (list != null) {
  24. //不为空则判断下是否为存储过程 如果是的话执行下
  25. handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
  26. } else {
  27. //没找到缓存就直接去数据库查
  28. list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  29. }
  30. } finally {
  31. //查询完成 次数-1
  32. queryStack--;
  33. }
  34.  
  35. //如果没有查询操作在进行
  36. if (queryStack == 0) {
  37.  
  38. //清空一些等待的任务
  39. for (DeferredLoad deferredLoad : deferredLoads) {
  40. deferredLoad.load();
  41. }
  42. // issue #601
  43. deferredLoads.clear();
  44. //如果缓存的scope是STATEMENT (默认为session),那则直接让一级缓存的作用只存在于一次查询中 即给我们一个控制一级缓存的地方
  45. if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
  46. //清除缓存
  47. clearLocalCache();
  48. }
  49. }
  50. return list;
  51. }
  52. //清除缓存 和存储过程输出参数缓存
  53. @Override
  54. public void clearLocalCache() {
  55. if (!closed) {
  56. localCache.clear();
  57. localOutputParameterCache.clear();
  58. }
  59. }
  60.  
  61.   /***************设置缓存操作************/
  62. private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  63. List<E> list;
  64. //设置一个缓存标记位
  65. localCache.putObject(key, EXECUTION_PLACEHOLDER);
  66. try {
  67. //查询
  68. list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  69. } finally {
  70. //清除缓存位
  71. localCache.removeObject(key);
  72. }
  73. //设置缓存
  74. localCache.putObject(key, list);
  75. //如果是存储过程
  76. if (ms.getStatementType() == StatementType.CALLABLE) {
  77. //存储下存储过程有关的东西
  78. localOutputParameterCache.putObject(key, parameter);
  79. }
  80. return list;
  81. }

  

  1. public class PerpetualCache implements Cache {
  2.  
  3. private final String id;
  4.  
  5. private Map<Object, Object> cache = new HashMap<>();
  6.  
  7. ..................
  8. }

由于Executor是在我们使用SqlSession.openSession构建的,而一级缓存的存储PerpetualCache 又并非是静态属性,其也是用的非静态Map类来存储的,所以我们可以得知一级缓存的生命周期就是SqlSession的生命周期,同时由于其时用Map即内存存储的对象,所以我们缓存获取到的对象同一开始存的对象是完全相同的。并且方法的刷新标志FlushRequied位true时也会清空缓存。当然了我们执行update操作,或者SqlSession.commit或者SqlSession.close或者SqlSession.rollBack时也会清除本地缓存,源代码如下

  1. @Override
  2. public int update(MappedStatement ms, Object parameter) throws SQLException {
  3. ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
  4. if (closed) {
  5. throw new ExecutorException("Executor was closed.");
  6. }
    //每次都会执行更新操作
  7. clearLocalCache();
  8. return doUpdate(ms, parameter);
  9. }
  1. @Override
  2. public void rollback(boolean required) throws SQLException {
  3. if (!closed) {
  4. try {
    //清除一级缓存
  5. clearLocalCache();
  6. flushStatements(true);
  7. } finally {
  8. if (required) {
  9. transaction.rollback();
  10. }
  11. }
  12. }
  13. }
  14.  
  15. @Override
  16. public void commit(boolean required) throws SQLException {
  17. if (closed) {
  18. throw new ExecutorException("Cannot commit, transaction is already closed");
  19. }
    //清除一级缓存
  20. clearLocalCache();
  21. flushStatements();
  22. if (required) {
  23. transaction.commit();
  24. }
  25. }
  26.  
  27. @Override
  28. public void commit(boolean required) throws SQLException {
  29. if (closed) {
  30. throw new ExecutorException("Cannot commit, transaction is already closed");
  31. }
    //清除一级缓存
  32. clearLocalCache();
  33. flushStatements();
  34. if (required) {
  35. transaction.commit();
  36. }
  37. }

   所以也可以得出结论。

    1.一级缓存的优先级低于二级缓存

    2.一级缓存默认开启,范围为一次SqlSession的生命周期,手动设置Confiuration的LocalCacheScope为statment可使其的生命周期降低为一次查询

    3.一级缓存使用内存管理,获得的都是同一个对象

    4.查询操作如何flushRequered为true则会清除一级缓存,所有更新操作都会清除一级缓存

    5.SqlSession执行commit或者close操作会清除一级缓存

完结

  至此一二级缓存都分析完了,有没有想到一开始的结果是如何出现的呢?这儿可以简单的分析下步骤

  1.查询数据,走到二级缓存后没有查询到数据,往下走

  2.走到一级缓存,依然没有数据,查询数据库,此时执行sql

  3.将结果存入一级缓存的map,由于一级缓存是map存储在内存中,所以一级缓存获取到的对象是一个对象

  4.此时二级缓存处获取到了查询的结果,将结果存储在二级缓存等待提交区,然后返回结果

  5.我们第一次获取到了结果  ,此时是从一级缓存拿到的,我们修改其中的属性则等于修改了一级缓存中的对象属性

  6.我们再次执行查询,由于查询条件方法相同所以缓存key相同,走到二级缓存,此时由于二级缓存还没提交,所以查询结果为空

  7.走到一级缓存,查询到了结果返回,此时不执行sql。

  8.再次走到二级缓存,二级缓存再次将结果存入等待队列,此时会覆盖之前的等待队列中的对象(属性已经被我们更改)

  9.将结果返回,此时得到的依旧是一级缓存的结果,且属性被第5步中更改了,但是和第一次查询到的对象依旧是同一个对象,所以属性是修改后的属性且和第一次查询的对象相同

  10.执行close方法,此时会提交本次的二级缓存等待队列

  11.再次执行查询方法,由于方法签名一致,走到二级缓存时获取到了缓存的对象(属性已经被更改),此时不执行sql,同时二级缓存不是缓存在内存中,所以获取到的是一个新对象

  

  

  

  

  

 

  

  

mybatis源码探索笔记-4(缓存原理)的更多相关文章

  1. mybatis源码探索笔记-5(拦截器)

    前言 mybatis中拦截器主要用来拦截我们在发起数据库请求中的关键步骤.其原理也是基于代理模式,自定义拦截器时要实现Interceptor接口,并且要对实现类进行标注,声明是对哪种组件的指定方法进行 ...

  2. mybatis源码探索笔记-1(构建SqlSessionFactory)

    前言 mybatis是目前进行java开发 dao层较为流行的框架,其较为轻量级的特性,避免了类似hibernate的重量级封装.同时将sql的查询与与实现分离,实现了sql的解耦.学习成本较hibe ...

  3. mybatis源码探索笔记-3(使用代理mapper执行方法)

    前言 前面两章我们构建了SqlSessionFactory,并通过SqlSessionFactory创建了我们需要的SqlSession,并通过这个SqlSession获取了我们需要的代理mapper ...

  4. mybatis源码探索笔记-2(构建SqlSession并获取代理mapper)

    前言 上篇笔记我们成功的装载了Configuration,并写入了我们全部需要的信息.根据这个Configuration创建了DefaultSqlSessionFactory.本篇我们实现构建SqlS ...

  5. Mybatis源码研究7:缓存的设计和实现

    Mybatis源码研究7:缓存的设计和实现 2014年11月19日 21:02:14 酷酷的糖先森 阅读数:1020   版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog ...

  6. mybatis源码学习(三)-一级缓存二级缓存

    本文主要是个人学习mybatis缓存的学习笔记,主要有以下几个知识点 1.一级缓存配置信息 2.一级缓存源码学习笔记 3.二级缓存配置信息 4.二级缓存源码 5.一级缓存.二级缓存总结 1.一级缓存配 ...

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

    前言 大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读Mybatis源码(一)构造篇 和 手把手带你阅读Mybatis源码(二)执行篇,主要说明了MyBatis是如何 ...

  8. mybatis源码学习:一级缓存和二级缓存分析

    目录 零.一级缓存和二级缓存的流程 一级缓存总结 二级缓存总结 一.缓存接口Cache及其实现类 二.cache标签解析源码 三.CacheKey缓存项的key 四.二级缓存TransactionCa ...

  9. 【MyBatis源码分析】插件实现原理

    MyBatis插件原理----从<plugins>解析开始 本文分析一下MyBatis的插件实现原理,在此之前,如果对MyBatis插件不是很熟悉的朋友,可参看此文MyBatis7:MyB ...

随机推荐

  1. mysql 同时支持多少连接MYSQL 查看最大连接数和修改最大连接数

    MySQL查看最大连接数和修改最大连接数 1.查看最大连接数 show variables like '%max_connections%'; 2.修改最大连接数 set GLOBAL max_con ...

  2. C语言数据结构——第一章 数据结构的概念

    一.数据结构的基本概念 1.1-数据结构是什么? 数据结构是计算机存储和组织数据的方式.数据结构是指相互之间存在一种或多种特定关系的数据元素的集合.一般情况下,精心选择的数据结构可以带来更高的运行或者 ...

  3. python入门(二十讲):爬虫

    什么是爬虫? 按照一定的规则,自动地抓取万维网信息的程序或脚本. 爬虫目的: 从网上爬取出来大量你想获取类型的数据,然后用来分析大量数据的类似点或者其他信息来对你所进行的工作提供帮助. 为什么选择py ...

  4. linux备忘命令

    1,安装vim以后把vim中的tab键设置为4个空格 vim ~/.vimrc一下,如果没有会创建新的, 然后添加下面两行: set ts=4 set expandtab 如果第二行内容是noexpa ...

  5. 【代码审计】seacms 前台Getshell分析

    一.漏洞分析 漏洞触发点search.php 211-213行 跟进parseIf 函数 ./include/main.class.php 这里要注意 3118行的位置,可以看到未做任何处理的eval ...

  6. 【资源分享】RPG Maker 2000/2003 简体中文版

    *----------------------------------------------[下载区]----------------------------------------------* ...

  7. Linux 内核内存池

    内核中经常进行内存的分配和释放.为了便于数据的频繁分配和回收,通常建立一个空闲链表——内存池.当不使用的已分配的内存时,将其放入内存池中,而不是直接释放掉. Linux内核提供了slab层来管理内存的 ...

  8. java中的main方法参数String[] args的说明

    参数String[] args 的作用是在运行main方法时,在控制台输入参数 class Test{ public static void main(String[] args){ for(Stri ...

  9. 本地mongodb数据库导出到远程数据库中

    把本地Mongodb中的数据导入(批量插入)到服务器的数据库中 1.导出数据: mongoexport -d admin -c users -o outdatafile.dat 选项解释: -d 指明 ...

  10. Cesium 基于MapBox底图加载3DTiles 模型

    3DTiles 模型采用   CATIA V5 R22 --->3dxml --->GLB--->B3DM var extent = Cesium.Rectangle.fromDeg ...