mybatis-plus如何禁用一级缓存的方法

前言

用过mybatis-plus的朋友可能会知道,mybatis-plus提供了多租户插件的功能,这个功能可以让开发人员不用手动写租户语句,由该插件自动帮你加上租户语句。今天的素材来源就是取自业务开发人员使用多租户插件时,遇到的一个神奇的问题

问题重现

业务开发人员要实现根据手机号码更新租户的密码功能,其代码形如下

 for(Tenant t : tenantList){
      ApplicationChainContext.getCurrentContext().put(ApplicationChainContext.TENANT_ID,t.getId()+"");
      Optional<SaasUser> user = this.findByUserPhone(req.getUserPhone());
      user.ifPresent(u -> {
        count.getAndSet(count.get() + 1);
        LambdaUpdateWrapper<SaasUser> wrapper = new LambdaUpdateWrapper<>();
        String md5Pwd = Md5Utils.hash(req.getNewUserPwd());
        wrapper.eq(SaasUser::getId,user.get().getId());
        wrapper.set(SaasUser::getUserPwd,md5Pwd);
        this.update(wrapper);
      });
    }

从代码上看起来没啥问题,因为使用了多租户插件,当我们执行this.findByUserPhone(req.getUserPhone());就会自动带上租户的信息。但在执行的时候发现一个问题,如下图

从图中我们可以发现,当查询不到用户信息时,后续的查询操作,都没有sql语句出现。这说明2点,sql语句要么被系统吃了,要么系统没有执行sql

问题分析

前面说了sql语句没有打印出来,说明sql要么没执行,要么就sql语句被系统吃了。到底是哪种,与其猜测,倒不如去官方找找问题的答案,很遗憾在mybatis-plus官网或者issue上并没找到答案,于是只好跟踪源码进行分析。最后发现mybatis-plus果然如他官网介绍的只做增强不做改变,他最终调用查询的逻辑,走的是原生mybatis的查询逻辑。其查询的核心代码如下

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (this.closed) {
      throw new ExecutorException("Executor was closed.");
    } else {
      if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
        this.clearLocalCache();
      }

      List list;
      try {
        ++this.queryStack;
        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);
        }
      } finally {
        --this.queryStack;
      }

      if (this.queryStack == 0) {
        Iterator var8 = this.deferredLoads.iterator();

        while(var8.hasNext()) {
          BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
          deferredLoad.load();
        }

        this.deferredLoads.clear();
        if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
          this.clearLocalCache();
        }
      }

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

当resultHandler为空 ,list的取值是this.localCache.getObject(key),即会走本地缓存,而不会进行数据库查询

问题破解

从源码可以得知,原生的mybatis默认会走本地缓存,即所谓的一级缓存,而mybatis-plus作为mybatis的增强版,其逻辑和mybatis原生逻辑是一样的。那如何禁用mybatis-plus的一级缓存呢,从源码分析,我们可以得知,当list为空时,则不会走缓存,而会查询数据。而list的缓存取值,来源于
this.localCache.getObject(key)。因此禁用缓存的逆向思维就是要么清空localCache,要么就是变更key,使this.localCache.getObject(key)取到的值为null。因此解决方案至少有以下两种

方案一:清空localCache

那要怎么清空?

通过源码我们可以看到清空localCache的地方有两处,一处是

if (queryStack == 0 && ms.isFlushCacheRequired()) {
   clearLocalCache();
  }

另外一处是

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    // issue #482
    clearLocalCache();
   }

因此我们可以通过变更configuration.getLocalCacheScope()为STATEMENT进行清空。可以通过在yml做如下配置

mybatis-plus:
  configuration:
    local-cache-scope: statement

方案二:变更localcache的key,使this.localCache.getObject(key)取到的值为null

我们先看下key的构成

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (this.closed) {
      throw new ExecutorException("Executor was closed.");
    } else {
      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是由statementId+原生sql+value(查询出来的对象)+ sqlsession.hashcode组成。

因此变更key,我们可以从原生sql入手。看到这边可能有朋友会说,租户id正常不都不一样吗,既然mybatis-plus通过多租户插件,其产生的sql语句不都不一样吗,进而产生的key不就都不一样了吗。如果跟踪源码就会发现其原生的sql是没有加上租户信息的。因此我们可以取巧在查询的sql语句中添加一个随机数,形如下

 public Optional<SaasUser> findByUserPhone(String userPhone) {
    LambdaQueryWrapper<SaasUser> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(SaasUser::getUserPhone,userPhone);
    wrapper.apply("{0} = {0}",UUID.randomUUID().toString());
    SaasUser saasUser = getBaseMapper().selectOne(wrapper);
    return Optional.ofNullable(saasUser);
  }

即在原先的查询代码语句多加

wrapper.apply("{0} = {0}",UUID.randomUUID().toString());

此时sql语句如下

Preparing: SELECT id, user_code, user_name, main_accout_flag, user_pwd, user_phone, admin_flag, user_status, last_login_time, login_ip, pwd_update_time, tenant_id, create_date, created_by, created_by_id, last_updated_by, last_updated_by_id, last_update_date, object_version_number, delete_flag FROM saas_user WHERE delete_flag = 0 AND (user_phone = ? AND ? = ?) AND tenant_id = 424210194470490118
==> Parameters: 111111(String), edcda7fe-ee43-481a-90f7-8b41cb51a3d1(String), edcda7fe-ee43-481a-90f7-8b41cb51a3d1(String)

这样每次产生的sql就会不一样,导致取到不一样key,进而使this.localCache.getObject(key)为空,这样就可以让mybatis每次都进行数据库查询,从而达到禁用一级缓存的目的

总结

方案一的配置是基于全局配置,方案二是基于局部配置。就个人而言,是比较推荐方案二,即通过添加随机值的方式。因为mybatis配置一级缓存的意义,本身就是出于提供性能考虑。不过方案要站在业务的视角进行考虑,为了确保功能能正确运行,有时候牺牲一些性能也无伤大雅

到此这篇关于mybatis-plus如何禁用一级缓存的方法的文章就介绍到这了,更多相关mybatis-plus 禁用一级缓存内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringBoot + Mybatis-plus实战之Mybatis-plus的一级缓存、二级缓存

    前言 现在的JAVA行业,貌似已经是SpringBoot + SpringCloud 的天下了,早期的SSH,SSM框架已经老去,与SpringBoot相结合的JPA框架虽然省去了很多的增删改查sql,但是比较笨拙,在面对一些复杂多变的逻辑时常常力不从心,而相对应的Mybatis由于其高度的灵活性受到广大JAVA攻城狮的欢迎.之前整合过了springboot+mybatis,前几天看到一个面试的问一个问题,Mybatis的一级缓存,二级缓存.我想这个应该也是一个重点吧,所以今天决定来详细解读一下

  • mybatis-plus如何禁用一级缓存的方法

    前言 用过mybatis-plus的朋友可能会知道,mybatis-plus提供了多租户插件的功能,这个功能可以让开发人员不用手动写租户语句,由该插件自动帮你加上租户语句.今天的素材来源就是取自业务开发人员使用多租户插件时,遇到的一个神奇的问题 问题重现 业务开发人员要实现根据手机号码更新租户的密码功能,其代码形如下 for(Tenant t : tenantList){ ApplicationChainContext.getCurrentContext().put(ApplicationCha

  • Asp.net禁用页面缓存的方法总结

    1.在Asp页面首部<head>加入  复制代码 代码如下: Response.Buffer   =   True      Response.ExpiresAbsolute   =   Now()   -   1      Response.Expires   =   0      Response.CacheControl   =   "no-cache"      Response.AddHeader   "Pragma",   "No-

  • mybatis中的一级缓存深入剖析

    mybatis中提供有一级缓存 和 二级缓存,这里记录一下一级缓存 一级缓存(mybatis中默认开启) SqlSession级别的缓存,操作数据库时需要构造SQLSession对象, 在对象中有一个数据结构(HashMap)用于存储缓存数据,不同的SQLSession对象之间的缓存数据是不共享的,即独立的 根据第一点,简单一点讲就是一级缓存是属于对象的(个人记法) 从别处搞来一个图,便于我们理解: 下面用spring整合mybatis来测试一下mybatis的一级缓存: 1.下面是servic

  • MyBatis 的 XML 配置文件和缓存使用步骤

    目录 MyBatis缓存介绍 MyBatis的XML整体介绍 一.整体配置文件介绍 二.Mybatis拦截器[不做要求] 参考案例 三.缓存 使用步骤 MyBatis缓存介绍 正如大多数持久层框架一样,MyBatis 同样提供了一级缓存和二级缓存的支持 1.一级缓存: 基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 Cache 就将清空.2. 二级缓存与一级缓存其机制相同

  • mybatis教程之查询缓存(一级缓存二级缓存和整合ehcache)

    1 缓存的意义 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题. 2 mybatis持久层缓存 mybatis提供一级缓存和二级缓存 mybatis一级缓存是一个SqlSession级别,sqlsession只能访问自己的一级缓存的数据,二级缓存是跨sqlSession,是mapper级别的缓存,对于mapper级别的缓存不同的sqlsession是可以共享的. 3 一级缓存 3.1 原

  • MyBatis 延迟加载、一级缓存、二级缓存(详解)

    使用ORM框架我们更多的是使用其查询功能,那么查询海量数据则又离不开性能,那么这篇中我们就看下mybatis高级应用之延迟加载.一级缓存.二级缓存.使用时需要注意延迟加载必须使用resultMap,resultType不具有延迟加载功能. 一.延迟加载 延迟加载已经是老生常谈的问题,什么最大化利用数据库性能之类之类的,也懒的列举了,总是我一提到延迟加载脑子里就会想起来了Hibernate get和load的区别.OK,废话少说,直接看代码. 先来修改配置项xml. 注意,编写mybatis.xm

  • 深入理解Mybatis一级缓存

    客户端向数据库服务器发送同样的sql查询语句,如果每次都去访问数据库,会导致性能的降低. 那么怎么提高呢? mybatis为我们提供了一级缓存的策略 在一个sqlSession开启和关闭之间,sqlSession对象内部(其实是Executor)会维护一个缓存的对象,当查询数据时候,先从缓存中寻找是否存在该条数据,存在就直接取出来,不存在,向数据库发送sql查询, 然后将查询后的数据存入缓存,和返回给程序. 这样会存在一个问题: 如果在第一次和第二次查询期间,有程序更改了要查讯的数据库的数据,就

  • 解决spring结合mybatis时一级缓存失效的问题

    之前了解到mybatis的一级缓存是默认开启的,作用域是sqlSession,是基 HashMap的本地缓存.不同的SqlSession之间的缓存数据区域互不影响. 当进行select.update.delete操作后并且commit事物到数据库之后,sqlSession中的Cache自动被清空 <setting name="localCacheScope" value="SESSION"/> 结论 spring结合mybatis后,一级缓存作用: 在未

  • Mybatis一级缓存和结合Spring Framework后失效的源码探究

    1.在下面的案例中,执行两次查询控制台只会输出一次 SQL 查询: mybatis-config.xml <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"

  • 关于mybatis的一级缓存和二级缓存的那些事儿

    一.缓存是什么 缓存其实就是存储在内存中的临时数据,这里的数据量会比较小,一般来说,服务器的内存也是有限的,不可能将所有的数据都放到服务器的内存里面,所以, 只会把关键数据放到缓存中,缓存因为速度快,使用方便而出名! 二.为什么需要缓存 BS架构里面,用户的所有操作都是对数据库的增删改查,其中查询的操作是最多的,但如果用户想要某个数据时每次都去数据库查询,这无疑会增加数据库的压力,而且获取时间效率也会降低,所以为了解决这些问题,缓存应用而生,使用了缓存之后,服务器只需要查询一次数据库,然后将数据

随机推荐