您现在的位置是:网站首页 / 大后端大后端

mybatis缓存,看这篇文章就够了,我小婊弟看了都说好

李先生2021-07-25 566人围观

简介 mybatis是一款非常流行的ORM框架,并且提供了一二级缓存。很多小伙伴都能说出一两点,但是很少有从根源上去分析过一二级缓存的原理一级一些隐藏的逻辑,本篇文章深入的讲解了一二级缓存的原理,并演示验证了你所了解的东西,看了准没错

mybatis一二级缓存,看这篇文章就够了,我小婊弟看了都说好

又是一个绝望周末,我正起劲的在B站学快乐舞呢。。

学跳舞学的嗨呢
学跳舞学的嗨呢

突然小婊弟走进来,哭哭啼啼的说我面试又挂了,那秃头面试官问我mybatis的缓存,我我一时竟然没答上来就就让我回家等通知了。

既然小婊弟面试遇到了,那今天就聊聊mybatis的一二级缓存吧。

先从概念开始,mybatis的缓存其实分为一级和二级缓存。先来看看mybatis官网是怎么解释这个一二级缓存的

官网地址 https://mybatis.org/mybatis-3/zh/sqlmap-xml.html#cache

Mybatis一级缓存

一级缓存概念

MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。 为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。

默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存

从官网文档可以看到对这个一级缓存的介绍很是简洁,二级缓存薛微有点复杂,二级缓存咱们后面再细谈。这里mybatis官网特别指明了三个点,敲黑板刻画重点了哈。

一级缓存特点

1.一级缓存是默认开启的
2.一级缓存仅仅对一个会话中的数据做缓存
3.这点很多人可能不会注意,但就是不多人注意的如果你提到了我觉得面试官可能眼睛一亮。一级缓存是本地缓存,本地缓存就会涉及到缓存不一致的情况。至于缓存一致性如何解决咱们放下一篇文章细谈哈

废话不多说,上菜。效果大不大试试就知道。

这个时候小婊弟突然问了一句这个一个会话是不是一次接口请求呀?我上去就是一巴掌

小婊弟挨打
小婊弟挨打

然后安慰下小婊弟,给他解释到

mybatis的会话是指,mybatis接到的一个或多个执行sql请求的这个过程叫做一次会话。

小婊弟嘤嘤嘤的擦着眼泪说知道了。

一级缓存验证

下面反手撸码,简单的给小婊弟演示下,看下官网说的一级缓存是会话中的数据缓存

源码在UserServiceImpl中查看

SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
log.info("表哥第一次查询");
List<User> userList1 = userMapper.getAllUser();
log.info("表哥第二次查询");
List<User> userList2 = userMapper.getAllUser();
return userList1;

在同一个会话中查询两次
在同一个会话中查询两次

从上图可以看到第二次查询其实并没有打印sql,说明第二次查询是通过缓存得到数据的。

但是小婊弟说我们平常做项目时不会自己去获取sqlsession会话啊,是spring通过XxMapper接口来调用的呀,能不能用常用方式验证下呢?说试就试。源码在UserServiceImpl中查看

log.info("表哥第一次查询");
List<User> userList1 = userMapper.getAllUser();
log.info("表哥第二次查询");
List<User> userList2 = userMapper.getAllUser();

演示翻车
演示翻车

小婊弟噗呲,他强忍了没笑声来,我觉得他忍住了~

小婊弟笑我
小婊弟笑我

咋回事呢, 表哥翻车了?别急,首先我们要明确一点,在spring中mybatis的sqlSession是SqlSessionFactory的openSession方法得到的,这个是能在DefaultSqlSessionFactory中看到的两种途径来获取SqlSession,如下图

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
                                             boolean autoCommit) {
    Transaction tx = null;

    DefaultSqlSession var8;
    // 省略部分逻辑代码
    Environment environment = this.configuration.getEnvironment();
    TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    Executor executor = this.configuration.newExecutor(tx, execType);
    var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
    return var8;
}

private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    DefaultSqlSession var8;
    boolean autoCommit;
    try {
      autoCommit = connection.getAutoCommit();
    } catch (SQLException var13) {
      autoCommit = true;
    }
    // 省略部分源码逻辑
    Environment environment = this.configuration.getEnvironment();
    TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
    Transaction tx = transactionFactory.newTransaction(connection);
    Executor executor = this.configuration.newExecutor(tx, execType);
    var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);

    return var8;
}

所以反过来思考,上面这两个方法是获取SqlSession的地方,但是刚翻车演示时打印了两条sql。表哥确定官网说的一级缓存是会话级缓存,所以肯定是创建了两个SqlSession。这时我表面沉静内心其实慌的一撇,翻着mybatis源码看到SqlSessionUtils类时突然眼前一亮。

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, 
                                       PersistenceExceptionTranslator exceptionTranslator) {
    Assert.notNull(sessionFactory, "No SqlSessionFactory specified");
    Assert.notNull(executorType, "No ExecutorType specified");
    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 = sessionFactory.openSession(executorType);
        registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
        return session;
    }
}


private static SqlSession sessionHolder(ExecutorType executorType, SqlSessionHolder holder) {
        SqlSession session = null;
        if (holder != null && holder.isSynchronizedWithTransaction()) {
            if (holder.getExecutorType() != executorType) {
                throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there 
                                                               is an existing transaction");
            }

            holder.requested();
            LOGGER.debug(() -> {
                return "Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction";
            });
            session = holder.getSqlSession();
        }

        return session;
    }

小婊弟,快来表哥给你看个宝贝,是这个地方mybatis做了特殊处理。mybatis在获取sqlsession的时候做了下判断,也就是开启spring事务的时候mybatis会在事务管理器中把sqlSession缓存起来了。所以我们演示个开启事务例子来看看结果对不对

演示结果
演示结果

这时我嘴角疯狂上扬的对小婊弟说,刚刚呢只是我给你卖关子了不并不是翻车啦。如果你关注表哥微信公众号:Java极客帮,我以后就不卖关子了。

经过上面一番狡辩,我们可以得出一个小结论

开启事务的时候,同一个事务中的一个或多个sql执行是在同一个会话中的,一个会话是有一个或者多个事务的。没有开启事务的时候一个sql执行是在一会会话中的。

一个会话有多个事务不是本文讲的范畴,关V注我下次再讲啦

一级缓存源码

这时小婊弟说,表哥哥~,能看看mybatis实现一级缓存的源码吗?好想再给他一巴掌哟,但是他刚刚又关注了我

给婊弟安排上
给婊弟安排上

BaseExecutor类中可以看到这段代码,先从localCache获取缓存。

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
                         CacheKey key, BoundSql boundSql) throws SQLException {

              // 省略部分逻辑

        List list;
        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);
        }
                // 省略部分逻辑

        return list;
    }
}

继续扒拉这个缓存,最终在PerpetualCache看到所谓的mybatis一级缓存,其实是使用了一个HashMap,印证了之前说的mybatis一级缓存是本地缓存,在分布式场景可能遇到数据不一致的情况,这个不在本文讨论范畴。小傻瓜持续关注我,我会给你交代的。

public class PerpetualCache implements Cache {
    private final String id;
    private final Map<Object, Object> cache = new HashMap();

    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }

    public Object getObject(Object key) {
        return this.cache.get(key);
    }

}

既然是缓存那就存在更新嘛,因为我执行了增删改数据都变了缓存会不会变呢?必须的,来上菜!

BaseExecutor

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);
    }
}

到这里有没有小可爱会问,我分页查询,第一页和第二页会不会是缓存呢?先看源码再给答案

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
                               BoundSql boundSql) {

        CacheKey cacheKey = new CacheKey();
        cacheKey.update(ms.getId());
        cacheKey.update(rowBounds.getOffset());
        cacheKey.update(rowBounds.getLimit());
        cacheKey.update(boundSql.getSql());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
        Iterator var8 = parameterMappings.iterator();

        while(var8.hasNext()) {
            ParameterMapping parameterMapping = (ParameterMapping)var8.next();
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                String propertyName = parameterMapping.getProperty();
                Object value;
                if (boundSql.hasAdditionalParameter(propertyName)) {
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) {
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    value = parameterObject;
                } else {
                    MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }

                cacheKey.update(value);
            }
        }

        if (this.configuration.getEnvironment() != null) {
            cacheKey.update(this.configuration.getEnvironment().getId());
        }

        return cacheKey;

}

答案是不会的啦,为啥呢?我给你看这个key是如何组成的,这个key是经过很多元素比如statement的ID,页数和第几页,sql语句,参数,包括mybatis环境等元素组装成的key所以放心,不会重复的啦。

好了,现在以及缓存搞懂了吗?小婊弟,做个了结吧

一级缓存小结

1.mybatis一级缓存是默认开启的,默认作用范围是SqlSession,其实是可以调整为更新的STATEMENT,查询完后就清空,等于关闭一级缓存
2.缓存作用范围是sqlSession会话内,所以一级缓存生命周期也是sqlSession。
3.mybatis的一级缓存结构是一个HashMap作为存储的,当增删改、设置localCacheScope=STATEMENT、会清除异己缓存

Mybatis二级缓存

小小婊弟,一级缓存简单过了一下,该二级缓存了吧。不然下次去面试又会被面试官按地上摩擦哟。

二级缓存概念

老规矩,先看mybatis咋说的。

要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:<cache/>

基本上就是这样。这个简单语句的效果如下:

1.映射语句文件中的所有 select 语句的结果将会被缓存。
2.映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
3.缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
4.缓存不会定时进行刷新(也就是说,没有刷新间隔)
5.缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
6.缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

提示 缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。

从官网这段文字大致理解下了要开启二级缓存是很简单的加上Cache配置就行了,从官网描述可以小小梳理下二级缓存的特点

1.二级缓存默认是关闭的,需要加<cache/>配置后才能启动默认二级缓存,当然也可以定制二级缓存的
2.能看出来是针对映射文件,那就是二级缓存作用范围是namespace
3.从官网介绍得出默认二级缓存使用LRU算法来清除不需要的缓存,当然也可以配置其他清除算法,比如FIFO、弱引用、软引用

小婊弟突然虎躯一震,表哥啊,你这一会弱的软的嘛意思呀? 我撇了一眼,关注我:Java极客帮,下次专门给你开小灶讲,别老打断我。

二级缓存验证

先按照官网默认的<cache/>演示

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.ziyoushu.repository.dao.UserMapper">
    <cache />
    <resultMap id="BaseResultMap" type="net.ziyoushu.domain.User">
        <id column="u_id" jdbcType="BIGINT" javaType="Long" property="uId" />
        <result column="user_name" jdbcType="VARCHAR" javaType="String" property="userName" />
        <result column="we_chat" jdbcType="VARCHAR" javaType="String" property="weChat" />
    </resultMap>
    <select id="getAllUser" resultMap="BaseResultMap">
        select * from user_demo;
    </select>
    <update id="update">
        update user_demo set we_chat = #{weChat} where u_id = #{uId}
    </update>
</mapper>

自定义缓存结果
自定义缓存结果

可以看到缓存是生效的,第二个sql结果是命中了二级缓存的。刚刚提到默认是LRU清理,那如何验证呢?

在初始化mybatis解析文件的时候 XMLMapperBuilder中有个方法

// 解析映射文件的Cache标签
private void cacheElement(XNode context) {
    if (context != null) {
        // 解析适配器
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = this.typeAliasRegistry.resolveAlias(type);
        // 清除策略,默认是LRU
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = this.typeAliasRegistry.resolveAlias(eviction);
        // 刷新频率
        Long flushInterval = context.getLongAttribute("flushInterval");
        // 缓存的大小
        Integer size = context.getIntAttribute("size");
        // 如果设置只读的话返回的对象是同一个,如果是false的话,返回的是序列化后的对象,所以对象必须要序列化
        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);
    }
}

public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, 
                         Long flushInterval, Integer size, boolean readWrite, 
                         boolean blocking, Properties props) {
        // 构建cache类
        Cache cache = (new CacheBuilder(this.currentNamespace)).
          implementation((Class)this.valueOrDefault(typeClass, PerpetualCache.class)).
          addDecorator((Class)this.valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).
          size(size).readWrite(readWrite).blocking(blocking).properties(props).build();
        this.configuration.addCache(cache);
        this.currentCache = cache;
        return cache;
    }

这些属性我这就不单独一一展示了在下面的例子中我附带添加可以吗?小傻瓜们。看到上面的type属性没,可以定制适配器耶。没错,下面就来自定义一个适配器。

​ 但是在搞之前不知道有没有小可爱还记得我在讲一级缓存的时候贴出了PerpetualCache的源码,是实现了Cache接口的。

我给大家打个初始化二级缓存的断点图,大家应该就豁然开朗了

截屏2021-07-25 上午12.50.44
截屏2021-07-25 上午12.50.44

默认的二级缓存其实和一级缓存实现方式大致一样,虽然默认是LruCache最终也是PerpetualCache,因为Mybatis的缓存机制也是采用委托的机制最终数据由PerpetualCache存储。我自嗨了半天,看这小婊弟说说不打断我就不打断我了,允许你提一个问题。

他说既然二级缓存默认是LruCache,那为啥你说数据还是存在PerpetualCache中呢?

是这样的,LruCache里面实现的是最近最少使用的实现逻辑,来看看他的实现逻辑

public class LruCache implements Cache {
    private final Cache delegate;
    private Map<Object, Object> keyMap;
    private Object eldestKey;

      // 省略一些逻辑

    // 使用LinkedHashMap构建一个LRU
    public void setSize(final int size) {
        this.keyMap = new LinkedHashMap<Object, Object>(size, 0.75F, true) {
            private static final long serialVersionUID = 4267176411845948333L;

            protected boolean removeEldestEntry(Entry<Object, Object> eldest) {
                // 超过1024个元素的时候就开始设定即将移除最少使用的缓存
                boolean tooBig = this.size() > size;
                if (tooBig) {
                    LruCache.this.eldestKey = eldest.getKey();
                }

                return tooBig;
            }
        };
    }

    // 在存入新值的时候检测有没有被
    public void putObject(Object key, Object value) {
        this.delegate.putObject(key, value);
        this.cycleKeyList(key);
    }

    // 访问缓存的时候通过LinkedHashMap 刷新访问,达到排序的效果
    public Object getObject(Object key) {
        this.keyMap.get(key);
        return this.delegate.getObject(key);
    }

    // 在写入缓存的时候判断有没有超过默认1024的缓存,如果有就删除指定缓存
    private void cycleKeyList(Object key) {
        this.keyMap.put(key, key);
        if (this.eldestKey != null) {
            this.delegate.removeObject(this.eldestKey);
            this.eldestKey = null;
        }
    }

   // 省略一些逻辑
}

小婊弟,这回懂了吗?小婊弟信心上脑的点点头。屏幕前的小可爱懂了吗?不懂的话请看我后续文章哟

自定义二级缓存

表哥这里就玩个有意思的,既然mybatis把Cache接口开放给咱们,而且在默认的缓存实现PerpetualCache中看到mybatis定义的缓存是HashMap结构是Key-Value结构,那就我这里就采用同样是Key-Value的Redis来作为缓存的存储介质

@Override
public List<User> getAll2() {
    log.info("表哥第一次查询");
    List<User> userList1 = userMapper.getAllUser();
    log.info("表哥第二次查询");
    List<User> userList2 = userMapper.getAllUser();
    return userList1;
}
@NoArgsConstructor
@Log4j
public class CustomerCache implements Cache {

    static RedisTemplate redisTemplate;
    private List<Object> objectList = new ArrayList<>();

    private String id;

    public CustomerCache(String id) {
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public void putObject(Object o, Object o1) {
        getRedisTemplate().opsForValue().set(o, o1);
        log.info("设置缓存:{}"+o.toString());
        objectList.add(o);
    }

    @Override
    public Object getObject(Object o) {
        log.info("查询缓存:{}"+o.toString());
        return getRedisTemplate().opsForValue().get(o);
    }

    @Override
    public Object removeObject(Object o) {
        log.info("删除缓存:{}"+o.toString());
        return getRedisTemplate().delete(o);
    }

    @Override
    public void clear() {
        for (Object o : objectList) {
            this.removeObject(o);
        }
    }

    @Override
    public int getSize() {
        return objectList.size();
    }

    public RedisTemplate getRedisTemplate() {
        if(null == redisTemplate) {
            redisTemplate = (RedisTemplate) SpringContextUtils.getBean("redisTemplate", RedisTemplate.class);
        }

        return redisTemplate;
    }
}

自定义缓存结果
自定义缓存结果

从redis工具上看也是存储进去了的

Redis中看结果
Redis中看结果

二级缓存源码

老规格,看完表象看本质。二级缓存实现代码就在CachingExecutor类的query方法里了。这里的Cache会委托给我们定制的CustomerCache最终来实现二级缓存的存储与删除

Cache实现源码
Cache实现源码

说到删除,那二级缓存的清除是如何触发呢?

  1. MappedStatement上标记flushCache=true的sql执行语句
  2. 执行增删改语句,即数据发生变化
  3. 自定义清除或者clearInterval设定了刷新周期,如自定义缓存可以设置redis过期时间
  4. 事务回滚

二级缓存小结

1.mybatis二级缓存是默认关闭的,,不同的namespace操作互不影响,所以出现连表操作的时候容易出现脏数据 ,请谨慎使用
2.缓存作用范围是namespace内,所以二级缓存生命周期也是namespace。
3.mybatis的二级缓存结构是一个HashMap作为存储的,可以实现Cache来自定义

好了,本文从就稍微简单的把mybatis的一二级缓存梳理了下.

本文演示所有代码地址:https://gitee.com/ziyoushu/zys-study-demo.git

如果觉得对你有帮助可以点个关注下次不迷路,后续给大家出更多实用文章。

  • 4 点赞
  • 0 收藏
  • 分享

作品评论