理解Mybatis一级缓存,以及如何真正使用到一级缓存

阅读这篇文章,你将会了解
1.什么是会话(SqlSession)、执行器(Executor)
2.什么是Mybatis一级缓存
3.一级缓存的生命周期
4.一级缓存的CacheKey生成策略
5.在日常开发时,怎么才能用到一级缓存(通过事务)

一.类关系图:

二.什么是会话(SqlSession)

  1. 在Mybatis中,SqlSession可以理解为数据库访问的最小粒度,每次的数据库访问,都建立在SqlSession的基础上。

  2. SqlSession的实现类有3种:
    (1)SqlSessionTemplate:与Spring整合时,用作代理的SqlSession,底层实际上还是使用DefaultSqlSession。
    (1)SqlSessionManager:在没有和Spring整合时,用作代理的SqlSession,底层实际上还是使用DefaultSqlSession。
    (3)DefaultSqlSession:默认的会话实现类,即后续所讲的执行期、缓存都是与DefaultSqlSession相关。

  3. 关于SqlSessionTemplate和SqlSessionManager的区别,可以看这篇文章:
    https://blog.csdn.net/u010841296/article/details/89367296

  4. 每个DefaultSqlSession都会实例化一个专属自己的Executor对象,以及一个Configuration对象。

public class DefaultSqlSession implements SqlSession {
	//配置信息
	private Configuration configuration;
	//执行器
	private Executor executor;
public class Configuration {
	protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
  1. Configuration对象里面记录了Mybatis解析xml、扫描mapper获取到的信息。
  2. 在DefaultSqlSession中,虽然定义了很多sql操作的基础接口,但是sql操作具体的逻辑并不是在SqlSession完成,而是交托给执行器(Executor)完成。
  3. DefaultSqlSession负责从mappedStatements中拿到用户调用方法全名(statement)对应的MappedStatement,这个MappedStatement实际上就是用户自定义的新增该查的Mapper方法。这个是有Mybatis启动时进行扫描绑定的。
    例如用户定义了一个方法Dao接口:com.jenson.pratice.mapper.TestMapper.selectByID,
    那么在mappedStatements中就会有一个键值对(“com.jenson.pratice.mapper.TestMapper.selectByID”,mappedStatement)

三.什么是执行器(Executor)

  1. Executor是DefaultSqlSession中的一个类属性,定义了基础的sql操作,比如事务的获取、提交、回滚,数据的插入、查询、更改、删除等基本操作。
  2. Executor不负责创建事务,但是Executor里面会有事务引用。
  3. 执行器里面会实例化一级缓存对象。
  4. 相比与DefaultSqlSession,Executor的职责是拿到MappedStatement后,进行具体的数据库访问逻辑。包括一级缓存的维护,也是在Executor中完成的。
public abstract class BaseExecutor implements Executor {
	//事务引用
	protected Transaction transaction;
	//缓存对象
	protected PerpetualCache localCache;

四.什么是Mybatis一级缓存(PerpetualCache)

Mybatis一级缓存实际上就是一个依赖于SqlSession的缓存对象,PerpetualCache里面的结构很简单,通过一个k-v结构的cache维护缓存数据。

public class PerpetualCache implements Cache {
  private final String id;
  private Map<Object, Object> cache = new HashMap<Object, Object>();

一级缓存的生命周期:

  1. PerpetualCache的生命周期是和SqlSession相关的,即只有在同一个SqlSession中,一级缓存才会用到。如果会话介绍,则缓存会清空;
  2. 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
  3. 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;
  4. SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用;

Mybatis在什么时候使用一级缓存:

  1. Mybatis在查询的时候会先生成一个CacheKey,判断当前会话的PerpetualCache中是否有这个key的缓存,如果有则返回,没有则执行查询。查询完成后会把结果集放入缓存。
  2. 在CachingExecutor、BaseExecutor中都有维护缓存的逻辑。其中CachingExecutor针对的是二级缓存,BaseExecutor针对的是一级缓存。
public abstract class BaseExecutor implements Executor {
  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);
      }
  1. 实际使用中,查询的调用链可能是这样的:

DefaultSqlSession --> CacheExecutor --> BaseExecutor --> SimpleExecutor

一级缓存如何生成CacheKey:

在BaseExecutor中,定义了一个方法(createCacheKey)去生成一个CacheKey。通过这个CacheKey判断2次查询是否相同,能否使用缓存。

这个CacheKey的值由下面几个部分组成:

  1. ms.getId():MappedStatement的ID
  2. 分页查询的参数offset、limit:这个是指MyBatis自身提供的分页功能是通过RowBounds来实现的,它通过rowBounds.offset和rowBounds.limit来过滤查询出来的结果集,这种分页功能是基于查询结果的再过滤,而不是进行数据库的物理分页。所以不要把他和sql语句中的分页混淆了。
  3. boundSql.getSql():这个sql字符串仍未处理参数的填充
  4. parameterMappings:会遍历这个列表,得到实际需要填充进sql的参数,然后放到CacheKey中
  5. configuration.getEnvironment().getId():一个环境id,解决issue #176的bug。
public abstract class BaseExecutor implements Executor {
   @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        //省略一堆代码。。。。。
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

开发时,如何使用到一级缓存:

我们经常在某个方法中进行多次数据库查询,在实际场景中,每次的数据库查询都会开启一个新的会话(SqlSession)。这种情况下我们是没有用到一级缓存的,因为根本就没有复用到SqlSession。

例如这样:

@Service("testService")
public class TestService {
    @Autowired
    private TestMapper testMapper;
    public void get(){
        System.out.println(testMapper.selectByID(1));
        System.out.println(testMapper.selectByID(1));
    }
}

get start
2019-05-02 17:44:34.097 DEBUG 98729 — [ main] c.j.p.mapper.TestMapper.selectByID : ==> Preparing: select * from test where id = ?
2019-05-02 17:44:34.121 DEBUG 98729 — [ main] c.j.p.mapper.TestMapper.selectByID : > Parameters: 1(Integer)
2019-05-02 17:44:34.140 DEBUG 98729 — [ main] c.j.p.mapper.TestMapper.selectByID : <
Total: 1
2019-05-02 17:44:34.142 DEBUG 98729 — [ main] c.j.p.mapper.TestMapper.selectByID : ==> Preparing: select * from test where id = ?
2019-05-02 17:44:34.142 DEBUG 98729 — [ main] c.j.p.mapper.TestMapper.selectByID : > Parameters: 1(Integer)
2019-05-02 17:44:34.143 DEBUG 98729 — [ main] c.j.p.mapper.TestMapper.selectByID : <
Total: 1
get end

那么我们怎样控制程序复用SqlSession,使get()能用到一级缓存呢?
其中一种办法就是开启一个事务:

@Service("testService")
public class TestService {
    @Autowired
    private TestMapper testMapper;
    
    //开启事务
    @Transactional(rollbackFor = Exception.class)
    public void get(){
        System.out.println("get start");
        testMapper.selectByID(1);
        testMapper.selectByID(1);
        System.out.println("get end");
    }
}

get start
2019-05-02 17:46:31.689 DEBUG 98744 — [ main] c.j.p.mapper.TestMapper.selectByID : ==> Preparing: select * from test where id = ?
2019-05-02 17:46:31.715 DEBUG 98744 — [ main] c.j.p.mapper.TestMapper.selectByID : > Parameters: 1(Integer)
2019-05-02 17:46:31.734 DEBUG 98744 — [ main] c.j.p.mapper.TestMapper.selectByID : <
Total: 1
get end

为什么开启了事务后,就可以复用SqlSession呢?

这里有2个关注点

1.TransactionInterceptor拦截器会对事务方法进行切面。

大致的调用链路是:
TransactionInterceptor.invoke
->TransactionAspectSupport.invokeWithinTransaction
->TransactionAspectSupport.createTransactionIfNecessary
->PlatformTransactionManager.getTransaction(txAttr)
->AbstractPlatformTransactionManager.getTransaction
->AbstractPlatformTransactionManager.prepareSynchronization
->TransactionSynchronizationManager.initSynchronization()

最终在TransactionSynchronizationManager.initSynchronization()中设置事务激活。

public abstract class TransactionSynchronizationManager {
	private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
			new NamedThreadLocal<>("Transaction synchronizations");
			
	public static void initSynchronization() throws IllegalStateException {
		if (isSynchronizationActive()) {
			throw new IllegalStateException("Cannot activate transaction synchronization - already active");
		}
		logger.trace("Initializing transaction synchronization");
		synchronizations.set(new LinkedHashSet<>());
	}

2.SqlSessionUtil.registerSessionHolder

在SqlSessionUtil.registerSessionHolder方法中,只有当TransactionSynchronizationManager.isSynchronizationActive()为true的时候,才会在对应ThreadLocal中注册SqlSessionHolder(包装了SqlSession)。这时候在一个事务中的数据库访问都会复用同一个SqlSession,所以可以用上一级缓存。

整个链路的入口是在SqlSessionTemplate中,SqlSessionTemplate中定义了一个拦截器SqlSessionInterceptor,在getSqlSession()的时候,会先查看是否有绑定SqlSession,如果没有则开启一个新会话,有则复用会话。

调用链路:
->SqlSessionInterceptor.invoke
->SqlSessionUtil.getSqlSession
->SqlSessionUtils.registerSessionHolder

  public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
	//是否有可复用的SqlSession
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Creating a new SqlSession");
    }
	//没有则打开一个会话
    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

只有事务激活的时候,才能注册SqlSessionHolder,复用SqlSession

  private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
    SqlSessionHolder holder;
    //只有事务激活的时候,才能注册SqlSessionHolder,复用SqlSession
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
      Environment environment = sessionFactory.getConfiguration().getEnvironment();

      if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registering transaction synchronization for SqlSession [" + session + "]");
        }

        holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
        TransactionSynchronizationManager.bindResource(sessionFactory, holder);
        TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
        holder.setSynchronizedWithTransaction(true);
        holder.requested();

判断事务是否激活

	/**
	 * Return if transaction synchronization is active for the current thread.
	 * Can be called before register to avoid unnecessary instance creation.
	 * @see #registerSynchronization
	 */
	public static boolean isSynchronizationActive() {
		return (synchronizations.get() != null);
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章