爲何要用數據庫鏈接池?mysql
若是咱們分析一下典型的【鏈接數據庫】所涉及的步驟,咱們將理解爲何:git
很明顯,【鏈接數據庫】是至關昂貴的操做,所以,應該想辦法儘量地減小、避免這種操做。github
這就是數據庫鏈接池發揮做用的地方。經過簡單地實現數據庫鏈接容器(容許咱們重用大量現有鏈接),咱們能夠有效地節省執行大量昂貴【鏈接數據庫】的成本,從而提升數據庫驅動應用程序的總體性能。sql
↑ 譯自 A Simple Guide to Connection Pooling in Java ,有刪改數據庫
HikariCP是一個輕量級的高性能JDBC鏈接池。GitHub連接:https://github.com/brettwooldridge/HikariCP併發
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自動生成。
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
① 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()); } }
原本打算調整鏈接池參數觀察對性能影響的,結果發現即便參數不變,運行時間起伏也有點大。因此暫時先這樣了。。。具體緣由待探究!