4.1. データベースシャーディング

4.1.1. Overview

データ永続層のスケール性の確保 で説明したシャーディング方式を、SpringのRoutingDataSourceやAOPの仕組みを用いて実現する方法を説明する。

本ガイドラインでは、以下に示すイメージの赤枠破線部分(Spring提供機能以外)について説明する。枠線外のShardKey RepositoryとStorage Device(記憶装置)については、データ永続層のスケール性の確保シャードキーの管理方針に基づき、Key-Value Store(KVS)を用いるのが望ましいため、クラウドプラットフォームで提供されるKVSを利用することを推奨する。クラウドプラットフォームにAWSを選択する場合のKVSについては、データベースシャーディング を参照されたい。

なお、イメージの赤枠実線部分(Sharding AOP、Routing Data Source、ShardKey Repository)は横断的な機能のため、アプリケーション開発者全員が作成する必要はない。

../../_images/data-access-mybatis3.png
項番 説明
(1)
Controllerが@ShardWithAccountアノテーション@Transactionalアノテーション付のServiceメソッドを呼び出す。
(2)

Sharding AOPがShardKey Repositoryを呼び出しシャードを特定する。

使用するシャードの解決をするためのShardKey Repositoryの実装は、使用するStorage Deviceによって変わる。

(3)
Sharding AOPは、(2)で特定したシャードをRouting Data Sourceへ伝播する。
(4)
Transaction AOPは、Transaction Managerを呼び出す。
(5)
Transaction Managerは、Routing Data Sourceから(3)で伝播されたシャードのConnectionを取得する。
(6)
Transaction Managerは、(5)で取得したConnectionでトランザクションを開始しConnection HolderへConnectionを格納する。
(7)
Serviceは、Shard RepositoryのDBアクセスメソッドを呼び出す。
(8)
Shard Repositoryは、Mybatis Springを経由してDBへクエリを発行する。
(9)
Mybatis Springは、(6)で格納したConnectionをConnection Holderから取得しDBへアクセスする。

4.1.1.1. 実現方針

  • RDB製品依存のシャーディングの仕組み(例:pg_shard等)は使用しない。
  • 性能劣化やアプリケーションの複雑性の回避のため、分散トランザクションは使用しない。
  • SpringRoutingDataSourceの仕組みを拡張し、シャード毎にデータソースを切り替える。
  • テスト容易性のため、シャード切り替えの制御処理をプログラマティックに記述させない(AOPで宣言的に記述)

4.1.2. How to use

シャーディングを行うにあたり、各シャードに対しデータソースを定義する必要がある。 本ガイドラインで紹介する方式では、Springの仕組みを用いつつ以下の実装を独自に行う必要がある。


4.1.2.1. 各シャードのデータソースの生成

シャーディングをする場合は、非シャード とシャード毎にデータソース情報の定義が必要になる。 さらに、シャードの増減や冗長なデータソース情報の定義を避けるため、以下のことを考慮する必要がある。

  • シャード数を増減させる際に、最小限の設定の変更だけで実現できる
  • 冗長な設定を削減しつつ、シャード個別のチューニングも可能にする

上記を考慮した、各シャードのデータソースを生成する手順を以下に示す。


4.1.2.1.1. 設定ファイルに各シャードのデータソース情報を定義

データソース情報の定義には、1つの共通情報と複数の個別情報があり、それぞれを定義する。

以下に、xxx-env/src/main/resources/application-local.ymlでの設定を示す。

database:
  # (1)
  common:
    data-source:
      # (5)
      driverClassName: org.postgresql.Driver
      maxActive: 96
      maxIdle: 16
      minIdle: 0
      maxWait: 90000
      password: postgres
      username: postgres
  # (2)
  default:
    schema:
      name: default
  # (3)
  data-sources:
    # (4)
    - schema: default
      # (5)
      url: jdbc:postgresql://localhost:5432/xxx
    - schema: xxx1
      url: jdbc:postgresql://localhost:5432/xxx1
    - schema: xxx2
      url: jdbc:postgresql://localhost:5432/xxx2
      # (6)
      maxActive: 30
項番 説明
(1)

データソースの共通情報を設定する(任意設定)。ここで設定した値は、シャードの個別情報の設定値で上書きされる。

全シャード共通のデータソース情報の基本となる設定値。 データソースのプロパティキーを設定する。この例では、データソースに Tomcat 9.0 JDBC Connection Pool を使用した場合の設定例を示している。 詳細は、公式サイト を参照されたい。

(2)
非シャード (デフォルトスキーマ)を指定するキーを設定する。(必須)
(3)

全てのシャードのデータソース個別情報を設定する。

省略されたプロパティについては、(1)の設定値が反映される。

(4)

schema(データソースキー)を設定する。

シャードのキーとなる値。 非シャード は1つ、シャードは1つ以上の設定が必須である。 非シャード の値は(2)で設定した値と同一になる。

(5)

データソースの設定値。

データソースのプロパティキーを設定する。この例では、データソースに Tomcat 9.0 JDBC Connection Pool を使用した場合の設定例を示している。 詳細は、公式サイト を参照されたい。

(6)

maxActiveを30に個別設定する。

共通情報を個別情報で上書きし、設定したシャード(schema=xxx2)だけmaxActiveが30となる。


4.1.2.1.2. 定義されたデータソース情報をオブジェクトにマッピング

データソース情報の定義には、1つの共通情報と複数の個別情報の2種類が定義されるため、それぞれをデータベースのプロパティクラスにマッピングするため、これらのプロパティクラスを実装する。また、それぞれのクラスのBean定義をする必要もある。

  • 共通情報プロパティクラスのBean定義

    以下に、共通情報プロパティクラスCommonDatabasePropertiesのBean定義例を示す。

    <bean id="commonDatabaseProperties"
      class="com.example.xxx.domain.common.shard.datasource.model.CommonDatabaseProperties" />
    

  • 個別情報プロパティクラスのBean定義

    以下に、個別情報プロパティクラスDatabasePropertiesのBean定義例を示す。

    <bean id="databaseProperties"
      class="com.example.xxx.domain.common.shard.datasource.model.DatabaseProperties" />
    

  • 共通情報プロパティクラスの実装

    以下に、共通情報プロパティクラスCommonDatabasePropertiesの実装例を示す。

    // omitted...
    // (1)
    @ConfigurationProperties(prefix = "database.common")
    public class CommonDatabaseProperties {
        // (2)
        private Map<String, String> dataSource = new HashMap<>();
    
        // getter & setter
    }
    
    項番 説明
    (1)

    ConfigurationPropertiesアノテーションをクラスへ付与する。

    アノテーションのprefix属性に、設定ファイルに各シャードのデータソース情報を定義のプレフィックスdatabase.commonを指定する。

    (2)

    データソース情報をマッピングするMapクラスを設定する。

    データソース情報をマッピングする項目は、設定ファイルに各シャードのデータソース情報を定義のプロパティキーdatabase.common.data-sourceの後のキー名と同じになる。定義されるプロパティキーの増減に対応するためMapクラスを使用している。


  • 個別情報プロパティクラスの実装

    以下に、個別情報プロパティクラスDatabasePropertiesの実装例を示す。

    // omitted...
    // (1)
    @ConfigurationProperties(prefix = "database")
    public class DatabaseProperties {
        // (2)
        private List<Map<String, String>> dataSources = new ArrayList<>();
    
        // getter & setter
    }
    
    項番 説明
    (1)

    ConfigurationPropertiesアノテーションをクラスへ付与する。

    アノテーションのprefix属性に、設定ファイルに各シャードのデータソース情報を定義のプレフィックスdatabaseを指定する。

    (2)

    データソース情報をマッピングするリストを設定する。

    データソース情報をマッピングするMapクラスのリスト。 データソース情報をマッピングする項目は、設定ファイルに各シャードのデータソース情報を定義のプロパティキーdatabase.data-sourcesの後のキー名と同じになる。定義されるプロパティキーの増減に対応するためMapクラスのリストを使用している。


4.1.2.1.3. マッピングされたデータベースのプロパティクラスを元にデータソースを生成

定義されたデータソース情報をオブジェクトにマッピングしたプロパティクラスを元にデータソースを生成するため、データソースビルダクラスとデータソースファクトリクラスを実装する。また、それぞれのクラスのBean定義をする必要もある。

Note

データソースファクトリクラスは、使用するデータソースに合わせて実装できるよう、DataSourceFactoryのインタフェースを用意し汎化しておく。

  • データソースファクトリクラスのBean定義

    以下に、データソースファクトリクラスTomcatDataSourceFactoryのBean定義例を示す。

    <bean id="dataSourceFactory"
      class="com.example.xxx.domain.common.shard.datasource.pool.TomcatDataSourceFactory" />
    

  • データソースビルダクラスのBean定義

    以下に、データソースビルダクラスRoutingDataSourceBuilderのBean定義例を示す。

    <bean id="routingDataSourceBuilder"
      class="com.example.xxx.domain.common.shard.datasource.RoutingDataSourceBuilder">
      <!-- (1) -->
      <constructor-arg index="0" ref="databaseProperties" />
      <constructor-arg index="1" ref="commonDatabaseProperties" />
      <constructor-arg index="2" ref="dataSourceFactory" />
    </bean>
    
    項番 説明
    (1)
    コンストラクタの引数でデータソースの個別情報プロパティクラス、共通情報プロパティクラスとデータソースファクトリクラスを設定する。

  • データソースファクトリクラスの実装

    データソースファクトリクラスはDataSourceFactoryのインタフェースを使用し、使用するデータソースに合わせて実装する。

    以下に、org.apache.tomcat.jdbc.pool.DataSourceを使用したデータソースファクトリクラスの実装例を示す。

    // (1)
    public class TomcatDataSourceFactory implements DataSourceFactory {
        // (2)
        private org.apache.tomcat.jdbc.pool.DataSourceFactory factory = new org.apache.tomcat.jdbc.pool.DataSourceFactory();
    
        @Override
        // (3)
        public DataSource create(Map<String, String> dataSourceProperties,
            Map<String, String> commonDataSourceProperties) {
            DataSource ret = null;
            Properties properties = new Properties();
            if (!commonDataSourceProperties.isEmpty()) {
                // (4)
                properties.putAll(commonDataSourceProperties);
            }
            // (5)
            properties.putAll(dataSourceProperties);
            try {
                // (6)
                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;
        }
    }
    
    項番 説明
    (1)
    データソースファクトリクラスは、DataSourceFactoryインタフェースの実装クラスとして作成する。
    (2)
    データソースを作成するorg.apache.tomcat.jdbc.pool.DataSourceFactoryのインスタンスを定義する。
    (3)
    データソース作成メソッドを実装する。
    (4)
    データソースの共通情報が定義されていたら共通情報を設定する。
    (5)
    データソースの個別情報で共通情報を上書きマージする。
    (6)
    データソースを作成する。

  • データソースビルダクラスの実装

    以下に、データソースビルダクラスRoutingDataSourceBuilderの実装例を示す。

    // omitted...
    // (1)
    public class RoutingDataSourceBuilder implements InitializingBean {
      // (2)
      @Value("${database.default.schema.name:default}")
      private String databaseDefaultSchemaName;
      // (3)
      private DatabaseProperties databaseProperties;
      // (4)
      private CommonDatabaseProperties commonDatabaseProperties;
      // (5)
      private DataSourceFactory dataSourceFactory;
      // (6)
      @Inject
      ApplicationContext applicationContext;
      // (7)
      @Inject
      DefaultListableBeanFactory factory;
      // (8)
      public RoutingDataSourceBuilder(DatabaseProperties databaseProperties,
              CommonDatabaseProperties commonDatabaseProperties,
              DataSourceFactory dataSourceFactory) {
          // omitted...
          this.databaseProperties = databaseProperties;
          this.commonDatabaseProperties = commonDatabaseProperties;
          this.dataSourceFactory = dataSourceFactory;
      }
      // (9)
      @Override
      public void afterPropertiesSet() throws Exception {
          List<Map<String, String>> dataSources = databaseProperties
                  .getDataSources();
          Map<Object, Object> targetDataSources = new HashMap<>();
          boolean defaultTargetDataSourceFlg = false;
          for (Map<String, String> dataSourceProperties : dataSources) {
              String sourceKey = dataSourceProperties
                      .get(ShardKeyResolver.SCHEMA_KEY_NAME);
              try {
                  javax.sql.DataSource source = dataSourceFactory.create(
                          dataSourceProperties, commonDatabaseProperties
                                  .getDataSource());
                  factory.registerSingleton(sourceKey, source);
              } catch (IllegalStateException e) {
                  throw new SystemException(LogMessages.E_AR_A0_L9007.getCode(), LogMessages.E_AR_A0_L9007
                          .getMessage(sourceKey), e);
              } catch (Exception e) {
                  throw new SystemException(LogMessages.E_AR_A0_L9008.getCode(), LogMessages.E_AR_A0_L9008
                          .getMessage(), e);
              }
    
              if (databaseDefaultSchemaName.equals(sourceKey)) {
                  // (10)
                  this.defaultTargetDataSource = applicationContext
                          .getBean(sourceKey);
                  defaultTargetDataSourceFlg = true;
              } else {
                  // (11)
                  targetDataSources.put(sourceKey, applicationContext
                          .getBean(sourceKey));
              }
          }
          if (!defaultTargetDataSourceFlg) {
              throw new SystemException(LogMessages.E_AR_A0_L9006.getCode(), LogMessages.E_AR_A0_L9006
                      .getMessage());
          }
          if (targetDataSources.isEmpty()) {
              throw new SystemException(LogMessages.E_AR_A0_L9005.getCode(), LogMessages.E_AR_A0_L9005
                      .getMessage());
          }
          this.targetDataSources = targetDataSources;
      }
      // (12)
      public Map<Object, Object> getTargetDataSources() {
          return targetDataSources;
      }
      // (13)
      public Object getDefaultTargetDataSource() {
          return defaultTargetDataSource;
      }
    }
    
    項番 説明
    (1)
    データソースビルダクラスは、InitializingBeanの実装クラスとして作成する。
    (2)
    設定ファイルに各シャードのデータソース情報を定義で指定した、 非シャード (デフォルトスキーマ)キーをインジェクトする。
    (3)
    コンストラクタで設定される、データソースの個別情報プロパティクラスを保持するフィールドを定義する。
    (4)
    コンストラクタで設定される、データソースの共通情報プロパティクラスを保持するフィールドを定義する。
    (5)
    コンストラクタで設定される、データソースファクトリクラスを保持するフィールドを定義する。
    (6)

    ApplicationContextのインジェクトする。

    一度登録したデータソースを取得するために使用する。

    (7)

    DefaultListableBeanFactoryのインジェクトする。

    データソースを実行時に動的にインスタンス化し、SpringのDIコンテナに登録してBeanとして扱えるようにするために使用する。

    (8)
    コンストラクタの引数で(2)、(3)と(4)を取得する。
    (9)
    InitializingBeanのメソッドafterPropertiesSet()をオーバーライドし、データソースを作成する。
    (10)
    非シャード のデータソースを保持する。
    (11)
    シャードのデータソースを保持する。
    (12)
    シャードのデータソースを取得するメソッドを定義する。
    (13)
    非シャード のデータソースを取得するメソッドを定義する。

4.1.2.2. シャーディング対応データソース(ルーティングデータソース)

各シャードのデータソースの生成で生成されたデータソースを、シャーディング対応データソースに格納するため、ルーティングデータソースクラスデータソースキーホルダクラスを実装する。データソースキーホルダクラスは、後述のシャードの割り当て決定で決定したデータソースキーを保持・伝播する入れ物のことである。また、それぞれのクラスのBean定義をする必要もある。

4.1.2.2.1. データソースキーホルダクラス

以下に、データソースキーホルダクラスDataSourceLookupKeyHolderのBean定義例を示す。

<bean id="dataSourceLookupKeyHolder"
  class="com.example.xxx.domain.common.shard.datasource.RoutingDataSourceLookupKeyHolder" />

以下に、データソースキーホルダクラスRoutingDataSourceLookupKeyHolderの実装例を示す。

// omitted...
public class RoutingDataSourceLookupKeyHolder {
    // (1)
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    // (2)
    public void set(String dataSourceKey) {
        contextHolder.set(dataSourceKey);
    }
    // (3)
    public String get() {
        return (String) contextHolder.get();
    }
    // (4)
    public void clear() {
        contextHolder.remove();
    }
}
項番 説明
(1)
スレッド毎にデータソースキーを保持する変数を定義する。
(2)
データソースキーを設定するメソッドを定義する。
(3)
データソースキーを取得するメソッドを定義する。
(4)
保持したデータソースキーを削除するメソッドを定義する。

4.1.2.2.2. ルーティングデータソースクラス

RoutingDataSourceは、Springが提供する、複数のデータソースを定義し動的に切り替えを行う仕組みである。簡単な使用方法は こちら を参照すること。

以下に、ルーティングデータソースクラスRoutingDataSourceのBean定義例を示す。

<bean id="routingDataSource"
  class="com.example.xxx.domain.common.shard.datasource.RoutingDataSource">
  <!-- (1) -->
  <constructor-arg index="0" ref="routingDataSourceBuilder" />
  <constructor-arg index="1" ref="dataSourceLookupKeyHolder" />
</bean>
項番 説明
(1)
コンストラクタの引数でデータソースビルダクラスとデータソースキーホルダクラスを設定する。

以下に、ルーティングデータソースクラスRoutingDataSourceの実装例を示す。

// omitted...
// (1)
public class RoutingDataSource extends AbstractRoutingDataSource {
    // omitted...
    // (2)
    @Value("${database.default.schema.name:default}")
    private String databaseDefaultSchemaName;
    // (3)
    private RoutingDataSourceLookupKeyHolder dataSourceLookupKeyHolder;
    // (4)
    public RoutingDataSource(
            RoutingDataSourceBuilder routingDataSourceBuilder,
            RoutingDataSourceLookupKeyHolder dataSourceLookupKeyHolder) {
        super.setDefaultTargetDataSource(routingDataSourceBuilder.getDefaultTargetDataSource());
        super.setTargetDataSources(routingDataSourceBuilder.getTargetDataSources());
        this.dataSourceLookupKeyHolder = dataSourceLookupKeyHolder;
    }
    // (5)
    @Override
    protected Object determineCurrentLookupKey() {
        // (6)
        return dataSourceLookupKeyHolder.get();
    }
}
項番 説明
(1)
実装クラスは、AbstractRoutingDataSourceのサブクラスとして実装する。
(2)
設定ファイルに各シャードのデータソース情報を定義で指定した、デフォルトキーをインジェクトする。
(3)
コンストラクタで設定される、データソースキーホルダクラスを保持するフィールドを定義する。
(4)

コンストラクタでデータソースビルダクラスとデータソースキーホルダクラスを取得する。

データソースビルダクラスから 非シャード のデータソースとシャードのデータソースのリストを取得して親クラスのコンストラクタへ渡す。

(5)
キー選択のメソッドをオーバーライドする。
(6)

データソースキーホルダクラスからデータソースキーを取得する。

データソースキーホルダクラスから取得した値がnullの場合は、 非シャード のデータソースが選択される。


4.1.2.3. シャードキーの取得

使用するシャードの解決をするため、後述の@ShardWithAccountアノテーション@ShardAccountParamアノテーションの情報を元にメソッド引数からシャードキーの値を取得するシャードアカウントヘルパークラスと、シャーディング対応データソース(ルーティングデータソース)で説明したデータソースキーホルダクラスにデータソースキーを設定するシャーディングインターセプタクラスを実装する。また、それぞれのクラスのBean定義をする必要もある。

また、トランザクション境界となるサービスクラスの対象メソッドにシャード対象であることを示すアノテーションを付与する必要がある。 以下で、付与する@ShardWithAccountアノテーション@ShardAccountParamアノテーションについても説明する。

4.1.2.3.1. @ShardWithAccountアノテーション

@ShardWithAccountアノテーションは、トランザクションを開始するサービスメソッドに付与し属性valueにシャードキーを保持するオブジェクトのパスを設定する。

以下に、@ShardWithAccountアノテーションの実装例を示す。

// (1)
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ShardWithAccount {
    // (2)
    String value() default "";
}
項番 説明
(1)
付与する対象をメソッドに設定する。
(2)

属性value

シャードキーを保持するオブジェクトのパスを設定する。


以下に、@ShardWithAccountアノテーションの使用例を示す。

// omitted...
public class TicketReserveServiceImpl implements TicketReserveService {
    // omitted...
    // (1)
    @Transactional
    // (2)
    @ShardWithAccount("reservation.repMember.customerNo")
    public String registerMemberReservation(Reservation reservation) {
        // omitted...
    }
    @Transactional
    // (3)
    public TicketReserveDto registerReservation(String reserveNo, Reservation reservation) {
        // omitted...
    }
    // omitted...
}
項番 説明
(1)
メソッドにトランザクション境界を示す@Transactionalアノテーションを付与する。
(2)

メソッドにシャード対象であることを示す@ShardWithAccountアノテーションを付与し、属性valueにシャードキーを保持するオブジェクトのパスreservation.repMember.customerNoを設定する。

シャードキーを保持するオブジェクトは、引数reservationのプロパティであるrepMemberが保持するjava.lang.String型のプロパティcustomerNoとなる。

(3)
メソッドにシャード対象であることを示す@ShardWithAccountアノテーションが付与されていないため、非シャード にアクセスする。

4.1.2.3.2. @ShardAccountParamアノテーション

@ShardAccountParamアノテーションは、@ShardWithAccountアノテーションが付与されたメソッドの引数に付与するマーカーアノテーションである。メソッド引数が複数ある場合に、シャードキーを保持するオブジェクトを特定するために使用する。

以下に、@ShardAccountParamアノテーションの実装例を示す。

// (1)
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ShardAccountParam {

}
項番 説明
(1)
付与する対象を引数に設定する。

以下に、@ShardAccountParamアノテーションの使用例を示す。

// omitted...
public class TicketReserveServiceImpl implements TicketReserveService {
    // omitted...
    // (1)
    @Transactional
    // (2)
    @ShardWithAccount("reservation.repMember.customerNo")
    // (3)
    public String registerMemberReservation(String xxxxxxx, @ShardAccountParam Reservation reservation) {
        // omitted...
    }
    // omitted...
}
項番 説明
(1)
メソッドにトランザクション境界を示す@Transactionalアノテーションを付与する。
(2)
メソッドにシャード対象であることを示す@ShardWithAccountアノテーションを付与する。
(3)
引数Reservationがシャードキーを保持するオブジェクトであるため、第2引数Reservation@ShardAccountParamアノテーションアノテーションを付与する。

4.1.2.3.3. シャードアカウントヘルパークラス

以下に、シャードアカウントヘルパークラス ShardAccountHelperのBean定義例を示す。

<bean id="shardAccountHelper"
  class="com.example.xxx.domain.common.shard.helper.ShardAccountHelper" />

以下に、シャードアカウントヘルパークラスShardAccountHelperの実装例を示す。

public class ShardAccountHelper {
    // omitted...
    // (1)
    public String getAccountValue(MethodInvocation invocation) throws Exception {
        String ret = null;
        // (2)
        Object target = invocation.getThis();
        if (target == null) {
            return null;
        }
        // (3)
        Class<?> targetClass = AopUtils.getTargetClass(target);
        // (4)
        Object[] arguments = invocation.getArguments();
        Class<?>[] classes = null;
        if (null != arguments && arguments.length > 0) {
            classes = invocation.getMethod().getParameterTypes();
        } else {
            return null;
        }
        // (5)
        Method method = ReflectionUtils.findMethod(targetClass, invocation
                .getMethod().getName(), classes);
        // (6)
        ShardWithAccount shardWithAccount = AnnotationUtils.findAnnotation(
                method, ShardWithAccount.class);
        if (null != shardWithAccount) {
            // (7)
            String value = shardWithAccount.value();
            if ("".equals(value)) {
                return null;
            }
            String[] values = value.split("[.]");
            Object obj = null;
            int argumentsLength = 0;
            if (arguments.length == 1) {
                obj = arguments[0];
            } else {
                ShardAccountParam shardAccountParam = null;
                Parameter[] parameters = method.getParameters();
                for (Parameter parameter : parameters) {
                    // // (8)
                    shardAccountParam = AnnotationUtils.findAnnotation(
                            parameter, ShardAccountParam.class);
                    if (null != shardAccountParam) {
                        // (9)
                        obj = arguments[argumentsLength];
                        break;
                    }
                    argumentsLength++;
                }
                if (null == shardAccountParam && values.length > 1) {
                    // omitted...
                }
            }
            if (null == obj) {
                // omitted...
            }
            // (10)
            if (values.length == 1) {
                ret = obj.toString();
            } else {
                String exp = value.substring(value.indexOf(".") + 1);
                ExpressionParser expressionParser = new SpelExpressionParser();
                Expression expression = expressionParser.parseExpression(exp);
                ret = expression.getValue(obj, String.class);
            }
        }
        return ret;
    }
}
項番 説明
(1)
シャードキーを取得するメソッドを定義する。
(2)
実行対象のオブジェクトを取得する。
(3)
実行対象のクラスを取得する。
(4)
実行対象メソッドの引数を取得する。
(5)
実行対象のメソッドを取得する。
(6)
実行対象のメソッドに付与された@ShardWithAccountアノテーションを取得する。
(7)
@ShardWithAccountアノテーションの属性valueの値を取得する。
(8)
メソッド引数が複数の場合に@ShardAccountParamアノテーションを取得する。
(9)
@ShardAccountParamアノテーションが付与されている引数のオブジェクトを取得する。
(10)
対象オブジェクトからシャードキーの値を取得する。

4.1.2.3.4. シャーディングインターセプタクラス

以下に、シャーディングインターセプタクラス AccountShardInterceptorのBean定義例を示す。

<bean id="accountShardInterceptor"
  class="com.example.xxx.domain.common.shard.interceptor.AccountShardInterceptor">
  <!-- (1) -->
  <constructor-arg index="0" ref="accountShardKeyRepository" />
  <constructor-arg index="1" ref="shardAccountHelper" />
  <constructor-arg index="2" ref="dataSourceLookupKeyHolder" />
</bean>

<aop:config>
  <!-- omitted... -->
  <!-- (2) -->
  <aop:advisor order="-1" advice-ref="accountShardInterceptor"
    pointcut="@annotation(com.example.xxx.domain.common.shard.annotation.ShardWithAccount)" />
</aop:config>
項番 説明
(1)
コンストラクタの引数でシャードキーリポジトリクラス、シャードアカウントヘルパークラスデータソースキーホルダクラスを設定する。
(2)

AOPの設定をする。

ここでは、@ShardWithAccountアノテーションが付与されたメソッド呼び出し時にシャーディングインターセプタクラスが動作する設定にしている。また、トランザクション開始前にシャードキーを取得するため、order="-1"を設定しトランザクションインターセプタより先に動作する設定とする。


以下に、シャーディングインターセプタクラスAccountShardInterceptorの実装例を示す。

// omitted...
// (1)
public class AccountShardInterceptor implements MethodInterceptor, InitializingBean {
    // (2)
    private AccountShardKeyRepository accountShardKeyRepository;
    // (3)
    private ShardAccountHelper shardAccountHelper;
    // (4)
    private RoutingDataSourceLookupKeyHolder dataSourceLookupKeyHolder;
    // (5)
    public AccountShardInterceptor(
            AccountShardKeyRepository accountShardKeyRepository,
            ShardAccountHelper shardAccountHelper,
            RoutingDataSourceLookupKeyHolder dataSourceLookupKeyHolder) {
        this.accountShardKeyRepository = accountShardKeyRepository;
        this.shardAccountHelper = shardAccountHelper;
        this.dataSourceLookupKeyHolder = dataSourceLookupKeyHolder;
    }
    // omitted...
    // (6)
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // (7)
        String beforeKey = dataSourceLookupKeyHolder.get();

        String dataSourceKey = null;
        // (8)
        String account = shardAccountHelper.getAccountValue(invocation);
        if (null != account) {
            // (9)
            Optional<ShardingAccount> shardingAccount = accountShardKeyRepository
                    .findById(acccount);
            if (shardingAccount != null) {
                // (10)
                dataSourceKey = shardingAccount.get().getDataSourceKey();
            }
        }
        // (11)
        dataSourceLookupKeyHolder.set(dataSourceKey);

        Object ret = null;
        try {
            ret = invocation.proceed();
        } finally {
            // (12)
            if (null != beforeKey) {
                dataSourceLookupKeyHolder.set(beforeKey);
            } else {
                dataSourceLookupKeyHolder.clear();
            }
        }
        return ret;
    }
}
項番 説明
(1)
シャーディングインターセプタクラスは、MethodInterceptorInitializingBeanの実装クラスとして作成する。
(2)
コンストラクタで設定される、シャードキーリポジトリクラスを保持するフィールドを定義する。
(3)
コンストラクタで設定される、シャードアカウントヘルパークラスを保持するフィールドを定義する。
(4)
コンストラクタで設定される、データソースキーホルダクラスを保持するフィールドを定義する。
(5)
コンストラクタ引数でシャードキーリポジトリクラス、シャードアカウントヘルパークラスデータソースキーホルダクラスを取得する。
(6)
シャードキーを設定する実行メソッドを定義する。
(7)
シャードのネスト処理に対応するため、一つ前のシャードキーを保持する。
(8)
シャードアカウントヘルパークラスからシャードキーを取得する。
(9)
シャードキーリポジトリクラスがKVSに問い合わせ結果を取得する。
(10)
(9)の結果からデータソースキーを取得する。
(11)
データソースキーホルダクラスに(7)で取得したデータソースキーを設定する。
(12)
データソースキーホルダクラスの状態を戻す。

4.1.2.4. 使用するシャードの解決

データソースキーは、前出で説明した通りクラウドプラットフォームで提供されるKVSを利用することを前提とする。KVSに永続化したシャードキー情報とデータソースキーのマッピングを取得するため、シャードキーリポジトリクラスの実装とBean定義が必要となる。

シャードキーリポジトリクラスの実装は、使用するKVSに合わせて実装する。AWSの場合は、KVSにDynamoDBを使用する。


4.1.2.5. シャードの割り当て決定

使用するシャードの解決により、シャードキーに対応するシャードを解決するためには、事前にシャードキーとシャードのマッピング情報をシャードキーの管理方針に従って管理されている必要がある。シャーディング対象のデータの要素が新たに作成された際に、その要素のシャードキーをインプットとし、何かしらのルールに従って割り当てるシャードを決定し、そのマッピング情報を保存する。ここでは、シャードキーを元にラウンドロビンでシャードを割り当てる例を紹介する。 なお、この割り当ては新たな要素が作成されたタイミングのみ実行すること。一度シャードの割り当てが実行された以降は、使用するシャードの解決により割り当てられたシャードを解決する。

シャードの割り当てを決定するため、シャードキーの値を元にデータソースキーを決定するシャードキーリゾルバクラスを実装する。また、このクラスのBean定義をする必要もある。

Note

シャードキーリゾルバクラスは、シャードの割り当てロジックを変更できるよう、ShardKeyResolverのインタフェースを用意し汎化しておく。また、シャードの割り当てを決定する時は、シャードキーをラウンドロビンのようにアクセス数が均等になるようにシャードをマッピングする。

4.1.2.5.1. シャードキーリゾルバクラス

以下に、シャードキーリゾルバクラス DataSourceKeyResolverのBean定義例を示す。

<bean id="shardKeyResolver"
    class="com.example.xxx.domain.common.shard.datasource.DataSourceKeyResolver">
    <!-- (1) -->
    <constructor-arg index="0" ref="databaseProperties" />
</bean>
項番 説明
(1)
コンストラクタの引数でデータソースの個別情報プロパティクラスを設定する。

以下に、シャードキーリゾルバクラス DataSourceKeyResolverの実装例を示す。

// omitted...
// (1)
public class DataSourceKeyResolver implements ShardKeyResolver, InitializingBean {
    // (2)
    @Value("${database.default.schema.name:default}")
    private String databaseDefaultSchemaName;
    // (3)
    private DatabaseProperties databaseProperties;
    // (4)
    private List<Map<String, String>> dataSources;
    // (5)
    public DataSourceKeyResolver(DatabaseProperties databaseProperties) {
        this.databaseProperties = databaseProperties;
    }
    // (6)
    @Override
    public void afterPropertiesSet() throws Exception {
        this.dataSources = new ArrayList<>();
        for (Map<String, String> dataSource : this.databaseProperties
                .getDataSources()) {
            if (!databaseDefaultSchemaName.equals(dataSource
                    .get(ShardKeyResolver.SCHEMA_KEY_NAME))) {
                this.dataSources.add(dataSource);
            }
        }
    }
    // (7)
    @Override
    public String resolveShardKey(String shardKey) {
        Integer key = Integer.valueOf(shardKey);
        int dataSourceIndex = key % (dataSources.size());
        Map<String, String> dataSource = dataSources.get(dataSourceIndex);
        return dataSource.get(ShardKeyResolver.SCHEMA_KEY_NAME);
    }
}
項番 説明
(1)
シャードキーリゾルバクラスは、ShardKeyResolverInitializingBeanインタフェースの実装クラスとして作成する。
(2)
設定ファイルに各シャードのデータソース情報を定義で指定した、デフォルトキーをインジェクトする。
(3)
コンストラクタで設定される、データソースの個別情報プロパティクラスを保持するフィールドを定義する。
(4)
シャード用データソースキーのリストを保持するフィールドを定義する。
(5)
コンストラクタの引数でデータソースの個別情報プロパティクラスを取得する。
(6)

InitializingBeanのメソッドafterPropertiesSet()をオーバーライドし、シャード用データソースキーのリストを作成する。

データソースの個別情報プロパティクラスからシャードのデータソースキーのリストを作成する。

(7)

引数のシャードキーを元にシャードの割り当てをするメソッドを定義する。

引数のシャードキー(例では数値)をシャード用データソースキーのリストサイズで除算した余りをインデックスとして、シャード用データソースキーのリストからデータソースキーを取得し返却する。


4.1.2.6. アプリケーションでのシャーディングの利用

アプリケーションでシャーディングを利用する方法について、チケット予約を例に以下の通り説明する。

前提条件として、会員のチケット予約情報のDBはシャード対象としてKVSにマッピング情報が登録されている。また、フライトの空席情報のDBはシャード対象外( 非シャード )とする。

4.1.2.6.1. シャードの割り当て

シャードを割り当てるには、シャードキーを決定してシャードとマッピングする必要がある。

以下で、新規会員登録を例に、会員情報登録サービスクラスMemberRegisterServiceImplの実装を元に説明する。

@Service
public class MemberRegisterServiceImpl implements MemberRegisterService {
  // omitted...
  // (1)
  @Inject
  private ShardKeyResolver shardKeyResolver;

  @Override
  @Transactional
  public Member register(Member member) {
      // omitted...
      // (2)
      int insertMemberCount = memberRepository.insert(member);
      // omitted...
      // (3)
      ShardingAccount shardingAccount = new ShardingAccount();
      // (4)
      shardingAccount.setId(member.getCustomerNo());
      // (5)
      shardingAccount.setDataSourceKey(shardKeyResolver.resolveShardKey(member.getCustomerNo()));
      // (6)
      accountShardKeyRepository.save(shardingAccount);
      // omitted...
      return member;
  }
}
項番 説明
(1)
シャードキーリゾルバクラスをインジェクトする。
(2)
会員情報を登録し、お客様番号を取得する。
(3)
シャードのマッピング情報のオブジェクトをインスタンス化する。
(4)
マッピング情報のシャードキーに(2)で取得したお客様番号を設定する。
(5)
シャードキーリゾルバクラスへ(2)で取得したお客様番号を渡してデータソースキーを取得しシャードに設定する。
(6)
シャードのマッピング情報をKVSに登録する。

Note

シャード対象となるデータを新規で登録する場合は、シャードを特定するシャードキーを作成してデータベースへデータを登録し、KVSへシャードのマッピング情報を登録する処理が必要になる。これらの処理のトランザクション境界が別々の場合に、最後の登録処理で例外が発生すると、最初に登録したデータを削除する処理が必要となる事に注意する。

処理順序や例外発生のタイミングによってKVSだけにデータが登録されることが予想されるため、シャードキーとシャードのマッピング情報を保持するKVSは、不要データの削除を定期的に行うことを推奨する。

上記の例では、DBのトランザクション境界内で最初にデータベースへ登録し、最後にKVSへ登録を行っている。これは、KVSの登録で例外発生した場合に、最初に登録したデータベースの削除処理を回避するためである。


4.1.2.6.2. シャードの解決

シャードを解決するには、トランザクション境界となるサービスクラスの対象メソッドにシャード対象であることを示す@ShardWithAccountアノテーションを付与する。

なお、@ShardWithAccountアノテーションが付与されていない場合は、非シャード にアクセスする。

以下で、チケット予約を例に、チケット予約サービスクラスTicketReserveServiceImplの実装を元に説明する。

@Service
public class TicketReserveServiceImpl implements TicketReserveService {
    // (1)
    @Inject
    TicketSharedService ticketSharedService;
    // (2)
    @Inject
    FlightRepository flightRepository;
    // (3)
    @Inject
    ReservationRepository reservationRepository;
    // omitted...
    @Transactional
    // (4)
    @ShardWithAccount("reservation.repMember.customerNo")
    public String registerMemberReservation(Reservation reservation) {
    }
    @Transactional
    // (5)
    public TicketReserveDto registerReservation(String reserveNo, Reservation reservation) {
    }
    // omitted...
}
項番 説明
(1)
チケット共通サービスをインジェクトする。
(2)
フライト情報リポジトリをインジェクトする。
(3)
チケット予約情報リポジトリをインジェクトする。
(4)

メソッドregisterMemberReservationはシャード対象のため、@ShardWithAccountアノテーションを付与する。

会員のチケット予約情報を登録するメソッドのため、シャード対象となる。

(5)

メソッドregisterReservationはシャード対象外のため、@ShardWithAccountアノテーションを付与しない。

フライトの空席数を更新するメソッドのため、シャード対象外となる。