Mybatis-02 集成Spring后一级缓存失效

一. 问题

我们知道 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 对象;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值