Mybatis源码-缓存机制
积累知识,胜过积蓄金银!毕竟在##column_title##开发的过程中,会遇到各种各样的问题,往往都是一些细节知识点还没有掌握好而导致的,因此基础知识点的积累是很重要的。下面本文《Mybatis源码-缓存机制》,就带大家讲解一下MySQL、缓存、jdbc、mybatis、缓存设计知识点,若是你对本文感兴趣,或者是想搞懂其中某个知识点,就请你继续往下看吧~
正文
一. 一级缓存机制展示
在
<settings><setting name="localCacheScope" value="SESSION"></setting></settings>
其中localCacheScope可以配置为SESSION(默认)或者STATEMENT,含义如下所示。
属性值 | 含义 |
---|---|
SESSION | 一级缓存在一个会话中生效。即在一个会话中的所有查询语句,均会共享同一份一级缓存,不同会话中的一级缓存不共享。 |
STATEMENT | 一级缓存仅针对当前执行的<settings><setting name="logImpl" value="STDOUT_LOGGING"></setting><setting name="cacheEnabled" value="false"></setting><setting name="localCacheScope" value="SESSION"></setting></settings> 映射接口如下所示。 public interface BookMapper { Book selectBookById(int id); } 映射文件如下所示。 <?xml version="1.0" encoding="UTF-8" ?><mapper namespace="com.mybatis.learn.dao.BookMapper"><resultmap id="bookResultMap" type="com.mybatis.learn.entity.Book"><result column="b_name" property="bookName"></result><result column="b_price" property="bookPrice"></result></resultmap><select id="selectBookById" resultmap="bookResultMap"> SELECT b.id, b.b_name, b.b_price FROM book b WHERE b.id=#{id} </select></mapper> public class MybatisTest { public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream(resource)); SqlSession sqlSession = sqlSessionFactory.openSession(false); BookMapper bookMapper = sqlSession.getMapper(BookMapper.class); System.out.println(bookMapper.selectBookById(1)); System.out.println(bookMapper.selectBookById(1)); System.out.println(bookMapper.selectBookById(1)); } } 在执行代码中,连续执行了三次查询操作,看一下日志打印,如下所示。 ![]() 可以知道,只有第一次查询时和数据库进行了交互,后面两次查询均是从一级缓存中查询的数据。现在往映射接口和映射文件中加入更改数据的逻辑,如下所示。 public interface BookMapper { Book selectBookById(int id); // 根据id更改图书价格 void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice); } <?xml version="1.0" encoding="UTF-8" ?><mapper namespace="com.mybatis.learn.dao.BookMapper"><resultmap id="bookResultMap" type="com.mybatis.learn.entity.Book"><result column="b_name" property="bookName"></result><result column="b_price" property="bookPrice"></result></resultmap><select id="selectBookById" resultmap="bookResultMap"> SELECT b.id, b.b_name, b.b_price FROM book b WHERE b.id=#{id} </select><insert id="updateBookPriceById"> UPDATE book SET b_price=#{bookPrice} WHERE id=#{id} </insert></mapper> 执行的操作为先执行一次查询操作,然后执行一次更新操作并提交事务,最后再执行一次查询操作,执行代码如下所示。 public class MybatisTest { public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream(resource)); SqlSession sqlSession = sqlSessionFactory.openSession(false); BookMapper bookMapper = sqlSession.getMapper(BookMapper.class); System.out.println(bookMapper.selectBookById(1)); System.out.println("Change database."); bookMapper.updateBookPriceById(1, 22.5f); sqlSession.commit(); System.out.println(bookMapper.selectBookById(1)); } } 执行结果如下所示。 ![]() 通过上述结果可以知道,在执行更新操作之后,再执行查询操作时,是直接从数据库查询的数据,并未使用一级缓存,即在一个会话中,对数据库的增,删,改操作,均会使一级缓存失效。 现在在执行代码中创建两个会话,先让会话1执行一次查询操作,然后让会话2执行一次更新操作并提交事务,最后让会话1再执行一次相同的查询。执行代码如下所示。 public class MybatisTest { public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream(resource)); SqlSession sqlSession1 = sqlSessionFactory.openSession(false); SqlSession sqlSession2 = sqlSessionFactory.openSession(false); BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class); BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class); System.out.println(bookMapper1.selectBookById(1)); System.out.println("Change database."); bookMapper2.updateBookPriceById(1, 22.5f); sqlSession2.commit(); System.out.println(bookMapper1.selectBookById(1)); } } 执行结果如下所示。 ![]() 上述结果表明,会话1的第一次查询是直接查询的数据库,然后会话2执行了一次更新操作并提交了事务,此时数据库中id为1的图书的价格已经变更为了22.5,紧接着会话1又做了一次查询,但查询结果中的图书价格为20.5,说明会话1的第二次查询是从缓存获取的查询结果。所以在这里可以知道, @Override public <e> List<e> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 获取Sql语句 BoundSql boundSql = ms.getBoundSql(parameter); // 生成CacheKey CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 调用重载的query()方法 return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }</e></e> 在上述 public CacheKey() { this.hashcode = DEFAULT_HASHCODE; this.multiplier = DEFAULT_MULTIPLIER; this.count = 0; this.updateList = new ArrayList(); } 同时hashcode,checksum,count和updateList字段会在 public void update(Object object) { int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); count++; checksum += baseHashCode; baseHashCode *= count; hashcode = multiplier * hashcode + baseHashCode; updateList.add(object); } 主要逻辑就是基于 public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { ...... // 创建CacheKey CacheKey cacheKey = new CacheKey(); // 基于MappedStatement的id更新CacheKey cacheKey.update(ms.getId()); // 基于RowBounds的offset更新CacheKey cacheKey.update(rowBounds.getOffset()); // 基于RowBounds的limit更新CacheKey cacheKey.update(rowBounds.getLimit()); // 基于Sql语句更新CacheKey cacheKey.update(boundSql.getSql()); ...... // 基于查询参数更新CacheKey cacheKey.update(value); ...... // 基于Environment的id更新CacheKey cacheKey.update(configuration.getEnvironment().getId()); return cacheKey; } 所以可以得出结论,判断 @Override 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."); } // queryStack是BaseExecutor的成员变量 // queryStack主要用于递归调用query()方法时防止一级缓存被清空 if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<e> list; try { queryStack++; // 先从一级缓存中根据CacheKey命中查询结果 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 (BaseExecutor.DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } deferredLoads.clear(); // 如果一级缓存作用范围是STATEMENT时,每次query()执行完毕就需要清空一级缓存 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { clearLocalCache(); } } return list; }</e></e></e></e> 上述 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 { // 调用doQuery()进行查询操作 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; }</e></e></e> @Override 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); } 所以 <settings><setting name="logImpl" value="STDOUT_LOGGING"></setting><setting name="cacheEnabled" value="true"></setting><setting name="localCacheScope" value="STATEMENT"></setting></settings> 上述配置文件中还将一级缓存的作用范围设置为了STATEMENT,目的是为了在例子中屏蔽一级缓存对查询结果的干扰。映射接口如下所示。 public interface BookMapper { Book selectBookById(int id); void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice); } 要使用二级缓存,还需要在映射文件中加入二级缓存相关的设置,如下所示。 <?xml version="1.0" encoding="UTF-8" ?><mapper namespace="com.mybatis.learn.dao.BookMapper"><!-- 二级缓存相关设置 --><cache eviction="LRU" type="org.apache.ibatis.cache.impl.PerpetualCache" flushinterval="600000" size="1024" readonly blocking="false"></cache><resultmap id="bookResultMap" type="com.mybatis.learn.entity.Book"><result column="b_name" property="bookName"></result><result column="b_price" property="bookPrice"></result></resultmap><select id="selectBookById" resultmap="bookResultMap"> SELECT b.id, b.b_name, b.b_price FROM book b WHERE b.id=#{id} </select><insert id="updateBookPriceById"> UPDATE book SET b_price=#{bookPrice} WHERE id=#{id} </insert></mapper> 二级缓存相关设置的每一项的含义,会在本小节末尾进行说明。 场景一:创建两个会话,会话1以相同 public class MybatisTest { public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream(resource)); SqlSession sqlSession1 = sqlSessionFactory.openSession(false); SqlSession sqlSession2 = sqlSessionFactory.openSession(false); BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class); BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class); System.out.println(bookMapper1.selectBookById(1)); System.out.println(bookMapper1.selectBookById(1)); System.out.println(bookMapper2.selectBookById(1)); } } 执行结果如下所示。 ![]() public class MybatisTest { public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream(resource)); SqlSession sqlSession1 = sqlSessionFactory.openSession(false); SqlSession sqlSession2 = sqlSessionFactory.openSession(false); BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class); BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class); System.out.println(bookMapper1.selectBookById(1)); sqlSession1.commit(); System.out.println(bookMapper1.selectBookById(1)); System.out.println(bookMapper2.selectBookById(1)); } } 执行结果如下所示。 ![]() 场景二中第一次查询后提交了事务,此时将查询结果缓存到了二级缓存,所以后续的查询全部在二级缓存中命中了查询结果。 场景三:创建两个会话,会话1执行一次查询并提交事务,然后会话2执行一次更新并提交事务,接着会话1再执行一次相同的查询。执行代码如下所示。 public class MybatisTest { public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream(resource)); // 将事务隔离级别设置为读已提交 SqlSession sqlSession1 = sqlSessionFactory.openSession( TransactionIsolationLevel.READ_COMMITTED); SqlSession sqlSession2 = sqlSessionFactory.openSession( TransactionIsolationLevel.READ_COMMITTED); BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class); BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class); System.out.println(bookMapper1.selectBookById(1)); sqlSession1.commit(); System.out.println("Change database."); bookMapper2.updateBookPriceById(1, 20.5f); sqlSession2.commit(); System.out.println(bookMapper1.selectBookById(1)); } } 执行结果如下所示。 ![]() 场景三的执行结果表明,执行更新操作并且提交事务后,会清空二级缓存,执行新增和删除操作也是同理。 场景四:创建两个会话,创建两张表,会话1首先执行一次多表查询并提交事务,然后会话2执行一次更新操作以更新表2的数据并提交事务,接着会话1再执行一次相同的多表查询。创表语句如下所示。 CREATE TABLE book( id INT(11) PRIMARY KEY AUTO_INCREMENT, b_name VARCHAR(255) NOT NULL, b_price FLOAT NOT NULL, bs_id INT(11) NOT NULL, FOREIGN KEY book(bs_id) REFERENCES bookstore(id) ); CREATE TABLE bookstore( id INT(11) PRIMARY KEY AUTO_INCREMENT, bs_name VARCHAR(255) NOT NULL ) 往book表和bookstore表中添加如下数据。 INSERT INTO book (b_name, b_price, bs_id) VALUES ("Math", 20.5, 1); INSERT INTO book (b_name, b_price, bs_id) VALUES ("English", 21.5, 1); INSERT INTO book (b_name, b_price, bs_id) VALUES ("Water Margin", 30.5, 2); INSERT INTO bookstore (bs_name) VALUES ("XinHua"); INSERT INTO bookstore (bs_name) VALUES ("SanYou") 创建 @Data public class BookStore { private String id; private String bookStoreName; } 创建 @Data public class BookDetail { private long id; private String bookName; private float bookPrice; private BookStore bookStore; } public interface BookMapper { Book selectBookById(int id); void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice); BookDetail selectBookDetailById(int id); } <?xml version="1.0" encoding="UTF-8" ?><mapper namespace="com.mybatis.learn.dao.BookMapper"><cache eviction="LRU" type="org.apache.ibatis.cache.impl.PerpetualCache" flushinterval="600000" size="1024" readonly blocking="false"></cache><resultmap id="bookResultMap" type="com.mybatis.learn.entity.Book"><result column="b_name" property="bookName"></result><result column="b_price" property="bookPrice"></result></resultmap><resultmap id="bookDetailResultMap" type="com.mybatis.learn.entity.BookDetail"><id column="id" property="id"></id><result column="b_name" property="bookName"></result><result column="b_price" property="bookPrice"></result><association property="bookStore"><id column="id" property="id"></id><result column="bs_name" property="bookStoreName"></result></association></resultmap><select id="selectBookById" resultmap="bookResultMap"> SELECT b.id, b.b_name, b.b_price FROM book b WHERE b.id=#{id} </select><insert id="updateBookPriceById"> UPDATE book SET b_price=#{bookPrice} WHERE id=#{id} </insert><select id="selectBookDetailById" resultmap="bookDetailResultMap"> SELECT b.id, b.b_name, b.b_price, bs.id, bs.bs_name FROM book b, bookstore bs WHERE b.id=#{id} AND b.bs_id = bs.id </select></mapper> 还需要添加 public interface BookStoreMapper { void updateBookPriceById(@Param("id") int id, @Param("bookStoreName") String bookStoreName); } 还需要添加 <?xml version="1.0" encoding="UTF-8" ?><mapper namespace="com.mybatis.learn.dao.BookStoreMapper"><cache eviction="LRU" type="org.apache.ibatis.cache.impl.PerpetualCache" flushinterval="600000" size="1024" readonly blocking="false"></cache><insert id="updateBookPriceById"> UPDATE bookstore SET bs_name=#{bookStoreName} WHERE id=#{id} </insert></mapper> 进行完上述更改之后,进行场景四的测试,执行代码如下所示。 public class MybatisTest { public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream(resource)); // 将事务隔离级别设置为读已提交 SqlSession sqlSession1 = sqlSessionFactory.openSession( TransactionIsolationLevel.READ_COMMITTED); SqlSession sqlSession2 = sqlSessionFactory.openSession( TransactionIsolationLevel.READ_COMMITTED); BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class); BookStoreMapper bookStoreMapper = sqlSession2.getMapper(BookStoreMapper.class); System.out.println(bookMapper1.selectBookDetailById(1)); sqlSession1.commit(); System.out.println("Change database."); bookStoreMapper.updateBookStoreById(1, "ShuXiang"); sqlSession2.commit(); System.out.println(bookMapper1.selectBookDetailById(1)); } } 执行结果如下所示。 ![]() 会话1第一次执行多表查询并提交事务时,将查询结果缓存到了二级缓存中,然后会话2对bookstore表执行了更新操作并提交了事务,但是最后会话1第二次执行相同的多表查询时,却从二级缓存中命中了查询结果,最终导致查询出来了脏数据。实际上,二级缓存的作用范围是同一命名空间下的多个会话共享,这里的命名空间就是映射文件的namespace,可以理解为每一个映射文件持有一份二级缓存,所有会话在这个映射文件中的所有操作,都会共享这个二级缓存。所以场景四的例子中,会话2对bookstore表执行更新操作并提交事务时,清空的是 <?xml version="1.0" encoding="UTF-8" ?><mapper namespace="com.mybatis.learn.dao.BookStoreMapper"><cache-ref namespace="com.mybatis.learn.dao.BookMapper"></cache-ref><insert id="updateBookStoreById"> UPDATE bookstore SET bs_name=#{bookStoreName} WHERE id=#{id} </insert></mapper> 执行代码如下所示。 public class MybatisTest { public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream(resource)); // 将事务隔离级别设置为读已提交 SqlSession sqlSession1 = sqlSessionFactory.openSession( TransactionIsolationLevel.READ_COMMITTED); SqlSession sqlSession2 = sqlSessionFactory.openSession( TransactionIsolationLevel.READ_COMMITTED); BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class); BookStoreMapper bookStoreMapper = sqlSession2.getMapper(BookStoreMapper.class); System.out.println(bookMapper1.selectBookDetailById(1)); sqlSession1.commit(); System.out.println("Change database."); bookStoreMapper.updateBookStoreById(1, "ShuXiang"); sqlSession2.commit(); System.out.println(bookMapper1.selectBookDetailById(1)); } } 执行结果如下所示。 ![]() 在 private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.isEmpty()) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); // 解析<cache-ref>标签 cacheRefElement(context.evalNode("cache-ref")); // 解析<cache>标签 cacheElement(context.evalNode("cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); 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); } }</cache></cache-ref> 在 private void cacheElement(XNode context) { if (context != null) { // 获取<cache>标签的type属性值 String type = context.getStringAttribute("type", "PERPETUAL"); Class extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); // 获取<cache>标签的eviction属性值 String eviction = context.getStringAttribute("eviction", "LRU"); Class extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); // 获取<cache>标签的flushInterval属性值 Long flushInterval = context.getLongAttribute("flushInterval"); // 获取<cache>标签的size属性值 Integer size = context.getIntAttribute("size"); // 获取<cache>标签的readOnly属性值并取反 boolean readWrite = !context.getBooleanAttribute("readOnly", false); // 获取<cache>标签的blocking属性值 boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } }</cache></cache></cache></cache></cache></cache> 单步跟踪 public Cache useNewCache(Class extends Cache> typeClass, Class extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { 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(); configuration.addCache(cache); currentCache = cache; return cache; } 在 public CacheBuilder(String id) { this.id = id; this.decorators = new ArrayList(); } 所以可以知道, public Cache build() { setDefaultImplementations(); // 创建PerpetualCache,作为基础Cache对象 Cache cache = newBaseCacheInstance(implementation, id); setCacheProperties(cache); if (PerpetualCache.class.equals(cache.getClass())) { // 为基础Cache对象添加缓存淘汰策略相关的装饰器 for (Class extends Cache> decorator : decorators) { cache = newCacheDecoratorInstance(decorator, cache); setCacheProperties(cache); } // 继续添加装饰器 cache = setStandardDecorators(cache); } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { cache = new LoggingCache(cache); } return cache; } <cache eviction="LRU" type="org.apache.ibatis.cache.impl.PerpetualCache" flushinterval="600000" size="1024" readonly blocking="true"></cache> 那么生成的二级缓存对象如下所示。 ![]() 整个装饰链如下图所示。 ![]() 现在回到 public void addCache(Cache cache) { caches.put(cache.getId(), cache); } 这里就印证了前面的猜想,即二级缓存 private void cacheRefElement(XNode context) { if (context != null) { // 在Configuration的cacheRefMap中将当前映射文件命名空间与引用的映射文件命名空间建立映射关系 configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace")); CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace")); try { // CacheRefResolver会将引用的映射文件的二级缓存从Configuration中获取出来并赋值给MapperBuilderAssistant的currentCache cacheRefResolver.resolveCacheRef(); } catch (IncompleteElementException e) { configuration.addIncompleteCacheRef(cacheRefResolver); } } } @Override public <e> List<e> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 获取Sql语句 BoundSql boundSql = ms.getBoundSql(parameterObject); // 创建CacheKey CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }</e></e> 继续看重载的 @Override public <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); // 从二级缓存中根据CacheKey命中查询结果 List<e> list = (List<e>) tcm.getObject(cache, key); if (list == null) { // 未命中缓存,则查数据库 list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 将从数据库查询到的结果缓存到二级缓存中 tcm.putObject(cache, key, list); } // 返回查询结果 return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }</e></e></e></e> 上述 public Object getObject(Cache cache, CacheKey key) { return getTransactionalCache(cache).getObject(key); } private TransactionalCache getTransactionalCache(Cache cache) { return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new); } 通过上述代码可以知道,一个二级缓存对应一个 @Override public Object getObject(Object key) { // 在二级缓存中命中查询结果 Object object = delegate.getObject(key); if (object == null) { // 未命中则将CacheKey添加到entriesMissedInCache中 // 用于统计命中率 entriesMissedInCache.add(key); } if (clearOnCommit) { return null; } else { return object; } } 到这里就可以知道了,在 private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); if (cache != null && ms.isFlushCacheRequired()) { tcm.clear(cache); } } 调用 @Override public void clear() { clearOnCommit = true; entriesToAddOnCommit.clear(); } 现在继续分析为什么将查询结果缓存到二级缓存中需要事务提交。从数据库中查询出来结果后, public void putObject(Cache cache, CacheKey key, Object value) { getTransactionalCache(cache).putObject(key, value); } 继续看 @Override public void putObject(Object key, Object object) { entriesToAddOnCommit.put(key, object); } 到这里就搞明白了,在事务提交之前,查询结果会被暂存到 @Override public void commit() { commit(false); } @Override public void commit(boolean force) { try { executor.commit(isCommitOrRollbackRequired(force)); dirty = false; } catch (Exception e) { throw ExceptionFactory.wrapException( "Error committing transaction. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } 在 @Override public void commit(boolean required) throws SQLException { delegate.commit(required); // 调用TransactionalCacheManager的commit()方法 tcm.commit(); } 在 public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { // 调用TransactionalCache的commit()方法 txCache.commit(); } } 继续看 public void commit() { if (clearOnCommit) { delegate.clear(); } flushPendingEntries(); reset(); } private void flushPendingEntries() { // 将entriesToAddOnCommit中暂存的查询结果全部缓存到二级缓存中 for (Map.Entry<object object> entry : entriesToAddOnCommit.entrySet()) { delegate.putObject(entry.getKey(), entry.getValue()); } for (Object entry : entriesMissedInCache) { if (!entriesToAddOnCommit.containsKey(entry)) { delegate.putObject(entry, null); } } }</object> 至此可以知道,当调用 boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); 即如果没有在CURD标签中显式的设置flushCache属性,则会给flushCache字段一个默认值,且默认值为非查询标签下默认为true,所以到这里就可以知道,如果是增,删,改操作,那么 TransactionalCache中的clearOnCommit字段会被置为true,从而在提交事务时会在 TransactionalCache的 commit()方法中将二级缓存清空。 到这里,二级缓存的源码分析结束。二级缓存的使用流程可以用下图进行概括,如下所示。 ![]() 总结关于 Mybatis的一级缓存,总结如下。
关于 Mybatis的二级缓存,总结如下。
好了,本文到此结束,带大家了解了《Mybatis源码-缓存机制》,希望本文对你有所帮助!关注golang学习网公众号,给大家分享更多数据库知识! |

- 上一篇
- Apache Doris ODBC Mysql外表在Ubuntu下使用方法及配置

- 下一篇
- 完蛋,我的事务怎么不生效?
-
- 高大的御姐
- 这篇技术贴太及时了,好细啊,真优秀,码住,关注作者大大了!希望作者大大能多写数据库相关的文章。
- 2023-04-17 11:54:29
-
- 壮观的战斗机
- 这篇技术文章真是及时雨啊,太详细了,很棒,码住,关注老哥了!希望老哥能多写数据库相关的文章。
- 2023-01-28 14:17:26
-
- 数据库 · MySQL | 5小时前 |
- MySQL无法启动?8个排查方法全解析
- 174浏览 收藏
-
- 数据库 · MySQL | 7小时前 |
- 主键与唯一键区别,如何选主键?
- 166浏览 收藏
-
- 数据库 · MySQL | 9小时前 |
- MySQL多表连接优化技巧与实战策略
- 221浏览 收藏
-
- 数据库 · MySQL | 21小时前 |
- MySQL排序优化与性能提升技巧
- 153浏览 收藏
-
- 数据库 · MySQL | 23小时前 |
- MySQL中WHERE与HAVING的区别详解
- 340浏览 收藏
-
- 数据库 · MySQL | 1天前 |
- MySQL排序优化与性能提升技巧
- 368浏览 收藏
-
- 数据库 · MySQL | 1天前 |
- MySQL连接池配置与优化方法
- 297浏览 收藏
-
- 数据库 · MySQL | 1天前 |
- MySQLGROUPBY使用技巧与常见问题
- 306浏览 收藏
-
- 数据库 · MySQL | 1天前 |
- MySQL缓存优化技巧分享
- 392浏览 收藏
-
- 数据库 · MySQL | 1天前 |
- MySQL安装到D盘教程及路径设置详解
- 279浏览 收藏
-
- 数据库 · MySQL | 2天前 |
- MySQL缓存设置及查询作用解析
- 470浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 511次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 498次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- 千音漫语
- 千音漫语,北京熠声科技倾力打造的智能声音创作助手,提供AI配音、音视频翻译、语音识别、声音克隆等强大功能,助力有声书制作、视频创作、教育培训等领域,官网:https://qianyin123.com
- 96次使用
-
- MiniWork
- MiniWork是一款智能高效的AI工具平台,专为提升工作与学习效率而设计。整合文本处理、图像生成、营销策划及运营管理等多元AI工具,提供精准智能解决方案,让复杂工作简单高效。
- 89次使用
-
- NoCode
- NoCode (nocode.cn)是领先的无代码开发平台,通过拖放、AI对话等简单操作,助您快速创建各类应用、网站与管理系统。无需编程知识,轻松实现个人生活、商业经营、企业管理多场景需求,大幅降低开发门槛,高效低成本。
- 107次使用
-
- 达医智影
- 达医智影,阿里巴巴达摩院医疗AI创新力作。全球率先利用平扫CT实现“一扫多筛”,仅一次CT扫描即可高效识别多种癌症、急症及慢病,为疾病早期发现提供智能、精准的AI影像早筛解决方案。
- 98次使用
-
- 智慧芽Eureka
- 智慧芽Eureka,专为技术创新打造的AI Agent平台。深度理解专利、研发、生物医药、材料、科创等复杂场景,通过专家级AI Agent精准执行任务,智能化工作流解放70%生产力,让您专注核心创新。
- 98次使用
-
- golang MySQL实现对数据库表存储获取操作示例
- 2022-12-22 499浏览
-
- 搞一个自娱自乐的博客(二) 架构搭建
- 2023-02-16 244浏览
-
- B-Tree、B+Tree以及B-link Tree
- 2023-01-19 235浏览
-
- mysql面试题
- 2023-01-17 157浏览
-
- MySQL数据表简单查询
- 2023-01-10 101浏览