缓存作用:
在程序访问数据库这个过程中,存在几个性能瓶颈:
- 网络通信
- 非关系型数据库将数据存储在硬盘当中,需要进行硬盘操作
- Java对象复用问题(Connection连接池,Statement对象)
缓存在程序和数据库之间搭建起一个桥梁,把数据存储在内存当中,提高查询效率,避免进行硬盘查询操作(空间换取时间)。
MyBatis实现缓存主要有俩种方式:
- ORM框架本身实现:优点:操作快,避免网络通信 缺点:获取的内存大小有限
- 第三方缓存系统(比如Redis):优点:缓存空间大 缺点:网络开销更高
内存的空间毕竟有限,空间不够时就要考虑把之前的数据拿出来,这个过程成为 换出。
换出 主要有俩种方式:二进制序列换出和JSON形式换出
换出 主要有俩种算法:LRU(最久未被使用)和FIFO(先进先出)
缓存这个机制应该怎么去实现?
现在考虑一个问题:现在如果让我们实现MyBatis的缓存,应该怎么进行实现?
一个基本的思路是在Dao层先进行缓存操作再去进行数据库操作。比如查询操作,先去缓存中查看是否有目标数据,如果有直接返回,如果没有再去数据库中进行查询。
上面这种方式有一个不能接收的缺点:冗余。除了增加开发量之外,如果后续对缓存进行更改,冗余会产生一个耦合问题。
再对上面的场景进行分析:我们为MyBatis添加缓存-》实际上是一个提高性能的额外操作-》为原始功能增加额外功能-》代理设计模式
实际上这是一个经典的代理设计模式的使用场景:为原始类增加额外功能。
所以我们基于代理设计模式对上面的过程进行优化:
使用AOP自定义模拟实现Cache
Dao接口:
public interface UserDao {
public void login(User user);
@Cache//使用Cache注解表明执行前先去缓存
public void queryAllUsers();
}
Impl:
public class UserDaoService implements UserDao{
@Override
public void login(User user) {
//这里为了方便就不去调用数据库了
System.out.println("执行login方法");
}
}
自定义一个注解表明这个sql是否去查缓存
/**
* @Author 86153
* @Description 注解。加上注解的方法需要使用Cache,否则不进行使用
* @Date 12:58 2024/6/7
* @Param
* @return
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
}
Aop:
public class MyBatisTest {
@Test
public void MyBatisCache() {
UserDao userDao = new UserDaoService();
UserDao userDaoProxy = (UserDao) Proxy.newProxyInstance(Test.class.getClassLoader(), new Class[]{UserDao.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Cache cache = method.getDeclaredAnnotation(Cache.class);
if(cache != null) {
System.out.println("连接Redis查看是否存在缓存");
return method.invoke(userDao,args);
}
return method.invoke(userDao,args);
}
});
System.out.println("---------------------------------");
userDaoProxy.queryAllUsers();
}
}
结果:
MyBatis实现Cache的方式:
Cache接口:
MyBatis实现了一个Cache接口:
对于Cache接口的实现方式:
实际上上面的实现方式包括俩大类:PerpetualCache为核心实现类,其它类为核心类的装饰器,用来增强核心类PerpetualCache的功能。
PerpetualCache使用HashMap来放置缓存,HashMap的key就是刚才提到的Cache类getId方法生成的唯一标识符。
装饰器可以套娃使用:
PerpetualCache使用HashMap结构来操作缓存。
Cache的工作过程:
MyBatis二层缓存体系:
在我们进行开发时,SqlSession的更换一定是频繁的。因为SqlSession和Connection对象控制着事务,不能被多用户,多请求共享。
所以一级缓存对于我们的开发的作用是有限的。
一级缓存分析(Executor的适配器设计模式):
我们之前提到过Executor是整个MyBatis进行数据库操作的底层实现类。更具体来说,MyBatis在这里使用了适配器模式:
BaseExecutor作为Executor接口的适配器,把Executor中一些基础功能比如一级缓存进行了实现,其它需要根据实际情况进行实现的方法交给它的子类(上图三个Executor核心类),BaseExecutor把这些方法规定为抽象方法,所以BaseExecutor定义为抽象类。
由此BaseExecutor的所有子类都不用再进行一级缓存的实现。
我们来看看BaseExecutor的query查询方法;
上面的localCache就是Cache的核心功能实现类PerpetualCache.
也因为一级缓存依附于Executor,而不同SqlSession使用不同的Executor,所以一级缓存更换SqlSession后就失效。
同一个sql查询语句,在不同的Mapper文件,它们在一级缓存中产生的key不一样所以也是不能利用的,即一级缓存不能跨Mapper。
那么这里AOP设计模式体现在哪里呢?事实上,我们在使用Dao接口实现类进行数据库操作时就是使用了AOP设计模式。MyBatis使用AOP设计模式底层调用了sqlsession的各种数据库操作,而sqlsession底层又是调用Executor:详情:MyBatis源码分析01 ---- 通过代理确定Mapper接口实现类过程-CSDN博客
二级缓存实现(CachingExecutor):
由于一级缓存是一个基础功能,MyBatis默认开启,所以一级缓存在适配器BaseExecutor中实现。而二级缓存是需要自己开启的,不属于基础功能。所以二级缓存单独通过CachingExecutor进行了实现。
具体来说,CachingExecutor作为一个装饰器对SimpleExecutor(负责基础数据库操作的Executor的实现类)针对缓存功能进行了增强。
验证:在之前的博客中我已经阐述了Executor是Configuration创建的,我们在Configuration中找到相关代码
可以看到Configuration在创建Executor时会判断cacheEbabled是否配置为true,进而确定是否对Executor进行缓存功能的增强。
接下来我们通过CachingExecutor的query查询方法由点及面研究一下CachingExecutor是如何增强核心Executor的缓存功能:
从上面的过程中我们也可以验证开启二级缓存四个条件中的三个:包括cache标签,cacheEnabled配置,useCache配置。(第四个条件是存在事务)。详情:MyBatis二级缓存开启条件-CSDN博客
CachingExecutor的update方法:
直接执行了清空缓存操作,避免脏数据
二级缓存Cache的创建时机和创建方法:
我们之前讲解过在创建sqlSessionFactory时MyBatis进行了xml配置文件的解析(OXM),Cache就是在这个过程中进行创建的:
具体构建过程:
总结:在进行创建SqlSessionFactoryBuilder时我们需要创建Confighration对象,进而我们需要解析俩个XML配置文件,在解析Mapper.xml文件时我们通过判断<cache/>标签进而创建二级缓存,创建完成后放入MappedStatement进而封装到Configuration中(所以之前CachingExecutor的query方法是从MappedStatement中拿到Cache的)。
MyBatis是先查二级缓存还是先查一级缓存
如果我们开启了二级缓存,那么Configuratuon在创建Executor时给我们返回的是一个CachingExecutor。
进而我们在来看看CachingExecutor的查询方法:
可以看到MyBatis先去查了二级缓存,二级缓存没查到时才去一级缓存查询。所以二级缓存也叫全局缓存。
MyBatis二级缓存如何针对每个查询sql放置缓存
在MyBatis创建Configuration对象进行xml文件解析时,MyBatis按照<cache/>(Mapper级别)标签创建PerpeturalCache即二级缓存的HashMap,每个 Mapper.xml 对应的命名空间都拥有自己的二级缓存,这意味着不同 Mapper.xml 文件之间的缓存是隔离的。同一个Mapper文件下的sql的缓存放在一个HashMap中,Key值为namespace.id,Value为一个List放置缓存内容。
验证:
我现在在俩个Mapper文件中设置同一个sql语句,分别调用他们的方法使产生相同的sql,最后发现它们将缓存放在不同的HahMap中:
缓存脏数据问题
MyBatis会在执行增删改操作时清空缓存:
在开启二级缓存后,MyBatis会在事务提交时即commit时清空以及和二级缓存:
清空一级缓存:
清空二级缓存:
如果没有开启二级缓存则会在BaseExecutor执行增删改操时清空一级缓存:
<cache-ref namespace=""/>标签作用:
<cache-ref namespace=""/>使不同Mapper文件利用同一个缓存。
经典使用场景:
当我们进行联表操作时,会在当前namespace的缓存里存储俩个实体类的数据。比如下面的sql语句会在UserDaoMapper.xml缓存里存储User和Order俩个实体类的数据,这个时候就会出现问题:如果我在OderDaoMapper.xml里更新了Oder表内容,那么在UserDaoMapper的缓存里就会出现脏数据。
解决方法:使用<cache-ref namespace=""/>是俩个Mapper公用同一个缓存,这样任意一个Mapper进行更新时就会清空缓存,杜绝了脏数据的发生。
但是联表可能出现脏数据直接清除缓存有点得不偿失,另一种做法是重写相关的清除缓存的接口只去清涉及到联表会产生脏数据的缓存。具体方法这里不再叙述。