走进Mybats的缓存包中,我们发现它只有一个cache的接口和一个缓存子类的实现。
再看看它上面有个装饰器的包,装饰者模式当然就是给功能做一些加强的,那么我们来看看它有啥加强功能,blockingCache,带锁的缓存,从里面可以看到它是重入锁实现的,loggingCache,日志缓存等。
好我们可以看出它的构成主要是一个perpretualCache和10的装饰类来增强,那什么时候要用什么包装呢?为什么要这样?
首先我们要了解下MyBatis的一级缓存和二级缓存。
一级缓存作用范围:会话级别(SqlSession)
既然一级缓存的作用范围是会话级别的,那么这个缓存是不是应该放在DefaultSqlSession中呢?
我们进入这个类看看,发现并没有这个成员变量,它只能被放到成员的两个对象中了,那全局配置类是不可能放的,放了不就变成了全局配置嘛,所以猜测肯定是放在了Executor执行器中。
那么之前我们说过Executor执行器有三种,通过注解也能指定,分别为simple(简单)、reuse(可复用)、batch(可复用可批量),那我们去执行器中看看吧。
找到执行器Executor,发现它的子类有个公共的父类,BaseExecutor,再看看里面的成员属性,果然有PerpetualCache,所以,其实它的缓存构建方式是,当我们请求一个SqlSession,它会生成一个执行器,而执行器中会生成缓存对象。
这也验证了上面的结论,一级缓存是存在sqlsession的执行器中的,它的作用域也就在一次会话中。
我们也可以用代码来证明一下。
/**
* 测试一级缓存需要先关闭二级缓存,localCacheScope设置为SESSION
* @throws IOException
*/
@Test
public void testCache() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
Blog blog = mapper0.selectBlogById(1);
System.out.println(blog);
System.out.println("第二次查询,相同会话,获取到缓存了吗?");
System.out.println(mapper1.selectBlogById(1));
System.out.println("第三次查询,不同会话,获取到缓存了吗?");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
System.out.println(mapper2.selectBlogById(1));
} finally {
session1.close();
}
}
从上面的代码我们可以看出只有同一个session中的sql查询才会去缓存中寻找,不同session缓存是不能共用,它会重新去发送SQL语句。
只需要把localCacheScope的范围缩小到一个statement就行了
当我们在同一个会话中,查询缓存之后,又对数据做了修改,此时缓存就会失效。
我们可以给某个标签加上flushCache为true来清楚缓存,但查询的flushCache默认为false,默认不清楚,增删改的flushCache默认为true,自动清空缓存,如果有必要,查询也可以设置为true来情况缓存。用代码来实现一下。
/**
* 一级缓存失效
* @throws IOException
*/
@Test
public void testCacheInvalid() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
System.out.println(mapper.selectBlogById(1));
Blog blog = new Blog();
blog.setBid(1);
blog.setName("2019年1月6日14:39:58");
mapper.updateByPrimaryKey(blog);
session.commit();
// 相同会话执行了更新操作,缓存是否被清空?
System.out.println("在执行更新操作之后,是否命中缓存?");
System.out.println(mapper.selectBlogById(1));
} finally {
session.close();
}
}
我们从结果中可以看出,增删改操作后,缓存会自动清除。
开启两个对话,当对话一读取了一条数据在缓存中,之后对话二对这条数据进行更新,对话一不会自动更新缓存,导致读取一级缓存中的老数据。看下代码。
/**
* 因为缓存不能跨会话共享,导致脏数据出现
* @throws IOException
*/
@Test
public void testDirtyRead() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1));
// 会话2更新了数据,会话2的一级缓存更新
Blog blog = new Blog();
blog.setBid(1);
blog.setName("after modified 112233445566");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
mapper2.updateByPrimaryKey(blog);
session2.commit();
// 其他会话更新了数据,本会话的一级缓存还在么?
System.out.println("会话1查到最新的数据了吗?");
System.out.println(mapper1.selectBlogById(1));
} finally {
session1.close();
session2.close();
}
}
从代码的结果来看,它确实读到了脏数据。那这种情况要怎么来解决呢?此时,我们就需要一个作用范围更广的缓存,也就是二级缓存。
二级缓存作用域:namespace级别,只要是同一个命名空间下的调用都能使用二级缓存,所以二级缓存应该作用于一级缓存之前。
那么二级缓存应该放到什么对象中来维护呢?显然SqlSession中的全局配置对象是不能放的,本线程执行器放的又是一级缓存,它到底应该放在哪里呢?它往上一级的工厂嘛?显然不合理,工厂就是来产生类的,不合适不是。
所以,mybatis想到,把二级缓存放到执行器的一个装饰者类中。
通过代码我们可以看出,Executor有个子类叫做CachingExecutor ,它用这个包装类包装了执行器,里面还有一个事务缓存管理器,这样就能达到比sqlsession更大范围的缓存管理了。
private final Executor delegate; //被包装的对象,也就是真正的执行器,还是要把sql丢在这里处理 private final TransactionalCacheManager tcm = new TransactionalCacheManager();
二级缓存的工作模型。
首先,mybatis的二级缓存和一级缓存是默认开启的,在全局配置文件中可以设置开关,但是如果只是在全局配置是不够的,还需要在具体的mapper中来配置,既然二级缓存的范围是在namspace下,那么它自然应该配置在mapper中了。
我们只需要在mapper下声明一个
1、size:默认缓存上限1024。
2、eviction:默认缓存淘汰策略LRU, Least Recently Use,最近最少使用的缓存淘汰掉(FIFO,先进先出;WEAK,基于弱引用的回收策略;SOFT基于软引用的淘汰策略)。
3、flushInterval:缓存多久清空一次。
4、readOnly:缓存对象是否只读,默认是false,通过读写copy序列化来操作,更加安全;true,直接返回对象,速度更快更不安全。
当我们开启二级缓存以后,首先看看能不能解决缓存数据共享问题,看代码。
/**
* 测试二级缓存一定要打开二级缓存开关
* @throws IOException
*/
@Test
public void testCache() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1));
// 事务不提交的情况下,二级缓存会写入吗?
session1.commit();
System.out.println("第二次查询");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
System.out.println(mapper2.selectBlogById(1));
} finally {
session1.close();
}
}
显然,二级缓存,可以跨会话共享。
那还会出现一级缓存的读到脏数据现象吗,看代码?
/**
* 测试二级缓存一定要打开二级缓存开关
* @throws IOException
*/
@Test
public void testCacheInvalid() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
SqlSession session3 = sqlSessionFactory.openSession();
try {
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
BlogMapper mapper3 = session3.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1));
session1.commit();
// 是否命中二级缓存
System.out.println("是否命中二级缓存?");
System.out.println(mapper2.selectBlogById(1));
Blog blog = new Blog();
blog.setBid(1);
blog.setName("2020-4-2");
mapper3.updateByPrimaryKey(blog);
session3.commit();
System.out.println("更新后再次查询,是否命中二级缓存?");
// 在其他会话中执行了更新操作,二级缓存是否被清空?
System.out.println(mapper2.selectBlogById(1));
} finally {
session1.close();
session2.close();
session3.close();
}
}
ok,完美解决了读到脏数据的问题,当其它会话更新操作之后,二级缓存会被清空,这样就需要再次去查询数据库,确保不会读到更改之前的脏数据。
只需要在方法中设置是否启用缓存即可。
1、以查询为主的业务:二级缓存是在同个namespace里面共享,可以支持跨会话,任意一个会话执行了增删改,都会清空二级缓存,如果频繁被清空就没有意义了。
2、只操作单表的业务:一个mapper操作一个表,如果其它mapper也要操作到我们这个表,那么也会清空二级缓存。
所以发现,增删改的操作都会清空二级缓存,这样就非常鸡肋了,一般线上项目中会使用一个单独的缓存中间件来实现,比如redis。