MyBatis缓存机制

381 阅读9分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

缓存

什么是缓存

存在于内存中的临时数据

为什么使用缓存

减少和数据库的交互次数,提高执行效率

  • 使用于缓存的数据:
  1. 经常查询且不经常改变的数据

  2. 数据的正确与否对最终结果影响不大

  • 不适用于缓存的数据:
  1. 经常改变的数据

  2. 数据的正确与否对最终结果的影响很大,例如商品的库存,银行的汇率等

MyBatis缓存机制

MyBatis提供了一级缓存和二级缓存,实现将用户经常查询的数据结果保存到一级缓存或二级缓存中,用户在后面的查询中就不用到DB中获取了,直接从缓存中返回,从而减少与DB的交互,提高系统响应速度。

一级缓存

🚦在我们的一次数据库会话过程中,如果我们执行了多个相同查询条件的SQL查询操作,即所要查询的数据为同一个,那么MyBatis是每次都到数据库取出数据再返回给我们还是会利用Mybatis的缓存呢?

  • 答案是利用缓存。MyBatis默认会给我们开启一级缓存,MyBatis的一级缓存指的是Mybatis中SqlSession对象的缓存。在每次查询时会先去一级缓存中查询,查询得到则从缓存中获取,否则回到DB中查询,从DB查询的结果会同时存入到SqlSession为我们提供的一块区域中,该区域的结构是一个Map,那么当我们再次查询同样的数据时,就可以从以及缓存中获取,避免直接对数据库进行查询,提高性能。一级缓存是SqlSession级别的,所以当一次数据库会话结束,SqlSession对象消失时,Mybatis的一级缓存就失效了。

🚦那么有人会问,如果一次数据库会话中查询同一个数据都从缓存中取,当对应的数据被修改后,我们岂不是无法获取到最新的数据?

  • 其实并不会,因为MyBatis在提供一级缓存的时候,也为我们提供相应的触发清空一级缓存的机制,即当调用SqlSession的修改、添加、删除,commit()、close()等方法时,就会清空一级缓存,此时的缓存都是无效的,就会到数据库中获取最新的数据。🍿例如,第一次发起查询用户id为1的用户信息,会先去找一级缓存中查找是否有id为1的用户信息,如果没有,从数据库查询用户信息,得到用户信息后,将用户信息存储到一级缓存中。第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。如果sqlSession去执行插入、更新、删除然后调用commit()操作,就会清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。同时还有另外一种方式让一级缓存失效就是在Mapper映射文件中的select标签中添加一个属性flushCache并将其值设置为true,那么执行查询的时候会先清空一级缓存中的所有数据,然后去DB中获取数据。

image.png

一级缓存的使用

MyBatis默认就为我们开启了一级缓存,因此,下面我们来测试以下一级缓存的效果。

  • 默认的mybatis不能打印出SQL日志,不便于查看调试,所以我们先配置Mybatis结合log4j打印sql。
  1. 在classpath(即resource目录下)路径下新建一个log4j.properties文件,内容如下
log4j.rootLogger=ERROR,R
#配置打印到控制台的相关配置
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.Target=System.out
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p %t (%10c{1}) - %m%n
#配置打印到文件相关配置
log4j.appender.R=org.apache.log4j.RollingFileAppender  
#打印的文件名称和文件位置
log4j.appender.R.File=../logs/CManager.log
#日志文件分割,每个日志文件大小
log4j.appender.R.MaxFileSize=30MB
#日志文件保留个数
log4j.appender.R.MaxBackupIndex=200
#设置以追加形式打印
log4j.appender.R.Append=true  
#日志级别
log4j.appender.R.Threshold=DEBUG  
log4j.appender.R.layout=org.apache.log4j.PatternLayout 
#日志格式
log4j.appender.R.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L - %m%n
#作用域
log4j.logger.com.asiainfo=TRACE
log4j.logger.org.exam=INFO
log4j.logger.org.springframework.beans.factory=INFO
  1. 在主配置文件中开启打印日志到slf4j
<settings>
        <!--打印日志到slf4j-->
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

一级缓存测试

  • 虽然MyBatis默认开启了一级缓存,但我们仍然可以在主配置文件中通过setting标签,配置MyBatis的一级缓存。可以配置一级缓存的级别,包括SESSIONSTATEMENT默认为SESSION级别,下面我们来测试一下:
  1. 首先配置一级缓存的级别的为SESSION,即会话级别
<setting name="localCacheScope" value="SESSION"/>
  1. 首先,我们在一次数据库会话中连续执行两次查找id为42的用户信息,为SESSION级别,代码如下:
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
SqlSession session = factory.openSession(true);
UserDao userDao = session.getMapper(UserDao.class);
//第一次获取
User user = userDao.getUserById(42);
System.out.println(user);
//第二次获取
User user2 = userDao.getUserById(42);
System.out.println(user2);

session.close();
is.close();

打印的SQL日志: 无标题.png 从上面的SQL日志我们可以看出,只有第一次的查询是从数据库中查找的,而第二次是直接从缓存中取的。

  1. 接着我们测试将一级缓存的级别改为STATEMENT的效果:
<setting name="localCacheScope" value="STATEMENT"/>

同样执行上面的两次查询,其打印的SQL日志如下:

image.png 从以上结果可以看出,两次的查询都是从数据库中获取的。因为当我们把一级缓存的级别改为STATEMENT后,即使两次相同的查询都是属于同一个数据库会话中即同一个SqlSession,但是两次查询是对应两个不同的Statement,所以,第二次查询不会去缓存中查找。

  1. 接着我们测试在一次数据库会话中,如果对数据库发生了修改、删除、插入等操作,一级缓存是否会失效。下面我们在两个相同的查询语句中增加一个插入数据的操作。
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
SqlSession session = factory.openSession(true);
UserDao userDao = session.getMapper(UserDao.class);
//第一次获取
User user = userDao.getUserById(42);

//插入用户
userDao.insertUser(new User("不喝奶茶的Programmer",new Date(),"男","China"));

//第二次获取
User user2 = userDao.getUserById(42);

session.close();
is.close();

执行的SQL日志如下:

image.png

从以上日志我们可以看出,如果在一次数据库会话中,对数据库发生了修改、删除、插入等操作,一级缓存失效了,第二次查询走数据库获取数据。

二级缓存

  • 一级缓存使用上存在局限性,必须要在同一个SqlSession中执行同样的查询,一级缓存才能提升查询速度,如果想在不同的SqlSession之间使用缓存来加快查询速度,此时我们需要用到二级缓存了。
  • Mybatis的二级缓存指的是SqlSessionFactory对象的缓存。由同一个SqlSessionFactory对象创建的SqlSession对象共享其缓存。二级缓存是mapper映射级别的缓存,多个SqlSession去操作同一个Mapper映射的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。

image.png

二级缓存的使用

Mybatis缓存存放的是数据而不是对象,当第二次查询时,从缓存中取出数据,并填充到新创建的对象中,然后再将该对象返回。

(1)让Mybatis支持二级缓存(在主配置文件SqlMapConfig.xml中配置)

<!--在SqlMapConfig.xml主配置文件中<configuration>标签下配置添加以下配置--> <!--配置二级缓存--> 
    <settings>     
    <!--默认也是为true-->     
    <setting name="cacheEnabled" value="true"/> 
</settings> 

(2)让当前的映射文件支持二级缓存(在UserDao.xml配置文件中配置)

    <!--开启user支持二级缓存--> 
    <cache/> 

(3)让当前的操作支持二级缓存(在select标签中配置)

<!--在查询语句的标签中,添加【useCache="true"】属性--> 
<!--根据id查找用户信息--> 
    <select id="findById" parameterType="java.lang.Integer" resultType="user" useCache="true">
        select * from user where id=#{uid}; 
    </select>  
    
将UserDao.xml映射文件中的<select>标签中设置useCache=”true”,
代表当前这个statement要使用二级缓存,如果不使用二级缓存可以设置为false。
注意:针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存。

二级缓存测试

  1. 首先,我们开启两个sqlSession,分别为sqlSession1和sqlSession2,测试在sqlSession1执行查询后不commit,sqlSession2执行第二次相同查询,会不会从二级缓存中获取?
略...


SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);

UserDao userDao1 = sqlSession1.getMapper(UserDao.class);
UserDao userDao2 = sqlSession2.getMapper(UserDao.class);

//第一次查询
System.out.println("sqlSession1:第一次查询--->"+userDao1.getUserById(42));

//sqlSession2第二次查询
System.out.println("sqlSession2:第二次查询--->"+userDao2.getUserById(42));

略...

image.png

从以上结果可以看出,当sqlSession1执行查询后不commit,sqlSession2执行第二次相同查询并不会从二级缓存中获取。(避免脏读)

  1. 在以上例子的基础上,在sqlSession1第一次查询后,提交事务,再测试sqlSession2第二次相同查询会不会从二级缓存中获取。
略...


SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);

UserDao userDao1 = sqlSession1.getMapper(UserDao.class);
UserDao userDao2 = sqlSession2.getMapper(UserDao.class);

//第一次查询
System.out.println("sqlSession1:第一次查询--->"+userDao1.getUserById(42));

//sqlSession1查询后,提交sqlSession1的事务
sqlSession1.commit();

//sqlSession2第二次查询
System.out.println("sqlSession2:第二次查询--->"+userDao2.getUserById(42));

略...

执行日志:

image.png 从以上日志可以看出,sqlSession1提交后,sqlSession2第二次执行相同的查询顺利地从二级缓存中获取到了数据。因此,我们可以发现,当Sqlsession1没有提交,则二级缓存不会生效。

  1. 最后,我们测试一下在两次查询过程中对数据库进行update,二级缓存会不会失效?
//3、使用工厂生产SqlSession对象
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
SqlSession sqlSession3 = factory.openSession(true);

//4、使用SqlSeesion创建Dao接口的代理对象
UserDao userDao1 = sqlSession1.getMapper(UserDao.class);
UserDao userDao2 = sqlSession2.getMapper(UserDao.class);
UserDao userDao3 = sqlSession3.getMapper(UserDao.class);

//第一次查询
System.out.println("sqlSession1:第一次查询--->"+userDao1.getUserById(42));

//sqlSession1查询后,提交sqlSession1的事务
sqlSession1.commit();

//sqlSession2第二次查询
System.out.println("sqlSession1提交之后sqlSession2:第二次查询--->"+userDao2.getUserById(42));

//sqlSession3执行更新
userDao3.updateById(new User(42,"爱喝奶茶的Programmer"));
//sqlSession3提交,同样,如果不提交,第三次查询不会走二级缓存
sqlSession3.commit();

//sqlSession2第三次查询
System.out.println("sqlSession3执行数据库更新操作且提交后:第三次查询--->"+userDao2.getUserById(42));

执行日志:

无标题.png 从上面的日志可以看出,在两次查询过程中对数据库进行update后进行提交,二级缓存会失效。

总结

经过以上的了解,我们了解了MyBatis一级缓存和二级缓存各自的特点。MyBatis一级缓存 是一个会话sqlSession级别的,在同一个sqlSession中缓存数据可以共享,而二级缓存是namespace级别或者说是mapper级别的,即多个SqlSession去操作同一个Mapper里的sql语句,多个SqlSession可以共用二级缓存。当然,MyBatis的缓存是否开启应该结合我们的业务情况,在多表关联查询的情况下,MyBatis的二级缓存非常容易出现脏读,因此,我们应该根据业务情况,合理利用MyBatis的缓存机制。

🏁以上就是对MyBatis缓存机制的介绍,如果有错误的地方,还请留言指正,如果觉得本文对你有帮助那就点个赞👍吧😋😻😍

默认标题_动态分割线_2021-07-15-0.gif