关于mybatis-plus:mybatisplus如何禁用一级缓存

11次阅读

共计 5574 个字符,预计需要花费 14 分钟才能阅读完成。

前言

用过 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 配置一级缓存的意义,自身就是出于提供性能思考。不过计划要站在业务的视角进行思考,为了确保性能能正确运行,有时候就义一些性能也无伤大雅

正文完
 0