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のリードレプリカを使用する
- レプリカDBはマルチAZ配置で構成し、リードレプリカの使用不可時のフェイルオーバーに対応する
spring-cloud-aws-jdbc
の仕組みを使用し、トランザクション単位でマスタDBとレプリカDBのデータソースを切り替える- レプリカDBにアクセスする場合は、Springの
@Transactional
アノテーションの属性readOnly
をtrue
に設定する
RDSのリードレプリカの詳細は AWS 公式サイト 、 マルチAZ配置によるフェイルオーバーについては AWS ユーザーガイド、 Spring Cloud AWSの詳細は Spring 公式サイト を参照されたい。
Warning
本ガイドでは、障害発生時にはマルチAZ配置によるフェイルオーバーにより可用性の確保を行う。 マルチAZ配置を行わない場合、フェイルオーバーが発生しないため、障害発生したレプリカDBのデータソースを選択する可能性を回避できない実装になっている。 マルチAZ配置を行わない場合には、レプリカ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-identifier
RDSのマスタDBのインスタンス識別子を設定する。設定例ではmyRdsDatabase
というDBインスタンス識別子を指定している。データソースは設定したDBインスタンス識別子名で登録される。設定例の場合myRdsDatabase
で参照できる。password
DBのパスワードを設定する。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="dataSource" 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) --> <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> <!-- (3) --> <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) --> <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> <!-- (3) --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="${rds.dbInstanceIdentifier}" /> <property name="rollbackOnCommitFailure" value="true" /> </bean>
項番 説明 (1)旧DataSource設定は不要のため削除する。(2)jdbc:initialize-database
のdata-source
属性にDBインスタンス識別子を設定する。(3)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インスタンスのエンドポイントを指定する方法も併用できる。