大聪明教你学Java | 深入浅出聊 Mybatis 的一级缓存和二级缓存

news2024/9/20 16:54:52

前言

🍊作者简介: 不肯过江东丶,一个来自二线城市的程序员,致力于用“猥琐”办法解决繁琐问题,让复杂的问题变得通俗易懂。
🍊支持作者: 点赞👍、关注💖、留言💌~

在计算机的世界中,缓存无处不在,操作系统有操作系统的缓存,数据库也会有数据库的缓存,我们还可以利用中间件(例如 Redis)来充当缓存。 MyBatis 作为一款优秀的 ORM 框架,也用到了缓存,那么今天咱们就一起来聊一聊 Mybatis 的一级缓存和二级缓存。

Mybatis 的一级缓存

首先我们先来看一张图片👇

在这里插入图片描述
我们在开发项目的过程中,如果我们开启了 Mybatis 的 SQL 语句打印,我们就会经常看到这句话:Creating a new SqlSession,其实这就是我们常说的 Mybatis 的一级缓存。

Mybatis 的一级缓存也就是在执行一次 SQL 查询或者 SQL 更新之后,这条 SQL 语句并不会消失,而是被 MyBatis 缓存起来,当再次执行相同SQL语句的时候,就会直接从缓存中进行提取,而不是再次执行SQL命令。一级缓存又被称为 SqlSession 级别的缓存,在操作数据库时需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的 SqlSession 之间的缓存数据区(HashMap)是互相不影响的。

在我们的应用系统的运行期间,我们可能在一次数据库会话中,执行多次查询条件相同的 SQL 语句,那么针对此情况,你来设计的话你会如何考虑呢?没错,就是加缓存,MyBatis 也是这样去处理的,如果是相同的 SQL 语句,会优先命中一级缓存,避免直接对数据库进行查询,造成数据库的压力,以提高性能。具体执行过程如下图所示👇

在这里插入图片描述

SqlSession 是一个接口,提供了一些 CRUD 的方法,而 SqlSession 的默认实现类是 DefaultSqlSession,DefaultSqlSession 类持有 Executor 接口对象,而 Executor 的默认实现是 BaseExecutor 对象,每个 BaseExecutor 对象都有一个 PerpetualCache 缓存,也就是上图的 Local Cache。当用户发起查询时,MyBatis 根据当前执行的语句生成 MappedStatement,在 Local Cache 进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 Local Cache,最后返回结果给用户。这时候可能有小伙伴要说了:我还在控制台上见到了“Closing non transactional SqlSession ”这句话,那我每次创建的 SqlSession 到最后都被关闭了,那我还缓存个毛线了 😥

在这里插入图片描述
事请当然不会像我们想象的那样,我们继续往下看👇

🍊 getSqlSession 源码

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
    Assert.notNull(sessionFactory, "No SqlSessionFactory specified");
    Assert.notNull(executorType, "No ExecutorType specified");
    // 如果当前我们开启了事物,那就从 ThreadLocal 里面获取 session
    SqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
        return session;
    } else {
        LOGGER.debug(() -> {
            return "Creating a new SqlSession";
        });
        // 没有获取到 session,创建一个 session
        session = sessionFactory.openSession(executorType);
        // 如果当前开启了事物,就把这个session注册到当前线程的 ThreadLocal 里面去
        registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
        return session;
    }
}

🍊 closeSqlSession 源码

public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
    Assert.notNull(session, "No SqlSession specified");
    Assert.notNull(sessionFactory, "No SqlSessionFactory specified");
    SqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);
    if (holder != null && holder.getSqlSession() == session) {
        LOGGER.debug(() -> {
            return "Releasing transactional SqlSession [" + session + "]";
        });
        holder.released();
    } else {
        LOGGER.debug(() -> {
            return "Closing non transactional SqlSession [" + session + "]";
        });
        session.close();
    }

}

我们使用官方的解释来说 closeSqlSession 方法就是:检查作为参数传递的 SqlSession 是否由 Spring TransactionSynchronizationManage 管理。如果不是,则关闭它,否则它只更新引用计数器,并在托管事务结束时让 Spring 调用关闭回调。简单点来说就是“如果我们方法是开启事物的,则当前事物内是获取的同一个 sqlSession,否则每次都是获取不同的 sqlSession”,所以我们也并不需要担心无法获取到对应的缓存。这时候有些小伙伴可能又有疑问了:Mybatis 的一级缓存什么情况下会过期呢?各位稍安勿躁,我们接着往下看👇

我们一开始就说了,Mybatis 的一级缓存是存在 sqlSession 里面的,毫无疑问当 sqlSession 被清空或者关闭的时候缓存就没了(在不开启事物的情况下,每次都会关闭 sqlSession);除此之外,在执行 insert、update、delete 的时候也会清空缓存。我们通过源码可以发现 sqlSession 的 insert 和 delete 方法的本质都是执行的 update 方法 👇

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
我们再来看看 update 的源码👇

public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (this.closed) {
        throw new ExecutorException("Executor was closed.");
    } else {
        this.clearLocalCache();
        return this.doUpdate(ms, parameter);
    }
}

执行到 this.clearLocalCache(); 的时候,缓存就已经被清理掉了,也就是说此时 Mybatis 的一级缓存就过期了🧐

我们说了这么多,相信各位小伙伴也了解到了 MyBatis 一级缓存的相关内容,不过 MyBatis 的一级缓存最大的共享范围就是一个 SqlSession 内部,那么如果多个 SqlSession 需要共享缓存该怎么办呢?没错!这时候就需要 MyBatis 的二级缓存登场了 😎

Mybatis 的二级缓存

如果需要多个 SqlSession 共享缓存,则需要我们开启二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示👇
在这里插入图片描述
当二级缓存开启后,同一个命名空间(namespace)所有的操作语句,都影响着一个共同的 cache,也就是二级缓存被多个 SqlSession 共享,我们可以将其理解成一个全局变量。当开启二级缓存后,数据的查询执行流程就变为了:二级缓存 → 一级缓存 → 数据库。关于查询的执行流程,我们可以通过源码加以佐证,在 CachingExecutor 文件下的 query 方法很容易就看到了,如果开启二级缓存那就走二级缓存,否则就走一级缓存,如下图所示👇

在这里插入图片描述
Mybatis 的二级缓存不像一级缓存默认就是开启的,我们需要在对应的 Mapper 文件里面加上 cache 标签,手动开启 Mybatis 的二级缓存👇

在这里插入图片描述
我们可以看到 cache 标签有多个属性,我们先来一起看一下这些属性都分别代表了什么含义:

  • type:指定自定义缓存的全类名(一般我们可以使用该 Mapper 文件的全路径作为 type 值)。
  • readOnly:是否只读。true 只读,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据,同时 MyBatis 为了加快获取数据的速度,直接就会将数据在缓存中的引用交给用户,虽然速度快变快了,但是安全性却降低了。如果不设置该属性的话,则默认为读写。
  • size:缓存存放多少个元素。
  • blocking:若缓存中找不到对应的key,是否会一直阻塞(blocking),直到有对应的数据进入缓存。
  • flushinterval:缓存刷新间隔,缓存多长时间刷新一次,默认不刷新。
  • eviction: 缓存回收策略,回收策略共有以下四种

LRU:最近最少回收,移除最长时间不被使用的对象(默认值)
FIFO:先进先出,按照缓存进入的顺序来移除它们
SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象
WEAK:弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象

🍊 解析 cache 标签的 cacheElement 方法源码

private void cacheElement(XNode context) {
    if (context != null) {
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = this.typeAliasRegistry.resolveAlias(type);
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = this.typeAliasRegistry.resolveAlias(eviction);
        Long flushInterval = context.getLongAttribute("flushInterval");
        Integer size = context.getIntAttribute("size");
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        this.builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }

}

不知道各位小伙伴知不知道 Mybatis 的二级缓存应用了什么设计模式呢?其中最明显的就是应用了装饰器模式~

public Cache build() {
	// 设置默认的缓存实现类和默认的装饰器(PerpetualCache 和 LruCache)
    this.setDefaultImplementations();
    // 创建基本的缓存
    Cache cache = this.newBaseCacheInstance(this.implementation, this.id);
    // 设置自定义的参数
    this.setCacheProperties((Cache)cache);
    // 如果是PerpetualCache 的缓存,将进一步进行处理
    if (PerpetualCache.class.equals(cache.getClass())) {
        Iterator var2 = this.decorators.iterator();

        while(var2.hasNext()) {
            Class<? extends Cache> decorator = (Class)var2.next();
            // 进行最基本的装饰
            cache = this.newCacheDecoratorInstance(decorator, (Cache)cache);
            // 设置自定义的参数
            this.setCacheProperties((Cache)cache);
        }
		// 创建标准的缓存,也就是根据配置来进行不同的装饰
        cache = this.setStandardDecorators((Cache)cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
    	// 如果是自定义的缓存实现,这里只进行日志装饰器
        cache = new LoggingCache((Cache)cache);
    }

    return (Cache)cache;
}

既然是装饰器模式,那肯定不止一两种装饰器😄 Mybatis 的源码中一共提供了多种装饰器,比如LruCache、ScheduledCache、LoggingCache 等等,我们通过类名就大概能猜到他们的作用👇

在这里插入图片描述

这里有一点是需要注意的:其实他们并不是 cache 的实现类,真正的实现类只有 PerpetualCache ,红框里面的类都是对 PerpetualCache 的包装。

我们了解了缓存装饰器,我们再来看看设置标准装饰器的源码👇

private Cache setStandardDecorators(Cache cache) {
    try {
    	// 获取当前 cache的参数
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        if (this.size != null && metaCache.hasSetter("size")) {
            metaCache.setValue("size", this.size);
        }
		// 如果设置了缓存刷新时间,就进行ScheduledCache 装饰
        if (this.clearInterval != null) {
            cache = new ScheduledCache((Cache)cache);
            ((ScheduledCache)cache).setClearInterval(this.clearInterval);
        }
		// 如果缓存可读可写,就需要进行序列化 默认就是 true,这也是为什么我们的二级缓存的需要实现序列化(即对应实体类必须实现序列化接口)
        if (this.readWrite) {
            cache = new SerializedCache((Cache)cache);
        }
		// 默认都装饰 日志和同步
        Cache cache = new LoggingCache((Cache)cache);
        cache = new SynchronizedCache(cache);
        // 如果开启了阻塞就装配阻塞
        if (this.blocking) {
            cache = new BlockingCache((Cache)cache);
        }

        return (Cache)cache;
    } catch (Exception var3) {
        throw new CacheException("Error building standard cache decorators.  Cause: " + var3, var3);
    }
}

看完这块代码,心理就是一个字:爽!! 能把装饰器模式用的如此精妙,也真是没谁了。该说不说,只要能把这块源码理解通透,那装饰器模式就真的完全掌握了😉

通过上面的源码,我们知道 Mybatis 的二级缓存默认就是可读可写的缓存,它会用 SynchronizedCache 进行装饰,我们来看来SynchronizedCache 的 putObject 方法👇

public void putObject(Object key, Object object) {
    if (object != null && !(object instanceof Serializable)) {
        throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
    } else {
        this.delegate.putObject(key, this.serialize((Serializable)object));
    }
}

这也就是为什么二级缓存的实体一定要实现序列化接口的原因了,当然如果将二级缓存设置为只读的缓存,那么也就不需要实现序列化接口了。

最后我们回归实际,在分布式架构盛行的当下,我们该如何选择使用哪种缓存呢?其实答案也很简单:除非对性能要求特别高,否则一级缓存和二级缓存都不建议使用,Mybatis 的一级缓存和二级缓存都是基于本地的,分布式环境下必然会出现脏读

虽然 Mybatis 的二级缓存可以通过实现 Cache 接口集中管理缓存,避免出现脏读的情况,但是有一定的开发成本,并且在多表查询时,使用不当极有可能会出现脏数据~

小结

本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇‍

希望各位小伙伴动动自己可爱的小手,来一波点赞+关注 (✿◡‿◡) 让更多小伙伴看到这篇文章~ 蟹蟹呦(●’◡’●)

如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。

你在被打击时,记起你的珍贵,抵抗恶意;
你在迷茫时,坚信你的珍贵,抛开蜚语;
爱你所爱 行你所行 听从你心 无问东西

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/182786.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【头歌】函数的递归调用

第1关&#xff1a;编写递归函数方法求x的n次方 (要求n>0)任务描述本关任务&#xff1a;编写递归函数方法求x的n次方 (要求n>0)。相关知识递归法在定义一个过程或函数时出现调用本过程或本函数的成分&#xff0c;称之为递归。若调用自身&#xff0c;称之为直接递归。若过程…

论文笔记:SEMI-SUPERVISED CLASSIFICATION WITH GRAPH CONVOLUTIONAL NETWORKS

ICLR 2017 1 abstract和intro部分 问题的setting 在图上进行节点分类&#xff0c;其中只有一部分节点有label ——>基于图的半监督学习传统的方法是使用平滑正则 其中L0表示图中有监督部分的lossf是神经网络&#xff0c;ΔD-A表示unnormalized的拉普拉斯矩阵 这种方…

Blender 物理属性 (五)动态绘画

文章目录动态绘画简介.以小船划过水面产生波纹为例.波浪属性.动态绘画简介. 1 动态绘画可以让一个物体在另一个物体上绘制东西 2 动态绘画至少需要两个物体&#xff0c;一个作为画布&#xff0c;另一个作为笔刷 3 两个物体必须接触才有效果 以小船划过水面产生波纹为例. 1 为…

【数据结构】浅识泛型

目录 1、包装类 1.1、基本数据类型和其包装类 1.2、装箱和拆箱 1.2.1、装箱 1.2.2、拆箱 1.2.3、面试题 2、泛型的概念 3、引出泛型 3.1、语法 4、泛型类的使用 4.1、语法 5、裸类型&#xff08;Raw Type&#xff09; 6、泛型是如何编译的 6.1、擦除机制 6.2、不…

Springboot+ssm371的在线考试系统maven idea

摘 要 I 1 绪论 1 1.1研究背景 1 1.2研究现状 1 1.3研究内容 2 2 系统关键技术 3 springboot是基于spring的快速开发框架, 相比于原生的spring而言, 它通过大量的java config来避免了大量的xml文件, 只需要简单的生成器便能生成一个可以运行的javaweb项目, 是…

DPO4104示波器

18320918653 DPO4104 详细说明&#xff1a; 美国泰克Tektronix DPO4104数字荧光示波器主要产品特色&#xff1a;Inspector智能存储管理2.串行触发和分析3.10.4”更大的显示器, 前面板上USB和CompactFlash端口, 及TekVPI?改善的探头接口, 更强的操作渐 变性商品名称 &#x…

针孔相机模型

针孔相机模型坐标系(1) 图像像素坐标系(2) 图像物理坐标系(3) 相机坐标系(4) 归一化平面坐标系(5) 世界坐标系畸变校正针孔相机模型中一般会涉及到图像像素坐标系、图像物理坐标系、相机坐标系、归一化平面坐标系和世界坐标系这5个坐标系。 坐标系 (1) 图像像素坐标系 图像像…

IDEA设置界面和控制台的滚动条颜色

前言 不知道大家是否和我一样有这么一个烦恼&#xff1a; IDEA自带的滚动条颜色很暗&#xff0c;配上一些主题颜色搭配很难发现。 所以今天就想着怎么可以修改滚动条颜色&#xff0c;首先去网上搜了搜都是什么鼠标滚轮加shift滚动&#xff0c;一点也不实用 偶然看到了个不错的…

图表控件LightningChart.NET 系列教程(七):LightningChart 组件——LightningChart.NET 函数库

LightningChart.NET SDK 是一款高性能数据可视化插件工具&#xff0c;由数据可视化软件组件和工具类组成&#xff0c;可支持基于 Windows 的用户界面框架&#xff08;Windows Presentation Foundation&#xff09;、Windows 通用应用平台&#xff08;Universal Windows Platfor…

匿名函数 lambda

匿名函数 lambda 匿名函数&#xff1a;一句话函数&#xff0c;比较简单的函数,没有函数名的函数 在Python中&#xff0c;lambda的语法是唯一的 lamlambda a,b:ab lamlam(a1,b2) # 传参 print(lam) # 3lmbda的语法是唯一的。其形式如下&#xff1a;lambda argument_list: expr…

python图像处理(prewitt算子)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 前面几章谈到了灰化、增强、旋转、滤波等内容,今天来谈一谈边缘检测。边缘检测是图像处理的重要内容。很多图像分割、图像识别的前一步就是边缘检测。某种意义上说,边缘检测的好坏…

7-查看和处理文件内容

7-查看和处理文件内容 文本文件 ASCII、UTF-8、Unicode、ANSItxt、xml、conf、properties、yml等配置文件、日志文件、源代码 二进制文件 可执行文件、图片、音频、视频 cat 全拼&#xff1a;concatenate [kənˈkt(ə)nˌeɪt] 连接 格式&#xff1a;cat 文件名 more/…

2. Spring 注解开发

文章目录1. 用注解开发定义bean2. 纯注解开发3. 注解开发的 bean 管理3.1 作用范围管理&#xff08;单例或非单例&#xff09;3.2 生命周期管理4. 注解开发的依赖注入4.1 引用类型的依赖注入4.2 简单类型的依赖注入4.2.1 直接注入值4.2.2 注入 properties 文件中的值5. 注解开发…

Acwing---1101. 献给阿尔吉侬的花束

献给阿尔吉侬的花束1.题目2.基本思想3.代码实现1.题目 阿尔吉侬是一只聪明又慵懒的小白鼠&#xff0c;它最擅长的就是走各种各样的迷宫。 今天它要挑战一个非常大的迷宫&#xff0c;研究员们为了鼓励阿尔吉侬尽快到达终点&#xff0c;就在终点放了一块阿尔吉侬最喜欢的奶酪。…

MYSQL不存在插入 存在更新的解决方法和对比

设置主键id自增&#xff0c;name为唯一索引 一、避免重复插入 insert ignore into&#xff08;有唯一索引&#xff09; 关键字/句: insert ignore into&#xff0c;如果插入的数据会导致 UNIQUE索引 或 PRIMARY KEY 发生冲突/重复&#xff0c;则忽略此次操作/不插入数据&…

Leetcode.1664 生成平衡数组的方案数

题目链接 Leetcode.1664 生成平衡数组的方案数 题目描述 给你一个整数数组 nums。你需要选择 恰好 一个下标&#xff08;下标从 0 开始&#xff09;并删除对应的元素。请注意剩下元素的下标可能会因为删除操作而发生改变。 比方说&#xff0c;如果 nums[6,1,7,4,1]nums [6,1…

spark转化操作

文章目录转化操作Transformer算子概念单RDD转换函数多RDD转换函数map与flatmap转化操作 由于spark的惰性计算特性&#xff0c;RDD只有在第一次行动操作中被用到时才会真正进行计算&#xff0c;因此我打算将文章内容分为"转化操作"和"行动操作"两部分&…

【web】微信小程序笔记小结(视图与逻辑)

过完年回来干正事了orz 来源&#xff1a;黑马程序员前端微信小程序开发教程 目录 I. 页面导航 ① 概念 ② 导航方式 1&#xff09;声明式导航 ※※ 导航到 tabBar 页面 ※※ 导航到非 tabBar 页面 ※※ 后退导航 2&#xff09;编程式导航 ※※ 导航到 tabBar 页面…

Nature立新规:ChatGPT等大模型不可以成为作者

众所周知&#xff0c;AI 的超参数决定着模型学习效果和速度。相比普通机器学习任务&#xff0c;深度学习需要的训练时间较长&#xff0c;因此调参技巧就显得尤为重要。 但鉴于深度学习「炼丹」的特性&#xff0c;不同的模型需要不同的超参数&#xff0c;而每个超参的意义又不同…

【头歌】汉诺塔(Hanoi)的递归算法

任务描述本关任务&#xff1a;汉诺塔(Hanoi)的递归算法。相关知识相传在古印度圣庙中&#xff0c;有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上&#xff0c;有三根杆(编号A、B、C)&#xff0c;在A杆自下而上、由大到小按顺序放置64个金盘(如下图)。游戏的目标&a…