目录
1、前言
mybatis绝对是目前绝大部分Java程序员日常开发必不可少的框架了,然而尴尬的是,大部分人都处于API工程师阶段,甚至连最基本的一级缓存和二级缓存是啥都不知道。
今天,通过这篇博客来带大家扫扫盲,了解一下mybatis中的一级缓存和二级缓存~
2、一级缓存
假设我们现在有这么一个场景:需要从10w条数据中拿到用户id,然后再去用户表把用户信息查询出来。
需求实现起来很简单:分页获取数据,遍历数据根据用户id查询用户信息
但是你有没有想过,如果这10w条数据都是一个用户操作的呢?在这种极端情况下,程序会进行10w次重复操作,为了避免这种情况,mybatis引入了一级缓存。
@Test
public void test1() throws IOException {
InputStream fileInputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(fileInputStream);
SqlSession sqlSession = factory.openSession(true);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user1 = userMapper.selectByid(1);
User user2 = userMapper.selectByid(1);
System.out.println(user1.equals(user2));
}
在该示例代码中,控制台只打印了一次查询语句。
2.1 会话缓存
在同一次查询会话中如果出现相同的语句及参数,就会从缓存中取出不再走数据库查询。
一级缓存只能作用于查询会话中,所以也叫做会话缓存。
那么问题来了,什么是会话呢?在WEB系统中最直白的意思就是:一次请求期间。
而在mybatis中的表现形式为:
SqlSession sqlSession = factory.openSession(true);
// ……
sqlSession.close();
从open到close之间的都是同一个会话。
2.2 一级缓存什么时候失效
既然是缓存,失效是必然存在的。那么在mybatis中,一级缓存在什么场景下会失效呢?
- 必须是相同的会话
- 必须是同一个mapper 接口中的同一个方法
- 中间没有执行session.clearCache() 方法
- 查询语句中间没有执行insert、update、delete方法
第一点在刚才已经说过了,既然是会话缓存,肯定必须是相同的会话。第二点也很好理解,方法都不一样了,你们要抓周树人,和我鲁迅有什么关系~
第三点就不用说了,缓存都清空了不失效还等着过年啊?
但是这里需要说明一下第四点,我们对刚才的代码进行一点点改动:
public void test1() throws IOException {
InputStream fileInputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(fileInputStream);
SqlSession sqlSession = factory.openSession(true);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user1 = userMapper.selectByid(1);
userMapper.insertUser(new User());
User user2 = userMapper.selectByid(1);
System.out.println(user1.equals(user2));
}
请问这种情况下还能命中缓存吗?既然刚才已经摆出结论了,肯定是无法命中的。
那如果是orderMapper执行了insert操作,结果会怎么样呢?答案是仍然无法命中,不是说好的抓周树人吗?怎么鲁迅也没了?
2.3 一级缓存的真相
要回答这个问题,我们得了解一下这个一级缓存到底长啥样,以及一级缓存是什么时候存入和清空的。
通过debug我们可以得到如下调用链:
在query方法中,我们能看到如下代码:
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
if (list != null) {
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
从localCache中尝试获取数据,如果list为null,才调用queryFromDatabase方法。
这个localCache就是我们要找的一级缓存,让我们来看看它长啥样:
这是一个hashMap。
然后在queryFromDatabase方法中可以看到,一级缓存是在这时候被放进去的:
那么一级缓存的清空又是在啥时候呢?
同样通过debug我们可以得到如下的调用链:
在update方法中调用了this.clearLocalCache(),很明显,这是一个清空缓存的方法,
public void clearLocalCache() {
if (!this.closed) {
this.localCache.clear();
this.localOutputParameterCache.clear();
}
}
注意,这里是直接把一级缓存里面所有数据都清空了,也就是说,管你是鲁迅还是周树人还是张三李四,统统一股脑全抓起来。
2.4 一级缓存是不安全的
那么问题来了,刚才说的清空,指的是当前会话的缓存呢还是所有会话中的缓存呢?
如果是当前会话中的缓存,那岂不是会有并发问题?
在查询时另一个会话并发去修改查询的数据,是否就会导致数据不正确?
我们来做这样一个操作
public static void main(String[] args) throws IOException {
InputStream fileInputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(fileInputStream);
SqlSession sqlSession = factory.openSession(true);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
new Thread(new Runnable() {
@Override
public void run() {
User user1 = userMapper.selectByid(1);
System.out.println("线程一:");
System.out.println(user1);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程一:");
System.out.println(user1);
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
User user2 = userMapper.selectByid(1);
user2.setName("周树人");
System.out.println("线程二:");
System.out.println(user2);
sqlSession.close();
}
}).start();
}
线程一和线程二同用一个session,然后线程一查询出来的李四居然被线程二查询出来的对象影响了!好好的李四变成了周树人……
3、二级缓存
相较于一级缓存的简单与脆弱,二级缓存可就强大多了(同时也复杂了许多……)
在我们的业务中,很多数据其实存在读多写少情况的,也就是说这些数据在缓存中应该生存挺久才对。但是一级缓存并不能达到我们想要的目的,于是mybatis就设计了二级缓存。
要使用二级缓存,我们需要在mapper上添加@CacheNamespace注解,然后运行如下实例:
@Test
public void test2() throws IOException {
InputStream fileInputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(fileInputStream);
SqlSession sqlSession = factory.openSession(true);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user1 = userMapper.selectByid(1);
sqlSession.close();
SqlSession sqlSession1 = factory.openSession(true);
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
User user2 = userMapper1.selectByid(1);
System.out.println(user1.equals(user2));
}
可以看到,我们不仅命中了二级缓存(只查询了一次),同时两次查询出来的对象也不是同一个。
3.1 二级缓存使用条件
我们先来看看,什么情况下二级缓存才会生效:
- 当会话提交或关闭之后才会填充二级缓存
- 必须是在同一个命名空间之下
- 必须是相同的statement 即同一个mapper 接口中的同一个方法
- 必须是相同的SQL语句和参数
- 如果readWrite=true ,实体对像必须实现Serializable 接口
第2、3、4条很好理解,这里稍微说一下1和5。二级缓存之所以不会产生一级缓存的问题,就是靠1和5来保证的。
首先,如果我们把1的约束去除,那么线程A在命中缓存时,有可能命中的就是其它线程会话未完成的脏数据。而如果把5的约束去除,那么线程A和线程B拿到的就不是序列化的对象,而是和一级缓存一样,直接拿的引用。
3.2 二级缓存清除条件
与一级缓存不同的是,一级缓存的清除是一把梭,但是二级缓存只会梭自己namespace中的缓存。
同时,只有修改操作才会清空缓存,并且任何一种增删改操作都会清空整个namespace 中的缓存。
3.3 二级缓存长啥样
这个问题的答案可能会让你出乎意料,因为在mybatis中,对二级缓存疯狂使用了装饰者模式,为什么我要用"疯狂"呢?
看看它的实现类就知道了:
并且你点进去任何一个实现类,都会发现它的构造器参数是传入一个Cache类型的对象:
也就是说,你可以把它们所有实现类全部套娃到一个对象中~
此外,我们可以看到,二级缓存实际上也是一个hashMap
4、结尾
事实上,在mysql8.0之前,也是有类似的缓存概念,但是在8.0之后被剔除了,因为这个缓存带来的提升并不明显,反而需要花费性能去维护。
mybatis的一级缓存个人觉得甚至还不如mysql被剔除的缓存好用……