読み書き分離パターンの選択
読み書き分離を実装する場合、適切なパターンを選択することが重要です。一般的に使用される読み書き分離パターンには、単純な読み書き分離とロードバランサーを活用した読み書き分離の2種類があります。データソースを動的に切り替えることで、これらのパターンを実装できます。以下では、Spring Bootベースの読み書き分離 구현と、さまざまなパターンのコード例について詳しく説明します。
読み書き分離パターン
- 単純な読み書き分離:読み取り操作ではスレーブデータベースを書き込み操作ではマスターデータベースを使用します。
- ロードバランス型読み書き分離:読み取り操作時、複数のスレーブデータベースから1つを選択して負荷を分散します。
開発環境の準備
Spring BootとMySQLを使用し、1つのマスターデータベースと2つのスレーブデータベースで構成される環境を想定します。SpringのDataSourceRouterクラスを使用してデータソースの動的切り替えを実装し、AOP Aspectを使用して読み取り操作と書き込み操作を区別します。
プロジェクト依存関係
pom.xmlに必要な依存関係を追加します:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
データソース設定
データソース設定クラスでマスターデータベースとスレーブデータベースを設定し、ルーティングデータソースを設定して読み書き分離を行います。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableTransactionManagement
public class DatabaseConfig {
@Bean(name = "masterDatabase")
public DataSource masterDatabase() {
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:3306/db_primary")
.username("admin")
.password("secret")
.driverClassName("com.mysql.cj.jdbc.Driver")
.build();
}
@Bean(name = "replicaDatabaseOne")
public DataSource replicaDatabaseOne() {
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:3306/db_replica_1")
.username("admin")
.password("secret")
.driverClassName("com.mysql.cj.jdbc.Driver")
.build();
}
@Bean(name = "replicaDatabaseTwo")
public DataSource replicaDatabaseTwo() {
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:3306/db_replica_2")
.username("admin")
.password("secret")
.driverClassName("com.mysql.cj.jdbc.Driver")
.build();
}
@Bean
public DataSource routerDataSource(
@Qualifier("masterDatabase") DataSource masterDatabase,
@Qualifier("replicaDatabaseOne") DataSource replicaDatabaseOne,
@Qualifier("replicaDatabaseTwo") DataSource replicaDatabaseTwo) {
AbstractRoutingDataSource router = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContext.getCurrentTarget();
}
};
Map<Object, Object> targetDatabases = new HashMap<>();
targetDatabases.put(DatabaseTarget.PRIMARY, masterDatabase);
targetDatabases.put(DatabaseTarget.REPLICA_1, replicaDatabaseOne);
targetDatabases.put(DatabaseTarget.REPLICA_2, replicaDatabaseTwo);
router.setDefaultTargetDataSource(masterDatabase);
router.setTargetDataSources(targetDatabases);
return router;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
データソースコンテキスト
現在のデータベースターゲット(マスタースレーブ)を保持するコンテキストクラスを定義します:
public class DatabaseContext {
private static final ThreadLocal<DatabaseTarget> currentTarget = new ThreadLocal<>();
public static void setCurrentTarget(DatabaseTarget target) {
currentTarget.set(target);
}
public static DatabaseTarget getCurrentTarget() {
return currentTarget.get();
}
public static void clear() {
currentTarget.remove();
}
}
public enum DatabaseTarget {
PRIMARY,
REPLICA_1,
REPLICA_2
}
AOP Aspect実装
AOP Aspectを使用して読み書き分離を実装します。読み取り操作前はスレーブデータベース、書き込み操作前はマスターデータベースを設定します。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
@Aspect
@Component
public class RoutingAspect {
private AtomicInteger roundRobin = new AtomicInteger(0);
@Before("execution(* com.example.repository.*.search*(..)) || execution(* com.example.repository.*.fetch*(..))")
public void configureReadDatabase() {
int replicaIndex = roundRobin.incrementAndGet() % 2;
if (replicaIndex == 0) {
DatabaseContext.setCurrentTarget(DatabaseTarget.REPLICA_1);
} else {
DatabaseContext.setCurrentTarget(DatabaseTarget.REPLICA_2);
}
}
@Before("execution(* com.example.repository.*.save*(..)) || execution(* com.example.repository.*.modify*(..)) || execution(* com.example.repository.*.remove*(..))")
public void configureWriteDatabase() {
DatabaseContext.setCurrentTarget(DatabaseTarget.PRIMARY);
}
}
データベース操作サービス
具体的なデータベース操作サービスを実装します:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public void createProduct(String productId, String productName, BigDecimal price) {
String sql = "INSERT INTO products (id, name, price) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, productId, productName, price);
}
public List<Product> findByName(String productName) {
String sql = "SELECT * FROM products WHERE name = ?";
return jdbcTemplate.query(sql, new Object[]{productName}, (resultSet, rowNumber) ->
new Product(
resultSet.getString("id"),
resultSet.getString("name"),
resultSet.getBigDecimal("price")
));
}
}
読み書き分離のテスト
ProductRepositoryのメソッドを呼び出してテストします:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.List;
@Component
public class IntegrationTestRunner implements CommandLineRunner {
@Autowired
private ProductRepository productRepository;
@Override
public void run(String... arguments) throws Exception {
productRepository.createProduct("prod001", "Sample Product", new BigDecimal("2500.00"));
List<Product> products = productRepository.findByName("Sample Product");
products.forEach(product -> System.out.println("Found: " + product.getName()));
}
}
実装のポイント
以下の手法により、Spring Bootベースの読み書き分離を実装できます:
- データソース設定:マスターデータベースとスレーブデータベースを設定し、ルーティングデータソースを通じて読み書き分離を行います。
- コンテキスト管理:現在のデータベースターゲットを保持するコンテキストクラスを定義します。
- AOP Aspect:AOP Aspectを使用して、読み取り操作と書き込み操作の前にそれぞれ適切なデータベースターゲットを設定します。ラウンドロビン方式による負荷分散も実装可能です。
- リポジトリ実装:具体的なデータベース操作をJdbcTemplateを使用して実装します。
この実装により、データベースの読み書き分離と負荷分散を効果的に行え、パフォーマンスとスケーラビリティを向上させることができます。プロジェクトの要件に応じて、読み書き分離パターンを適切に選択してください。