Skip to content

MyBatis 缓存

本文介绍MyBatis的一级缓存和二级缓存。

1. 环境搭建

先看一下整个项目结构:

image-20250715172844086

首先创建一个普通的Maven工程,pom.xml依赖内容如下:

xml
<dependencies>
  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.19</version>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.38</version>
  </dependency>
  <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.5.18</version>
  </dependency>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.8.2</version>
    <scope>test</scope>
  </dependency>
</dependencies>

首先创建一个名为test的数据库,然后准备SQL文件初始化数据库:

sql
DROP TABLE IF EXISTS demo;

CREATE TABLE demo(
   id INT AUTO_INCREMENT PRIMARY KEY,
   value1 VARCHAR(255),
   value2 VARCHAR(255)
);

INSERT INTO demo(value1, value2) VALUES('1111','aaaa'),('2222','bbbb'),('3333','cccc');

准备实体类和数据访问接口:

java
public interface DemoDao {
    @Select("select * from demo where id = #{id}")
    DemoEntity queryById(Integer id);
}
java
@Data
@EqualsAndHashCode
public class DemoEntity {
    private Integer id;
    private String value1;
    private String value2;
}

准备数据库连接信息,保存在resources/db.properties文件中:

properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
jdbc.username=root
jdbc.password=123456

然后准备MyBatis配置文件resources/mybatis.xml

xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <!--加载连接数据库的四要素-->
    <properties resource="db.properties"></properties>

    <settings>
        <!--自动将下划线命名转换为驼峰命名-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
      	<!-- 配置package,让@Select注解生效 -->
        <package name="org.example.dao"/>
    </mappers>

</configuration>

最后配置resources/logback.xml文件:

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- 控制台输出日志 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%-4relative [%thread] %-5level %logger{35} -%kvp- %msg %n</pattern>
        </encoder>
    </appender>

    <!-- 定义根日志记录器日志级别 -->
    <root level="INFO">
        <!-- ref的值需要与appender的name一致 -->
        <appender-ref ref="STDOUT" />
    </root>

    <!-- 定义某个包或某个类的日志级别 -->
    <logger name="org.example.dao" level="DEBUG" additivity="false">
        <appender-ref ref="STDOUT" />
    </logger>

</configuration>

准备测试类:

java
@Slf4j
public class MyBatisTest {

    @Test
    public void test1() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();
        DemoEntity demoEntity1 = sqlSession.selectOne("org.example.dao.DemoDao.queryById", 1);

        log.info("demoEntity1: {}", demoEntity1);

        DemoEntity demoEntity2 = sqlSession.selectOne("org.example.dao.DemoDao.queryById", 1);
        log.info("demoEntity2: {}", demoEntity2);

        log.info("demoEntity1 == demoEntity2: {}", demoEntity1 == demoEntity2);

    }
}

结果如下:

txt
303  [main] DEBUG org.example.dao.DemoDao.queryById -- ==>  Preparing: select * from demo where id = ? 
315  [main] DEBUG org.example.dao.DemoDao.queryById -- ==> Parameters: 1(Integer) 
329  [main] DEBUG org.example.dao.DemoDao.queryById -- <==      Total: 1 
329  [main] INFO  org.example.MyBatisTest -- demoEntity1: DemoEntity(id=1, value1=1111, value2=aaaa) 
330  [main] INFO  org.example.MyBatisTest -- demoEntity2: DemoEntity(id=1, value1=1111, value2=aaaa) 
330  [main] INFO  org.example.MyBatisTest -- demoEntity1 == demoEntity2: true

可以发现,第二次查询没有打印出SQL语句,说明是查询的缓存数据,并且两次查询返回的数据是同一个数据。

经过以上的例子,证实了MyBatis中存在缓存(实际是一级缓存的存在)。

2. 一级缓存

2.1 什么是一级缓存

在MyBatis中,一级缓存(也叫本地缓存)是存在SqlSession中的,也就是说,在同一个SqlSession中执行查询语句,如果在缓存中查询到同样SQL的查询语句,那么就会从缓存中获取数据,而不是从数据库中。

MyBatis的一级缓存是默认开启的。

2.2 什么时候存入一级缓存

先说结论,查询结果是否会放入 MyBatis 一级缓存,取决于调用的方法以及是否启用了缓存。

  • 如果调用的是 selectList()selectOne()selectMap() 方法,并且对应的<select>语句中没有设置 useCache=false,则查询结果会放入 MyBatis 的一级缓存(本地缓存)。

  • 如果使用了 ResultHandler(如调用 select() 的重载方法),则不会将结果放入一级缓存。

  • 使用 Cursor(如 selectCursor() 方法)进行查询时,不会将结果缓存到一级缓存中。

所以,如果该方法是通过普通 selectList/selectOne/selectMap 系列方法执行的,并且未禁用缓存,则结果会被放入一级缓存,否则不会。

先看看MyBatis中SqlSession的类图:

image-20250715192037529

TIP

在SqlSession中使用Executor执行SQL语句,在Executor的基础实现类BaseExecutor中有属性 loalCache,其类型为PerpetualCache,就是对java.util.Map的简单包装

然后,跟踪DefaultSqlSession中的查询方法,最终找到如下方法,从数据库中查询数据:

txt
org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
java
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
    ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    localCache.removeObject(key);
  }
  localCache.putObject(key, list);
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}

重点在第10行,就是往本地缓存中加入数据。

问题来了,localCache.putObject(key, list);中的list是刚从数据库中查询出来的数据,那key是什么呢,方法参数中显示key的类型为CacheKey,这又是什么?

2.3 CacheKey是什么

PerpetualCache中,putObject()就是往Map中添加数据:

java
public class PerpetualCache implements Cache {

  private final String id;

  private final Map<Object, Object> cache = new HashMap<>();
  
  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }
}

我们主要关注这里的key,即CacheKey是什么,源码如下:

Details
java
public class CacheKey implements Cloneable, Serializable {

  private final int multiplier;
  private int hashcode;
  private long checksum;
  private int count;
  private List<Object> updateList;
  
  // update 方法
  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }
  
  // equals方法
  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    if ((hashcode != cacheKey.hashcode) || (checksum != cacheKey.checksum) || (count != cacheKey.count)) 		 {
      return false;
    }

    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

  // hashCode方法
  @Override
  public int hashCode() {
    return hashcode;
  }
}

总结一下,CacheKey就是由多个对象组成的一个唯一标识。

在如下方法中,创建CacheKey

txt
org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
java
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
    throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameter);
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

进入createCacheKey()方法:

java
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  // 创建cacheKey对象
  CacheKey cacheKey = new CacheKey();
  // 向cacheKey中添加对象
  cacheKey.update(ms.getId());     // StatementId
  cacheKey.update(rowBounds.getOffset());  // 分页参数offset
  cacheKey.update(rowBounds.getLimit());   // 分页参数limit
  cacheKey.update(boundSql.getSql());      // SQL
  
  // 获取SQL参数
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
  // 将SQL参数依次加入到cacheKey中
  MetaObject metaObject = null;
  for (ParameterMapping parameterMapping : parameterMappings) {
    if (parameterMapping.getMode() != ParameterMode.OUT) {
      Object value;
      String propertyName = parameterMapping.getProperty();
      if (boundSql.hasAdditionalParameter(propertyName)) {
        value = boundSql.getAdditionalParameter(propertyName);
      } else if (parameterObject == null) {
        value = null;
      } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
        value = parameterObject;
      } else {
        if (metaObject == null) {
          metaObject = configuration.newMetaObject(parameterObject);
        }
        value = metaObject.getValue(propertyName);
      }
      cacheKey.update(value);
    }
  }
  // 从配置中获取环境,将EnvironmentID加入到cacheKey中
  if (configuration.getEnvironment() != null) {
    cacheKey.update(configuration.getEnvironment().getId());
  }
  return cacheKey;
}

通过以上的分析,缓存中的Key就是由要执行的查询SQL语句各部分组成的,包括StatementId,分页参数、SQL语句、查询条件和环境。

所以,我们也能总结出缓存命中原则。

2.4 缓存命中原则

首先总结缓存命中原则,只有同时满足以下情况,才能从缓存中查询数据:

MyBatis 的一级缓存能否命中,只有同时满足以下情况,才能从缓存中查询数据(必须全部满足才能命中):

  1. 同一个 SqlSession(物理上是同一个 Executor 对象)。
  2. 同一个 statementId(Mapper 接口的方法全限定名,例如org.example.dao.DemoDao.queryById)。
  3. 同一个 SQL(由 MyBatis 最终生成的带 ? 占位符的 SQL 字符串,例如select id, value1, value2 from demo where id = ? )。
  4. 同一个 参数集合parameterObjectequals 结果相同,也就是最终传递给SQL的参数要相同)。
  5. 同一个 RowBounds(分页范围相同;无分页时就是 RowBounds.DEFAULT)。
  6. 同一个 Environment

只要上述项完全一致,MyBatis 就直接把上一次查询放在 PerpetualCache 里的结果对象原样返回;任何一项不同,都会视为新的查询,重新发送 SQL 到数据库查询数据。

下面就以多个例子演示缓存未命中的情况。

情况一:不是同一个SqlSession

java
@Test
public void testSqlSession() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    // 第一个SqlSession
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    DemoEntity demoEntity = sqlSession1.selectOne("org.example.dao.DemoDao.queryById", 1);
    log.info("demoEntity: {}", demoEntity);

    // 第二个SqlSession
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    DemoEntity demoEntity2 = sqlSession2.selectOne("org.example.dao.DemoDao.queryById", 1);
    log.info("demoEntity2: {}", demoEntity2);

    log.info("demoEntity == demoEntity2: {}", demoEntity == demoEntity2);
}

通过之前的分析,我们可以找到,缓存是在BaseExecutor中的,而Executor是在SqlSession中的,有两个不同的SqlSession,也就意味着有两个缓存,那么肯定是无法命中缓存的。

情况二:不同的StatementId

首先在DemoDao中添加一个方法:

java
public interface DemoDao {
    DemoEntity queryById(Integer id);
    DemoEntity queryById2(Integer id);
}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.example.dao.DemoDao">

    <select id="queryById" resultType="org.example.entity.DemoEntity">
        select id, value1, value2 from demo
        where id = #{id}
    </select>
    <select id="queryById2" resultType="org.example.entity.DemoEntity">
        select id, value1, value2 from demo
        where id = #{id}
    </select>
</mapper>

除了方法名不同,其余完全相同。

java
@Test
public void testStatementId() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession sqlSession = sqlSessionFactory.openSession();
    // queryById 方法
    DemoEntity demoEntity1 = sqlSession.selectOne("org.example.dao.DemoDao.queryById", 1);
    log.info("demoEntity1: {}", demoEntity1);

    // queryById2 方法
    DemoEntity demoEntity2 = sqlSession.selectOne("org.example.dao.DemoDao.queryById2", 1);
    log.info("demoEntity2: {}", demoEntity2);

    log.info("demoEntity1 == demoEntity2: {}", demoEntity1 == demoEntity2);
}

情况三:不同的SQL

再在接口中添加如下方法:

java
public interface DemoDao {
	DemoEntity queryByIdByType(Map<String, Object> map);
}
xml
<select id="queryByIdByType" resultType="org.example.entity.DemoEntity">
    <if test="type==1">
        select id, value1, value2 from demo
        where id = #{id}
    </if>
    <if test="type==2">
        select id, value1, value2 from demo
        where                id = #{id}
    </if>
</select>

可以看到,当type=2时,SQL语句会有一长串的空格。

java
@Test
public void testSql() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession sqlSession = sqlSessionFactory.openSession();
    Map<String, Object> map1 = new HashMap<>(){{
        put("id", 1);
        put("type",1);
    }};
    DemoEntity demoEntity1 = sqlSession.selectOne(
            "org.example.dao.DemoDao.queryByIdByType", map1);
    log.info("demoEntity1: {}", demoEntity1);

    map1.put("type",2);
    DemoEntity demoEntity2 = sqlSession.selectOne(
            "org.example.dao.DemoDao.queryByIdByType", map1);
    log.info("demoEntity2: {}", demoEntity2);

    log.info("demoEntity1 == demoEntity2: {}", demoEntity1 == demoEntity2);
}

结果:

txt
304  [main] DEBUG o.e.dao.DemoDao.queryByIdByType -- ==>  Preparing: select id, value1, value2 from demo where id = ? 
316  [main] DEBUG o.e.dao.DemoDao.queryByIdByType -- ==> Parameters: 1(Integer) 
333  [main] DEBUG o.e.dao.DemoDao.queryByIdByType -- <==      Total: 1 
334  [main] INFO  org.example.MyBatisTest -- demoEntity1: DemoEntity(id=1, value1=1111, value2=aaaa) 
335  [main] DEBUG o.e.dao.DemoDao.queryByIdByType -- ==>  Preparing: select id, value1, value2 from demo where id = ? 
335  [main] DEBUG o.e.dao.DemoDao.queryByIdByType -- ==> Parameters: 1(Integer) 
335  [main] DEBUG o.e.dao.DemoDao.queryByIdByType -- <==      Total: 1 
335  [main] INFO  org.example.MyBatisTest -- demoEntity2: DemoEntity(id=1, value1=1111, value2=aaaa) 
335  [main] INFO  org.example.MyBatisTest -- demoEntity1 == demoEntity2: false

从日志来看,两条SQL完全一样,其实不一样的是其中一条SQL多了很多空格。

情况四:参数不同

java
@Test
public void testParam() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession sqlSession = sqlSessionFactory.openSession();
    DemoEntity demoEntity1 = sqlSession.selectOne(
            "org.example.dao.DemoDao.queryById",
            1);
    log.info("demoEntity1: {}", demoEntity1);

    DemoEntity demoEntity2 = sqlSession.selectOne(
            "org.example.dao.DemoDao.queryById",
            2);
    log.info("demoEntity2: {}", demoEntity2);

    log.info("demoEntity1 == demoEntity2: {}", demoEntity1 == demoEntity2);
}

结果显而易见,结果集都不一样了,肯定不能从缓存中查询数据。

情况四:分页参数不同

java
@Test
public void testPage() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession sqlSession = sqlSessionFactory.openSession();
    List<DemoEntity> demoEntity1 = sqlSession.selectList(
            "org.example.dao.DemoDao.queryById",
            1,
            new RowBounds(0,1));

    log.info("demoEntity1: {}", demoEntity1);

    List<DemoEntity> demoEntity2 = sqlSession.selectList(
            "org.example.dao.DemoDao.queryById",
            1,
            new RowBounds(0,2));
    log.info("demoEntity2: {}", demoEntity2);

    log.info("demoEntity1 == demoEntity2: {}", demoEntity1 == demoEntity2);
}

2.5 缓存失效

缓存失效,更确切地说,应该是指缓存什么时候被清除了。

在以下情况下,缓存会被清除:

  • 调用了SqlSession.close()
  • 调用了SqlSession.commit()
  • 调用了SqlSession.rollback()
  • 调用了update()/insert()/delete()等修改方法;
  • 调用了SqlSession.clearCache()方法;

下面演示或讲解以上导致缓存清除的情况。

情况一:调用close()方法

由于调用了close()方法,就不能再使用SqlSession对象了,所以我们以跟踪源码的方式查看close()的实现:

txt
org.apache.ibatis.executor.BaseExecutor#close
java
public void close(boolean forceRollback) {
  try {
    try {
      rollback(forceRollback);
    } finally {
      if (transaction != null) {
        transaction.close();
      }
    }
  } catch (SQLException e) {
    log.warn("Unexpected exception on closing transaction.  Cause: " + e);
  } finally {
    transaction = null;
    deferredLoads = null;
    localCache = null;
    localOutputParameterCache = null;
    closed = true;
  }
}

在第15行,直接把本地缓存置为null了,所以可见调用close()会清空缓存。

情况二和情况三:调用了commit()方法或调用了rollback()方法

java
@Test
public void testCommit() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession sqlSession = sqlSessionFactory.openSession();

    DemoEntity demoEntity1 = sqlSession.selectOne("org.example.dao.DemoDao.queryById", 1);
    log.info("demoEntity1: {}", demoEntity1);
  	// 情况二
    sqlSession.commit();
	  // 情况三	
  	sqlSession.rollback();

    DemoEntity demoEntity2 = sqlSession.selectOne("org.example.dao.DemoDao.queryById", 1);
    log.info("demoEntity2: {}", demoEntity2);

    log.info("demoEntity1 == demoEntity2: {}", demoEntity1 == demoEntity2);  // false
}

结果显示两次查询都是查的数据库。

情况四:调用了update()/insert()/delete()方法

首先在接口中增加一个insert()方法:

java
public interface DemoDao {
    void insert(DemoEntity demoEntity);
}
xml
<insert id="insert" parameterType="org.example.entity.DemoEntity">
    insert into demo (value1, value2)
    values (#{value1}, #{value2})
</insert>

然后测试情况如下:

java
@Test
public void testInsert() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession sqlSession = sqlSessionFactory.openSession();

    DemoEntity demoEntity1 = sqlSession.selectOne("org.example.dao.DemoDao.queryById", 1);
    log.info("demoEntity1: {}", demoEntity1);

    DemoEntity newDemoEntity = new DemoEntity();
    newDemoEntity.setValue1("4444");
    newDemoEntity.setValue1("dddd");
    sqlSession.insert("org.example.dao.DemoDao.insert", newDemoEntity);

    DemoEntity demoEntity2 = sqlSession.selectOne("org.example.dao.DemoDao.queryById", 1);
    log.info("demoEntity2: {}", demoEntity2);

    log.info("demoEntity1 == demoEntity2: {}", demoEntity1 == demoEntity2);
}

结果如下:

Details
txt
301  [main] DEBUG org.example.dao.DemoDao.queryById -- ==>  Preparing: select id, value1, value2 from demo where id = ? 
313  [main] DEBUG org.example.dao.DemoDao.queryById -- ==> Parameters: 1(Integer) 
327  [main] DEBUG org.example.dao.DemoDao.queryById -- <==      Total: 1 
328  [main] INFO  o.example.MyBatisCacheInvalidTest -- demoEntity1: DemoEntity(id=1, value1=1111, value2=aaaa) 
329  [main] DEBUG org.example.dao.DemoDao.insert -- ==>  Preparing: insert into demo (value1, value2) values (?, ?) 
329  [main] DEBUG org.example.dao.DemoDao.insert -- ==> Parameters: dddd(String), null 
339  [main] DEBUG org.example.dao.DemoDao.insert -- <==    Updates: 1 
339  [main] DEBUG org.example.dao.DemoDao.queryById -- ==>  Preparing: select id, value1, value2 from demo where id = ? 
339  [main] DEBUG org.example.dao.DemoDao.queryById -- ==> Parameters: 1(Integer) 
340  [main] DEBUG org.example.dao.DemoDao.queryById -- <==      Total: 1 
340  [main] INFO  o.example.MyBatisCacheInvalidTest -- demoEntity2: DemoEntity(id=1, value1=1111, value2=aaaa) 
340  [main] INFO  o.example.MyBatisCacheInvalidTest -- demoEntity1 == demoEntity2: false

可以看到缓存在执行了插入语句后失效了。

情况五:调用了clearCache()方法

java
@Test
public void testClearCache() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession sqlSession = sqlSessionFactory.openSession();

    DemoEntity demoEntity1 = sqlSession.selectOne("org.example.dao.DemoDao.queryById", 1);
    log.info("demoEntity1: {}", demoEntity1);

    sqlSession.clearCache();

    DemoEntity demoEntity2 = sqlSession.selectOne("org.example.dao.DemoDao.queryById", 1);
    log.info("demoEntity2: {}", demoEntity2);

    log.info("demoEntity1 == demoEntity2: {}", demoEntity1 == demoEntity2);
}

跟踪源码,最后找到BaseExecutor的方法:

java
public void clearLocalCache() {
  if (!closed) {
    localCache.clear();
    localOutputParameterCache.clear();
  }
}
txt
org.apache.ibatis.cache.impl.PerpetualCache#clear
java
@Override
public void clear() {
  cache.clear();
}

2.6 Spring中的SqlSession管理

在Spring环境下,我们不用手动进行SqlSession管理,即不用自己获取、关闭SqlSession

  • 事务方法中,Spring 会从 SqlSessionFactory 获取一个 SqlSession 实例,并将其绑定到当前线程的事务上下文中。这意味着在这个事务的整个生命周期内,当前线程会使用这同一个 SqlSession

  • 非事务方法中,如果没有活动的 Spring 事务,每次通过数据访问层调用数据库操作时,会:

    • 获取新的 SqlSession: 从 SqlSessionFactory 获取一个新的 SqlSession
    • 执行操作并提交: 执行 DAO 方法(例如 queryById)。
    • 立即关闭 SqlSession: 操作完成后,立即关闭这个 SqlSession

    也就是说每次独立的 DAO 调用都会获取并关闭一个新的 SqlSession

因此,在事务方法中,一级缓存是有效的,在非事务方法中,一级缓存无效(因为不是同一个SqlSession)。

下面演示在Spring环境下事务和非事务方法中,一级缓存情况。

首先引入依赖:

Details
xml
<dependencies>
  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.19</version>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.38</version>
  </dependency>
  <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.5.18</version>
  </dependency>

  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>6.2.8</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>6.2.8</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.2.8</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>6.2.8</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>6.2.8</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>6.2.8</version>
  </dependency>
  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>3.0.4</version>
  </dependency>
  
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>6.2.8</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
  </dependency>
</dependencies>

然后编写服务方法:

java
@Service
@Slf4j
public class DemoService {

    @Autowired
    private DemoDao demoDao;

    @Transactional
    public void testWithTransaction() {

        DemoEntity demoEntity1 = demoDao.queryById(1);
        log.info("demoEntity1: {}", demoEntity1);

        DemoEntity demoEntity2 = demoDao.queryById(1);
        log.info("demoEntity2: {}", demoEntity2);

        log.info("demoEntity1 == demoEntity2: {}", demoEntity1 == demoEntity2);
    }

    public void testWithoutTransaction() {
        DemoEntity demoEntity1 = demoDao.queryById(1);
        log.info("demoEntity1: {}", demoEntity1);

        DemoEntity demoEntity2 = demoDao.queryById(1);
        log.info("demoEntity2: {}", demoEntity2);

        log.info("demoEntity1 == demoEntity2: {}", demoEntity1 == demoEntity2);
    }

}

配置类:

java
@Configuration
@ComponentScan("org.example")
@EnableTransactionManagement
public class MyBatisConfig {

    /**
     * 配置并返回一个数据源Bean
     *
     * @return 配置好的数据源对象
     */
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        return dataSource;
    }

    /**
     * 配置并返回一个SqlSessionFactoryBean,用于获取SqlSession对象
     *
     * @param dataSource 数据源实例
     * @return 配置好的SqlSessionFactoryBean对象
     * @throws IOException 如果加载映射文件时发生IO异常
     */
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws IOException {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath:mapper/**/*.xml")
        );

        return sqlSessionFactoryBean;
    }

    /**
     * 配置并返回一个MapperScannerConfigurer,用于扫描Mapper接口并注册为Bean
     *
     * @return 配置好的MapperScannerConfigurer对象
     */
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("org.example.dao");
        mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactoryBean");
        return mapperScannerConfigurer;
    }

    /**
     * 配置并返回一个事务管理器
     *
     * @param dataSource 数据源实例
     * @return 配置好的DataSourceTransactionManager对象
     */
    @Bean
    DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }
}

然后编写测试代码:

java
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = MyBatisConfig.class)
@Slf4j
public class MyBatisSpringTest {

    @Autowired
    private DemoService demoService;

    @Test
    public void test() {
        demoService.testWithoutTransaction();
        log.info("-------------------分隔线------------------------");
        demoService.testWithTransaction();
    }
}

结果如下:

txt
482  [main] DEBUG org.example.dao.DemoDao.queryById -- ==>  Preparing: select id, value1, value2 from demo where id = ? 
493  [main] DEBUG org.example.dao.DemoDao.queryById -- ==> Parameters: 1(Integer) 
508  [main] DEBUG org.example.dao.DemoDao.queryById -- <==      Total: 1 
509  [main] INFO  org.example.service.DemoService -- demoEntity1: DemoEntity(id=1, value1=1111, value2=aaaa) 
525  [main] DEBUG org.example.dao.DemoDao.queryById -- ==>  Preparing: select id, value1, value2 from demo where id = ? 
525  [main] DEBUG org.example.dao.DemoDao.queryById -- ==> Parameters: 1(Integer) 
526  [main] DEBUG org.example.dao.DemoDao.queryById -- <==      Total: 1 
527  [main] INFO  org.example.service.DemoService -- demoEntity2: DemoEntity(id=1, value1=1111, value2=aaaa) 
527  [main] INFO  org.example.service.DemoService -- demoEntity1 == demoEntity2: false 
527  [main] INFO  org.example.MyBatisSpringTest -- -------------------分隔线------------------------ 
544  [main] DEBUG org.example.dao.DemoDao.queryById -- ==>  Preparing: select id, value1, value2 from demo where id = ? 
545  [main] DEBUG org.example.dao.DemoDao.queryById -- ==> Parameters: 1(Integer) 
545  [main] DEBUG org.example.dao.DemoDao.queryById -- <==      Total: 1 
545  [main] INFO  org.example.service.DemoService -- demoEntity1: DemoEntity(id=1, value1=1111, value2=aaaa) 
545  [main] INFO  org.example.service.DemoService -- demoEntity2: DemoEntity(id=1, value1=1111, value2=aaaa) 
545  [main] INFO  org.example.service.DemoService -- demoEntity1 == demoEntity2: true

可以看到在事务方法中,一级缓存生效了。

3. 二级缓存

3.1 什么是二级缓存

二级缓存,属于Mapper层级的缓存,意思是一个Mapper文件就有一个缓存,当这个Mapper文件中的查询语句执行后,会将结果保存在对应Mapper缓存中(如果开启了的话),这样,不同的SqlSession操作同一个Mapper文件执行查询操作时,会先去Mapper缓存中查询。所以说二级缓存是跨SqlSession的。

img

图源:https://tech.meituan.com/2018/01/19/mybatis-cache.html

3.2 开启二级缓存

首先在mybatis.xml配置文件中开启缓存:

xml
<settings>
  <setting name="cacheEnabled" value="true"/>
</settings>

然后在需要开启缓存的Mapper XML 文件中添加以下标签,开启缓存:

xml
<mapper namespace="xxx">
    <cache/>
</mapper>

最后,对需要缓存的数据添加序列化能力,也就是继承Serializable接口:

java
@Data
@EqualsAndHashCode
public class DemoEntity implements Serializable {
    private Integer id;
    private String value1;
    private String value2;
}

之后,我们就可以使用二级缓存了:

java
@Test
public void test() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    DemoEntity demoEntity1 = sqlSession1.selectOne("org.example.dao.DemoDao.queryById",1);
    log.info("demoEntity1: {}", demoEntity1);

  	// 一定要关闭,二级缓存才生效
    sqlSession1.close();

    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    DemoEntity demoEntity2 = sqlSession2.selectOne("org.example.dao.DemoDao.queryById",1);
    log.info("demoEntity2: {}", demoEntity2);

    log.info("demoEntity1 == demoEntity2: {}", demoEntity1 == demoEntity2);
}

从结果可以看到,sqlSession2没有查询数据库,而是走的缓存。

3.3 MappedStatement

在 MyBatis 中,MappedStatement 是一个非常核心且重要的内部对象。简单来说,它代表了 MyBatis Mapper XML 文件中定义的一个完整的 SQL 操作(比如 <select><insert><update><delete> 标签)。

MyBatis 在启动时会解析所有的 Mapper XML 文件,并将每个 <select><insert><update><delete> 标签以及它们的属性和子元素封装成一个 MappedStatement 对象。这些 MappedStatement 对象会被存储在 Configuration 对象中,供后续的 SQL 执行查找和使用。

最重要的是,在 MappedStatement 对象,有一个缓存属性cache,这就是二级缓存。

首先,在项目中多加一个DAO 接口,整体项目结构如下:

image-20250716143110718

然后用反射的方式,从Configuration中获取MappedStatement

java
public void test1() throws IOException, NoSuchFieldException, IllegalAccessException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    Configuration configuration = sqlSessionFactory.getConfiguration();

    // 用反射的方式获取mappedStatementMap
    Field mappedStatementsField = Configuration.class.getDeclaredField("mappedStatements");
    mappedStatementsField.setAccessible(true);
    Map<String, MappedStatement> mappedStatementMap =
            (Map<String, MappedStatement>) mappedStatementsField.get(configuration);

    Set<Object> cacheSet = new HashSet<>();
    mappedStatementMap.forEach((key, value) -> {
        System.out.println(key + " ---> " + value);
        cacheSet.add(value.getCache());
    });

    System.out.println(cacheSet);
}

可以看到,每条SQL语句(每个方法),都有一个全限定名的key,和一个以方法名的key。

最主要的是关注cache

txt
[org.apache.ibatis.cache.decorators.SynchronizedCache@70ed52de, org.apache.ibatis.cache.decorators.SynchronizedCache@6be968ce]

可以发现,只有两个缓存,也就是说一个Mapper XML文件只对应一个缓存,这个缓存的实现是SynchronizedCache(其实就是PerpetualCache的装饰)。

txt
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache

img

图源:https://tech.meituan.com/2018/01/19/mybatis-cache.html

一个Mapper XML文件只对应一个缓存,也就说明了为什么SqlSession可以共享二级缓存。

在以下方法中,可以查看二级缓存的作用:

txt
org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
java
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
    CacheKey key, BoundSql boundSql) throws SQLException {
  // 从MappedStatement中获取二级缓存
  Cache cache = ms.getCache();
  if (cache != null) {
    // 二级缓存不为空
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      // 从二级缓存中查询数据
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        // 二级缓存中没有数据,则委托其他Executor查询数据,就是之前的一级缓存-->数据库的逻辑
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        // 将数据放入二级缓存
        tcm.putObject(cache, key, list); 
      }
      return list;
    }
  }
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

3.4 TCM是什么

TCM是TransactionalCacheManager类,在CachingExecutor中作为属性存在:

java
public class CachingExecutor implements Executor {

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
}

TCM的存在是管理事务中的查询语句:

  • 当事务提交时(commit()),TCM才会将查询结果刷新到二级缓存中,其他SqlSession才能查询到二级缓存中的数据;
  • 当事务回滚时(rollback()),TCM会将事务中的查询结果丢弃,不保存到二级缓存中;

TransactionalCacheManager结构如下:

java
public class TransactionalCacheManager {

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

  // 事务提交
  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

  // 事务回滚
  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }
}

TransactionalCache的结构如下:

java
public class TransactionalCache implements Cache {

  // 实际的二级缓存
  private final Cache delegate;  
  private boolean clearOnCommit;
  // 待保存或丢弃的缓存数据
  private final Map<Object, Object> entriesToAddOnCommit;
  // 未命中缓存的键
  private final Set<Object> entriesMissedInCache;

  // 事务提交
  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }
  
  // 事务回滚
  public void rollback() {
    unlockMissedEntries();
    reset();
  }
  
  // 将事务中的查询结果清空,也就是不保存到二级缓存中
  private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }

  // 将事务中的查询结果保存到二级缓存中
  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }
}

当关闭SqlSession时,会根据配置判断是否回滚或提交:

txt
org.apache.ibatis.executor.CachingExecutor#close
java
@Override
public void close(boolean forceRollback) {
  try {
    if (forceRollback) {
      tcm.rollback();
    } else {
      tcm.commit();
    }
  } finally {
    delegate.close(forceRollback);
  }
}

3.5 二级缓存小结

有关二级缓存的类图关系如下:

image-20250716152017430

  • 首先,使用SqlSession查询数据时,会调用CachingExecutor的查询方法;
  • CachingExecutor的查询方法中,会先从MappedStatement中获取二级缓存Cache(可以把二级缓存理解为两部分:一部分是放生效的缓存数据,一部分是放未生效的缓存数据);
    • 从二级缓存(已生效的部分)中查询数据,如果二级缓存中没有数据,那么就委托BaseExecutor查询数据,从一级缓存或数据库中查询;
      • 查询到数据后,将查询结果放入未生效的缓存数据中
    • 如果二级缓存(已生效的部分)中查询到数据,直接返回;
  • 调用方法结束,根据情况调用以下三种方法:
    • commit():将二级缓存中未生效的缓存数据,放入已生效的数据中;
    • rollback():丢弃二级缓存中未生效的缓存数据;
    • close():根据配置调用commit()rollback()

3.6 二级缓存使用案例

commit() / rollback() / close() 对二级缓存的影响

java
@Test
public void test() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    DemoEntity demoEntity1 = sqlSession1.selectOne("org.example.dao.DemoDao.queryById",1);
    log.info("demoEntity1: {}", demoEntity1);

    //sqlSession1.rollback();  // 二级缓存不生效
    sqlSession1.commit();  // 二级缓存生效
    //sqlSession1.close();  // 二级缓存生效

    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    DemoEntity demoEntity2 = sqlSession2.selectOne("org.example.dao.DemoDao.queryById",1);
    log.info("demoEntity2: {}", demoEntity2);

    log.info("demoEntity1 == demoEntity2: {}", demoEntity1 == demoEntity2);
}

TIP

有一个奇怪的问题,最后demoEntity1 == demoEntity2的比较结果是false,为什么?

因为在前面缓存的装饰链上,存在SerializedCache,这个缓存器用于序列化与反序列缓存数据,所以反序列化产生的对象是不一样的。

update / insert / delete 对二级缓存的影响

增删改操作对二级缓存的影响,需要分两步来看:

  • 第一步,执行增删改操作,在这一步中,会将二级缓存中未生效的部分清空,然后将TransactionalCache.clearOnCommit置为true
  • 第二步,执行commit()rollback()操作:
    • 执行commit()操作:由于将TransactionalCache.clearOnCommit置为true,所以会将二级缓存中已生效的部分清空,然后再把未生效的数据放入已生效的部分中(有可能在同一个事务中,执行了增删改操作后,又执行了查询操作,所以未生效的部分也是有数据的),最后将TransactionalCache.clearOnCommit置为false
    • 执行rolback()操作:将未生效的部分清空,将TransactionalCache.clearOnCommit置为false

以下代码是第一步的体现(org.apache.ibatis.executor.CachingExecutor中的方法):

java
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
  flushCacheIfRequired(ms);
  return delegate.update(ms, parameterObject);
}

private void flushCacheIfRequired(MappedStatement ms) {
  Cache cache = ms.getCache();
  if (cache != null && ms.isFlushCacheRequired()) {
    tcm.clear(cache);
  }
}

3.7 其他配置

在Mapper XML 中的<cache>`标签用于声明这个namespace使用二级缓存:

xml
<cache/>

<cache>还有以下属性可以配置:

  • typecache使用的类型,默认是PerpetualCache,我们可以实现自己的缓存,例如将缓存数据保存到Redis中;
  • eviction: 定义回收的策略,常见的有:
    • FIFO:先进先出,最先保存的缓存数据被回收;
    • LRU:最近最少使用的缓存数据被回收;
  • flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
  • size: 最多缓存对象的个数。
  • readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
  • blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。

cache-ref代表引用别的命名空间的Cache配置,这样两个命名空间的操作使用的就是同一个Cache。

xml
<cache-ref namespace="mapper.StudentMapper"/>

在增删改查标签中,有以下属性与缓存有关:

xml
<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>
  • flushCache:是否刷新缓存,这里的刷新是指清空未生效的部分,并且在提交时先清空已生效的部分;

    java
    private void flushCacheIfRequired(MappedStatement ms) {
      Cache cache = ms.getCache();
      if (cache != null && ms.isFlushCacheRequired()) {  //ms.isFlushCacheRequired()
        tcm.clear(cache);
      }
    }
  • useCache:是否使用缓存,只对select有效;

4. 总结

本文了解了MyBatis的缓存机制,包括一级缓存和二级缓存,总的建议是:

  • 可以使用一级缓存;
  • 不使用二级缓存,原因如下:
    • 数据一致性问题:如果数据在外部被修改(例如,通过其他应用程序直接修改数据库,或者在不同的 Mapper 中修改),二级缓存可能无法感知,导致缓存中的数据与数据库不一致。
    • 引入二级缓存会增加内存消耗,并且缓存的序列化/反序列化、更新和淘汰等操作都会带来一定的性能开销。对于更新频繁的数据,过度使用二级缓存反而可能降低性能,因为缓存的维护成本会超过查询带来的收益。

TIP

MyBatis缓存会带来脏读问题吗?

在一级缓存中,假如一个SqlSession1查询并缓存了数据A,另一个SqlSession2修改了数据A为A1,但是SqlSession1无法感知,仍然会查询缓存,查到旧数据。这算脏读吗?

应该不算,一级缓存的作用域可以理解为事务,那么这和事务隔离级别的要求相符。

在二级缓存中,一个Mapper XML中的缓存有了数据,并且该SqlSession已结束,之后数据在另一个Mapper或其他应用程序被修改了,那么之后再次查询该数据,仍然会走缓存。

所以,我认为二级缓存是会带来脏读问题的。

参考资料

[1] https://tech.meituan.com/2018/01/19/mybatis-cache.html

[2] https://www.bilibili.com/video/BV1zA411N7oN