閱讀這篇文章,你將會了解
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);
}