阅读这篇文章,你将会了解
1.什么是会话(SqlSession)、执行器(Executor)
2.什么是Mybatis一级缓存
3.一级缓存的生命周期
4.一级缓存的CacheKey生成策略
5.在日常开发时,怎么才能用到一级缓存(通过事务)
一.类关系图:
二.什么是会话(SqlSession)
-
在Mybatis中,SqlSession可以理解为数据库访问的最小粒度,每次的数据库访问,都建立在SqlSession的基础上。
-
SqlSession的实现类有3种:
(1)SqlSessionTemplate:与Spring整合时,用作代理的SqlSession,底层实际上还是使用DefaultSqlSession。
(1)SqlSessionManager:在没有和Spring整合时,用作代理的SqlSession,底层实际上还是使用DefaultSqlSession。
(3)DefaultSqlSession:默认的会话实现类,即后续所讲的执行期、缓存都是与DefaultSqlSession相关。 -
关于SqlSessionTemplate和SqlSessionManager的区别,可以看这篇文章:
https://blog.csdn.net/u010841296/article/details/89367296 -
每个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");
- Configuration对象里面记录了Mybatis解析xml、扫描mapper获取到的信息。
- 在DefaultSqlSession中,虽然定义了很多sql操作的基础接口,但是sql操作具体的逻辑并不是在SqlSession完成,而是交托给执行器(Executor)完成。
- DefaultSqlSession负责从mappedStatements中拿到用户调用方法全名(statement)对应的MappedStatement,这个MappedStatement实际上就是用户自定义的新增该查的Mapper方法。这个是有Mybatis启动时进行扫描绑定的。
例如用户定义了一个方法Dao接口:com.jenson.pratice.mapper.TestMapper.selectByID,
那么在mappedStatements中就会有一个键值对(“com.jenson.pratice.mapper.TestMapper.selectByID”,mappedStatement)
三.什么是执行器(Executor)
- Executor是DefaultSqlSession中的一个类属性,定义了基础的sql操作,比如事务的获取、提交、回滚,数据的插入、查询、更改、删除等基本操作。
- Executor不负责创建事务,但是Executor里面会有事务引用。
- 执行器里面会实例化一级缓存对象。
- 相比与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>();
一级缓存的生命周期:
- PerpetualCache的生命周期是和SqlSession相关的,即只有在同一个SqlSession中,一级缓存才会用到。如果会话介绍,则缓存会清空;
- 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
- 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;
- SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用;
Mybatis在什么时候使用一级缓存:
- Mybatis在查询的时候会先生成一个CacheKey,判断当前会话的PerpetualCache中是否有这个key的缓存,如果有则返回,没有则执行查询。查询完成后会把结果集放入缓存。
- 在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);
}
- 实际使用中,查询的调用链可能是这样的:
DefaultSqlSession --> CacheExecutor --> BaseExecutor --> SimpleExecutor
一级缓存如何生成CacheKey:
在BaseExecutor中,定义了一个方法(createCacheKey)去生成一个CacheKey。通过这个CacheKey判断2次查询是否相同,能否使用缓存。
这个CacheKey的值由下面几个部分组成:
- ms.getId():MappedStatement的ID
- 分页查询的参数offset、limit:这个是指MyBatis自身提供的分页功能是通过RowBounds来实现的,它通过rowBounds.offset和rowBounds.limit来过滤查询出来的结果集,这种分页功能是基于查询结果的再过滤,而不是进行数据库的物理分页。所以不要把他和sql语句中的分页混淆了。
- boundSql.getSql():这个sql字符串仍未处理参数的填充
- parameterMappings:会遍历这个列表,得到实际需要填充进sql的参数,然后放到CacheKey中
- 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);
}