认识Mybatis的一二级缓存
一次完整的数据库请求,首先根据配置文件生成SqlSessionFactory,再通过SqlSessionFactory开启一次SqlSession,在每一个SqlSession中维护着一个Executor实例,通过Executor实例,可以获取到Statement然后结合输入的参数,
查询结果集,Mybatis的一级缓存是在发生executor阶段,在executor内部维护着一个PerpetualCache实例完成缓存,PerpetualCache由一个id和HashMap组成,一级缓存和Sqlsession实例进行绑定的,每一次sqlSession都有一个
PerpetualCache进行缓存,不同的sqlSession之间不会相互影响,所以当SqlSession关闭或者commit的时候,一级缓存就会消失。二级缓存是mapper级别的缓存,二级缓存和namespace进行绑定,多个SqlSession去操作同一个Mapper的sql语句,
多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的,二级缓存也是作用于Executor实例之中,由CachingExecutor对Executor的实现类BaseExecutor类进行增强,从而增加了缓存的功能,二级缓存需要将查询结果序列化,
所以用来接收结果的pojo需要实现Serializable接口。
一、一级缓存
可以通过代码来认识一级缓存,需要准备以下jar包:
(1)Mybatis的核心包
(2) log4j日志包
搭建一个简单的demo:
自行创建一个user表,字段自定,mybatisConfig.xml,Mapper.xml, Mapper.java, ServiceExecutor.java内容如下:
mybatisConfig.xml
<?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> <settings> <setting name="logImpl" value="LOG4J"/> <!-- <setting name="cacheEnabled" value="true" /> --> </settings> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"></transactionManager> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/goods"/> <property name="username" value="root"/> <property name="password" value="www1928..com"/> </dataSource> </environment> </environments> <mappers> <mapper resource="com/idt/mybatis/mapper/UserMapper.xml"/> </mappers> </configuration>
log4j.properties
# Global logging configuration
log4j.rootLogger=DEBUG, stdout
# MyBatis logging configuration...
log4j.logger.com.idt.mybatis=DEBUG
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
Mapper.xml
package com.idt.mybatis.mapper; import java.util.List; import java.util.Map; public interface UserMapper { public List<Map<String, String>> getUserList(); }
ServiceExecutor.java
package com.idt.mybatis.service; import java.util.List; import java.util.Map; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import com.idt.mybatis.mapper.UserMapper; public class ServiceExecutor { private static SqlSessionFactory sqlSessionFactory; static { sqlSessionFactory = new SqlSessionFactoryBuilder().build(ServiceExecutor.class.getClassLoader().getResourceAsStream("mybatisConfig.xml")); } /* * 一级缓存验证 */ public void queryUserList() { SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); /*第一次查询*/ List<Map<String, String>> userList1 = userMapper.getUserList(); //清除缓存 //sqlSession.clearCache(); /*第二次查询*/ List<Map<String, String>> userList2 = userMapper.getUserList(); sqlSession.close(); } public static void main(String[] args) { ServiceExecutor executor = new ServiceExecutor(); executor.queryUserList(); } }
执行查询,打印控制台的日志如下:
可以看到代码中的两次查询却只执行了一次,所以在sqlSession没有关闭的情况下,第二次查询到的是缓存里的数据,通过sqlSession.clearCache()方法我们清空缓存然后再执行:
清空缓存之后,可以发现两次查询都取访问了数据库,前面已经说过,一级缓存只作用在sqlSession级别,所以当sqlSession关闭或者提交之后都会清空缓存。
源码中如下
在SSM环境中,Mybatis的SqlSessionFactory交由Spring维护,在每次Sqlsession执行查询之后都是自动close,所以在SSM环境,一级缓存是失效的。
二、二级缓存
由于一级缓存适用环境少,所以为了Mybatis为了提供能适用更多场景的二级缓存,二级缓存不再局限于SqlSession级别,而是作用于Mapper(namespace)级别,或者说在SqlSessionFactory级别
在原来的demo做以下添加:
在mybatisConfig.xml添加
<setting name="cacheEnabled" value="true" />
在Mapper.xml修改如下
<?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.idt.mybatis.mapper.UserMapper"> <cache> </cache> <select id="getUserList" resultType="map"> select * from t_user </select> <select id="getUserByname" resultType="map"> select * from t_user where loginname = #{loginname} </select> </mapper>
在Mapper.java新增接口
public Map<String, String> getUserByname(@Param("loginname") String name);
在ServiceExecutor.java中添加方法:
/* * 二级缓存验证 */ public void queryUserByname() { SqlSession sqlSession1 = sqlSessionFactory.openSession(); UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class); Map<String, String> user1 = userMapper1.getUserByname("liSi"); System.out.println("第一次查询:" + user1.toString()); sqlSession1.close(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class); Map<String, String> user2 = userMapper2.getUserByname("liSi"); System.out.println("第二次查询:" + user2.toString()); sqlSession1.close(); }
执行该方法:
可以看出第二次的数据来自于缓存,两次虽然是不同的sqlsession但是有着相同的sqlSessionFactory,查询sql以及相同的查询参数,mybatis会根据这个进行判断是否两次查询一样,来减少数据的连接然后提高查询效率,
但是问题也随之而来,如果在两次查询之间添加一个修改操作,那么mybatis还会用缓存之中的数据吗?前面提高过,二级缓存是作用于namespace级别,也就是在一个namespace中进行update,del, insert等操作都是清空缓存,
在之前的源码截图中可以看到,所以我们要验证不再一个namespace下,会不会因为出现缓存出现脏读,将demo做如下修改:
增加新的mapper映射UserMapper2,
UserMapper2.xml
<?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.idt.mybatis.mapper.UserMapper"> <delete id="updateUser" > update t_user set loginname=#{newname} where loginname = #{loginname} </delete> </mapper>
UserMapper.java
package com.idt.mybatis.mapper; import org.apache.ibatis.annotations.Param; public interface UserMapper2 { public int updateUser(@Param("newname") String newname, @Param("loginname") String name); }
mybatisConfig.xml
<mapper resource="com/idt/mybatis/mapper/UserMapper2.xml"/>
ServiceExecutor.java
public void queryUserByname() { SqlSession sqlSession1 = sqlSessionFactory.openSession(); UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class); Map<String, String> user1 = userMapper1.getUserByname("liSi"); System.out.println("第一次查询:" + user1.toString()); sqlSession1.close(); /*更新操作*/ SqlSession sqlSession3 = sqlSessionFactory.openSession(); UserMapper2 userMapper3 = sqlSession3.getMapper(UserMapper2.class); userMapper3.updateUser("zhangSan", "liSi"); SqlSession sqlSession2 = sqlSessionFactory.openSession(); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class); Map<String, String> user2 = userMapper2.getUserByname("liSi"); System.out.println("第二次查询:" + user2.toString()); sqlSession1.close(); }
从上面的打印日志中可以看出,在进行了更新操作之后,第二次的查询仍然读取了缓存中的数据,造成了脏读,所以mybatis的二级缓存不适用于哪些对数据实时性要求比较高的场景中。