0%

mybatis执行器

mybatis执行器

mybatis中包含四大件是指:executor, statementHandler,parameterHandler,resultHandler对象

它们都是sqlSession的底层类实现,本篇的重点是了解一下executor执行器的作用和一些原理。

mybatis中有三类执行器:

  • 简单执行器:SimpleExecutor,是执行器的默认实现,继承了BaseExecutor抽象类,利用StatementHandler完成。每次调用执行方法都会构建一个StatementHandler,并预设参数,然后执行。
  • 可重用执行器:ReuseExecutor,可重复使用JDBCStatement,减少预编译次数。该执行器在执行SQL时会把Statement缓存起来,如果下次碰到相同的SQL,就会取出来使用。
  • 批处理执行器:BatchExecutor,每次的执行操作 不会立即提交到数据库,而是把对应的Statement对象填充好参数之后暂存起来。调用doFlushStatements的时候一次性提交到数据库,可用于批处理插入的场景。

执行器.png

执行器的顶层接口为Executor,它定义了数据修改、数据查询、缓存维护等基本功能,还有一些辅助的API接口,比如提交事务、回滚事务等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 简单列几个接口,还有一些没列出来
public interface Executor {

int update(MappedStatement ms, Object parameter) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

void commit(boolean required) throws SQLException;

void rollback(boolean required) throws SQLException;

CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

}

抽象接口BaseExecutor实现Executor并提供一些通用能力,比如有一级缓存,获取连接等。比如针对查询来说,它实现了顶层接口的query方法,并实现一级缓存的相关逻辑,若缓存查询不同,通过doQuery方法,下放给具体实现子类,也就是说,子类只需要实现具体的doQuery方法。

mybatis中有三种执行器,分别为简单执行器、可重用执行器、批处理执行器。在这种结构下,若希望添加一级缓存应该如何处理?可以在BaseExecutor上层抽象一层接口,这个抽象接口实现二级缓存的逻辑。但是mybatis不采用这种方式,它通过装饰者模式,构建CachingExecutor类实现于Executor接口,并将Executor作为他的一个属性,当自身的二级缓存获取不到值时,调用delegate的同名方法,进入具体的执行器中。

一级缓存

一级缓存.png

​ 一级缓存实际上就是一个HashMap,根据一些相关的参数信息生成一个key,结果集为值的hashMap。它的CacheKey的主要参数为statementIDSQL,执行参数等,所以有时候尽管是一模一样的SQL也不会命中mybatis的一级缓存。

mybatis的代码是极其精简的,这是仅是使用HashMap来存储一级缓存的内容,它甚至都不使用concurrentHashMap,这是因为SqlSession本身就不是线程安全的,对于SqlSession来说他需要创建一个执行器,执行器对应于一条statementID,如果出现并发,两个线程获取同一个SqlSession,那么就有可能导致执行器中绑定的statementId不一致,导致得到一级缓存的返回类型值不一样,从而报错。并且SqlSession获取对应的JDBCConnection连接同时得到事务,如果两个线程获取同一个线程,那么就表示两个线程拥有同一个事务,所以SqlSession不是线程安全了,在Spring中通过很多的手段去保证并发情况下SqlSession的线程安全。

​ 所以一级缓存是与会话相关,如果会话被关闭,那么一级缓存就失效。

一级缓存执行流程.png

​ 由mybatis生成对应的mapper的动态代理,在执行对应的SQL方法时,开启一个SqlSession会话,通过会话和相关信息开启一个执行器,在执行器中先调用BaseExecutor#query(),具体的一级缓存逻辑就是在query()方法中,如果一级缓存中获取不到,调用执行器实现子类的doQuery()方法。

1
2
3
4
5
6
7
8
@Test
public void testBySpring(){
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserMapper mapper = context.getBean(UserMapper.class);
User user = mapper.selectById(1);
User user1 = mapper.selectById(1);
System.out.println(user == user1);
}

​ 看一段结合Spring之后的缓存问题,测试结果为false,说明一级缓存没有生效,这是由于Spring在每次执行具体的调用时都自动进行了事务的提交,也就是说每执行一次selectById就会开启一次会话。所以很多人会说在Spring集成了mybatis之后,mybatis的一级缓存失效了。

​ 那么如果就是想在Spring中使用mybatis的一级缓存。可以通过手动开启事务的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testBySpringTransaction(){
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserMapper mapper = sqlSession.getMapper(UserMapper.class);

DataSourceTransactionManager transactionManager = (DataSourceTransactionManager)context.getBean("txManager");
// 手动开启事务
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

User user = mapper.selectById(1);
User user1 = mapper.selectById(1);
System.out.println(user == user1);
}

Spring中对Mapper做了一些动态代理的处理,通过getBean()获取到的UserMapper对象实际上已经是被Spring动态代理过的对象,

spring结合mybatis会话.png

  • Mapper:是被spring修改过的动态代理对象,做一些statementID相关预处理等操作。
  • SqlSessionTemplate:它实现了SqlSession接口,并将数据库的相关操作,比如queryupdateinsert转发给具体的sqlSession。而它的能力为拦截之后获取对应的sqlSession会话对象。
  • SqlSessionFactory:去创建对应的事务、执行器,然后返回DefaultSqlSession对象。

那么现在来看一些spring使得mybatis一级缓存没有生效的具体源码:

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

// 省略一些代码..

// 获取一个SqlSessionHolder,这个是存储在ThreadLocal对象中
// 也就是说它是跟线程绑定,与事务和线程绑定的原理对应上
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

// 从这里获取对应的session
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}

LOGGER.debug(() -> "Creating a new SqlSession");
session = sessionFactory.openSession(executorType);

registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

return session;
}

可以看出在sessionHolder处如果获取到对应的session则直接返回,如果获取不到则创建一个新的session,所以这里实际上事务决定了会话,如果事务被提交,则事务被删除,也就导致 session会话被删除,如果一个事务一直不提交,那么在这个事务内的所有数据库操作就会触发一级缓存的相关逻辑。

二级缓存

​ 二级缓存也称作是应用级缓存,与一级缓存不同的是它的作用范围是整个应用,而且可以跨线程调用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据。由于二级缓存的作用范围是整个应用,所以需要为二级缓存考虑溢出淘汰的机制,而一级缓存的生命周期是一个会话,所以并没有特别为一级缓存考虑淘汰机制。

二级缓存功能.png

  • 过期清理:清理一些存放时间过久的数据,设置一个有效期,对超过有效期的缓存进行清理。
  • 线程安全:二级缓存是跨线程使用的,所以需要考虑到线程安全的问题。
  • 命中率统计:根据命中率统计来给用户提供反馈,告诉用户某次查询是否命中了缓存。

二级缓存设计结构.png

通过装饰者模式结合责任链的方式来实现二级缓存。每一种类型的缓存都是单一职责,当做完自己本身需要做的事情之后,就将需要缓存的相关参数等信息传递给责任链的下一个位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Cache {

String getId();

void putObject(Object key, Object value);

Object getObject(Object key);

Object removeObject(Object key);

void clear();

int getSize();

}

​ 这是缓存的顶层接口,非常简单,它通过接口屏蔽了复杂的底层调用,在使用的过程中,只需要通过这个接口Cache进行相关缓存的操作。

1
2
3
4
5
6
7
@Test
public void cacheTest2() {
Cache cache = configuration.getCache("cn.com.xiaocainiaoya.mapper.UserMapper");
User user = Mock.newUser();
cache.putObject("cacheTest", user);// 设置缓存
cache.getObject("cacheTest");
}

二级缓存.png

1.chcheEnabled:全局缓存开关,只要这个参数配置了false,整个二级缓存就关闭。

2.useCache:表示当前的statement要不要使用缓存。

1
2
3
4
5
6
public interface UserMapper {

@Select({"select * from users where id=#{userId}"})
@Options(useCache = false)
User selectByid(Integer id);
}

3.flushCache:在查询时,将整个二级缓存清空。注意是整个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface UserMapper {

@Select({"select * from users where id=#{userId}"})
@Options(flushCache = Options.FlushCachePolicy.TRUE)
User selectByid(Integer id);
}

// selectByid2在没有配置flushCache的情况下,也是命中不了缓存的。
@Test
public void cacheTest5() {
// 查询1
SqlSession session1 = factory.openSession(true);
UserMapper mapper1 = session1.getMapper(UserMapper.class);
mapper1.selectByid2(10);
// flush cache清空
User user = mapper1.selectByid(10); //清空了,提交

session1.commit();
// 查询2
SqlSession session2 = factory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User use2 = mapper2.selectByid2(10);
}

4.<cache/>@CacheNamespace是标记在xml文件或者对应的mapper文件,默认都是使用全限定类名作为缓存的命名空间,如果两个文件同时标记时会报错。

1
2
3
4
5
6
7
@CacheNamespace
public interface UserMapper {
}

<mapper namespace="org.coderead.mybatis.UserMapper">
<cache/>
</mapper>

5.在第四点中说到,对应的mapper文件和xml不能同时标记,那么如果就是想共用,那么就需要使用到缓存空间引用。<cache-ref/>CacheNamespaceRef

1
2
3
4
5
6
7
@CacheNamespace
public interface UserMapper {
}

<mapper namespace="org.coderead.mybatis.UserMapper">
<cache-ref namespace="cn.com.xiaocainiao.UserMapper" />
</mapper>

由于二级缓存是跨线程使用,所以需要在事务提交之后的数据库操作,才能命中缓存。

二级缓存脏读.png

每一个会话都有一个事务缓存管理器,暂存区的个数取决于访问了多少个mapper。下方的缓存空间就是对应的mapper的缓存命名空间。会话一旦关闭,它所对应的事务缓存管理器就会被清理,从而导致暂存区数据被清理。在数据的操作过程,都是先操作暂存区,只有在事务提交或者事务回滚之后才会将暂存区数据提交到对应的缓存空间。

![image-20221127115857163](/Users/jiangjiamin/Library/Application Support/typora-user-images/image-20221127115857163.png)

1
2
3
4
5
6
7
@Test
public void cacheTest6() {
// 可以通过这个简单的代码,debug下暂存区的结构
SqlSession session1 = factory.openSession(true);
session1.getMapper(UserMapper.class);
session1.getMapper(UserMapper.class);
}

二级缓存的执行流程:

二级缓存执行流程.png

根据上图可知:查询是直接查询缓存空间,其他操作是对暂存区数据进行操作,当进行commit操作时将暂存区的操作数据添加到缓存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// TransactionalCache#commit
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
// 取出需要填充到二级缓存空间的数据,遍历进行填充
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);
}
}
}
-------------本文结束感谢您的阅读-------------