一. 问题
我们知道 Mybatis 的一级缓存作用域是 SqlSession,或者说是 Executor;当我们用 Spring 与 Mybatis 作集成时,会出现一级缓存 “失效” 的现象;这是为什么?我们先看下面这个例子;
在这个例子中,使用 UserMapper 调了两次 selectById() 方法;
public User getUserById(int id) {
User user = userMapper.selectById(id);
userMapper.selectById(id);
log.info("getUserById: {}", user);
return user;
}
观察日志会发现创建了两次 SqlSession,一级缓存肯定没有生效;
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@229b1f9b] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@222783764 wrapping com.mysql.cj.jdbc.ConnectionImpl@59e46c53] will not be managed by Spring
==> Preparing: select id, username, password from user where id=?
==> Parameters: 4(Integer)
<== Columns: id, username, password
<== Row: 4, zq0444, 111122
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@229b1f9b]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7f483f66] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@332184096 wrapping com.mysql.cj.jdbc.ConnectionImpl@59e46c53] will not be managed by Spring
==> Preparing: select id, username, password from user where id=?
==> Parameters: 4(Integer)
<== Columns: id, username, password
<== Row: 4, zq0444, 111122
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7f483f66]
二. 解决
解决方式是给 service 层方法加上事务;
@Transactional
public User getUserById(int id) {
User user = userMapper.selectById(id);
userMapper.selectById(id);
log.info("getUserById: {}", user);
return user;
}
观察日志会发现只创建了一个一次 SqlSession;
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25d76791]
JDBC Connection [HikariProxyConnection@490203255 wrapping com.mysql.cj.jdbc.ConnectionImpl@7690908f] will be managed by Spring
==> Preparing: select id, username, password from user where id=?
==> Parameters: 4(Integer)
<== Columns: id, username, password
<== Row: 4, zq0444, 111122
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25d76791]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25d76791] from current transaction
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25d76791]
2024-06-15 20:06:28.969 INFO 1484 --- [nio-8080-exec-2] com.example.demo.service.SqlService : getUserById: User(id=4, username=zq0444, password=111122)
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25d76791]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25d76791]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25d76791]
为什么会出现这种现象呢?我们需要跟进源码 debug 看看;
三. 原因
1. UserMapper分析
Spring 为我们生成的 UserMapper 是一个代理类;
这里需要重点看一下 UserMapper 类的结构;
1、这个 UserMapper 是一个代理类,它的 InvocationHandler 是 MapperProxy 类;
2、MapperProxy 内聚合了一个 SqlSessionTemplate 类,SqlSessionTemplate 实现了 SqlSession 类;
3、MapperProxy 的 invoke() 调用了目标类 sqlSessionTemplate 的方法;
4、SqlSessionTemplate 内聚合了一个 sqlSessionProxy,sqlSessionProxy 也是一个代理类,它实现了 SqlSession 类,它的 invocationHandler 是 SqlSessionInterceptor 类;
5、我们需要看 SqlSessionInterceptor 的 Invoke() 做了啥;
类图分析如下,其实代理的目标类是 SqlSession,干活的类是 SqlSession;
我们主要看 SqlSessionInterceptor 的 invoke();
2. SqlSessionInterceptor
我们直接看 SqlSessionInterceptor 的 invoke();
// -------------------------- SqlSessionInterceptor -------------------------
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// 1. 获取 SqlSession 对象
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
// 2. 执行目标类目标方法,也就是 sqlSession.xxx()
Object result = method.invoke(sqlSession, args);
// 3. 如果当前不是事务的话,会执行 sqlSession.commit(true)
if (!isSqlSessionTransactional(sqlSession,
SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
// ...
} finally {
closeSqlSession(sqlSession,SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
其中最重要的是 getSession();
// ---------------------------- SqlSessionUtils ---------------------------------
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory,
ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
// 1. 获取 SqlSessionHolder 对象
// SqlSessionHolder 是从 ThreadLocal 线程变量中获取的
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
// 2. 从 SqlSessionHolder 中获取 SqlSession
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
// 3. 如果线程变量中没有 SqlSession 的话,需要通过 SqlSessionFactory 创建出 sqlSession
// SqlSessionFactory.openSession() 会创建出 SqlSession 和关联的 Executor
LOGGER.debug(() -> "Creating a new SqlSession");
session = sessionFactory.openSession(executorType);
// 4. 将 sqlSession 注册到 SessionHolder 中
// 这里只有开启了事务才会注入到 SessionHolder 中
// 没有开启事务的话不会注入到 SessionHolder 中
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
// 5. 返回该 SqlSession
return session;
}
registerSessionHolder() 是有条件的,只有开启了事务才会注入;
// ---------------------------- SqlSessionUtils ---------------------------------
private static void registerSessionHolder(SqlSessionFactory sessionFactory,
ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator,
SqlSession session) {
SqlSessionHolder holder;
// 1. 在开启事务的情况下
// 在 @Transactional 相关的 TransactionInterceptor 中
// 开启事务时,会使 TransactionSynchronizationManager.isSynchronizationActive() true
if (TransactionSynchronizationManager.isSynchronizationActive()) {
Environment environment = sessionFactory.getConfiguration().getEnvironment();
// 2. 如果当前是 Spring 管理的事务
if (environment.getTransactionFactory() instanceof
SpringManagedTransactionFactory) {
LOGGER.debug("Registering transaction synchronization for SqlSession");
// 3. 将 SqlSession 绑定到 TransactionSynchronizationManager 的线程变量里
holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
TransactionSynchronizationManager.bindResource(sessionFactory, holder);
TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
holder.setSynchronizedWithTransaction(true);
holder.requested();
} else {
// ...
}
} else {
LOGGER.debug("SqlSession was not registered for synchronization because synchronization is not active");
}
}
到此,我们就知道了,只有开启事务的情况下,UserMapper.selectById() 才能获取到同一个 SqlSession 对象,进而实现一级缓存;否则每次都会重新创建一个 SqlSession 对象;