MyBatis第三讲

目录

3.1 一级缓存

3.1.1 一级缓存介绍

3.1.2 一级缓存的生命周期

3.1.3 一级缓存的工作流程

3.1.4 一级缓存的不足

3.2 二级缓存

3.2.1 二级缓存的配置

3.2.2 二级缓存的生命周期

3.2.3 二级缓存的相关问题

3.2.4 二级缓存的不足

3.3 MyBatis的查询顺序

3.4 Cache使用的注意事项

3.1 一级缓存

3.1.1 一级缓存介绍

一级缓存也叫本地缓存,MyBatis 的一级缓存是默认开启的,不需要任何的配置。

一级缓存是SqlSession级别的,通过同一个SqlSession查询的数据会被缓存,下次查询相同的数据,就会从缓存中直接获取,不会从数据库重新访问

每当我们使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话。

一级缓存是为了解决资源浪费的问题,如果在数据库的一次会话中,在极短的时间内反复执行完全相同的查询语句,结果大多数时候是相同的,如果不使用缓存,那么每次查询都需要连接数据库,会造成巨大的资源浪费。所以将查到的数据进行缓存,当下次查询时,经过判断如果有完全一样的查询,会直接将结果从缓存中取出,不需要再进行一次数据库连接。

3.1.2 一级缓存的生命周期

因为一级缓存是SqlSession级别的,所以一级缓存会随着会话的出现而出现,随着会话的关闭而消失。

令sqlSession失效的四种方式

a>)大概过程是:MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。

1)其中executor是MyBatis的sql顶层执行器接口,声明了对数据库所有的增删改查操作: query()和queryCursor()方法用于执行查询操作,update()方法用于执行插入、删除、修改操作。

1.1)BaseExecutor是Executor的抽象实现类,采用模板设计模式,它声明的抽象方法由子类SimpleExecutor、ReuseExecutor、BatchExecutor实现。

1.1.1)SimpleExecutor是基础的Executor,能够完成基本的增删改查操作

1.1.2)BatchExecutor则会对调用同一个Mapper执行的update、insert和delete操作,调用Statement对象的批量操作功能。

1.1.3)ResueExecutor对JDBC中的Statement对象做了缓存,当执行相同的SQL语句时,直接从缓存中取出Statement对象进行复用,避免了频繁创建和销毁Statement对象,从而提升系统性能

b>)如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。

c>)如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。

d>)SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用

映射文件:
    <cache readOnly="true"/>
    <select id="selectAll" resultType="com.lwl.entity.Dept">
        select  * from dept
    </select>
​
    <delete id="deleteDept">
        delete from dept where deptno=#{deptno}
    </delete>
​
mapper接口:
    public interface DeptMapper {
    /**
     * 查询所有部门
     */
    List<Dept> selectAll();
​
    /**
     * 删除一个部门
     */
    int deleteDept(Integer deptno);
    }
//测试类
SqlSession sqlSession;
    DeptMapper mapper;
    @Before
    public void before(){
        sqlSession = SqlSessionFactoryUtil.getSqlSession();
        mapper = sqlSession.getMapper(DeptMapper.class);
    }
//    当两次查询中没有别的操作时,会通过一次缓存的数据直接取出,不会再次连接数据库。
    @org.junit.Test
    public void testSelect(){
        List<Dept> depts = mapper.selectAll();
        System.out.println("depts =++++++++++++++++++++++++++= " + depts);
​
        List<Dept> depts1 = mapper.selectAll();
        System.out.println("depts1 =*************************= " + depts1);
    }
    /*
    * 查询一次后,会将查询的数据放在缓存中
    * 如果在使用一次缓存后,把sqlSession关闭,在调用时会报这个错误
    * Cause: org.apache.ibatis.executor.ExecutorException: Executor was closed.
    * 如果再使用,需要重新创建sqlsession会话对象,而且查询时需要再次连接数据库
    */
    @org.junit.Test
    public void testSelectClose(){
        List<Dept> depts = mapper.selectAll();
        System.out.println("depts =++++++++++++++++++++++++++= " + depts);
        sqlSession.close();
​
        SqlSession sqlSession1 = SqlSessionFactoryUtil.getSqlSession();
        DeptMapper mapper1 = sqlSession1.getMapper(DeptMapper.class);
        List<Dept> depts1 = mapper1.selectAll();
        System.out.println("depts1 = " + depts1);
    }
    /*
     * 查询一次后,会将查询的数据放在缓存中
     * 如果在使用一次缓存后,把sqlSession刷新,
     * 如果再使用查询时,不需要再次连接数据库
     */
    @org.junit.Test
    public void testSelectClearCache(){
        List<Dept> depts = mapper.selectAll();
        System.out.println("depts =++++++++++++++++++++++++++= " + depts);
        sqlSession.clearCache();
​
        List<Dept> depts1 = mapper.selectAll();
        System.out.println("depts1 = " + depts1);
    }
    /*
     * 查询一次后,会将查询的数据放在缓存中
     * 当在一次查询后,进行了增删改操作,那么在进行下一次查询时,需要再次连接数据库
     */
    @org.junit.Test
    public void testSelectCUD(){
        List<Dept> depts = mapper.selectAll();
        System.out.println("depts =++++++++++++++++++++++++++= " + depts);
​
        int i = mapper.deleteDept(20);
        System.out.println("i = " + i);
​
        List<Dept> depts1 = mapper.selectAll();
        System.out.println("depts1 = " + depts1);
    }

3.1.3 一级缓存的工作流程

1)对于一个查询,会根据statementid,params和rowbounds构建的key值对比Cache中的值。

2)如果Cache中有相等的值,代表了命中,代表是完全重复的查询,将该key值对应的value返回。

3)如果Cache中没有相等的值,代表没有命中,此时会连接数据库,从数据库中查找数据,并将key和查询的数据库信息作为value以键值对的形式存储到Cache中,将查询结果返回。

3.1.4 一级缓存的不足

一级缓存不能跨会话共享,不同的会话之间对于同样的数据可能有不一样的缓存,这样在多个会话或分布式的环境下,会存在脏数据的问题。如果想要解决这个问题就需要用到二级缓存,因为二级缓存可以跨会话。

mybatis中一级缓存(Local Cache)无法关闭。

3.2 二级缓存

二级缓存是SqlSessionFactory级别,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取 。

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

3.2.1 二级缓存的配置

1)在核心配置文件<settings>中,设置全局配置属性cacheEnabled=“true”,这个属性默认值为true,所以可以不用配置

2)在映射文件中设置标签<cache/>

4)查询的数据所转换的实体类类型必须实现序列化的接口。

2)
<!--    开启二级缓存-->
    <cache/>
    <select id="selectAll" resultType="com.lwl.entity.Dept">
        select  * from dept
    </select>
​
    <delete id="deleteDept">
        delete from dept where deptno=#{deptno}
    </delete>
    SqlSession sqlSession;
    DeptMapper mapper;
    @Before
    public void before(){
        sqlSession = SqlSessionFactoryUtil.getSqlSession();
        mapper = sqlSession.getMapper(DeptMapper.class);
    }
    @After
    public void after(){
        sqlSession.commit();
        sqlSession.close();
    }
​
    /*
    * 实体类未实现序列化接口时,会出现的错误
    * Cause: java.io.NotSerializableException: com.lwl.entity.Dept
    *
    * SqlSession关闭之后,一级缓存中的数据会写入二级缓存,所以这里只能用close方法,而不能用刷新缓存的方法
    * */
    @Test
    public void testCacheTwo(){
//        第一次查询,二级缓存中没有数据,所以查不到,一共查询一次,命中率为0/1=0.0
        List<Dept> depts = mapper.selectAll();
        System.out.println("depts = " + depts);
        sqlSession.close();
//        创建了一个新的会话,不是原来的会话,所以这里是跨会话查询
        sqlSession = SqlSessionFactoryUtil.getSqlSession();
        mapper = sqlSession.getMapper(DeptMapper.class);
//        第二次查询,二级缓存中有数据,查不到,一共查询两次,命中率为1/2=0.5
        List<Dept> depts1 = mapper.selectAll();
        System.out.println("depts1 = " + depts1);
//第三次查询相同的数据,则命中率为0.66666
    }
4)
@Data
public class Dept  implements Serializable {
    private Integer deptno;
    private String dname;
    private String loc; 
}

在mapper配置文件中添加的cache标签可以设置一些属性:

1)eviction属性:缓存回收策略

1.1)LRU(Least Recently Used) – 最近最少使用的:移除最长时间不被使用的对象。

1.2)FIFO(First in First out) – 先进先出:按对象进入缓存的顺序来移除它们。

1.3)SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。

1.4)WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。默认的是 LRU。

2)flushInterval属性:刷新间隔,单位毫秒,默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新

3)size属性:引用数目,正整数代表缓存最多可以存储多少个对象,太大容易导致内存溢出

4)readOnly属性:只读,true/false

4.1)true:只读缓存;会给所有调用者返回缓存对象的相同实例(弱拷贝:共用一份缓存文件)。因此这些对象不能被修改。这提供了很重要的性能优势。

4.2)false:读写缓存;会返回缓存对象的拷贝(强拷贝:再复制一份缓存文件通过序列化)。这会慢一些,但是安全,因此默认是false。

mapper映射文件:
<!--    开启二级缓存-->
    <cache readOnly="true"/>
    <select id="selectAll" resultType="com.lwl.entity.Dept">
        select  * from dept
    </select>
 
测试类:
 @Test
    public void testResdonly(){
        SqlSession sqlSession1 = SqlSessionFactoryUtil.getSqlSession();
        SqlSession sqlSession2 = SqlSessionFactoryUtil.getSqlSession();
        DeptMapper mapper1 = sqlSession1.getMapper(DeptMapper.class);
        DeptMapper mapper2 = sqlSession2.getMapper(DeptMapper.class);
//        第一次查询
        List<Dept> depts = mapper1.selectAll();
        sqlSession1.close();//关闭后,数据存到了二级缓存里面
//        第二次查询
        List<Dept> depts1 = mapper2.selectAll();
//        将二级缓存中的数据取出进行修改
        Dept dept = depts1.get(0);
        dept.setDname("只读测试");
        System.out.println("depts = " + depts);
        System.out.println("depts1 = " + depts1);
//        当开启二级缓存中的只读属性时,相当于是查询出来的缓存只有一份(弱拷贝),第二个会话修改了缓存中的数据之后,第一个会话结果打印出来也是被修改过的
//Readonly=true:depts = [Dept(deptno=1, dname=只读测试, loc=郑州), Dept(deptno=2, dname=行政部, loc=郑州), Dept(deptno=3, dname=马上国庆, loc=郑州)]
//Readonly=true:depts1 = [Dept(deptno=1, dname=只读测试, loc=郑州), Dept(deptno=2, dname=行政部, loc=郑州), Dept(deptno=3, dname=马上国庆, loc=郑州)]
//        当不开启二级缓存的只读属性时,相当于是第二个会话将缓存复制了一份(强拷贝),即使是第二个会话修改了缓存中的数据,也不会更改第一个会话缓存中的数据
//Readonly=false:depts = [Dept(deptno=1, dname=学术部, loc=郑州), Dept(deptno=2, dname=行政部, loc=郑州), Dept(deptno=3, dname=马上国庆, loc=郑州)]
//Readonly=false:depts1 = [Dept(deptno=1, dname=只读测试, loc=郑州), Dept(deptno=2, dname=行政部, loc=郑州), Dept(deptno=3, dname=马上国庆, loc=郑州)]
    }

3.2.2 二级缓存的生命周期

二级缓存的生命周期和应用同步

令二级缓存失效的情况:

两次查询之间执行任意的增删改,会使二级缓存失效,同时也会使一级缓存失效。

3.2.3 二级缓存的相关问题

1)查询时二级缓存应该在一级缓存之前还是一级缓存之后?

作为一个作用范围更广的缓存,它肯定是在SqlSession 的外层,否则不可能被多个SqlSession 共享。而一级缓存是在SqlSession内部的,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。

2)二级缓存放在哪个对象中维护呢?

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

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

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

3.2.4 二级缓存的不足

缓存是以namespace为单位的,不同namespace下的操作互不影响。

insert,update,delete操作会清空所在namespace下的全部缓存。

通常使用MyBatis Generator生成的代码中,都是各个表独立的,每个表都有自己的namespace。

1) 针对一个表的某些操作不在他独立的namespace下进行,会导致数据不一致。

2)多表操作

这些都是不能使用二级缓存来实现的

3.3 MyBatis的查询顺序

先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用。

如果二级缓存没有命中,再查询一级缓存

如果一级缓存也没有命中,则查询数据库

3.4 Cache使用的注意事项

1) 只能在【只有单表操作】的表上使用缓存 ,而且要保证这个表在整个系统中只有单表操作,而且和该表有关的全部操作必须在一个namespace下。

2) 在可以保证查询操作数远远大于insert,update,delete操作的情况下使用缓存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值