個人認為mybatis一級快取和二級快取不是很好的設計,工作中我基本上也不會使用一級快取和二級緩存,因為一旦使用不當會造成很多問題,所以我們今天就來看看到底會有什麼問題?
上一節我們介紹了Executor會呼叫StatementHandler執行sql,起一個承上啟動的作用。 Executor的設計是典型的裝飾者模式,SimpleExecutor,ReuseExecutor是具體實作類別,而CachingExecutor是裝飾器類別。
可以看到特定元件實作類別有一個父類別BaseExecutor,而這個父類別是一個範本模式的典型應用,操作一級快取的操作都在這個類別中,而具體的操作資料庫的功能則讓子類別去實作。
「二級快取則是一個裝飾器類,當開啟二級快取的時候,會使用CachingExecutor對特定實現類別進行裝飾,所以查詢的時候一定是先查詢二級快取再查詢一級快取」「那麼一級快取和二級快取有什麼差別呢?」
「一级缓存和二级缓存key的构建规则是一致的,都是一个CacheKey对象,因为Mybatis中涉及动态SQL等多方面的因素,缓存的key不能仅仅通过String来表示」
当执行sql的如下4个条件都相等时,CacheKey才会相等
「当查询的时候先从缓存中查询,如果查询不到的话再从数据库中查询」
org.apache.ibatis.executor.BaseExecutor#query当使用同一个SqlSession执行更新操作时,会先清空一级缓存。因此一级缓存中内容被使用的概率也很低
「看到美团技术团队上关于一级缓存和二级缓存的一些测试写的挺不错的,就直接引用过来了」
原文地址:https://tech.meituan.com/2018/01/19/mybatis-cache.html 测试代码github地址:https://github.com/kailuncen/mybatis-cache-demo
接下来通过实验,了解MyBatis一级缓存的效果,每个单元测试后都请恢复被修改的数据。
首先是创建示例表student,创建对应的POJO类和增改的方法,具体可以在entity包和mapper包中查看。
CREATE TABLE `student` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(200) COLLATE utf8_bin DEFAULT NULL, `age` tinyint(3) unsigned DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
在以下实验中,id为1的学生名称是凯伦
「实验1」
开启一级缓存,范围为会话级别,调用三次getStudentById,代码如下所示:
执行结果:我们可以看到,只有第一次真正查询了数据库,后续的查询使用了一级缓存。
「实验2」
增加了对数据库的修改操作,验证在一次数据库会话中,如果对数据库发生了修改操作,一级缓存是否会失效。
执行结果:我们可以看到,在修改操作后执行的相同查询,查询了数据库,一级缓存失效。
「实验3」
开启两个SqlSession,在sqlSession1中查询数据,使一级缓存生效,在sqlSession2中更新数据库,验证一级缓存只在数据库会话内部共享。
输出如下sqlSession1和sqlSession2读的时相同的数据,但是都查询了数据库,说明了「一级缓存只在数据库会话层面共享」
sqlSession2更新了id为1的学生的姓名,从凯伦改为了小岑,但sqlSession1之后的查询中,id为1的学生的名字还是凯伦,出现了脏数据,也证明了之前的设想,一级缓存只在数据库会话层面共享
「MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement,即进行如下配置」
<setting name="localCacheScope" value="STATEMENT"/>
原因也很简单,看BaseExecutor的query()方法,当配置成STATEMENT时,每次查询完都会清空缓存「看到这你可能会想,我用mybatis后没设置这个参数啊,好像也没发生脏读的问题啊,其实是因为你和spring整合了」
当mybatis和spring整合后(整合的相关知识后面还有一节)
「当mybatis和spring整合后,未开启事务的情况下,不会有任何问题,因为一级缓存没有生效。当开启事务的情况下,可能会有问题,由于一级缓存的存在,在事务内的查询隔离级别是可重复读,即使你数据库的隔离级别设置的是提交读」
// Configuration protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
「而二级缓存是Configuration对象的成员变量,因此二级缓存的生命周期是整个应用级别的。并且是基于namespace构建的,一个namesapce构建一个缓存」
「二级缓存不像一级缓存那样查询完直接放入一级缓存,而是要等事务提交时才会将查询出来的数据放到二级缓存中。」
因为如果事务1查出来直接放到二级缓存,此时事务2从二级缓存中拿到了事务1缓存的数据,但是事务1回滚了,此时事务2不就发生了脏读了吗?
「二级缓存的相关配置有如下3个」
「1.mybatis-config.xml」
<settings> <setting name="cacheEnabled" value="true"/> </settings>
这个是二级缓存的总开关,只有当该配置项设置为true时,后面两项的配置才会有效果
从Configuration类的newExecutor方法可以看到,当cacheEnabled为true,就用缓存装饰器装饰一下具体组件实现类,从而让二级缓存生效「2.mapper映射文件中」mapper映射文件中如果配置了62aecd17e676a41d3547c3bf97bb07b0和c9f60baecdceda902422ce5608e73ae9中的任意一个标签,则表示开启了二级缓存功能,没有的话表示不开启
<cache type="" eviction="FIFO" size="512"></cache>
二级缓存的部分配置如上,type就是填写一个全类名,用来指定二级缓存的实现类,这个实现类需要实现Cache接口,默认是PerpetualCache(你可以利用这个属性将mybatis二级缓存和Redis,Memcached等缓存组件整合在一起)
org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache
这个eviction表示缓存清空策略,可填选项如下
选项 | 解释 | 装饰器类 |
---|---|---|
LRU | 最近最少使用的:移除最长时间不被使用的对象 | LruCache |
FIFO | 先进先出:按对象进入缓存的顺序来移除它们 | FifoCache |
SOFT | 软引用:移除基于垃圾回收器状态和软引用规则的对象 | SoftCache |
WEAK | 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象 | WeakCache |
典型的裝飾者模式的實現,換緩存清空策略就是換裝飾器。 「3.221f08282418e2996498697df914ce4e節點中的useCache屬性」
該屬性表示查詢產生的結果是否要儲存的二級快取中,useCache屬性的預設值為true,這個配置可以將二級快取細分到語句層級
#二級快取是基於namespace實現的,也就是一個mapper映射檔案用一個快取
在本實驗中,id為1的學生名稱初始化為點點。
「實驗1」
測試二級快取效果,不提交事務,sqlSession1查詢完資料後,sqlSession2相同的查詢是否會從快取中取得資料。 執行結果:我們可以看到,當sqlsession沒有呼叫commit()方法時,二級快取並沒有發揮作用。
「實驗2」
測試二級快取效果,當提交交易時,sqlSession1查詢完資料後,sqlSession2相同的查詢是否會從快取中取得數據。 從圖上可知,sqlsession2的查詢,使用了緩存,快取的命中率是0.5。
「實驗3」
測試update作業是否會重新整理該namespace下的二級快取。
我們可以看到,在sqlSession3更新資料庫,並提交交易後,sqlsession2的StudentMapper namespace下的查詢走了資料庫,沒有走Cache。
「實驗4」
验证MyBatis的二级缓存不适应用于映射文件中存在多表查询的情况。
getStudentByIdWithClassInfo的定义如下
通常我们会为每个单表创建单独的映射文件,由于MyBatis的二级缓存是基于namespace的,多表查询语句所在的namspace无法感应到其他namespace中的语句对多表查询中涉及的表进行的修改,引发脏数据问题。
执行结果:
在这个实验中,我们引入了两张新的表,一张class,一张classroom。class中保存了班级的id和班级名,classroom中保存了班级id和学生id。我们在StudentMapper中增加了一个查询方法getStudentByIdWithClassInfo,用于查询学生所在的班级,涉及到多表查询。在ClassMapper中添加了updateClassName,根据班级id更新班级名的操作。
当sqlsession1的studentmapper查询数据后,二级缓存生效。保存在StudentMapper的namespace下的cache中。当sqlSession3的classMapper的updateClassName方法对class表进行更新时,updateClassName不属于StudentMapper的namespace,所以StudentMapper下的cache没有感应到变化,没有刷新缓存。当StudentMapper中同样的查询再次发起时,从缓存中读取了脏数据。
「实验5」
为了解决实验4的问题呢,可以使用Cache ref,让ClassMapper引用StudenMapper命名空间,这样两个映射文件对应的SQL操作都使用的是同一块缓存了。
mapper文件中的配置如下
<cache-ref namespace="mapper.StudentMapper"/>
执行结果:
不过这样做的后果是,缓存的粒度变粗了,多个Mapper namespace下的所有操作都会对缓存使用造成影响。
mybatis的一級快取和二級快取都是基於本地的,分散式環境下必然會出現髒讀。
二級緩存可以透過實現Cache接口,來集中管理緩存,避免髒讀,但是有一定的開發成本,並且在多表查詢時,使用不當極有可能會出現髒數據
以上是為什麼Mybatis一級和二級快取都不建議使用?的詳細內容。更多資訊請關注PHP中文網其他相關文章!