本文共 5729 字,大约阅读时间需要 19 分钟。
mybatis绝对是目前绝大部分Java程序员日常开发必不可少的框架了,然而尴尬的是,大部分人都处于API工程师阶段,甚至连最基本的一级缓存和二级缓存是啥都不知道。
今天,通过这篇博客来带大家扫扫盲,了解一下mybatis中的一级缓存和二级缓存~
假设我们现在有这么一个场景:需要从10w条数据中拿到用户id,然后再去用户表把用户信息查询出来。
需求实现起来很简单:分页获取数据,遍历数据根据用户id查询用户信息
但是你有没有想过,如果这10w条数据都是一个用户操作的呢?在这种极端情况下,程序会进行10w次重复操作,为了避免这种情况,mybatis引入了一级缓存。
@Testpublic 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));}
在该示例代码中,控制台只打印了一次查询语句。
在同一次查询会话中如果出现相同的语句及参数,就会从缓存中取出不再走数据库查询。
一级缓存只能作用于查询会话中,所以也叫做会话缓存。
那么问题来了,什么是会话呢?在WEB系统中最直白的意思就是:一次请求期间。
而在mybatis中的表现形式为:
SqlSession sqlSession = factory.openSession(true);// ……sqlSession.close();
从open到close之间的都是同一个会话。
既然是缓存,失效是必然存在的。那么在mybatis中,一级缓存在什么场景下会失效呢?
第一点在刚才已经说过了,既然是会话缓存,肯定必须是相同的会话。第二点也很好理解,方法都不一样了,你们要抓周树人,和我鲁迅有什么关系~
第三点就不用说了,缓存都清空了不失效还等着过年啊?
但是这里需要说明一下第四点,我们对刚才的代码进行一点点改动:
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操作,结果会怎么样呢?答案是仍然无法命中,不是说好的抓周树人吗?怎么鲁迅也没了?
要回答这个问题,我们得了解一下这个一级缓存到底长啥样,以及一级缓存是什么时候存入和清空的。
通过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(); } }
注意,这里是直接把一级缓存里面所有数据都清空了,也就是说,管你是鲁迅还是周树人还是张三李四,统统一股脑全抓起来。
那么问题来了,刚才说的清空,指的是当前会话的缓存呢还是所有会话中的缓存呢?
如果是当前会话中的缓存,那岂不是会有并发问题?
在查询时另一个会话并发去修改查询的数据,是否就会导致数据不正确?
我们来做这样一个操作
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,然后线程一查询出来的李四居然被线程二查询出来的对象影响了!好好的李四变成了周树人……
相较于一级缓存的简单与脆弱,二级缓存可就强大多了(同时也复杂了许多……)
在我们的业务中,很多数据其实存在读多写少情况的,也就是说这些数据在缓存中应该生存挺久才对。但是一级缓存并不能达到我们想要的目的,于是mybatis就设计了二级缓存。
要使用二级缓存,我们需要在mapper上添加@CacheNamespace注解,然后运行如下实例:
@Testpublic 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));}可以看到,我们不仅命中了二级缓存(只查询了一次),同时两次查询出来的对象也不是同一个。
我们先来看看,什么情况下二级缓存才会生效:
第2、3、4条很好理解,这里稍微说一下1和5。二级缓存之所以不会产生一级缓存的问题,就是靠1和5来保证的。
首先,如果我们把1的约束去除,那么线程A在命中缓存时,有可能命中的就是其它线程会话未完成的脏数据。而如果把5的约束去除,那么线程A和线程B拿到的就不是序列化的对象,而是和一级缓存一样,直接拿的引用。
与一级缓存不同的是,一级缓存的清除是一把梭,但是二级缓存只会梭自己namespace中的缓存。
同时,只有修改操作才会清空缓存,并且任何一种增删改操作都会清空整个namespace 中的缓存。
这个问题的答案可能会让你出乎意料,因为在mybatis中,对二级缓存疯狂使用了装饰者模式,为什么我要用"疯狂"呢?
看看它的实现类就知道了: 并且你点进去任何一个实现类,都会发现它的构造器参数是传入一个Cache类型的对象:也就是说,你可以把它们所有实现类全部套娃到一个对象中~
此外,我们可以看到,二级缓存实际上也是一个hashMap
事实上,在mysql8.0之前,也是有类似的缓存概念,但是在8.0之后被剔除了,因为这个缓存带来的提升并不明显,反而需要花费性能去维护。
mybatis的一级缓存个人觉得甚至还不如mysql被剔除的缓存好用……
转载地址:http://xmfmf.baihongyu.com/