做者:等你歸去來html
cnblogs.com/yougewe/p/10072740.htmljava
項目中經常使用mybatis配合spring進行數據庫操做,可是咱們知道,數據的操做是要求作到線程安全的,並且按照原來的jdbc的使用方式,每次操做完成以後都要將鏈接關閉,可是實際使用中咱們並無這麼幹。面試
更讓人疑惑的點是,spring中默認使用單例形式來加載bean,而每每咱們也不會改變這種默認,因此,是全部線程共享數據鏈接?spring
讓咱們來看看真相!sql
天然是要個栗子的:數據庫
咱們來看下spring中配置mybatis數據庫操做bean(使用 druid 鏈接池):apache
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}" />
<property name="driverClassName" value="${jdbc.driver}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:mybatis-config.xml" />
</bean>
<!-- scope="prototype" 另說,另討論,咱們先以mapper形式看一下 -->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
<!-- 事務 -->
<bean name="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>複製代碼
而在java代碼中使用則是使用依賴注入直接使用 @resource sqlSession, 以下:設計模式
@Resource
private SqlSessionTemplate sqlSession;
@Override
public User getUser(Map<String, String> cond) {
// 此句執行db查詢
User result = sqlSession.selectOne(NAME_SPACE
+ ".getUser", cond);
return result;
}複製代碼
這個sqlSession就是直接去操做數據庫了看起來是這樣,是在bean初始化的時候依賴注入的!緩存
因此,難道每次進入該操做的時候,sqlSession 的實例都會變化嗎?答案是否認的。安全
那麼,確定就是往下使用的時候才發生的變化唄!
再往下走,能夠看到,調用了一個代理來進行具體的查詢!
// org/mybatis/spring/SqlSessionTemplate.selectOne()
public <T> T selectOne(String statement, Object parameter) {
return this.sqlSessionProxy.<T> selectOne(statement, parameter);
}複製代碼
爲啥要用代理呢?本身直接查不就好了嗎?其實,用代理是有好處的,那就能夠能夠進行另外的包裝!
代理是怎麼生成的呢?其實只要看一下 SqlSessionTemplate 的構造方法就知道了!
/**
* 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
* @param executorType
* @param exceptionTranslator
*/
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;
// 生成代理 SqlSessionInterceptor 爲 InvocationHandler
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}複製代碼
從上面的代碼,看不到細節,可是,大體仍是知道代理的具體實現了!即便用 SqlSessionInterceptor 去處理具體查詢邏輯!
咱們來看下 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 { 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); } } } }複製代碼
SqlSessionInterceptor 是 SqlSessionTemplate 的內部類,目的只有一個,就是處理多個 session 的db操做!
全部請求都被 invoke() 攔截,從而作相應處理:
進入請求,先生成一個新的sqlSession,爲本次db操做作準備;
經過反射調用請求進來的方法,將 sqlSession 回調,進行復雜查詢及結果映射;
若是須要當即提交事務,do it;
若是出現異常,包裝異常信息,從新拋出;
操做完成後,關閉本次session;
到這裏,其實咱們好像已經明白了,其實外面的 sqlSession 單例,並不會影響具體的db操做控制,因此不用擔憂session的線程安全問題!
不過,還有個點值得考慮下,若是我一次請求裏有屢次數據庫操做,難道我真的要建立多個sqlSession或者說數據庫鏈接?不會吧!
若是這個問題得不到解決,可能你並不真正瞭解session的定義了!
因此咱們須要繼續看一下 session 究竟是怎麼獲取的?
getSqlSession() 方法是在 SqlSessionUtils 中實現的!以下:
/**
* Gets an SqlSession from Spring Transaction Manager or creates a new one if needed.
* Tries to get a SqlSession out of current transaction. If there is not any, it creates a new one.
* Then, it synchronizes the SqlSession with the transaction if Spring TX is active and
* <code>SpringManagedTransactionFactory</code> is configured as a transaction manager.
*
* @param sessionFactory a MyBatis {@code SqlSessionFactory} to create new sessions
* @param executorType The executor type of the SqlSession to create
* @param exceptionTranslator Optional. Translates SqlSession.commit() exceptions to Spring exceptions.
* @throws TransientDataAccessResourceException if a transaction is active and the
* {@code SqlSessionFactory} is not using a {@code SpringManagedTransactionFactory}
* @see SpringManagedTransactionFactory
*/
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, "No SqlSessionFactory specified");
notNull(executorType, "No ExecutorType specified");
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
// 若是已經有holder,則直接返回,複用鏈接
if (holder != null && holder.isSynchronizedWithTransaction()) {
if (holder.getExecutorType() != executorType) {
throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
}
holder.requested();
if (logger.isDebugEnabled()) {
logger.debug("Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
}
return holder.getSqlSession();
}
if (logger.isDebugEnabled()) {
logger.debug("Creating a new SqlSession");
}
SqlSession session = sessionFactory.openSession(executorType);
// Register session holder if synchronization is active (i.e. a Spring TX is active)
//
// Note: The DataSource used by the Environment should be synchronized with the
// transaction either through DataSourceTxMgr or another tx synchronization.
// Further assume that if an exception is thrown, whatever started the transaction will
// handle closing / rolling back the Connection associated with the 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();
} else {
if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
if (logger.isDebugEnabled()) {
logger.debug("SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional");
}
} else {
throw new TransientDataAccessResourceException(
"SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
}
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("SqlSession [" + session + "] was not registered for synchronization because synchronization is not active");
}
}
return session;
}複製代碼
如上獲取 sqlSession 邏輯,主要分兩種狀況!
若是存在holder,則返回原有的sqlSession,到於這個holder咱們稍後再說;
若是沒有,則建立一個新鏈接!
因此,看起來狀況還不是太糟,至少有複用的概念了!
那麼問題來了,複用?如何作到線程安全?因此咱們要看下 SqlSessionHolder 的實現了!
獲取holder是經過 TransactionSynchronizationManager.getResource(sessionFactory); 獲取的:
public static Object getResource(Object key) {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
// 實際獲取
Object value = doGetResource(actualKey);
if (value != null && logger.isTraceEnabled()) {
logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +
Thread.currentThread().getName() + "]");
}
return value;
}
private static Object doGetResource(Object actualKey) {
Map<Object, Object> map = resources.get();
if (map == null) {
return null;
}
Object value = map.get(actualKey);
// Transparently remove ResourceHolder that was marked as void...
if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
map.remove(actualKey);
// Remove entire ThreadLocal if empty...
if (map.isEmpty()) {
resources.remove();
}
value = null;
}
return value;
}複製代碼
我們忽略對 key 的處理,實際是直接調用 doGetResource() 獲取holder。而 doGetResource() 中,則使用了 resources 來保存具體的 kv。 resources 明顯是個共享變量,可是看起來這裏沒有任何的加鎖操做!這是爲什麼?
只要看一下 resources 的定義就知道了,其實現爲 ThreadLocal, 因此是線程安全了!
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<Map<Object, Object>>("Transactional resources");複製代碼
在新的請求進來時,天然是沒有值的,因此直接返回null.然後續進入,則獲取緩存返回!
而對於沒有獲取到 holder 的狀況,則須要從新建立一個 session 了!
這裏獲取session由DefaultSqlSessionFactory 進行建立!以下:
// org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSession()
public SqlSession openSession(ExecutorType execType) {
return openSessionFromDataSource(execType, null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
// SpringManagedTransactionFactory
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}複製代碼
建立 session 幾件事:
根據環境配置,開啓一個新事務,該事務管理器會負責後續jdbc鏈接管理工做;
根據事務建立一個 Executor,備用;
用DefaultSqlSession 將 executor 包裝後返回,用於後續真正的db操做;
至此,真正的 sqlSession 已經建立成功!返回後,就能夠真正使用了!
等等,建立的session好像並無保存,那麼仍是那個問題,每一個sql都會建立一個 sqlSession ? 好吧,是這樣的!前面的holder,只是用於存在事務操做的鏈接!(holder的理解出了誤差哦)
可是有一點,這裏雖然建立了多個 sqlSession 實例,可是並不意味着有多個db鏈接,具體使用db鏈接時,則通常會會使用鏈接池來進行優化!如前面提到的 druid 就是個不錯的選擇!
真實的jdbc鏈接獲取,是在進行真正的 query 時,才進行調用 getConnection() 進行接入!
具體則是在 doQuery() 時,進行st的組裝時調用的 ,以下:
// SimpleExecutor.prepareStatement()
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 獲取 jdbc 鏈接,返回 java.sql.Connection
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection);
handler.parameterize(stmt);
return stmt;
}
// 調用 BaseExecutor.getConnection()
protected Connection getConnection(Log statementLog) throws SQLException {
// SpringManagedTransaction 管理 connection
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}複製代碼
經過前面經過事務管理工廠建立的 SpringManagedTransaction 進行 connection 獲取!一個事務管理器只會存在一次獲取數據庫鏈接的操做!
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
// 而 SpringManagedTransaction 又將connection交由 DataSourceUtils 進行管理!
// org/springframework/jdbc/datasource/DataSourceUtils
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
try {
// 真正的鏈接獲取
return doGetConnection(dataSource);
}
catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
}
}
/**
* Actually obtain a JDBC Connection from the given DataSource.
* Same as {@link #getConnection}, but throwing the original SQLException.
* <p>Is aware of a corresponding Connection bound to the current thread, for example
* when using {@link DataSourceTransactionManager}. Will bind a Connection to the thread
* if transaction synchronization is active (e.g. if in a JTA transaction).
* <p>Directly accessed by {@link TransactionAwareDataSourceProxy}.
* @param dataSource the DataSource to obtain Connections from
* @return a JDBC Connection from the given DataSource
* @throws SQLException if thrown by JDBC methods
* @see #doReleaseConnection
*/
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(dataSource.getConnection());
}
return conHolder.getConnection();
}
// Else we either got no holder or an empty thread-bound holder here.
logger.debug("Fetching JDBC Connection from DataSource");
// 經過接入的dataSource進行鏈接獲取,這裏將會是最終的jdbc鏈接
Connection con = dataSource.getConnection();
if (TransactionSynchronizationManager.isSynchronizationActive()) {
logger.debug("Registering transaction synchronization for JDBC Connection");
// Use same Connection for further JDBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(con);
}
else {
holderToUse.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
return con;
}複製代碼
上面的實現主要作三件事:
再次確認,是否存在事務處理,holder是否存在,若是有則複用;
若是沒有,那再從數據源處獲取鏈接;
獲取新鏈接成功後,檢查若是存在事務,則將新獲取的鏈接放入holder中保存起來,以備下次使用;
獲取jdbc鏈接後,就能夠真正發起execute()查詢了。
數據庫鏈接的疑問算是解答了!咱們發現,外部的框架並無多少爲咱們節省db鏈接的動做!而是把最終 getConnection() 交給 datasource 數據源!
而真正解決咱們鏈接複用的問題的,是像 Druid 這樣的鏈接池組件!因此,我們能夠單獨來看這些中間件了!
2. 面試題內容聚合
3. 設計模式內容聚合
4. Mybatis內容聚合
5. 多線程內容聚合