为什么?Mybatis的一级和二级缓存都不建议使用?

缓存是在哪起作用的?

个人认为mybatis一级缓存和二级缓存并不是一个很好的设计,工作中我基本上也不会使用一级缓存和二级缓存,因为一旦使用不当会造成很多问题,所以我们今天就来看看到底会有什么问题?

Executor会调用StatementHandler执行sql,起一个承上启下的作用。

Executor的设计是一个典型的装饰者模式,SimpleExecutor,ReuseExecutor是具体实现类,而CachingExecutor是装饰器类。

可以看到具体组件实现类有一个父类BaseExecutor,而这个父类是一个模板模式的典型应用,操作一级缓存的操作都在这个类中,而具体的操作数据库的功能则让子类去实现。

「二级缓存则是一个装饰器类,当开启二级缓存的时候,会使用CachingExecutor对具体实现类进行装饰,所以查询的时候一定是先查询二级缓存再查询一级缓存」

「那么一级缓存和二级缓存有什么区别呢?」

一级缓存

// BaseExecutorprotected PerpetualCache localCache;

一级缓存是BaseExecutor中的一个成员变量localCache(对HashMap的一个简单封装),因此一级缓存的生命周期与SqlSession相同,如果你对SqlSession不熟悉,你可以把它类比为JDBC编程中的Connection,即数据库的一次会话。

「一级缓存和二级缓存key的构建规则是一致的,都是一个CacheKey对象,因为Mybatis中涉及动态SQL等多方面的因素,缓存的key不能仅仅通过String来表示」

当执行sql的如下4个条件都相等时,CacheKey才会相等

1 mappedStatment的id

2 指定查询结构集的范围

3 查询所使用SQL语句

4 用户传递给SQL语句的实际参数值

「当查询的时候先从缓存中查询,如果查询不到的话再从数据库中查询」

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包中查看。

CREATETABLE`student`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`name`varchar(200)COLLATEutf8_binDEFAULTNULL,`age`tinyint(3)unsignedDEFAULTNULL,  PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=4DEFAULTCHARSET=utf8COLLATE=utf8_bin;

在以下实验中,id为1的学生名称是凯伦

「实验1」

开启一级缓存,范围为会话级别,调用三次getStudentById,代码如下所示:一级缓存测试

「看到美团技术团队上关于一级缓存和二级缓存的一些测试写的挺不错的,就直接引用过来了」

原文地址:https://tech.meituan.com/2018/01/19/mybatis-cache.html 测试代码github地址:https://github.com/kailuncen/mybatis-cache-demo

接下来通过实验,了解MyBatis一级缓存的效果,每个单元测试后都请恢复被修改的数据。

首先是创建示例表student,创建对应的POJO类和增改的方法,具体可以在entity包和mapper包中查看。

CREATETABLE`student`(`id`int(11)unsignedNOTNULLAUTO_INCREMENT,`name`varchar(200)COLLATEutf8_binDEFAULTNULL,`age`tinyint(3)unsignedDEFAULTNULL,  PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=4DEFAULTCHARSET=utf8COLLATE=utf8_bin;

在以下实验中,id为1的学生名称是凯伦

「实验1」

开启一级缓存,范围为会话级别,调用三次getStudentById,代码如下所示:

执行结果:   

我们可以看到,只有第一次真正查询了数据库,后续的查询使用了一级缓存。

「实验2」

增加了对数据库的修改操作,验证在一次数据库会话中,如果对数据库发生了修改操作,一级缓存是否会失效。

执行结果 

我们可以看到,在修改操作后执行的相同查询,查询了数据库,一级缓存失效。

「实验3」

开启两个SqlSession,在sqlSession1中查询数据,使一级缓存生效,在sqlSession2中更新数据库,验证一级缓存只在数据库会话内部共享。

    输出如下

sqlSession1和sqlSession2读的时相同的数据,但是都查询了数据库,说明了「一级缓存只在数据库会话层面共享」

sqlSession2更新了id为1的学生的姓名,从凯伦改为了小岑,但sqlSession1之后的查询中,id为1的学生的名字还是凯伦,出现了脏数据,也证明了之前的设想,一级缓存只在数据库会话层面共享

「MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement,即进行如下配置」

原因也很简单,看BaseExecutor的query()方法,当配置成STATEMENT时,每次查询完都会清空缓存

「看到这你可能会想,我用mybatis后没设置这个参数啊,好像也没发生脏读的问题啊,其实是因为你和spring整合了」

当mybatis和spring整合后(整合的相关知识后面还有一节)

在未开启事务的情况之下,每次查询,spring都会关闭旧的sqlSession而创建新的sqlSession,因此此时的一级缓存是没有起作用的

在开启事务的情况之下,spring使用threadLocal获取当前线程绑定的同一个sqlSession,因此此时一级缓存是有效的,当事务执行完毕,会关闭sqlSession

「当mybatis和spring整合后,未开启事务的情况下,不会有任何问题,因为一级缓存没有生效。当开启事务的情况下,可能会有问题,由于一级缓存的存在,在事务内的查询隔离级别是可重复读,即使你数据库的隔离级别设置的是提交读」

二级缓存

//Configurationprotected final Map caches =newStrictMap<>("Caches collection");

「而二级缓存是Configuration对象的成员变量,因此二级缓存的生命周期是整个应用级别的。并且是基于namespace构建的,一个namesapce构建一个缓存」

「二级缓存不像一级缓存那样查询完直接放入一级缓存,而是要等事务提交时才会将查询出来的数据放到二级缓存中。」

因为如果事务1查出来直接放到二级缓存,此时事务2从二级缓存中拿到了事务1缓存的数据,但是事务1回滚了,此时事务2不就发生了脏读了吗?

「二级缓存的相关配置有如下3个」

「1.mybatis-config.xml」

这个是二级缓存的总开关,只有当该配置项设置为true时,后面两项的配置才会有效果

从Configuration类的newExecutor方法可以看到,当cacheEnabled为true,就用缓存装饰器装饰一下具体组件实现类,从而让二级缓存生效

「2.mapper映射文件中」mapper映射文件中如果配置了和中的任意一个标签,则表示开启了二级缓存功能,没有的话表示不开启

二级缓存的部分配置如上,type就是填写一个全类名,用来指定二级缓存的实现类,这个实现类需要实现Cache接口,默认是PerpetualCache(你可以利用这个属性将mybatis二级缓存和Redis,Memcached等缓存组件整合在一起)

org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache

这个eviction表示缓存清空策略,可填选项如下

选项解释装饰器类LRU最近最少使用的:移除最长时间不被使用的对象LruCacheFIFO先进先出:按对象进入缓存的顺序来移除它们FifoCacheSOFT软引用:移除基于垃圾回收器状态和软引用规则的对象SoftCacheWEAK弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象WeakCache

典型的装饰者模式的实现,换缓存清空策略就是换装饰器。

「3.<select>节点中的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文件中的配置如下

执行结果:

不过这样做的后果是,缓存的粒度变粗了,多个Mapper namespace下的所有操作都会对缓存使用造成影响。

总结

mybatis的一级缓存和二级缓存都是基于本地的,分布式环境下必然会出现脏读。

二级缓存可以通过实现Cache接口,来集中管理缓存,避免脏读,但是有一定的开发成本,并且在多表查询时,使用不当极有可能会出现脏数据

「除非对性能要求特别高,否则一级缓存和二级缓存都不建议使用」

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容