聊聊MyBatis缓存机制(一)

前言

Mybatis是常见的Java数据库访问层框架,虽然我们在日常的开发中一般都是使用Mybatis Plus,但是从官网信息可以知道,其实Mybatis Plus只是让开发者在使用上更简单,并没有改动核心原理。在日常工作中,大多数开发者都是使用的默认缓存配置,但是Mybatis缓存机制有一些不足之处,在使用过程中容易引起脏数据,存在一些潜在的隐患。带着个人的兴趣,希望从应用及源码的角度为读者梳理MyBatis缓存机制。

一级缓存

一级缓存介绍

在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。
在这里插入图片描述
每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。具体实现类的类关系图如下图所示。
在这里插入图片描述

一级缓存配置

一级缓存的配置值有两个选项,SESSION或者STATEMENT,默认是SESSION级别,Configuration类中可以进行设置
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty(“localCacheScope”, “SESSION”)));

public enum LocalCacheScope {
    
    
  SESSION,STATEMENT
}

一级缓存的使用

一级缓存说的是如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
我准备了一个SkyworthUser的实体类,定义UserInfoMapper接口继承BaseMapper,这样无需编写mapper.xml文件,即可获取CRUD功能。
下面是Service实现类的代码

@Service
public class UserServiceImpl implements UserService {
    
    
    @Autowired
    private UserInfoMapper userInfoMapper;

    public SkyworthUser getUserInfoById(String id){
    
    
        return userInfoMapper.selectById(id);
    }
 }

根据理解我这个方法重复查询两次,那必须是会使用所谓的一级缓存的,所以进行尝试调用

 AnnotationConfigApplicationContext annotationConfigApplicationContext =
            new AnnotationConfigApplicationContext(StartConfig.class);
        UserService bean = annotationConfigApplicationContext.getBean(UserService.class);
        SkyworthUser userInfoById = bean.getUserInfoById("0381321c-089b-43ef-b5d5-e4556c5671e9");
        SkyworthUser userInfoById2 = bean.getUserInfoById("0381321c-089b-43ef-b5d5-e4556c5671e9");
        System.out.println(userInfoById.toString());
        System.out.println(userInfoById2.toString());

为了证明我的猜想,我在userInfoById2这一行打个断点,执行完第一个之后,我手动去修改一下数据库的某个属性值,看下userInfoById2输出是不是和userInfoById一致。

userInfoById:输出内容如下
10:40:40.494 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@55a609dd]
SkyworthUser(id=0381321c-089b-43ef-b5d5-e4556c5670e9, account=222559180720275487, password={bcrypt}$2a 10 10 10BTO05duVM6mcc6encPsMeuRfpzvqNs/eTtdbD1gnuuzq6GgZdXjP., name=于琦海, department=362295371, position=, avatarUrl=https://static-legacy.dingtalk.com/media/lADPDgQ9qnh-UtfNAtDNAtA_720_720.jpg, email=null, phone=18628218225, remark=null, unionid=jKvvlRFAg7BDZZdWyciPZcAiEiE, sex=null, lastLoginTime=null, status=1, createTime=Mon Jul 19 10:01:17 CST 2021, updateTime=null, isFirstLogin=0)

修改数据库的isFirstLogin=1

userInfoById2:输出内容如下
SkyworthUser(id=0381321c-089b-43ef-b5d5-e4556c5670e9, account=222559180720275487, password={bcrypt}$2a 10 10 10BTO05duVM6mcc6encPsMeuRfpzvqNs/eTtdbD1gnuuzq6GgZdXjP., name=于琦海, department=362295371, position=, avatarUrl=https://static-legacy.dingtalk.com/media/lADPDgQ9qnh-UtfNAtDNAtA_720_720.jpg, email=null, phone=18628218225, remark=null, unionid=jKvvlRFAg7BDZZdWyciPZcAiEiE, sex=null, lastLoginTime=null, status=1, createTime=Mon Jul 19 10:01:17 CST 2021, updateTime=null, isFirstLogin=1)

怎么回事?说好的一级缓存默认开启呢?明明什么配置都没用修改,直接调用就这样了?
带着问题开始源码的查看,找到具体的原因。

一级缓存源码分析

分析源码从两个地方出发,首先是@MapperScan注解,另外一个是MybatisPlusAutoConfiguration.class类。博主下载的是mybatis plus的原代码,所以会有MybatisPlusAutoConfiguration这个类。
首先分析@MapperScan注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
    
    

注解中@Import注解引入了MapperScannerRegistrar.class,MapperScannerRegistrar 实现了ImportBeanDefinitionRegistrar接口,ImportBeanDefinitionRegistrar在运行时动态地注册 BeanDefinition,查看源码后,可以看出其实就是为了注册MapperScannerConfigurer类到容器中,为了方便查看,只截出部分原代码,如下所示:

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    
    
@Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    
    
    AnnotationAttributes mapperScanAttrs = AnnotationAttributes
        .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    if (mapperScanAttrs != null) {
    
    
      registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
          generateBaseBeanName(importingClassMetadata, 0));
    }
  }

  void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
      BeanDefinitionRegistry registry, String beanName) {
    
    

    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
    builder.addPropertyValue("processPropertyPlaceHolders", true);
	............
}

此时spring需要初始化MapperScannerConfigurer这个类,MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor,InitializingBean等接口,InitializingBean在初始化完成后会执行afterPropertiesSet()方法,所以一般需要关注下这个接口,但是MapperScannerConfigurer好像没有做什么处理。

接下来就是BeanDefinitionRegistryPostProcessor接口,它是Spring中的一个扩展点,用于BeanDefinition加载到Spring容器前或之后对其进行修改或者添加,BeanDefinitionRegistryPostProcessor 执行的时机是在BeanDefinition加载和解析完成之后,Bean实例化和依赖注入之前,也就是在BeanDefinition的预处理阶段。

在 BeanDefinition 的预处理阶段,Spring 容器会调用所有实现了 BeanDefinitionRegistryPostProcessor 接口的类的 postProcessBeanDefinitionRegistry() 方法和 postProcessBeanFactory() 方法,用于对 BeanDefinition 进行修改或添加。其中,postProcessBeanDefinitionRegistry() 方法用于在 BeanDefinition 加载到 Spring 容器之前对其进行修改或添加,而 postProcessBeanFactory() 方法用于在 BeanDefinition 加载到 Spring 容器之后对其进行修改或添加。

postProcessBeanFactory方法是个空方法,所以不管他,只关注postProcessBeanDefinitionRegistry方法,如下所示:

 @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    
    
    if (this.processPropertyPlaceHolders) {
    
    
      processPropertyPlaceHolders();
    }

    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setMarkerInterface(this.markerInterface);
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
    scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
    if (StringUtils.hasText(lazyInitialization)) {
    
    
      scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
    }
    if (StringUtils.hasText(defaultScope)) {
    
    
      scanner.setDefaultScope(defaultScope);
    }
    scanner.registerFilters();
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

上面的代码就是创建了ClassPathMapperScanner类,执行了scan方法,从字面意思理解就是扫描,估计就是Mapper接口的扫描了。
实际上执行是ClassPathBeanDefinitionScanner类中的scan方法。

public int scan(String... basePackages) {
    
    
		int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

		doScan(basePackages);

scan调用了本类中的doScan方法,但是这个doScan被子类ClassPathMapperScanner重写了,所以直接看这个方法,代码如下:

@Override
  public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    
    
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
    
    
      LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
          + "' package. Please check your configuration.");
    } else {
    
    
      processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
  }

其实就是调用父类的doScan方法,但是自己在后续做了一些处理,super.doScan(basePackages)获取的是一个BeanDefinitionHolder的集合,BeanDefinitionHolder包含了beanDefinition和beanName,如果集合不为空,那么processBeanDefinitions(beanDefinitions)应该就是将接口按照一定规则生成代理对象,这样就能把接口初始化,猜测大概思想和FactoryBean差不多。

private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class;

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    
    
 definition.setBeanClass(this.mapperFactoryBeanClass);

processBeanDefinitions(beanDefinitions);的简化代码如上所示,很明显,我们的UserInfoMapper接口在初始化的时候,会用到MapperFactoryBean来生成代理对象,完美的预测成功,因为MapperFactoryBean实现了FactoryBean接口,在获取的时候会调用getObject方法来生成代理对象。

MapperFactoryBean extends SqlSessionDaoSupport implements FactoryBean,我们了解FactoryBean,但是继承了SqlSessionDaoSupport,这个类需要查看下具体是做什么的,查看发现SqlSessionDaoSupport 是个抽象类,继承了DaoSupport,DaoSupport肯定也是一个抽象类,但是它实现了InitializingBean。

所以我们查看下DaoSupport的afterPropertiesSet()方法,发现有个checkDaoConfig方法和initDao方法,主要看这个checkDaoConfig方法,因为initDao是个空的方法并且没有被重写过;通过类的继承关系我们可以发现MapperFactoryBean是重写了checkDaoConfig方法的,所以直接查看MapperFactoryBean的checkDaoConfig方法

@Override
  protected void checkDaoConfig() {
    
    
    super.checkDaoConfig();

    notNull(this.mapperInterface, "Property 'mapperInterface' is required");

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
    
    
      try {
    
    
        configuration.addMapper(this.mapperInterface);
      } catch (Exception e) {
    
    
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
        throw new IllegalArgumentException(e);
      } finally {
    
    
        ErrorContext.instance().reset();
      }
    }
  }

这段代码有点抽象,因为我们不知道configuration.addMapper(this.mapperInterface);具体是干嘛的,并且Configuration这个类是什么作用,类里面什么注释都没有,吐槽下阿里的大佬们…不理解就先跳过,我们只知道把这个UserInfoMapper接口放到这个Configuration类,直接点进去查看addMapper方法做了什么事情

点进去就到了Configuration中的addMapper方法,方法内是 mapperRegistry.addMapper(type)方法,继续往下走,就到了MapperRegistry的addMapper方法,这个方法被MybatisMapperRegistry重写了,那么就查看里面的逻辑,如下图所示:

@Override
    public <T> void addMapper(Class<T> type) {
    
    
        if (type.isInterface()) {
    
    
            if (hasMapper(type)) {
    
    
                // TODO 如果之前注入 直接返回
                return;
                // TODO 这里就不抛异常了
//                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
    
    
                // TODO 这里也换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
                knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
                // It's important that the type is added before the parser is run
                // otherwise the binding may automatically be attempted by the
                // mapper parser. If the type is already known, it won't try.
                // TODO 这里也换成 MybatisMapperAnnotationBuilder 而不是 MapperAnnotationBuilder
                MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
    
    
                if (!loadCompleted) {
    
    
                    knownMappers.remove(type);
                }
            }
        }
    }

当看到第一个判断逻辑的时候,我几乎确定了我之前的流程是没问题的,所以加油往下,parser.parse()方法很特别,感觉是解析什么东西,有没有可能是UserInfoMapper命名空间的xml解析,带着猜测继续往下走,parse进入MybatisMapperAnnotationBuilder 的parse方法,代码如下:

public void parse() {
    
    
        String resource = type.toString();
        // 避免重复加载
        if (!configuration.isResourceLoaded(resource)) {
    
    
            // 如果没有加载过xml文件,就重新加载,此处一般是加载好了,具体的加载地方在sqlSessionFactoryBean,
            // 感兴趣的可以先自己看看,后续如果有时间可能把详细讲下mybatis的运行流程。
            loadXmlResource();
            // 避免重复加载
            configuration.addLoadedResource(resource);
            String mapperName = type.getName();
            // 设置命名空间
            assistant.setCurrentNamespace(mapperName);
            // 解析二级缓存
            parseCache();
            parseCacheRef();
            InterceptorIgnoreHelper.InterceptorIgnoreCache cache = InterceptorIgnoreHelper.initSqlParserInfoCache(type);
            for (Method method : type.getMethods()) {
    
    
                if (!canHaveStatement(method)) {
    
    
                    continue;
                }
                if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
                    && method.getAnnotation(ResultMap.class) == null) {
    
    
                    parseResultMap(method);
                }
                try {
    
    
                    // TODO 加入 注解过滤缓存
                    InterceptorIgnoreHelper.initSqlParserInfoCache(cache, mapperName, method);
                    // 解析方法上的注解方法
                    parseStatement(method);
                } catch (IncompleteElementException e) {
    
    
                    // TODO 使用 MybatisMethodResolver 而不是 MethodResolver
                    configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
                }
            }
            // TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql
            try {
    
    
                // https://github.com/baomidou/mybatis-plus/issues/3038
                if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
    
    
                    parserInjector();
                }
            } catch (IncompleteElementException e) {
    
    
                configuration.addIncompleteMethod(new InjectorResolver(this));
            }
        }
        parsePendingMethods();
    }

到这里我们大概知道了我们的UserInfoMapper接口会生成MapperFactoryBean类,在初始化完这个类之后会对这个类进行特定的处理,把信息放到Configuration这个类里面去,方便后续直接使用,设置了命名空间、缓存信息等。

但是我们要创建UserInfoMapper类的代理对象的时候会执行MapperFactoryBean.getObject方法。最后的逻辑会进入到MybatisMapperRegistry的getMapper方法,代码如下:

@Override
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    
    
        // TODO 这里换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
        // fix https://github.com/baomidou/mybatis-plus/issues/4247
        MybatisMapperProxyFactory<T> mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
    
    
            mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.entrySet().stream()
                .filter(t -> t.getKey().getName().equals(type.getName())).findFirst().map(Map.Entry::getValue)
                .orElseThrow(() -> new BindingException("Type " + type + " is not known to the MybatisPlusMapperRegistry."));
        }
        try {
    
    
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
    
    
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }

在这个代码中newInstance就是生成代理对象,核心代码如下:

public T newInstance(SqlSession sqlSession) {
    
    
        final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
protected T newInstance(MybatisMapperProxy<T> mapperProxy) {
    
    
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{
    
    mapperInterface}, mapperProxy);
    }

由此可以判断,如果我们执行userinfoMapper的方法时,会进入到MybatisMapperProxy类的invoke方法。到此我们就可以开始分析为什么我们一级缓存没有生效的原因。

问题排查

回忆一下,我们这样调用了两次,在第二次执行前修改数据库字段值,发现没有走缓存,两次查询不一致。因为博主使用的mybatis Plus+spring的源码,没有用Springboot,所以这样调用一下,在service里调用两次也是一样的效果,测试过。

 AnnotationConfigApplicationContext annotationConfigApplicationContext =
            new AnnotationConfigApplicationContext(StartConfig.class);
        UserService bean = annotationConfigApplicationContext.getBean(UserService.class);
        SkyworthUser userInfoById = bean.getUserInfoById("0381321c-089b-43ef-b5d5-e4556c5671e9");
        SkyworthUser userInfoById2 = bean.getUserInfoById("0381321c-089b-43ef-b5d5-e4556c5671e9");
        System.out.println(userInfoById.toString());
        System.out.println(userInfoById2.toString());

1、bean.getUserInfoById方法最终进入到MybatisMapperProxy的invoke方法中

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
        try {
    
    
            if (Object.class.equals(method.getDeclaringClass())) {
    
    
                return method.invoke(this, args);
            } else {
    
    
                // cachedInvoker会组装PlainMethodInvoker或者DefaultMethodInvoker
                return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
            }
        } catch (Throwable t) {
    
    
            throw ExceptionUtil.unwrapThrowable(t);
        }
    }

2、cachedInvoker会组成PlainMethodInvoker,所以cachedInvoker(method).invoke()方法会进入到PlainMethodInvoker的invoke方法

 @Override
        public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    
    
            return mapperMethod.execute(sqlSession, args);
        }

3、执行execute方法进入MybatisMapperMethod的execute方法,我们的getUserInfoById是使用的selectOne

 public Object execute(SqlSession sqlSession, Object[] args) {
    
    
        Object result;
        switch (command.getType()) {
    
    
            case INSERT: {
    
    
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.insert(command.getName(), param));
                break;
            }
            case UPDATE: {
    
    
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.update(command.getName(), param));
                break;
            }
            case DELETE: {
    
    
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.delete(command.getName(), param));
                break;
            }
            case SELECT:
                if (method.returnsVoid() && method.hasResultHandler()) {
    
    
                    executeWithResultHandler(sqlSession, args);
                    result = null;
                } else if (method.returnsMany()) {
    
    
                    result = executeForMany(sqlSession, args);
                } else if (method.returnsMap()) {
    
    
                    result = executeForMap(sqlSession, args);
                } else if (method.returnsCursor()) {
    
    
                    result = executeForCursor(sqlSession, args);
                } else {
    
    
                    // TODO 这里下面改了
                    if (IPage.class.isAssignableFrom(method.getReturnType())) {
    
    
                        result = executeForIPage(sqlSession, args);
                        // TODO 这里上面改了
                    } else {
    
    
                        Object param = method.convertArgsToSqlCommandParam(args);
                        result = sqlSession.selectOne(command.getName(), param);
                        if (method.returnsOptional()
                            && (result == null || !method.getReturnType().equals(result.getClass()))) {
    
    
                            result = Optional.ofNullable(result);
                        }
                    }
                }
                break;
            case FLUSH:
                result = sqlSession.flushStatements();
                break;
            default:
                throw new BindingException("Unknown execution method for: " + command.getName());
        }
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    
    
            throw new BindingException("Mapper method '" + command.getName()
                + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
    }

4、可以断点一路跟下来,会到DefaultSqlSession的selectList方法

 private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    
    
    try {
    
    
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
    
    
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
    
    
      ErrorContext.instance().reset();
    }
  }

5、从configuration中获取到ms,这个ms是之前的MapperFactoryBean初始化之后组装放入configuration中的,后续mybatis会使用Executor来执行sql,会进入到BaseExecutor的query方法,核心代码如下:

@Override
  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 (closed) {
    
    
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
    
    
      clearLocalCache();
    }
    List<E> list;
    try {
    
    
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
    
    
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
    
    
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
    
    
      queryStack--;
    }
    if (queryStack == 0) {
    
    
      for (DeferredLoad deferredLoad : deferredLoads) {
    
    
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    
    
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

6、 list = resultHandler == null ? (List) localCache.getObject(key) : null; localCache就是我们的一级缓存,我们打断点,查看两次获取的是不是一样的,最后发现每次获取到的都是空,但是key都是一样的。这个时候我开始怀疑可能不是一个sqlSession,所以我在MybatisMapperProxy中的invode方法中打断点,查看这个值。
在这里插入图片描述
确实,两次的sqlSessionProxy是不一样的,那么意思我两次查询用的是两个sqlSession,所以导致每次从缓存中获取为空。
所以我们需要返回回去查看MapperFactoryBean的getObject方法中的getSqlSession方法获取sqlsession的逻辑。

7、查看sqlsessionTemplate的构造函数

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {
    
    

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
        new Class[] {
    
     SqlSession.class }, new SqlSessionInterceptor());
  }

8、发现sqlSessionProxy 两次都不一致,所以查看这个代理对象的invoke方法

private class SqlSessionInterceptor implements InvocationHandler {
    
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
      SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
      try {
    
    
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
    
    
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
    
    
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
    
    
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator
              .translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
    
    
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
    
    
        if (sqlSession != null) {
    
    
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

9、第一句就是getSqlsession,继续往内部去查,进入到SqlSessionUtils的getSqlsession的逻辑

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {
    
    

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
    
    
      return session;
    }

    LOGGER.debug(() -> "Creating a new SqlSession");
    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

10、查看里面的逻辑,发现sessionHolder方法里面有个奇怪的东西,让我猜测是不是和事务有关。

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(() -> "Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
      session = holder.getSqlSession();
    }
    return session;
  }

11、这里面确实判断了是不是属于同一个事务,如果是在事务中,直接从holder中获取,不然就是session为null,就导致了博主这边连续调用两次,但是缓存并没有生效的原因。

复测问题

我写个方法,加上事务,在return这里打断点,isFirstLogin字段一开始为1,然后执行到之后去数据库修改为0,查看两次的值

@Transactional
 public SkyworthUser getUserInfoById(String id){
    
    
     	// 执行第一次
        userInfoMapper.selectById(id);
     	// 执行第二次
        return userInfoMapper.selectById(id);
    }

第一次:
SkyworthUser(id=0381321c-089b-43ef-b5d5-e4556c5670e9, account=222559180720275487, password={bcrypt}$2a 10 10 10BTO05duVM6mcc6encPsMeuRfpzvqNs/eTtdbD1gnuuzq6GgZdXjP., name=于琦海, department=362295371, position=, avatarUrl=https://static-legacy.dingtalk.com/media/lADPDgQ9qnh-UtfNAtDNAtA_720_720.jpg, email=null, phone=18628218225, remark=null, unionid=jKvvlRFAg7BDZZdWyciPZcAiEiE, sex=null, lastLoginTime=null, status=1, createTime=Mon Jul 19 10:01:17 CST 2021, updateTime=null, isFirstLogin=1)

第二次:
SkyworthUser(id=0381321c-089b-43ef-b5d5-e4556c5670e9, account=222559180720275487, password={bcrypt}$2a 10 10 10BTO05duVM6mcc6encPsMeuRfpzvqNs/eTtdbD1gnuuzq6GgZdXjP., name=于琦海, department=362295371, position=, avatarUrl=https://static-legacy.dingtalk.com/media/lADPDgQ9qnh-UtfNAtDNAtA_720_720.jpg, email=null, phone=18628218225, remark=null, unionid=jKvvlRFAg7BDZZdWyciPZcAiEiE, sex=null, lastLoginTime=null, status=1, createTime=Mon Jul 19 10:01:17 CST 2021, updateTime=null, isFirstLogin=1)

并且在list = resultHandler == null ? (List) localCache.getObject(key) : null;中是直接从localCache获取到list直接返回的,这个问题就解决了。

总结

  • Mybatis一级缓存的生命周期和Sqlsession一致,一级缓存就是perpetualCache这个类
  • Mybatis一级缓存内部设计较为简单,只是一个没有容量的HashMap,在缓存功能上有所欠缺
  • Mybatis一级缓存的最大范围是sqlSession内部,有多个sqlsession或者分布式环境下,数据库写操作会引起脏数据,建议设定缓存级别是statement,默认是session级别。

参考资料

聊聊Mybatis缓存机制

猜你喜欢

转载自blog.csdn.net/Tanganling/article/details/129856475