Mybatis深入浅出之缓存机制
一、简述
MyBatis是常见的Java数据库访问层框架之一。MyBatis为提高其数据库查询性能,提供了缓存机制(查询缓存),包括一级缓存和二级缓存。由于项目的业务场景多样化以及分布式构架系统的普及,Mybatis缓存造成一些脏数据场景也是偶有发生,本文结合Mybatis源码以及官网等相关资料,帮助更多的开发人员熟悉掌握Mybatis缓存机制。
二、一级缓存
Mybatis在一次Sqlsession数据库查询之后,会将查询结果以键值对形式存储到内存中,当前Sqlsession后续以相同sql查询时,会直接去查询内存缓存,避免数据库查询,提高查询性能。
应用场景:当前线程同一个SqlSession会话中执行多次相同sql查询。
1、流程图:
2、 时序图:
3、一级缓存特点
- 作用粒度:同一个SqlSession对象sql查询。
- 默认设置:Mybatis默认开启一级缓存。
- 作用:同一Sqlsession对象执行多次相同sql查询,避免数据库访问,提高查询性能,节省数据库访问开销。
- 清理方式:增删查改commit()都清理一级缓存(包括查询commit)
- 关闭方式:配置中localCacheScope设置为:STATEMENT。 即:<setting name="localCacheScope" value="STATEMENT"/> ,默认数值:SESSION,即开启一级缓存。
- 脏读解决方案:方案1->关闭一级缓存;方案2->开启二级缓存。
4、验证
-
一级缓存命中
1>测试demo
2>配置
3>运行结果
第一次查询进行数据库连接查询,第二次第三次查询为命中缓存查询。
-
清理一级缓存
执行结果:第一次查询之后,进行了commit操作,会清理一级缓存。因此第二次查询时仍然需要连接数据库,发送查询sql至数据库执行sql查询。
-
关闭一级缓存
执行结果:关闭一级缓存,同一SqlSession多次sql查询不共享缓存,每次都需要发送sql至数据库查询。
三、二级缓存
一级缓存其共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存,其共享范围为Namespace。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询。
适用场景:多线程执行相同namespace下查询语句,直接查询二级缓存,避免数据库连接查询,提高性能。
1、流程图
- 二级缓存开启后,同一个namespace下的所有sql操作语句,都影响着同一个Cache,即二级缓存可以被多个SqlSession共享,可以视为一个全局的变量。
- 二级缓存依赖于一级缓存,因此关闭一级缓存,即使二级缓存开启也没有数据。(sqlSession.close()执行完,Mybatis会将一级缓存对象拷贝存储到二级缓存)
- 查询执行顺序:二级缓存 -> 一级缓存 -> 数据库。
- 更新执行顺序:数据库->一级缓存->二级缓存。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, parameterObject, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } }
2、开启二级缓存
- 步骤一:配置setting,即:<setting name="cacheEnabled" value="true"/>
- 步骤二:指定namespace配置开启,即:<cache/>
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
执行结果:sqlSession2查询命中二级缓存。命中率为:1/2=0.5。
注意:
1、二级缓存依赖于一级缓存写入,不关闭sqlSession1,不会写入当前namespace的二级缓存,造成二级缓存失效。
2、Mybatis二级缓存对象存储在硬盘中,因此需要namespace下实体对象序列化,如果不序列话运行会报错:
Exception in thread "main" org.apache.ibatis.cache.CacheException:
Error serializing object. Cause: java.io.NotSerializableException: ****。
public class TruckInfo implements Serializable {}
3、二级缓存特点
- Mybatis默认关闭二级缓存。
- 作用粒度:相同namespace下所有sql语句共享缓存,即:支持不同sqlSession共享缓存。
- 数据写入时机:sqlSession.close(),即:sql会话关闭后,统一将当前namespace下的一级缓存写入二级缓存。节约I/O,提高I/O性能。
- 二级缓存清理:与清理一级缓存相同,执行commit()方法,但是仅仅针对增删改commit,查询commit无法触发清理。
- 关闭单个查询语句二级缓存方式:xml sql语句设置关闭useCache="false"。
1>二级缓存清理:
2>关闭单个sql查询二级缓存
关闭了二级缓存查询,即使存储了缓存也不起作用。
4、二级缓存特殊场景
- 多个sql.xml文件中namespace相同,则这些xml产生的Mapper对象仍然共享同一个namespace二级缓存。
- 两张表TA,其namespaceA、TB其namespaceB,A表的部分sqlA不规范编写到namespaceB中,则该部分sqlA的二级缓存存储在namespaceB中,当使用namespaceA二级缓存时可能造成脏数据,因为部分更新缓存存储在namespaceB中。
解决方案:将namespaceA引用关联到namespaceB中,保证两个映射文件对应的SQL操作都使用的是同一块缓存。
- 应对分布式部署项目,一个应用多个实例机器造成脏数据(二级缓存)。
解决方案:<1>禁用二级缓存,<2>使用分布式缓存中间件。
5、二级缓存中间件
当前有不少第三方二级缓存中间件,比如:memcache、ehcache等,根据Mybatis规范要求,整合第三方二级缓存中间件,必须实现org.apache.ibatis.cache接口。
缓存中间件的一个优点在于:可以设置二级缓存为内存缓存,减少了I/O消耗,提高了查询性能。
6、Mybatis缓存机制
四、缓存比较
对比项 | 一级缓存 | 二级缓存 |
作用粒度 | sqlSession | nameSpace |
开启方式 | 默认开启 | 配置开启 |
存储方式 | 内存 | 硬盘 |
清理时机 | 增删改查commit() | 增删改commit() |
存储时机 | 执行完sql | sqlSession.close() |
关闭方式 | 设置statement模式 |
不启用->全局关闭 设置useCache="false"->单个sql查询关闭 |
脏数据场景 | 多线程并发场景,不同线程不同sqlSession更新数据 |
分布式多实例机器部署 namespace不规范编写其他表sql |
五、验证代码
- github:https://github.com/gavincoder/Mybatis.git
- gitee:https://gitee.com/gavincoderspace/mybatis.git