Mybatis 缓存机制解析

版权声明:本文为神州灵云作者的原创文章,未经神州灵云允许不得转载。

本文作者:Kevin

引言:

Mybatis是一款常用的Java数据库访问层框架,他很好的封装了数据层的jdbc操作,也将Java的对象与数据库数据进行了转换,为数据调用提供了便捷。

作为数据持久化框架,Mybatis也有着自己的缓存机制,缓存在提高重复查询效率的同时,也带来了读脏数据的风险,充分理解Mybatis的缓存机制才能让我们在开发中正确使用Mybatis缓存。

1. Mybatis缓存总览:
在讨论缓存前,先看看Mybatis总体的实现原理
Mybatis的实现流程大致如下图所示:
1.1 Mybatis流程图.jpg

Mybatis在通过config配置文件创建SqlSessionFactory,随后使用SqlSessionFactory开启SqlSession,再获得Mapper并映射Sql语句,进入Executor进行查询。通过各类Handler处理参数等,调用底层原生Jdbc完成查询,最后如果有返回数据,使用MappedStatement完成结果的映射,转化成Java对象。

可以看到缓存在整个流程的Executor处被使用,Executor会先判断缓存,如果命中了缓存,将直接返回缓存中的结果。反之,如果没有命中,就会继续之后的步骤去数据库查询。

2. 缓存对象的实现:
Mybatis缓存使用一个Cache对象,一级二级缓存通用。通过阅读源码可以看到,Mybatis的Cache的底层依然是一个HashMap。
2.1 底层实现PerpetualCache.jpg

随后通过各种装饰类对缓存进行层层装饰,以实现各种功能,也隔离了不同功能逻辑。
2.2 cache装饰类.jpg

比如SynchronizeCache实现了线程同步cache,内部方法使用synchronized进行了修饰,LoggingCache实现了日志功能,LruCache实现了最近最少使用算法,FifoCache实现了先进先出算法。

缓存Cache的Key值是通过mappedStementId + offset + limit + SQL + queryParams + environment来生成的CacheKey对象。CacheKey类内包含属性哈希值(hashcode),校验和(checksum),count计数与对象列表。在判断命中时会优先判断哈希,校验和与计数,提高判断效率,如果都一样再对对象列表进行逐一判断。
2.3 CecheKey内部属性.jpg

3. 一级缓存:
Mybatis默认开启一级缓存,缓存默认是session级别,同一个会话中的重复Sql语句都可以使用缓存。 同样要注意的是, 两个session的缓存是不会相互影响的,所以要注意分布式查询可能会引起读脏数据的情况。

每个Executor都拥有自己的localCache.在得到Sql语句后,先匹配缓存,如果缓存命中就不会继续执行,直接返回缓存结果。如果没有命中,查询数据库后会将结果写入localCache。

之前说过,缓存在Executor被使用,SqlSession在执行update/insert/delete时都会执行Excutor的update方法,并在其中清空localCache。
3.1.jpg

而在查询(query)时,会先生成CacheKey对象,对查询数据进行缓存匹配。在最后完成全部的查询操作后,如果缓存的级别是statement将会清空localCache, 而如果是session级别缓存就会被保留,供session再次调用。

总的来说,Mybatis的一级缓存的实现还是比较简单的,每个SqlSession都持有一个Executor。并在Executor维护一个Local Cache(HashMap)来实现SqlSession级别的缓存,相对的对于分布式多SqlSession的支持就比较差,甚至可能造成脏数据问题。

多SqlSession脏数据问题实验:


public void test() {
   SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
   SqlSession sqlSession2 = sqlSessionFactory.openSession(true);

   BanIpMapper banIpMapper1 = sqlSession1.getMapper(BanIpMapper.class);
   BanIpMapper banIpMapper2 = sqlSession2.getMapper(BanIpMapper.class);

   System.out.println("session1 read" + banIpMapper1.selectByPrimaryKey(1));
   System.out.println("session1 read" + banIpMapper1.selectByPrimaryKey(1));
   System.out.println("session2 update" + banIpMapper2.updateByPrimaryKey(new BanIp(1, "1.2.3.4")));
   System.out.println("session1 read" + banIpMapper1.selectByPrimaryKey(1));
   System.out.println("session2 read" + banIpMapper2.selectByPrimaryKey(1));
}

3.2.jpg

可以看到在session2更新后session1读到了脏数据。

4. 二级缓存
Mybatis也提供了二级缓存功能,主要解决了跨SqlSession的缓存问题,让我们来看看他是如何实现的。

二级缓存的实现主要是通过CachingExecutor类来实现的,如果开启了二级缓存,会使用CachingExecutor包装Executor,CachingExecutor负责与全局二级缓存交互。
4.1 二级缓存流程图.jpg

CachingExecutor会优先查询二级缓存,如果没有匹配在委托给它包装的Executor匹配一级缓存或查询数据库。所以数据查询的优先级是二级缓存->一级缓存->数据库。
4.2 Executor.jpg

二级缓存的范围不再是session或statement而是全局,通过为每个语句设置不同的namespace(cache标签)来规划缓存的更新等。同一个namespace的缓存是同时清除,同时提交的。二级缓存通过TransactionalCacheManager管理, 内部是一个HashMap来储存多个namespace的事务缓存。在进行Executer提交与回滚时,TransactionalCacheManager内的缓存也会全部进行提交与回滚。
4.3.jpg
4.4.jpg
4.5.jpg

TansactionalCache对象包含几个特殊属性,entriesMissedInCache用于储存未命中的Key,用于计算命中率。作为二级缓存内的缓存单元,TansactionalCache是基于事务的缓存,在事务提交之前,缓存不会被清除,防止事务回滚后缓存失效。通过阅读源码可以发现,这一控制通过clearOnCommit标志实现,在执行clear() 时将标志设为true,而不是直接删除。等到事务提交,调用缓存commit() 时判断标志,如果clearOnCommit为true,才真正清除缓存。同样的,缓存在被存入后会先暂存在entriesToAddOnCommit中,等到事务提交后,才会将其中的数据推给delegate缓存存储。
4.6.jpg

先对一级缓存,二级缓存通过namespace来管控全局缓存,细化了缓存的控制,同时也实现了缓存在session间的共享。但是同时,由于不同namespace之间的隔离,在多表查询时,非常容易出现脏数据(多表查询与各个表的更新语句在不同一个namespace时,单个表更新不会刷新多表查询的缓存)。 如果为了解决多表查询脏数据问题,而将多个表格的操作放在同一个namespace中,又会导致缓存颗粒度很大,导致频繁刷新缓存。二级缓存也没有解决分布式调用的脏数据问题

总结:
Mybatis作为一个数据访问层框架,在缓存部分使用了较为简单的设计,基本实现了本地缓存的能力,也实现了多功能缓存与多SqlSession缓存共享。但是对分布式环境的支持较差,二级缓存设计也存在缺陷。建议使用第三方缓存,而不是直接使用Mybatis的缓存功能。当然Mybatis在缓存设计中的一些方法,比如使用装饰类分离功能逻辑,对事务逻辑的处理还是值得学习借鉴的。

神州灵云公司官网(二维码).png

猜你喜欢

转载自blog.csdn.net/dclingcloud/article/details/86704706