ScalaでのJDBCによるDB操作の勉強をした - $shibayu36->blog; の続き。今回はHikariCP を利用して、DBのコネクションプールをScalaで利用してみたのでメモ。DBにはPostgreSQLを利用した。
依存の追加
build.sbtに以下を追加。
libraryDependencies += "org.postgresql" % "postgresql" % "42.1.4" libraryDependencies += "com.zaxxer" % "HikariCP" % "2.7.1"
単純にDB接続してみる
https://github.com/brettwooldridge/HikariCP#initialization や https://jyn.jp/java-hikaricp-mysql-sqlite/ あたりを参考にした。
とりあえず接続してINSERTしたりSELECTしたりする例はこんな感じ。
import com.zaxxer.hikari.{ HikariConfig, HikariDataSource } import scala.util.Random /** $ createdb hikaricp-example $ psql hikaricp-example CREATE TABLE person ( id SERIAL NOT NULL PRIMARY KEY, name VARCHAR(254) NOT NULL ); */ object HikariCPExample { def main(args: scala.Array[String]) = { val config = new HikariConfig() config.setDriverClassName("org.postgresql.Driver") config.setJdbcUrl("jdbc:postgresql:hikaricp-example") config.setUsername("dbuser") config.setPassword("dbuser") val ds = new HikariDataSource(config) val conn = ds.getConnection() val newName = Random.alphanumeric.take(10).mkString println(newName) val st1 = conn.prepareStatement("INSERT INTO person (name) VALUES(?)") st1.setString(1, newName) val rowsInserted = st1.executeUpdate() println(rowsInserted + " inserted. name = " + newName) st1.close() val limit = 3 val st2 = conn.prepareStatement("SELECT id, name FROM person ORDER BY id DESC LIMIT ?") st2.setInt(1, limit) val rs2 = st2.executeQuery() while (rs2.next()) { println("id: " + rs2.getInt(1)) println("name: " + rs2.getString("name")) } rs2.close() st2.close() conn.close() } }
- HikariConfigで接続などの設定をする
- HikariDataSourceのインスタンスを作る
- あとはHikariDataSourceからコネクションを獲得し、利用して、最後にcloseして終了
という感じで利用できる。
並列にコネクションを取得してみる
並列処理でHikariCPを使ってみて、コネクションプールが使われる様子を見てみる。例えば、最大でコネクションプールは3つだけ作るという設定で並列処理してみる。
import com.zaxxer.hikari.{ HikariConfig, HikariDataSource } object ParallelHikariCPExample { def main(args: scala.Array[String]) = { val config = new HikariConfig() config.setDriverClassName("org.postgresql.Driver") config.setJdbcUrl("jdbc:postgresql:hikaricp-example") config.setUsername("dbuser") config.setPassword("dbuser") config.setMaximumPoolSize(3) val ds = new HikariDataSource(config) (1 to 10).par.foreach { index => val conn = ds.getConnection() println(s"Connection $index get connection") Thread.sleep(index * 1000) conn.close() println(s"Connection $index close connection") } } }
これを実行してみる。Scalaのスレッド数の上限で処理がブロックしないように適当に設定しておく。
$ sbt -Dscala.concurrent.context.numThreads=16 -Dscala.concurrent.context.maxThreads=16 > runMain ParallelHikariCPExample [info] Running ParallelHikariCPExample Connection 8 get connection Connection 1 get connection Connection 7 get connection Connection 1 close connection Connection 6 get connection Connection 7 close connection Connection 5 get connection Connection 6 close connection Connection 9 get connection Connection 8 close connection Connection 2 get connection Connection 2 close connection Connection 3 get connection Connection 5 close connection Connection 4 get connection Connection 3 close connection Connection 10 get connection Connection 4 close connection Connection 9 close connection Connection 10 close connection
少しわかりづらいが、
- 最初に三つコネクションを取得でき、そこで上限なので他はgetConnectionでブロックする
- その後、一つのコネクションがcloseで開放されるたびに、どれかのスレッドがコネクションを獲得できるようになる
という動きをした。コネクションプールを設定した数だけ作り、接続できたことが分かる。
HikariCPのベンチマークを取ってみる
実際にJDBCを直接使っての接続と、HikariCPでコネクションプールを使っての接続で、どの程度速くなるのかベンチマークを取ってみた。ベンチマーク取得にはnanobench を利用する。使い方は以前 nanobenchを使ってJavaのベンチマークを取る - $shibayu36->blog; の記事で書いた。
以下のようにベンチマークコードを書く。
import com.zaxxer.hikari.{ HikariConfig, HikariDataSource } import java.sql._ import me.geso.nanobench.Benchmark; object HikariCPBenchmark { def main(args: scala.Array[String]): Unit = { new Benchmark(HikariCPBenchmarkInner).warmup(1).runByTime(3).timethese().cmpthese() } } object HikariCPBenchmarkInner { @Benchmark.Bench def jdbc: Unit = { Class.forName("org.postgresql.Driver") for (i <- 1 to 100) { val conn = DriverManager.getConnection( "jdbc:postgresql:hikaricp-example", "dbuser", "dbuser" ) conn.close() } } @Benchmark.Bench def hikaricp: Unit = { val config = new HikariConfig() config.setDriverClassName("org.postgresql.Driver") config.setJdbcUrl("jdbc:postgresql:hikaricp-example") config.setUsername("dbuser") config.setPassword("dbuser") val ds = new HikariDataSource(config) for (i <- 1 to 100) { val conn = ds.getConnection() conn.close() } ds.close() } }
実行してみる。
> runMain HikariCPBenchmark [info] Compiling 1 Scala source to /Users/shibayu36/development/src/github.com/shibayu36/scala-playground/target/scala-2.11/classes... [info] Running HikariCPBenchmark Warm up: 1 Score: jdbc: 17 wallclock secs ( 1.16 usr + 2.43 sys = 3.59 CPU) @ 9.75/s (n=35) hikaricp: 10 wallclock secs ( 1.22 usr + 1.89 sys = 3.10 CPU) @ 511.82/s (n=1589) Comparison chart: Rate jdbc hikaricp jdbc 9.75/s -- -98% hikaricp 512/s 5150% -- [success] Total time: 43 s, completed Sep 18, 2017 8:16:40 AM
これを見ると、HikariCPを使うと接続が50倍ほど速くなることが分かった。コネクションプール便利。
まとめ
今回はScalaでHikariCPを使ってコネクションプールを利用するのを試してみた。並列でコネクションプールを使うとどのようになるか、実際にどの程度速くなるかなどを試せて勉強になった。
参考
- ScalaでのJDBCによるDB操作の勉強をした - $shibayu36->blog;
- https://github.com/brettwooldridge/HikariCP
- [Java]HikariCPでMySQL/SQLiteに接続してみる | 純規の暇人趣味ブログ
- Scalaで簡単に並列処理 | ハックノート
- 簡単に並列処理をする方法について勉強になった
- Scalaのデフォルト実行コンテキストの並列数を変える方法 - Qiita
- スレッド数を変える方法
- https://github.com/tokuhirom/nanobench
- nanobenchを使ってJavaのベンチマークを取る - $shibayu36->blog;