欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

大聪明教你学Java|Mybatis的一级缓存和二级缓存--????作者简介:不愿过河东,一个来自二线城市的程序员,致力于用 "猥琐 "的方法解决琐碎的问题,让复杂的问题变得简单易懂。支持作者:喜欢????,关注????,留言????~! 前言。 在计算机世界中,缓存无处不在;操作系统有操作系统缓存,数据库会有数据库缓存,我们还可以利用中间件(如 Redis)来充当缓存。MyBatis 作为一个优秀的 ORM 框架,也用于缓存,所以今天我们就来谈谈 Mybatis 的一级缓存和二级缓存。 Mybatis 一级缓存 首先,我们来看一张图片????。 我们在开发项目的过程中,如果打开Mybatis的SQL语句打印,经常会看到这样一句话:创建一个新的 SqlSession,其实这就是我们常说的 Mybatis 一级缓存。 Mybatis 的一级缓存也就是在执行一次 SQL 查询或 SQL 更新后,这条 SQL 语句并不会消失,而是被 MyBatis 缓存起来,当再次执行同样的 SQL 语句时,就会

最编程 2024-07-05 22:50:02
...
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 接口集中管理缓存,避免出现脏读的情况,但是有一定的开发成本,并且在多表查询时,使用不当极有可能会出现脏数据~

小结

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

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

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

推荐阅读