GVKun编程网logo

mybatis缓存(了解)(mybatis 缓存)

12

在这篇文章中,我们将为您详细介绍mybatis缓存的内容,并且讨论关于了解的相关问题。此外,我们还会涉及一些关于MyBatis缓存(5)、mybatis缓存(三)、Mybatis一级缓存和二级缓存Re

在这篇文章中,我们将为您详细介绍mybatis缓存的内容,并且讨论关于了解的相关问题。此外,我们还会涉及一些关于MyBatis 缓存(5)、mybatis 缓存(三)、Mybatis一级缓存和二级缓存 Redis缓存、mybatis入门教程(九)------mybatis缓存的知识,以帮助您更全面地了解这个主题。

本文目录一览:

mybatis缓存(了解)(mybatis 缓存)

mybatis缓存(了解)(mybatis 缓存)

声明

本文为其他博主文章总结,仅用作个人学习,特此声明

参考文章链接

动态 SQL_MyBatis中文网

(3条消息) 狂神说 | Mybatis完整版笔记_小七rrrrr的博客-CSDN博客_狂神说mybatis笔记

缓存(了解)

查询:连接数据,耗资源! 一次查询的结果,给他暂存在一个可以取到的地方! ---> 内存 : 缓存 我们再次查询相同数据的时候,直接走缓存,就不用走数据库了。

1.什么是缓存[Cache]?

  • 存在内中的临时数据
  • 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题

2.为什么使用缓存?

  • 减少和数据库的交互次数,减少系统开销,提高系统效率

3.什么样的数据能使用缓存?

  • 经常查询并且不经常改变的数据【可以使用缓存】

Mybatis包含一个非常强大的查询缓存特性,它可以非常方便的定制和配置缓存。缓存可以极大的提升查询效率。

Mybatis系统默认定义了两级缓存:一级缓存和二级缓存

  • 默认情况下,只有一级缓存开启。(sqlSession级别的缓存,也称本科缓存)
  • 二级缓存需要手动开启和配置,它是基于namespace级别的缓存
  • 为了提高扩展性,Mybatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存

缓存原理如下图所示


1. 一级缓存

  1. 开启日志

  2. 测试一个session中查询两次相同记录。

    public class MyTest {
        @Test
        public void test(){
            sqlSession sqlSession = MybatisUtils.getsqlSession();
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            User user = mapper.queryUser(1);
            System.out.println(user);
            System.out.println("=============");
            User user1 = mapper.queryUser(1);
            System.out.println(user1);
            System.out.println(user == user1);
            sqlSession.close();
        }
    }
    
  3. 查看日志输出


以下情况缓存会失效:

1.查询不同的数据内容

public class MyTest {
    @Test
    public void test(){
        sqlSession sqlSession = MybatisUtils.getsqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.queryUser(1);
        System.out.println(user);
        System.out.println("=============");
        User user1 = mapper.queryUser(2);
        System.out.println(user1);
        System.out.println(user == user1);
        sqlSession.close();
    }
}


2.映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。

public class MyTest {
    @Test
    public void test(){
        sqlSession sqlSession = MybatisUtils.getsqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.queryUser(1);
        System.out.println(user);
        mapper.UpdateUser(new User(2,"asas","12345678"));
        System.out.println("=============");
        User user1 = mapper.queryUser(1);
        System.out.println(user1);
        System.out.println(user == user1);
        sqlSession.close();
    }
}


3.查询不同的mapper.xml

4.手动清除缓存

public class MyTest {
    @Test
    public void test(){
        sqlSession sqlSession = MybatisUtils.getsqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.queryUser(1);
        System.out.println(user);
        sqlSession.clearCache();
        System.out.println("=============");
        User user1 = mapper.queryUser(1);
        System.out.println(user1);
        System.out.println(user == user1);
        sqlSession.close();
    }
}

一级缓存默认开启,只在一次sqlseesion中有效,一级缓存就是一个map


2. 二级缓存

1、什么是二级缓存

  • 二级缓存也叫全局缓存,一级缓存作用于太低,所以诞生了二级缓存;
  • 基于namespace级别的缓存,一个名称空间,对应一个二级缓存;
  • 工作机制
    • 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
    • 如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中;
    • 新的会话查询信息,就可以从二级缓存中获取内容
    • 不同的mapper查出的数据会放在自己对应的缓存(map)中

2、测试二级缓存

1.开启全局缓存

<setting name="cacheEnabled" value="true"/>
cacheEnabled 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 true | false true

2.在要使用二级缓存的Mapper中开启

<cache eviction="FIFO"
       flushInterval="60000"
       size="512"
       readOnly="true"/>

3.测试

public class MyTest {
    @Test
    public void test(){
        sqlSession sqlSession = MybatisUtils.getsqlSession();
        sqlSession sqlSession1 = MybatisUtils.getsqlSession();

        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.queryUser(1);
        System.out.println(user);
        sqlSession.close();
  
        //关掉第一个sqlsession才会将一级缓存中的数据保存到二级缓存中
        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        User user1 = mapper1.queryUser(1);
        System.out.println(user1);
        System.out.println(user == user1);
        sqlSession1.close();
    }
}


3、小结

  1. 只要开启了二级缓存,只在同一个Mapper下有效
  2. 所有的数据都会先放在一级缓存中;只有当会话提交或者关闭的时候,才会提交到二级缓存中

需要注意的是:只用cache时要加序列化

<cache/>

实体类

import lombok.Data;
import java.io.Serializable;

@Data
public class User implements Serializable {
    private int id;
    private String name;
    private String pwd;

    public User(int id, String name, String pwd) {
        this.id = id;
        this.name = name;
        this.pwd = pwd;
    }
}

3. 自定义缓存-ehcache

Ehcache是一种广泛使用的开源Java分布式缓存,主要面向通用缓存

要在程序中使用ehcache:

1.导入依赖

<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.2.0</version>
</dependency>

2.ehcache的配置文件ehcache.xml

<?xml version="1.0" encoding="UTF8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
        updateCheck="false">
<!--
    diskStore :为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置,参数解释如下:
        user.hoeme - 用户主目录
        user.dir -  用户当前工作目录
        javaio.tmpdir - 默认临时文件
-->
    <diskStore path="./tmpdir/Tmp_EhCache/">

    <defaultCache
        eternal="false"
        maxElementsInMemory="10000"
        overflowTodisk="false"
        diskPersistent="false"
        timetoIdleSeconds="1800"
        timetoLiveSeconds="259200"
        memoryStoreevictionPolicy="LRU"/>

    <cache
        name="cloud_user"
        eternal="false"
        maxElementsInMemory="5000"
        overflowTodisk="false"
        diskPersistent="false"
        timetoIdleSeconds="1800"
        timetoLiveSeconds="1800"
        memoryStoreevictionPolicy="LRU"/>
        </ehcache>
    <!--
        defaultCache:默认缓存策略,当cache找不到定义的缓存时,则使用这个缓存策略,只能定义一个
    -->
    <!--
       name:缓存名称。
       maxElementsInMemory:缓存最大个数。
       eternal:对象是否永久有效,一但设置了,timeout将不起作用。
       timetoIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
       timetoLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
       overflowTodisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
       diskSpoolBufferSizeMB:这个参数设置diskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
       maxElementsOndisk:硬盘最大缓存个数。
       diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
       diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
       memoryStoreevictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
       clearOnFlush:内存数量最大时是否清除。
    -->

3.使用ehchache

<!--自定义缓存,用第三方缓存覆盖-->
<cache type = "org.mybatis.caches.ehcache.EhcacheCache"/>

MyBatis 缓存(5)

MyBatis 缓存(5)

MyBatis有必要使用缓存吗?为什么?

一般的ORM框架都会提供缓存功能来提升查询效率、减少数据库的压力。跟Hibernate一样,Mybatis也有一级缓存、二级缓存,并预留了集成第三方的缓存接口。

在Mybatis中,与缓存相关的类都在cache包中,其中有一个Cache接口,只有一个默认的实现类PerpetualCache,它是用HashMap实现的。

PerpetualCache这个对象是一定会创建的,所以是基础缓存。但是缓存又可以有很多额外的功能,比如回收策略、日志记录、定时刷新等等,如果需要的话,就可以给基础缓存加上这些功能。

除了基础缓存之外,MyBatis也定义了很多装饰器,同样实现了Cache接口,通过这些装饰器可以额外实现很多功能。

所有缓存可以分为三大类:基本缓存、淘汰算法缓存、装饰器缓存。

类型 缓存实现类 描述 作用 装饰条件
基本缓存 PerpetualCache 缓存基本实现类 默认是PeretualCache也可以自定义比如RedisCache、EhCache等,具备基本能功能的缓存
淘汰算法缓存 LruCache LRU淘汰策略的缓存 当缓存达到上限时,删除最少使用的缓存(Laste Recently Use) eviction="LRU"(默认)
FifoCache FIFO策略缓存 当缓存达到上限时,删除最先入队的缓存 evication="FIFO"
SoftCache \ WeakCache 带清理策略的缓存 通过JVM的软引用和弱引用来实现缓存,当JVM内存不足时,会自动清理这些缓存。 evication="SOFT" \ evication="WEAK"
装饰器缓存 LoggingCache 带日志功能的缓存 如可以输出缓存命中率 基本
SynchronnizedCache 同步缓存 基于synchronized关键字实现,解决并发问题 基本
BlockingCache 阻塞缓存 通过在get/put方式中加锁,保证只有一个线程操作缓存,基于java重入锁实现 bloking=true
SerializedCache 支持序列化的缓存 将对象序列化以后存到缓存中,取出时反序列化 readOnly=false(默认)
ScheduledCache 定时调度的缓存 在进行get 、put、remove、getSize等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认一小时),如果是则清空缓存——每隔一段时间清空一次缓存 flushInterval不为空
TransactionnalCache 事务缓存 在二级缓存中使用,可一次存入多个缓存,移除多个缓存 在TransactionalCacheManager中用Map维护对应关系

一级缓存

一级缓存也叫本地缓存(Local Cache),MyBatis的一级缓存是在绘画(SqlSession)层进行缓存的。MyBatis一缓存默认是开启的,不需要任何配置(localCacheScope=STATEMENT相当于关闭一级缓存)。

可以在BaseExecutor的query()方法中找到localCacheScope清除逻辑:

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    // issue #482
    clearLocalCache();
}

在MyBatis执行流程里面,涉及到这么多对象,那么缓存PerpetualCache应该放在哪个对象里面去呢?

如果要在同一个会话里共享一级缓存,最好的办法是在SqlSession里创建的,作为SqlSession的一个属性,跟SqlSession生命周期相同,这样就不需要为SqlSession编号、再根据编号查找对应缓存了。

DefaultSqlSession里只有两个对象属性:Configuration和Executor。

Configuration是全局的,不属于SqlSession,所以缓存只可能放在Executor里,因为Executor是一个接口,实际上他是在基本执行器中(SimpleExecutor\ReuseExecutor\BatchExecutor的父类BaseExecutor的构造函数中持有了PerpetualCache)。

在同一个会话里,多次执行相同SQL语句,会直接从内存取到缓存的结果,不要再发送SQL到数据库。但在不同的会话里,即使执行的SQL一样,也不能使用一级缓存(因为跨了Session)。

下边来通过实际例子来验证一级缓存,并通过分析源码了解实现原理。

    /**
     * 测试一级缓存作用域,是在SqlSession -> Executor -> localCache 中存储
     */
    @Test
    public void testFirestCache(){
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();

        try {
            //使用相同sqlsession执行查询方法,只打印了1次查询语句
            UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
            UserMapper userMapper2 = sqlSession1.getMapper(UserMapper.class);

            User user1 = userMapper1.byId(1L);
            User user2 = userMapper2.byId(1L);

            LoggerUtil.printThread("相同Session第1个结果:" + user1.toString());
            LoggerUtil.printThread("相同Session第2个结果:" + user2.toString());

            LoggerUtil.split();

            //使用不同session对象执行查询,会打印出2次查询语句
            UserMapper userMapper3 = sqlSession2.getMapper(UserMapper.class);
            User user3 = userMapper3.byId(1L);

            LoggerUtil.printThread("不同Session结果:" + user3.toString());
        }finally {
            sqlSession1.close();
            sqlSession2.close();
        }
    }

执行结果:

通过上边的测试代码可以看出,在使用sqlSession1的分别执行的两次查询,只输出一条执行sql语句,表示第二次查询时使用了缓存,未发送sql到数据库。

在sqlSession2执行的相同SQL时新输出了一条SQL表示没有使用缓存。

一级缓存的不足

使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个回话或者分布式环境下,会存在查到过时数据的问题(缓存脏读)。如下别的例子:

    /**
     * 缓存脏读,当有两个sqlsession同时操作一条数据时,会导致其中一个sqlsession不触发缓存清空,导致其中一个使用旧的缓存数据(已是脏数据)
     */
    @Test
    public void testCacheDirtyRead(){

        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();

        try {
            UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
            UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

            User user1 = userMapper1.byId(1001L);

            LoggerUtil.printThread("session1 第1个结果:" + user1.toString());

            LoggerUtil.split();

            LoggerUtil.printThread("在session2 中更新数据。");

            User newUser = new User(1001L,"测试" + DateUtil.now(),20);
            userMapper2.update(newUser);
            sqlSession2.commit();
            LoggerUtil.printThread("session2 第1个结果(更新数据):" + userMapper2.byId(1001L).toString());

            LoggerUtil.split();

            //使用session1再次查询,使用缓存,导致使用旧数据
            User user3 = userMapper1.byId(1001L);

            LoggerUtil.printThread("session1 第2个结果(旧数据):" + user3.toString());
        }finally {
            sqlSession1.close();
            sqlSession2.close();
        }
    }

执行结果:

可以看到,在session2时已把用户名称修改为最新时间,但因为session1中使用一级缓存,不知道其他session的变更,所以导致缓存脏读。

如果要结果这个问题,就需要用到工作范围更广的二级缓存。

二级缓存

二级缓存是用来解决一级缓存不能跨会话共享问题的,范围是namespace级别的,可以被多个SqlSession共享(只要是同一个接口里的相同方法,都可以共享),生命周期和应用同步。

如果开启了二级缓存,二级缓存应用是工作在一级缓存之前,还是一级缓存之后呢?

作为一个作用范围更广的缓存,它肯定是在SqlSession的外层,否则不可能被多个SqlSession共享。

而一级缓存是在SqlSession内部的,所以肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。

二级缓存是在哪里维护的呢?

要夸会话共享的话,SqlSession本身和它里面的BaseExecutor已经满足不了需求了,那我们应该在BaseExecutor之外创建一个对象。

但是二级缓存是不一定开启的。也就是说,开启了二级缓存,就启用这个对象,如果没有,就不用这个对象,我们应用怎么做呢?

实际上MyBatis用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis在创建Executor的时候会对Executor进行装饰。

CachingExecutor对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派给真正的查询器Executor实现类,比如SimpleExecutor来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。

开启二级缓存的方法

在mybatis-config.xml中配置了(可以不配置,默认是true)

<setting  name="cacheEnabled" value="true" />

只要没显示的设置cacheEnabled=false,都会用CachingExecutor装饰基本的执行器(Simple、Reuse、Batch)。

二级缓存的总开关默认是开启的。但是每个Mapper的二级缓存开关是默认关闭的。一个Mapper要用到二级缓存,还要单独打开它自己的开关。

<!-- 在mapper.xml中声明这个namespace使用二级缓存 -->

<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
	   size="1024" <!--最多缓存对象个数,默认1024-->
	   eviction="LRU" <!--回收策略-->
	   flushInterval="120000" <!--自动刷新时间ms,未配置时只有调用时刷新-->
	   readOnly="false"<!--默认是false(安全),改为true可读写时,对象必须支持序列化-->
	   />

mapper.xml配置了<cache>之后,select()会被缓存。update()、delete()、insert()会刷新缓存。

如果二级缓存拿到结果了,就直接返回(最外层判断),否则再到一级缓存,最后到数据库。

如果一个Mapper需要开启二级缓存,但是这个里面的某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办?

我们可以在单个Statement ID上显示关闭二级缓存(默认是true):

<select id="byId" resultMap="BaseResultMap" useCache="false" >
  ...
</select>

什么场景适合使用二级缓存?

  1. 因为所有增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单查询等(查多写少)。如果写多查少就失去了缓存的意义。

  2. 如果多个namespace中针对同一个表的操作,比如User表,如果在一个namespace中刷新了缓存,另一个namespace中没有刷新,就会出现脏数据的情况。

所以,推荐在一个Mapper里只操作单表的情况使用。

如果让多个namespace共享一个二级缓存,应该怎么做?

<cache-ref namespace="<其他命名空间>" />

cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个cache。在关联表比较少,或者按照业务可以对表进行分组的时候可以使用。

注意:这种情况下,多个Mapper操作都会引起缓存刷新,缓存的意义已经不大了。

mybatis 缓存(三)

mybatis 缓存(三)

mybatis的缓存分为一级缓存和二级缓存

一级缓存:基于SqlSession级别的缓存,也就是说,缓存了这个SqlSession执行所有的select.MapperStatement的结果集;同一个查询语句,只会请求一次;但是当前SqlSession执行增删改操作或者commit/rollback操作时,会清空SqlSession的一级缓存;

禁止一级缓存(同理也禁止了二级缓存)

xml方式:
<select id="ping" flushCache="true" resultType="string">
...
</select>

注解方式:
@Options(flushCache = FlushCachePolicy.TRUE)

一级缓存导致的问题:每个SqlSession可能会对同一个mapperStatement缓存不同的数据,如:

  • sqlSession1 查询了userId=1的数据,一级缓存生效
  • sqlSession2更新了userId=1的name值,然后在查询,一级缓存生效

这导致了sqlSession1和sqlSession2对于userId=1的缓存数据不一致,引入脏数据

一级缓存源代码:

public abstract class BaseExecutor implements Executor {
  // 一级缓存
  protected PerpetualCache localCache;
  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    ...
    // 默认使用PerpetualCache
    this.localCache = new PerpetualCache("LocalCache");
    ...
  }

  @Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ...
    // 增删改删除一级缓存
    clearLocalCache();
    ...
  }

  @SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ...
    // flushCache=true时清空一级缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    ...
    // 判断一级缓存是否有值
    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);
    }
    ...
  }

  @Override
  public boolean isCached(MappedStatement ms, CacheKey key) {
    // 一级缓存是否包含cachekey
    return localCache.getObject(key) != null;
  }

  @Override
  public void commit(boolean required) throws SQLException {
    ...
    // commit 删除缓存
    clearLocalCache();
    ...
  }

  @Override
  public void rollback(boolean required) throws SQLException {
    ...
    // rollback删除缓存
    clearLocalCache();
    ...
  }

  @Override
  public void clearLocalCache() {
    if (!closed) {
      // 清空缓存
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ...
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      // 删除原来的一级缓存
      localCache.removeObject(key);
    }
    // 将新获取的值放入一级缓存
    localCache.putObject(key, list);
    。。。
  }
}

二级缓存:基于SqlSessionFactory缓存,所有SqlSession查询的结果集都会共享;其实这样描述是不准确的,二级缓存是基于namespace的缓存,每个mapper对应一个全局Mapper namespace;当第一个Sqlsession查出的结果集,缓存在namespace中,第二个sqlsession再查找时会从nameSpace中获取;每个namespace是单例的;只有sqlSession调用了commit方法才会生效

禁止二级缓存:

mybatis-congif.xml:将所有的namespace都关闭二级缓存
     <settings>
        <setting name="cacheEnabled" value="false"/>
        ...
     </settings>

对单个namespace是否使用二级缓存
    <cache /> 当前namespace是否使用二级缓存
    <cache-ref namespace="..." /> 当前namespace和其它namespace共用缓存

对一个namespace中的单个MapperStatement关闭二级缓存
    <select id="selectParentBeans" resultMap="ParentBeanResultMap" useCache="false">
        select * from parent
    </select>

二级缓存导致的问题:当某个namespace出现多表查询时,会引起脏数据,如:

  • A namespace中的mapperStatement id = "xxx"查询了表A id=1和表B id=5的数据,二级缓存生效
  • 表B 更新了id=5的数据;

此时再来查A表中的mapperStatement id = "xxx"时,还是使用了A namespace中的二级缓存;这引起了B表id=5的脏数据

当然解决上述问题可以使用<cache-ref>;但这会导致缓存粒度变粗,多个namespace的操作都会影响该缓存;

二级缓存源码:

Configuration#
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  ...
  // 判断是否使用二级缓存
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  // 对executor制定插件
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}
public class CachingExecutor implements Executor {

  // 委托设计模式
  private final Executor delegate;
  // 二级缓存
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    // 更新调用清空缓存方法(flushCache=false时不情况二级缓存)
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // 判断该namespace是否含有缓存
    Cache cache = ms.getCache();
    if (cache != null) {
      // 看是否需要清空该namespace下的二级缓存
      flushCacheIfRequired(ms);
      // 当前MapperStatement是否需要使用二级缓存
      if (ms.isUseCache() && resultHandler == null) {
          ...
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 存入二级缓存
          tcm.putObject(cache, key, list); // issue #578 and #116
          ...
      }
    }
  }

  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    // flushCache=true时清空该namespace下的二级缓存,反之则不情况缓存
    if (cache != null && ms.isFlushCacheRequired()) {
      tcm.clear(cache);
    }
  }
}
public class TransactionalCacheManager {
  // 存储二级缓存,以namespace的cache作为key
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
  // 清空namespace的缓存
  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }
  // 获取某个namespace中的cacheKey的缓存
  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  // 放入某个namespace cacheKey的缓存
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }
  // 二级缓存提交
  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }
  // 二级缓存回滚
  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }
  // transactionalCaches缓存有则不存储,没有则存入
  private TransactionalCache getTransactionalCache(Cache cache) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }

}

在看源代码的时候,transactionalCaches引起深思

  • 每个sqlSession中有新的CachingExecutor
  • 每个CachingExecutor有新的TransactionalCacheManager
  • TransactionalCacheManager中的transactionalCaches是每个sqlSession独享的,如何达到线程安全且多个sqlSession共享呢?

其实二级缓存是以namespace粒度存储在Mapper里面的;每个mapper是全局共享的;而且getTransactionalCache这个方法已经将当前的namespace存放于每个cachingExecutor中了,所以达到了线程安全且sqlSession共享

Mybatis一级缓存和二级缓存 Redis缓存

Mybatis一级缓存和二级缓存 Redis缓存

一级缓存

  • Mybatis的一级缓存存放在SqlSession的生命周期,在同一个SqlSession中查询时,Mybatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。
  • 如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map缓存对象中已经存在改键值时,则会返回缓存中的对象。(一个SqlSession连续两次查询 得到的是同一个java对象)
  • 任何的insert update delete操作都会清空一级缓存(增删改任何记录都会清空当前SqlSession的缓存)。

Spring整合Mybatis的时候一级缓存的问题:

  在未开启事物的情况之下,每次查询,spring都会关闭旧的sqlSession而创建新的sqlSession,因此此时的一级缓存是没有启作用的

  在开启事物的情况之下,spring使用threadLocal获取当前资源绑定同一个sqlSession,因此此时一级缓存是有效的

Spring结合Mybatis一级缓存失效的问题

二级缓存

Mybatis二级缓存可以理解为存在SqlSessionFactory的生命周期

开启二级缓存:

1.在mybatis-config.xml添加如下代码

<settings>
        <setting name="cacheEnable" value="true"></setting>
    </settings>

2.在对应的XXXMapper.xml的namespace下添加<cache/>元素

二级缓存特点:

SqlSession1调用getMapper获取对象user1

SqlSession1调用getMapper获取对象user2

user1和user2是同一个实例(原理同一级缓存)

如果二级配置可读写的缓存 <cache readOnly="false"/>,不同SqlSession之间通过序列化和反序列化来保证通过缓存获取数据。

SqlSession2调用getMapper获取对象user1_

SqlSession2调用getMapper获取对象user2_

user1_和user2_就是反序列化得到的结果 是不同的实例

Redis缓存一致性

Mybatis默认提供的缓存是基于Map实现的内存缓存,已经可以基本满足应用。但当需要缓存大量数据的时候可以使用Redis缓存数据库来保存Mybatis的二级缓存数据。

但MySQL和Redis是两个事物,不好做强一致性。

简单点:可以延时双删+过期时间保证最终一致性。

双删的原因是防止并发情况下 update_db的过程中 其他事物发现redis缓存是空 重新赋予了Redis的值 此时如果赋值 是错误的数据

第二次延时删除的原因是要考虑MySQL数据库主从同步的耗时(如果立即删除 有别的线程从MySQL的从库查到的数据放到Redis中 此时的从库可能是没同步的错误数据)

rm_redis
update_db
sleep xxx ms
rm_redis

mybatis入门教程(九)------mybatis缓存

mybatis入门教程(九)------mybatis缓存

9. Mybatis 缓存

   正如大多数持久层框架一样,MyBatis 同样提供了一级缓存二级缓存的支持

1. 一级缓存: 基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 Cache 就将清空。
2. 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。
3. 对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被clear。


9.1 Mybatis的一级缓存


9.1.1  数据表准备

/*用户表*/
drop table if exists user;
create table user(
	id int primary key auto_increment,
	username varchar(50) unique,
	password varchar(100),
	nickname varchar(50),
	salt varchar(100),
	locked boolean
)engine=InnqDB default charset=utf8;


9.1.2  User实体类准备

package com.mscncn.batis.model;


public class User {
	private int id;
	private int age;
	private String userName;
	private String userAddress;
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}
	public String getUserAddress() {
		return userAddress;
	}
	public void setUserAddress(String userAddress) {
		this.userAddress = userAddress;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	
}

9.1.3 UserMapper.java

public interface UserMapper {
	public User getUserById(int id);
}

9.1.4 UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?> 
<!DOCTYPE mapper 
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 
<!-- 这里namespace必须是PostsMapper接口的路径,不然要运行的时候要报错 “is not known to the MapperRegistry”--> 
<mapper namespace="com.mscncn.batis.mapper.UserMapper"> 
    <!-- 这儿的resultType是配置在mybatis-config.xml中得别名 -->
    <select id="getUserById" parameterType="int" resultType="User">
    		select * from user where id=#{id}
    </select>
</mapper>

9.1.5 测试

@Test 
    public void testgetUserById() { 
       SqlSession sqlSession = sqlSessionFactory.openSession(); 
       try { 
    	   		UserMapper mapper = sqlSession.getMapper(UserMapper.class); 
    	   		//注意,重写User的toString方法
    	   		User u1=mapper.getUserById(1);
    	   		System.out.println(u1);
    	   		User u2=mapper.getUserById(1);
    	   		System.out.println(u2);
           sqlSession.commit();//这里一定要提交,不然数据进不去数据库中 
       } finally { 
           sqlSession.close(); 
       } 
    }


9.1.6 测试结果


   从以上结果中可以看出,两次调用getUserById方法,但是只有一次查询数据库的过程,这种现象产生的原因就是mybatis的一级缓存,并且一级缓存是默认开启的。


9.2  Mybatis的二级缓存


9.2.1 没有开启Mybatis二级缓存之前,测试

 @Test 
    public void testCache2() { 
       SqlSession sqlSession = sqlSessionFactory.openSession(); 
       SqlSession sqlSession2 = sqlSessionFactory.openSession(); 
       try { 
    	   		UserMapper mapper = sqlSession.getMapper(UserMapper.class); 
    	   		UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class); 
    	   		//注意,重写User的toString方法
    	   		User u1=mapper.getUserById(1);
    	   		System.out.println(u1);
    	   		User u2=mapper2.getUserById(1);
    	   		System.out.println(u2);
           sqlSession.commit();//这里一定要提交,不然数据进不去数据库中 
       } finally { 
           sqlSession.close(); 
       } 
    }


  测试结果:

  两个session,分别查询id为1 的 User ,那么mybatis与数据库交互了两次,这样说明mybatis现在没有开启二级缓存,需要我们手动的开启。


9.2.2 User.java

    实体类实现可序列化接口:

public class User implements Serializable {...}

    如果实体类不实现可序列化接口,使用二级缓存,那么会报下列异常:

org.apache.ibatis.cache.CacheException: Error serializing object.  Cause: java.io.NotSerializableException:

   

9.2.3 Mybatis 配置文件

    默认配置:

<settings>
<setting name="cacheEnabled" value="true"/>
</settings>

 必须开启缓存配置,才能使用mybatis的二级缓存,不然不能使用


9.2.4 Mybatis二级缓存测试

@Test 
    public void testCache2() { 
    	SqlSession sqlSession = sqlSessionFactory.openSession(); 
        SqlSession sqlSession2 = sqlSessionFactory.openSession(); 
        try { 
     	   		UserMapper mapper = sqlSession.getMapper(UserMapper.class); 
     	   		UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class); 
     	   		//注意,重写User的toString方法
     	   		User u1=mapper.getUserById(1);
     	   		sqlSession.commit();
     	   		System.out.println(u1);
     	   		User u2=mapper2.getUserById(1);
     	   		System.out.println(u2);
            sqlSession.commit();//这里一定要提交,不然数据进不去数据库中 
        } finally { 
            sqlSession.close(); 
        } 
    }

   这儿需要注意,必须使用的是两个不同的session,并且第一个session必须提交才能使用二级缓存(二级缓存必须提交前面的session,现在还没有找到原因)

9.2.5 测试结果

9.3 总结

  mybatis 一级缓存:默认开启

  1. 必须同一个session,如果session对象已经close()过了就不能用了

  2. 查询条件必须一致

  3. 没有执行过session.cleanCache();清理缓存

  4. 没有执行过增删改操作(这些操作都会清理缓存) 

 mybatis 二级缓存: 

mybatis-config.xml 中默认配置

  <settings> 
        <setting name="cacheEnabled" value="true" /> 
  </settings>

 必须手动开启在Mapper.xml中添加

 <cache/> 有默认的参数值

<cache />
<?xml version="1.0" encoding="UTF-8" ?> 
<!DOCTYPE mapper 
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 
<!-- 这里namespace必须是PostsMapper接口的路径,不然要运行的时候要报错 “is not known to the MapperRegistry”--> 
<mapper namespace="com.mscncn.batis.mapper.UserMapper"> 
	<cache />
    <!-- 这儿的resultType是配置在mybatis-config.xml中得别名 -->
    <select id="getUserById" parameterType="int" resultType="User">
    		select * from user where id=#{id}
    </select>
</mapper>

1. 映射语句文件中的所有select语句将会被缓存。 

2. 映射语句文件中的所有insert,update和delete语句会刷新缓存。 

3. 缓存会使用Least Recently Used(LRU,最近最少使用的)算法来收回。 

4. 缓存会根据指定的时间间隔来刷新。 

5. 缓存会存储1024个对象


<cache 

eviction="FIFO"  //回收策略为先进先出

flushInterval="60000" //自动刷新时间60s

size="512" //最多缓存512个引用对象

readOnly="true"/> //只读






关于mybatis缓存了解的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于MyBatis 缓存(5)、mybatis 缓存(三)、Mybatis一级缓存和二级缓存 Redis缓存、mybatis入门教程(九)------mybatis缓存等相关知识的信息别忘了在本站进行查找喔。

本文标签: