在項目中使用C3P0作為數據庫連接池,被技術總監懟了
本文轉載自微信公眾號「Java極客技術」,作者鴨血粉絲。轉載本文請聯系Java極客技術公眾號。
一、介紹
數據庫連接是一項非常關鍵的、有限的、昂貴的資源,這一點在多用戶的網頁應用程序中體現得尤為突出。
記得之前做的一個項目,當時的應用程序配置的c3p0數據庫連接池,最大允許的連接數是500,結果上線沒多久,并發量直接上來了,導致大量的數據插入失敗,當晚的心情可想而知~
從那一次事故之后,讓我對應用程序的數據庫連接數有了一次深刻的認識,為了防止再次栽跟頭,特意抽了一個時間來編寫程序測試案例,用于測試各個數據源連接池的穩定性,以防止自己再次踩坑!
話不多說,直接擼起來!
二、程序實例
熟悉 web 系統開發的同學,基本都知道,在 Java 生態中開源的常用數據庫連接池有以下幾種:
- dbcp:DBCP是一個依賴Jakarta commons-pool對象池機制的數據庫連接池,DBCP可以直接的在應用程序中使用,Tomcat的數據源使用的就是DBCP
- c3p0:c3p0是一個開放源代碼的JDBC連接池,它在lib目錄中與Hibernate一起發布,包括了實現jdbc3和jdbc2擴展規范說明的Connection和Statement池的DataSources對象
- druid:阿里出品,淘寶和支付寶專用數據庫連接池,但它不僅僅是一個數據庫連接池,它還包含一個ProxyDriver,一系列內置的JDBC組件庫,一個SQL Parser。支持所有JDBC兼容的數據庫,包括Oracle、MySql、Derby、Postgresql、SQL Server、H2等等。
今天我們就一起來對比一下,這三種數據源連接池的穩定性。
2.1、創建測試表
下面以 mysql 數據庫為例,首先創建一個t_test表,方面后續進行插入數據操作。
- CREATE TABLE t_test (
- id bigint(20) unsigned NOT NULL COMMENT '主鍵ID',
- name varchar(32) NOT NULL COMMENT '名稱',
- PRIMARY KEY (id)
- ) ENGINE=InnoDB COMMENT='測試表';
2.2、 編寫測試用例
以dbcp為例,首先創建一個dbcp-jdbc.properties配置文件。
- username=root
- password=Hello@123456
- driverClassName=com.mysql.jdbc.Driver
- url=jdbc:mysql://192.168.31.200:3306/testdb?useUnicode=true&characterEncoding=UTF-8
- initialSize=5
- maxActive=1000
- maxIdle=5
- removeAbandoned=ture
- removeAbandonedTimeout=20
- logAbandoned=true
- maxWait=100
接著,創建一個連接池工具DbcpJdbcUtil。
- public class DbcpJdbcUtil {
- private static final Logger logger = LoggerFactory.getLogger(DbcpJdbcUtil.class);
- /**jdbc配置文件*/
- private static Properties prop = new Properties();
- private static BasicDataSource dataSource = null;
- // 它是事務專用連接!
- private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
- static {
- classPathSourceRead();
- }
- private static void classPathSourceRead(){
- //讀取指定位置的配置文檔(讀取class目錄文件)
- try {
- logger.info("jdbc路徑:" + SysConstants.getValue());
- prop.load(DbcpJdbcUtil.class.getClassLoader().getResourceAsStream(SysConstants.getValue()));
- logger.info("數據配置信息" + JSON.toJSONString(prop));
- logger.info("初始化默認jdbc配置文件成功!");
- } catch (Exception e) {
- logger.error("初始化默認jdbc文件失敗!",e);
- }
- }
- /**
- * 從連接池獲取數據源
- * @return
- * @throws Exception
- */
- public static BasicDataSource getDataSource() throws Exception {
- try {
- if (dataSource == null) {
- synchronized (DbcpJdbcUtil.class) {
- if (dataSource == null) {
- dataSource = new BasicDataSource();
- dataSource.setUsername(prop.getProperty("username"));
- dataSource.setPassword(prop.getProperty("password"));
- dataSource.setDriverClassName(prop.getProperty("driverClassName"));
- dataSource.setUrl(prop.getProperty("url"));
- dataSource.setInitialSize(Integer.valueOf(prop.getProperty("initialSize")));
- dataSource.setMaxActive(Integer.valueOf(prop.getProperty("maxActive")));
- dataSource.setMaxIdle(Integer.valueOf(prop.getProperty("maxIdle")));
- dataSource.setRemoveAbandoned(Boolean.valueOf(prop.getProperty("removeAbandoned")));
- dataSource.setRemoveAbandonedTimeout(Integer.valueOf(prop.getProperty("removeAbandonedTimeout")));
- dataSource.setLogAbandoned(Boolean.valueOf(prop.getProperty("logAbandoned")));
- dataSource.setMaxWait(Integer.valueOf(prop.getProperty("maxWait")));
- }
- }
- }
- return dataSource;
- } catch (Exception e) {
- logger.error("根據數據庫名稱獲取數據庫資源失敗," , e);
- throw new Exception("根據數據庫名稱獲取數據庫資源失敗");
- }
- }
- /**
- * 使用連接池返回一個連接對象
- *
- * @return
- * @throws SQLException
- */
- public static Connection getConnection() throws Exception {
- try {
- Connection con = tl.get();
- // 當con不等于null,說明已經調用過beginTransaction(),表示開啟了事務!
- if (con != null)
- return con;
- return getDataSource().getConnection();
- } catch (Exception e) {
- logger.error("獲取數據庫連接失敗!", e);
- throw new SQLException("獲取數據庫連接失敗!");
- }
- }
- /**
- * 開啟事務 1. 獲取一個Connection,設置它的setAutoComnmit(false)
- * 2. 還要保證dao中使用的連接是我們剛剛創建的! --------------
- * 3. 創建一個Connection,設置為手動提交
- * 4. 把這個Connection給dao用!
- * 5. 還要讓commitTransaction或rollbackTransaction可以獲取到!
- *
- * @throws SQLException
- */
- public static void beginTransaction() throws Exception {
- try {
- Connection con = tl.get();
- if (con != null) {
- con.close();
- tl.remove();
- //throw new SQLException("已經開啟了事務,就不要重復開啟了!");
- }
- con = getConnection();
- con.setAutoCommit(false);
- tl.set(con);
- } catch (Exception e) {
- logger.error("數據庫事物開啟失敗!", e);
- throw new SQLException("數據庫事物開啟失敗!");
- }
- }
- /**
- * 提交事務 1. 獲取beginTransaction提供的Connection,然后調用commit方法
- *
- * @throws SQLException
- */
- public static void commitTransaction() throws SQLException {
- Connection con = tl.get();
- try {
- if (con == null)
- throw new SQLException("還沒有開啟事務,不能提交!");
- con.commit();
- } catch (Exception e) {
- logger.error("數據庫事物提交失敗!", e);
- throw new SQLException("數據庫事物提交失敗!");
- } finally {
- if (con != null) {
- con.close();
- }
- tl.remove();
- }
- }
- /**
- * 回滾事務 1. 獲取beginTransaction提供的Connection,然后調用rollback方法
- *
- * @throws SQLException
- */
- public static void rollbackTransaction() throws SQLException {
- Connection con = tl.get();
- try {
- if (con == null)
- throw new SQLException("還沒有開啟事務,不能回滾!");
- con.rollback();
- } catch (Exception e) {
- logger.error("數據庫事物回滾失敗!", e);
- throw new SQLException("數據庫事物回滾失敗!");
- } finally {
- if (con != null) {
- con.close();
- }
- tl.remove();
- }
- }
- /**
- * 釋放連接
- * @param connection
- * @throws SQLException
- */
- public static void releaseConnection(Connection connection) throws SQLException {
- try {
- Connection con = tl.get();
- // 判斷它是不是事務專用,如果是,就不關閉! 如果不是事務專用,那么就要關閉!
- // 如果con == null,說明現在沒有事務,那么connection一定不是事務專用的!
- //如果con != null,說明有事務,那么需要判斷參數連接是否與con相等,若不等,說明參數連接不是事務專用連接
- if (con == null || con != connection)
- connection.close();
- } catch (Exception e) {
- logger.error("數據庫連接釋放失敗!", e);
- throw new SQLException("數據庫連接釋放失敗!");
- }
- }
- }
最后,編寫單元測試程序DBCPTest。
- public class DBCPTest {
- private static final int sumCount = 1000000;
- private static final int threadNum = 600;
- private void before(String path) {
- SysConstants.putValue(path);
- new DBCPService().insert("delete from t_test");
- }
- @Test
- public void testMysql() {
- long start = System.currentTimeMillis();
- String path = "config/mysql/dbcp-jdbc.properties";
- before(path);
- for (int i =0; i < 1; i++) {
- String sql = "insert into t_test(id,name) values('" +i+ "','dbcp-mysql-" + i + "')";
- new DBCPService().insert(sql);
- }
- System.out.println("耗時:" + (System.currentTimeMillis() - start));
- }
- @Test
- public void testThreadMysql() throws InterruptedException {
- String path = "config/mysql/dbcp-jdbc.properties";
- before(path);
- BlockingQueue<String> queue = new LinkedBlockingQueue<String>();
- for (int i = 0; i < sumCount; i++) {
- String sql = "insert into t_test(id,name) values('" +i+ "','dbcp-mysql-" + i + "')";
- queue.put(sql);
- }
- long start = System.currentTimeMillis();
- final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
- for (int i = 0; i < threadNum; i++) {
- final int finalI = i + 1;
- new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("thread " + finalI + " start");
- boolean isGo = true;
- while (isGo) {
- String sql = queue.poll();
- if(sql != null) {
- new DBCPService().insert(sql);
- }else {
- isGo =false;
- System.out.println("thread " + finalI + " finish");
- countDownLatch.countDown();
- }
- }
- }
- }).start();
- }
- countDownLatch.await();
- System.out.println("耗時:" + (System.currentTimeMillis() - start));
- }
- }
c3p0、druid的配置也類似,這里就不在重復介紹了!
三、性能測試
程序編寫完成之后,下面我們就一起來結合各種不同的場景來測試一下各個數據連接池的表現。
為了進一步擴大測試范圍,本次測試還將各個主流的數據庫也拉入進去,測試的數據庫分別是:mysql-5.7、oracle-12、postgresql-9.6
3.1、插入10萬條數據
首先,我們來測試一下,各個數據庫插入10萬條數據,采用不同的數據源連接池,看看它們的表現如何?
- 測試dbcp執行結果
- 測試c3p0執行結果
測試druid執行結果
從上面測試結果,我們可以基本得出如下結論:
- 從數據連接池性能角度看:dbcp >= druid > c3p0
- 從數據庫性能角度看:oracle > postgresql > mysql
其中druid對postgresql的支持性能最好,c3p0的表現比較差!
3.2、插入100萬條數據
可能有的同學,還不太認可,下面我們就來測試一下插入100萬條,看看它們的表現如何?
- 測試dbcp執行結果
- 測試c3p0執行結果
- 測試druid執行結果
從上面測試結果,我們可以基本得出如下結論:
- 從數據連接池性能角度看:druid性能比較穩定,dbcp、c3p0都有某種程度的執行失敗
- 從數據庫性能角度看:postgresql > oracle > mysql
還是一樣的結論,druid對postgresql的支持性能最好,c3p0的表現比較差!
四、小結
從上面的測試結果,我們可以很清晰的看到,在數據連接池方面,druid和dbcp旗鼓相當,而并發方面druid的穩定性大于dbcp,c3p0相比druid和dbcp,穩定性和執行速度要弱些。
在數據庫方面,postgresql速度要優于oracle,而oracle對各個數據源的支持和穩定性要有優勢,mysql相比oracle和postgresql,執行速度要弱些。
如果在實際開發中,數據源連接池推薦采用druid,數據庫的選用方面 postgresql > oracle > mysql。