1. 程式人生 > 實用技巧 >JDBC的架構設計

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做了一個簡單的比較。