2009-12-02傳智播客 數(shù)據(jù)庫——JDBC開發(fā) 連接池與事務(wù)
Posted on 2009-12-02 23:20 長城 閱讀(757) 評論(1) 編輯 收藏Today is JDBC高級部分,課程的主要內(nèi)容有連接池與事務(wù)。這都是在應(yīng)用開發(fā)中比較常用,比較重要的。
一、使用配置文件優(yōu)化JDBCUtil,使用工廠模式隔離DAO層:
昨天有說過將對數(shù)據(jù)庫獲取連接和釋放的操作單封裝到一個類中,如:
import java.sql.*; import cn.itcast.cc.exception.JDBCUtilException; /** * JDBC工具類 * * @author Administrator * */ public class JDBCUtil { // 連接數(shù)據(jù)庫時所需要的參數(shù) private static String url = "jdbc:mysql://localhost:3306/jdbc"; private static String username = "root"; private static String password = "root"; // 類被加載時就加載驅(qū)動 static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } // 獲取連接 public static Connection getConnection() throws JDBCUtilException { try { return DriverManager.getConnection(url, username, password); } catch (SQLException e) { throw new JDBCUtilException(e); } } // 釋放連接等資源,下面是一種健康的釋放方式 public static void release(ResultSet rs, Statement sta, Connection conn) throws JDBCUtilException { if (rs != null) { try { rs.close(); } catch (SQLException e) { throw new JDBCUtilException(e); } rs = null; } if (sta != null) { try { sta.close(); } catch (SQLException e) { throw new JDBCUtilException(e); } sta = null; } if (conn != null) { try { conn.close(); } catch (SQLException e) { throw new JDBCUtilException(e); } conn = null; } } }
從上面我們看到其中裝載數(shù)據(jù)庫驅(qū)動和獲取數(shù)據(jù)庫連接的代碼:
Class.forName("com.mysql.jdbc.Driver"); DriverManager.getConnection(url, username, password); |
我們使用了固定的url,它是一個成員。在以后的開發(fā)中,如果要使用其他的數(shù)據(jù)庫或者數(shù)據(jù)庫的用戶名密碼被改變了,我們還需要手動的修改上邊的代碼,重新編譯。這是一種不良的設(shè)計方法,為了使我們的代碼與數(shù)據(jù)庫分離,我們可以將這些參數(shù)配置到配置文件中。這樣在需要使用時去讀取配置文件中的對應(yīng)值即可。以后換數(shù)據(jù)庫或更改了用戶名,直接修改一下配置文件即可。
昨天有提到過DAO層,DAO層專門用于處理對數(shù)據(jù)庫的CURD操作等,比如,添加用戶、查找用戶、修改用戶、刪除用戶,我們可以把這些操作封裝到一個類中(UserDao.java),它所處位置就是DAO層。比如,用于處理用戶注冊的Servlet,在這個Servlet中直接實例化一個UserDao對象。然后調(diào)用userDaoObj.add(userbean);方法即可實現(xiàn)用戶的注冊。
如果我想換一種數(shù)據(jù)存儲方式,比如配置文件,我只將用戶信息保存在配置文件中。這樣我就需要修改UserDao.java,但有時又需要UserDao.java這個功能類怎么辦?我只能重新創(chuàng)建一個專門處理配置文件數(shù)據(jù)的類(UserDaoPro.java)。這樣我又需要修改Servlet中的代碼,將實例化UserDao的代碼修改為實例化UserDaoPro對象。那UserDaoPro與UserDao的接口是不是相同的呢?如果不相同麻煩就大了,JAVA是提倡使用接口編程的,就是為了實現(xiàn)接口的統(tǒng)一化。
可見,上面的這種實現(xiàn)方式是存在問題的。即使接口統(tǒng)一,我們還需要修改Servlet中的代碼。此時工廠模式派上了用場,還記得工廠模式吧!是這樣的:
1.定義一個UserDao接口,統(tǒng)一對用戶操作的接口。
2.定義一個UserDaoFactory的工廠類,專門生產(chǎn)UserDao對象。
3. UserDaoFactory工廠從配置文件中讀取實現(xiàn)了UserDao接口的類名稱。使用此類生產(chǎn)產(chǎn)品——UserDao對象。
4.在Servlet中使用UserDaoFactory工廠來創(chuàng)建需要的UserDao對象。
這樣就實現(xiàn)了DAO層與Servlet的完全分離!完美!
二、數(shù)據(jù)庫連接池:
每當有一個新的連接發(fā)生時,數(shù)據(jù)庫就需要創(chuàng)建一個Connection對象來處理連接。訪問結(jié)束后,Connection對象被釋放。數(shù)據(jù)庫創(chuàng)建一個Connection對象,是十分耗時且消耗較大的服務(wù)器資源。試想,如果有500個用戶同時訪問數(shù)據(jù)庫,數(shù)據(jù)庫同時創(chuàng)建500個Connection將會是什么樣子!因此,連接池這一技術(shù)誕生了。
連接池,在服務(wù)器加載WEB應(yīng)用后。WEB應(yīng)用會自動創(chuàng)建一個連接池,池中包含多個Connection對象。每當有新的連接請求時,便從這個池中拿出一個Connection對象用于處理連接,使用完成后,便還回給這個池子。下面代碼為連接池的實現(xiàn)原理:
import java.io.*; import java.lang.reflect.*; import java.sql.*; import java.util.*; import javax.sql.DataSource; /** * 自己編寫的簡單連接池類,單例模式實現(xiàn)。 * * @author Administrator * */ public class JDBCUtil implements DataSource { private static LinkedList<Connection> conns = new LinkedList<Connection>(); private static JDBCUtil myjdbcutil = new JDBCUtil(); private JDBCUtil() { // 取配置文件 InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream( "cn/itcast/cc/db/myjdbc.properties"); // 裝載配置文件 Properties pro = new Properties(); try { pro.load(in); } catch (IOException e) { e.printStackTrace(); } // 取出配置項 String driver = pro.getProperty("driverClassName"); String url = pro.getProperty("url"); String username = pro.getProperty("username"); String password = pro.getProperty("password"); int initialSize = Integer.parseInt(pro.getProperty("initialSize")); // 加載驅(qū)動,填充連接池 try { // 常用的數(shù)據(jù)庫,驅(qū)動類被加載時,都會自已加載驅(qū)動。 Class.forName(driver); for (int i = 0; i < initialSize; i++) { conns.add(DriverManager.getConnection(url, username, password)); } } catch (Exception e) { e.printStackTrace(); } } // 返回單例的實例 public static JDBCUtil getInstance() { return myjdbcutil; } public Connection getConnection() throws SQLException { if (conns.size() > 0) { final Connection conn = conns.pop(); // 此處需要使用動態(tài)代理技術(shù),因為返回的Connection對象在關(guān)閉時需要被收回到LinkedList中。 // 動態(tài)代理,增強Connection.colse()方法,將它回收到LinkedList中。但不銷毀! return (Connection) Proxy.newProxyInstance(JDBCUtil.class .getClassLoader(), conn.getClass().getInterfaces(), new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 如果是close方法,則將conn回收到List中。 if (method.getName().equals("close")) { conns.push(conn); return null; } return method.invoke(conn, args); } }); } throw new RuntimeException("服務(wù)器忙,請稍后連接!"); }
}
有很多服務(wù)器為用戶提供了DataSource(數(shù)據(jù)源)的實現(xiàn)。DataSource中包含了連接池的實現(xiàn)。也有一些第三方組織單獨提供了連接池的實現(xiàn):DBCP、C3P0。所以,在以后的開發(fā)中,我們可以直接使用這些資源。
著名的DBCP比較常用,使用DBCP必須引入Commons-dbcp.jar和Commons-pool.jar。Tomcat中的連接池也是使用DBCP實現(xiàn)的。DBCP的使用非常簡單:
import java.io.InputStream; import java.sql.*; import java.util.Properties; import javax.sql.DataSource; import org.apache.commons.dbcp.BasicDataSourceFactory; public class DBCPUtil { private static DataSource ds = null; static { try { // 取配置文件 InputStream in = JDBCUtil.class.getClassLoader() .getResourceAsStream( "cn/itcast/cc/db/dbcpconfig.properties"); // 裝載配置文件 Properties pro = new Properties(); pro.load(in); // 創(chuàng)建數(shù)據(jù)源 ds = BasicDataSourceFactory.createDataSource(pro); } catch (Exception e) { e.printStackTrace(); } } // 獲取連接 public static Connection getConnection() throws SQLException { return ds.getConnection(); } }
其中的配置文件“dbcpconfig.properties”:
#連接設(shè)置 #驅(qū)動器 driverClassName=com.mysql.jdbc.Driver #數(shù)據(jù)庫連接URL url=jdbc:mysql://localhost:3306/jdbc #數(shù)據(jù)庫用戶名 username=root #數(shù)據(jù)庫密碼 password=root #初始連接池數(shù)量 initialSize=10 #最大連接數(shù)量 maxActive=50 #最大空閑連接 maxIdle=20 #最小空閑連接 minIdle=5 #超時等待時間以毫秒為單位 6000毫秒/1000等于60秒 maxWait=60000 #JDBC驅(qū)動建立連接時附帶的連接屬性屬性的格式必須為這樣:[屬性名=property;] #注意:"user" 與 "password" 兩個屬性會被明確地傳遞,因此這里不需要包含他們。 connectionProperties=useUnicode=true;characterEncoding=UTF8 #指定由連接池所創(chuàng)建的連接的自動提交(auto-commit)狀態(tài)。 defaultAutoCommit=true #driver default 指定由連接池所創(chuàng)建的連接的只讀(read-only)狀態(tài)。 #如果沒有設(shè)置該值,則“setReadOnly”方法將不被調(diào)用。(某些驅(qū)動并不支持只讀模式,如:Informix) defaultReadOnly= #driver default 指定由連接池所創(chuàng)建的連接的事務(wù)級別(TransactionIsolation)。 #可用值為下列之一:(詳情可見javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE defaultTransactionIsolation=READ_UNCOMMITTED |
上面是直接使用Apache提供的DBCP實現(xiàn)的連接池,接下來看一下使用JNDI技術(shù)在Tomcat中配置連接池,在Tomcat服務(wù)的conf\context.xml文件中添加:
<Context> <Resource name="jdbc/datasource" auth="Container" type="javax.sql.DataSource" username="root" password="root" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/jdbc" maxActive="8" maxIdle="4"/> </Context> |
Name: 對象在JNDI容器中的名稱。
Auth:所使用的容器。
Type:對象類型。
Username:數(shù)據(jù)庫用戶名。
Password:數(shù)據(jù)庫密碼。
driverClassName:數(shù)據(jù)庫驅(qū)動類。
url:連接數(shù)據(jù)庫的URL。
maxActive:最大連接數(shù)。
maxIdle:最大空閑連接數(shù)。
注意其中的數(shù)據(jù)庫驅(qū)動,對應(yīng)的jar包必須放置到Tomcat目錄下的lib目錄中。這樣服務(wù)器才能找得到。
在WEB應(yīng)用中獲取數(shù)據(jù)庫連接的代碼:
// 初始化JNDI環(huán)境 Context initCtx = new InitialContext(); // 獲取JNDI容器 Context envCtx = (Context) initCtx.lookup("java:comp/env"); // 獲取數(shù)據(jù)源 DataSource dataSource = (DataSource)envCtx.lookup("jdbc/datasource"); // 獲取數(shù)據(jù)庫連接 dataSource.getConnection(); |
JNDI是一個對象容器,服務(wù)器根據(jù)配置文件提供的信息。創(chuàng)建這些對象,并按照指定的名稱保存在容器中。WEB應(yīng)用,無論在何處都可以根據(jù)名稱獲取在JNDI容器中的對象。
三、數(shù)據(jù)庫事務(wù)管理:
什么是事務(wù)?老方有這樣的面試題。事務(wù)指邏輯上的一組操作,這組操作要不全部生效,要不全部失效。在控制臺中操作事務(wù)的命令:
start transaction 開啟事務(wù) Rollback 回滾事務(wù) Commit 提交事務(wù) set transction isolation level 設(shè)置事務(wù)隔離級別 select @@tx_isolation 查詢當前事務(wù)隔離級別 |
事務(wù)的特性(ACID),這在面試中是比較常見的。老方一再強調(diào),一定要記下來!OK,讓我們看一看。
事務(wù)的原子性(Automicity),它更像是事務(wù)的定義。就是邏輯上的一組操作,不可分割,要么全部生效,要么全部失效。
事務(wù)的一致性(Consistency),老方的PPT有中說:事務(wù)必須使數(shù)據(jù)庫從一個一致性狀態(tài)變換到另外一個一致性狀態(tài)。比如,銀行轉(zhuǎn)賬,A-B。A賬戶中減少100元,B帳戶中增加一百元。這看起來比較好理解,但讓我有點疑問的是,如果從數(shù)據(jù)庫中刪除一個用戶呢?難道不用事務(wù)處理嗎?所以我對這個一致性的理解除了老方說的,還有只要事務(wù)處理符合正確的邏輯,就是一致性。
隔離性(Isolation),如果多個用戶同時訪問數(shù)據(jù)庫,同時訪問一張表。這樣會造成一些訪問錯誤,所以有了隔離性這一定義。將多個并發(fā)事務(wù)之間要相互隔離。
持久性(Durability),事務(wù)一旦被提交,它對數(shù)據(jù)庫中數(shù)據(jù)的改變是永久性的,無論其他操作或數(shù)據(jù)庫故障不會對它有任何影響。
在JDBC中使用事務(wù)的語句:
// 開啟事務(wù) conn.setAutoCommit(false); // 回滾事務(wù) conn.rollback(); // 提交事務(wù) conn.commit(); // ... // 設(shè)置保存點 sp = conn.setSavepoint(); // 回滾到指定保存點 conn.rollback(sp); |
方老師使用了一個經(jīng)典的案例,來講解事務(wù)處理——銀行轉(zhuǎn)賬系統(tǒng)!
例,用戶A轉(zhuǎn)賬10000元給用戶B。應(yīng)該是A賬戶中減少10000元,B賬戶中增加10000元。那么在A提交了10000元后,數(shù)據(jù)庫發(fā)生問題(比如斷電),那B帳戶增加10000元的語句還沒有執(zhí)行。使用事務(wù)可以很好解決這一問題:
1. 開啟事務(wù)
2. A帳戶減少10000元
3. 中間發(fā)生錯誤,回滾事務(wù)
4. B帳戶增加10000元
5. 提交事務(wù)
OK,在上同這一過程中,如果沒有執(zhí)行到第5步。數(shù)據(jù)庫的內(nèi)容就不會發(fā)生改變。在此有必要解釋一下回滾事務(wù),回滾事務(wù)在提交事務(wù)后執(zhí)行沒有什么效果,回滾事務(wù)在提交事務(wù)之前執(zhí)行,會擦除在開啟事務(wù)之后執(zhí)行的SQL語句。
上面是針對單一事務(wù)進行的處理,在并發(fā)事務(wù)中會遇到一些問題。設(shè)置事務(wù)的隔離級別可以很好的解決這一問題。
首先我們來看一下并發(fā)事務(wù)容易產(chǎn)生的問題:
臟讀:
例,用戶A賬戶減少10000元,用戶B賬戶增加了10000元,但A還沒有提交事務(wù)。此時,B去查詢它的賬戶發(fā)現(xiàn)A已經(jīng)給自己轉(zhuǎn)賬了10000元。然后,A回滾了事務(wù)。B被A給騙了,這就是臟讀,B讀到的是未提交的數(shù)據(jù)。我雖對數(shù)據(jù)庫事務(wù)的實現(xiàn)原理不了解(事務(wù)是通過日志處理的),我打個比方就更好理解這一原因了。數(shù)據(jù)庫對所有用戶維護了一個虛表,在開啟事務(wù)中,提交事務(wù)前對數(shù)據(jù)的修改,都是修改的虛表,用戶查詢的也是虛表的內(nèi)容,當執(zhí)行了提交事務(wù)操作后,將虛表中被修改的內(nèi)容保存到數(shù)據(jù)庫。實際技術(shù)細節(jié)就不深入了!
不可重復(fù)讀:
例,用戶A原賬戶內(nèi)容為100萬元,此時銀行將數(shù)據(jù)顯示到電腦屏幕和保存到文件中。銀行工作人員在電腦屏幕上看到的是100萬元,但此時,用戶A向賬戶里又存了100萬元。結(jié)果保存到文件的是200萬元。到是100萬元還是200萬元?工作人員有些迷惑。與臟讀相反的是,臟讀是讀取了前一事務(wù)未提交的數(shù)據(jù),不可重復(fù)讀讀取的是前一事務(wù)提交了的事務(wù)。注意,不可重復(fù)讀針對的是某一記錄!
虛讀:
例,銀行要統(tǒng)計A的報表。A的賬戶中有200萬元,屏幕上報表顯示的總額是200萬元,此時,用戶A向賬戶中又存入了100萬元。報表打印出來的總額就是300萬元。工作人員一對比,到底哪個是正確的?注意,虛讀針對的是整個表。
不可重復(fù)讀和虛讀,有點難理解。這看起來不是什么問題,如果發(fā)生這一問題,工作人員就刷新一下表唄!如果工作人員處理的是1萬個用戶的報表,同時不幸運的是這1萬個用戶都發(fā)生了虛讀的問題。那工作人員能受了嗎?你可能還會說那就以打印的報表為準唄!問題是,可能屏幕上的那個才是正確的,這樣就要重新打印報表。所以為了使問題得以簡單的解決,事務(wù)的隔離性發(fā)揮了它的作用!
處理上邊的三種并發(fā)事務(wù)產(chǎn)生的問題,數(shù)據(jù)庫定義了四種隔離級別:
Serializable:可避免臟讀、不可重復(fù)讀、虛讀情況的發(fā)生。 Repeatable read:可避免臟讀、不可重復(fù)讀情況的發(fā)生。 Read committed:可避免臟讀情況發(fā)生。 Read uncommitted:最低級別,以上情況均無法保證。 |
Serializable,最高安全級別。設(shè)置了此級別,當一個事務(wù)訪問了某一表時,此表就會被封死。其他事務(wù)對此表的訪問就會被掛起,直到上一事務(wù)提交后才執(zhí)行一個事務(wù)。OK,這看起來十分容易理解。
Repeatable read,讓我再小深入一下,當每個事務(wù)線程訪問同一表時,數(shù)據(jù)庫針對每個線程維護了一張?zhí)摂M表。此時各線程看到的都是原始的數(shù)據(jù)內(nèi)容,對表數(shù)據(jù)的修改相互不發(fā)生影響,即使事務(wù)被提交了。數(shù)據(jù)庫可能為每個線程維護了一張?zhí)摂M表嗎?當然不可以,我想這只是為了便于理解。技術(shù)細節(jié)不深入研究!
Read committed,數(shù)據(jù)庫對每個用戶的查詢顯示的都是原始內(nèi)容(真實內(nèi)容)。如果某些用戶對此表的事務(wù)沒有提交就不會影響原始內(nèi)容。所以其他用戶查看到的都是原始內(nèi)容或提交了的數(shù)據(jù)內(nèi)容。
Read uncommitted,這個就不多說了。
今天的內(nèi)容也就事務(wù)的隔離性有些難度吧!一般也不會去用那最高安全級別,這一級別在銀行中比較常用。
正如老方所說,是不是感覺越學(xué)越簡單了!確實如此,我想大家應(yīng)該也對此有感覺。不知不覺明天又休息了!
加油!