記一次升級Oracle驅動引起的死鎖

問題描述

近期項目須要從虛擬機環境遷移到容器環境,其中有一個項目在遷移到容器環境以後的兩天以內出現了2次「死鎖(deadlock)」的問題,部分關鍵日誌以下:html

Found one Java-level deadlock:
=============================
"DefaultMessageListenerContainer-9":
  waiting to lock monitor 0x00007fde3400bf38 (object 0x00000000dda358d0, a oracle.jdbc.driver.T4CConnection),
  which is held by "DefaultMessageListenerContainer-7"
"DefaultMessageListenerContainer-7":
  waiting to lock monitor 0x00007fdea000b478 (object 0x00000000dda35578, a oracle.jdbc.driver.T4CConnection),
  which is held by "DefaultMessageListenerContainer-9"
Java stack information for the threads listed above:
===================================================
"DefaultMessageListenerContainer-9":
    at oracle.jdbc.oracore.OracleTypeADT.linearize(OracleTypeADT.java:1280)
    - waiting to lock <0x00000000dda358d0> (a oracle.jdbc.driver.T4CConnection)
    at oracle.sql.ArrayDescriptor.toBytes(ArrayDescriptor.java:653)
    at oracle.sql.ARRAY.toBytes(ARRAY.java:711)
    - locked <0x00000000dda35578> (a oracle.jdbc.driver.T4CConnection)
    at oracle.jdbc.driver.OraclePreparedStatement.setArrayCritical(OraclePreparedStatement.java:6049)
    at oracle.jdbc.driver.OraclePreparedStatement.setARRAYInternal(OraclePreparedStatement.java:6008)
    - locked <0x00000000dda35578> (a oracle.jdbc.driver.T4CConnection)
    at oracle.jdbc.driver.OraclePreparedStatement.setArrayInternal(OraclePreparedStatement.java:5963)
    at oracle.jdbc.driver.OracleCallableStatement.setArray(OracleCallableStatement.java:4833)
    at oracle.jdbc.driver.OraclePreparedStatementWrapper.setArray(OraclePreparedStatementWrapper.java:114)
"DefaultMessageListenerContainer-7":
    at oracle.jdbc.oracore.OracleTypeADT.linearize(OracleTypeADT.java:1280)
    - waiting to lock <0x00000000dda35578> (a oracle.jdbc.driver.T4CConnection)
    at oracle.sql.ArrayDescriptor.toBytes(ArrayDescriptor.java:653)
    at oracle.sql.ARRAY.toBytes(ARRAY.java:711)
    - locked <0x00000000dda358d0> (a oracle.jdbc.driver.T4CConnection)
    at oracle.jdbc.driver.OraclePreparedStatement.setArrayCritical(OraclePreparedStatement.java:6049)
    at oracle.jdbc.driver.OraclePreparedStatement.setARRAYInternal(OraclePreparedStatement.java:6008)
    - locked <0x00000000dda358d0> (a oracle.jdbc.driver.T4CConnection)
    at oracle.jdbc.driver.OraclePreparedStatement.setArrayInternal(OraclePreparedStatement.java:5963)
    at oracle.jdbc.driver.OracleCallableStatement.setArray(OracleCallableStatement.java:4833)
    at oracle.jdbc.driver.OraclePreparedStatementWrapper.setArray(OraclePreparedStatementWrapper.java:114)
    at

日誌仍是挺明顯的,線程DefaultMessageListenerContainer-9得到了鎖0x00000000dda35578,等待獲取0x00000000dda358d0;而DefaultMessageListenerContainer-7正好相反,從而致使死鎖;java

問題分析

以上的錯誤日誌和Oracle的驅動類有關,因此猜想是驅動版本的問題,因此找相關人員分別拉取了虛擬機環境和容器環境的生產Oracle驅動jar包,結果以下:spring

#虛擬機
[19:38:21 oracle@tomcat-384 lib]$ ls -l ojdbc-1.4.jar
-rw-r--r-- 1 oracle oinstall 1378346 Jul  3  2014 ojdbc-1.4.jar
 
#容器
[oracle@7f666c76b7-dx2gq lib]$ ls -l ojdbc6.jar
-rw-r--r-- 1 oracle oinstall 2739670 Aug 11  2015 ojdbc6.jar

兩個環境使用了不一樣的版本,容器使用了高版本(11.2.0.4.0),虛擬機使用的是低版本(10.1.0.5.0);Google查詢了和Oracle驅動相關產生死鎖的問題,查到了Oracle官方有以下文檔:
Java-level deadlock with 11.2
提供給咱們的方案是「Upgraded the Oracle JDBC driver from 10.2 to 11.2.」,正好和咱們遇到的狀況相反,咱們是高版本有問題,低版本沒有問題,因此須要進一步分析;sql

源碼分析

首先找到相關的邏輯代碼類,此處爲了更好的看出問題,使用了以下的模擬類,大體以下:tomcat

//測試Dao,配置在spring下的單例
public class TestDaoImpl {

    //共享的兩個ArrayDescriptor
    private ArrayDescriptor param1Desc;
    private ArrayDescriptor param2Desc;

    private String param1;
    private String param2;
    private DataSource dataSource;

    public void callProc(Object param) {
        // 準備的兩個ARRAY參數
        ARRAY param1Array = null;
        ARRAY param2Array = null;

        CallableStatement callable = null;
        Connection conn = null;

        try {
            // 從鏈接池獲取鏈接
            conn = DataSourceUtils.getConnection(dataSource);
            param1Array = wrapProcParameter1(param, conn);
            param2Array = wrapProcParameter2(param, conn);

            callable = conn.prepareCall("{ call testProc " + "(?,?,?)}");
            callable.setArray(1, param1Array);
            callable.setArray(2, param2Array);

            callable.execute();

        } catch (Exception e) {
            // 異常處理
        } finally {
            // 關閉處理
        }
    }

    private ARRAY wrapProcParameter1(Object param, Connection conn) throws SQLException {
        if (null == this.param1Desc) {
            this.param1Desc = new ArrayDescriptor(this.param1, conn);
        }
        //省略
        ARRAY array1 = new ARRAY(this.param1Desc, conn, param);
        return array1;
    }

    private ARRAY wrapProcParameter2(Object param, Connection conn) throws SQLException {
        if (null == this.param2Desc) {
            this.param2Desc = new ArrayDescriptor(this.param2, conn);
        }
        //省略
        ARRAY array2 = new ARRAY(this.param2Desc, conn, param);
        return array2;
    }
}

大體的邏輯是經過從鏈接池獲取的Connection建立了一個存儲過程,而後給存儲過程設置了兩個ARRAY參數,在建立ARRAY時須要指定相應的ArrayDescriptor,最後執行存儲過程;
產生異常分別在兩次setArray的地方,線程1在setArray1的地方,線程2在setArray2的地方,全部以此爲入口分別查看兩個驅動版本相關類:OraclePreparedStatement,ARRAY,ArrayDescriptor以及OracleTypeADT;多線程

驅動11.2.0.4.0版本

首先查看OraclePreparedStatement中調用的setArray,最終會調用以下方法:

在方法setARRAYInternal中使用了connection做爲了對象鎖,接下來OraclePreparedStatement會調用ARRAY,而後ARRAY調用ArrayDescriptor,最後ArrayDescriptor在調用OracleTypeADT,爲了方便看出問題直接展現OracleTypeADT中使用鎖的地方:

一樣使用connection作爲鎖對象,這樣就存在同時須要獲取兩把鎖了,而上面兩把鎖都是connection對象,應該不會出現死鎖,可是深刻發現其實OracleTypeADT中的connection對象是從ArrayDescriptor中獲取的,而ArrayDescriptor是一個共享的類變量,這樣在多線程環境下就會出現被賦值不一樣的connection,從而致使出現死鎖的問題;
大體流程以下:
1.首先線程1獲取conn1,而後線程2獲取conn2;
2.而後線程1建立Array1,同時對共享的ArrayDescriptor1設置connection=conn1;
3.線程1掛起,線程2建立Array1,同時對共享的ArrayDescriptor1設置connection=conn2,對共享的ArrayDescriptor2設置connection=conn2;
4.線程2繼續佔用cpu,執行setArray1,這時候都是Array1和ArrayDescriptor1中的鎖都是conn2,因此沒有問題,繼續執行setArray2,在執行完獲取第一把鎖conn2以後,線程2掛起;
5.線程1搶佔cpu,對共享的ArrayDescriptor2設置connection=conn1,而後執行setArray1;但此時Array1中的connection是conn1,而ArrayDescriptor1中的connection是conn2,因此出現線程1佔用了conn1,等待conn2鎖;
6.此時線程2再次搶到cpu,可是在獲取第二把鎖時,此時ArrayDescriptor2中的connection已經被設置成了conn1,而conn1已經被線程1佔有,因此等待獲取conn1;
7.死鎖出現了線程1佔有了conn1鎖,等待conn2鎖;線程2佔有了conn2鎖,等待conn1鎖;從而致使死鎖發生;oracle

從上面的分析能夠看出主要緣由是ArrayDescriptor被設置成了類變量,被多個線程所訪問,解決死鎖問題能夠把ArrayDescriptor改爲局部變量;可是若是僅是業務形成的問題,那應該在驅動ojdbc-1.4中存在一樣的死鎖問題,可是此項目在虛擬機環境中一直沒有出現過問題;繼續看ojdbc-1.4源碼;app

驅動10.1.0.5.0版本

一樣分析此驅動版本中的相同類,同上首先查看OraclePreparedStatement中調用的setArray,最終會調用以下方法:

一樣使用了connection做爲對象鎖,再看OracleTypeADT,相關代碼以下:

能夠看到這裏並無使用connection做爲鎖,而是使用了內置鎖,因此就不會出現死鎖問題;源碼分析

問題總結

首先就是在遷移環境時必定要保證相關的依賴公共jar保證版本的一致,就算是低版本,高版本也不同保證向下兼容;其次也是最重要的寫業務邏輯時遇到公共變量時必定要謹慎,是否會出現多線程問題;測試

相關文章
相關標籤/搜索