百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

MyBatis一二级缓存原理(mybatis二级缓存和一级缓存)

bigegpt 2024-08-03 11:48 11 浏览

1、MyBatis缓存的使用

MyBatis拥有一级缓存和二级缓存,MyBatis默认开启一级缓存(无法关闭),二级缓存默认也是开启,但需要在每个Mapper.xml既每个MappedStatement类添加Cache标签,二级缓存才会有效。在Configuration中将cacheEnabled设置为false,将关闭二级缓存。

MyBatis二级缓存的使用:

  • 在MyBatis主配置文件中指定cacheEnabled属性值为true
在MyBatis-config.xml中配置
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>
  • 在MyBatis Mapper配置文件中,配置缓存策略、缓存刷新频率、缓存的容量等属性:
<cache eviction="FIFO"
        flushInterval="6000"
        size="512"
        readOnly="true"/>
  • 在配置Mapper时,通过useCache属性指定Mapper执行时是否使用缓存。另外,还可以通过配置flushCache属性指定Mapper执行后是否刷新缓存,针对这条语句开启二级缓存 例如:
<select id="listAllUser"
        flushCache="false"
        useCache="true"
        resultType="com.blog4java.mybatis.example.entity.UserEntity">
    select 
    <include refid="userAllFied"/>
    from user
</select>


2、MyBatis 缓存实现类

MyBatis的缓存是基于JVM堆内存实现的,既所有缓存数据都存放在Java对象中。MyBatis通过Cache接口定义缓存对象的行为,Cache接口代码如下:

public interface Cache {
    //  用于缓存缓存Key,一般Key是由Mapper的命名空间名称
    String getId();
    //  Key为CacheKey实例,value为需要缓存的对象
    void putOject(Object key Object value);
    //  根据CacheKey获取缓存的对象
    Object getObject(Object key);
    //  根据CacheKey移除数据
    Object removeObject(Object key);
    //  清空缓存
    void clear();
    //  获取大小
    int getSize();
    //  读写锁,该方法在3.2.6版本后已经不在使用
    ReadWriteLock getReadWriteLock();
    
}

MyBatis采用装饰器模式设计缓存类,Cache接口有一个基本的实现类,既PerpetualCache类,该类内部只有一个类型为String的id属性和一个类型为HashMap的cache属性。此类重写了equals()方法,当两个缓存对象的id相同时,既认为两个缓存对象相同,另外,PerpetualCache还重写了hashCod()方法,仅缓存对象的Id作为因子生成hashCode。

实现Cache接口的实现类有如下几个:

  • PrepetualCache: 基本实现类,类中仅有一个类型为String的id属性和一个类型为HashMap的cache属。
  • 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类似,只是使用不同的引用类型。


3、MyBatis 一级缓存实现原理

MyBatis的一级缓存是存放在BaseExecutor中,由于BaseExecutor是由SqlSession创建的,所以一级缓存是属于Session级别的。

public abstract class BaseExecutor implements Executor {
...
  //    一级缓存
  protected PerpetualCache localCache;
  //    缓存存储过程的结果
  protected PerpetualCache localOutputParameterCache;
  //    MyBatis的配置信息
  protected Configuration configuration;

...

查询操作(query):

   
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //  获取绑定的Sql语句对象
    BoundSql boundSql = ms.getBoundSql(parameter);
    //  根据与这条SQL语句相关联的值(MappedStatement,参数parameter,返回值访问rowBounds,SQL语句boundSql),获取对应的CacheKey,保证CacheKey是此条SQL语句的唯一标识。
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }
  
  
  
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());
    //  如果此Executor已关闭,将抛出异常
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    //  如果flushCacheRequired为true,表示每次请求都刷新缓存。更新,新增,删除语句默认都是true,而查询语句默认是false
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      //    清空一级缓存
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      //    如果ResultHandler为null,将从一级缓存中获取数据。
      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 (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }


//  从数据库中查找数据,并将数据写入到缓存中
  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 {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    //  将数据库中查出来的结果存储到一级缓存中
    localCache.putObject(key, list);
    
    //  如果该mappedStatement是CALLABLE类型的
    if (ms.getStatementType() == StatementType.CALLABLE) {
    //  将数据结果存储到存放存储过程的cache中
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

更新操作

  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);
  }


4、MyBatis 二级缓存

MyBatis二级缓存存在于CachingExecutor对象中,在该对象中具有一个tcm属性,是TransactionalCacheManager类型,里面封装了Map<Cache, TransactionalCache>对象。Cache对应与一个Namespace,既一个Mapper,其中TransactionalCache对应一个MappedStatement(既:一个Mapper中的一个方法)

二级缓存是Namespace级别的

二级缓存在Configuration中默认是开启的,但还需要在每个Mapper中添加<cache></cache>标签。同时满足两个都开启才算开启二级缓存。
缓存只针对于查询操作,修改操作会清空所有缓存。

从源码的角度分析

 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    //  构建缓存Key,通过此CacheKey能够获取二级缓存中的数据
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //  如果没有添加<cache>标签,在加载MappedStatement时,MappedStatement对象中的cache将会null,
    Cache cache = ms.getCache();
    if (cache != null) {
    //  是否每次执行清空此namespace中的缓存
      flushCacheIfRequired(ms);
      //    如果是开启缓存,并且ResulHandler为null
      if (ms.isUseCache() && resultHandler == null) {
        //  执行存储过程相关的方法
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        //  从缓存中获取数据
        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); // issue #578 and #116
        }
        return list;
      }
    }
    
    //  如果没有开启二级缓存将执行此代码
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

注意:

在分布式环境下,务必将MyBatis的localCacheScope属性设置为STATEMENT,避免其他节
点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。


小结

  • 每执行一条Sql语句对应一个MappedStatement对象,只有当执行同一条SQL语句,并且条件参数,返回结果条数范围相同时,才会读取到之前缓存的结果。
  • 每条更新操作(新增,修改,删除)的SQL语句都会清空一级缓存。
  • 每条查询语句对应的MappedStatement对象的flushCacheRequired属性默认都是false
  • 在Spring中每执行一次sql语句就会开启一个SqlSession,操作执行完后,就会关闭SqlSession。 在一般的开发中,sql语句返回的结果我们无法在下次执行的SQL语句的时候获取到此缓存的值,因为之前的SqlSession已关闭。但存在一个情况会使用到SqlSession,就是在某个方法中添加了@Transactional注解(事务注解),在里面同时执行两次sql语句,第二次会从一级缓存中获取数据。因为添加@Transactional事务的方法里,只有所有sql语句执行成功,才会commit,等commit之后才会关闭SqlSession。


文章对你如果有帮助,请点个赞,谢谢!

相关推荐

当Frida来“敲”门(frida是什么)

0x1渗透测试瓶颈目前,碰到越来越多的大客户都会将核心资产业务集中在统一的APP上,或者对自己比较重要的APP,如自己的主业务,办公APP进行加壳,流量加密,投入了很多精力在移动端的防护上。而现在挖...

服务端性能测试实战3-性能测试脚本开发

前言在前面的两篇文章中,我们分别介绍了性能测试的理论知识以及性能测试计划制定,本篇文章将重点介绍性能测试脚本开发。脚本开发将分为两个阶段:阶段一:了解各个接口的入参、出参,使用Python代码模拟前端...

Springboot整合Apache Ftpserver拓展功能及业务讲解(三)

今日分享每天分享技术实战干货,技术在于积累和收藏,希望可以帮助到您,同时也希望获得您的支持和关注。架构开源地址:https://gitee.com/msxyspringboot整合Ftpserver参...

Linux和Windows下:Python Crypto模块安装方式区别

一、Linux环境下:fromCrypto.SignatureimportPKCS1_v1_5如果导包报错:ImportError:Nomodulenamed'Crypt...

Python 3 加密简介(python des加密解密)

Python3的标准库中是没多少用来解决加密的,不过却有用于处理哈希的库。在这里我们会对其进行一个简单的介绍,但重点会放在两个第三方的软件包:PyCrypto和cryptography上,我...

怎样从零开始编译一个魔兽世界开源服务端Windows

第二章:编译和安装我是艾西,上期我们讲述到编译一个魔兽世界开源服务端环境准备,那么今天跟大家聊聊怎么编译和安装我们直接进入正题(上一章没有看到的小伙伴可以点我主页查看)编译服务端:在D盘新建一个文件夹...

附1-Conda部署安装及基本使用(conda安装教程)

Windows环境安装安装介质下载下载地址:https://www.anaconda.com/products/individual安装Anaconda安装时,选择自定义安装,选择自定义安装路径:配置...

如何配置全世界最小的 MySQL 服务器

配置全世界最小的MySQL服务器——如何在一块IntelEdison为控制板上安装一个MySQL服务器。介绍在我最近的一篇博文中,物联网,消息以及MySQL,我展示了如果Partic...

如何使用Github Action来自动化编译PolarDB-PG数据库

随着PolarDB在国产数据库领域荣膺桂冠并持续获得广泛认可,越来越多的学生和技术爱好者开始关注并涉足这款由阿里巴巴集团倾力打造且性能卓越的关系型云原生数据库。有很多同学想要上手尝试,却卡在了编译数据...

面向NDK开发者的Android 7.0变更(ndk android.mk)

订阅Google官方微信公众号:谷歌开发者。与谷歌一起创造未来!受Android平台其他改进的影响,为了方便加载本机代码,AndroidM和N中的动态链接器对编写整洁且跨平台兼容的本机...

信创改造--人大金仓(Kingbase)数据库安装、备份恢复的问题纪要

问题一:在安装KingbaseES时,安装用户对于安装路径需有“读”、“写”、“执行”的权限。在Linux系统中,需要以非root用户执行安装程序,且该用户要有标准的home目录,您可...

OpenSSH 安全漏洞,修补操作一手掌握

1.漏洞概述近日,国家信息安全漏洞库(CNNVD)收到关于OpenSSH安全漏洞(CNNVD-202407-017、CVE-2024-6387)情况的报送。攻击者可以利用该漏洞在无需认证的情况下,通...

Linux:lsof命令详解(linux lsof命令详解)

介绍欢迎来到这篇博客。在这篇博客中,我们将学习Unix/Linux系统上的lsof命令行工具。命令行工具是您使用CLI(命令行界面)而不是GUI(图形用户界面)运行的程序或工具。lsoflsof代表&...

幻隐说固态第一期:固态硬盘接口类别

前排声明所有信息来源于网络收集,如有错误请评论区指出更正。废话不多说,目前固态硬盘接口按速度由慢到快分有这几类:SATA、mSATA、SATAExpress、PCI-E、m.2、u.2。下面我们来...

新品轰炸 影驰SSD多款产品登Computex

分享泡泡网SSD固态硬盘频道6月6日台北电脑展作为全球第二、亚洲最大的3C/IT产业链专业展,吸引了众多IT厂商和全球各地媒体的热烈关注,全球存储新势力—影驰,也积极参与其中,为广大玩家朋友带来了...