【MyBatis系列8】给我五分钟,带你彻底掌握MyBatis的缓存工作原理(1)

总目录展示

该笔记共八个节点(由浅入深),分为三大模块。

高性能。 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。该笔记将从设计数据的动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化这4个方面重点介绍。

一致性。 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知。因此,将用一个节点来专门讲解如何设计秒杀减库存方案。

高可用。 虽然介绍了很多极致的优化思路,但现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,还要设计一个PlanB来兜底,以便在最坏情况发生时仍然能够从容应对。笔记的最后,将带你思考可以从哪些环节来设计兜底方案。


篇幅有限,无法一个模块一个模块详细的展示(这些要点都收集在了这份《高并发秒杀顶级教程》里),麻烦各位转发一下(可以帮助更多的人看到哟!)

由于内容太多,这里只截取部分的内容。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

下图就是MyBatis中缓存相关类:

在这里插入图片描述

需要注意的是decorators包下面的所有类也实现了Cache接口,那么为什么我还是要说Cache只有一个实现类呢?其实看名字就知道了,这个包里面全部是装饰器,也就是说这其实是装饰器模式的一种实现。

我们随意打开一个装饰器:

在这里插入图片描述

可以看到,最终都是调用了delegate来实现,只是将部分功能做了增强,其本身都需要依赖Cache的唯一实现类PerpetualCache(因为装饰器内需要传入Cache对象,故而只能传入PerpetualCache对象,因为接口是无法直接new出来传进去的)

在MyBatis中存在两种缓存,即一级缓存二级缓存

一级缓存


一级缓存也叫本地缓存,在MyBatis中,一级缓存是在会话(SqlSession)层面实现的,这就说明一级缓存作用范围只能在同一个SqlSession中,跨SqlSession是无效的。

MyBatis中一级缓存是默认开启的,不需要任何配置。

我们先来看一个例子验证一下一级缓存是不是真的存在,作用范围又是不是真的只是对同一个SqlSession有效。

一级缓存真的存在吗

package com.lonelyWolf.mybatis;

import com.lonelyWolf.mybatis.mapper.UserAddressMapper;

import com.lonelyWolf.mybatis.mapper.UserMapper;

import com.lonelyWolf.mybatis.model.LwUser;

import org.apache.ibatis.io.Resources;

import org.apache.ibatis.session.SqlSession;

import org.apache.ibatis.session.SqlSessionFactory;

import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;

import java.io.InputStream;

import java.util.List;

public class TestMyBatisCache {

public static void main(String[] args) throws IOException {

String resource = “mybatis-config.xml”;

//读取mybatis-config配置文件

InputStream inputStream = Resources.getResourceAsStream(resource);

//创建SqlSessionFactory对象

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

//创建SqlSession对象

SqlSession session = sqlSessionFactory.openSession();

UserMapper userMapper = session.getMapper(UserMapper.class);

List userList = userMapper.selectUserAndJob();

List userList2 = userMapper.selectUserAndJob();

}

}

执行后,输出结果如下:

在这里插入图片描述

我们可以看到,sql语句只打印了一次,这就说明第2次用到了缓存,这也足以证明一级缓存确实是存在的而且默认就是是开启的。

一级缓存作用范围

现在我们再来验证一下一级缓存是否真的只对同一个SqlSession有效,我们对上面的示例代码进行如下改变:

SqlSession session1 = sqlSessionFactory.openSession();

SqlSession session2 = sqlSessionFactory.openSession();

UserMapper userMapper1 = session1.getMapper(UserMapper.class);

UserMapper userMapper2 = session2.getMapper(UserMapper.class);

List userList = userMapper1.selectUserAndJob();

List userList2 = userMapper2.selectUserAndJob();

这时候再次运行,输出结果如下:

在这里插入图片描述

可以看到,打印了2次,没有用到缓存,也就是不同SqlSession中不能共享一级缓存。

一级缓存原理分析

首先让我们来想一想,既然一级缓存的作用域只对同一个SqlSession有效,那么一级缓存应该存储在哪里比较合适是呢?

是的,自然是存储在SqlSession内是最合适的,那我们来看看SqlSession的唯一实现类DefaultSqlSession:

在这里插入图片描述

DefaultSqlSession中只有5个成员属性,后面3个不用说,肯定不可能用来存储缓存,然后Configuration又是一个全局的配置文件,也不合适存储一级缓存,这么看来就只有Executor比较合适了,因为我们知道,SqlSession只提供对外接口,实际执行sql的就是Executor。

既然这样,那我们就进去看看Executor的实现类BaseExecutor:

在这里插入图片描述

看到果然有一个localCache。而上面我们有提到PerpetualCache内缓存是用一个HashMap来存储缓存的,那么接下来大家肯定就有以下问题:

  • 缓存是什么时候创建的?

  • 缓存的key是怎么定义的?

  • 缓存在何时使用

  • 缓存在什么时候会失效?

接下来就让我们逐一分析

一级缓存CacheKey的构成

既然缓存那么肯定是针对的查询语句,一级缓存的创建就是在BaseExecutor中的query方法内创建的:

在这里插入图片描述

createCacheKey这个方法的代码就不贴了,在这里我总结了一下CacheKey的组成,CacheKey主要是由以下6部分组成

  • 1、将Statement中的id添加到CacheKey对象中的updateList属性

  • 2、将offset(分页偏移量)添加到CacheKey对象中的updateList属性(如果没有分页则默认0)

  • 3、将limit(每页显示的条数)添加到CacheKey对象中的updateList属性(如果没有分页则默认Integer.MAX_VALUE)

  • 4、将sql语句(包括占位符?)添加到CacheKey对象中的updateList属性

  • 5、循环用户传入的参数,并将每个参数添加到CacheKey对象中的updateList属性

  • 6、如果有配置Environment,则将Environment中的id添加到CacheKey对象中的updateList属性

一级缓存的使用

创建完CacheKey之后,我们继续进入query方法:

在这里插入图片描述

可以看到,在查询之前就会去localCache中根据CacheKey对象来获取缓存,获取不到才会调用后面的queryFromDatabase方法

一级缓存的创建

queryFromDatabase方法中会将查询得到的结果存储到localCache中

在这里插入图片描述

一级缓存什么时候会被清除

一级缓存的清除主要有以下两个地方:

  • 1、就是获取缓存之前会先进行判断用户是否配置了flushCache=true属性(参考一级缓存的创建代码截图),如果配置了则会清除一级缓存。

  • 2、MyBatis全局配置属性localCacheScope配置为Statement时,那么完成一次查询就会清除缓存。

  • 3、在执行commit,rollback,update方法时会清空一级缓存。

PS:利用插件我们也可以自己去将缓存清除,后面我们会介绍插件相关知识。

二级缓存


一级缓存因为只能在同一个SqlSession中共享,所以会存在一个问题,在分布式或者多线程的环境下,不同会话之间对于相同的数据可能会产生不同的结果,因为跨会话修改了数据是不能互相感知的,所以就有可能存在脏数据的问题,正因为一级缓存存在这种不足,所以我们需要一种作用域更大的缓存,这就是二级缓存。

二级缓存的作用范围

一级缓存作用域是SqlSession级别,所以它存储的SqlSession中的BaseExecutor之中,但是二级缓存目的就是要实现作用范围更广,那肯定是要实现跨会话共享的,在MyBatis中二级缓存的作用域是namespace,也就是作用范围是同一个命名空间,所以很显然二级缓存是需要存储在SqlSession之外的,那么二级缓存应该存储在哪里合适呢?

在MyBatis中为了实现二级缓存,专门用了一个装饰器来维护,这就是我们上一篇文章介绍Executor时还留下的没有介绍的一个对象:CachingExecutor。

如何开启二级缓存

二级缓存相关的配置有三个地方:

1、mybatis-config中有一个全局配置属性,这个不配置也行,因为默认就是true。

想详细了解mybatis-config的可以点击这里

2、在Mapper映射文件内需要配置缓存标签:

想详细了解Mapper映射的所有标签属性配置可以点击这里

3、在select查询语句标签上配置useCache属性,如下:

select * from lw_user

以上配置第1点是默认开启的,也就是说我们只要配置第2点就可以打开二级缓存了,而第3点是当我们需要针对某一条语句来配置二级缓存时候则可以使用。

不过开启二级缓存的时候有两点需要注意:

1、需要commit事务之后才会生效

2、如果使用的是默认缓存,那么结果集对象需要实现序列化接口(Serializable)

如果不实现序列化接口则会报如下错误:

在这里插入图片描述

接下来我们通过一个例子来验证一下二级缓存的存在,还是用上面一级缓存的例子进行如下改造:

SqlSession session1 = sqlSessionFactory.openSession();

UserMapper userMapper1 = session1.getMapper(UserMapper.class);

List userList = userMapper1.selectUserAndJob();

session1.commit();//注意这里需要commit,否则缓存不会生效

SqlSession session2 = sqlSessionFactory.openSession();

UserMapper userMapper2 = session2.getMapper(UserMapper.class);

List userList2 = userMapper2.selectUserAndJob();

然后UserMapper.xml映射文件中,新增如下配置:

运行代码,输出如下结果:

在这里插入图片描述

上面输出结果中只输出了一次sql,说明用到了缓存,而因为我们是跨会话的,所以肯定就是二级缓存生效了。

二级缓存原理分析

上面我们提到二级缓存是通过CachingExecutor对象来实现的,那么就让我们先来看看这个对象:

在这里插入图片描述

我们看到CachingExecutor中只有2个属性,第1个属性不用说了,因为CachingExecutor本身就是Executor的包装器,所以属性TransactionalCacheManager肯定就是用来管理二级缓存的,我们再进去看看TransactionalCacheManager对象是如何管理缓存的:

在这里插入图片描述

TransactionalCacheManager内部非常简单,也是维护了一个HashMap来存储缓存。

HashMap中的value是一个TransactionalCache对象,继承了Cache。

在这里插入图片描述

注意上面有一个属性是临时存储二级缓存的,为什么要有这个属性,我们下面会解释。

二级缓存的创建和使用

我们在读取mybatis-config全局配置文件的时候会根据我们配置的Executor类型来创建对应的三种Executor中的一种,然后如果我们开启了二级缓存之后,只要开启(全局配置文件中配置为true)就会使用CachingExecutor来对我们的三种基本Executor进行包装,即使Mapper.xml映射文件没有开启也会进行包装。

接下来我们看看CachingExecutor中的query方法:

在这里插入图片描述

上面方法大致经过如下流程:

  • 1、创建一级缓存的CacheKey

  • 2、获取二级缓存

  • 3、如果没有获取到二级缓存则执行被包装的Executor对象中的query方法,此时会走一级缓存中的流程。

  • 4、查询到结果之后将结果进行缓存。

需要注意的是在事务提交之前,并不会真正存储到二级缓存,而是先存储到一个临时属性,等事务提交之后才会真正存储到二级缓存。这么做的目的就是防止脏读。因为假如你在一个事务中修改了数据,然后去查询,这时候直接缓存了,那么假如事务回滚了呢?所以这里会先临时存储一下。

所以我们看一下commit方法:

在这里插入图片描述

二级缓存如何进行包装

最开始我们提到了一些缓存的包装类,这些都到底有什么用呢?

在回答这个问题之前,我们先断点一下看看获取到的二级缓存长啥样:

在这里插入图片描述

从上面可以看到,经过了层层包装,从内到外一次经过如下包装:

最后

看完上述知识点如果你深感Java基础不够扎实,或者刷题刷的不够、知识不全面

小编专门为你量身定制了一套<Java一线大厂高岗面试题解析合集:JAVA基础-中级-高级面试+SSM框架+分布式+性能调优+微服务+并发编程+网络+设计模式+数据结构与算法>

image

针对知识面不够,也莫慌!还有一整套的<Java核心进阶手册>,可以瞬间查漏补缺

image

全都是一丢一丢的收集整理纯手打出来的

更有纯手绘的各大知识体系大纲,可供梳理:Java筑基、MySQL、Redis、并发编程、Spring、分布式高性能架构知识、微服务架构知识、开源框架知识点等等的xmind手绘图~

image

image

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

0044263)]

针对知识面不够,也莫慌!还有一整套的<Java核心进阶手册>,可以瞬间查漏补缺

[外链图片转存中…(img-ue7XuViE-1715480044263)]

全都是一丢一丢的收集整理纯手打出来的

更有纯手绘的各大知识体系大纲,可供梳理:Java筑基、MySQL、Redis、并发编程、Spring、分布式高性能架构知识、微服务架构知识、开源框架知识点等等的xmind手绘图~

[外链图片转存中…(img-QaE2GTx2-1715480044264)]

[外链图片转存中…(img-rmfUjvHt-1715480044264)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值