聊聊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缓存机制

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/5716.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

HTML5 <!DOCTYPE> 标签

实例 <!DOCTYPE> 声明非常重要&#xff0c;它是一种标准通用标记语言的文档类型声明&#xff0c;通过该标签&#xff0c;浏览器能够了解HTML5文档正在使用的HTML规范&#xff0c;<!DOCTYPE> 声明是HTML5文档的起始点&#xff0c;也就是说它必须位于HTML5文档的第一…

《SpringBoot》第03章 自动配置机制(二) 根注解@SpringBootApplication

前言 之前介绍到了把启动类封装成BeanDefinition注入进IOC容器&#xff0c;那么这个启动类就会跟普通的bean一样在refresh()中被实例化&#xff0c;那么显而易见作为启动类这个实例化并不简单&#xff0c;肯定会存在一些特殊处理&#xff0c;那么就需要研究一下其注解SpringBo…

AI只会淘汰不进步的程序员

最近AI界的大新闻有点多&#xff0c;属于多到每天很努力都追不上&#xff0c;每天都忙着体验各种新产品或申请试用新产品。各种自媒体肯定也不会放过这个机会&#xff0c;AI取代程序员的文章是年年有&#xff0c;今天特别多。那么AI到底会不会取代程序员的工作呢&#xff1f;先…

[chapter4][5G-NR][传输方案]

前言&#xff1a; 多天线传输的基本过程传输方案 前面见过数据加扰&#xff0c;调制&#xff0c;层映射的一些基本原理&#xff0c;算法。 这里重点讲一下传输方案 目录&#xff1a; 1&#xff1a; 下行传输方案 2&#xff1a; 上行传输方案 3&#xff1a; 资源块映射 备注&…

.net开发安卓从入门到放弃 最后的挣扎(排查程序闪退问题记录-到目前为止仍在继续)

安卓apk闪退问题排查记录logcat程序包名先看日志&#xff08;以下日志是多次闪退记录的系统日志&#xff0c;挑拣几次有代表性的发上来&#xff09;最近一次闪退adb shell tophelp一个demo说明adb shell dumpsys meminfo <package_name>ps&#xff1a;写在前面&#xff0…

训练中文版chatgpt

文章目录1. 斯坦福的模型——小而低廉&#xff1a;Alpaca: A Strong Open-Source Instruction-Following Model2. Meta 模型&#xff1a;LLaMA&#xff1a;open and efficient foundation language models3.ChatGLM4.斯坦福开源机器人小羊驼Vicuna&#xff0c;130亿参数匹敌90%…

SSM+LayUi实现的学籍管理系统(分为管理员、教师、学生三个角色,实现了专业管理,班级管理,学生管理,老师管理,课程管理,开课管理以及用户管理等)

博客目录jspservletmysql实现的停车场管理系统实现功能截图系统功能使用技术完整源码jspservletmysql实现的停车场管理系统 本系统是一个servlet原生框架实现的停车场管理系统&#xff0c;总共分为两个角色&#xff0c;普通用户和管理员&#xff0c;实现了用户管理、停车信息管…

Linux基础IO

本篇博客来讲述Linux中的新一模块--文件IO&#xff0c;我们来做简单的介绍和陈述。 在笔者之前的文章之中&#xff0c;已经对C语言中的文件操作做了简要介绍&#xff0c;我们旧事重提&#xff0c;再次进行一个简要的回顾。 目录 1.文件的操作 1.1打开文件 1.2向文件写入数…

Java多态

目录 1.多态是什么&#xff1f; 2.多态的条件 3.重写 3.1重写的概念 3.2重写的作用 3.3重写的规则 4.向上转型与向下转型 4.1向上转型 4.2向下转型 5.多态的优缺点 5.1 优点 5.2 缺点 面向对象程序三大特性&#xff1a;封装、继承、多态。 1.多态是什么&#xff1…

七结(4.2)遍历集合与javaFX界面

今天由学长学界们进行了一次授课&#xff0c;算是温习了一遍面向对象的知识&#xff0c;同时配置了关于javaFX的环境&#xff0c;以及一些关于项目的知识。 java学习总结&#xff1a; Collection的遍历&#xff1a; 迭代器遍历&#xff08;Iterator&#xff09;&#xff1a;…

leetcode 87. Scramble String(扰乱字符串)

scramble(字符串s) 算法&#xff1a; s长度为1时结束。 s可以拆分成2部分x和y&#xff0c;sxy, 这两部分可以交换&#xff0c;也可以不交换&#xff0c;即 s xy 或 s yx. 上面scramble还会递归作用于x 和 y. 给出相同长度的字符串s1, s2, 问s2是否可通过scramble(s1)获得。 …

WTW-16P 应用电路

1、WTW-16P 按键控制 PWM 输出应用电路 软件设置&#xff1a; 按键控制模式。 I/O 口定义&#xff1a; 选取 I/O 口 P00、P01、P02、P03 作为触发口&#xff0c;在编辑 WT588D 语音工程时&#xff0c;把触发口的按键定义为可触发播放的触发方式&#xff0c;就可进行工作。 BUS…

如何提高网站安全防护?

网站的安全问题一直是很多运维人员的心头大患&#xff0c;一个网站的安全性如果出现问题&#xff0c;那么后续的一系列潜在危害都会起到连锁反应。就好像网站被挂马&#xff0c;容易遭受恶意请求呀&#xff0c;数据泄露等等都会成为杀死网站的凶手。 1、让服务器有一个安全稳定…

百度双塔召回引擎MOBIUS

1. 概述 对于一个搜索或者推荐系统来说&#xff0c;分阶段的设计都是当下的一个标配&#xff0c;主要是为了平衡效率和效果&#xff0c;在百度的广告系统中&#xff0c;也是分成了如下的三层结构&#xff1a; 最上层的Matching阶段负责从全库中找到与query相关的候选集&#x…

KD2511N系列微电阻测试仪

一、产品概述 KD2511N系列微电阻测试仪是一款对变压器、电机、开关、继电器、接插件等各类直流电阻进行测试的仪器。其基本测试精度最高可达 0.05%&#xff0c;并具有较高的测量速度。 KD2511N微电阻测试仪使用了高精度恒流流经被测件以及四端测量&#xff0c;有效的消除了 引线…

Kafka 如何保证消息的消费顺序

文章目录先直接给出答案吧。在集群或者多partition下无法保障完全顺序消费&#xff0c;但是可以保障分区顺序消费。具体下面讲解。我们在使用消息队列的过程中经常有业务场景需要严格保证消息的消费顺序&#xff0c;比如我们同时发了 2 个消息&#xff0c;这 2 个消息对应的操作…

蓝桥杯——根据手册写底层

一、 DS18B20温度传感器 1.官方所给源码 /* # DS1302代码片段说明1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。2. 参赛选手可以自行编写相关代码或以该代码为基础&#xff0c;根据所选单片机类型、运行速度和试题中对单片机时钟频率的要求&#xff0c;进行代码…

ssm入门

文章目录1.介绍ssm2.Spring篇基础内容&#x1fa85;什么是IOC&#xff08;控制反转&#xff09;Spring和IOC之间的关系IOC容器的作用以及内部存放IoC入门案例&#x1f4ec;DI&#xff08;Dependency Injection&#xff09;依赖注入依赖注入的概念IOC容器中哪些bean之间要建立依…

函数微分和导数的定义

1.我们先来看可导的定义&#xff1a; 相信这个大家都看的懂。 2.接下来我们看可微的定义&#xff1a; 你们有没用想过为什么会有可微&#xff0c;他是用来干什么的&#xff0c;我们接下来看下面这张图&#xff0c;特别是结合图2-11来说&#xff0c; 我们可以看到书上说可微是在…

【day2】Android Jetpack Compose环境搭建

【day2】Android Jetpack Compose环境搭建 以下是适用于 Jetpack Compose 的环境要求&#xff1a; Android Studio 版本&#xff1a;4.2 Canary 15 或更高版本Gradle 版本&#xff1a;7.0.0-beta02 或更高版本Android 插件版本&#xff1a;4.2.0-beta15 或更高版本Kotlin 版本…
最新文章