5.10. データベースリードレプリカ¶
目次
5.10.1. Overview¶
本ガイドラインでは、AWSのAmazon RDS(以後、RDS)とspring-cloud-aws-jdbcを使用してリードレプリカを行う場合について説明する。
リードレプリカの概要は、リードレプリカ方式、 AWS READ REPLICAの詳細は AWS 公式サイト を参照されたい。
項番 説明 (1)Controllerが @Transactionalアノテーション付のServiceメソッドを呼び出す。 (2)TransactionInterceptorは、DataSourceTransactionManagerのメソッドを呼び出してトランザクションの開始を依頼する。 (3)DataSourceTransactionManagerはLazyConnectionDataSourceProxyからConnectionを取得する。 このときReadOnlyRoutingDataSourceはトランザクションが読み取り専用の場合レプリカDBのDataSourceを返し、読み取り専用でない場合マスタDBのDataSourceを返却する。 (4)(3)で取得した Connectionでトランザクションを開始しConnection HolderへConnectionを格納する。 (5)ServiceはMyBatis Springを経由してDBへクエリを発行する。 (6)MyBatis Springは、(4)で格納した ConnectionをConnection Holderから取得する。 (7)MyBatis Springは、更新系の場合はマスタDBに、参照系の場合はレプリカDBにアクセスする。
5.10.1.1. 実装方針¶
- レプリカDBへのデータのレプリケーションはRDSのリードレプリカを使用する
spring-cloud-aws-jdbcの仕組みを使用し、トランザクション単位でマスタDBとレプリカDBのデータソースを切り替える- レプリカDBにアクセスする場合は、Springの
@Transactionalアノテーションの属性readOnlyをtrueに設定する
RDSのリードレプリカの詳細は AWS 公式サイト 、 Spring Cloud AWSの詳細は Spring 公式サイト を参照されたい。
Warning
本ガイドの実装例は障害発生したレプリカDBのデータソースを選択する可能性を回避できない実装になっている。 そのため、レプリカDBに障害が発生した場合に障害を回避することができない。 本ガイドの実装例を利用する場合、レプリカDBの障害に対して運用面での対処(リードレプリカ復旧手順)を検討する必要がある。
5.10.1.2. リードレプリカ使用時の注意点¶
本ガイドラインで紹介するPostgreSQLのリードレプリカでは、マスタDBからのレプリケーションに遅延が生じるなど注意すべき点が存在する。
詳細は、AWS公式ドキュメント PostgreSQL リードレプリカ を参照されたい。
5.10.2. How to use¶
5.10.2.1. 依存ライブラリの設定¶
Spring Cloud AWSを利用してRDSへのアクセスを行うための依存ライブラリの追加を行う。
xxx-domain/pom.xml
<!-- (1) --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-aws-jdbc</artifactId> </dependency>
項番 説明 (1)spring-cloud-aws-jdbcの依存関係を追加する。
5.10.2.2. データソースの設定¶
Spring Cloud AWS JDBCを利用してRDSへのアクセスを行うためのBean定義を行う。 Bean定義の詳細については、 Spring Cloud AWS Data Access with JDBC を参照されたい。
xxx-domain.xml
<!-- (1) --> <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/cloud/aws/jdbc" xsi:schemaLocation="http://www.springframework.org/schema/cloud/aws/jdbc http://www.springframework.org/schema/cloud/aws/jdbc/spring-cloud-aws-jdbc.xsd"> <!-- (2) --> <jdbc:data-source db-instance-identifier="myRdsDatabase" password="password" read-replica-support="true"> <!-- (3) --> <jdbc:pool-attributes initialSize="1" maxActive="200" minIdle="10" testOnBorrow="true" validationQuery="SELECT 1" /> </jdbc:data-source>
項番 属性名 内容 (1)xmlns:jdbcSpring Cloud AWS JDBCの Namespaceを定義する。値としてhttp://www.springframework.org/schema/cloud/aws/jdbcを指定する。xsi:schemaLocationスキーマのURLを指定する。値にhttp://www.springframework.org/schema/cloud/aws/jdbcとhttp://www.springframework.org/schema/cloud/aws/jdbc/spring-cloud-aws-jdbc.xsdを追加する。(2)db-instance-identifierRDSのマスタDBのインスタンス識別子を設定する。設定例ではmyRdsDatabaseというDBインスタンス識別子を指定している。データソースは設定したDBインスタンス識別子名で登録される。設定例の場合myRdsDatabaseで参照できる。passwordDBのパスワードを設定する。read-replica-supportリードレプリカを使用するかどうかを設定する。trueを指定した場合、読み取り専用トランザクションはレプリカDBにルーティングされ、書き込み操作時にはマスタDBにルーティングされる。(3)jdbc:pool-attributesデータソースのコネクションプールのプロパティを設定することができる。詳細はSpring公式サイトData source pool configurationを参照されたい。Note
jdbc:data-source内の設定値はプロパティファイルに書き出して読み込ませることができない。 環境によって設定値を変更する場合Springのプロファイルの仕組みを使って実現することができる。 詳細はSpring公式サイトXML bean definition profilesを参照されたい。
5.10.2.3. データソース利用箇所の設定¶
データソースのBean名はdb-instance-identifierの設定値で登録されるため、データソースのBeanを参照する際は設定したマスタDBのインスタンス識別子に変更する必要がある。
application-local.yml
# (1) rds: dbInstanceIdentifier: myRdsDatabase
項番 説明 (1)データソースの設定で jdbc:data-source要素のdb-instance-identifier属性に設定したRDSのマスタDBのインスタンス識別子をrds.dbInstanceIdentifierに設定する。xxx-codelist.xml
<bean id="jdbcTemplateForCodeList" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- (1) --> <property name="dataSource" ref="${rds.dbInstanceIdentifier}" /> <property name="fetchSize" value="${codelist.jdbc.fetchSize:1000}" /> </bean>
項番 説明 (1)jdbcTemplateForCodeListのdataSourceのref属性にDBインスタンス識別子を設定する。
xxx-env.xml 変更前
<!-- (1) --> <bean id="realDataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${database.driverClassName}" /> <property name="url" value="${database.url}" /> <property name="username" value="${database.username}" /> <property name="password" value="${database.password}" /> <property name="defaultAutoCommit" value="false" /> <property name="maxTotal" value="${cp.maxActive}" /> <property name="maxIdle" value="${cp.maxIdle}" /> <property name="minIdle" value="${cp.minIdle}" /> <property name="maxWaitMillis" value="${cp.maxWait}" /> </bean> <!-- (2) --> <bean id="dataSource" class="net.sf.log4jdbc.Log4jdbcProxyDataSource"> <constructor-arg index="0" ref="realDataSource" /> </bean> <!-- (3) --> <jdbc:initialize-database data-source="dataSource" ignore-failures="ALL"> <jdbc:script location="classpath:/database/${database}-schema.sql" encoding="UTF-8" /> <jdbc:script location="classpath:/database/${database}-dataload.sql" encoding="UTF-8" /> </jdbc:initialize-database> <!-- (4) --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> <property name="rollbackOnCommitFailure" value="true" /> </bean>
xxx-env.xml 変更後
<!-- (1) --> <!-- 削除 --> <!-- (2) --> <!-- 削除 --> <!-- (3) --> <jdbc:initialize-database data-source="${rds.dbInstanceIdentifier}" ignore-failures="ALL"> <jdbc:script location="classpath:/database/${database}-schema.sql" encoding="UTF-8" /> <jdbc:script location="classpath:/database/${database}-dataload.sql" encoding="UTF-8" /> </jdbc:initialize-database> <!-- (4) --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="${rds.dbInstanceIdentifier}" /> <property name="rollbackOnCommitFailure" value="true" /> </bean>
項番 説明 (1)旧DataSource設定は不要のため削除する。(2)データソースをラップしていると、データソースの情報が正しく認識できずレプリカノードを正しく参照できなくなるため削除する。Warning
spring-cloud-aws-jdbcを用いてリードレプリカ方式を実現する場合はデータソースをラップしないことを推奨する。 例えば、ログ出力の為に
net.sf.log4jdbc.Log4jdbcProxyDataSource等でデータソースをラップしていると、データソースの情報が正しく認識できずレプリカノードを正しく参照できなくなる。(3)jdbc:initialize-databaseのdata-source属性にDBインスタンス識別子を設定する。(4)transactionManagerのdataSourceのref属性にDBインスタンス識別子を設定する。
xxx-infra.xml
<!-- define the SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- (1) --> <property name="dataSource" ref="${rds.dbInstanceIdentifier}" /> <property name="configLocation" value="classpath:/META-INF/mybatis/mybatis-config.xml" /> </bean>
項番 説明 (1)sqlSessionFactoryのdataSourceのref属性にDBインスタンス識別子を設定する。
5.10.2.4. リードレプリカへのアクセスを行うサービスクラスの実装¶
リードレプリカへのアクセスを行うサービスクラスの実装例を以下に示す。
MemberUpdateServiceImpl.Java
public class MemberUpdateServiceImpl implements MemberUpdateService { @Transactional(readOnly = true) //(1) public Member findMember(String customerNo) throws IOException { // omitted } @Transactional // (2) public void updateMember(Member member) throws IOException { // omitted } }
項番 説明 (1)読み取り処理は@Transactional(readOnly = true)を指定することで、リードレプリカインスタンスを参照する。(2)readOnly=trueでない場合、マスタDBにルーティングされ書き込み処理が行われる。
5.10.3. How to extend¶
5.10.3.1. データベースシャーディングと併用する場合の実装¶
本ガイドラインで紹介しているデータベースシャーディングでは各シャードに対してデータソースを定義する独自の実装を行っている。 このため、シャーティングと併用する場合How to useで紹介した方法ではリードレプリカを使用することはできない。
この項ではデータベースシャーディングで紹介しているデータソースファクトリクラスを拡張することによって、 シャーディングとリードレプリカの併用を実現する方法を紹介する。
5.10.3.1.1. リードレプリカに対応したデータソースファクトリの実装¶
マッピングされたデータベースのプロパティクラスを元にデータソースを生成で紹介しているデータソースファクトリクラスであるTomcatDataSourceFactoryを拡張し、
リードレプリカに対応したデータソース( ReadOnlyRoutingDataSource)を作成するAmazonRdsReadReplicaTomcatDataSourceFactoryを実装する。
データソースファクトリクラスのBean定義
以下に、データソースファクトリクラス
AmazonRdsReadReplicaTomcatDataSourceFactoryのBean定義例を示す。<bean id="dataSourceFactory" class="com.example.xxx.domain.common.shard.datasource.pool.AmazonRdsReadReplicaTomcatDataSourceFactory" />
TomcatDataSourceFactoryの修正マッピングされたデータベースのプロパティクラスを元にデータソースを生成で実装したデータソースファクトリクラスを下記のように変更する。
public class TomcatDataSourceFactory implements DataSourceFactory { protected static final String dbInstanceIdentifierKey = "dbInstanceIdentifier"; protected org.apache.tomcat.jdbc.pool.DataSourceFactory factory = new org.apache.tomcat.jdbc.pool.DataSourceFactory(); @Override public DataSource create(Map<String, String> dataSourceProperties, Map<String, String> commonDataSourceProperties) { DataSource ret = null; Properties properties = new Properties(); if (!commonDataSourceProperties.isEmpty()) { properties.putAll(commonDataSourceProperties); } properties.putAll(dataSourceProperties); try { // (1) if (properties.containsKey(dbInstanceIdentifierKey)) { ret = createReadReplicaDataSource(properties); } else { ret = factory.createDataSource(properties); } } catch (Exception e) { throw new SystemException(LogMessages.E_AR_A0_L9008.getCode(), LogMessages.E_AR_A0_L9008 .getMessage(), e); } return ret; } // (2) protected DataSource createReadReplicaDataSource(Properties properties) throws Exception { throw new SystemException(LogMessages.E_AR_A0_L9010.getCode(), LogMessages.E_AR_A0_L9010 .getMessage(dbInstanceIdentifierKey)); } }
項番 説明 (1)dbInstanceIdentifierというキー名がプロパティに設定されている場合、createReadReplicaDataSourceメソッドを実行する。(2)リードレプリカに対応したデータソースを作成するcreateReadReplicaDataSourceメソッドを定義する。 リードレプリカ対応データソースファクトリクラスによってオーバーライドされる想定のため、TomcatDataSourceFactoryを使用したままdbInstanceIdentifierKeyを定義している場合、システム例外が発生するように設定している。
リードレプリカに対応したデータソースファクトリクラスの実装
上記で修正した
TomcatDataSourceFactoryを拡張して実装する。以下に、リードレプリカに対応したデータソースファクトリクラスの実装例を示す。
// (1) public class AmazonRdsReadReplicaTomcatDataSourceFactory extends TomcatDataSourceFactory { @Value("${database.rdsRegion}") private String defaultRegion; private static final String driverUrlOptionKey = "driverUrlOption"; private static final String replicaRegionKey = "replicaRegion"; private static final String driverClassNameKey = "driverClassName"; private StaticDatabasePlatformSupport databasePlatformSupport = new StaticDatabasePlatformSupport(); @Override // (2) protected DataSource createReadReplicaDataSource(Properties properties) throws Exception { // (3) String region = defaultRegion; if (!StringUtils.isEmpty(properties.getProperty(replicaRegionKey))) { region = properties.getProperty(replicaRegionKey); } AmazonRDS amazonRds = AmazonRDSClientBuilder.standard().withRegion(region).build(); // (4) String dbInstanceIdentifier = (String) properties.get(dbInstanceIdentifierKey); DBInstance dbInstance = getDbInstance(amazonRds, dbInstanceIdentifier); // (5) if (dbInstance.getReadReplicaDBInstanceIdentifiers().isEmpty()) { return createDataSourceInstance(dbInstance, properties); } // (6) Map<Object, Object> replicaMap = new HashMap<>( dbInstance.getReadReplicaDBInstanceIdentifiers().size()); for (String replicaName : dbInstance.getReadReplicaDBInstanceIdentifiers()) { replicaMap.put(replicaName, createDataSourceInstance(amazonRds, replicaName, properties)); } // (7) ReadOnlyRoutingDataSource dataSource = new ReadOnlyRoutingDataSource(); dataSource.setTargetDataSources(replicaMap); dataSource.setDefaultTargetDataSource(createDataSourceInstance(dbInstance, properties)); // (8) dataSource.afterPropertiesSet(); // (9) return new LazyConnectionDataSourceProxy(dataSource); } // (10) private DBInstance getDbInstance(AmazonRDS amazonRds, String identifier) throws IllegalStateException { DBInstance instance; try { DescribeDBInstancesResult describeDBInstancesResult = amazonRds .describeDBInstances(new DescribeDBInstancesRequest() .withDBInstanceIdentifier(identifier)); instance = describeDBInstancesResult.getDBInstances().get(0); } catch (DBInstanceNotFoundException e) { throw new SystemException(LogMessages.E_AR_A0_L9009.getCode(), LogMessages.E_AR_A0_L9009 .getMessage(identifier), e); } return instance; } // (11) private DataSource createDataSourceInstance(AmazonRDS amazonRds, String identifier, Properties properties) throws Exception { DBInstance instance = getDbInstance(amazonRds, identifier); return createDataSourceInstance(instance, properties); } // (12) private DataSource createDataSourceInstance(DBInstance instance, Properties properties) throws Exception { properties.setProperty("url", createUrl(instance, properties)); if (!properties.containsKey(driverClassNameKey)) { properties.setProperty(driverClassNameKey, getDriverClassName(instance)); } return factory.createDataSource(properties); } // (13) private String createUrl(DBInstance instance, Properties properties) { StringBuilder sb = new StringBuilder(); String url = databasePlatformSupport.getDatabaseUrlForDatabase( DatabaseType.fromEngine(instance.getEngine()), instance.getEndpoint().getAddress(), instance.getEndpoint().getPort(), instance.getDBName()); sb.append(url); if (properties.containsKey(driverUrlOptionKey)) { sb.append("?").append(properties.getProperty(driverUrlOptionKey)); } return sb.toString(); } //(14) private String getDriverClassName(DBInstance instance) { return databasePlatformSupport.getDriverClassNameForDatabase( DatabaseType.fromEngine(instance.getEngine())); } }
項番 説明 (1)リードレプリカに対応したデータソースファクトリクラスを TomcatDataSourceFactoryクラスを拡張して作成する。(2)リードレプリカに対応したデータソース作成メソッドを実装する。 (3)リージョンを指定して AmazonRDSを作成する。(4)DBインスタンス識別子を使用して DBInstanceを取得する。(5)(4)で取得した DBInstanceのReadReplicaDBInstanceIdentifiersが空の場合、リードレプリカ非対応のデータソースを作成する。(6)ReadReplicaDBInstanceIdentifiersを使用して、レプリカごとにインスタンスを作成する。 作成したインスタンスは、インスタンス識別子をキーにしてMapに格納する。(7)ReadOnlyRoutingDataSourceを使用してリードレプリカに対応したデータソースを作成する。(8)afterPropertiesSet()メソッドを呼び出し初期化を行う。(9)LazyConnectionDataSourceProxyを使用して(7)作成したデータソースをラップして返却する。(10)DBインスタンス識別子から DBInstanceを作成して返却するメソッドを実装する。(11)DBインスタンス識別子を使用して DataSourceを作成するメソッドを実装する。(12)DBInstanceを使用してDataSourceを作成するメソッドを実装する。(13)DBInstanceとプロパティを使用してDBのURLを作成するメソッドを実装する。(14)DBInstanceを使用してドライバークラス名を取得する。
5.10.3.1.2. 設定ファイルの記述¶
データソースについての設定例を以下に示す。設定ファイルに各シャードのデータソース情報を定義で説明済みの内容は省略するので必要に応じて参照されたい。
application-local.yml
database: # (1) rdsRegion: ap-northeast-1 common: data-source: driverClassName: org.postgresql.Driver maxActive: 96 maxIdle: 16 minIdle: 0 maxWait: 90000 password: password username: username default: schema: name: default data-sources: - schema: default # (2) dbInstanceIdentifier: myRdsDatabase # (3) driverUrlOption: socketTimeout=120&connectTimeout=120 # (4) replicaRegion: us-east-1 - schema: example1 dbInstanceIdentifier: anotherRdsDatabase # (5) password: another username: another - schema: example2 # (6) url: jdbc:postgresql://localhost:5432/example2?socketTimeout=120&connectTimeout=120
項番 説明 (1)RDSのリージョンを設定する。 (2)RDSのDBインスタンス識別子を設定する。ここでは myRdsDatabaseを設定している。(3)ドライバーのURLオプションを設定する。 (4)replicaRegionはrdsRegionに設定したリージョンと違う設定値を使用したい場合に設定する。(5)データソース個別設定を行う。ここでは passwordとusernameを個別に設定している。(6)DBインスタンス識別子ではなく、URLにRDSインスタンスのエンドポイントを指定する方法も併用できる。
