mybatis作为强大的持久层框架,缓存是必不可少的。Mybatis的缓存分为一级缓存和二级缓存,在本质上是相同的,它们都是用的Cache接口实现。
一级缓存默认是开启,并且是不可以关闭的,原因是因为Mybatis的一些关键特性都是基于mybatis一级缓存实现的,例如: h和建立级联映射、避免循环引用等。而且mybatis结果集映射是高度依赖CacheKey的。
首先看下一级缓存的应用,在一个SqlSession 中,对User表根据id进行两次查询.
@Test
public void findUserById() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory =new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession=sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
System.out.println("===========第一次查询开始=============");
User user1 = userMapper.findUserById(1);
System.out.println(user1);
System.out.println("===========第一次查询结束=============");
System.out.println("===========第二次查询开始=============");
User user2 = userMapper.findUserById(1);
System.out.println(user2);
System.out.println("===========第二次查询结束=============");
sqlSession.close();
}
控制台打印:
对user进行查询后,再进行update操作:
@Test
public void updateUser2() throws IOException {
//加载配置文件
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
//加载sqlsessionFactroy对象
SqlSessionFactory sqlSessionFactory =new SqlSessionFactoryBuilder().build(resourceAsStream);
//加载sqlSession对象
SqlSession sqlSession =sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
System.out.println("===========第一次查询开始=============");
User user1 = userMapper.findUserById(1);
System.out.println(user1);
user1.setUsername("豆豆");
user1.setPassword("qwe123");
user1.setId(1);
sqlSession.update("user.updateUser",user1);
//提交事务
sqlSession.commit();
System.out.println("===========第一次查询结束=============");
System.out.println("===========第二次查询开始=============");
User user2 = userMapper.findUserById(1);
System.out.println(user2);
user2.setUsername("豆豆1");
user2.setPassword("qwer1234");
user2.setId(1);
sqlSession.update("user.updateUser",user1);
//提交事务
sqlSession.commit();
System.out.println("===========第二次查询结束=============");
//释放资源
sqlSession.close();
}
总结
1、第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从 数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
2、 如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的 一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
3、 第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直 接从缓存中获取用户信息
Mybatis 的缓存基于jvm堆实现的, 所有的缓存数据都放在java对象中,通过Cache解控定义缓存对象的行为:
Mybatis 的一级缓存是SqlSession级别的缓存,使用PerpetualCache 实例实现。在BaseExecutor类中定义了两个PerpetualCache属性,如下:
// Mybatis 一级缓存对象,用于缓存Mybatis 查询结果
protected PerpetualCache localCache;
//存储过程输出参数缓存,用于缓存存储过程调用结果
protected PerpetualCache localOutputParameterCache;
这两个属性由BaseExecutor构造方法进行初始化
在进行查询操作时,会先创建CacheKey对象。如果两次查询操作CacheKey对象相同,就会被认为这两次查询执行的是相同的SQL语句。 通过BaseExecutor.createCacheKey()方法创建CacheKey对象,代码如下:
在代码中可知,影响缓存的key的因素有以下4个:
(1)Mapper的Id,即Mappper命名空间与
(2)查询结果的偏移量及查询的条数。
(3)具体的SQL语句及SQL语句中需要传递的所有参数。
(4)Mybatis 主配置文件中,通过标签配置的环境信息对应的id属性值。
执行两次查询时,需要4个因素完全相同,才会认为两次查询执行的是相同的SQL语句,缓存才会失效。
在BaseExecutor类的query()方法职工,根据缓存key从localCache属性中查找 是否有缓存对象,如果查询不到,则调用queryFromDatabase()方法从数据库中获取数据,然后将数据写入localCache对象中。代码如下:
值得注意的是,如果localCacheScope 属性设置为STATEMENT,则每次查询操作完成后,都会调用clearLocalCache()方法清空缓存。在分布式环境下,必须将localCacheScope 属性设置为STATEMENT,避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。
二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于sqlSession的,而二级缓存是基于mapper文件的namespace的,也就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中.
开启二级缓存
一级缓存默认是开启状态的, 但是二级缓存是需要手动开启,
首先在全局配置环境文件sqlMapConfig.xml文件中加入以下代码:
然后在相关的xxxMapper.xml文件中开启缓存
mapper.xml文件中就这么一个空标签,其实这里可以配置,PerpetualCache这个类是mybatis默认实现缓存功能的类。我们不写type就使用mybatis默认的缓存,也可以去实现Cache接口 来自定义缓存。测试二级缓存和sqlSession无关
@Test
public void testTwoCache() throws IOException {
//加载外部资源
InputStream resoureAsStream = Resources.getResourceAsStream("mybatis-config.xml");
//获取sqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resoureAsStream);
//获取SqlSession对象
SqlSession sqlSession1 =sqlSessionFactory.openSession();
SqlSession sqlSession2 =sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
System.out.println("===========第一次查询开始=============");
User user1 = userMapper1.findUserById(1);
System.out.println(user1);
sqlSession1.close();
System.out.println("===========第一次查询结束=============");
System.out.println("===========第二次查询开始=============");
User user2 = userMapper2.findUserById(1);
System.out.println(user2);
sqlSession2.close();
System.out.println("===========第二次查询结束=============");
}
可以看出上面两个不同的sqlSession,第一个关闭了,第二次查询依然不发出sql查询语句:
mybatis中还可以配置userCache和flushCache等配置项,userCache是用来设置是否禁用二级缓存的,在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出sql去查询,默认情况是true,即该sql使用二级缓
这种情况是针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存,直接从数 据库中获取。
在mapper的同一个namespace中,如果有其它insert、update, delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。
设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则 不
会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。
一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。所以我们不用设置,默认即可.
如果CacheEnable 属性值为true,则使用CachingExecutor 对Executor对象进行装饰增加二级缓存功能。CachingExecutor 类中维护TransactionalCacheManager 用于管理所有的二级缓存对象。在TransactionalCacheManager类中通过一个HashMap对象维护所有二级缓存实例对应的TransactionalCache对象。getObject()方法和putObject()方法中都会调用getTransactionalCache()方法获取二级缓存对象对应的TransactionalCache对象,然后对TransactionalCache对象进行操作。在getTransactionalCache()方法中,首先从HashMap对象中获取二级缓存对象对应的TransactionalCache对象,如果获取不到,则创建新的TransactionalCache对象添加到HashMap对象中。
在CachingExecutor的query()方法中,首先调用createCacheKey()方法创建缓存Key对象,然后调用MappedStatement对象的getCache()方法获取MappedStatement对象中维护的二级缓存对象。然后尝试从二级缓存对象中获取结果,如果获取不到,则调用目标Executor对象的query()方法从数据库获取数据,再将数据添加到二级缓存中。当执行更新语句后,同一命名空间下的二级缓存将会被清空。代码如下:
在flushCachelfRequired()方法中会判断标签的flushCache属性,如果属性值为true,就清空缓存。标签的flushCache属性值默认为false,而标签的flushCache属性值默认为true.以update()方法为例,代码如下:
在XMLMapperBuilder在解析Mapper配置时会调用cacheRefElement()方法解析标签,代码如下:
在获取标签的所有属性信息后,调用MapperBuilderAssistant对象的userNewCache()方法创建二级缓存实例,然后通过MapperBuilderAssistant的currentCache属性保存二级缓存对象的引用。在调用MapperBuilderAssistant对象的addMappedStatement()方法创建MappedStatement对象时会将当前命名空间对应的二级缓存对象的引用添加到MappedStatement对象中。下面是创建MappedStatement对象的关键代码: