Mybatis一级缓存与二级缓存原理及失效场景

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中,一级缓存在什么场景下会失效呢?

  1. 必须是相同的会话
  2. 必须是同一个mapper 接口中的同一个方法
  3. 中间没有执行session.clearCache() 方法
  4. 查询语句中间没有执行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 二级缓存使用条件

我们先来看看,什么情况下二级缓存才会生效:

  1. 当会话提交或关闭之后才会填充二级缓存
  2. 必须是在同一个命名空间之下
  3. 必须是相同的statement 即同一个mapper 接口中的同一个方法
  4. 必须是相同的SQL语句和参数
  5. 如果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被剔除的缓存好用……

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值