浅谈MyBatis的缓存机制

前言

MyBatis是一款常见的功能强大的java数据库持久层框架。在日常开发中,大多数程序员会使用默认的缓存配置,在不了解缓存应用场景的时候,会造成一些隐患,读取到脏数据。下面就来根据实例和源码谈谈MyBatis的一级和二级缓存机制,以帮助我们更好的了解缓存的应用场景,避免不当使用造成的损失。

一级缓存

一般在应用运行期间,我们可能在一次数据库会话中,多次执行相同条件的查询语句,Mybatis为这种场景提供了缓存机制来加快返回结果,避免多次直接查询数据库。如果相同的sql语句会优先命中一级缓存,避免直接查询数据库,提高性能。

每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入LocalCache,再把结果返回给用户。

MyBatis一级缓存配置如下,一级缓存有两种选项,SESSION和STATEMENT,默认是SESSION级别的,即在一个会话中共享一个缓存,STATEMENT可以理解为在执行一个statement期间有效。

当开启SESSION级别,并且在未调用Commit方法提交事务情况下,查询结果如下:

当开启STATEMENT级别查询结果如下:

当添加数据时,验证一级缓存是否会失效:

当开启两个Sqlsession时,验证一级缓存只在数据库会话中共享。如下图我们看到在Mapper1修改了数据后,Mapper2在未提交事务前,读取的还是原来的数据,这样就导致两个Sqlsession中读取到的数据不一致。

当Session2调用Commit方法提交事务后,其查询结果如下图所示:

一级缓存源码分析

接下来对MyBatis查询相关的核心类和一级缓存的源码进行简要走读。

SqlSession: 接口提供了用户和数据库之间交互需要的所有方法,默认实现类是DefaultSqlSession。

Executor: SqlSession向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor。

阅读BaseExecutor源码可以看到,LocalCache是其成员变量,cache定义了基本的操作。

BaseExecutor成员变量之一的PerpetualCache,是对Cache接口最基本的实现,实现也非常简单,内部通过HashMap来对一级缓存操作。

为执行和数据库的交互,首先需要初始化SqlSession,通过DefaultSqlSessionFactory开启SqlSession:

在初始化SqlSesion时,会使用Configuration类创建一个Executor:

当查询数据库时,调用SqlSession的SelectList方法,最终会调用Executor的Query方法,下面来看下这个方法的具体实现:

当没有命中缓存时才会去查询数据库,查询数据库后会写回缓存。当返回结果前,判断LocalCache级别,如果是STATEMENT就会清除缓存。

在DefaultSqlSession源码中,insert/update/delete方法都是调用Update方法,在BaseExecutor源码中,Update方法会清除缓存。

当SqlSession调用Commit方法提交事务,最终会调用BaseExecutor的Commit方法,而Commit方法中会清除缓存。

自此一级缓存的大致工作流程分析完毕,MyBatis一级缓存内部设计简单,只是简单的通过HashMap来存储查询,功能上有所欠缺,而且在多个Sqlsession或者分布式环境中存在读脏数据的风险。

二级缓存

在上文中提到的一级缓存中,其最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。进入一级缓存的查询流程前,先在二级缓存中查询。开启二级缓存配置,在MyBatis的Mapper.xml中配置Cache或者 Cache-ref 。

当两个Sqlsession,不提交事务时,Session1查询后,Session2是否命中缓存。

运行结果:

结果发现当Sqlsession没有调用Commit()方法时,,二级缓存不起作用。

当两个Sqlsession,当Session1查询提交事务后,Session2是否命中缓存。

运行结果:

从结果可知,命中了缓存,缓存的命中率是0.5。然后测试下Update操作是否会刷新缓存。

测试结果:

结果可以看到Sesson3提交事务后,Session2没有走缓存,而是查询了数据库。

在多表联合查询中,二级缓存也会引起数据不可重复读的问题,这是因为二级缓存是基于Namespace级别的,所以为了解决这个问题,可以通过Cache-ref,让多个Namespace公用一个缓存,这部分实验请读者自己验证,这里不再赘述。

二级缓存源码分析

和一级缓存相似,只是用CachingExecutor装饰了BaseExecutor的子类,在使用Delegate之前来实现它的缓存操作。

我们直接看CachingExecutor类的Query方法,

MappedStatement中获取Cache,本质上是装饰器模式的使用,具体的装饰链如下图。

flushCacheIfRequired(ms)方法判断是否需要刷新缓存,一般除了select以外,其他的操作都会刷新缓存。

CachingExecutor中有个TransactionalCacheManager类来操作缓存,这个就是代码中的TCM,它的类中实例化了一个HashMap,存储Cache与TransactionalCache的映射关系。TransactionalCache实现了Cache接口,CachingExecutor会默认使用它包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。我们可以看到先通过TCM查询缓存,一直查询到PerpetualCache,如果查询不到,再通过Delegate查询,Delegate就是前文描述的一级缓存查询过程,这里不再赘述,然后再把查询到的结果写入二级缓存中,调用TCM.putObject方法。

TCM的PutObject方法不是直接操作缓存,而是把数据放入待提交的名为EntriesToAddOnCommit的Map中,通过源码分析得到,不调用Commit方法,二级缓存是不会起作用的,现在看下CachingExecutor的Commit方法。

具体操作会调用TransactionalCacheManager的commit方法。

TransactionalCache的Commit方法代码如下:

最终调用AddEntry类的Commit方法:

这样结果就存储到了PerpetualCache的hashMap里面。

CachingExecutor中的更新操作,会调用tcm的clear方法,

这样会清空两个待提交的map,然后设ClearOnCommit为True。

当调用Commit方法后,当ClearOnCommit为True时,会调用Delegate的clear方法,最终清空PerpetualCache中的HashMap。

二级缓存比一级缓存粒度更细,能够到Namespace级别。但是在分布式环境下,使用二级缓存同样会造成数据不可重复读问题。

MyBatis有一级二级缓存,用户也可以自定义缓存,只需要实现Cache接口,然后配置到Mapper.xml的Cache标签中的Type属性指向自定义类,这里留给读者自己去验证。

刘石

享米Java工程师

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180906G1ICG600?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

同媒体快讯

扫码关注云+社区

领取腾讯云代金券