MyBatis 缓存初探

MyBatis 的缓存分为一级缓存和二级缓存。
先说一下一级缓存的使用:

MyBatis一级缓存

MyBatis 的一级缓存是基于 SqlSession的,在同一个SqlSession对象的生命周期中,MyBatis 会把执行的方法和参数通过算法生成缓存的key,将key和查询结果放到一个 Map 对象中去,执行参数相同的同一条查询操作得到的是同一个实例对象。

MyBatis 默认开启一级缓存。

示例代码:

package com.kay;

import com.kay.dao.UserMapper;
import com.kay.entity.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.IOException;
import java.io.Reader;

/**
 * Created by kay on 2018/3/2.
 *
 * 测试一级缓存在 同一个SqlSession内保存
 *
 */
public class UserMapperTest {

    private static SqlSessionFactory sqlSessionFactory;

    @BeforeClass
    public static void init(){
        try {
            Reader reader = Resources.getResourceAsReader("SqlMapConfig.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            reader.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testSelect(){
        SqlSession sqlSession = sqlSessionFactory.openSession();
        User user1=null;
        try {
            user1 = (User) sqlSession.selectOne("getById",1);
            System.out.println("user1:"+user1);
//            user1.setName("xxxx");   //TODO 其实只是更新了缓存中的数据?
//            sqlSession.commit();     //TODO 加上这句就不会走缓存了

            User user2 = (User)sqlSession.selectOne("getById", 1);
            System.out.println("user2:"+user2);
        }finally {
            sqlSession.close();
        }

        sqlSession = sqlSessionFactory.openSession();
        try {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            User user3 = userMapper.getById(1);
            System.out.println("user3:"+user3);

            //Assert.assertNotEquals(user1,user3);  //不同sqlSession不是同一个对象
//            userMapper.updateUser(new User(2, "newUser")); //TODO 更新之后就不走缓存了

            User user4 = userMapper.getById(1);
          //  Assert.assertEquals(user3,user4);   //是同一个对象
        }finally {
            sqlSession.close();
        }
    }
}

这里写图片描述

从日志可以看到:在同一个sqlSession 对象中,都是只执行了一次查询,对象为同一对象。

通过clear 或者 commit 操作之后就会更新缓存。(update/delete/insert修改数据)

来通过源码来看一下一级缓存的查找过程:
1.首先去MapperProxy代理里面执行invoke:
这里写图片描述
可以看到 通过 cachedMapperMethod()方法获取一个接口方法,然后执行该查询。
2.进入execute调用里面,实则是调用了 SqlSession的selectOne方法:
这里写图片描述
selectOne最后也是调用的selectList 方法,传入 语句和参数:
这里写图片描述
调用 DefaultSqlSession 的selectList方法,拿到sql语句的id取配置里面获取这个执行对象,DefaultSqlSession对象里面持有了一个 CachingExecutor 对象
这里写图片描述
CachingExecutor 对象根据执行方法和参数算出缓存的key值:
这里写图片描述
然后调用自身的query方法去查缓存:
这里写图片描述
为什么这个Cache为空呢?因为这个缓存是二级缓存,我们并没有打开,于是就委托给一个代理去执行查询,看到这里就知道,缓存是先从二级里面去找的。来看看这个代理对象时什么:
这里写图片描述
通过调试看到代理是Executor接口的一个实现类:SimpleExecutor ,于是我们找到这个类里面去看它的query 方法做了什么:
这里写图片描述
但是我们发现它并没有query方法,于是我们想到一般的接口都会有一个顶层的抽象类,封装一些通用的处理方法,减少重复工作,果不其然,我们继续找到了BaseExecutor,它里面就有query方法:看它拿到缓存key值之后做了什么:
关键就是这句代码了:
这里写图片描述

list = resultHandler == null?(List)this.localCache.getObject(key)
看名字我们就知道了,this.localCache.getObject(key)去本地缓存找对应key的value!
看看这个localCache 是个啥:
这里写图片描述
进入该类:噢,原来里面有个HashMap:
这里写图片描述

执行完查询后list就有值了:
这里写图片描述
到这里,我们就知道PerpetualCache就是来放一级缓存的Map包装!

MyBatis 二级缓存

首先打开二级缓存配置:

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

二级缓存是基于mapper命名空间的(或者说sqlSesionFactory),在需要的namespace里面设置缓存:

<!-- 启用缓存-->
    <cache/>

话不多说,还是同一段代码:

package com.kay;

import com.kay.dao.UserMapper;
import com.kay.entity.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.IOException;
import java.io.Reader;

/**
 * Created by kay on 2018/3/2.
 *
 * 测试二级缓存在 同一个 namespace /SqlSessionFactory 内保存
 *
 */
public class UserMapperCache2 {

    private static SqlSessionFactory sqlSessionFactory;


    @BeforeClass
    public static void init(){
        try {
            Reader reader = Resources.getResourceAsReader("SqlMapConfig.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            reader.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testSelect(){
        SqlSession sqlSession = sqlSessionFactory.openSession();

        User user1=null;
        try {
            user1 = (User) sqlSession.selectOne("getById",1);
            System.out.println("user1:"+user1);

            User user2 = (User)sqlSession.selectOne("getById", 1);
            System.out.println("user2:"+user2);

            //到这里还是使用的一级缓存,所有 user1和user2 是同一个对象

            Assert.assertEquals(user1,user2);   //是同一个对象
        }finally {
            sqlSession.close();
        }

        //开启新的sqlSession,TODO  此处开始变为二级缓存,
       sqlSession = sqlSessionFactory.openSession();

        try {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            User user3 = userMapper.getById(1);

            System.out.println("user3:"+user3);

            Assert.assertNotEquals(user1,user3);  //不同sqlSession不是同一个对象

            User user4 = userMapper.getById(1);
            System.out.println("user4:"+user4);
            Assert.assertEquals(user3,user4);   //是同一个对象

        }finally {
            sqlSession.close();
        }
    }
}

这里写图片描述

user1和user2在同一个sqlSession内,其实用的还是一级缓存,此时二级缓存中还没有东西,所以二次查询缓存命中都是为0.
关闭sqlSession之后,缓存被同步到了二级缓存之中,新开启sqlSession后一级缓存失效,进入二级缓存,此时命中,也就是三分之一,0.333333.下次查询又是命中二级缓存,命中率为四分之二,0.5.

为什么后面二次都不是同一个对象了呢??

首先看一下我们的缓存怎么配置的:<cache/>

看下缓存还可以怎么设置,有哪些属性:

<!--1.收回策略:LRU/FIFO/SOFT/WEAK
        2.刷新间隔 毫秒单位
        3.引用数目,默认1024个(无论集合或对象)
        4.只读属性,默认false 开启后会返回对象的拷贝,慢/更安全-->
    <cache eviction="FIFO"
           flushInterval="600000"
           size="1024"
           readOnly="true"/>

注意 readOnly 这个属性,默认为可读写,此时会采用序列化的方式将对象缓存,使用到的类是:SerializedCache,当我们设置不同的读写属性时会发生什么呢:

  • 可读写(readOnly = false) 默认使用 序列化对象,获取时反序列化会得到新的对象实例
  • 只读 (readOnly = true ) 使用Map 来存储对象,所以返回同一个实例

这就是为什么返回的不是一个实例。

关于二级缓存的深入解析,参考《MyBatis 从入门到精通》作者的一篇博文《深入了解MyBatis二级缓存》,我就不班门弄斧了,这篇文章分析的很透彻。
连接:http://blog.csdn.net/isea533/article/details/44566257

祝大家元宵节快乐,晚安
2018/3/3 kay

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

带着天使反上帝 - Kaybee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值