mybatis一级缓存二级缓存具体例子

 

 

 

看过其他老师的帖子后自己整理一下具体的例子,方便复习的时候用到,哪里有问题,还请各位老师务必指点我一下,我好做出更改,防止误导大家

一、参考贴子

可以先看看这几位老师的帖子

MyBatis中一、二级缓存机制的实现原理

SpringBoot+Mybatis一级缓存和二级缓存详解

mybatis一级缓存二级缓存

spring结合mybatis不用手动关闭sqlSession 原理

 

二、先测试一级缓存(可以理解为是基于SQLSession级别的)

(1)先采用不整合springboot的方式

我去找了个mybatis的项目,用里面的mybatis的confi.xml ,我们手动创建SQLSessionFactory,利用他来获取一个session,这里我们两次查询用的是同一个session

可以看到这里第二次查询确实是没有查询数据库

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 核心配置文件中的跟节点 -->
<configuration>
<!-- 加载properties的一种写法-->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
     <!--<plugins>-->
		<!--&lt;!&ndash; mybatis的分页插件 &ndash;&gt;-->
        <!--<plugin interceptor="com.github.pagehelper.PageInterceptor">-->
            <!--&lt;!&ndash; 设置数据库类型 Oracle,Mysql,MariaDB,SQLite,Hsqldb,PostgreSQL六种数据库&ndash;&gt;-->
            <!--<property name="helperDialect" value="mysql"/>-->
        <!--</plugin>-->
 	<!--</plugins>-->
    <typeAliases>
        <typeAlias type="com.zgy.ssm.po.Dept" alias="Dept"/>
    </typeAliases>
<!-- 环境配置,default是唯一标识 -->
<environments default="development">
<!-- 子环境配置,与 environments里面的default对应-->
<environment id="development">
<!-- 子事务管理  与jdbc的一致-->
<transactionManager type="JDBC"/>
<!-- 数据来源,管理数据的一个节点,里面放多个数据,可以管理多个数据,-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/jiushi"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!-- 需要映射的mapper文件,-->
<mappers>
<!-- mapper文件的文件路径,在核心配置文件中加载mapper文件-->
<mapper resource="mapper/Dept.xml"/>
</mappers>
</configuration>
public class test {
    @Test
    public void test01() {

        String resource = "config.xml";
        // 读取配置文件
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 构建sqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 获取sqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
       
        try {
            List<Dept> findall = sqlSession.selectList("findall");
            System.out.println("第一次查询结果:"+findall.get(0).getDname());


            List<Dept> findall2 = sqlSession.selectList("findall");
            System.out.println("第二次查询结果:"+findall2.get(0).getDname());
            sqlSession.commit();


        } finally {
            sqlSession.close();
        }


    }
}

 


(2)不自己创建sqlsession直接使用springboot中的dao(mapper)方法查询,

可以看到两次都查询了数据库

@RunWith(SpringRunner.class)
@SpringBootTest
class TestMybatisCache {
@Autowired
private EverydayPayDao everydayPayDao;
@Autowired
private SqlSessionFactory sqlSessionFactory;


    @Test
    void testMD5(){
        List<EverydayPay> list1 = everydayPayDao.findList();
        System.out.println("第一次查询完成");

        List<EverydayPay> list2 = everydayPayDao.findList();
        System.out.println("第二次查询完成");

    }

}

(3)为什么springboot中要两次查询

通过查看   https://my.oschina.net/u/3574106/blog/3005054 老师的帖子,你们先看,这位老师写的很详细

我自己也跟着找一下,springboot整合mybaits,就去找 MybatisAutoConfiguration 这个自动配置类

它给我们注入了一个SqlSessionTemplate,那么我们再去找这个SqlSessionTemplate

目光转移到SqlSessionTemplate,看其构造方法,发现它使用了一个代理,而实际的增删改查方法,实际是靠代理来执行的,

    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
        Assert.notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
        Assert.notNull(executorType, "Property 'executorType' is required");
        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        this.exceptionTranslator = exceptionTranslator;
        this.sqlSessionProxy = (SqlSession)Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionTemplate.SqlSessionInterceptor());
    }

 

看到这个Proxy.newProxyInstanc() 方法没有,大家回忆一下我们学习java基础的时候,这个java的动态代理的实现方式,是不是有一步就是这样来实现的:(声明一个接口,声明被代理类对象实现这个接口,动态代理类要实现InvocationHandler接口,我放段代码你们看一下,就是说我用代理类代理了被代理类后,执行的方法其实是代理类中被加工后的invoke方法

public interface Subject {
    void doSomething();
}

public class realSubjectImpl implements Subject {
    @Override
    public void doSomething() {
        System.out.println("我是被代理类,我实现了接口的方法");
    }
}

/**
 * 这是我们的动态代理类,动态代理要实现InvocationHandler的invoke才能达到动态代理的功能
 */
@Configuration
public class MyInvocationHandler implements InvocationHandler {
    private Object target;

    public Object bind(Object target){
        this.target=target;
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
    }
    /**
     * 动态代理要通过这个方法来实现
     * @param proxy  被代理对象
     * @param method 要实现的方法
     * @param args   方法运行需要的参数
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result=null;
        System.out.println("我是动态代理,我可以在实现被代理类方法之前做一些事情");
        result=method.invoke(proxy,args);
        System.out.println("我是动态代理,我可以在实现了被代理类方法之后做一些事情");
        return result;
    }
}


    @Test
    void testReflexProxy(){
        Subject subject=null;
        try {
            /**
             * 注意这里接收方法返回值用接口接收,不要用被代理类来接收,否则会报错
             */
    
            MyInvocationHandler myInvocationHandler=new MyInvocationHandler();
            subject =(Subject) myInvocationHandler.bind(new realSubjectImpl());
            
            subject.doSomething();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

 

 

private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
      try {
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator
              .translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

SqlSessionTemplate实际上是通过内部类SqlSessionInterceptor提供的反射功能去执行具体的操作,可以看到invoke方法里面加入了commit跟close方法,这也就是为什么在springboot中,不加事务的情况每次查询都会关闭sqlsession,并在下次查询的时候重新建立一个sqlsession的原因,

在上面原理的情况下,默认每次查询完之后自动commit,这就导致两次查询使用的不是同一个sqlSessioin,根据一级缓存的原理,它将永远不会生效。

 

(4)问:为什么是用SqlSessionTemplate呢?

答:这里我确实没有研究,当然我写的帖子也是片面的,我也是到处搜帖子找答案来搞清楚,大家可以看一下这个帖子  https://blog.csdn.net/u010841296/article/details/89367296

我们加几个断点试一下

还是执行springboot中的方法来看,确实是走到了

2021-04-10 09:48:14.396 DEBUG 3228 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'sqlSessionTemplate'
2021-04-10 09:48:14.396 DEBUG 3228 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'sqlSessionTemplate' via factory method to bean named 'sqlSessionFactory'

第一次查询结束,并关闭sqlsession

第二次也执行完毕

 

 

(5)commit方法中执行了什么

通过查看https://www.cnblogs.com/happyflyingpig/p/7739749.html 中老师写的

我点击session的commit方法往里查看,我debug看一下,

还是执行第二种(采用springboot)方法,看看能不能到这里,走到了

那就是说第一个执行完后就会清空sqlsession对象中的数据,也就是说缓存中的数据被清空了,

这里我之前有个地方理解错了,我以为数据库连接池里的对象就是sqlsession对象,实际不是,连接池是我们sqlsession跟数据库连接的桥梁,

我当时想通过设置数据库连接池的最大最小以及初始化连接数为1来创造出两次查询使用一个sqlsession来完成的场景,但是一直不是一个sqlsession(不加事务的情况下),没能成功,

这里是我理解错了,连接池是连接池,sqlsession是sqlsession

还是采用springboot的查询方式,通过查看控制台,可以看到这种情况下,查询完一个非事务的语句后就会关闭当前sqlsession,再来新的查询就会新创建sqlsession,

那如果我加上事务注解会如何呢,打断点试一下

他没有走我打的断点,而且使用了缓存,而且两次使用的是同一个sqlsession,因此在加了事务注解的情况下springboot中使用连接池一级缓存是好使的

再来试一下,通过第一种方式创建一个sqlsession来执行两次查询,第一次查询完我调用clearcache()方法来试一下,看看结果,会查询两次了


public class test {
    @Test
    public void test01() {

        String resource = "config.xml";
        // 读取配置文件
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 构建sqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 获取sqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            List<Dept> findall = sqlSession.selectList("findall");
            System.out.println("第一次查询结果:"+findall.get(0).getDname());
            sqlSession.clearCache();

            List<Dept> findall2 = sqlSession.selectList("findall");
            System.out.println("第二次查询结果:"+findall2.get(0).getDname());
            sqlSession.commit();

        } finally {
            sqlSession.close();
        }


    }
}

 

查看上面的帖子可以知道:

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

(6)提问

问:比如我用sqlsession1查询了一条记录,然后我用sqlsession2更新了这条记录,然后再次用sqlsession1查询这条记录,那么会如何

①:(在非事务的情况下)用springboot

通过上面的例子我们可以知道,(在非事务的情况下)用springboot,我们的查询-更新-查询,这三个操作都会在执行完关闭sqlsession,查询前创建新的sqlsession,因此数据是实时更新的

②:(在事务的情况下)用springboot

这样的情况下,三个操作共用一个sqlsession,,当执行update操作后会清空PerpetualCache对象的数据,但是该对象可以继续使用

③手动创建sqlsession1和sqlsession2,会如何

按道理来说,这里我更新了数据,但是不是用同一个sqlsession更新的,不会影响另一个sqlsession的数据。

这里要在只用mybatis的情景下使用,可以看到,第二次查询还是使用了缓存,没有查询更新后的数据

    @Test
    public void test01() {

        String resource = "config.xml";
        // 读取配置文件
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 构建sqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 获取sqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        try {
            List<Dept> findall = sqlSession.selectList("findall");
            System.out.println("第一次查询结果:"+findall.get(0).getDname());
            //sqlSession.clearCache();
            System.out.println("******************************");

            Dept dept=new Dept();
            dept.setDeptno(1);
            dept.setDname("2");
            dept.setLoc("1");
            sqlSession2.update("updateDept",dept);
            sqlSession2.commit();
            System.out.println("******************************");

            List<Dept> findall2 = sqlSession.selectList("findall");
            System.out.println("第二次查询结果:"+findall2.get(0).getDname());
            sqlSession.commit();

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

(7)两种一级缓存模式

大家也了解一下(复制其他老师的帖子),两种一级缓存模式
一级缓存的作用域有两种:session(默认)和statment,可通过设置local-cache-scope 的值来切换,默认为session。
二者的区别在于session会将缓存作用于同一个sqlSesson,而statment仅针对一次查询,所以,local-cache-scope: statment可以理解为关闭一级缓存。

 

二、二级缓存(可以看成是基于namespace范围的)

默认情况下,mybatis打开了二级缓存,但它并未生效,因为二级缓存的作用域是namespace,所以还需要在Mapper.xml文件中配置一下才能使二级缓存生效

下面的测试我就全在springboot项目中测试了,

(1)准备工作

 

配置文件中开启二级缓存:

数据库:

 

dept实体类中加一个ename字段,记得要实现序列化

测试dept,我们先把dept的xml中加上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">

<mapper namespace="com.zgy.everydayPay.dao.DeptDao">
<cache></cache>
	<select id="findList" resultType="com.zgy.everydayPay.entity.Dept">
		SELECT * FROM dept
	</select>
	<select id="findDeptAndEmp" resultType="com.zgy.everydayPay.entity.Dept">
	SELECT a.*,b.ename FROM dept a left join  emp b on a.deptno=b.deptno
	</select>

</mapper>
<?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">

<mapper namespace="com.zgy.everydayPay.dao.EmpDao">
	<update id="updateEmp" parameterType="com.zgy.everydayPay.entity.Emp">
		update emp  set ename=#{ename} where empno = #{empno}
	</update>

	<update id="updateDept" parameterType="com.zgy.everydayPay.entity.Dept">
		update dept  set dname=#{dname} where deptno = #{deptno}
	</update>
</mapper>

(2)先测试两个相同的dept的finllist

    @Test
//    @Transactional
    void testL2Cache(){
        List<Dept> list = deptDao.findList();

        System.out.println("第一次查询完成时部门名称:"+list.get(0).getDname());
        
        List<Dept> list2 = deptDao.findList();
        System.out.println("第二次查询完成时部门名称:"+list2.get(0).getDname());
        
    }

可以看到这里第二次没有查询数据库

(3)执行deptdao的 findDeptAndEmp()方法两次,在没有其他干扰的情况下,

结果跟上面的一样

    @Test
//    @Transactional
    void testL2Cache1(){
        List<Dept> list = deptDao.findDeptAndEmp();

        System.out.println("第一次查询完成时emp名称:"+list.get(0).getEname());

        List<Dept> list2 = deptDao.findDeptAndEmp();
        System.out.println("第二次查询完成时emp名称:"+list2.get(0).getEname());

    }

(4)执行deptdao的 findDeptAndEmp()方法两次,在中间加上empdao中的更新emp操作

可以看到,在emp的mapper里的对deptmaper有影响的操作不会影响到dept的mapper,这样会导致数据不一致,我们在emp的mapper中执行对dept的修改,也是不会使dept重新查询数据库,这样就会导致数据不同步,因为两个mapper.xml的作用域不同,要想合到一个作用域,就需要用到cache-ref

    @Test
//    @Transactional
    void testL2Cache2(){
        List<Dept> list = deptDao.findDeptAndEmp();

        System.out.println("第一次查询完成时emp名称:"+list.get(0).getEname());

        Emp emp=new Emp();
        emp.setEmpno(1);
        emp.setEname("李四");
        emp.setDeptno(1);
        empDao.updateEmp(emp);


        List<Dept> list2 = deptDao.findDeptAndEmp();
        System.out.println("第二次查询完成时emp名称:"+list2.get(0).getEname());

    }

 

(5)使用cache-ref

在emp的mapper.xml中加入cache-ref标签,再试一下,我想把数据库还原,还是上面的那个查询,可以看到第二次重新查询了

<cache-ref namespace="com.zgy.everydayPay.dao.DeptDao"></cache-ref>

(6)我执行一个findlist,但是会带上dept这个实例当做参数,我两次传入的dept的里面的属性不一致,第二次是还是从缓存中查询吗

问:什么是statementid

mybatis提供了四个主要的statement: insert select update delete 每一个statement都有一个id,

 

 

 

    @Test
//    @Transactional
    void testL2Cache3(){
        Dept dept=new Dept();

        dept.setLoc("地方1");
        List<Dept> list = deptDao.findListByDept(dept);
        System.out.println("第一次查询完成时loc名称:"+list.get(0).getLoc());
        
        dept.setLoc("地方3");
        List<Dept> list2 = deptDao.findListByDept(dept);
        System.out.println("第二次查询完成时loc名称:"+list2.get(0).getLoc());
    }

 

可以看到第二次查询了数据库

 

(7)二级缓存片面的理解

他把数据存到哪里去了?

通过查看老师的帖子https://blog.csdn.net/qq_39768738/article/details/107788756 中的这一块

if (list == null) {
                    // 这里会先查一级缓存,没有的话便会到数据库中查找,详见上面一级缓存查询逻辑
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    // TransactionalCacheManager对象会创建一个TransactionalCache对象,它对传入的Cache进行再一次的包装,它持有的entriesToAddOnCommit对象用来保存临时的缓存结果
                    // 当SqlSession执行commit或者close时都会执行transactionCache的flushPendingEntries方法将entriesToAddOnCommit中的数据提交到mapper的cache当中
                    this.tcm.putObject(cache, key, list);
————————————————
版权声明:本文为CSDN博主「机械熊猫侠」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_39768738/article/details/107788756

这里写的很清楚,那么我找到这里,然后打上断点,要是执行到这里那就说明是这样的,那我就这么理解了。自己扒源码确实看不懂,老师要是这里不加注释我根本就不知道从哪里下手

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值