一级缓存

先声明一个测试类FirstCacheTest,然后我们就可以在里面写测试方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;

public class FirstCacheTest {
private SqlSession sqlSession;

@Before
public void init() {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(UserMapper.class.getResourceAsStream("/mybatis-config.xml"));
sqlSession = sqlSessionFactory.openSession();
}
}

一级缓存的命中场景

一张图了解下一级缓存的命中场景

Mybatis一级缓存命中场景1

运行时参数相关

SQL和参数必须相同
1
2
3
4
5
6
7
8
@Test
public void test1() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
UserPO userPO = userMapper.selectUserById(1L);
UserPO userPO1 = userMapper.selectUserById(2L);
// false
System.out.println(userPO == userPO1);
}
必须是相同的statementID

第一个查询的staementID是: org.example.mybatis_reader.mybatis.selectUserById

第二个查询的staementID是: org.example.mybatis_reader.mybatis.selectUserById1

1
2
3
4
5
6
7
8
@Test
public void test2() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
UserPO userPO = userMapper.selectUserById(1L);
UserPO userPO1 = userMapper.selectUserById1(1L);
// false
System.out.println(userPO == userPO1);
}
必须是同一个SqlSession
1
2
3
4
5
6
7
8
@Test
public void test3() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
UserPO userPO = userMapper.selectUserById(1L);
UserPO userPO1 = sqlSession.selectOne("org.example.mybatis_reader.mybatis.UserMapper.selectUserById", 1L);
// TRUE
System.out.println(userPO == userPO1);
}
RowBounds返回行范围必须相同

分页信息必须相同

1
2
3
4
5
6
7
8
9
10
@Test
public void test4() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 默认为 RowBounds.DEFAULT
UserPO userPO = userMapper.selectUserById(1L);
List<UserPO> userPO1 = sqlSession.selectList("org.example.mybatis_reader.mybatis.UserMapper.selectUserById",
1L, new RowBounds(0,5));
// FALSE
System.out.println(userPO == userPO1.get(0));
}

操作与配置相关

未手动清空缓存
1
2
3
4
5
6
7
8
9
10
@Test
public void test5() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
UserPO userPO = userMapper.selectUserById(1L);
// 清空缓存
sqlSession.clearCache();
UserPO userPO1 = userMapper.selectUserById(1L);
// FALSE
System.out.println(userPO == userPO1);
}
未调用flushCache=true的查询

即未设置flushCache = Options.FlushCachePolicy.TRUE

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test6() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
UserPO userPO = userMapper.selectUserById(1L);
// selectUserById1方法设置了flushCache,会导致一级缓存失效 ,等同于调用了clearCache()
userMapper.selectUserById1(1L);
UserPO userPO1 = userMapper.selectUserById(1L);
// FALSE
System.out.println(userPO == userPO1);
}
@Select("SELECT * FROM T_USER WHERE id = #{id}")
@Options(flushCache = Options.FlushCachePolicy.TRUE)
UserPO selectUserById1(Long id);
未执行update操作
1
2
3
4
5
6
7
8
9
10
@Test
public void test7() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
UserPO userPO = userMapper.selectUserById(1L);
// 执行了更新操作,会清空一级缓存
userMapper.updateUserNameById1(1L, "张三改名");
UserPO userPO1 = userMapper.selectUserById(1L);
// FALSE
System.out.println(userPO == userPO1);
}
缓存作用域不是STATEMENT

默认的缓存作用域是SESSION,如果要修改就在mybatis-config.xml中的中添加一个配置

1
2
3
<settings>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
1
2
3
4
5
6
7
8
@Test
public void test8() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
UserPO userPO = userMapper.selectUserById(1L);
UserPO userPO1 = userMapper.selectUserById(1L);
// FALSE
System.out.println(userPO == userPO1);
}

一级缓存源码流程

Mybatis一级缓存执行过程

一级缓存特性

  1. 与会话相关
  2. 与参数条件相关
  3. 提交、修改会清空缓存

一级缓存的失效场景

在Spring的体系下,Mybatis的一级缓存是失效的。根本原因是Spring代理的Mapper类在发起调用时是新构建一个会话的,而一级缓存恰好是会话级别的,不同会话级别的缓存肯定是不通用的,这也是为什么一级缓存会失效;在我们添加了事务后,所有的请求都在一个会话下,一级缓存也就再次生效了。我们可以验证一下。

我们需要将程序改造一下,首先在resources目录下新建spring.xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- DataSource configuration -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/blog_db"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>

<!-- SqlSessionFactory configuration -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>
<!-- MapperScannerConfigurer configuration -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="org.example.mybatis_reader.mybatis"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

<!-- TransactionManager configuration -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

</beans>

然后执行下测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 通过 Spring构建的Mapper会导致一级缓存失效
*/
@Test
public void test9() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserMapper userMapper = context.getBean(UserMapper.class);
UserPO userPO = userMapper.selectUserById(1L);
UserPO userPO1 = userMapper.selectUserById(1L);
// FALSE
System.out.println(userPO == userPO1);
}

/**
* 改造一下,加入事务,会发现一级缓存又生效了
*/
@Test
public void test10() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
// 开启事务
DataSourceTransactionManager trxManager = (DataSourceTransactionManager) context.getBean("transactionManager");
trxManager.getTransaction(new DefaultTransactionAttribute());
UserMapper userMapper = context.getBean(UserMapper.class);
UserPO userPO = userMapper.selectUserById(1L);
UserPO userPO1 = userMapper.selectUserById(1L);
// TRUE
System.out.println(userPO == userPO1);
}

那么问题来了,同样是用UserMapper发起的调用,结果为什么不同呢?关键就在于UserMapper的构造上。在前面的例子中,我们使用的是UserMapper本身,但是在test9()和test10()中,Spring通过代理的方式对UserMapper进行了增强,所以它的骨骼是这样的:

image-20240607110049609

现在可以看下为什么test09()中一级缓存会失效,但是在test10()中会生效了。

在会话拦截器SqlSessionInterceptor构建SqlSession时,是使用的SqlSessionUtils工具类的,看下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取SqlSession
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) {
// 省略代码
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}

看下getSqlSession方法的实现,发现它是通过sessionHolder方法去构建SqlSession,然后判断sessionHolder方法的返回值是否是null,不是的话就直接返回这个sqlSession。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {

notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
// 如果开启了事务,holder会有值,否则就是NULL
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
// 构建SqlSession
SqlSession session = sessionHolder(executorType, holder);
if (session != null) { // 不是null,直接返回
return session;
}

LOGGER.debug(() -> "Creating a new SqlSession");
session = sessionFactory.openSession(executorType);
// 构建并注册SqlSessionHolder
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

return session;
}

重点就在sessionHolder()方法中,这里分为两种情况

如果holder不为NULL并且holder的事务标识为TRUE,就从holder中获取SqlSession;否则返回NULL值。这也是为什么开启了事务的时候走了一级缓存的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}