Talk about MyBatis cache mechanism (1)

foreword

Mybatis is a common Java database access layer framework. Although we generally use Mybatis Plus in our daily development, we can know from the official website that Mybatis Plus only makes it easier for developers to use and does not change the core principle. In daily work, most developers use the default cache configuration, but the Mybatis cache mechanism has some shortcomings. It is easy to cause dirty data during use, and there are some potential hidden dangers. With personal interest, I hope to sort out the MyBatis cache mechanism for readers from the perspective of application and source code.

L1 cache

Introduction to Level 1 Cache

During the running of the application, we may execute SQL with exactly the same query conditions multiple times in one database session. MyBatis provides a level-1 cache solution to optimize this part of the scene. If it is the same SQL statement, it will hit the level-1 cache first to avoid directly querying the database and improve performance. The specific execution process is shown in the figure below.
insert image description here
Each SqlSession holds an Executor, and each Executor has a LocalCache. When the user initiates a query, MyBatis generates a MappedStatement based on the currently executed statement, and performs a query in the Local Cache. If the cache hits, the result is returned to the user directly. If the cache is not hit, the database is queried, the result is written into the Local Cache, and finally the result is returned to the user. The class diagram of the specific implementation class is shown in the figure below.
insert image description here

Level 1 cache configuration

There are two options for the configuration value of the first-level cache, SESSION or STATEMENT, the default is the SESSION level, which can be set in the Configuration class
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty(“localCacheScope”, “SESSION”)));

public enum LocalCacheScope {
    
    
  SESSION,STATEMENT
}

Use of Level 1 Cache

The first-level cache means that if the SQL statement is the same, it will hit the first-level cache first to avoid directly querying the database and improve performance.
I have prepared a SkyworthUser entity class, defined the UserInfoMapper interface to inherit from BaseMapper, so that the CRUD function can be obtained without writing the mapper.xml file.
The following is the code of the Service implementation class

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

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

According to my understanding, this method repeats the query twice, it must use the so-called first-level cache, so try to call

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

In order to prove my conjecture, I set a breakpoint on the userInfoById2 line. After executing the first one, I manually modify a property value in the database to see if the output of userInfoById2 is consistent with userInfoById2.

userInfoById: The output is as follows
10:40:40.494 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@55a609dd]
SkyworthUser(id=038132 1c-089b-43ef-b5d5-e4556c5670e9, account=222559180720275487, password={bcrypt}$2a 10 1010BTO05duVM6mcc6encPsMeuRfpzvqNs/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)

Modify the database isFirstLogin=1

userInfoById2: The output is as follows
SkyworthUser(id=0381321c-089b-43ef-b5d5-e4556c5670e9, account=222559180720275487, password={bcrypt}$2a 10 1010BTO05duVM6mcc6encPsMeuRfpzvqNs/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)

what happened? Is the first-level cache enabled by default? Obviously there is no need to modify any configuration, just call it directly?
Start to check the source code with the question and find the specific reason.

Level 1 cache source code analysis

The analysis source code starts from two places, the first is the @MapperScan annotation, and the other is the MybatisPlusAutoConfiguration.class class. The blogger downloaded the original code of mybatis plus, so there will be a class called MybatisPlusAutoConfiguration.
First analyze the @MapperScan annotation

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

The @Import annotation in the annotation introduces MapperScannerRegistrar.class. MapperScannerRegistrar implements the ImportBeanDefinitionRegistrar interface. ImportBeanDefinitionRegistrar dynamically registers BeanDefinition at runtime. After viewing the source code, it can be seen that it is actually to register the MapperScannerConfigurer class into the container. For the convenience of viewing, only part of the original code is cut out, as shown below:

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

At this time, spring needs to initialize the MapperScannerConfigurer class. MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean and other interfaces. InitializingBean will execute the afterPropertiesSet() method after initialization, so you generally need to pay attention to this interface, but MapperScannerConfigurer does not seem to do anything.

Next is the BeanDefinitionRegistryPostProcessor interface, which is an extension point in Spring and is used to modify or add BeanDefinition before or after it is loaded into the Spring container. The timing of BeanDefinitionRegistryPostProcessor execution is after BeanDefinition loading and parsing is completed, before Bean instantiation and dependency injection, that is, in the preprocessing stage of BeanDefinition.

In the preprocessing stage of BeanDefinition, the Spring container will call the postProcessBeanDefinitionRegistry() method and postProcessBeanFactory() method of all classes that implement the BeanDefinitionRegistryPostProcessor interface to modify or add BeanDefinition. Among them, the postProcessBeanDefinitionRegistry() method is used to modify or add the BeanDefinition before it is loaded into the Spring container, and the postProcessBeanFactory() method is used to modify or add the BeanDefinition after it is loaded into the Spring container.

The postProcessBeanFactory method is an empty method, so regardless of him, just focus on the postProcessBeanDefinitionRegistry method, as shown below:

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

The above code creates the ClassPathMapperScanner class and executes the scan method, which literally means scanning, which is probably the scanning of the Mapper interface.
In fact, the implementation is the scan method in the ClassPathBeanDefinitionScanner class.

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

		doScan(basePackages);

scan calls the doScan method in this class, but this doScan is rewritten by the subclass ClassPathMapperScanner, so look directly at this method, the code is as follows:

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

In fact, it is to call the doScan method of the parent class, but I have done some subsequent processing. What super.doScan(basePackages) gets is a collection of BeanDefinitionHolder. BeanDefinitionHolder contains beanDefinition and beanName. If the collection is not empty, then processBeanDefinitions(beanDefinitions) should generate proxy objects according to certain rules for the interface, so that the interface can be initialized. The idea is similar to FactoryBean.

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

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

The simplified code of processBeanDefinitions(beanDefinitions); is shown above. Obviously, when our UserInfoMapper interface is initialized, it will use MapperFactoryBean to generate a proxy object. The perfect prediction is successful, because MapperFactoryBean implements the FactoryBean interface, and the getObject method will be called to generate a proxy object when it is acquired.

MapperFactoryBean extends SqlSessionDaoSupport implements FactoryBean, we understand FactoryBean, but inherits SqlSessionDaoSupport, this class needs to check what it does specifically, check and find that SqlSessionDaoSupport is an abstract class, inherited DaoSupport, DaoSupport must also be an abstract class, but it implements InitializingBean.

So we look at the afterPropertiesSet() method of DaoSupport and find that there is a checkDaoConfig method and an initDao method. We mainly look at this checkDaoConfig method, because initDao is an empty method and has not been rewritten; through the inheritance relationship of the class, we can find that MapperFactoryBean rewrites the checkDaoConfig method, so directly check the checkDaoConfig method of MapperFactoryBean

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

This code is a bit abstract, because we don’t know what configuration.addMapper(this.mapperInterface); specifically does, and what is the function of the Configuration class. There are no comments in the class, and the big guys from Alibaba... skip it if you don’t understand it. We only know to put the UserInfoMapper interface in the Configuration class, and click directly to see what the addMapper method does.

Click to go to the addMapper method in Configuration. Inside the method is the mapperRegistry.addMapper(type) method. If you continue to go down, you will arrive at the addMapper method of MapperRegistry. This method is rewritten by MybatisMapperRegistry. Then check the logic inside, as shown in the following figure:

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

When I saw the first judgment logic, I was almost sure that my previous process was fine, so keep going. The parser.parse() method is very special. It feels like parsing something. Is it possible that it is the xml parsing of the UserInfoMapper namespace. Going down with a guess, parse enters the parse method of MybatisMapperAnnotationBuilder, and the code is as follows:

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

At this point we probably know that our UserInfoMapper interface will generate the MapperFactoryBean class. After the class is initialized, it will perform specific processing on this class, put the information into the Configuration class, and facilitate subsequent direct use. Namespaces, cache information, etc. are set.

But when we want to create a proxy object of the UserInfoMapper class, we will execute the MapperFactoryBean.getObject method. The final logic will enter the getMapper method of MybatisMapperRegistry, the code is as follows:

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

In this code, newInstance is to generate a proxy object, and the core code is as follows:

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

It can be judged from this that if we execute the userinfoMapper method, we will enter the invoke method of the MybatisMapperProxy class. At this point we can start to analyze why our first level cache does not take effect.

Troubleshooting

Recall that we called it twice in this way, and modified the database field value before the second execution, and found that the cache was not used, and the two queries were inconsistent. Because the source code of mybatis Plus+spring used by the blogger does not use Springboot, so calling it like this, calling it twice in the service also has the same effect, and it has been tested.

 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. The bean.getUserInfoById method finally enters the invoke method of MybatisMapperProxy

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. The cachedInvoker will form the PlainMethodInvoker, so the cachedInvoker(method).invoke() method will enter the invoke method of the PlainMethodInvoker

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

3. Execute the execute method to enter the execute method of MybatisMapperMethod, our getUserInfoById is the selectOne used

 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. You can follow the breakpoint all the way, and you will reach the selectList method of DefaultSqlSession

 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. Obtain ms from configuration. This ms is assembled and put into configuration after the previous MapperFactoryBean is initialized. Subsequent mybatis will use Executor to execute SQL and enter the query method of BaseExecutor. The core code is as follows:

@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 is our first-level cache. We break the point to check whether the two fetches are the same. Finally, we find that the fetches are empty each time, but the keys are the same. At this time, I began to suspect that it might not be a sqlSession, so I broke the point in the invoke method in MybatisMapperProxy to check this value.
insert image description here
Indeed, the two sqlSessionProxy are different, so it means that I used two sqlSession for the two queries, so it is empty every time I get it from the cache.
So we need to go back and look at the logic of getting sqlsession by the getSqlSession method in the getObject method of MapperFactoryBean.

7. View the constructor of 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. It is found that sqlSessionProxy is inconsistent twice, so check the invoke method of this proxy object

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. The first sentence is getSqlsession, continue to check internally, and enter the logic of getSqlsession in SqlSessionUtils

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. Check the logic inside and find that there is something strange in the sessionHolder method, which makes me guess whether it is related to the transaction.

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. It is indeed judged whether it belongs to the same transaction. If it is in the transaction, it is obtained directly from the holder, otherwise the session is null, which causes the blogger to call twice in a row, but the reason why the cache does not take effect.

retest question

I write a method, add a transaction, break the return point here, the isFirstLogin field is 1 at the beginning, and then go to the database to change it to 0 after execution, and check the value twice

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

First time:
SkyworthUser(id=0381321c-089b-43ef-b5d5-e4556c5670e9, account=222559180720275487, password={bcrypt}$2a 10 1010BTO05duVM6mcc6encPsMeuRfpzvqNs/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)

Second time:
SkyworthUser(id=0381321c-089b-43ef-b5d5-e4556c5670e9, account=222559180720275487, password={bcrypt}$2a 10 1010BTO05duVM6mcc6encPsMeuRfpzvqNs/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)

And in list = resultHandler == null ? (List) localCache.getObject(key): null;, the list is directly obtained from localCache and returned directly, and this problem is solved.

Summarize

  • The life cycle of the Mybatis first-level cache is consistent with that of Sqlsession, and the first-level cache is the perpetualCache class
  • The internal design of the Mybatis level-1 cache is relatively simple, just a HashMap without capacity, which lacks the cache function
  • The maximum range of Mybatis level-1 cache is inside the sqlsession. In multiple sqlsessions or in a distributed environment, database write operations will cause dirty data. It is recommended to set the cache level to statement, and the default is session level.

References

Talk about the Mybatis cache mechanism

Guess you like

Origin blog.csdn.net/Tanganling/article/details/129856475