欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

MyBatis 缓存避免陷阱:分布式环境中的MyBatis一级缓存、二级缓存实施分析和数据一致性问题

最编程 2024-07-06 08:05:29
...

简介


高并发环境下,合理使用缓存能够减少IO,显著提升系统性能。所以MyBatis也提供了缓存 :一级缓存和二级缓存。

MyBatis提供的缓存默认是本地缓存,分布式环境下可能会带来数据一致性问题,当然可以扩展为分布式缓存解决。

MyBatis的一级缓存分析:默认开启而且不能关闭


MyBatis的一级缓存是基于SqlSession实现的,实现逻辑是在BaseExecutor类中完成,每创建一个SqlSession就会创建新的BaseExecutor,即SqlSession对象创建的时候会创建关联的一级缓存SqlSession是非线程安全的,一般基于ThreadLocal实现线程安全使用。MyBatis的一级缓存是默认开启而且不能关闭

创建SqlSession源码:

org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
#openSessionFromDataSource

由源码可知,每创建一个SqlSession就会创建新的Executor,除了CachingExecutor,其它都继承自BaseExecutor,一级缓存的实现主要在BaseExecutor体现:

其中,localCache属性用于缓存MyBatis查询结果,localOutputParameterCache属性用于缓存存储过程调用结果,从构造函数的实现说明,BaseExecutor每次实例化,都会创建新的一级缓存对象:localCache及localOutputParameterCache。

分析一下缓存key的生成:

org.apache.ibatis.executor.BaseExecutor#query
(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

从上面的代码可以看出,缓存的Key与下面这些因素有关:

(1)Mapper的Id,即Mapper命名空间与<select|update|insert|delete>标签的Id组成的全局限定名。

(2)查询结果的偏移量及查询的条数。

(3)具体的SQL语句及SQL语句中需要传递的所有参数。

(4)MyBatis主配置文件中,通过<environment>标签配置的环境信息对应的Id属性值。

从缓存中获取数据时:

org.apache.ibatis.executor.BaseExecutor#query(
org.apache.ibatis.mapping.MappedStatement, 
java.lang.Object, org.apache.ibatis.session.RowBounds, 
org.apache.ibatis.session.ResultHandler, 
org.apache.ibatis.cache.CacheKey, 
org.apache.ibatis.mapping.BoundSql)

先从一级缓存中查询查询,如果查询不到再从数据库中查询。

查询开始前和查询后都有清理一级缓存的判断,如图所示:

默认情况下:其中配置:ms.isFlushCacheRequired() 为fasle,配置见:

flushCache Setting this to true will cause the local and 2nd level caches to be flushed whenever this statement is called. Default: false for select statements. https://mybatis.org/mybatis-3/sqlmap-xml.html#select

其中:onfiguration.getLocalCacheScope() 为LocalCacheScope.SESSION,见配置:

https://mybatis.org/mybatis-3/configuration.html#settings

即:默认情况下查询前和后都不会清空一级缓存,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除。

一级缓存默认开启而且不能关闭,原因见:

Local cache is used by some basic functionality (association/collection mapping, circular references). Besides, the current result mapping code heavily relies on CacheKey, so it may be quite difficult (if possible) to add such option https://github.com/mybatis/mybatis-3/issues/1278

在分布式环境下,一定要将MyBatis的localCacheScope属性设置为STATEMENT,查询时清空一级缓存,避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。

MyBatis的二级缓存分析:全局缓存,基于Mapper实现,默认关闭


MyBatis的二级缓存属于全局缓存,所有的SqlSession都可以使用。缓存实现维护在全局的Configuration对象中,通过缓存执行器(CacheExecutor)实现,需要我们手动开启。

开启二级缓存配置:

1、settings中配置:cacheEnabled 为true(默认true),为二级缓存第一层全局开关

https://mybatis.org/mybatis-3/configuration.html#settings

org.apache.ibatis.session.Configuration

只有开启全局配置开关cacheEnabled为true时,CachingExecutor才会生效。

2、Mapper配置文件中配置:<cache/>,为二级缓存第二层开关

https://mybatis.org/mybatis-3/sqlmap-xml.html#cache

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

只有配置 了<cache .../> 才会为Mapper生成缓存对象:

org.apache.ibatis.builder.xml.XMLMapperBuilder
#configurationElement

从二级缓存查询数据:

org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

二级缓存的key和一级缓存的key是一样的。

在全局二级缓存开启的状况下,select语句的flushCache和useCache属性如果不设置,那么默认是启用二级缓存的。在执行insert、update、delete语句时,flushCache默认为true,表示任何时候语句被调用,都会导致本地缓存和二级缓存被清空。

MyBatis的一、二级缓存查询关系


一级缓存是SqlSession级别,默认开启。二级缓存是Mapper级别,通过配置开启。在开启二级缓存的状况下,查询数据的顺序为二级缓存→一级缓存→数据库。

Spring + Mybatis 对一级缓存的影响


1、非事务环境下每次都开启新的SqlSession,一级缓存失效;

2、事务环境下,SqlSession引用计数申请与释放,缓存与事务的隔离级别配合,可能导致缓存一致性问题;

其实现可以参考源码:

org.mybatis.spring.SqlSessionTemplate.
SqlSessionInterceptor

小结


在分布式环境下建议禁用MyBatis的一级、二级缓存,否则可能出现数据一致性问题。二级缓存虽然默认关闭,建议设置settings中配置:cacheEnabled 为false,全局关闭此。一级缓存默认开启而且不能关闭,可以设置localCacheScope属性设置为STATEMENT,查询时清空一级缓存。

推荐阅读