title: “mybati缓存了解”
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
draft: false
author: “ggball”
tags: [“mybatis”]
categories: [“java”]
description: “mybati缓存了解”
mybatis的缓存
首先来看下mybatis对缓存的规范,规范嘛就是定义的接口啦。
缓存接口
Cache接口 定义了缓存的方法
public interface Cache {/**获取缓存的id* @return The identifier of this cache*/String getId();/**添加缓存* @param key Can be any object but usually it is a {@link CacheKey}* @param value The result of a select.*/void putObject(Object key, Object value);/**根据缓存键获取缓存* @param key The key* @return The object stored in the cache.*/Object getObject(Object key);/**移除缓存* As of 3.3.0 this method is only called during a rollback * for any previous value that was missing in the cache.* This lets any blocking cache to release the lock that * may have previously put on the key.* A blocking cache puts a lock when a value is null * and releases it when the value is back again.* This way other threads will wait for the value to be * available instead of hitting the database.** * @param key The key* @return Not used*/Object removeObject(Object key);/*** Clears this cache instance*/ void clear();/*** Optional. This method is not called by the core.* * @return The number of elements stored in the cache (not its capacity).*/int getSize();/** * Optional. As of 3.2.6 this method is no longer called by the core.* * Any locking needed by the cache must be provided internally by the cache provider.* * @return A ReadWriteLock */ReadWriteLock getReadWriteLock();}
实现
mybatis实现了多种缓存,比如perpetualCache 是Cache接口的默认实现,通过hashMap来操作缓存,logginCache,在具有缓存的功能下,添加了打印日志的功能。
-
BlockingCache:阻塞版本的缓存装饰器,能够保证同一时间只有一个线程到缓存中查找指定的Key对应的数据。
-
FifoCache:先入先出缓存装饰器,FifoCache内部有一个维护具有长度限制的Key键值链表(LinkedList实例)和一个被装饰的缓存对象,Key值链表主要是维护Key的FIFO顺序,而缓存存储和获取则交给被装饰的缓存对象来完成。
-
LoggingCache:为缓存增加日志输出功能,记录缓存的请求次数和命中次数,通过日志输出缓存命中率。LruCache:最近最少使用的缓存装饰器,当缓存容量满了之后,使用LRU算法淘汰最近最少使用的Key和Value。
-
LruCache中通过重写LinkedHashMap类的removeEldestEntry()方法获取最近最少使用的Key值,将Key值保存在LruCache类的eldestKey属性中,然后在缓存中添加对象时,淘汰eldestKey对应的Value值。具体实现细节读者可参考LruCache类的源码。
-
ScheduledCache:自动刷新缓存装饰器,当操作缓存对象时,如果当前时间与上次清空缓存的时间间隔大于指定的时间间隔,则清空缓存。清空缓存的动作由getObject()、putObject()、removeObject()等方法触发。
-
SerializedCache:序列化缓存装饰器,向缓存中添加对象时,对添加的对象进行序列化处理,从缓存中取出对象时,进行反序列化处理。
-
SoftCache:软引用缓存装饰器,SoftCache内部维护了一个缓存对象的强引用队列和软引用队列,缓存以软引用的方式添加到缓存中,并将软引用添加到队列中,获取缓存对象时,如果对象已经被回收,则移除Key,如果未被回收,则将对象添加到强引用队列中,避免被回收,如果强引用队列已经满了,则移除最早入队列的对象的引用。
-
SynchronizedCache:线程安全缓存装饰器,SynchronizedCache的实现比较简单,为了保证线程安全,对操作缓存的方法使用synchronized关键字修饰。
-
TransactionalCache:事务缓存装饰器,该缓存与其他缓存的不同之处在于,TransactionalCache增加了两个方法,即commit()和rollback()。当写入缓存时,只有调用commit()方法后,缓存对象才会真正添加到TransactionalCache对象中,如果调用了rollback()方法,写入操作将被回滚。WeakCache:弱引用缓存装饰器,功能和SoftCache类似,只是使用不同的引用类型。
mybatis一级缓存
概念:
会话(session)级别的缓存称为一级缓存,默认开启的。
为什么使用一级缓存?
mybatis毕竟是查询数据库的一个半orm框架,查询数据库势必要消耗服务器的性能,为了减少服务器的性能,使用了缓存。将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。
实现原理自己的概括
当程序与数据库建立了一次会话,中间开始查询数据,每次查询会根据mapper的id、命名空间、sql等等创建缓存key,先去查询本地缓存是否有值,如果有值,则获取解析值,返回,如果没有值,则去查询数据库,再把结果缓存到本地缓存。
一级缓存实现原理
首先来看下缓存实例是存在哪里的,在BaseExecutor中有本地缓存localCache,所以继承BaseExecutor的执行器都有localCache,包括但不限于
SimpleExecutor、BatchExecutor,ReuseExecutor。
接下来大概介绍查询流程,具体介绍用到一级缓存的地方
@Testpublic void testMybatisCache () throws IOException {// 获取配置文件输入流InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");// 通过SqlSessionFactoryBuilder的build()方法创建SqlSessionFactory实例SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);// 调用openSession()方法创建SqlSession实例SqlSession sqlSession = sqlSessionFactory.openSession();// 获取UserMapper代理对象UserMapper userMapper = sqlSession.getMapper(UserMapper.class);// 执行Mapper方法,获取执行结果List<UserEntity> userList = userMapper.listAllUser();UserMapper userMapper1 = sqlSession.getMapper(UserMapper.class);// 执行Mapper方法,获取执行结果List<UserEntity> userList1 = userMapper.listAllUser();System.out.println(JSON.toJSONString(userList));}@Overridepublic SqlSession openSession() {return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;try {// 获取Mybatis主配置文件配置的环境信息final Environment environment = configuration.getEnvironment();// 创建事务管理器工厂final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);// 创建事务管理器tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 根据Mybatis主配置文件中指定的Executor类型创建对应的Executor实例final Executor executor = configuration.newExecutor(tx, execType);// 创建DefaultSqlSession实例return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {closeTransaction(tx); // may have fetched a connection so lets call close()throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);} finally {ErrorContext.instance().reset();}}
- 从mybatis配置文件获取配置信息输入流,然后利用SqlSessionFactoryBuilder的build的方法创建SqlSession工厂
- openSession方法创建默认SqlSession(获取mybatis环境信息,创建事务管理器,创建执行器,构造默认SqlSession)
- 调用sqlSession的getMapper方法,利用动态代理(实现InvocationHadler)创建代理对象
- 调用Mapper的方法,实际上就是调用代理对象的invoke方法,而且调用查询方法(不管是默认还是自己写的),最后都会调用sqlSession的select相关方法。
根据上面的流程可以知道,SqlSession,Executor,localcache之间的关系
接下来主要看SqlSession的select方法
@Overridepublic void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {try {MappedStatement ms = configuration.getMappedStatement(statement);executor.query(ms, wrapCollection(parameter), rowBounds, handler);} catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);} finally {ErrorContext.instance().reset();}}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {// 获取BoundSql对象,BoundSql是对动态SQL解析生成的SQL语句和参数映射信息的封装BoundSql boundSql = ms.getBoundSql(parameter);// 创建CacheKey,用于缓存KeyCacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);// 调用重载的query()方法return query(ms, parameter, rowBounds, resultHandler, key, boundSql);}@Overridepublic <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.");}if (queryStack == 0 && ms.isFlushCacheRequired()) {clearLocalCache();}List<E> list;try {queryStack++;// 从缓存中获取结果list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 缓存中获取不到,则调用queryFromDatabase()方法从数据库中查询list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}if (queryStack == 0) {for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}// issue #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}return list;}@Overridepublic CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {if (closed) {throw new ExecutorException("Executor was closed.");}CacheKey cacheKey = new CacheKey();
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();// 所有参数值for (ParameterMapping parameterMapping : parameterMappings) {if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;String propertyName = parameterMapping.getProperty();if (boundSql.hasAdditionalParameter(propertyName)) {value = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {value = parameterObject;} else {MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00}}// Environment Idif (configuration.getEnvironment() != null) {
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00}return cacheKey;}
- 调用SqlSession的select方法,从configuration拿出mappedSatement(里面封装了mapper的属性),调用内置的执行器的query方法
- 执行器的query方法,先获取BoundSql对象,创建cacheKey(利用MapperId,偏移量,SQL语句…),调用重载的query
- 先根据cacheKey去执行器的localCache查询是否有值,如果没有再调用queryFromDatabase查询数据库,缓存结果到localCache。注意:LocalCacheScope=SATATEMENT时,每次查询都会清空缓存。
注意:在分布式环境下,务必将MyBatis的localCacheScope属性设置为STATEMENT,避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。
在MyBatis中,关于缓存设置的参数一共有2个:localCacheScope,cacheEnabled。
<!-- 二级缓存开关 有效值: true|false,默认值为true -->
<settingname="cacheEnabled"value="true"/>
<!-- 是否清除一级缓存 SESSION不清除,STATEMENT清除 有效值:SESSION|STATEMENT,默认值为SESSION -->
<settingname="localCacheScope"value="SESSION"/>
mybatis二级缓存
概念
二级缓存是全局的缓存,即使不同会话之间也能共享二级缓存,默认是不开启的;
二级缓存实现原理
首先说下如何开启他,在mybatis配置文件添加<settingname="cacheEnabled"value="true"/>
和在对应的mapper.xml添加cache实例
其次看下二级缓存是如何生效的
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;// 根据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);}// 如果cacheEnabled属性为ture,这使用CachingExecutor对上面创建的Executor进行装饰if (cacheEnabled) {executor = new CachingExecutor(executor);}// 执行拦截器链的拦截逻辑executor = (Executor) interceptorChain.pluginAll(executor);return executor;}public class CachingExecutor implements Executor {private final Executor delegate;private final TransactionalCacheManager tcm = new TransactionalCacheManager();public CachingExecutor(Executor delegate) {this.delegate = delegate;delegate.setExecutorWrapper(this);}@Overridepublic Transaction getTransaction() {return delegate.getTransaction();}@Overridepublic void close(boolean forceRollback) {try {//issues #499, #524 and #573if (forceRollback) { tcm.rollback();} else {tcm.commit();}} finally {delegate.close(forceRollback);}}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);// 调用createCacheKey()方法创建缓存KeyCacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}@Overridepublic <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {flushCacheIfRequired(ms);return delegate.queryCursor(ms, parameter, rowBounds);}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {// 获取MappedStatement对象中维护的二级缓存对象Cache cache = ms.getCache();if (cache != null) {// 判断是否需要刷新二级缓存flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, boundSql);// 从MappedStatement对象对应的二级缓存中获取数据@SuppressWarnings("unchecked")List<E> list = (List<E>) tcm.getObject(cache, key);if (list == null) {// 如果缓存数据不存在,则从数据库中查询数据list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// 將数据存放到MappedStatement对象对应的二级缓存中tcm.putObject(cache, key, list); // issue #578 and #116}return list;}}return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}}public class TransactionalCacheManager {// 通过HashMap对象维护二级缓存对应的TransactionalCache实例private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();public void clear(Cache cache) {getTransactionalCache(cache).clear();}public Object getObject(Cache cache, CacheKey key) {// 获取二级缓存对应的TransactionalCache对象,然后根据缓存Key获取缓存对象return getTransactionalCache(cache).getObject(key);}public void putObject(Cache cache, CacheKey key, Object value) {getTransactionalCache(cache).putObject(key, value);}public void commit() {for (TransactionalCache txCache : transactionalCaches.values()) {txCache.commit();}}public void rollback() {for (TransactionalCache txCache : transactionalCaches.values()) {txCache.rollback();}}private TransactionalCache getTransactionalCache(Cache cache) {// 获取二级缓存对应的TransactionalCache对象TransactionalCache txCache = transactionalCaches.get(cache);if (txCache == null) {// 如果获取不到则创建,然后添加到Map中txCache = new TransactionalCache(cache);transactionalCaches.put(cache, txCache);}return txCache;}}
-
还是在
sqlSessionFactory.openSession();
时,会创建执行器,当cacheEnabled属性为ture,会创建CachingExecutor缓存执行器。 -
看下CachingExecutor的结构,它包含了一个委托执行器(使用了委托模式),用来真正执行的sql,而自己主要的作用是放在了建立和使用二级缓存
-
当执行sql,最后会进入到执行器,如果执行器是CachingExecutor时,会调用他的query方法,进入方法后,首先会从
MappedStatement
拿出二级缓存实例(你以为这就是二级缓存?不,你错了),然后判断是否要刷新缓存,再根据二级缓存实例从缓存管理器
(CacheExecutor维护了TransactionalCacheManager缓存管理器,缓存管理器里面维护了二级缓存实例和TransactionalCache的关系)中得到TransactionalCache,再利用cacheKey获取TransactionalCache中对应的二级缓存,如果缓存不存在,则使用委托执行器去数据库查询数据,再缓存结果,如果存在,则直接返回。再回过头看,当时的
MappedStatement
是如何get二级缓存实例的;private void configurationElement(XNode context) {try {// 获取命名空间String namespace = context.getStringAttribute("namespace");if (namespace == null || namespace.equals("")) {throw new BuilderException("Mapper's namespace cannot be empty");}// 设置当前正在解析的Mapper配置的命名空间builderAssistant.setCurrentNamespace(namespace);// 解析<cache-ref>标签cacheRefElement(context.evalNode("cache-ref"));// 解析<cache>标签cacheElement(context.evalNode("cache"));// 解析所有的<parameterMap>标签parameterMapElement(context.evalNodes("/mapper/parameterMap"));// 解析所有的<resultMap>标签resultMapElements(context.evalNodes("/mapper/resultMap"));// 解析所有的<sql>标签sqlElement(context.evalNodes("/mapper/sql"));
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
} catch (Exception e) {
throw new BuilderException(“Error parsing Mapper XML. The XML location is '” + resource + "'. Cause: " + e, e);
}
}
private void cacheElement(XNode context) throws Exception {if (context != null) {String type = context.getStringAttribute("type", "PERPETUAL");Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);String eviction = context.getStringAttribute("eviction", "LRU");Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);Long flushInterval = context.getLongAttribute("flushInterval");Integer size = context.getIntAttribute("size");boolean readWrite = !context.getBooleanAttribute("readOnly", false);boolean blocking = context.getBooleanAttribute("blocking", false);Properties props = context.getChildrenAsProperties();builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);}}MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType).resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType).keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang).resultOrdered(resultOrdered).resultSets(resultSets).resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType).flushCacheRequired(valueOrDefault(flushCache, !isSelect)).useCache(valueOrDefault(useCache, isSelect)).cache(currentCache);
如上面的代码所示,在获取<cache>标签的所有属性信息后,调用MapperBuilderAssistant对象的userNewCache()方法创建二级缓存实例,然后通过MapperBuilderAssistant的currentCache属性保存二级缓存对象的引用。在调用MapperBuilderAssistant对象的addMappedStatement()方法创建MappedStatement对象时会将当前命名空间对应的二级缓存对象的引用添加到MappedStatement对象中,所以这就是需要配置<cache>的原因。流程![image-20211116235954488](https://img-blog.csdnimg.cn/img_convert/8ad5873a6586eb7912f170272d6da1c2.png)### mybatis二级缓存解决了什么问题解决了一级缓存在不同session存在脏读的问题,但是分布式二级缓存也存在脏读。![image-20211117000030143](https://img-blog.csdnimg.cn/img_convert/cca57efae1991272139c4998773fe8e7.png)### 总结MyBatis一级缓存是SqlSession级别的缓存,默认就是开启的,而且无法关闭;二级缓存需要在MyBatis主配置文件中通过设置cacheEnabled参数值来开启。