Tomcat JDBC 數據源

2022-03-03 11:45 更新

概述

JNDI 數據源配置的相關內容已經在 JNDI 資源文檔中詳細介紹過。但從 Tomcat 用戶的反饋意見來看,有些配置的細節(jié)問題非常棘手。

針對常用的數據庫,我們已經給 Tomcat 用戶提供了一些配置范例,以及關于數據庫使用的一些通用技巧。本章就將展示這些范例和技巧。

另外,雖然有些注意事項來自于用戶所提供的配置和反饋信息,但你可能也有不同的實踐。如果經過試驗,你發(fā)現某些配置可能具有廣泛的助益作用,或者你覺得它們會使本章內容更加完善,請務必不吝賜教。

請注意,對比 Tomcat 7.x 和 Tomcat 8.x,JNDI 資源配置多少有些不同,這是因為使用的 Apache Commons DBCP 庫的版本不同所致。所以,為了在 Tomcat 8 中使用,你最好修改老版本的 JNDI 資源配置,以便能夠匹配下文范例中的格式。詳情可參看Tomcat 遷移文檔

另外還要提示的是,一般來說(特別是對于本教程而言),JNDI 數據源配置會假定你已經理解了 ContextHost 的配置偏好,其中包括在后者配置偏好中的應用自動部署的相關內容。

DriverManager,服務提供者機制以及內存泄露

java.sql.DriverManager 支持服務提供者機制。這項功能的實際作用在于:對于所有可用的 JDBC 驅動,只要它們聲明提供 META-INF/services/java.sql.Driver 文件,就會被自動發(fā)現、加載并注冊,從而減輕了我們在創(chuàng)建 JDBC 連接之前還需要顯式地加載數據庫驅動的負擔。但在 servlet 容器環(huán)境的所有 Java 版本中,卻根本沒法實現這種功能。問題在于 java.sql.DriverManager 只會掃描一次驅動。

Tomcat 自帶的阻止 JRE 內存泄漏偵聽器可以在一定程度上解決這個問題,它會在 Tomcat 啟動時觸發(fā)驅動掃描。該偵聽器默認是啟用的。只有可見于該偵聽器的庫(比如 $CATALINA_BASE/lib 中的庫)才能被數據庫驅動所掃描。如果你想禁用該功能,那么一定要記?。菏紫仁褂? JDBC 的 Web 應用會觸發(fā)掃描,從而當該應用重新加載時會出錯;對于其他依賴該功能的 Web 應用來說也會導致出錯。

所以,假如應用的 WEB-INF/lib 目錄中存在數據庫驅動,那么這些應用就不能依賴服務提供者機制,而應該顯式地注冊驅動。

java.sql.DriverManager 中的驅動已經被認為是內存泄露之源。當 Web 應用停止運行時,它所注冊的任何驅動都必須重新注冊。當 Web 應用停止運行時,Tomcat 會嘗試自動尋找并重新注冊任何由 Web 應用類加載器所加載的 JDBC 驅動。但最好是由應用通過 ServletContextListener 來實現這一點。

數據庫連接池(DBCP 2)配置

Apache Tomcat 的默認數據庫連接池實現基于的是 Apache Commons 項目的庫,具體來說是這兩個庫:

  • Commons DBCP
  • Commons Pool

這兩個庫都位于一個 JAR 文件中:$CATALINA_HOME/lib/tomcat-dbcp.jar。但該文件只包括連接池所需要的類,包名也已經改變了,以避免與應用沖突。

DBCP 2.0 支持 JDBC 4.1。

安裝

可參閱 DBCP 文檔了解完整的配置參數。

防止數據庫連接池泄露

數據庫連接池創(chuàng)建并管理著一些與數據庫的連接。與打開新的連接相比,回收或重用現有的數據庫連接要更為高效一些。

連接池化還存在一個問題。Web 應用必須明確地關閉 ResultSet、Statement,以及 Connection。假如 Web 應用無法關閉這些資源時,會導致這些資源再也無法被重用,從而造成了數據庫連接池“泄露”。如果再也沒有可用連接時,最終這將導致 Web 應用數據庫連接失敗。

針對該問題,有一個解決辦法:通過配置 Apache Commons DBCP,記錄并恢復這些廢棄的數據庫連接。它不僅能恢復這些連接,而且還能針對打開這些連接而又永遠不關閉它們的代碼生成堆棧跟蹤。

為了配置 DBCP 數據源來移除并回收廢棄的數據庫連接,將下列屬性(一個或全部)添加到你的 DBCP 數據源中的 Resource 配置中:

removeAbandonedOnBorrow=true

removeAbandonedOnMaintenance=true

以上屬性默認都為 false。注意,只有當 timeBetweenEvictionRunsMillis 為正值,從而啟用池維護時,removeAbandonedOnMaintenance 才能生效。關于這些屬性的詳情,可查看 DBCP 文檔 。

使用 removeAbandonedTimeout 屬性設置某個數據庫連接閑置的秒數,超過此時段,即被認為是廢棄連接。

removeAbandonedTimeout="60"

默認的去除廢棄連接的超時為 300 秒。

logAbandoned 設為 true,可以讓 DBCP 針對那些拋棄數據庫連接資源的代碼,記錄堆棧跟蹤信息。

logAbandoned="true"

默認為 false

MySQL DBCP 范例

0. 簡介

已報告的能夠正常運作的 MySQL 與 JDBC 驅動的版本號為:

  • MySQL 3.23.47、使用 InnoDB 的 MySQL 3.23.47、MySQL 3.23.58 以及 MySQL 4.0.1 alpha
  • Connector/J 3.0.11-stable (JDBC 官方驅動)
  • mm.mysql 2.0.14 (一個較老的 JDBC 第三方驅動)

在繼續(xù)下一步的操作之前,千萬不要忘了將 JDBC 驅動的 JAR 文件復制到 $CATALINA_HOME/lib 中。

1. MySQL 配置

一定要按照下面的說明去操作,否則會出現問題。

創(chuàng)建一個新的測試用戶、一個新的數據庫,以及一張新的測試表。必須為 MySQL 用戶指定一個密碼。如果密碼為空,那么在連接時,就會無法正常驅動。

mysql> GRANT ALL PRIVILEGES ON *.* TO javauser@localhost
    ->   IDENTIFIED BY 'javadude' WITH GRANT OPTION;
mysql> create database javatest;
mysql> use javatest;
mysql> create table testdata (
    ->   id int not null auto_increment primary key,
    ->   foo varchar(25),
    ->   bar int);

注意:一旦測試結束,就該把上例中的這個用戶刪除!

下面在 testdata 表中插入一些測試數據:

mysql> insert into testdata values(null, 'hello', 12345);
Query OK, 1 row affected (0.00 sec)

mysql> select * from testdata;
+----+-------+-------+
| ID | FOO   | BAR   |
+----+-------+-------+
|  1 | hello | 12345 |
+----+-------+-------+
1 row in set (0.00 sec)

mysql>

2. 上下文配置

Context 中添加資源聲明,以便在 Tomcat 中配置 JNDI 數據源。

范例如下:

<Context>

    <!-- maxTotal: Maximum number of database connections in pool. Make sure you
         configure your mysqld max_connections large enough to handle
         all of your db connections. Set to -1 for no limit.
         -->

    <!-- maxIdle: Maximum number of idle database connections to retain in pool.
         Set to -1 for no limit.  See also the DBCP documentation on this
         and the minEvictableIdleTimeMillis configuration parameter.
         -->

    <!-- maxWaitMillis: Maximum time to wait for a database connection to become available
         in ms, in this example 10 seconds. An Exception is thrown if
         this timeout is exceeded.  Set to -1 to wait indefinitely.
         -->

    <!-- username and password: MySQL username and password for database connections  -->

    <!-- driverClassName: Class name for the old mm.mysql JDBC driver is
         org.gjt.mm.mysql.Driver - we recommend using Connector/J though.
         Class name for the official MySQL Connector/J driver is com.mysql.jdbc.Driver.
         -->

    <!-- url: The JDBC connection url for connecting to your MySQL database.
         -->

  <Resource name="jdbc/TestDB" auth="Container" type="javax.sql.DataSource"
               maxTotal="100" maxIdle="30" maxWaitMillis="10000"
               username="javauser" password="javadude" driverClassName="com.mysql.jdbc.Driver"
               url="jdbc:mysql://localhost:3306/javatest"/>

</Context>

3. web.xml 配置

為該測試應用創(chuàng)建一個 WEB-INF/web.xml 文件:

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
    version="2.4">
  <description>MySQL Test App</description>
  <resource-ref>
      <description>DB Connection</description>
      <res-ref-name>jdbc/TestDB</res-ref-name>
      <res-type>javax.sql.DataSource</res-type>
      <res-auth>Container</res-auth>
  </resource-ref>
</web-app>

4. 測試代碼

創(chuàng)建一個簡單的 test.jsp 頁面,稍后將用到它。

<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<sql:query var="rs" dataSource="jdbc/TestDB">
select id, foo, bar from testdata
</sql:query>

<html>
  <head>
    <title>DB Test</title>
  </head>
  <body>

  <h2>Results</h2>

<c:forEach var="row" items="${rs.rows}">
    Foo ${row.foo}<br/>
    Bar ${row.bar}<br/>
</c:forEach>

  </body>
</html>

JSP 頁面用到了 JSTL 的 SQL 和 Core taglibs。你可以從 Apache Tomcat Taglibs - Standard Tag Library 項目中獲取它,不過要注意應該是 1.1.x 或之后的版本。下載 JSTL 后,將 jstl.jarstandard.jar 復制到 Web 應用的 WEB-INF/lib 目錄中。

最后,將你的應用部署到 $CATALINA_BASE/webapps,可以采用兩種方式:或者將應用以名叫 DBTest.war 的 WAR 文件形式部署;或者把應用放入一個叫 DBTest 的子目錄中。

部署完畢后,就可以在瀏覽器輸入 http://localhost:8080/DBTest/test.jsp,查看你的第一個勞動成果了。

Oracle 8i、9i 與 10g

0. 簡介

Oracle 需要的配置和 MySQL 差不多,只不過也存在一些常見問題。

針對過去版本的 Oracle 的驅動可能以 .zip 格式(而不是 .jar 格式)進行分發(fā)的。Tomcat 只使用 *.jar 文件,而且它們還必須安裝在 $CATALINA_HOME/lib 中。因此,classes111.zipclasses12.zip 這樣的文件后綴應該改成 .jar。因為 jar 文件本來就是一種 zip 文件,因此不需要將原 zip 文件解壓縮然后創(chuàng)建相應的 jar 文件,只需改換后綴名即可。

對于 Oracle 9i 之后的版本,應該使用 oracle.jdbc.OracleDriver 而不是 oracle.jdbc.driver.OracleDriver,因為 Oracle 規(guī)定開始棄用 oracle.jdbc.driver.OracleDriver,下一個重大版本將不再支持這一驅動類。

1. 上下文配置

跟前文 MySql 的配置一樣,你也需要在 Context 中定義數據源。下面定義一個叫做 myoracle 的數據源,使用上文說的短驅動來連接(用戶名為 scott,密碼為 tiger)到名為 mysid 的SID(Oracle 系統(tǒng)ID,標識一個數據庫的唯一標示符)。 用戶 scott 使用的 Schema 就是默認的 schema。

使用 OCI 驅動,只需在 URL 字符串中將 thin 變?yōu)?oci 即可。

<Resource name="jdbc/myoracle" auth="Container"
              type="javax.sql.DataSource" driverClassName="oracle.jdbc.OracleDriver"
              url="jdbc:oracle:thin:@127.0.0.1:1521:mysid"
              username="scott" password="tiger" maxTotal="20" maxIdle="10"
              maxWaitMillis="-1"/>

2. web.xml 配置

在創(chuàng)建 Web 應用的 web.xml 文件時,一定要遵從 Web 應用部署描述符文件中 DTD 所需要的元素順序。

<resource-ref>
 <description>Oracle Datasource example</description>
 <res-ref-name>jdbc/myoracle</res-ref-name>
 <res-type>javax.sql.DataSource</res-type>
 <res-auth>Container</res-auth>
</resource-ref>

3. 代碼范例

可以使用上文所列的范例應用(假如你創(chuàng)建了所需的 DB 實例和表,等等),將數據源代碼用下面的代碼替換:

Context initContext = new InitialContext();
Context envContext  = (Context)initContext.lookup("java:/comp/env");
DataSource ds = (DataSource)envContext.lookup("jdbc/myoracle");
Connection conn = ds.getConnection();
//etc.

PostgreSQL

0. 簡介

PostgreSQL 配置與 Oracle 基本相似。

1. 所需文件

將 Postgres 的 JDBC jar 文件復制到 $CATALINA_HOME/lib 中。和 Oracle 配置一樣,jar 文件必須放在這個目錄中,DBCP 類加載器才能找到它們。不管接下來如何配置,這是首先必須要做的。

2. 資源配置

目前有兩種選擇:定義一個能夠被 Tomcat 所有應用所共享的數據源,或者定義只能被單個應用所使用的數據源。

2a. 共享數據源配置

如果想定義能夠被多個 Tomcat 應用所共享的數據源,或者只想在文件中定義自己的數據源,則采用如下配置:

盡管有些用戶反饋說這樣可行,但本文檔作者卻沒有成功,希望有人能闡述清楚。

<Resource name="jdbc/postgres" auth="Container"
          type="javax.sql.DataSource" driverClassName="org.postgresql.Driver"
          url="jdbc:postgresql://127.0.0.1:5432/mydb"
          username="myuser" password="mypasswd" maxTotal="20" maxIdle="10" maxWaitMillis="-1"/>
2b. 應用專屬的資源配置

如果希望專門為某一應用定義數據源,其他 Tomcat 應用無法使用,可以使用如下配置。這種方法對 Tomcat 安裝的損害性要小一些。

在你的應用的 Context 中創(chuàng)建一個資源定義,如下所示:

<Context>

<Resource name="jdbc/postgres" auth="Container"
          type="javax.sql.DataSource" driverClassName="org.postgresql.Driver"
          url="jdbc:postgresql://127.0.0.1:5432/mydb"
          username="myuser" password="mypasswd" maxTotal="20" maxIdle="10"
maxWaitMillis="-1"/>
</Context>

3. web.xml 配置

<resource-ref>
 <description>postgreSQL Datasource example</description>
 <res-ref-name>jdbc/postgres</res-ref-name>
 <res-type>javax.sql.DataSource</res-type>
 <res-auth>Container</res-auth>
</resource-ref>

4. 訪問數據庫

在利用程序訪問數據庫時,記住把 java:/comp/env 放在你的 JNDI lookup 方法參數的前部,如下面這段代碼所示。另外,可以用任何你想用的值來替換 jdbc/postgres,不過記得也要用同樣的值來修改上面的資源定義文件。

InitialContext cxt = new InitialContext();
if ( cxt == null ) {
   throw new Exception("Uh oh -- no context!");
}

DataSource ds = (DataSource) cxt.lookup( "java:/comp/env/jdbc/postgres" );

if ( ds == null ) {
   throw new Exception("Data source not found!");
}

非 DBCP 的解決方案

這些方案或者使用一個單獨的數據庫連接(建議僅作測試用?。?,或者使用其他一些池化技術。

Oracle 8i 與 OCI 客戶端

簡介

雖然并不能嚴格地解決如何使用 OCI 客戶端來創(chuàng)建 JNDI 數據源的問題,但這些注意事項卻能和上文提到的 Oracle 與 DBCP 解決方案結合起來使用。

為了使用 OCI 驅動,應該先安裝一個 Oracle 客戶。你應該已經通過光盤安裝好了 Oracle 8i(8.1.7)客戶端,并從 otn.oracle.com 下載了適用的 JDBC/OCI 驅動(Oracle8i 8.1.7.1 JDBC/OCI 驅動)。

classes12.zip 重命名為 classes12.jar 后,將其復制到 $CATALINA_HOME/lib 中。根據 Tomcat 的版本以及你所使用的 JDK,你可能還必須該文件中的刪除 javax.sql.* 類。

連接起來

確保在 $PATHLD_LIBRARY_PATH(可能在 $ORAHOME\bin)目錄下存在 ocijdbc8.dll.so 文件,另外還要確認能否使用 System.loadLibrary("ocijdbc8"); 這樣的簡單測試程序加載本地庫。

下面你應該創(chuàng)建一個簡單測試用 servlet 或 jsp,其中應該包含以下關鍵代碼:

DriverManager.registerDriver(new
oracle.jdbc.driver.OracleDriver());
conn =
DriverManager.getConnection("jdbc:oracle:oci8:@database","username","password");

目前數據庫是 host:port:SID 形式,如果你試圖訪問測試用servlet/jsp,那么你會得到一個 ServletException 異常,造成異常的根本原因在于 java.lang.UnsatisfiedLinkError:get_env_handle

分析一下,首先 UnsatisfiedLinkError 表明:

  • JDBC 類文件和 Oracle 客戶端版本不匹配。消息中透露出的意思是沒有找到需要的庫文件。比如,你可能使用 Oracle 8.1.6 的 class12.zip 文件,而 Oracle 客戶端版本則是 8.1.5。classeXXXs.zip 文件必須與 Oracle 客戶端文件版本相一致。

  • 出現了一個 $PATH, LD_LIBRARY_PATH 問題。

  • 有報告稱,忽略從 otn 網站下載的驅動,使用 $ORAHOME\jdbc\lib 目錄中的 class12.zip 文件,同樣能夠正常運作。

接下來,你可能還會遇到另一個錯誤消息:ORA-06401 NETCMN: invalid driver designator

Oracle 文檔是這么說的:“異常原因:登錄(連接)字符串包含一個不合法的驅動標識符。解決方法:修改字符串,重新提交?!彼?,如下面這樣來修改數據庫(host:port:SID)連接字符串:

(description=(address=(host=myhost)(protocol=tcp)(port=1521))(connect_data=(sid=orcl)))

常見問題

下面是一些 Web 應用在使用數據庫時經常會遇到的問題,以及一些應對技巧。

數據庫連接間歇性失敗

Tomcat 運行在 JVM 中。JVM 周期性地會執(zhí)行垃圾回收(GC),清除不再使用的 Java 對象。當 JVM 執(zhí)行 GC 時,Tomcat 中的代碼執(zhí)行就會終止。如果配置好的數據庫連接建立的最長時間小于垃圾回收的時間,數據庫連接就會失敗。

在啟動 Tomcat 時,將 -verbose:gc 參數添加到 CATALINA_OPTS 環(huán)境變量中,就能知道垃圾回收所占用的時間了。在啟用 verbose:gc 后, $CATALINA_BASE/logs/catalina.out 日志文件就能包含每次垃圾回收的數據,其中也包括它所占用的時間。

正確調整 JVM 后,垃圾回收可以做到在 99% 的情況下占用時間不超過 1 秒。剩余的情況則只占用幾秒鐘的時間,只有極少數情況下 GC 會占用超過 10 秒鐘的時間。

保證讓數據庫連接超時設定在 10~15 秒。對于 DBCP,可以使用 maxWaitMillis 參數來設置。

隨機性的連接關閉異常

當某一請求從連接池中獲取了一個數據庫連接,然后關閉了它兩次時,往往會出現這樣的異常消息。使用連接池時,關閉連接,就會把它歸還給連接池,以便之后其他的請求能夠重用該連接,而并不會關閉連接。Tomcat 使用多個線程來處理并發(fā)請求。下面這個范例就演示了,在 Tomcat 中,一系列事件導致了這種錯誤。

運行在線程 1 中的請求 1 獲取了一個連接。

請求 1 關閉了數據庫連接。

JVM 將運行的線程切換為線程 2

線程 2 中運行的請求 2 獲取了一個數據庫連接。
(同一個數據庫連接剛被請求 1 關閉)

JVM 又將運行的線程切換回為線程 1

請求 1 第二次關閉了數據庫連接。

JVM 將運行的線程切換回線程 2

請求 2線程 2 試圖使用數據庫連接,但卻失敗了。因為請求 1 已經關閉了它。

  Connection conn = null;
  Statement stmt = null;  // Or PreparedStatement if needed
  ResultSet rs = null;
  try {
    conn = ... get connection from connection pool ...
    stmt = conn.createStatement("select ...");
    rs = stmt.executeQuery();
    ... iterate through the result set ...
    rs.close();
    rs = null;
    stmt.close();
    stmt = null;
    conn.close(); // Return to connection pool
    conn = null;  // Make sure we don't close it twice
  } catch (SQLException e) {
    ... deal with errors ...
  } finally {
    // Always make sure result sets and statements are closed,
    // and the connection is returned to the pool
    if (rs != null) {
      try { rs.close(); } catch (SQLException e) { ; }
      rs = null;
    }
    if (stmt != null) {
      try { stmt.close(); } catch (SQLException e) { ; }
      stmt = null;
    }
    if (conn != null) {
      try { conn.close(); } catch (SQLException e) { ; }
      conn = null;
    }
  }

上下文與全局命名資源

注意,雖然在上面的說明中,把 JNDI 聲明放在一個 Context 元素里面,但還是有可能(而且有時更需要)把這些聲明放在服務器配置文件的 GlobalNamingResources 區(qū)域。被放置在 GlobalNamingResources 區(qū)域的資源將會被服務器的各個上下文所共享。

JNDI 資源命名和 Realm 交互

為了讓 Realm 能運作,realm 必須指向定義在 <GlobalNamingResources><Context> 區(qū)域中的數據源,而不是<ResourceLink> 重新命名的數據源。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號