MyBatis-一级缓存与二级缓存

前言

MyBatis是一个简单,小巧但功能非常强大的ORM开源框架,它的功能强大也体现在它的缓存机制上。
每当我们使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话。
在对数据库的一次会话中,我们有可能会反复地执行完全相同的查询语句,如果不采取一些措施的话,每一次查询都会查询一次数据库,而我们在极短的时间内做了完全相同的查询,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。
为了解决这一问题,减少资源的浪费,MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。

什么是一级缓存

MyBatis会在一次会话的表示----一个SqlSession对象中创建一个本地缓存(local cache),对于每一次查询,都会尝试根据查询的条件去本地缓存中查找是否在缓存中,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。
在这里插入图片描述
对于会话(Session)级别的数据缓存,我们称之为一级数据缓存,简称一级缓存。

一级缓存是如何工作的

由于MyBatis使用SqlSession对象表示一次数据库的会话,那么,对于会话级别的一级缓存也应该是在SqlSession中控制的。
实际上, MyBatis只是一个MyBatis对外的接口,SqlSession将它的工作交给了Executor执行器这个角色来完成,负责完成对数据库的各种操作。当创建了一个SqlSession对象时,MyBatis会为这个SqlSession对象创建一个新的Executor执行器,而缓存信息就被维护在这个Executor执行器中,MyBatis将缓存和对缓存相关的操作封装成了Cache接口中
在这里插入图片描述

一级缓存的生命周期

a. MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
b. 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
c. 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;
d.SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用;

一级会话的工作流程

1.对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;
2. 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
3. 如果命中,则直接将缓存结果返回;
4. 如果没命中:
4.1 去数据库中查询数据,得到查询结果;
4.2 将key和查询到的结果分别作为key,value对存储到Cache中;
4.3. 将查询结果返回;
5. 结束。

Cache接口的设计一级CacheKey的定义

我们知道,Cache最核心的实现其实就是一个Map,将本次查询使用的特征值作为key,将查询结果作为value存储到Map中。
现在最核心的问题出现了:怎样来确定一次查询的特征值?
换句话说就是:怎样判断某两次查询是完全相同的查询?
也可以这样说:如何确定Cache中的key值?
MyBatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:

  1. 传入的 statementId
  2. 查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offset和rowBounds.limit表示);
  3. 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() )
  4. 传递给java.sql.Statement要设置的参数值
    即3、4两条MyBatis最本质的要求就是:
    调用JDBC的时候,传入的SQL语句要完全相同,传递给JDBC的参数值也要完全相同。

综上所述,CacheKey由以下条件决定:
statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值

一级缓存的性能分析

1.MyBatis对会话(Session)级别的一级缓存设计的比较简单,就简单地使用了HashMap来维护,并没有对HashMap的容量和大小进行限制。
读者有可能就觉得不妥了:如果我一直使用某一个SqlSession对象查询数据,这样会不会导致HashMap太大,而导致 java.lang.OutOfMemoryError错误啊? 读者这么考虑也不无道理,不过MyBatis的确是这样设计的。
MyBatis这样设计也有它自己的理由:
a. 一般而言SqlSession的生存时间很短。一般情况下使用一个SqlSession对象执行的操作不会太多,执行完就会消亡;
b. 对于某一个SqlSession对象而言,只要执行update操作(update、insert、delete),都会将这个SqlSession对象中对应的一级缓存清空掉,所以一般情况下不会出现缓存过大,影响JVM内存空间的问题;
c. 可以手动地释放掉SqlSession对象中的缓存。

  1. 一级缓存是一个粗粒度的缓存,没有更新缓存和缓存过期的概念
    MyBatis的一级缓存就是使用了简单的HashMap,MyBatis只负责将查询数据库的结果存储到缓存中去, 不会去判断缓存存放的时间是否过长、是否过期,因此也就没有对缓存的结果进行更新这一说了。

根据一级缓存的特性,在使用的过程中,我认为应该注意:
1、对于数据变化频率很大,并且需要高时效准确性的数据要求,我们使用SqlSession查询的时候,要控制好SqlSession的生存时间,SqlSession的生存时间越长,它其中缓存的数据有可能就越旧,从而造成和真实数据库的误差;同时对于这种情况,用户也可以手动地适时清空SqlSession中的缓存;
2、对于只执行、并且频繁执行大范围的select操作的SqlSession对象,SqlSession对象的生存时间不应过长。

举例说明:
例1、看下面这个例子,下面的例子使用了同一个SqlSession指令了两次完全一样的查询,将两次查询所耗的时间打印出来,结果如下:

package com.gupaoedu.mybatis.v2;

import com.gupaoedu.mybatis.v2.mapper.Blog;
import com.gupaoedu.mybatis.v2.mapper.BlogMapper;
import com.gupaoedu.mybatis.v2.session.DefaultSqlSession;
import com.gupaoedu.mybatis.v2.session.SqlSessionFactory;

import java.util.Date;
/**
 * @author :杨晓尚
 * @date :Created in 2020/11/21 9:11
 * @description:启动
 * @modified By:
 * @version: $
 */
public class TestMybatis {

    public static void main(String[] args) {
        SqlSessionFactory factory = new SqlSessionFactory();
        DefaultSqlSession sqlSession = factory.build().openSqlSession();
        Date first = new Date();
        // 获取MapperProxy代理
        BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
        Blog blog = mapper.selectBlogById(1);

        System.out.println("第一次查询: " + blog+ (System.currentTimeMillis() -first.getTime())+"ms");
        System.out.println();
        Date second = new Date();
        blog = mapper.selectBlogById(1);
        System.out.println("第二次查询: " + blog+(System.currentTimeMillis() -second.getTime())+"ms");

    }
}

在这里插入图片描述
由上面的结果你可以看到,第一次查询耗时405ms,而第二次查询耗时不足1ms,这是因为第一次查询后,MyBatis会将查询结果存储到SqlSession对象的缓存中,当后来有完全相同的查询时,直接从缓存中将结果取出。

什么是二级缓存

MyBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。
在这里插入图片描述

如上图所示,当开一个会话时,一个SqlSession对象会使用一个Executor对象来完成会话操作,MyBatis的二级缓存机制的关键就是对这个Executor对象做文章。如果用户配置了"cacheEnabled=true",那么MyBatis在为SqlSession对象创建Executor对象时,会对Executor对象加上一个装饰者:CachingExecutor,这时SqlSession使用CachingExecutor对象来完成操作请求。CachingExecutor对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的Executor对象来完成查询操作,之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中,然后在返回给用户。

在这里插入图片描述
CachingExecutor是Executor的装饰者,以增强Executor的功能,使其具有缓存查询的功能,这里用到了设计模式中的装饰者模式。

二级缓存划分

MyBatis并不是简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,即是Mapper级别的,即每一个Mapper都可以拥有一个Cache对象,具体如下:
a.为每一个Mapper分配一个Cache缓存对象(使用节点配置);
b.多个Mapper共用一个Cache缓存对象(使用节点配置)

使用二级缓存,必备的条件

MyBatis对二级缓存的支持粒度很细,它会指定某一条查询语句是否使用二级缓存。
虽然在Mapper中配置了,并且为此Mapper分配了Cache对象,这并不表示我们使用Mapper中定义的查询语句查到的结果都会放置到Cache对象之中,我们必须指定Mapper中的某条选择语句是否支持缓存,即如下所示,在 节点中配置useCache=“true”,Mapper才会对此Select的查询支持缓存特性,否则,不会对此Select查询,不会经过Cache缓存。如下所示,Select语句配置了useCache=“true”,则表明这条Select语句的查询会使用二级缓存。

<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" useCache="true">

总之,要想使某条Select查询支持二级缓存,你需要保证:

  1. MyBatis支持二级缓存的总开关:全局配置变量参数 cacheEnabled=true
  2. 该select语句所在的Mapper,配置了 或节点,并且有效
  3. 该select语句的参数 useCache=true

一级缓存和二级缓存的使用顺序

请注意,如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:
二级缓存 ———> 一级缓存——> 数据库

总结

在开始的mybatis的定义中就说明了:mybatis是一款优秀的持久层框架。我理解的框架就是把一些重复性的,可模块化的工作给封装起来的一个工具。持久层框架就是把有关数据库持久层的一些操作给封装,例如数据库的连接,数据库连接池,数据库的驱动包等等,可以通过配置文件的方式直接进行配置连接。通过框架去管理这些东西,简化开发流程。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值