前言
Cloud Native
我們能否繞開 http 協(xié)議,直接測試數(shù)據(jù)庫的性能?是否覺得從數(shù)據(jù)庫中導(dǎo)出 CSV 文件來構(gòu)造壓測數(shù)據(jù)很麻煩?怎樣在壓測結(jié)束后做數(shù)據(jù)清理?能不能通過數(shù)據(jù)庫中的插入(刪除)記錄對壓測請求做斷言?使用阿里云性能測試工具 PTS 可以輕松解決上述問題。
什么是 JDBC
Cloud Native
JDBC(Java DataBase Connectivity,Java 數(shù)據(jù)庫連接)是一種用于執(zhí)行 SQL 語句的 Java API,可以為多種關(guān)系數(shù)據(jù)庫提供統(tǒng)一訪問,它由一組用 Java 語言編寫的類和接口組成。JDBC 提供了一種基準(zhǔn),據(jù)此可以構(gòu)建更高級的工具和接口,使數(shù)據(jù)庫開發(fā)人員能夠編寫數(shù)據(jù)庫應(yīng)用程序。
簡單地說,JDBC 可做三件事:與數(shù)據(jù)庫建立連接、發(fā)送操作數(shù)據(jù)庫的語句并處理結(jié)果。
JDBC 的設(shè)計原理
Cloud Native
1
整體架構(gòu)
JDBC 制定了一套和數(shù)據(jù)庫進行交互的標(biāo)準(zhǔn),數(shù)據(jù)庫廠商提供這套標(biāo)準(zhǔn)的實現(xiàn),這樣就可以通過統(tǒng)一的 JDBC 接口來連接各種不同的數(shù)據(jù)庫。可以說 JDBC 的作用是屏蔽了底層數(shù)據(jù)庫的差異,使得用戶按照 JDBC 寫的代碼可以在各種不同的數(shù)據(jù)庫上進行執(zhí)行。那么這是如何實現(xiàn)的呢?如下圖所示:
JDBC 定義了 Driver 接口,這個接口就是數(shù)據(jù)庫的驅(qū)動程序, 所有跟數(shù)據(jù)庫打交道的操作最后都會歸結(jié)到這里 ,數(shù)據(jù)庫廠商必須實現(xiàn)該接口,通過這個接口來完成上層應(yīng)用的調(diào)用者和底層具體的數(shù)據(jù)庫進行交互。Driver 是通過 JDBC 提供的 DriverManager 進行注冊的,注冊的代碼寫在了 Driver 的靜態(tài)塊中,如 MySQL 的注冊代碼如下所示:
static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException(“Can’t register driver!”); } }作為驅(qū)動定義的規(guī)范 Driver,它的主要目的就是和數(shù)據(jù)庫建立連接,所以其接口也很簡單,如下所示:public interface Driver { //建立連接 Connection connect(String url, java.util.Properties info) throws SQLException; boolean acceptsURL(String url) throws SQLException; DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) throws SQLException; int getMajorVersion(); int getMinorVersion(); boolean jdbcCompliant(); public Logger getParentLogger() throws SQLFeatureNotSupportedException;}
作為 Driver 的管理者 DriverManager,它不僅負責(zé) Driver 的注冊/注銷,還可以直接獲取連接。它是怎么做到的呢?觀察下面代碼發(fā)現(xiàn),實際是通過遍歷所以已經(jīng)注冊的 Driver,找到一個能夠成功建立連接的 Driver,并且將 Connection 返回,DriverManager 就像代理一樣,將真正建立連接的過程還是交給了具體的 Driver。
for(DriverInfo aDriver : registeredDrivers) { // If the caller does not have permission to load the driver then // skip it. if(isDriverAllowed(aDriver.driver, callerCL)) { try { println(” trying ” + aDriver.driver.getClass().getName()); Connection con = aDriver.driver.connect(url, info); if (con != null) { // Success! println(“getConnection returning ” + aDriver.driver.getClass().getName()); return (con); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(” skipping: ” + aDriver.getClass().getName()); } }
2
Connection 設(shè)計
通過上節(jié)我們知道數(shù)據(jù)庫提供商通過實現(xiàn)Driver接口來向用戶提供服務(wù),Driver接口的核心方法就是獲取連接。Connection是和數(shù)據(jù)庫打交道的核心接口,下面我們看看它的設(shè)計方案。
通過觀察設(shè)計圖我們發(fā)現(xiàn)主要有兩類接口:DataSource 和 Connection。下面我們逐一進行介紹。
- DataSource
直接看源碼,如下所示,發(fā)現(xiàn)它的核心方法竟然和 Driver 一樣,也是獲取連接。那為什么還要 DataSource 呢?Driver 本身不就是獲取連接的嗎?下面我們就看看 DataSource 到底是怎么獲取連接的。
public interface DataSource extends CommonDataSource, Wrapper { Connection getConnection() throws SQLException; Connection getConnection(String username, String password) throws SQLException;}
然而我們發(fā)現(xiàn) JDBC 只定義了 DataSource 的接口,并沒有給出具體實現(xiàn),下面我們就以 Spring 實現(xiàn)的 SimpleDriverDataSource 為例,來看看它是怎么做的,代碼如下所示,發(fā)現(xiàn) DataSource 的 getConnection(…)方法,最后竟然還是交由 driver.connect(…)去真正建立連接。所以又回到最開始我們所描述的, Driver 才是真正的與數(shù)據(jù)庫打交道的接口。
protected Connection getConnectionFromDriver(Properties props) throws SQLException { Driver driver = getDriver(); String url = getUrl(); Assert.notNull(driver, “Driver must not be null”); if (logger.isDebugEnabled()) { logger.debug(“Creating new JDBC Driver Connection to [” + url + “]”); } return driver.connect(url, props); }
那么問題來了,為什么還需要 DataSource 這樣的接口,豈不多此一舉么?顯然不會。DataSource 是加強版的 Driver。它將核心的建立連接的過程交由 Driver 執(zhí)行,而對于建立緩存,處理分布式事務(wù)和連接池等看似與建立連接無關(guān)的事情自己來處理。如類的設(shè)計圖所示,以 PTS 使用的 Druid 連接池為例:
- ConnectionPoolDataSource:連接池的實現(xiàn),此數(shù)據(jù)源實現(xiàn)并不直接創(chuàng)建數(shù)據(jù)庫物理連接,而是一個邏輯實現(xiàn),它的作用在于池化數(shù)據(jù)庫物理連接。
- PooledConnection:配合 ConnectionPoolDataSource,由它獲取一個池化對象 PooledConnection,再通過該 PooledConnection 間接獲取到物理連接。
顯然,通過連接池我們可以從連接的管理中抽身,提高連接的利用效率,也能提升壓力機的施壓能力。
3
Statement 設(shè)計
建立連接之后,用戶可能要開始寫 SQL 語句,并且交由數(shù)據(jù)庫去執(zhí)行了。這些是通過 Statement 來實現(xiàn)的。主要分為:
- Statement:定義一個靜態(tài)的 SQL 語句,數(shù)據(jù)庫每次執(zhí)行都需要重新編譯,一般用于僅執(zhí)行一次查詢并返回結(jié)果的情形。
- PreparedStatement:定義一個帶參的預(yù)編譯的 SQL 語句,下次執(zhí)行時,會從緩存中取出遍以后的語句,而不需要重新編譯一遍,適用于執(zhí)行多次相同邏輯的 SQL 語句,當(dāng)然它還有防 SQL 注入等功能,安全性和效率較高,使用比較頻繁。對于性能測試來說,選擇 PreparedStatement 最為合適。
- CallableStatement:用來調(diào)用存儲過程。
4
ResultSet 設(shè)計
JDBC 使用 ResultSet 接口來承接 Statement 的執(zhí)行結(jié)果。ResultSet 使用指針的方式(next())來逐條獲取檢索結(jié)果,當(dāng)指針指向某條數(shù)據(jù)時,用戶可以自由的選擇獲取某一列的數(shù)據(jù)。PTS 通過將 ResultSet 轉(zhuǎn)化成 CSV 文件,輔助用戶以一條 SQL 語句,構(gòu)造復(fù)雜的壓測數(shù)據(jù)。
5
JDBC 架構(gòu)總結(jié)
通過上面的介紹我們發(fā)現(xiàn),JDBC 的設(shè)計還是層次感分明的。
(1)Driver 和 DriverManager 是面向數(shù)據(jù)庫的,設(shè)計了一套 Java 訪問數(shù)據(jù)的規(guī)范,數(shù)據(jù)庫廠商只需要實現(xiàn)這套規(guī)范即可;
(2)DataSource 和 Connection 是面向應(yīng)用程序開發(fā)者的,它們不關(guān)心 JDBC 具體是如何跟數(shù)據(jù)庫進行交互的,通過統(tǒng)一的 DataSource 接口就可以拿到 Connection,用戶的數(shù)據(jù)操作都可以通過這個 Connection 來實現(xiàn)了;
(3)Statement 承載了具體的 SQL 命令,用戶可以定義不同的 Statement 來向數(shù)據(jù)庫發(fā)送指令;
(4)ResultSet 是用來承載 SQL 命令的執(zhí)行結(jié)果。
至此,完成了 加載驅(qū)動 -> 建立連接 -> 執(zhí)行命令 -> 返回結(jié)果 這樣的和數(shù)據(jù)庫交互的整個過程。如果把這個過程靈活的嵌入到 PTS 性能測試中,便可以解決前言提到的各種問題。
JDBC 在性能測試中的應(yīng)用
Cloud Native
1
數(shù)據(jù)庫性能測試
- 背景
大多數(shù)對數(shù)據(jù)庫的操作都是通過 HTTP、FTP 或其他協(xié)議執(zhí)行的,但是在某些情況下,繞開中間協(xié)議直接測試數(shù)據(jù)庫也很有意義。例如我們希望不觸發(fā)所有相關(guān)查詢,而只測試特定 high-value 查詢的性能;驗證新數(shù)據(jù)庫在高負載下的性能。2.驗證某些數(shù)據(jù)庫連接池參數(shù),例如最大連接數(shù) 3.節(jié)省時間和資源。當(dāng)我們想要優(yōu)化 SQL 時,修改代碼中的 SQL 語句和其他數(shù)據(jù)庫操作非常繁瑣,通過 JDBC 壓測,我們可以避免侵入代碼,集中精力在 SQL 調(diào)優(yōu)上。
- 步驟
1、創(chuàng)建場景。 我們在 PTS 控制臺的【壓測中心】->【 創(chuàng)建場景 】中創(chuàng)建 PTS 壓測場景;
2、場景配置。PTS 支持對 MySQL、PostgreSQL 等四種數(shù)據(jù)庫發(fā)起壓測。用戶填寫 JDBC URL、用戶名、密碼和 SQL 即可發(fā)起壓測。同時,PTS 還支持提取 ResultSet 中的數(shù)據(jù)作為出參,給下游 API 使用;對響應(yīng)進行斷言。
3、壓測中監(jiān)控和壓測報告。PTS 支持綁定阿里云 RDS 云資源監(jiān)控,在壓測過程中觀察 RDS 實時性能指標(biāo)。此外,PTS 還提供清晰完備的壓測報告以及采樣日志,供用戶隨時查看。
2
壓測數(shù)據(jù)構(gòu)造
- 背景
在模擬不同用戶登錄、壓測業(yè)務(wù)參數(shù)傳遞等場景中,需要使用參數(shù)功能來實現(xiàn)壓測的請求中各種動態(tài)操作。如果使用傳統(tǒng)的 CSV 文件參數(shù),會受到文件大小的限制,且手動創(chuàng)建耗費精力。使用 JDBC 來構(gòu)造壓測數(shù)據(jù),可以避免以上問題。
- 步驟
1、 添加數(shù)據(jù)源。 在場景編輯-數(shù)據(jù)源管理中,選擇添加 DB 數(shù)據(jù)源,輸入 URL、用戶名、密碼和 SQL。
2、添加參數(shù)。填寫自定義參數(shù)名和列索引。
3、調(diào)試驗證。點擊調(diào)試場景,即可驗證提取的結(jié)果集是否符合預(yù)期。接著,我們就可以在任意想要使用參數(shù)的地方使用${}引用即可。
3
壓測臟數(shù)據(jù)清理
- 背景
針對寫請求的壓測,會在數(shù)據(jù)庫中生成大量臟數(shù)據(jù)。如何在壓測結(jié)束后自動清理?
- 步驟
PTS 給用戶提供了解決方案。PTS 支持對串聯(lián)鏈路作邏輯上的順序編排,即前置鏈路、普通鏈路和后置鏈路。執(zhí)行順序由先到后。設(shè)置某條串聯(lián)鏈路為后置鏈路,填寫循環(huán)次數(shù)即可。