JPAでマスター/スレーブ構成のMySQLを使うぞ

この記事はJava EE Advent Calendarの18日目のエントリです。昨日はn_agetsuさんのApache Shiro を使ってみましたでした。

Webサービスやソーシャルゲームのボトルネックになりやすいのがデータベースアクセスです。そしてこれらのサービスではデータベースにMySQLが多く使われています。

高負荷なMySQLの負荷分散の一つにデータベースをマスター/スレーブのレプリケーション構成にしてINSERT/UPDATE/DELETEなど更新系のクエリはマスターに対して行い、スレーブにマスターの更新内容をレプリケート、SELECTなど参照系のクエリはスレーブのデータベースにクエリを発行して負荷分散を行う手法があります。

このエントリではそのようなマスター/スレーブのレプリケーション構成のMySQLにJPAを使ってクエリを発行する方法をご紹介します。

MySQLのJDBCドライバ

クエリを分散して発行するためのオープンソースJDBCプロキシのHA-JDBCというプロダクトがありますが、こういったプロダクトを使うまでもなく実はMySQLのJDBCドライバはマスター/スレーブのレプリケーション構成のデータベースに標準で対応しています。以下のようなURLを指定することで簡単に負荷分散が行えます。

jdbc:mysql:replication://master,slave1,slave2,slave3/[database-name]

通常のjdbc:mysqlスキームではなく、jdbc:mysql:replicationスキームを使います。ドライバはcom.mysql.jdbc.ReplicationDriverです。更新系クエリを発行するマスターをホスト名の先頭に記述し、続けてスレーブのホスト名をカンマ区切りで記述します。

通常通りjava.sql.Connectionインスタンスを取得しクエリを発行するとマスターのホストに対してクエリが発行され、ConnectionインスタンスのsetReadOnlyメソッドにtrueを指定してからクエリを発行するとスレーブのホストに対してクエリが発行される仕組みになっています。

EntityManagerからConnectionの取得

さて、Connectionを直接扱う場合上記の方法で良いですが、javax.persistence.EntityManagerで隠蔽されているJPAではどのようにすれば良いでしょうか。

EntityManagerにはunwrapというメソッドが定義されていて、引数に指定したクラスのインスタンスを取得することができます。 とは言え残念ながら実装によって取得できるインスタンスはまちまちです。

EclipseLinkの場合

EclipseLinkはこのように直接Connectionインスタンスを取得することができます。

entityManager.unwrap(java.sql.Connection.class).setReadOnly(true);

Hibernateの場合

HibernateはSessionImplementorインスタンスを取得してからconnectionメソッドでConnectionインスタンスを取得する必要があります。

entityManager.unwrap(org.hibernate.engine.spi.SessionImplementor.class).connection().setReadOnly(true);

クエリを発行してみる

実際にクエリを発行してみて、MySQLに出力されるログを見比べてみましょう。Userエンティティを永続化したあとにEntityManagerからConnectionインスタンスを取得してsetReadOnlyメソッドを呼び出したあとクエリを発行しています。

public class UserIT {
	@Test
	public void persistSuccess() throws Exception {
		Map<String, String> properties = ImmutableMap.of("javax.persistence.jdbc.url", System.getProperty("javax.persistence.jdbc.url"));

		@Cleanup
		EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("sample-mysql-replication", properties);
		@Cleanup
		EntityManager entityManager = entityManagerFactory.createEntityManager();

		entityManager.getTransaction().begin();
		User user = new User();
		user.setName("name");
		user.setPassword("password");
		entityManager.persist(user);
		entityManager.getTransaction().commit();

		// from slave
		entityManager.getTransaction().begin();
		// for EclipseLink
		entityManager.unwrap(Connection.class).setReadOnly(true);
		// for Hibernate
		// entityManager.unwrap(SessionImplementor.class).connection().setReadOnly(true);
		assertThat(entityManager.createQuery("from User u where u.name = 'name'", User.class).getSingleResult(), is(user));
		entityManager.getTransaction().commit();
	}
}

これを実行したログを見てみます。まずこちらがマスターのログです。

Query	CREATE TABLE USER (ID BIGINT AUTO_INCREMENT NOT NULL, NAME VARCHAR(255) NOT NULL UNIQUE, PASSWORD VARCHAR(255) NOT NULL, PRIMARY KEY (ID))
Query	SET autocommit=0
Query	INSERT INTO USER (NAME, PASSWORD) VALUES ('name', 'password')
Query	SELECT LAST_INSERT_ID()
Query	commit
Query	SET autocommit=1
Query	SET autocommit=0
Query	rollback
Quit

そしてスレーブのログです。

Query	CREATE TABLE USER (ID BIGINT AUTO_INCREMENT NOT NULL, NAME VARCHAR(255) NOT NULL UNIQUE, PASSWORD VARCHAR(255) NOT NULL, PRIMARY KEY (ID))
Query	BEGIN
Query	INSERT INTO USER (NAME, PASSWORD) VALUES ('name', 'password')
Query	COMMIT /* implicit, from Xid_log_event */
Query	SET autocommit=0
Query	SELECT ID, NAME, PASSWORD FROM USER WHERE (NAME = 'name')
Query	commit
Query	SET autocommit=1
Quit

ご覧のとおりCREATE TABLEやINSERTはマスターとスレーブの両方に発行されたログが出力されていますが、SELECTはスレーブ側だけに出力されていることから、正しくクエリが分散していることがわかりました。

まとめと次回予告

今回のサンプルはこちらにまとめてあります。ぜひご自身の環境でも実行してみてください。

サンプルのpom.xmlをよく見ると結合テストについて色々書いてあることに気づくでしょう。次回はJava Advent Calendar23日目のエントリとしてマスター/スレーブ構成のMySQLをMavenでローカルマシンに構築して結合テストを行う方法をご紹介する予定です。

明日は我らがyoshioteradaのエントリですね!Java EE 8のはなし!

「JPAでマスター/スレーブ構成のMySQLを使うぞ」への2件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>