Mybatis源码-缓存机制
来源:SegmentFault
时间:2023-01-22 17:00:52 397浏览 收藏
积累知识,胜过积蓄金银!毕竟在##column_title##开发的过程中,会遇到各种各样的问题,往往都是一些细节知识点还没有掌握好而导致的,因此基础知识点的积累是很重要的。下面本文《Mybatis源码-缓存机制》,就带大家讲解一下MySQL、缓存、jdbc、mybatis、缓存设计知识点,若是你对本文感兴趣,或者是想搞懂其中某个知识点,就请你继续往下看吧~
正文
一. 一级缓存机制展示
在
其中localCacheScope可以配置为SESSION(默认)或者STATEMENT,含义如下所示。
属性值 | 含义 |
---|---|
SESSION | 一级缓存在一个会话中生效。即在一个会话中的所有查询语句,均会共享同一份一级缓存,不同会话中的一级缓存不共享。 |
STATEMENT | 一级缓存仅针对当前执行的 映射接口如下所示。 public interface BookMapper { Book selectBookById(int id); } 映射文件如下所示。 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); }
执行的操作为先执行一次查询操作,然后执行一次更新操作并提交事务,最后再执行一次查询操作,执行代码如下所示。 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 在上述 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 上述 private @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); } 所以 上述配置文件中还将一级缓存的作用范围设置为了STATEMENT,目的是为了在例子中屏蔽一级缓存对查询结果的干扰。映射接口如下所示。 public interface BookMapper { Book selectBookById(int id); void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice); } 要使用二级缓存,还需要在映射文件中加入二级缓存相关的设置,如下所示。
二级缓存相关设置的每一项的含义,会在本小节末尾进行说明。 场景一:创建两个会话,会话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); }
还需要添加 public interface BookStoreMapper { void updateBookPriceById(@Param("id") int id, @Param("bookStoreName") String bookStoreName); } 还需要添加
进行完上述更改之后,进行场景四的测试,执行代码如下所示。 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表执行更新操作并提交事务时,清空的是
执行代码如下所示。 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); // 解析 在 private void cacheElement(XNode context) { if (context != null) { // 获取 单步跟踪 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; } 那么生成的二级缓存对象如下所示。 整个装饰链如下图所示。 现在回到 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 继续看重载的 @Override public 上述 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 至此可以知道,当调用 boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); 即如果没有在CURD标签中显式的设置flushCache属性,则会给flushCache字段一个默认值,且默认值为非查询标签下默认为true,所以到这里就可以知道,如果是增,删,改操作,那么 TransactionalCache中的clearOnCommit字段会被置为true,从而在提交事务时会在 TransactionalCache的 commit()方法中将二级缓存清空。 到这里,二级缓存的源码分析结束。二级缓存的使用流程可以用下图进行概括,如下所示。 总结关于 Mybatis的一级缓存,总结如下。
关于 Mybatis的二级缓存,总结如下。
好了,本文到此结束,带大家了解了《Mybatis源码-缓存机制》,希望本文对你有所帮助!关注golang学习网公众号,给大家分享更多数据库知识! |
-
499 收藏
-
244 收藏
-
235 收藏
-
157 收藏
-
101 收藏
-
368 收藏
-
475 收藏
-
266 收藏
-
273 收藏
-
283 收藏
-
210 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习