内容简介:默认情况下一级缓存是开启的,且是不能关闭的。一级缓存是指SqlSession级别的缓存,当在同一个SqlSession中执行相同的SQL语句查询时,第二次以后的查询不会从数据库查询,而是直接从sqlSession中的本地缓存获取。通常我们会使用Spring集成Mybatis通过mapper接口进行数据库操作。此时SqlSession的生命周期会和Spring事物一致。如果不是在事物中执行查询则每次都会新建SqlSession查询完之后就会销毁,否则会复用同一个通过ThreadLocal与事物进行了绑定的S
概述
Mybatis
是目前 Java 开发中最常用的轻量级ORM框架。正如大多数持久层框架一样,MyBatis同样提供了一级缓存和二级缓存的特性以提高性能。两种缓存的粒度是一样的,都对应一条 sql 查询语句。但二者的生命周期不同,一级缓存的生命周期是 SqlSession
对象的使用期间,随着SqlSession对象的死亡而消失;二级缓存的生命周期是同外部缓存生命周期一样长的;并且是首先查询二级缓存,然后再查询一起缓存。了解Mybatis缓存原理,能够使我们在开发中避免踩一些坑。
一级缓存
默认情况下一级缓存是开启的,且是不能关闭的。一级缓存是指SqlSession级别的缓存,当在同一个SqlSession中执行相同的SQL语句查询时,第二次以后的查询不会从数据库查询,而是直接从sqlSession中的本地缓存获取。
通常我们会使用Spring集成Mybatis通过mapper接口进行数据库操作。此时SqlSession的生命周期会和Spring事物一致。如果不是在事物中执行查询则每次都会新建SqlSession查询完之后就会销毁,否则会复用同一个通过ThreadLocal与事物进行了绑定的SqlSession。
有时在一个复杂的业务方法事物中,我们会拆分为多个子方法处理不同的子逻辑,这样可能会导致查询同一个对象多次,一级缓存的方案优化这部分场景,如果是相同的SQL语句且未执行过update,最终会命中一级缓存,避免多次直接查询数据库,提高性能。
对SqlSession的操作Mybatis内部都是通过Executor来执行的。Executor的生命周期和SqlSession是一致的。Mybatis在Executor中创建了本地缓存(一级缓存)。如下图:
所有Executor都是继承自BaseExecutor,一级缓存的创建就在此类构造函数中:
protected BaseExecutor(Configuration configuration, Transaction transaction) { this.transaction = transaction; this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>(); //Mybatis一级缓存,在创建SqlSession->Executor时候动态创建,随着sqlSession销毁而销毁 this.localCache = new PerpetualCache("LocalCache"); this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache"); this.closed = false; this.configuration = configuration; this.wrapper = this; }
一级缓存的实现很简单,不能像二级缓存那样设置淘汰规则过期时间等,底层使用HashMap存储
一级缓存的配置方式:
<setting name="localCacheScope" value="SESSION"/>
scope有SESSION或STATEMENT两个选项,默认为SESSION级别,即在一个SqlSession中执行的所有语句,都会共享这一个缓存。STATEMENT级别可以理解为缓存只对当前执行的这一个Statement有效,每次执行完查询就会清空缓存,相当一级缓存失效
继续看下Executor中是怎么使用缓存的。具体入口为BaseExecutor的query方法:
//SqlSession.select*会调用此方法(总是先查询一级缓存,缓存中不存在再查询数据库) public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); // 通过sql,参数构建缓存的key CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }
首先会将MappedStatement的Id、对应sql的offset、limit、Sql本身以及Sql中的参数传入了CacheKey这个类,最终构成CacheKey。 CacheKey中有hashcode,checksum和count,updateList等属性,两个CacheKey相等必须hashcode,checksum和count都相等,且updateList中的元素一一对应相同
对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:
1. 传入的statementId (namespace+mapperId)分别对应接口名和方法名 2. 查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offset和rowBounds.limit表示); 3. 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() ) 4. 传递给java.sql.Statement要设置的参数值,包括顺序
继续往下看:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } //先清一级缓存,再查询,但仅仅查询堆栈为0才清,为了处理递归调用 if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { queryStack++; //查询localCache缓存 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { //从数据库中查询数据 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); // 如果配置的一级缓存scope为STATEMENT而非SESSION则,每次清空本地缓存,相当于一级缓存被禁用 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; }
如果缓存未命中,实际查询数据库:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // 由SimpleExecutor、BatchExecutor、ReuseExecutor实现具体的数据库查询方式 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } // 将结果放入本地缓存 localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
如果遇到insert/delete/update方法,一级缓存就会被清空。 SqlSession的delete和insert方法统一会走Executor的update流程。
在BaseExecutor的update方法中会清空本地缓存:
public int update(MappedStatement ms, Object parameter) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } // 清空本地缓存 clearLocalCache(); return doUpdate(ms, parameter); }
实验1
一级缓存默认配置,非事物中多次执行相同语句: 可以看到两次查询返回的不是同一个对象,两者的hashCode不同,两次都真正查询了数据库。
实验2
一级缓存默认配置,事物中多次执行相同语句: 可以看到前两次查询返回的是同一个对象,且两者hashCode相同。 之后执行了一次update,一级缓存被清空,再次查询访问了数据库返回的是不同的对象
二级缓存
如果需要在多个SqlSession间共享缓存,则需要使用Myabtis二级缓存。二级缓存具体实现可由Redis,EhCache等外部缓存代替HashMap,功能更丰富,容量更大
二级缓存实现由CachingExecutor装饰BaseExecutor的具体子类,默认是SimpleExecutor。首先会在CachingExecutor查询二级缓存中是否存在,实现了二级缓存的查询和写入功能,若查询不存在则委托具体职责给delegate Executor。查询执行的流程为 二级缓存 -> 一级缓存 -> 数据库
具体类关系图如下图所示:
二级缓存的使用只需要完成三步:
1、 mapper返回的实体需要可序列化
2、 Myabtis配置文件中开启二级缓存,生成的执行器为CachingExecutor
<setting name="cacheEnabled" value="true"/>
3、 在映射XML中配置cache或者cache-ref(代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache)
<cache type="com.dianwoda.RedisCache eviction="LRU" flushInterval="60000" size="512""></cache> type: org.apache.ibatis.cache.Cache的实现类 eviction: 回收的策略,有FIFO,LRU flushInterval:自动刷新缓存时间间隔,单位毫秒 (仅对PerpetualCache有效,底层为HashMap) size:最多缓存对象个数上限 (仅对PerpetualCache有效) readOnly:是否只读,若配置可读写,则需要对应的实体类能够序列化。 (仅对PerpetualCache有效) blocking 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。 (仅对PerpetualCache有效)
这里使用 redis 作为cache实际实现,每个mapper namespace使用一个Redis Hash结构存储:
public class MybatisCache implements Cache { private RedisTemplate<String, Object> redisTemplate; private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private String id; public MybatisCache(String id) { this.id = id; } @Override public String getId() { return id; } @Override public void putObject(Object key, Object value) { getRedisTempalte().opsForHash().put(id, key.toString(), value); } @Override public Object getObject(Object key) { return getRedisTempalte().opsForHash().get(id, key.toString()); } @Override public Object removeObject(Object key) { getRedisTempalte().opsForHash().delete(id, key.toString()); return null; } @Override public void clear() { getRedisTempalte().delete(id); } @Override public int getSize() { return Math.toIntExact(getRedisTempalte().opsForHash().size(id)); } @Override public ReadWriteLock getReadWriteLock() { return readWriteLock; } public RedisTemplate getRedisTempalte() { if (redisTemplate != null) { return redisTemplate; } redisTemplate = SpringCtxUtils.getBean("redisTemplate"); return redisTemplate; } }
cache的初始化在 org.mybatis.spring.CacheBuilder的build方法中
源码分析
一次select首先会执行CachingExecutor的query方法:
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; } } return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
Cache cache = ms.getCache()
会获取对应MappedStatement中配置的Cache cache应用了装饰模式,LoggingCache提供日志功能,并记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
flushCacheIfRequired
然后判断是否要强制刷新缓存。如果select sql设置了 flushCache="true"
则会在select之后刷新缓存。insert/update/delte将会强制刷新缓存。
对于具体配置的Cache的操作是委托给TransactionalCacheManager进行操作的,其中持有了一个Map
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
TransactionalCache也实现了Cache接口,装饰了具体的初始生成的cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。
如果不调用commit方法的话,由于TranscationalCache的作用,并不会对二级缓存造成直接的影响。在commit时依据标志位、entriesToAddOnCommit等临时数据委托给装饰的Cache进行清空缓存,方式缓存对象等操作
rollback仅仅清空标志位、entriesToAddOnCommit等临时数据,不影响实际二级缓存
public void commit() { if (clearOnCommit) { delegate.clear(); } flushPendingEntries(); reset(); } public void rollback() { unlockMissedEntries(); reset(); }
继续往下走,如果查询语句开启了useCache并且resultHandler为null则会查询二级缓存 尝试从tcm中获取缓存的数据
List<E> list = (List<E>) tcm.getObject(cache, key);
这个方法会将获取值的职责不断传递给每个装饰者,最终到达具体的实现类MybatisCache。如果没有命中二级缓存,会把key加入Miss集合,这个主要是为了统计命中率。 如果查询不到数据,在二级缓存执行流程后就会进入一级缓存的执行流程。如果从一级缓存或数据库中查询到数据,则调用tcm.putObject方法,往二级缓存中放入值
if (list == null) { list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 }
由于任何的更新修改删除操作都会导致一个mapper namespace下的所有缓存被清空,在清空频繁的情况下,性能是较差的。Mybatis二级缓存的粒度为mapper namespace级别,粒度较粗适用于读多写少的情况,且不太适合连表查询的sql,不过对业务代码入侵较少。
总结
本文介绍了MyBatis一二级缓存的基本概念,并简单对其原理机制进行了分析。个人建议MyBatis二级缓存特性在生产环境中进行关闭,一级缓存可以开启对性能影响不大,仅作为一个ORM框架使用可能较为合适。
reference
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 缓存原理与微服务缓存自动管理
- Memcache缓存系统原理
- http缓存机制及其原理
- ThinkPhp缓存原理及使用详解
- 理解高性能内存缓存对象缓存Memcached原理 (Memcached核心概念,部署)
- 聊聊缓存淘汰算法:LRU 实现原理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
大型网站技术架构
李智慧 / 电子工业出版社 / 2013-9-1 / 59.00元
《大型网站技术架构:核心原理与案例分析》通过梳理大型网站技术发展历程,剖析大型网站技术架构模式,深入讲述大型互联网架构设计的核心原理,并通过一组典型网站技术架构设计案例,为读者呈现一幅包括技术选型、架构设计、性能优化、Web 安全、系统发布、运维监控等在内的大型网站开发全景视图。 《大型网站技术架构:核心原理与案例分析》不仅适用于指导网站工程师、架构师进行网站技术架构设计,也可用于指导产品经......一起来看看 《大型网站技术架构》 这本书的介绍吧!