上一篇[MyBatis框架原理2:SqlSession运行过程][1]介绍了MyBatis的工作流程,其中涉及到了MyBatis缓存的使用,首先回顾一下工作流程图:

如果开启了二级缓存,数据查询执行过程就是首先从二级缓存中查询,如果未命中则从一级缓存中查询,如果也未命中则从数据库中查询。MyBatis的一级和二级缓存都是基于Cache接口的实现,下面先来看看Cache接口和其各种实现类。

Cache接口及常用装饰器

  1. public interface Cache {
  2. String getId();
  3. //缓存中添加数据,key为生成的CacheKey,value为查询结果
  4. void putObject(Object key, Object value);
  5. //查询
  6. Object getObject(Object key);
  7. //删除
  8. Object removeObject(Object key);
  9. //清空缓存
  10. void clear();
  11. //获取缓存数量
  12. int getSize();
  13. //获取读写锁
  14. ReadWriteLock getReadWriteLock();
  15. }

Cache接口位于MyBatis的cache包下,定义了缓存的基本方法,其实现类采用了装饰器模式,通过实现类的组装,可以实现操控缓存的功能。cache包结构如下:

  • PerpetualCache是Cache接口的实现类,通过内部的HashMap来对缓存进行基本的操作,通常配合装饰器类一起使用。
  • BlockingCache装饰器:保证只有一个线程到数据库中查询指定key的数据,如果该线程在BlockingCache中未查找到数据,就获取key对应的锁,阻塞其他查询这个key的线程,通过其内部ConcurrentHashMap来实现,源码如下:
  1. public class BlockingCache implements Cache {
  2. //阻塞时长
  3. private long timeout;
  4. private final Cache delegate;
  5. //key和ReentrantLock对象一一对应
  6. private final ConcurrentHashMap<Object, ReentrantLock> locks;
  7. @Override
  8. public Object getObject(Object key) {
  9. //获取key的锁
  10. acquireLock(key);
  11. //根据key查询
  12. Object value = delegate.getObject(key);
  13. //如果命中缓存,释放锁,未命中则继续持有锁
  14. if (value != null) {
  15. releaseLock(key);
  16. }
  17. return value;
  18. }
  19. @Override
  20. //从数据库获取结果后,将结果放入BlockingCache,然后释放锁
  21. public void putObject(Object key, Object value) {
  22. try {
  23. delegate.putObject(key, value);
  24. } finally {
  25. releaseLock(key);
  26. }
  27. }
  28. ...
  • FifoCache装饰器: 先入先出规则删除最早的缓存,通过其内部的Deque实现。
  • LruCache装饰器: 删除最近使用最少的缓存, 通过内部的LinkedHashMap实现。
  • SynchronizedCache装饰器:同步Cache。
  • LoggingCache装饰器: 提供日志功能,记录和输出缓存命中率。
  • SerializedCache装饰器:序列化功能。

CacheKey

CacheKey对象是用来确认缓存项的唯一标识,由其内部ArrayList添加的所有对象来确认两个CacheKey是否相同,通常ArrayList内将添加MappedStatement的id,SQL语句,用户传递给SQL语句的参数以及查询结果集范围RowBounds等,CacheKey源码如下:

  1. public class CacheKey implements Cloneable, Serializable {
  2. ...
  3. private final int multiplier;
  4. private int hashcode;
  5. private long checksum;
  6. private int count;
  7. private List<Object> updateList;
  8. public CacheKey() {
  9. this.hashcode = DEFAULT_HASHCODE;
  10. this.multiplier = DEFAULT_MULTIPLYER;
  11. this.count = 0;
  12. this.updateList = new ArrayList<Object>();
  13. }
  14. //向updateLis中添加对象
  15. public void update(Object object) {
  16. int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
  17. count++;
  18. checksum += baseHashCode;
  19. baseHashCode *= count;
  20. hashcode = multiplier * hashcode + baseHashCode;
  21. updateList.add(object);
  22. }
  23. @Override
  24. //重写equals方法判断CacheKey是否相同
  25. public boolean equals(Object object) {
  26. if (this == object) {
  27. return true;
  28. }
  29. if (!(object instanceof CacheKey)) {
  30. return false;
  31. }
  32. final CacheKey cacheKey = (CacheKey) object;
  33. if (hashcode != cacheKey.hashcode) {
  34. return false;
  35. }
  36. if (checksum != cacheKey.checksum) {
  37. return false;
  38. }
  39. if (count != cacheKey.count) {
  40. return false;
  41. }
  42. //比较updateList中每一项
  43. for (int i = 0; i < updateList.size(); i++) {
  44. Object thisObject = updateList.get(i);
  45. Object thatObject = cacheKey.updateList.get(i);
  46. if (!ArrayUtil.equals(thisObject, thatObject)) {
  47. return false;
  48. }
  49. }
  50. return true;
  51. }
  52. }

一级缓存

一级缓存是session级别缓存,只存在当前会话中,在没有任何配置下,MyBatis默认开启一级缓存,当一个SqlSession第一次执行SQL语句和参数查询时,将生成的CacheKey和查询结果放入缓存中,下一次通过相同的SQL语句和参数查询时,就会从缓存中获取,当进行更新或者插入操作时,一级缓存会进行清空。在上一篇中说到,MayBatis进行一级缓存查询和写入是由BaseExecutor执行的,源码如下:

  • 初始化缓存:

    一级缓存是Cache接口的PerpetualCache实现类对象
  1. public abstract class BaseExecutor implements Executor {
  2. ...
  3. protected PerpetualCache localCache;
  4. protected PerpetualCache localOutputParameterCache;
  5. protected Configuration configuration;
  6. protected int queryStack;
  7. private boolean closed;
  8. protected BaseExecutor(Configuration configuration, Transaction transaction) {
  9. this.transaction = transaction;
  10. this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
  11. //一级缓存初始化
  12. this.localCache = new PerpetualCache("LocalCache");
  13. this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
  14. this.closed = false;
  15. this.configuration = configuration;
  16. this.wrapper = this;
  17. }
  18. ...
  • 生成CacheKey

    BaseExecutor生成CacheKey,CacheKey的updateList中放入了MappedStatement,传入SQL的参数,结果集范围rowBounds和boundSql:
  1. @Override
  2. public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  3. BoundSql boundSql = ms.getBoundSql(parameter);
  4. CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  5. return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  6. }
  • 将查询结果和CacheKey放入缓存:
  1. private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  2. List<E> list;
  3. //缓存中放入CacheKey和占位符
  4. localCache.putObject(key, EXECUTION_PLACEHOLDER);
  5. try {
  6. //在数据库中查询操作
  7. list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  8. } finally {
  9. localCache.removeObject(key);
  10. }
  11. //缓存中放入CacheKey和结果集
  12. localCache.putObject(key, list);
  13. if (ms.getStatementType() == StatementType.CALLABLE) {
  14. localOutputParameterCache.putObject(key, parameter);
  15. }
  16. //返回结果
  17. return list;
  18. }
  • 再次执行相同查询条件时从缓存获取结果:
  1. public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  2. ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  3. if (closed) {
  4. throw new ExecutorException("Executor was closed.");
  5. }
  6. if (queryStack == 0 && ms.isFlushCacheRequired()) {
  7. clearLocalCache();
  8. }
  9. List<E> list;
  10. try {
  11. queryStack++;
  12. //从缓存获取结果
  13. list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
  14. if (list != null) {
  15. handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
  16. } else {
  17. //未命中缓存,则从数据库查询
  18. list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  19. }
  20. } finally {
  21. queryStack--;
  22. }
  23. if (queryStack == 0) {
  24. for (DeferredLoad deferredLoad : deferredLoads) {
  25. deferredLoad.load();
  26. }
  27. // issue #601
  28. deferredLoads.clear();
  29. if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
  30. // issue #482
  31. clearLocalCache();
  32. }
  33. }
  34. return list;
  35. }
  • 更新操作时清空缓存:
  1. public int update(MappedStatement ms, Object parameter) throws SQLException {
  2. ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
  3. if (closed) {
  4. throw new ExecutorException("Executor was closed.");
  5. }
  6. //清空缓存
  7. clearLocalCache();
  8. return doUpdate(ms, parameter);
  9. }

通过以下代码验证下,分别开两个session进行相同的查询,第一个session查询两次:

  1. public void testSelect() {
  2. SqlSession sqlSession = sqlSessionFactory.openSession();
  3. User user = sqlSession.selectOne("findUserById", 1);
  4. System.out.println(user);
  5. User user2 = sqlSession.selectOne("findUserById", 1);
  6. System.out.println(user2);
  7. sqlSession.close();
  8. System.out.println("sqlSession closed!===================================");
  9. //新建会话
  10. SqlSession sqlSession2 = sqlSessionFactory.openSession();
  11. User user3 = sqlSession2.selectOne("findUserById", 1);
  12. System.out.println(user3);
  13. sqlSession2.close();
  14. }

把日志设置为DEBUG级别得到运行日志:

  1. DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
  2. DEBUG [main] - ==> Parameters: 1(Integer)
  3. DEBUG [main] - <== Total: 1
  4. User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
  5. User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
  6. DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
  7. DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
  8. DEBUG [main] - Returned connection 369241501 to pool.
  9. sqlSession closed!===================================
  10. DEBUG [main] - Opening JDBC Connection
  11. DEBUG [main] - Checked out connection 369241501 from pool.
  12. DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
  13. DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
  14. DEBUG [main] - ==> Parameters: 1(Integer)
  15. DEBUG [main] - <== Total: 1
  16. User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
  17. DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
  18. DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
  19. DEBUG [main] - Returned connection 369241501 to pool.

第一次会话中,虽然查询了两次id为1的用户,但是只执行了一次SQL,关闭会话后开启一次新的会话,再次查询id为1的用户,SQL再次执行,说明了一级缓存只存在SqlSession中,不同SqlSession不能共享。

二级缓存

二级缓存是Mapper级别缓存,也就是同一Mapper下不同的session共享二级缓存区域。

只需要在XML映射文件中增加cache标签或cache-ref标签标签就可以开启二级缓存,cache-ref标签配置的是共享其指定Mapper的二级缓存区域。具体配置信息如下:

  • blocking : 是否使用阻塞缓存
  • readOnly : 是否只读
  • eviction: 缓存策略,可指定Cache接口下装饰器类FifoCache、LruCache、SoftCache和WeakCache
  • flushInterval : 自动刷新缓存时间
  • size : 设置缓存个数
  • type : 设置缓存类型,用于自定义缓存类,默认为PerpetualCache

二级缓存是在MyBatis的解析配置文件时初始化,在XMLMapperBuilder中将缓存配置解析:

  1. private void cacheElement(XNode context) throws Exception {
  2. if (context != null) {
  3. //指定默认类型为PerpetualCache
  4. String type = context.getStringAttribute("type", "PERPETUAL");
  5. Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
  6. //默认缓存策略为LruCache
  7. String eviction = context.getStringAttribute("eviction", "LRU");
  8. Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
  9. Long flushInterval = context.getLongAttribute("flushInterval");
  10. Integer size = context.getIntAttribute("size");
  11. boolean readWrite = !context.getBooleanAttribute("readOnly", false);
  12. boolean blocking = context.getBooleanAttribute("blocking", false);
  13. Properties props = context.getChildrenAsProperties();
  14. //委托builderAssistant构建二级缓存
  15. builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  16. }
  17. }

构建过程:

  1. public Cache useNewCache(Class<? extends Cache> typeClass,
  2. Class<? extends Cache> evictionClass,
  3. Long flushInterval,
  4. Integer size,
  5. boolean readWrite,
  6. boolean blocking,
  7. Properties props) {
  8. Cache cache = new CacheBuilder(currentNamespace)
  9. //设置缓存类型,默认为PerpetualCache
  10. .implementation(valueOrDefault(typeClass, PerpetualCache.class))
  11. //设置缓存策略,默认使用LruCache装饰器
  12. .addDecorator(valueOrDefault(evictionClass, LruCache.class))
  13. //设置刷新时间
  14. .clearInterval(flushInterval)
  15. //设置大小
  16. .size(size)
  17. //设置是否只读
  18. .readWrite(readWrite)
  19. .blocking(blocking)
  20. .properties(props)
  21. .build();
  22. configuration.addCache(cache);
  23. currentCache = cache;
  24. return cache;
  25. }

最终得到默认的二级缓存对象结构为:

CachingExecutor将初始化的Cache对象用TransactionalCache包装后放入TransactionalCacheManager的Map中,下面代码中的tcm就是TransactionalCacheManager对象,CachingExecutor执行二级缓存操作过程:

  1. public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
  2. throws SQLException {
  3. //从Configuration的MappedStatement中获取二级缓存
  4. Cache cache = ms.getCache();
  5. if (cache != null) {
  6. //判断是否需要刷新缓存,SELECT不刷新,INSERT|UPDATE|DELETE刷新缓存
  7. flushCacheIfRequired(ms);
  8. if (ms.isUseCache() && resultHandler == null) {
  9. ensureNoOutParams(ms, boundSql);
  10. @SuppressWarnings("unchecked")
  11. //从二级缓存中获取数据
  12. List<E> list = (List<E>) tcm.getObject(cache, key);
  13. if (list == null) {
  14. //委托BaseExecutor查询
  15. list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  16. //查询结果放入二级缓存
  17. tcm.putObject(cache, key, list); // issue #578 and #116
  18. }
  19. return list;
  20. }
  21. }
  22. return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  23. }

通过之前一级缓存的例子验证二级缓存,只需要在UserMapper映射文件中加入cache标签,并且让相关POJO类实现java.io.Serializable接口,运行得到日志:

  1. DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
  2. DEBUG [main] - ==> Parameters: 1(Integer)
  3. DEBUG [main] - <== Total: 1
  4. User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
  5. DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.0
  6. User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
  7. DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5c072e3f]
  8. DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@5c072e3f]
  9. DEBUG [main] - Returned connection 1543974463 to pool.
  10. sqlSession closed!===================================
  11. DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.3333333333333333
  12. User [id=1, username=小明, birthday=null, sex=1, address=四川成都]

不同session查询同一条记录时,总共只执行了一次SQL语句,并且日志打印出了缓存的命中率,这时候不同session已经共享了二级缓存区域。

[1]: https://www.cnblogs.com/abcboy/p/9656302.html

MyBatis框架原理3:缓存的更多相关文章

  1. MyBatis框架原理4:插件

    插件的定义和作用 首先引用MyBatis文档对插件(plugins)的定义: MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用.默认情况下,MyBatis 允许使用插件来拦截的方法调用 ...

  2. MyBatis框架原理2:SqlSession运行过程

    获取SqlSession对象 SqlSession session = sqlSessionFactory.openSession(); 首先通过SqlSessionFactory的openSessi ...

  3. [原创]关于mybatis中一级缓存和二级缓存的简单介绍

    关于mybatis中一级缓存和二级缓存的简单介绍 mybatis的一级缓存: MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候 ...

  4. MyBatis学习--查询缓存

    简介 以前在使用Hibernate的时候知道其有一级缓存和二级缓存,限制ORM框架的发展都是互相吸收其他框架的优点,在Hibernate中也有一级缓存和二级缓存,用于减轻数据压力,提高数据库性能. m ...

  5. Mybatis的二级缓存配置

    一个项目中肯定会存在很多共用的查询数据,对于这一部分的数据,没必要每一个用户访问时都去查询数据库,因此配置二级缓存将是非常必要的.  Mybatis的二级缓存配置相当容易,要开启二级缓存,只需要在你的 ...

  6. 使用Redis做MyBatis的二级缓存

    使用Redis做MyBatis的二级缓存 通常为了减轻数据库的压力,我们会引入缓存.在Dao查询数据库之前,先去缓存中找是否有要找的数据,如果有则用缓存中的数据即可,就不用查询数据库了. 如果没有才去 ...

  7. mybatis0210 mybatis和ehcache缓存框架整合

    .1mybatis和ehcache缓存框架整合 一般不用mybatis来管理缓存而是用其他缓存框架在管理缓存,因为其他缓存框架管理缓存会更加高效,因为别人专业做缓存的而mybatis专业做sql语句的 ...

  8. MyBatis延迟加载和缓存

    一.延迟加载 1.主对象的加载: 根本没有延迟的概念,都是直接加载. 2.关联对象的加载时机: 01.直接加载: 访问主对象,关联对象也要加载 02.侵入式延迟: 访问主对象,并不加载关联对象 访问主 ...

  9. SSM-MyBatis-17:Mybatis中一级缓存(主要是一级缓存存在性的证明,增删改对一级缓存会造成什么影响)

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 缓存------------------------------------------> 很熟悉的一个 ...

随机推荐

  1. C语言/C++知识

    <C与指针>pdf 下载: 新浪微盘: https://vdisk.weibo.com/s/A6gkKkHrGH0g

  2. BZOJ1079 [SCOI2008]着色方案[组合计数DP]

    $有a_{1}个1,a_{2}个2,...,a_{n}个n(n<=15,a_{n}<=5),求排成一列相邻位不相同的方案数.$ 关于这题的教训记录: 学会对于复杂的影响分开计,善于发现整体 ...

  3. :last-child的坑-CSS3选择器

    CSS3选择器之:last-child - Eric 真实经历 最近开发项目的时候发现了一个这么多年忽略的问题,和大家分享一下.项目使用的是Antd组件库,有一个搜索框是这样的: 为了保证下拉框的内容 ...

  4. JAVA笔记6-继承和权限控制

    1. (1)类的成员的权限修饰符有public,protected,private或default,限定其他对象对该类对象成员的访问权限. (2)class的权限修饰符只可以是public或defau ...

  5. JVM启动参数大全及默认值

    Java启动参数共分为三类: 其一是标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容: 其二是非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足, ...

  6. Java——静态代理、动态代理

    https://blog.csdn.net/giserstone/article/details/17199755 代理的作用:业务类只需要关注业务逻辑本身,保证了业务类的重用性 一 静态代理 特点: ...

  7. CPU风扇转速异常

    本文适用于Ubuntu 16.04,造冰箱的大熊猫@cnblogs 2018/10/9 近日发现一个问题,新笔记本的CPU风扇转速很高.笔记本刚刚启动,就能听到风扇呼呼的声音,转速高的异常.以前不是这 ...

  8. PX4学习之-uORB msg 自动生成模板解读

    最后更新日期 2019-06-22 一.前言 在 PX4学习之-uORB简单体验 中指出, 使用 uORB 进行通信的第一步是新建 msg.在实际编译过程中,新建的 msg 会转换成对应的 .h..c ...

  9. Java程序,JVM之间的关系

    java程序是跑在JVM上的,严格来讲,是跑在JVM实例上的.一个JVM实例其实就是JVM跑起来的进程,二者合起来称之为一个JAVA进程.各个JVM实例之间是相互隔离的. 每个java程序都运行于某个 ...

  10. JSP之Bean

    <jsp:useBean id=" " class" "/>创建JavaBean对象,并把创建的对象保存到域对象 比如:<jsp:useBea ...