MyBatis的数据缓存

1. 缓存概述

mybatis中,缓存的功能由根接口Cacheorg.apache.ibatis.cache.Cache)定义。整个体系采用装饰器设计模式,数据存储和缓存的基本功能由PerpetualCacheorg.apache.ibatis.cache.impl.PerpetualCache)永久缓存实现,然后通过一系列的装饰器来对PerpetualCache永久缓存进行缓存策略等方便的控制。

用于装饰PerpetualCache的标准装饰器共有8个(全部在org.apache.ibatis.cache.decorators包中):

1.FifoCache:先进先出算法,缓存回收策略

2.LoggingCache:输出缓存命中的日志信息

3.LruCache:最近最少使用算法,缓存回收策略

4.ScheduledCache:调度缓存,负责定时清空缓存

5.SerializedCache:缓存序列化和反序列化存储

6.SoftCache:基于软引用实现的缓存管理策略

7.SynchronizedCache:同步的缓存装饰器,用于防止多线程并发访问

8.WeakCache:基于弱引用实现的缓存管理策略

另外,还有一个特殊的装饰器TransactionalCache:事务性的缓存

 

 1. 一级缓存

一级缓存,又叫本地缓存,是PerpetualCache类型的永久缓存,保存在执行器中(BaseExecutor),而执行器又在SqlSessionDefaultSqlSession)中,所以一级缓存的生命周期与SqlSession是相同的

 

1.1. 一级缓存原理分析

 

既然一级缓存的作用域只对同一个SqlSession有效,那么一级缓存应该存储在哪里比较合适是呢?是的,自然是存储在SqlSession内是最合适的

 

SqlSession的唯一实现类DefaultSqlSessionDefaultSqlSession中只有5个成员属性,后面3个肯定不可能用来存储缓存,然后Configuration又是一个全局的配置文件,也不合适存储一级缓存,因此只有Executor比较合适,SqlSession只提供对外接口,实际执行sql的就是Executor

 

 

看看Executor的实现类BaseExecutor

对于localCachePerpetualCache内缓存用一个HashMap来存储因此存在以下问题:

  • 缓存是什么时候创建的?
  • 缓存的key是怎么定义的?
  • 缓存在何时使用
  • 缓存在什么时候会失效?

1.1.1. 一级缓存CacheKey的构成

缓存是针对查询语句,一级缓存的创建就是在BaseExecutor中的query方法内创建的:

 

 

 

 

 

CacheKey主要是由以下6部分组成

1、将Statement中的id添加到CacheKey对象中的updateList属性

2、将offset(分页偏移量)添加到CacheKey对象中的updateList属性(如果没有分页则默认0)

3、将limit(每页显示的条数)添加到CacheKey对象中的updateList属性(如果没有分页则默认Integer.MAX_VALUE)

4、将sql语句(包括占位符?)添加到CacheKey对象中的updateList属性

5、循环用户传入的参数,并将每个参数添加到CacheKey对象中的updateList属性

6、如果有配置Environment,则将Environment中的id添加到CacheKey对象中的updateList属性

 

1.1.1. 一级缓存的使用

 

创建完CacheKey之后,我们继续进入query方法:

在查询之前就会去localCache中根据CacheKey对象来获取缓存,获取不到才会调用后面的queryFromDatabase方法

 

1.1.1. 一级缓存的创建

 

queryFromDatabase方法中会将查询得到的结果存储到localCache

 

 

一.1.1.1. 一级缓存什么时候会被清除

一级缓存的清除主要有以下两个地方:

1、就是获取缓存之前会先进行判断用户是否配置了flushCache=true属性(参考一级缓存的创建代码截图),如果配置了则会清除一级缓存。

2MyBatis全局配置属性localCacheScope配置为Statement时,那么完成一次查询就会清除缓存。

3、在执行commitrollbackupdate方法时会清空一级缓存。

PS:利用插件我们也可以自己去将缓存清除

 

1.1.1. 一级缓存问题:

 

一级缓存因为只能在同一个SqlSession中共享,所以会存在一个问题,在分布式或者多线程的环境下,不同会话之间对于相同的数据可能会产生不同的结果,因为跨会话修改了数据是不能互相感知的,所以就有可能存在脏数据的问题,正因为一级缓存存在这种不足,所以我们需要一种作用域更大的缓存。

 

1. 二级缓存

二级缓存,又叫自定义缓存,实现了Cache接口的类都可以作为二级缓存,所以可配置如encache等的第三方缓存。二级缓存以namespace名称空间为其唯一标识,被保存在Configuration核心配置对象中

每次构建SqlSessionFactory对象时都会创建新的Configuration对象,因此,二级缓存的生命周期与SqlSessionFactory是相同的。在创建每个MapperedStatement对象时,都会根据其所属的namespace名称空间,给其分配Cache缓存对象

二级缓存对象的默认类型为PerpetualCache,如果配置的缓存是默认类型,则mybatis会根据配置自动追加一系列装饰器。

所有的缓存对象的操作与维护都是由Executor器执行来完成的,一级缓存由BaseExecutor(包含SimpleExecutorReuseExecutorBatchExecutor三个子类)负责维护,二级缓存由CachingExecutor负责维护。因此需要注意的是:配置了二级缓存不代表mybatis就会使用二级缓存,还需要确保在创建SqlSession的过程中,mybatis创建是CachingExecutor类型的执行器。

1.1. 二级缓存的作用范围

一级缓存作用域是SqlSession级别,所以它存储的SqlSession中的BaseExecutor之中,但是二级缓存目的就是要实现作用范围更广,那肯定是要实现跨会话共享的,在MyBatis中二级缓存的作用域是namespace,也就是作用范围是同一个命名空间,所以很显然二级缓存是需要存储在SqlSession之外的,那么二级缓存应该存储在哪里合适呢?

MyBatis中为了实现二级缓存,专门用了一个装饰器来维护,对象:CachingExecutor

1.2. 如何开启二级缓存

二级缓存相关的配置有三个地方:

1mybatis-config中有一个全局配置属性,这个不配置也行,因为默认就是true

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

2、在Mapper映射文件内需要配置缓存标签:

<cache/> 或 <cache-ref namespace="com.lonelyWolf.mybatis.mapper.UserAddressMapper"/>

3、在select查询语句标签上配置useCache属性,如下:

<select id="selectUserAndJob" resultMap="JobResultMap2" useCache="true">

        select * from lw_user

</select>

以上配置第1点是默认开启的,也就是说我们只要配置第2点就可以打开二级缓存了,而第3点是当我们需要针对某一条语句来配置二级缓存时候则可以使用。不过开启二级缓存的时候有两点需要注意:

1、需要commit事务之后才会生效

2、如果使用的是默认缓存,那么结果集对象需要实现序列化接口(Serializable)

 

1.1. 二级缓存原理分析

 

上面我们提到二级缓存是通过CachingExecutor对象来实现的,那么就让我们先来看看这个对象:CachingExecutor中只有2个属性,第1个属性不用说,因为CachingExecutor

本身就是Executor的包装器,所以属性TransactionalCacheManager肯定就是用来管理二级缓存的,看看TransactionalCacheManager对象是如何管理缓存的:

TransactionalCacheManager内部非常简单,也是维护了一个HashMap来存储缓存。

HashMap中的value是一个TransactionalCache对象,继承了Cache

 

 1.1. 二级缓存的创建和使用

我们在读取mybatis-config全局配置文件的时候会根据我们配置的Executor类型来创建对应的三种Executor中的一种,然后如果我们开启了二级缓存之后,只要开启(全局配置文件中配置为true)就会使用CachingExecutor来对我们的三种基本Executor进行包装,即使Mapper.xml映射文件没有开启也会进行包装。

接下来我们看看CachingExecutor中的query方法:

上面方法大致经过如下流程:

  • 1、创建一级缓存的CacheKey
  • 2、获取二级缓存
  • 3、如果没有获取到二级缓存则执行被包装的Executor对象中的query方法,此时会走一级       缓存中的流程。
  • 4、查询到结果之后将结果进行缓存。

需要注意的是在事务提交之前,并不会真正存储到二级缓存,而是先存储到一个临时属性,等事务提交之后才会真正存储到二级缓存。这么做的目的就是防止脏读。因为假如你在一个事务中修改了数据,然后去查询,这时候直接缓存了,那么假如事务回滚了呢?所以这里会先临时存储一下。

所以我们看一下commit方法:

 

 1.1. 二级缓存如何进行包装

最开始我们提到了一些缓存的包装类,这些都到底有什么用呢?

在回答这个问题之前,我们先断点一下看看获取到的二级缓存长啥样:

 

 

从上面可以看到,经过了层层包装,从内到外一次经过如下包装:

1PerpetualCache:第一层缓存,这个是缓存的唯一实现类,肯定需要。

2LruCache:二级缓存淘汰机制之一。因为我们配置的默认机制,而默认就是LRU算法淘汰机制。淘汰机制总共有4中,我们可以自己进行手动配置。

3SerializedCache:序列化缓存。这就是为什么开启了默认二级缓存我们的结果集对象需要实现序列化接口。

4LoggingCache:日志缓存。

5SynchronizedCache:同步缓存机制。这个是为了保证多线程机制下的线程安全性。

 

下面就是MyBatis中所有缓存的包装汇总:

 

 

1.1. 二级缓存应该开启吗

既然一级缓存默认是开启的,而二级缓存是需要我们手动开启的,那么我们什么时候应该开启二级缓存呢?

1、因为所有的update操作(insert,delete,uptede)都会触发缓存的刷新,从而导致二级缓存失效,所以二级缓存适合在读多写少的场景中开启。

2、因为二级缓存针对的是同一个namespace,所以建议是在单表操作的Mapper中使用,或者是在相关表的Mapper文件中共享同一个缓存。

1.2. 自定义缓存

一级缓存可能存在脏读情况,那么二级缓存是否也可能存在呢?

是的,默认的二级缓存毕竟也是存储在本地缓存,所以对于微服务下是可能出现脏读的情况的,所以这时候我们可能会需要自定义缓存,比如利用redis来存储缓存,而不是存储在本地内存当中。

1.3. MyBatis官方提供的第三方缓存

MyBatis官方也提供了一些第三方缓存的支持,如:encacheredis。下面我们以redis为例来演示一下:

引入pom文件:

 

<dependency>

 

       <groupId>org.mybatis.caches</groupId>

 

       <artifactId>mybatis-redis</artifactId>

 

       <version>1.0.0-beta2</version>

 

</dependency>

 

然后缓存配置如下:

 

<cache type="org.mybatis.caches.redis.RedisCache"></cache>

 

然后在默认的resource路径下新建一个redis.properties文件:

 

host=localhost

 

port=6379

 

然后执行上面的示例,查看Cache,已经被Redis包装:

 

 1.1. 自己实现二级缓存

如果要实现一个自己的缓存的话,那么我们只需要新建一个类实现Cache接口就好了,然后重写其中的方法,如下:

package com.lonelyWolf.mybatis.cache;

import org.apache.ibatis.cache.Cache;

public class MyCache implements Cache {

    @Override

    public String getId() {

        return null;

    }

    @Override

    public void putObject(Object o, Object o1) {

    }

    @Override

    public Object getObject(Object o) {

        return null;

    }

    @Override

    public Object removeObject(Object o) {

        return null;

    }

    @Override

    public void clear() {

    }

    @Override

    public int getSize() {

        return 0;

    }

}
View Code

上面自定义的缓存中,我们只需要在对应方法,如putObject方法,我们把缓存存到我们想存的地方就行了,方法全部重写之后,然后配置的时候type配上我们自己的类就可以实现了

参考连接:https://blog.csdn.net/mingtianhaiyouwo/article/details/51674537

https://blog.csdn.net/weixin_28689507/article/details/112337488

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2022-10-25 10:56  雨也飘柔  阅读(137)  评论(0)    收藏  举报