JDBC的架構設計
本文探討JDBC需要解決的問題及如何解決和設計的,包括:
- JDBC要解決的問題
- 資料庫事務
- JDBC的架構設計
- JDBC程式碼注意點
- Spring是如何處理事務
- 什麼是事務的傳播特性
- Redis事務與資料庫事務的區別
問題
在《架構風格:萬金油CS與分層》中提到,三層架構一般分為:
- Presentation tier 表現層
- Logic tier 業務邏輯層
- Data access tier 資料訪問層
那業務邏輯層與資料訪問層是如何通訊的呢?(假設資料訪問層使用的是資料庫進行資料持久化,這也是目前比較普遍的做法)
單看「業務邏輯層」和「資料訪問層」,這是明顯的CS風格:
- 「業務邏輯層」是Client
- 「資料訪問層」是Server
CS風格的約束如下:
- Server元件提供了一組服務,並監聽對這些服務的請求(這裡就是資料庫服務),
- Client元件通過一個聯結器將請求傳送到Server,希望執行一個服務(執行sql),
- Server可以拒絕這個請求,也可以執行這個請求並將響應傳送回Client(sql執行結果)。
除了上面的約束外,針對資料庫訪問而言,還應該包括如下約束:
- 相容各種資料庫。不能每個資料庫都有一套介面,這樣會導致業務邏輯層與資料訪問層的緊耦合。
- 事務支援。需要支援資料庫事務,否則資料會出現混亂。
設計
針對上面的約束,可以抽象出四個元件:
- 連線元件:對與資料訪問層的連線的抽象
- 執行元件:執行對資料庫的操作的抽象
- 結果元件:對返回操作後的結果的抽象
- 適配元件:適配各個資料庫,就是對各個資料庫的抽象
這就是JDBC的結構:
- Connection:連線元件。提供統一的API建立資料庫連線。
- Statement:執行元件。針對各種執行有不同的實現,Statement,PreparedStatement,CallableStatement。提供統一的API執行sql。
- ResultSet:結果元件,對結果的抽象,提供統一的API操作結果資料。
- Driver:適配元件,對各個不同的資料庫操作進行適配,統一為一套對外一致的API。各個Driver統一由DriverManager進行管理。
基於上面的設計,也就統一了JDBC操作資料庫的流程:
- 註冊驅動
- 建立連線
- 建立執行物件
- 執行語句
- 處理結果
- 釋放資源
如果你google一下JDBC程式碼,一般會得到類似下面的程式碼:
// 1.註冊驅動
Class.forName("com.mysql.jdbc.Driver"); // 這一行程式碼實際早就不需要了!!!// 2.建立連線Connection conn = DriverManager.getConnection(url, user, password);// 3.建立執行物件Statement st = conn.createStatement();// 4.執行語句ResultSet rs = st.executeQuery("select * from table1");// 5.處理結果while (rs.next()) { ......}// 6.釋放資源rs.close();st.close();conn.close();
實際上,早就已經不需要第一步「註冊驅動」的程式碼了!在google第一頁的十條結果中,只有一條結果提到了此問題!
你可以嘗試一下,將第一行程式碼註釋掉,程式還是能正常的執行。原因就是現在的JDBC驅動一般都使用了SPI來自動的註冊驅動了,不再需要手動的去註冊了。
SPI你可以認為是Java原生提供的的依賴注入!以mysql的驅動為例:
- 在mysql的驅動包META-INF目錄下,有一個services目錄
- 裡面有個檔案,叫java.sql.Driver。裡面的內容為
- com.mysql.jdbc.Driver
- com.mysql.fabric.jdbc.FabricMySQLDriver
- 名稱是介面,檔案裡的內容是實現
- JVM會根據上面的配置,去找對應的類,並把它載入進去
- DriverManager通過ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);這行程式碼載入驅動,將其加到驅動列表中進行管理。實現註冊驅動的邏輯
效能優化
從上面的程式碼可以看到,每次進行sql查詢的時候,都需要建立連線。我們都知道建立連線是個很耗時的操作,如何能降低建立連線的開銷呢?
在《非功能性約束之效能(1)-效能銀彈:快取》中,已經給出答案了。就是引入「快取」!
這裡的「快取」就是連線池!即:
- 先建立一批連線,一般是在應用啟動時
- 當需要連線的時候,直接從連線池中獲取
- 操作完了之後,將連線再還到連線池中
如果你瞭解IO多路複用或EDA架構風格,那麼你可能會有個疑問。為什麼資料庫連線不使用IO多路複用技術呢?
IO多路複用或EDA架構風格可以參考《EDA風格與Reactor模式》!
其實在《EDA風格與Reactor模式》裡已經給出了答案:如果請求資料量太大或處理時間很長!即使是主從Reactor模型,執行緒池也會被耗盡!耗盡後,導致後續的請求積壓。
關係資料庫的底層原理決定了其效能瓶頸在於磁碟讀寫,也就是說如果使用IO多路複用,瓶頸會出現在執行執行緒(資料庫查詢的磁碟操作),而不在IO通訊。使用IO多路複用並不會提高資料庫的訪問,而且程式設計模型要複雜得多。
而redis能使用IO多路複用的原因是它主要是記憶體操作,速度要比磁碟操作快得多,瓶頸更可能出在IO通訊上。
什麼是事務
JDBC相關的另一個大的主題就是事務!
滿足ACID四個特性的資料庫操作,稱為資料庫事務
這裡的事務指資料庫本地事務,不包括分散式事務,分散式事務後續討論!
四個特性包括:
- 原子性(Atomicity):事務作為一個整體被執行,包含在其中的對資料庫的操作要麼全部被執行,要麼都不執行
- 一致性(Consistency):事務應確保資料庫的狀態從一個一致狀態轉變為另一個一致狀態。一致狀態的含義是資料庫中的資料應滿足完整性約束
- 隔離性(Isolation):多個事務併發執行時,一個事務的執行不應影響其他事務的執行
- 永續性(Durability):已被提交的事務對資料庫的修改應該永久儲存在資料庫中
其中,隔離性又分為四種隔離級別,隔離級別從嚴格到寬鬆依次為:
- 可序列性(Serializability):所有的事務依次逐個執行。這是最嚴格的的隔離級別,實際就是對事務進行排隊,一個一個的執行,保證了每個事務的獨立性,但是嚴重的影響了效能。如果有一個事務的執行耗時很長,那麼後面的事務就全部等待。此隔離級別基本不會使用。
- 可重複讀取(Repeatable Read):一個事務在整個過程中可以多次重複執行某個查詢。相對寬鬆的隔離級別,即在一個事務中某個查詢,多次執行時,所得到的資料內容是一樣的,但是得到的資料條數不一定是一樣的,即所謂的幻讀。
- 已提交讀(Read Committed):一個事務只能讀取另一個事務已經提交的資料。更寬鬆的隔離級別,此隔離級別可能引起幻讀和不可重複讀。這是大部分資料庫的預設隔離級別。
- 未提交讀(Read Uncommitted):一個事務可以讀取另一個事務修改但還沒有提交的資料。最寬鬆的隔離級別,此隔離級別可能引起幻讀,不可重複讀,髒讀。
- 幻讀(行):一個事務中多次讀取,取到另外一個事務已經提交的新增資料(表級)
- 不可重複讀:多次讀取同一個資料返回結果有所不同(行級)
- 髒讀:一個事務讀取到另外一個事務未提交的更新的資料(行級)
事務在程式碼中的體現就是conn.setAutoCommit(false),conn.commit(),conn.rollback():
......
// 2.建立連線Connection conn = DriverManager.getConnection(url, user, password);// 關閉自動提交,開啟事務conn.setAutoCommit(false);Statement st = conn.createStatement();try { ResultSet rs = st.executeQuery("select * from table1"); // 提交事務 conn.commit();} catch(Exception e) { conn.rollback(); // 出現異常進行事務回滾}......
Spring中的事務
Spring通過Transactional註解來簡化了事務的管理。
Transactional對事務的處理流程如下:
- 開啟事務
- 將conn物件儲存到ThreadLocal中
- 執行sql。「這裡就是你寫程式碼的地方」
- 從ThreadLocal中取回conn物件
- 提交事務
Transactional的屬性包括:
- isolation:配置事務的隔離級別
- readOnly:配置只讀。預設false。需要事務才能生效
- rollbackFor:配置事務回滾。預設在遇到RuntimeException時進行回滾
- propagation:配置事務的傳播特性
事務的傳播特性:
- propagation=Propagation.NOT_SUPPORTED:針對某個方法不開啟事務
- propagation=Propagation.REQUIRED:如果有事務,那麼加入事務,沒有的話新建立一個, 預設的事務支援
- propagation=Propagation.REQUIREDS_NEW:不管是否存在事務,都建立一個新的事務,原來的掛起,新的執行完畢,繼續執行老的事務
- propagation=Propagation.MANDATORY:必須在一個已有的事務中執行,否則丟擲異常
- propagation=Propagation.NEVER:不能在一個事務中執行,就是當前必須沒有事務,否則丟擲異常
- propagation=Propagation.SUPPORTS:其他bean呼叫這個方法,如果在其他bean中聲明瞭事務,就是用事務。沒有宣告,就不用事務。
- propagation=Propagation.NESTED:如果一個活動的事務存在,則執行在一個巢狀的事務中,如果沒有活動的事務,則按照REQUIRED屬性執行,它使用一個單獨的事務
- propagation=Propagation.REQUIRED,readOnly=true:只讀,不能更新,刪除
- propagation=Propagation.REQUIRED,timeout=30:超時30秒
- propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT:資料庫隔離級別
事務總結
redis的事務
從表面看redis事務和jdbc事務很類似,都是下面的流程:
- 開始事務「redis通過MULTI指令執行」
- 輸入需要執行的命令
- 執行事務「redis通過EXEC指令執行」
實際上有本質的區別。redis的事務實際上就是指令的批量執行,中間某條指令的失敗不會導致前面已做指令的回滾,也不會造成後續的指令不做。
總結
本文通過業務邏輯層與資料持久層的通訊約束,來闡述JDBC的設計及總結本地事務的相關知識點,同時與redis做了一個簡單的比較。