JAVA鏈接數據庫 #03# HikariCP

爲何用數據庫鏈接池?

爲何要用數據庫鏈接池?mysql

若是咱們分析一下典型的【鏈接數據庫】所涉及的步驟,咱們將理解爲何:git

  1. 使用數據庫驅動程序打開與數據庫的鏈接
  2. 打開TCP套接字以讀取/寫入數據
  3. 經過套接字讀取/寫入數據
  4. 關閉鏈接
  5. 關閉套接字

很明顯,【鏈接數據庫】是至關昂貴的操做,所以,應該想辦法儘量地減小、避免這種操做。github

這就是數據庫鏈接池發揮做用的地方。經過簡單地實現數據庫鏈接容器(容許咱們重用大量現有鏈接),咱們能夠有效地節省執行大量昂貴【鏈接數據庫】的成本,從而提升數據庫驅動應用程序的總體性能。sql

↑ 譯自 A Simple Guide to Connection Pooling in Java ,有刪改數據庫

HikariCP快速入門

HikariCP是一個輕量級的高性能JDBC鏈接池。GitHub連接:https://github.com/brettwooldridge/HikariCP併發

一、依賴

  1. HikariCP
  2. slf4j (不須要日誌實現也能跑)
  3. logback-core
  4. logback-classic

1和2以及相應數據庫的JDBC驅動是必要的,日誌實現能夠用其它方案。dom

二、簡單的草稿程序

package org.sample.dao;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.sample.entity.Profile;
import org.sample.exception.DaoException;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class Test {
    private static HikariConfig config = new HikariConfig();
    private static HikariDataSource ds;

    static {
        config.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/profiles?characterEncoding=utf8");
        config.setUsername("root");
        config.setPassword("???????");
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        ds = new HikariDataSource(config);
        config = new HikariConfig();
    }

    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }

    private Test(){}

    public static void main(String[] args) {
        Profile profile = new Profile();
        profile.setUsername("testname3");
        profile.setPassword("123");
        profile.setNickname("testnickname");
        int i = 0;
        try {
            Connection conn = Test.getConnection();
            String sql = "INSERT ignore INTO `profiles`.`profile` (`username`, `password`, `nickname`) " +
                    "VALUES (?, ?, ?)"; // 添加ignore出現重複不會拋出異常而是返回0
            try (PreparedStatement ps = conn.prepareStatement(sql)) {
                ps.setString(1, profile.getUsername());
                ps.setString(2, profile.getPassword());
                ps.setString(3, profile.getNickname());
                i = ps.executeUpdate();
            }
        } catch (SQLException e) {
            throw new DaoException(e);
        }
        System.out.println(i);
    }
}

三、設置鏈接池參數(只列舉經常使用的)

一臺四核的電腦基本能夠所有采用默認設置?socket

autoCommit:控制由鏈接池所返回的connection默認的autoCommit情況。默認值爲是true。
connectionTimeout:該參數決定無可用connection時的最長等待時間,超時將拋出SQLException。容許的最小值爲250,默認值是30000(30秒)。
maximumPoolSize:該參數控制鏈接池所容許的最大鏈接數(包括在用鏈接和空閒鏈接)。基本上,此值將肯定應用程序與數據庫實際鏈接的最大數量。它的合理值最好由你的具體執行環境肯定。當鏈接池達到最大鏈接數,而且沒有空閒鏈接時,調用getConnection()將會被阻塞,最長等待時間取決於connectionTimeout。 對於這個值設定多少比較好,涉及的東西有點多,詳細可參看About Pool Sizing,通常能夠簡單用這個公式計算:鏈接數 = ((核心數 * 2) + 有效磁盤數),默認值是10。
minimumIdle:控制最小的空閒鏈接數,當鏈接池內空閒的鏈接數少於minimumIdle,且總鏈接數不大於maximumPoolSize時,HikariCP會盡力補充新的鏈接。出於性能方面的考慮,不建議設置此值,而是讓HikariCP把鏈接池當作固定大小的處理,minimumIdle的默認值等於maximumPoolSize。
maxLifetime:用來設置一個connection在鏈接池中的最大存活時間。一個使用中的connection永遠不會被移除,只有在它關閉後纔會被移除。用微小的負衰減來避免鏈接池中的connection一次性大量滅絕。咱們強烈建議設置這個值,它應該比數據庫所施加的時間限制短個幾秒。若是設置爲0,則表示connection的存活時間爲無限大,固然還要受制於idleTimeout。默認值是1800000(30分鐘)。(不大理解,然而mysql的時間限制不是8個小時???)
idleTimeout:控制一個connection所被容許的最大空閒時間。當空閒的鏈接數超過minimumIdle時,一旦某個connection的持續空閒時間超過idleTimeout,就會被移除。只有當minimumIdle小於maximumPoolSize時,這個參數才生效。默認值是600000(10分鐘)。
poolName:用戶定義的鏈接池名稱,主要顯示在日誌記錄和JMX管理控制檯中,以標識鏈接池以及它的配置。默認值由HikariCP自動生成。

四、MySQL配置

參閱MySQL Configuration

jdbcUrl=jdbc:mysql://127.0.0.1:3306/profiles?characterEncoding=utf8
username=root
password=test
dataSource.cachePrepStmts=true
dataSource.prepStmtCacheSize=250
dataSource.prepStmtCacheSqlLimit=2048
dataSource.useServerPrepStmts=true
dataSource.useLocalSessionState=true
dataSource.rewriteBatchedStatements=true
dataSource.cacheResultSetMetadata=true
dataSource.cacheServerConfiguration=true
dataSource.elideSetAutoCommits=true
dataSource.maintainTimeStats=false

五、修改Java鏈接數據庫#02#中的代碼

① HikariCPDataSource.java,hikari.properties如上所示。

package org.sample.db;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;
import java.sql.SQLException;

public class HikariCPDataSource {
    private static final String HIKARI_PROPERTIES_FILE_PATH = "/hikari.properties";
    private static HikariConfig config = new HikariConfig(HIKARI_PROPERTIES_FILE_PATH);
    private static HikariDataSource ds = new HikariDataSource(config);

    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }
}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

② ConnectionFactory.java

package org.sample.db;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * 線程池版
 */
public class ConnectionFactory {

    private ConnectionFactory() {
        // Exists to defeat instantiation
    }

    private static final ThreadLocal<Connection> LocalConnectionHolder = new ThreadLocal<>();

    public static Connection getConnection() throws SQLException {
        Connection conn = LocalConnectionHolder.get();
        if (conn == null || conn.isClosed()) {
            conn = HikariCPDataSource.getConnection();
            LocalConnectionHolder.set(conn);
        }
        return conn;
    }

    public static void removeLocalConnection() {
        LocalConnectionHolder.remove();
    }
}

 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

③ ConnectionProxy.java(代碼分層有錯誤!)

package org.sample.manager;

import org.sample.db.ConnectionFactory;
import org.sample.exception.DaoException;

import java.sql.Connection;

/**
 * 對應線程池版本ConnectionFactory,方便在Service層進行事務控制
 */
public class ConnectionProxy {
    public static void setAutoCommit(boolean autoCommit) {
        try {
            Connection conn = ConnectionFactory.getConnection();
            conn.setAutoCommit(autoCommit);
        } catch (Exception e) {
            throw new DaoException(e);
        }
    }

    public static void commit() {
        try {
            Connection conn = ConnectionFactory.getConnection();
            conn.commit();
        } catch (Exception e) {
            throw new DaoException(e);
        }
    }

    public static void rollback() {
        try {
            Connection conn = ConnectionFactory.getConnection();
            conn.rollback();
        } catch (Exception e) {
            throw new DaoException(e);
        }
    }

    public static void close() {
        try {
            Connection conn = ConnectionFactory.getConnection();
            conn.close();
            ConnectionFactory.removeLocalConnection();
        } catch (Exception e) {
            throw new DaoException(e);
        }
    }

    // TODO 設置隔離級別
}

其它地方把LocalConnectionFactory改成ConnectionFactory,LocalConnectionProxy改成ConnectionProxy就好了!後續若是要換其它鏈接池,只須要改變ConnectionFactory.java裏的一小點代碼。

六、測試

package org.sample.manager;

import org.junit.Test;
import org.sample.dao.ProfileDAO;
import org.sample.dao.impl.ProfileDAOImpl;
import org.sample.entity.Profile;
import org.sample.exception.DaoException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import static org.junit.Assert.assertTrue;

public class DaoTest {

    private static final Logger LOGGER = Logger.getLogger(DaoTest.class.getName());

    private static final String ORIGIN_STRING = "hello";
    private static String RandomString() {
        return Math.random() + ORIGIN_STRING + Math.random();
    }
    private static Profile RandomProfile() {
        Profile profile = new Profile(RandomString(), ORIGIN_STRING, RandomString());
        return profile;
    }

    private static final ProfileDAO PROFILE_DAO = ProfileDAOImpl.INSTANCE;

    private class Worker implements Runnable {
        private final Profile profile = RandomProfile();

        @Override
        public void run() {
            LOGGER.info(Thread.currentThread().getName() + " has started his work");
            try {
                // ConnectionProxy.setAutoCommit(false);
                PROFILE_DAO.saveProfile(profile);
                // ConnectionProxy.commit();
            } catch (DaoException e) {
                e.printStackTrace();
            } finally {
                try {
                    ConnectionProxy.close();
                } catch (DaoException e) {
                    e.printStackTrace();
                }
            }
            LOGGER.info(Thread.currentThread().getName() + " has finished his work");
        }
    }

    /**
     * numTasks指併發線程數。
     * -- 不用鏈接池:
     * numTasks<=100正常運行,完成100個任務耗時大概是550ms~600ms
     * numTasks>100報錯「too many connections」,偶爾不報錯,這是來自mysql數據庫自己的限制
     * -- 採用鏈接池
     * numTasks>10000仍正常運行,完成10000個任務耗時大概是26s(池大小是10)
     */
    private static final int NUM_TASKS = 2000;

    @Test
    public void test() throws Exception {
        List<Runnable> workers = new LinkedList<>();
        for(int i = 0; i != NUM_TASKS; ++i) {
            workers.add(new Worker());
        }
        assertConcurrent("Dao test ", workers, Integer.MAX_VALUE);
    }

    public static void assertConcurrent(final String message, final List<? extends Runnable> runnables, final int maxTimeoutSeconds) throws InterruptedException {
        final int numThreads = runnables.size();
        final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<Throwable>());
        final ExecutorService threadPool = Executors.newFixedThreadPool(numThreads);
        try {
            final CountDownLatch allExecutorThreadsReady = new CountDownLatch(numThreads);
            final CountDownLatch afterInitBlocker = new CountDownLatch(1);
            final CountDownLatch allDone = new CountDownLatch(numThreads);
            for (final Runnable submittedTestRunnable : runnables) {
                threadPool.submit(new Runnable() {
                    public void run() {
                        allExecutorThreadsReady.countDown();
                        try {
                            afterInitBlocker.await();
                            submittedTestRunnable.run();
                        } catch (final Throwable e) {
                            exceptions.add(e);
                        } finally {
                            allDone.countDown();
                        }
                    }
                });
            }
            // wait until all threads are ready
            assertTrue("Timeout initializing threads! Perform long lasting initializations before passing runnables to assertConcurrent", allExecutorThreadsReady.await(runnables.size() * 10, TimeUnit.MILLISECONDS));
            // start all test runners
            afterInitBlocker.countDown();
            assertTrue(message +" timeout! More than" + maxTimeoutSeconds + "seconds", allDone.await(maxTimeoutSeconds, TimeUnit.SECONDS));
        } finally {
            threadPool.shutdownNow();
        }
        assertTrue(message + "failed with exception(s)" + exceptions, exceptions.isEmpty());
    }
}

原本打算調整鏈接池參數觀察對性能影響的,結果發現即便參數不變,運行時間起伏也有點大。因此暫時先這樣了。。。具體緣由待探究!

相關文章
相關標籤/搜索