『手撕 Mybatis 源码』11 - 二级缓存

二级缓存

概述

  1. 启用二级缓存需要进行三步配置
  • 开启映射器配置文件中的缓存配置
  <settings><!--cacheEnabled值默认就为true--><setting name="cacheEnabled" value="true"/></settings>
  • 在需要使用二级缓存的 Mapper 配置文件中配置标签
  <!--type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。eviction: 定义回收的策略,常见的有FIFO,LRU。flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。size: 最多缓存对象的个数。readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。--><cache></cache>
  • 在具体 CURD 标签上配置 useCache=true
  <!--useCache默认值也是true--><select id="findByCondition" resultType="com.itheima.pojo.User" useCache="true">SELECT id, name FROM  user WHERE id = #{id}</select>
  1. 新增二级缓存测试,测试的 case 先是创建一个会话,执行一次查询,然后其中一个会话进行一次 commit()(否则没办法生效二级缓存),然后再使用另外一个会话查询一次
public class CacheTest {/*** 测试二级缓存*/@Testpublic void secondLevelCacheTest() throws IOException {InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");// 2. (1)解析了配置文件,封装configuration对象 (2)创建了DefaultSqlSessionFactory工厂对象SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);// 3.问题:openSession() 执行逻辑是什么?// 3. (1)创建事务对象 (2)创建了执行器对象 cachingExecutor (3)创建了 DefaultSqlSession 对象SqlSession sqlSession1 = sqlSessionFactory.openSession();SqlSession sqlSession2 = sqlSessionFactory.openSession();// 发起第一次查询,查询ID为1的用户User user1 = sqlSession1.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);// 必须要调用 sqlSession 的 commit 方法或者 close() 方法,才能让二级缓存生效sqlSession1.commit();// 第二次查询User user2 = sqlSession2.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);// 返回是 false,因为存储的是数据本身,所以两个不是同一个地址System.out.println(user1==user2);System.out.println(user1);System.out.println(user2);sqlSession1.close();}
}

标签 <cache> 解析

  1. 问题
  • cache 标签如何被解析的?
  1. 首先继续回到解析配置文件部分
public class CacheTest {/*** 测试二级缓存*/@Testpublic void secondLevelCacheTest() throws IOException {InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");// 2. (1)解析了配置文件,封装configuration对象 (2)创建了DefaultSqlSessionFactory工厂对象SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);...}
}
  1. SqlSessionFactoryBuilder 创建 XMLConfigBuilder 解析配置文件,然后开始分析
public class SqlSessionFactoryBuilder {...public SqlSessionFactory build(InputStream inputStream) {return build(inputStream, null, null);}public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {try {// XMLConfigBuilder:用来解析XML配置文件// 使用构建者模式(至少4个以上成员变量):好处:降低耦合、分离复杂对象的创建// 1. 创建 XPathParser 解析器对象,根据 inputStream 解析成了 document 对象 // 2. 创建全局配置对象 Configuration 对象XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);// parser.parse():使用XPATH解析XML配置文件,将配置文件封装到Configuration对象// 返回DefaultSqlSessionFactory对象,该对象拥有Configuration对象(封装配置文件信息)// 3. parse():配置文件就解析完成了return build(parser.parse());} catch (Exception e) {throw ExceptionFactory.wrapException("Error building SqlSession.", e);} finally {ErrorContext.instance().reset();try {inputStream.close();} catch (IOException e) {// Intentionally ignore. Prefer previous error.}}}
}
  1. XMLConfigBuilder 开始从根节点 /configuration 开始解析,因为关注的是二级缓存,我们从解析 /mappers 开始
public class XMLConfigBuilder extends BaseBuilder {private final XPathParser parser;...public Configuration parse() {if (parsed) {throw new BuilderException("Each XMLConfigBuilder can only be used once.");}parsed = true;// parser.evalNode("/configuration"):通过XPATH解析器,解析configuration根节点// 1. 从 configuration 根节点开始解析,最终将解析出的内容封装到 Configuration 对象中parseConfiguration(parser.evalNode("/configuration"));return configuration;}private void parseConfiguration(XNode root) {try {...// 2. 解析 </mappers> 标签 加载映射文件流程主入口mapperElement(root.evalNode("mappers"));} catch (Exception e) {throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);}}
}
  1. mapperElement() 在解析 /mapper 节点时,不管是解析的是要通过 /package 节点还是本身 /mapper解析,最终会调用 XMLMapperBuilder 来解析 mapper 映射文件
public class XMLConfigBuilder extends BaseBuilder {protected final Configuration configuration;...private void mapperElement(XNode parent) throws Exception {if (parent != null) {// 获取<mappers>标签的子标签for (XNode child : parent.getChildren()) {// <package>子标签if ("package".equals(child.getName())) {// 获取mapper接口和mapper映射文件对应的package包名String mapperPackage = child.getStringAttribute("name");// 1. 将包下所有的 mapper 接口以及它的代理工厂对象存储到一个 Map 集合中,key 为 mapper 接口类型,value 为代理对象工厂configuration.addMappers(mapperPackage);} else {// <mapper>子标签....}}}}
}
  1. configuration.addMappers() 最终就交由 XMLMapperBuilder 来解析 mapper 映射文件
public class XMLMapperBuilder extends BaseBuilder {protected final Configuration configuration;private final XPathParser parser;...public void parse() {// mapper 映射文件是否已经加载过 resource = "mapper/UserMapper.xml"if (!configuration.isResourceLoaded(resource)) {// 1. 从映射文件中的<mapper>根标签开始解析,直到完整的解析完毕configurationElement(parser.evalNode("/mapper"));...}...}
}
  1. XMLMapperBuilder 解析 mapper.xml 时,就会解析对应的 <cache> 子标签,这个是开启二级缓存的标志
public class XMLMapperBuilder extends BaseBuilder {...private void configurationElement(XNode context) {try {...// 解析<cache>子标签cacheElement(context.evalNode("cache"));...} catch (Exception e) {throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);}}
}
  1. 解析 /cache 节点时,首先检查是否已经指定 type 属性,如果有,则创建指定 type 的缓存对象,否则默认的二级缓存,创建的是 PerpetualCache,然后根据二级缓存的所有配置,创建对应的属性对象,通过 builderAssistant.useNewCache() 创建二级缓存
public class XMLMapperBuilder extends BaseBuilder {...private final MapperBuilderAssistant builderAssistant;protected final TypeAliasRegistry typeAliasRegistry;private void cacheElement(XNode context) {if (context != null) { // 如果是指定 <cache type="redisCache"> </cache>,那就用配置的,否则用 PERPETUALCache// 1. 解析<cache>标签type属性的值,在这可以自定义type的值,比如redisCache,如果没有指定默认就是PERPETUALString type = context.getStringAttribute("type", "PERPETUAL");Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);// 获取负责过期的eviction对象,默认策略为LRUString eviction = context.getStringAttribute("eviction", "LRU");Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);// 清空缓存的频率 0代表不清空Long flushInterval = context.getLongAttribute("flushInterval");// 缓存容器的大小Integer size = context.getIntAttribute("size");// 是否只读boolean readWrite = !context.getBooleanAttribute("readOnly", false);// 是否阻塞boolean blocking = context.getBooleanAttribute("blocking", false);// 获得 Properties 属性Properties props = context.getChildrenAsProperties();// 2. 创建二级缓存builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);}}
}
  1. MapperBuilderAssistant 就会将所有 /cache 属性组装生成 cache 对象,然后添加到 configuration 对象中(其中内部会获取 cache 对象的 id:com.itheima.mapper.UserMapper 保存到 Map 中),然后把缓存赋值给 currentCache 属性,等会还得使用
public class MapperBuilderAssistant extends BaseBuilder {// 当前 cache 对象private Cache currentCache;...public Cache useNewCache(Class<? extends Cache> typeClass,Class<? extends Cache> evictionClass,Long flushInterval,Integer size,boolean readWrite,boolean blocking,Properties props) {// 1. 生成 cache 对象Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class)).addDecorator(valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size).readWrite(readWrite).blocking(blocking).properties(props).build();// 2. 添加到configuration中configuration.addCache(cache);// 3. 并赋值给 MapperBuilderAssistant 中的 currentCache 属性currentCache = cache;return cache;}
}
  1. 解析完 /cache 节点后,还需要解析 /select|/insert|/update|/delete 节点,因为这里会把刚刚创建的二级缓存也放到 MappedStatement
public class XMLMapperBuilder extends BaseBuilder {.../***  解析映射文件* @param context 映射文件根节点<mapper>对应的XNode*/private void configurationElement(XNode context) {try {...// 1. 按顺序解析 <select>\<insert>\<update>\<delete> 子标签// 2. 将cache对象封装到MappedStatement中buildStatementFromContext(context.evalNodes("select|insert|update|delete"));} catch (Exception e) {throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);}}
}
  1. 下面解析 /select|/insert|/update|/delete 节点前,会分别创建 XMLStatementBuilder 来解析并创建 MappedStatement 对象
public class XMLMapperBuilder extends BaseBuilder {protected final Configuration configuration;...private void buildStatementFromContext(List<XNode> list) {if (configuration.getDatabaseId() != null) { //# 判断是否配置过 databaseIdbuildStatementFromContext(list, configuration.getDatabaseId());}// 1. 构建 MappedStatementbuildStatementFromContext(list, null);}/*** 2、专门用来解析MappedStatement* @param list* @param requiredDatabaseId*/private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {for (XNode context : list) {// 2. MappedStatement 解析器final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);try {// 3. 解析 select 等 4 个标签,创建 MappedStatement 对象statementParser.parseStatementNode();} catch (IncompleteElementException e) {configuration.addIncompleteStatement(statementParser);}}}
}
  1. statementParser.parseStatementNode() 解析节点时,先判断是否标记了 useCache 属性,然后通过构建者助手,创建 MappedStatement 对象
public class XMLStatementBuilder extends BaseBuilder {private final MapperBuilderAssistant builderAssistant;private final XNode context;.../*** 解析<select>\<insert>\<update>\<delete>子标签*/public void parseStatementNode() {...// 1. 获取 sql 上的属性,默认就是 trueboolean useCache = context.getBooleanAttribute("useCache", isSelect);...// 2. 通过构建者助手,创建 MappedStatement 对象builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,resultSetTypeEnum, flushCache, useCache, resultOrdered,keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);}
}
  1. 然后 MapperBuilderAssistant 当时是把二级缓存放到了 currentCache 属性中,为的就是把创建出来的缓存存入 MappedStatement 中,创建 MappedStatement 后,存入 Configuration 对象,最终返回构建好的 DefaultSqlSessionFactory。从上面流程可以看出,二级缓存,其实是配置文件命名空间下共享的
public class MapperBuilderAssistant extends BaseBuilder {// 当前cache对象private Cache currentCache;protected final Configuration configuration;.../***  通过构建者助手,创建MappedStatement对象* @param id* @param sqlSource* @param statementType* @param sqlCommandType* @param fetchSize* @param timeout* @param parameterMap* @param parameterType* @param resultMap* @param resultType* @param resultSetType* @param flushCache* @param useCache* @param resultOrdered* @param keyGenerator* @param keyProperty* @param keyColumn* @param databaseId* @param lang* @param resultSets* @return*/public MappedStatement addMappedStatement(String id,SqlSource sqlSource,StatementType statementType,SqlCommandType sqlCommandType,Integer fetchSize,Integer timeout,String parameterMap,Class<?> parameterType,String resultMap,Class<?> resultType,ResultSetType resultSetType,boolean flushCache,boolean useCache,boolean resultOrdered,KeyGenerator keyGenerator,String keyProperty,String keyColumn,String databaseId,LanguageDriver lang,String resultSets) {if (unresolvedCacheRef) {throw new IncompleteElementException("Cache-ref not yet resolved");}id = applyCurrentNamespace(id, false);boolean isSelect = sqlCommandType == SqlCommandType.SELECT;//利用构建者模式,去创建MappedStatement.Builder,用于创建MappedStatement对象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);// 1. 将 cache 对象存入到 MappedStatement中,相同 Mapper 中的 MappedStatement 共用同一个 CacheParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);if (statementParameterMap != null) {statementBuilder.parameterMap(statementParameterMap);}// 2. 通过 MappedStatement.Builder,构建一个 MappedStatementMappedStatement statement = statementBuilder.build();// 3. 将 MappedStatement 对象存储到 Configuration 中的 Map 集合中,key 为 statement 的 id,value 为 MappedStatement 对象configuration.addMappedStatement(statement);return statement;}
}
  1. 总结
    在这里插入图片描述

二级缓存执行

  1. 问题
  • 同时开启一级缓存,二级缓存。优先级?
  • 为什么只有执行 sqlSession.commit() 或者 sqlSession.close() 二级缓存才会生效
  1. 继续沿用上一次的测试 case 来研究解决这些问题
public class CacheTest {/*** 测试二级缓存*/@Testpublic void secondLevelCacheTest() throws IOException {InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");// 2. (1)解析了配置文件,封装configuration对象 (2)创建了DefaultSqlSessionFactory工厂对象SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);// 3.问题:openSession()执行逻辑是什么?// 3. (1)创建事务对象 (2)创建了执行器对象cachingExecutor (3)创建了DefaultSqlSession对象SqlSession sqlSession1 = sqlSessionFactory.openSession();SqlSession sqlSession2 = sqlSessionFactory.openSession();// 发起第一次查询,查询ID为1的用户User user1 = sqlSession1.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);// **必须要调用sqlSession的commit方法或者close方法,才能让二级缓存生效sqlSession1.commit();// 第二次查询User user2 = sqlSession2.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);// 返回是 false,因为存储的是数据本身,所以两个不是同一个地址System.out.println(user1==user2);System.out.println(user1);System.out.println(user2);sqlSession1.close();}
}
  1. 首先第一次执行 selectOne() 继续经过多次 selectList() 方法,生成 MappedStatement 然后委派给 Executor 执行
public class DefaultSqlSession implements SqlSession {private final Configuration configuration;private final Executor executor;...private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {try {// 根据传入的 statementId 即 user.findUserById,获取 MappedStatement 对象MappedStatement ms = configuration.getMappedStatement(statement);// 1. 调用执行器的查询方法// wrapCollection(parameter)是用来装饰集合或者数组参数return executor.query(ms, wrapCollection(parameter), rowBounds, handler);} catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}}
}
  1. CachingExecutor 执行查询时,会先生成缓存键 CacheKey,然后从 MappedStatement 中获取二级缓存,如果 MappedStatement 对应的 sql 语句配置了 flushCache=true,那么就会在执行前刷新缓存。这里的 case 并没有配置,所以继续执行。尝试从二级缓存中获取数据
public class CachingExecutor implements Executor {private final TransactionalCacheManager tcm = new TransactionalCacheManager();...//第一步@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {// 获取绑定的 SQL 语句,比如 "SELECT * FROM user WHERE id = ? "BoundSql boundSql = ms.getBoundSql(parameterObject);// 1. 生成缓存KeyCacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {// 2. 获取二级缓存,需要在配置文件中配置 <cache></cache> 标签//   <select id="findUserById" parameterType="int" resultType="com.itheima.pojo.User" flushCache="true">Cache cache = ms.getCache();// SynchronizeCache 是包装类,实际调用是 PerpetualCacheif (cache != null) {// 3. 刷新(每次查询前清空)二级缓存 (存在缓存且 flushCache 为true时)flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) { // 默认就是 true    <select id="findUserById" parameterType="int" resultType="com.itheima.pojo.User" useCache="true">ensureNoOutParams(ms, boundSql);// 处理输出参数@SuppressWarnings("unchecked")// 4. 从二级缓存中查询数据List<E> list = (List<E>) tcm.getObject(cache, key);...return list;}}// 委托给 BaseExecutor 执行return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
}
  1. 这里实际是由事务缓存管理器 TransactionalCacheManager 来获取缓存数据,它只有一个关键属性 transactionalCaches,其中 key 是二级缓存,而 TransactionalCache 是对应的一个临时事务缓存,一开始时 TransactionalCache 还没创建,所以会先创建,然后从 TransactionalCache 获取数据。后面才明白 TransactionalCache 的作用
/*** @author Clinton Begin* 事务缓存管理器*/
public class TransactionalCacheManager {// Cache(原生的 PertualCache) 与 TransactionalCache(队员生的包装对象) 的映射关系表private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();...public Object getObject(Cache cache, CacheKey key) {// 1. 直接从 TransactionalCache 中获取缓存return getTransactionalCache(cache).getObject(key);}private TransactionalCache getTransactionalCache(Cache cache) {// 从映射表中获取 TransactionalCache,没有则新建return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);}
}
  1. 实际 TransactionalCache 包装了二级缓存,所以在执行 getObject() 方法时,会先从二级缓存中拿到数据,如果拿不到,则存入一个 entriesMissedInCache 的 Set 集合中,返回返回一个空对象
public class TransactionalCache implements Cache {/*** 委托的 Cache 对象。** 实际上,就是二级缓存 Cache 对象。*/private final Cache delegate;/***   在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中*/private final Set<Object> entriesMissedInCache;@Overridepublic Object getObject(Object key) {// issue #116// 1. 查询的时候是直接从 delegate 中去查询的,也就是从真正的缓存对象中查询Object object = delegate.getObject(key);// 2. 如果不存在,则添加到 entriesMissedInCache 中if (object == null) {// 3. 缓存未命中,则将 key 存入到 entriesMissedInCache 中entriesMissedInCache.add(key);}// issue #146// 如果 clearOnCommit 为 true ,表示处于持续清空状态,则返回 nullif (clearOnCommit) {return null;} else {// 4. 返回 valuereturn object;}}
}
  1. 第一次执行 selectOne() 肯定是没有缓存结果的,所以 CachingExecutor 还是会委派给 SimpleExecutor 执行查询
public class CachingExecutor implements Executor {private final Executor delegate;...@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {// 获取二级缓存,需要在配置文件中配置 <cache></cache> 标签//   <select id="findUserById" parameterType="int" resultType="com.itheima.pojo.User" flushCache="true">Cache cache = ms.getCache();// SynchronizeCache 是包装类,实际调用是 PerpetualCacheif (cache != null) {...if (list == null) {// 1. 委托给 BaseExecutor 执行list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);...}return list;}}// 委托给 BaseExecutor 执行return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
}
  1. SimpleExecutor 执行查询则首先通过一级缓存获取数据,如果没有数据,就从数据查询,查询后将数据存入一级缓存,然后返回数据。从这里得知第一个问题答案,明显是二级缓存最优先
public abstract class BaseExecutor implements Executor {protected PerpetualCache localCache;...@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.");}// 如果配置了flushCacheRequired为true,则会在执行器执行之前就清空本地一级缓存if (queryStack == 0 && ms.isFlushCacheRequired()) {// 清空缓存clearLocalCache();}List<E> list;try {// 查询堆栈 + 1queryStack++;// 1. 从一级缓存中获取数据list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {// 已有缓存结果,则处理本地缓存结果输出参数(只有存储过程会走)handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 2. 没有缓存结果,则从数据库查询结果list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {// 查询堆栈数 -1queryStack--;}...return list;}private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list; // key:1463130193:-1799038108:com.itheima.mapper.UserMapper.findByCondition:0:2147483647:SELECT id, name FROM  user WHERE id = ?:1:development// 首先向本地缓存中存入一个 ExecutionPlaceholder 的枚举类占位 valuelocalCache.putObject(key, EXECUTION_PLACEHOLDER);try {// 3. 执行doQuery方法list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {// 执行完成移除这个keylocalCache.removeObject(key);}// 4. 查询结果存入缓存中localCache.putObject(key, list);// 如果 MappedStatement 的类型为 CALLABLE,则向 localOutputParameterCache 缓存中存入 value 为 parameter 的缓存if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}return list;}
}
  1. 当获取到结果得时候,就要存到二级缓存中,这时候并不是真的存,下面会继续解释
public class CachingExecutor implements Executor {private final Executor delegate;...@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {// 获取二级缓存,需要在配置文件中配置 <cache></cache> 标签//   <select id="findUserById" parameterType="int" resultType="com.itheima.pojo.User" flushCache="true">Cache cache = ms.getCache();// SynchronizeCache 是包装类,实际调用是 PerpetualCacheif (cache != null) {...if (list == null) {// 1. 委托给 BaseExecutor 执行list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// 2. 将查询结果,要存到二级缓存中(注意:此处只是存到map集合中,没有真正存到二级缓存中)tcm.putObject(cache, key, list); }return list;}}// 委托给 BaseExecutor 执行return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
}
  1. TransactionalCacheManager 通过 cache 获取对应的 TransactionalCache,如果没有则新建,然后将结果存入缓存
/*** @author Clinton Begin* 事务缓存管理器*/
public class TransactionalCacheManager {// Cache(原生的 PertualCache) 与 TransactionalCache(队员生的包装对象) 的映射关系表private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();...public void putObject(Cache cache, CacheKey key, Object value) {// 1. 直接存入 TransactionalCache 的缓存中,其中 key 为 1463130193:-1799038108:com.itheima.mapper.UserMapper.findByCondition:0:2147483647:SELECT id, name FROM  user WHERE id = ?:1:developmentgetTransactionalCache(cache).putObject(key, value);}private TransactionalCache getTransactionalCache(Cache cache) {// 从映射表中获取 TransactionalCachereturn MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);}
}
  1. 实际 TransactionalCache 存入时,只会存入到 entriesToAddOnCommit 这个 Map 缓存中,并非真实的缓存对象 delegate
public class TransactionalCache implements Cache {// 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中private final Map<Object, Object> entriesToAddOnCommit;...@Overridepublic void putObject(Object key, Object object) {// 1. 将键值对存入到 entriesToAddOnCommit 这个Map中中,而非真实的缓存对象 delegate 中entriesToAddOnCommit.put(key, object);}
}
  1. 查询到数据后,必须执行 commit() 操作,二级缓存才会真正生效,下面来看看 commit() 方法是什么时候把数据放入二级缓存
public class CacheTest {/*** 测试二级缓存*/@Testpublic void secondLevelCacheTest() throws IOException {... // 1. 必须要调用sqlSession的commit方法或者close方法,才能让二级缓存生效sqlSession1.commit();...}
}
  1. commit() 操作的提交,也是交由 CachingExecutor 来执行
public class DefaultSqlSession implements SqlSession {private final Executor executor;...@Overridepublic void commit() {commit(false);}@Overridepublic void commit(boolean force) {try {// 1. 执行 commit() 方法executor.commit(isCommitOrRollbackRequired(force));dirty = false;} catch (Exception e) {throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}}
}
  1. 除了 CachingExecutor 委派 SimpleExecutor 执行真正的 commit(),与二级缓存相关最值得关注的是 TransactionalCacheManagercommit() 操作
public class CachingExecutor implements Executor {private final Executor delegate;private final TransactionalCacheManager tcm = new TransactionalCacheManager();...@Overridepublic void commit(boolean required) throws SQLException {delegate.commit(required);// 1. 事务缓存管理器tcm.commit();}
}
  1. TransactionalCacheManager 执行 commit() 时,会遍历 transactionalCaches 也来执行 commit() 操作
/*** @author Clinton Begin* 事务缓存管理器*/
public class TransactionalCacheManager {// Cache(原生的 PertualCache) 与 TransactionalCache(队员生的包装对象) 的映射关系表private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();...public void commit() {for (TransactionalCache txCache : transactionalCaches.values()) {// 1. 执行事务缓存提交txCache.commit();}}
}
  1. TransactionalCachecommit(),会将 entriesToAddOnCommitentriesMissedInCache 真正刷入二级缓存的 PertualCache 中。首先遍历 entriesToAddOnCommit 把在 Map 中输入插入真正的缓存后,再将 entriesMissedInCache 不在 entriesToAddOnCommit 中的数据也刷入二级缓存中,只不过对应 key 的值为 null
public class TransactionalCache implements Cache {/*** 委托的 Cache 对象。** 实际上,就是二级缓存 Cache 对象。*/private final Cache delegate;/*** 提交时,清空 {@link #delegate}** 初始时,该值为 false* 清理后{@link #clear()} 时,该值为 true ,表示持续处于清空状态*/private boolean clearOnCommit;// 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中private final Map<Object, Object> entriesToAddOnCommit;// 在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中private final Set<Object> entriesMissedInCache;...public void commit() {// 如果 clearOnCommit 为 true ,则清空 delegate 缓存if (clearOnCommit) {delegate.clear();}// 1. 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate(cache) 中flushPendingEntries();// 重置reset();}/*** 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中*/private void flushPendingEntries() {// 将 entriesToAddOnCommit 中的内容转存到 delegate 中for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {// 2. 在这里真正的将 entriesToAddOnCommit 的对象逐个添加到 delegate 中,只有这时,二级缓存才真正的生效delegate.putObject(entry.getKey(), entry.getValue());}// 3. 将 entriesMissedInCache 刷入 delegate 中for (Object entry : entriesMissedInCache) {if (!entriesToAddOnCommit.containsKey(entry)) {delegate.putObject(entry, null);}}}
}
  1. 完成 commit() 后,执行第二次 selectOne() 时,因为数据已经在二级缓存中,所以就能查找到数据,这就解答了第 2 个问题,为什么要在 commit() 时拿到数据,因为第一次存的时候只是放在个临时 Map 中,没真正存到二级缓存
  2. 总结
  • 需要 commit() 才会放入二级缓存
    在这里插入图片描述
  • 流程总结
    在这里插入图片描述

更新方法不会清空二级缓存

  1. 问题:
  • update() 方法为什么不会清空二级缓存?
  1. 首先在测试 case 第一次 selectOne()commit() 后面增加一个 update() 操作
public class CacheTest {/*** 测试二级缓存*/@Testpublic void secondLevelCacheTest() throws IOException {...// 发起第一次查询,查询ID为1的用户User user1 = sqlSession1.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);// 必须要调用sqlSession的commit方法或者close方法,才能让二级缓存生效sqlSession1.commit();// 更新操作,清空了 2 级缓存SqlSession sqlSession3 = sqlSessionFactory.openSession();User user = new User();user.setId(1L);user.setName("tom");// 1. 增加更新操作sqlSession3.update("com.itheima.mapper.UserMapper.updateUser",user);sqlSession3.commit(); // 只有事务提交,才会清除二级缓存...}
}
  1. 执行 update() 时,SqlSession 创建 MappedStatement 后,实际还是交由 CachingExecutor 来执行
public class DefaultSqlSession implements SqlSession {private final Configuration configuration;private final Executor executor;...@Overridepublic int update(String statement, Object parameter) {try {dirty = true;MappedStatement ms = configuration.getMappedStatement(statement);// 1. 执行 update() 操作return executor.update(ms, wrapCollection(parameter));} catch (Exception e) {throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}}
}
  1. CachingExecutorupdate() 时,进行二级缓存的清空,也就是委派 TransactionalCacheManager 进行 clear() 操作
public class CachingExecutor implements Executor {private final Executor delegate;private final TransactionalCacheManager tcm = new TransactionalCacheManager();...@Overridepublic int update(MappedStatement ms, Object parameterObject) throws SQLException {// 1. 执行二级缓存的清空flushCacheIfRequired(ms);return delegate.update(ms, parameterObject);}private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();if (cache != null && ms.isFlushCacheRequired()) {// 2. 二级缓存的清空tcm.clear(cache);}}
}
  1. TransactionalCacheManager 根据原生 Cache,拿到 TransactionalCache 执行 clear() 操作
/*** @author Clinton Begin* 事务缓存管理器*/
public class TransactionalCacheManager {// Cache(原生的 PertualCache) 与 TransactionalCache(队员生的包装对象) 的映射关系表private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();...public void clear(Cache cache) {// 1. 获取 TransactionalCache 对象,并调用该对象的 clear 方法,下同getTransactionalCache(cache).clear();}private TransactionalCache getTransactionalCache(Cache cache) {// 从映射表中获取 TransactionalCachereturn MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);}
}
  1. update() 方法的二级缓存清理实际只是把属性 clearOnCommit 标记为 true,同时把 entriesToAddOnCommit 这个 Map 清空了,但是由于第一次 selectOne() 的时候,已经 commit() 了,所以落到了真正的二级缓存中,只清空了个临时事务缓存
public class TransactionalCache implements Cache {/*** 提交时,清空 {@link #delegate}** 初始时,该值为 false* 清理后{@link #clear()} 时,该值为 true ,表示持续处于清空状态*/private boolean clearOnCommit;// 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中private final Map<Object, Object> entriesToAddOnCommit;...@Overridepublic void clear() {// 1. 标记 clearOnCommit 为 trueclearOnCommit = true;// 2. 清空 entriesToAddOnCommitentriesToAddOnCommit.clear();}
}
  1. update() 方法执行完成后,开始执行 commit() 方法,SqlSession 再次委派 CachingExecutor 执行 commit() 方法
public class DefaultSqlSession implements SqlSession {private final Configuration configuration;private final Executor executor;...@Overridepublic void commit() {commit(false);}@Overridepublic void commit(boolean force) {try {// 1. 执行 commit() 方法executor.commit(isCommitOrRollbackRequired(force));dirty = false;} catch (Exception e) {throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}}
}
  1. CachingExecutor 委派 SimpleExecutor 执行完 commit() 方法后,再调用 TransactionalCacheManager 执行 commit() 方法
public class CachingExecutor implements Executor {private final Executor delegate;private final TransactionalCacheManager tcm = new TransactionalCacheManager();...@Overridepublic void commit(boolean required) throws SQLException {delegate.commit(required);// 1. 和二级缓存相关tcm.commit();}
}
  1. TransactionalCacheManager 遍历 transactionalCaches,执行 commit() 方法
/*** @author Clinton Begin* 事务缓存管理器*/
public class TransactionalCacheManager {// Cache(原生的 PertualCache) 与 TransactionalCache(对原生的包装对象) 的映射关系表private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();...public void commit() {for (TransactionalCache txCache : transactionalCaches.values()) {// 1. TransactionalCache 包装对象执行 commit() 方法txCache.commit();}}
}
  1. 由于 update() 已经把 clearOnCommit 属性标记为 true,这时候,就真的把对应的二级缓存都清空了
public class TransactionalCache implements Cache {/*** 委托的 Cache 对象。** 实际上,就是二级缓存 Cache 对象。*/private final Cache delegate;...public void commit() {// 如果 clearOnCommit 为 true ,则清空 delegate 缓存if (clearOnCommit) {// 1. 真正清理二级缓存delegate.clear();}// 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate(cache) 中flushPendingEntries();// 重置reset();}
}
  1. 总结
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/5550.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【嵌入式Qt开发入门】在Ubuntu下编写C++

在 Ubuntu 上面编写 C&#xff0c;本文内容主要介绍在 Ubuntu 在终端窗口下使用 vi/vim 编辑一 个 C源文件。通过编写最简单的示例“Hello,World&#xff01;”。带领大家学习如何在 Ubuntu 终端下编辑和编译 C。这里要求大家会在 Ubuntu 上使用 vi/vim&#xff0c;也就是要求大…

迪赛智慧数——柱状图(象形动态图):高考填报专业考虑的因素

效果图 填报志愿是高考后的一大重要环节&#xff0c;你的职业生涯就在这里起航了。那么&#xff0c;应该怎么填报志愿呢&#xff1f;高考填报专业考虑的因素很多&#xff0c;过半的人会考虑专业就业前景及薪资&#xff0c;其次是个人兴趣和是否为双一流建设学科。 数据源&…

Andriod 开发 SearchView默认弹出软键盘

SearchView默认弹出软键盘&#xff0c;遮挡了主界面 这很明显是SearchView是默认自动获取了焦点&#xff0c;所以上网搜了一下如何清除焦点&#xff1a; SearchView searchView getActivity().findViewById(R.id.searchViewSearchbar); searchView.clearFocus(); 然而没用&…

Android 安卓开发语言kotlin与Java该如何选择

一、介绍 如今在Android开发中&#xff0c;应用层开发语言主要是Java和Kotlin&#xff0c;Kotlin是后来加入的&#xff0c;主导的语言还是Java。kotlin的加入仿佛让会kotlin语言的开发者更屌一些&#xff0c;其实不然。 有人说kotlin的引入是解决开发者复杂的逻辑&#xff0c;并…

【xss漏洞-svg标签】详解svg标签+触发XSS

目录 一、理论知识 SVG标签的使用 二、实战部分 一、理论知识 SVG标签的使用 代码中的SVG标签和onload事件本身并不依赖于其他特定的标签来触发弹窗。无论它们被放置在哪个标签内&#xff0c;只要浏览器解析并加载了这个SVG标签&#xff0c;onload事件就会被触发。 注&am…

GO web开发

go web开发 简介 go官方提供了http服务&#xff0c;但它的功能很简单。 这里介绍web开发中的一些问题&#xff0c;和web框架&#xff08;echo&#xff09;怎么解决这些问题 &#xff0c;对于具体的echo的使用&#xff0c;可看官网 官网&#xff1a; https://echo.labstack…

Redis 高可用 RDB AOF

---------------------- Redis 高可用 ---------------------------------------- 在web服务器中&#xff0c;高可用是指服务器可以正常访问的时间&#xff0c;衡量的标准是在多长时间内可以提供正常服务&#xff08;99.9%、99.99%、99.999%等等&#xff09;。 但是在Redis语境…

MATLAB App Designer基础教程 Matlab GUI入门(一)

MATLAB GUI入门 第一天 学习传送门&#xff1a; 【MATLAB App Designer基础教程Matlab GUI界面设计&#xff08;全集更新完毕-用户界面设计appdesigner&#xff08;中文&#xff09;Matlab Gui教程】 https://www.bilibili.com/video/BV16f4y147x9/?p2&share_sourcecopy_…

区块链运行原理

文章目录 前言区块链的结构区块链的交易过程区块链的共识机制区块链交易存在的问题特性总结 前言 上文《认识区块链》中可以知道区块链是一个通过各种加密算法、共识机制以及其他技术可以实现一个点对点的电子现金系统&#xff0c;从而达到去第三方的效果&#xff08;通常称之…

基于深度学习的高精度老虎检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度老虎检测识别系统可用于日常生活中或野外来检测与定位老虎目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的老虎目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5目标检测模型…

zabbix6.0LTS 配置proxy分布式监控

一、环境介绍 角色IP备注zabbix_server192.168.1.17zabbix_proxy192.168.1.14rpm包方式安装zabbix_agent192.168.1.18源码包安装 二、部署zabbix_proxy数据库 zabbix_proxy必须要安装一个数据库.zabbix官网推荐使用mariadb数据库&#xff0c;本人尝试过使用mysql8.0。由于内…

ASP.NET Core MVC -- 入门

先决条件&#xff08;开发配置二选一&#xff09;&#xff1a; 带有 ASP.NET 和 Web 开发工作负载的Visual Studio Visual Studio Code Visual Studio Code用于 Visual Studio Code 的 C#&#xff08;最新版本&#xff09;.NET 7.0 SDK 创建Web应用 visual studio ctrl F5 …