文章出處

上午花了大半天排查一個多數據源主從切換的問題,記錄一下:

背景:

項目的數據庫采用了讀寫分離多數據源,采用AOP進行攔截,利用ThreadLocal及AbstractRoutingDataSource進行數據源切換,數據源代碼如下:

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DBContext.getDBKey();
    }
}

AOP細節就不講了,大致是攔截mybatis的Mapper層,約定對方法前綴,比如update/delete/insert/save開頭的認為是寫方法,切換到主庫,其它方法切換到從庫。spring的xml配置如下:

數據源:

 1     <bean id="dsAlfred" class="cn.mwee.utils.datasource.RoutingDataSource">
 2         <property name="targetDataSources">
 3             <map key-type="java.lang.String">
 4                 <entry key="master" value-ref="dsAlfred_master"/>
 5                 <entry key="slave1" value-ref="dsAlfred_slave1"/>
 6                 <entry key="slave2" value-ref="dsAlfred_slave2"/>
 7                 <entry key="history" value-ref="dsAlfred_history"/>
 8             </map>
 9         </property>
10         <property name="defaultTargetDataSource" ref="dsAlfred_master"/>
11     </bean>

事務部分:

1     <bean id="alfredTxManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
2         <property name="dataSource" ref="dsAlfred"/>
3     </bean>
4     <tx:annotation-driven transaction-manager="alfredTxManager"/>

一直用了很久,都很正常(不管是事務方法,還是非事務方法),最近幾天發現有一個服務,更新數據庫時,一直報read-only異常,當時判斷應該是連接到從庫上了(注:從庫是只讀權限,無法更新數據),方法偽代碼如下:

1 @Transactional
2 void doSomeThing(){
3   xxxMapper.select(...);
4    yyyMapper.update(...);
5    ...
6 } 

執行到第4行的時候,死活切換不到master主庫上來,哪怕在doSomeThing方法的首行,設置DBContext.setDBKey("master") 都不好使,而其它類似的方法都正常。于是對比了代碼,發現這個方法被調用的地方,最近加了幾行代碼,偽代碼如下:

    public void method1(){
        xxxMapper.select(...);
... doSomeThing(); }

即:在調用doSomeThing()方法前,最近因為需求變更,前面加了一行查詢操作(大家不用糾結為啥加這一行,產品需要~_~),把這個查詢去掉,再執行,就ok了,然后... 然后就開始思考人生了...

各種百度,google后,最后在org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin 這個類的源代碼中找到了答案:

 1 @Override
 2     protected void doBegin(Object transaction, TransactionDefinition definition) {
 3         DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
 4         Connection con = null;
 5 
 6         try {
 7             if (txObject.getConnectionHolder() == null ||
 8                     txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
 9                 Connection newCon = this.dataSource.getConnection();
10                 if (logger.isDebugEnabled()) {
11                     logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
12                 }
13                 txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
14             }
15 
16             txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
17             con = txObject.getConnectionHolder().getConnection();
18 
19             Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
20             txObject.setPreviousIsolationLevel(previousIsolationLevel);
21 
22             // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
23             // so we don't want to do it unnecessarily (for example if we've explicitly
24             // configured the connection pool to set it already).
25             if (con.getAutoCommit()) {
26                 txObject.setMustRestoreAutoCommit(true);
27                 if (logger.isDebugEnabled()) {
28                     logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
29                 }
30                 con.setAutoCommit(false);
31             }
32 
33             prepareTransactionalConnection(con, definition);
34             txObject.getConnectionHolder().setTransactionActive(true);
35 
36             int timeout = determineTimeout(definition);
37             if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
38                 txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
39             }
40 
41             // Bind the connection holder to the thread.
42             if (txObject.isNewConnectionHolder()) {
43                 TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
44             }
45         }
46 
47         catch (Throwable ex) {
48             if (txObject.isNewConnectionHolder()) {
49                 DataSourceUtils.releaseConnection(con, this.dataSource);
50                 txObject.setConnectionHolder(null, false);
51             }
52             throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
53         }
54     }

注意:第7-16行,在開始一個事務前,如果當前上下文的連接對象為空,獲取一個連接對象,然后保存起來,下次doBegin再調用時,就直接用這個連接了,根本不做任何切換(類似于緩存命中!)

這樣就解釋得通了: doSomeThing()方法被調用前,加了一段select方法,相當于已經切換到了slave從庫,然后再進入doBegin方法時,就直接拿這個庫的鏈接了,不再進行切換。那為啥其它同樣啟用事務的方法,又能正常連到主庫呢?同樣的解釋,因為這類方法前面,沒有任何其它操作,而xml中的動態數據源配置,默認連接的就是master主庫,因此沒有問題。

弄明白了之后,解決辦法自然就有了:

    public void method1(){
        DBContext.setDBKey("master");//先切換到主庫
        xxxMapper.select(...);
        ...
        doSomeThing();
    }

先切到主庫上來,這樣后面再調用有事務的方法時,就仍然保持在主庫的連接上。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()