上一期,分析了下關(guān)于mybatis的處理sqlsession線程安全的問題,主要是通過sqlSessionManager代理類增強(qiáng)的形式,通過每次創(chuàng)建一個(gè)新的DefautSqlsession或者將當(dāng)前線程放入到Threadlocal中實(shí)現(xiàn)的,那么我們?cè)谑褂胢ybatis的時(shí)候,一般不可能單獨(dú)使用mybatis的,一般都是和sprig框架配合使用,現(xiàn)在都是面向spring編程了,所以,本次我們一起分析下spring是怎樣保證sqlSession線程安全的,
我們先通過一個(gè)案例,看看結(jié)果,一般在引入spring后,我們都會(huì)通過SqlSessionTemplete操作mybatis,當(dāng)掃描到@Mapper的注解后,在執(zhí)行業(yè)務(wù)邏輯代碼,執(zhí)行數(shù)據(jù)庫交互的時(shí)候,這個(gè)時(shí)候,就會(huì)被SqlSessionTemplete攔截了,所以,不管是我們顯示的調(diào)用SqlSessionTemplete執(zhí)行相關(guān)的操作,還是在引入spring框架后,,默認(rèn)的執(zhí)行,都是通過SqlSessionTempete攔截保證sqlSqssion線程安全的,那馬接下來,我們就一起分析下,看看spring到底是怎樣支持sqlSession線程安全的
我們先通過一個(gè)案例來說明問題,下面是一個(gè)特備簡單的案例,我們通過他,先來分析下,然后在通過多線程來分析具體的線程安全問題
@Test
public void test5(){
final RyxAccount ryxAccountByPrimaryKey = ryxAccountService.getRyxAccountByPrimaryKey(1);
System.out.println(ryxAccountByPrimaryKey);
final RyxAccount ryxAccountByPrimaryKey1 = ryxAccountService.getRyxAccountByPrimaryKey(1);
System.out.println(ryxAccountByPrimaryKey1);
}
當(dāng)我們執(zhí)行 ryxAccountService.getRyxAccountByPrimaryKey的接口方法的時(shí)候,到mapper接口,SqlSqlSessionTemplete通過代理的方式攔截mapper接口,生成代理類,獲取sqlSession,執(zhí)行后續(xù)的數(shù)據(jù)庫crud
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
private final ExecutorType executorType;
private final SqlSession sqlSessionProxy;
private final PersistenceExceptionTranslator exceptionTranslator;
省略........
/**
* Constructs a Spring managed {@code SqlSession} with the given
* {@code SqlSessionFactory} and {@code ExecutorType}.
* A custom {@code SQLExceptionTranslator} can be provided as an
* argument so any {@code PersistenceException} thrown by MyBatis
* can be custom translated to a {@code RuntimeException}
* The {@code SQLExceptionTranslator} can also be null and thus no
* exception translation will be done and MyBatis exceptions will be
* thrown
*
* @param sqlSessionFactory a factory of SqlSession
* @param executorType an executor type on session
* @param exceptionTranslator a translator of exception
*/
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
//jdk動(dòng)態(tài)代理方法
this.sqlSessionProxy = (SqlSession) newProxyInstance(
//獲取類加載器
SqlSessionFactory.class.getClassLoader(),
//具體需要代理的類
new Class[] { SqlSession.class },
//執(zhí)行增強(qiáng)的方法
new SqlSessionInterceptor());
}
省略......
/**
* Proxy needed to route MyBatis method calls to the proper SqlSession got
* from Spring's Transaction Manager
* It also unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to
* pass a {@code PersistenceException} to the {@code PersistenceExceptionTranslator}.
*/
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
}
看到這段代碼是不是有似曾相識(shí)的感覺,沒錯(cuò),前一期在介紹SqlSesionManager的時(shí)候,也是這個(gè)寫法,通過動(dòng)態(tài)代理的技術(shù)增強(qiáng)目標(biāo)方法,這里的作用就是在生成目標(biāo)類之前,獲取sqlSession(線程安全的),接下來我們吧重心放到SqlSessionInterceptor 的getSqlSession中去
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
//參數(shù)校驗(yàn)
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
/**
* 1.1
* 這個(gè)步驟都是指的是存在事務(wù)的情況下的結(jié)果,也就是說,在你的方法上開啟了事務(wù)注解的時(shí)候,才有意義,當(dāng)不開啟事務(wù)注解的時(shí)候,
* 會(huì)直接調(diào)用openSession返回session,也就是說,不存在事務(wù)的情況下,每一個(gè)線程都是相當(dāng)于開啟一個(gè)新的session,也就不會(huì)存在一級(jí)緩存的問題
* 當(dāng)然也就不會(huì)存在線程安全問題
* 調(diào)用事務(wù)同步管理器方法獲取SqlSession回話持有者,回去ThreadLocal中去取會(huì)話持有者對(duì)象.會(huì)話持有者對(duì)象
* 放在ThreadLocal中.ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
*/
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
//會(huì)話持有者的數(shù)量執(zhí)行+1操作,有人想獲取持有者資源,將引用計(jì)數(shù)器加1
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
//如果回話持有者中不存在SqlSession.則調(diào)用sessionFactory開啟一個(gè)session
LOGGER.debug(() -> "Creating a new SqlSession");
session = sessionFactory.openSession(executorType);
/**
* 1.2
* 注冊(cè)到會(huì)話持有者中,如果存在事務(wù),就會(huì)將當(dāng)前的會(huì)話持有者和sessionFactory添加到thredLocal中
*/
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
這節(jié)我們的重點(diǎn)是分析SqlSession.所以事物我們暫且放下,看懂了,代碼比較簡單.我這里在總結(jié)下
1:當(dāng)我們引入spring框架后,我們執(zhí)行查詢,SqLSessionTemplete戶通過代理方式"攔截"執(zhí)行方法,由于DefaultSqSLession線程不安全
2:這里獲取線程安全的SqlSession,
3:判斷是否存在事務(wù)
4:事務(wù)不存在,直接調(diào)用SqlSessionFactory獲取sesison返回,直接執(zhí)行后續(xù)的方法,也就是說,不通的線程訪問,或者同一個(gè)線程范根同樣的查詢方法,都會(huì)去創(chuàng)建一個(gè)新的session,這樣的話,也就導(dǎo)致以及緩存不存在了.
如果存在事務(wù),回去事務(wù)資源管理器獲取session持有者,如果存在,就將當(dāng)前的引用計(jì)數(shù)加1,如果不存在,則通過SqlSessionFactory.openSession獲取一個(gè)session,然后注冊(cè)到事務(wù)管理器中,這里是將當(dāng)前資源添加到ThreadLOcal中,,在local中定義了一個(gè)map類似于這樣的結(jié)構(gòu)ThreadLocal<Map<String,Object>>,
5:調(diào)用DefaultSqLSession執(zhí)行后續(xù)的邏輯,所以也就是說,當(dāng)存在事物的情況下,一級(jí)緩存才有意義
我們通過案例來加強(qiáng)一下理解,以下案例,查詢兩次,默認(rèn)回去走SqLSessionTemplete,我們執(zhí)行下,看看結(jié)果
@Test
public void test5(){
final RyxAccount ryxAccountByPrimaryKey = ryxAccountService.getRyxAccountByPrimaryKey(1);
System.out.println(ryxAccountByPrimaryKey);
final RyxAccount ryxAccountByPrimaryKey1 = ryxAccountService.getRyxAccountByPrimaryKey(1);
System.out.println(ryxAccountByPrimaryKey1);
}
我們看到結(jié)果中查詢兩次都走了數(shù)據(jù)庫查詢,一級(jí)緩存沒有起作用
再來看另一個(gè)案例,加了注解后,第二次查詢回去走緩存
*/
@Test
@Transactional
public void test5(){
final RyxAccount ryxAccountByPrimaryKey = ryxAccountService.getRyxAccountByPrimaryKey(1);
System.out.println(ryxAccountByPrimaryKey);
final RyxAccount ryxAccountByPrimaryKey1 = ryxAccountService.getRyxAccountByPrimaryKey(1);
System.out.println(ryxAccountByPrimaryKey1);
}
那就有一個(gè)問題,為什么mybatis不適用線程安全額SqlSessionManager那,而是默認(rèn)使用線程不安全的DefaultSqlSession那
我覺得這個(gè)問題的答案是
DefaultSqlSession已經(jīng)開發(fā)完了,那馬在他的基礎(chǔ)上修改的話,勢(shì)必要考慮的東西比較多,還不如直接通過代理的方式增強(qiáng)這個(gè)方法,調(diào)用底層的DefauleSqLSession,如果再來個(gè)框架,整合mybatis,又要修改這一塊,導(dǎo)致越來越臃腫,通過代理的方式不修改原來代碼的基礎(chǔ)上,實(shí)現(xiàn)了該功能,其實(shí)是很值得我們借鑒的,做到了解耦操作.
文中要是有不合理的地方,還請(qǐng)包涵指正,