Javaにおけるシャーディングパターン:水平パーティショニングをマスターしてアプリケーションのスループットを向上
別名
- データパーティショニング
- 水平パーティショニング
シャーディングデザインパターンの意図
シャーディングは、水平パーティショニングを通じてデータベースのスケーラビリティとパフォーマンスを大幅に向上させる、重要なJavaデザインパターンです。
実際の例を用いたシャーディングパターンの詳細な説明
実際の例
数百万人のユーザーとトランザクションを持つ大規模なeコマースWebサイトを考えてみましょう。膨大な量のデータを処理し、システムが応答性を維持できるようにするために、ユーザーデータは複数のデータベースサーバーにシャーディングされます。たとえば、IDが0〜4で終わるユーザーは1つのサーバーに保存され、5〜9で終わるユーザーは別のサーバーに保存される可能性があります。この分散により、システムは複数のサーバー間で読み取りおよび書き込み操作を並列化することで、より高い負荷を処理できます。
平易な言葉で
Webアプリケーションにおいて、保守性とスケーラビリティを向上させるために、処理ロジックをビューから分離します。
Wikipediaによると
水平パーティショニングは、データベーステーブルの行を、列に分割する(これは正規化と垂直パーティショニングが異なる程度で行うこと)のではなく、個別に保持するデータベース設計原則です。各パーティションはシャードの一部を形成し、そのシャードは別のデータベースサーバーまたは物理的な場所に配置される場合があります。
水平パーティショニングのアプローチには多くの利点があります。テーブルが分割されて複数のサーバーに分散されるため、各データベースの各テーブルの行の合計数が減少します。これによりインデックスサイズが縮小され、一般的に検索パフォーマンスが向上します。データベースシャードは別のハードウェアに配置でき、複数のシャードを複数のマシンに配置できます。これにより、データベースを多数のマシンに分散でき、パフォーマンスが大幅に向上します。さらに、データベースシャードがデータの実際のセグメンテーション(例:ヨーロッパの顧客とアメリカの顧客)に基づいている場合、適切なシャードメンバーシップを簡単かつ自動的に推測し、関連するシャードのみをクエリできる可能性があります。
Javaにおけるシャーディングパターンのプログラム例
シャーディングは、非常に大きなデータベースを、データシャードと呼ばれるより小さく、高速で、管理しやすい部分に分離するデータベースパーティショニングの一種です。シャードという言葉は、全体の一部を意味します。ソフトウェアアーキテクチャでは、データベースまたは検索エンジンの水平パーティションを指します。各個々のパーティションは、シャードまたはデータベースシャードと呼ばれます。
指定されたコードでは、シャードを管理するShardManager
クラスがあります。これには、異なるシャーディング戦略を実装する2つのサブクラスHashShardManager
とRangeShardManager
があります。Shard
クラスは、データを保存するシャードを表します。Data
クラスは、シャードに保存されるデータを表します。
ShardManager
は、シャードを管理するための基本的な構造を提供する抽象クラスです。シャードにデータを保存するstoreData
メソッドと、データを保存するシャードを決定するallocateShard
メソッドがあります。allocateShard
メソッドは抽象であり、サブクラスによって実装される必要があります。
public abstract class ShardManager {
protected Map<Integer, Shard> shardMap = new HashMap<>();
public abstract int storeData(Data data);
protected abstract int allocateShard(Data data);
}
HashShardManager
は、ハッシュベースのシャーディング戦略を実装するShardManager
のサブクラスです。allocateShard
メソッドでは、データキーのハッシュを計算し、それを使用してデータを保存するシャードを決定します。
public class HashShardManager extends ShardManager {
@Override
protected int allocateShard(Data data) {
var shardCount = shardMap.size();
var hash = data.getKey() % shardCount;
return hash == 0 ? hash + shardCount : hash;
}
}
RangeShardManager
は、範囲ベースのシャーディング戦略を実装するShardManager
の別のサブクラスです。allocateShard
メソッドでは、データの型を使用してデータを保存するシャードを決定します。
public class RangeShardManager extends ShardManager {
@Override
protected int allocateShard(Data data) {
var type = data.getType();
return switch (type) {
case TYPE_1 -> 1;
case TYPE_2 -> 2;
case TYPE_3 -> 3;
};
}
}
Shard
クラスは、シャードを表します。シャードにデータを保存するstoreData
メソッドと、IDでシャードからデータを取得するgetDataById
メソッドがあります。
public class Shard {
@Getter
private final int id;
private final Map<Integer, Data> dataStore;
public Shard(final int id) {
this.id = id;
this.dataStore = new HashMap<>();
}
public void storeData(Data data) {
dataStore.put(data.getKey(), data);
}
public Data getDataById(final int id) {
return dataStore.get(id);
}
}
Data
クラスは、シャードに保存されるデータを表します。キー、値、および型があります。
@Getter
@Setter
public class Data {
private int key;
private String value;
private DataType type;
public Data(final int key, final String value, final DataType type) {
this.key = key;
this.value = value;
this.type = type;
}
enum DataType {
TYPE_1, TYPE_2, TYPE_3
}
}
これは、ルックアップ、範囲、およびハッシュの3つの異なるシャーディング戦略を示す例のmain
関数です。各戦略は、データを保存するシャードを異なる方法で決定します。ルックアップ戦略はルックアップテーブルを使用し、範囲戦略はデータ型を使用し、ハッシュ戦略はデータキーのハッシュを使用します。
public static void main(String[] args) {
var data1 = new Data(1, "data1", Data.DataType.TYPE_1);
var data2 = new Data(2, "data2", Data.DataType.TYPE_2);
var data3 = new Data(3, "data3", Data.DataType.TYPE_3);
var data4 = new Data(4, "data4", Data.DataType.TYPE_1);
var shard1 = new Shard(1);
var shard2 = new Shard(2);
var shard3 = new Shard(3);
var manager = new LookupShardManager();
manager.addNewShard(shard1);
manager.addNewShard(shard2);
manager.addNewShard(shard3);
manager.storeData(data1);
manager.storeData(data2);
manager.storeData(data3);
manager.storeData(data4);
shard1.clearData();
shard2.clearData();
shard3.clearData();
var rangeShardManager = new RangeShardManager();
rangeShardManager.addNewShard(shard1);
rangeShardManager.addNewShard(shard2);
rangeShardManager.addNewShard(shard3);
rangeShardManager.storeData(data1);
rangeShardManager.storeData(data2);
rangeShardManager.storeData(data3);
rangeShardManager.storeData(data4);
shard1.clearData();
shard2.clearData();
shard3.clearData();
var hashShardManager = new HashShardManager();
hashShardManager.addNewShard(shard1);
hashShardManager.addNewShard(shard2);
hashShardManager.addNewShard(shard3);
hashShardManager.storeData(data1);
hashShardManager.storeData(data2);
hashShardManager.storeData(data3);
hashShardManager.storeData(data4);
shard1.clearData();
shard2.clearData();
shard3.clearData();
}
最後に、プログラムの出力は次のとおりです。
18:32:26.503 [main] INFO com.iluwatar.sharding.LookupShardManager -- Data {key=1, value='data1', type=TYPE_1} is stored in Shard 2
18:32:26.505 [main] INFO com.iluwatar.sharding.LookupShardManager -- Data {key=2, value='data2', type=TYPE_2} is stored in Shard 2
18:32:26.505 [main] INFO com.iluwatar.sharding.LookupShardManager -- Data {key=3, value='data3', type=TYPE_3} is stored in Shard 1
18:32:26.505 [main] INFO com.iluwatar.sharding.LookupShardManager -- Data {key=4, value='data4', type=TYPE_1} is stored in Shard 1
18:32:26.506 [main] INFO com.iluwatar.sharding.RangeShardManager -- Data {key=1, value='data1', type=TYPE_1} is stored in Shard 1
18:32:26.506 [main] INFO com.iluwatar.sharding.RangeShardManager -- Data {key=2, value='data2', type=TYPE_2} is stored in Shard 2
18:32:26.506 [main] INFO com.iluwatar.sharding.RangeShardManager -- Data {key=3, value='data3', type=TYPE_3} is stored in Shard 3
18:32:26.506 [main] INFO com.iluwatar.sharding.RangeShardManager -- Data {key=4, value='data4', type=TYPE_1} is stored in Shard 1
18:32:26.506 [main] INFO com.iluwatar.sharding.HashShardManager -- Data {key=1, value='data1', type=TYPE_1} is stored in Shard 1
18:32:26.506 [main] INFO com.iluwatar.sharding.HashShardManager -- Data {key=2, value='data2', type=TYPE_2} is stored in Shard 2
18:32:26.506 [main] INFO com.iluwatar.sharding.HashShardManager -- Data {key=3, value='data3', type=TYPE_3} is stored in Shard 3
18:32:26.506 [main] INFO com.iluwatar.sharding.HashShardManager -- Data {key=4, value='data4', type=TYPE_1} is stored in Shard 1
Javaでシャーディングパターンを使用する場合
- 単一のデータベースの容量を超える大量のデータセットを処理する場合に使用します。
- 堅牢なスケーラビリティを必要とするJavaアプリケーションに最適で、シャーディングはデータベースの負荷を効果的に分散することでパフォーマンスを向上させます。
- 高可用性と耐障害性を必要とするアプリケーションに役立ちます。
- 読み取りおよび書き込み操作をシャード間で並列化できる環境で効果的です。
Javaにおけるシャーディングパターンの実際の応用例
- Apache Cassandra、MongoDB、Amazon DynamoDBなどの分散データベース。
- ソーシャルネットワーク、eコマースプラットフォーム、SaaS製品などの大規模なWebアプリケーション。
シャーディングパターンの利点とトレードオフ
利点
- 負荷を分散することでパフォーマンスを向上させます。
- 水平スケーリングを可能にすることでスケーラビリティを向上させます。
- 個々のシャードへの障害を隔離することで可用性と耐障害性を向上させます。
トレードオフ
- 複数のシャードの管理と保守の複雑さ。
- データの増加に伴うシャードのリバランスにおける潜在的な課題。
- クロスシャードクエリの遅延の増加。
関連するJavaデザインパターン
- キャッシング:シャーディングと組み合わせて使用すると、パフォーマンスをさらに向上させることができます。
- データマッパー:シャーディングされた環境で複雑になる可能性のあるデータベースインタラクションの詳細を抽象化およびカプセル化するのに役立ちます。
- リポジトリ:複数のシャードを扱う場合に便利な、データアクセスロジックを一元的に管理する方法を提供します。
- サービスロケーター:分散システム内の異なるシャードを検索して操作するために使用できます。