Introduction
Read replicas load balancing distributes database read queries across multiple replica servers to improve performance, increase availability, and scale read operations. This implementation provides a comprehensive solution for managing read replicas in Java applications.
Architecture Overview
System Architecture
public class ReadReplicaLoadBalancer {
/**
* Read Replica Load Balancing Components:
* 1. Replica Discovery & Health Monitoring
* 2. Load Balancing Strategies (Round Robin, Weighted, Least Connections)
* 3. Connection Pool Management
* 4. Metrics & Monitoring
* 5. Failover & Recovery
*/
private ReplicaManager replicaManager;
private LoadBalancingStrategy strategy;
private HealthMonitor healthMonitor;
private MetricsCollector metricsCollector;
public ReadReplicaLoadBalancer(LoadBalancingStrategy strategy) {
this.replicaManager = new ReplicaManager();
this.strategy = strategy;
this.healthMonitor = new HealthMonitor();
this.metricsCollector = new MetricsCollector();
}
public Connection getReadConnection() throws SQLException {
DataSource replica = strategy.selectReplica(replicaManager.getHealthyReplicas());
return replica.getConnection();
}
public void executeQuery(String sql, ResultSetHandler handler) {
try (Connection conn = getReadConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
ResultSet rs = stmt.executeQuery();
handler.handleResult(rs);
} catch (SQLException e) {
metricsCollector.recordError();
throw new RuntimeException("Query execution failed", e);
}
}
}
Core Components
Replica Configuration Management
public class ReplicaConfig {
private final String id;
private final String url;
private final String username;
private final String password;
private final int weight;
private final boolean enabled;
private final Map<String, String> properties;
public ReplicaConfig(String id, String url, String username, String password,
int weight, boolean enabled) {
this.id = id;
this.url = url;
this.username = username;
this.password = password;
this.weight = weight;
this.enabled = enabled;
this.properties = new HashMap<>();
}
// Builder pattern for easy configuration
public static class Builder {
private String id;
private String url;
private String username;
private String password;
private int weight = 1;
private boolean enabled = true;
public Builder id(String id) { this.id = id; return this; }
public Builder url(String url) { this.url = url; return this; }
public Builder username(String username) { this.username = username; return this; }
public Builder password(String password) { this.password = password; return this; }
public Builder weight(int weight) { this.weight = weight; return this; }
public Builder enabled(boolean enabled) { this.enabled = enabled; return this; }
public ReplicaConfig build() {
return new ReplicaConfig(id, url, username, password, weight, enabled);
}
}
// Getters
public String getId() { return id; }
public String getUrl() { return url; }
public int getWeight() { return weight; }
public boolean isEnabled() { return enabled; }
}
@Component
public class ReplicaManager {
private final Map<String, ReplicaConfig> replicaConfigs;
private final Map<String, DataSource> replicaDataSources;
private final List<ReplicaConfig> healthyReplicas;
private final HealthMonitor healthMonitor;
public ReplicaManager() {
this.replicaConfigs = new ConcurrentHashMap<>();
this.replicaDataSources = new ConcurrentHashMap<>();
this.healthyReplicas = new CopyOnWriteArrayList<>();
this.healthMonitor = new HealthMonitor();
}
public void addReplica(ReplicaConfig config) {
replicaConfigs.put(config.getId(), config);
DataSource dataSource = createDataSource(config);
replicaDataSources.put(config.getId(), dataSource);
// Start health monitoring
healthMonitor.monitorReplica(config, dataSource);
}
public void removeReplica(String replicaId) {
replicaConfigs.remove(replicaId);
replicaDataSources.remove(replicaId);
healthyReplicas.removeIf(config -> config.getId().equals(replicaId));
}
public List<ReplicaConfig> getHealthyReplicas() {
return new ArrayList<>(healthyReplicas);
}
public DataSource getDataSource(String replicaId) {
return replicaDataSources.get(replicaId);
}
public void updateReplicaHealth(String replicaId, boolean healthy) {
ReplicaConfig config = replicaConfigs.get(replicaId);
if (config != null) {
if (healthy && !healthyReplicas.contains(config)) {
healthyReplicas.add(config);
} else if (!healthy) {
healthyReplicas.remove(config);
}
}
}
private DataSource createDataSource(ReplicaConfig config) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(config.getUrl());
dataSource.setUsername(config.getUsername());
dataSource.setPassword(config.getPassword());
dataSource.setMaximumPoolSize(10);
dataSource.setMinimumIdle(2);
dataSource.setConnectionTimeout(30000);
dataSource.setIdleTimeout(600000);
dataSource.setMaxLifetime(1800000);
return dataSource;
}
}
Health Monitoring
@Component
public class HealthMonitor {
private final ScheduledExecutorService scheduler;
private final Map<String, ScheduledFuture<?>> monitoringTasks;
private final ReplicaManager replicaManager;
private final MetricsCollector metricsCollector;
public HealthMonitor() {
this.scheduler = Executors.newScheduledThreadPool(2);
this.monitoringTasks = new ConcurrentHashMap<>();
this.replicaManager = new ReplicaManager(); // In real app, inject this
this.metricsCollector = new MetricsCollector();
}
public void monitorReplica(ReplicaConfig config, DataSource dataSource) {
ScheduledFuture<?> task = scheduler.scheduleAtFixedRate(
() -> checkReplicaHealth(config, dataSource),
0, 30, TimeUnit.SECONDS); // Check every 30 seconds
monitoringTasks.put(config.getId(), task);
}
public void stopMonitoring(String replicaId) {
ScheduledFuture<?> task = monitoringTasks.remove(replicaId);
if (task != null) {
task.cancel(false);
}
}
private void checkReplicaHealth(ReplicaConfig config, DataSource dataSource) {
boolean isHealthy = performHealthCheck(dataSource);
replicaManager.updateReplicaHealth(config.getId(), isHealthy);
if (isHealthy) {
metricsCollector.recordHealthCheckSuccess(config.getId());
} else {
metricsCollector.recordHealthCheckFailure(config.getId());
}
}
private boolean performHealthCheck(DataSource dataSource) {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
// Simple health check query
ResultSet rs = stmt.executeQuery("SELECT 1");
return rs.next() && rs.getInt(1) == 1;
} catch (SQLException e) {
return false;
}
}
@PreDestroy
public void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
Load Balancing Strategies
Strategy Interface
public interface LoadBalancingStrategy {
DataSource selectReplica(List<ReplicaConfig> healthyReplicas);
String getStrategyName();
}
// Round Robin Strategy
@Component
public class RoundRobinStrategy implements LoadBalancingStrategy {
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public DataSource selectReplica(List<ReplicaConfig> healthyReplicas) {
if (healthyReplicas.isEmpty()) {
throw new NoHealthyReplicasException("No healthy replicas available");
}
int index = Math.abs(counter.getAndIncrement() % healthyReplicas.size());
ReplicaConfig selected = healthyReplicas.get(index);
return getDataSource(selected);
}
@Override
public String getStrategyName() {
return "RoundRobin";
}
private DataSource getDataSource(ReplicaConfig config) {
// Implementation to get DataSource from ReplicaManager
return null;
}
}
// Weighted Round Robin Strategy
@Component
public class WeightedRoundRobinStrategy implements LoadBalancingStrategy {
private final AtomicInteger counter = new AtomicInteger(0);
private final List<ReplicaConfig> weightedReplicas = new ArrayList<>();
@Override
public DataSource selectReplica(List<ReplicaConfig> healthyReplicas) {
if (healthyReplicas.isEmpty()) {
throw new NoHealthyReplicasException("No healthy replicas available");
}
updateWeightedList(healthyReplicas);
int index = Math.abs(counter.getAndIncrement() % weightedReplicas.size());
ReplicaConfig selected = weightedReplicas.get(index);
return getDataSource(selected);
}
private void updateWeightedList(List<ReplicaConfig> healthyReplicas) {
weightedReplicas.clear();
for (ReplicaConfig config : healthyReplicas) {
for (int i = 0; i < config.getWeight(); i++) {
weightedReplicas.add(config);
}
}
}
@Override
public String getStrategyName() {
return "WeightedRoundRobin";
}
}
// Least Connections Strategy
@Component
public class LeastConnectionsStrategy implements LoadBalancingStrategy {
private final ConnectionTracker connectionTracker;
public LeastConnectionsStrategy(ConnectionTracker connectionTracker) {
this.connectionTracker = connectionTracker;
}
@Override
public DataSource selectReplica(List<ReplicaConfig> healthyReplicas) {
if (healthyReplicas.isEmpty()) {
throw new NoHealthyReplicasException("No healthy replicas available");
}
ReplicaConfig selected = healthyReplicas.stream()
.min(Comparator.comparingInt(connectionTracker::getActiveConnections))
.orElse(healthyReplicas.get(0));
return getDataSource(selected);
}
@Override
public String getStrategyName() {
return "LeastConnections";
}
}
// Latency-Based Strategy
@Component
public class LatencyBasedStrategy implements LoadBalancingStrategy {
private final LatencyMonitor latencyMonitor;
public LatencyBasedStrategy(LatencyMonitor latencyMonitor) {
this.latencyMonitor = latencyMonitor;
}
@Override
public DataSource selectReplica(List<ReplicaConfig> healthyReplicas) {
if (healthyReplicas.isEmpty()) {
throw new NoHealthyReplicasException("No healthy replicas available");
}
ReplicaConfig selected = healthyReplicas.stream()
.min(Comparator.comparingDouble(latencyMonitor::getAverageLatency))
.orElse(healthyReplicas.get(0));
return getDataSource(selected);
}
@Override
public String getStrategyName() {
return "LatencyBased";
}
}
Connection Tracking
@Component
public class ConnectionTracker {
private final Map<String, AtomicInteger> activeConnections = new ConcurrentHashMap<>();
private final Map<String, AtomicLong> totalConnections = new ConcurrentHashMap<>();
public void connectionAcquired(String replicaId) {
activeConnections.computeIfAbsent(replicaId, k -> new AtomicInteger(0))
.incrementAndGet();
totalConnections.computeIfAbsent(replicaId, k -> new AtomicLong(0))
.incrementAndGet();
}
public void connectionReleased(String replicaId) {
AtomicInteger counter = activeConnections.get(replicaId);
if (counter != null) {
counter.decrementAndGet();
}
}
public int getActiveConnections(ReplicaConfig config) {
AtomicInteger counter = activeConnections.get(config.getId());
return counter != null ? counter.get() : 0;
}
public long getTotalConnections(ReplicaConfig config) {
AtomicLong counter = totalConnections.get(config.getId());
return counter != null ? counter.get() : 0;
}
public Map<String, Integer> getActiveConnectionsSnapshot() {
return activeConnections.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get()));
}
}
Metrics and Monitoring
Comprehensive Metrics Collection
@Component
public class MetricsCollector {
private final MeterRegistry meterRegistry;
private final Map<String, Counter> errorCounters = new ConcurrentHashMap<>();
private final Map<String, Timer> queryTimers = new ConcurrentHashMap<>();
private final Map<String, Gauge> connectionGauges = new ConcurrentHashMap<>();
public MetricsCollector(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
public void recordQueryExecution(String replicaId, long duration, boolean success) {
Timer timer = queryTimers.computeIfAbsent(replicaId,
id -> Timer.builder("database.query.duration")
.tag("replica", id)
.register(meterRegistry));
timer.record(duration, TimeUnit.MILLISECONDS);
if (!success) {
Counter counter = errorCounters.computeIfAbsent(replicaId,
id -> Counter.builder("database.query.errors")
.tag("replica", id)
.register(meterRegistry));
counter.increment();
}
}
public void recordHealthCheckSuccess(String replicaId) {
Counter.builder("database.health.check")
.tag("replica", replicaId)
.tag("status", "success")
.register(meterRegistry)
.increment();
}
public void recordHealthCheckFailure(String replicaId) {
Counter.builder("database.health.check")
.tag("replica", replicaId)
.tag("status", "failure")
.register(meterRegistry)
.increment();
}
public void registerConnectionGauge(String replicaId, Supplier<Number> connectionSupplier) {
Gauge.builder("database.connections.active")
.tag("replica", replicaId)
.register(meterRegistry, connectionSupplier);
}
public void recordReplicaSelection(String strategy, String replicaId) {
Counter.builder("loadbalancer.selection")
.tag("strategy", strategy)
.tag("replica", replicaId)
.register(meterRegistry)
.increment();
}
}
@Component
public class LatencyMonitor {
private final Map<String, CircularBuffer<Long>> latencyBuffers = new ConcurrentHashMap<>();
private final int bufferSize = 100;
public void recordLatency(String replicaId, long latencyMs) {
CircularBuffer<Long> buffer = latencyBuffers.computeIfAbsent(replicaId,
id -> new CircularBuffer<>(bufferSize));
buffer.add(latencyMs);
}
public double getAverageLatency(ReplicaConfig config) {
CircularBuffer<Long> buffer = latencyBuffers.get(config.getId());
if (buffer == null || buffer.size() == 0) {
return 0.0;
}
return buffer.stream()
.mapToLong(Long::longValue)
.average()
.orElse(0.0);
}
public double get95thPercentileLatency(ReplicaConfig config) {
CircularBuffer<Long> buffer = latencyBuffers.get(config.getId());
if (buffer == null || buffer.size() == 0) {
return 0.0;
}
List<Long> sorted = buffer.stream().sorted().collect(Collectors.toList());
int index = (int) Math.ceil(0.95 * sorted.size()) - 1;
return sorted.get(Math.max(0, index));
}
}
// Circular buffer implementation for latency tracking
public class CircularBuffer<T> {
private final T[] buffer;
private int head = 0;
private int tail = 0;
private int size = 0;
@SuppressWarnings("unchecked")
public CircularBuffer(int capacity) {
this.buffer = (T[]) new Object[capacity];
}
public void add(T item) {
buffer[tail] = item;
tail = (tail + 1) % buffer.length;
if (size < buffer.length) {
size++;
} else {
head = (head + 1) % buffer.length;
}
}
public Stream<T> stream() {
return IntStream.range(0, size)
.mapToObj(i -> buffer[(head + i) % buffer.length]);
}
public int size() {
return size;
}
}
Spring Boot Integration
Configuration Classes
@Configuration
@EnableConfigurationProperties(ReplicaProperties.class)
public class ReadReplicaConfig {
@Bean
@Primary
public DataSource routingDataSource(ReplicaProperties properties) {
Map<Object, Object> targetDataSources = new HashMap<>();
// Primary write data source
targetDataSources.put("primary", createDataSource(properties.getPrimary()));
// Read replicas
properties.getReplicas().forEach((id, config) -> {
targetDataSources.put(id, createDataSource(config));
});
ReadWriteRoutingDataSource routingDataSource = new ReadWriteRoutingDataSource();
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(targetDataSources.get("primary"));
return routingDataSource;
}
@Bean
public ReplicaManager replicaManager(ReplicaProperties properties) {
ReplicaManager manager = new ReplicaManager();
properties.getReplicas().forEach((id, config) -> {
manager.addReplica(new ReplicaConfig.Builder()
.id(id)
.url(config.getUrl())
.username(config.getUsername())
.password(config.getPassword())
.weight(config.getWeight())
.enabled(config.isEnabled())
.build());
});
return manager;
}
@Bean
public LoadBalancingStrategy loadBalancingStrategy(ReplicaProperties properties) {
String strategy = properties.getLoadBalancing().getStrategy();
switch (strategy.toLowerCase()) {
case "roundrobin":
return new RoundRobinStrategy();
case "weighted":
return new WeightedRoundRobinStrategy();
case "leastconnections":
return new LeastConnectionsStrategy(new ConnectionTracker());
case "latency":
return new LatencyBasedStrategy(new LatencyMonitor());
default:
return new RoundRobinStrategy();
}
}
@Bean
public ReadReplicaLoadBalancer readReplicaLoadBalancer(
ReplicaManager replicaManager,
LoadBalancingStrategy strategy) {
return new ReadReplicaLoadBalancer(strategy);
}
private DataSource createDataSource(ReplicaProperties.DataSourceConfig config) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(config.getUrl());
dataSource.setUsername(config.getUsername());
dataSource.setPassword(config.getPassword());
dataSource.setDriverClassName(config.getDriverClassName());
dataSource.setMaximumPoolSize(config.getMaxPoolSize());
dataSource.setMinimumIdle(config.getMinIdle());
dataSource.setConnectionTimeout(config.getConnectionTimeout());
return dataSource;
}
}
@ConfigurationProperties(prefix = "database.read-replicas")
@Data
public class ReplicaProperties {
private DataSourceConfig primary;
private Map<String, DataSourceConfig> replicas = new HashMap<>();
private LoadBalancing loadBalancing = new LoadBalancing();
@Data
public static class DataSourceConfig {
private String url;
private String username;
private String password;
private String driverClassName = "com.mysql.cj.jdbc.Driver";
private int maxPoolSize = 10;
private int minIdle = 2;
private long connectionTimeout = 30000;
private int weight = 1;
private boolean enabled = true;
}
@Data
public static class LoadBalancing {
private String strategy = "roundrobin";
private int healthCheckInterval = 30;
private int connectionTimeout = 5000;
}
}
Routing Data Source
public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<Boolean> readOnlyContext = new ThreadLocal<>();
@Override
protected Object determineCurrentLookupKey() {
Boolean readOnly = readOnlyContext.get();
if (readOnly != null && readOnly) {
return "read"; // This will be resolved by the load balancer
}
return "primary";
}
public static void setReadOnly(boolean readOnly) {
readOnlyContext.set(readOnly);
}
public static void clearContext() {
readOnlyContext.remove();
}
}
@Component
public class ReadReplicaAspect {
private final ReadReplicaLoadBalancer loadBalancer;
public ReadReplicaAspect(ReadReplicaLoadBalancer loadBalancer) {
this.loadBalancer = loadBalancer;
}
@Around("@annotation(readOnly)")
public Object routeToReadReplica(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable {
ReadWriteRoutingDataSource.setReadOnly(true);
try {
return joinPoint.proceed();
} finally {
ReadWriteRoutingDataSource.clearContext();
}
}
@Around("execution(* org.springframework.data.repository.Repository+.*(..)) && " +
"@annotation(org.springframework.transaction.annotation.Transactional) && " +
"!@annotation(org.springframework.transaction.annotation.Transactional(readOnly = false))")
public Object routeReadTransactions(ProceedingJoinPoint joinPoint) throws Throwable {
ReadWriteRoutingDataSource.setReadOnly(true);
try {
return joinPoint.proceed();
} finally {
ReadWriteRoutingDataSource.clearContext();
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}
Usage Examples
Service Layer Implementation
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
private final ReadReplicaLoadBalancer loadBalancer;
public UserService(UserRepository userRepository, ReadReplicaLoadBalancer loadBalancer) {
this.userRepository = userRepository;
this.loadBalancer = loadBalancer;
}
// Write operation - goes to primary
public User createUser(User user) {
return userRepository.save(user);
}
// Read operation - goes to read replica
@ReadOnly
@Transactional(readOnly = true)
public User findUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
// Complex read operation with custom SQL
@ReadOnly
public List<User> findActiveUsers() {
String sql = "SELECT * FROM users WHERE status = 'ACTIVE'";
List<User> users = new ArrayList<>();
loadBalancer.executeQuery(sql, resultSet -> {
while (resultSet.next()) {
users.add(mapResultSetToUser(resultSet));
}
});
return users;
}
// Batch read operations
@ReadOnly
public Map<Long, User> findUsersByIds(List<Long> userIds) {
String sql = "SELECT * FROM users WHERE id IN (" +
userIds.stream().map(String::valueOf).collect(Collectors.joining(",")) + ")";
Map<Long, User> users = new HashMap<>();
loadBalancer.executeQuery(sql, resultSet -> {
while (resultSet.next()) {
User user = mapResultSetToUser(resultSet);
users.put(user.getId(), user);
}
});
return users;
}
private User mapResultSetToUser(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
user.setStatus(rs.getString("status"));
return user;
}
}
Monitoring and Administration
Admin Endpoints
@RestController
@RequestMapping("/admin/read-replicas")
public class ReadReplicaAdminController {
private final ReplicaManager replicaManager;
private final HealthMonitor healthMonitor;
private final MetricsCollector metricsCollector;
public ReadReplicaAdminController(ReplicaManager replicaManager,
HealthMonitor healthMonitor,
MetricsCollector metricsCollector) {
this.replicaManager = replicaManager;
this.healthMonitor = healthMonitor;
this.metricsCollector = metricsCollector;
}
@GetMapping("/status")
public ResponseEntity<Map<String, Object>> getReplicaStatus() {
Map<String, Object> status = new HashMap<>();
List<ReplicaConfig> healthyReplicas = replicaManager.getHealthyReplicas();
status.put("healthyReplicas", healthyReplicas.size());
status.put("totalReplicas", replicaManager.getReplicaCount());
status.put("replicas", getDetailedReplicaInfo());
return ResponseEntity.ok(status);
}
@PostMapping("/{replicaId}/enable")
public ResponseEntity<Void> enableReplica(@PathVariable String replicaId) {
// Implementation to enable a replica
return ResponseEntity.ok().build();
}
@PostMapping("/{replicaId}/disable")
public ResponseEntity<Void> disableReplica(@PathVariable String replicaId) {
// Implementation to disable a replica
return ResponseEntity.ok().build();
}
@GetMapping("/metrics")
public ResponseEntity<Map<String, Object>> getMetrics() {
Map<String, Object> metrics = new HashMap<>();
// Add various metrics
return ResponseEntity.ok(metrics);
}
private List<Map<String, Object>> getDetailedReplicaInfo() {
// Implementation to get detailed replica information
return Collections.emptyList();
}
}
Testing
Comprehensive Test Suite
@SpringBootTest
@TestPropertySource(properties = {
"database.read-replicas.primary.url=jdbc:h2:mem:primary;DB_CLOSE_DELAY=-1",
"database.read-replicas.replicas.replica1.url=jdbc:h2:mem:replica1;DB_CLOSE_DELAY=-1",
"database.read-replicas.replicas.replica2.url=jdbc:h2:mem:replica2;DB_CLOSE_DELAY=-1",
"database.read-replicas.load-balancing.strategy=roundrobin"
})
public class ReadReplicaLoadBalancerTest {
@Autowired
private ReadReplicaLoadBalancer loadBalancer;
@Autowired
private ReplicaManager replicaManager;
@Test
void testRoundRobinSelection() throws SQLException {
// Get multiple connections and verify round-robin distribution
Set<String> selectedReplicas = new HashSet<>();
for (int i = 0; i < 10; i++) {
try (Connection conn = loadBalancer.getReadConnection()) {
String url = conn.getMetaData().getURL();
selectedReplicas.add(url);
}
}
// Should have used multiple replicas
assertTrue(selectedReplicas.size() > 1);
}
@Test
void testNoHealthyReplicas() {
// Disable all replicas
replicaManager.getHealthyReplicas().forEach(config ->
replicaManager.updateReplicaHealth(config.getId(), false));
assertThrows(NoHealthyReplicasException.class, () -> {
loadBalancer.getReadConnection();
});
}
@Test
void testQueryExecution() {
String sql = "SELECT 1";
loadBalancer.executeQuery(sql, resultSet -> {
assertTrue(resultSet.next());
assertEquals(1, resultSet.getInt(1));
});
}
}
Performance Optimization
Connection Pool Tuning
@Configuration
public class ConnectionPoolConfig {
@Bean
@ConfigurationProperties("database.read-replicas.connection-pool")
public HikariConfig hikariConfig() {
return new HikariConfig();
}
@Bean
public HikariDataSource primaryDataSource() {
HikariConfig config = hikariConfig();
config.setPoolName("PrimaryPool");
config.setReadOnly(false);
return new HikariDataSource(config);
}
@Bean
public HikariDataSource replicaDataSource(ReplicaProperties properties) {
HikariConfig config = hikariConfig();
config.setPoolName("ReplicaPool");
config.setReadOnly(true);
// Additional replica-specific configuration
return new HikariDataSource(config);
}
}
// Custom connection pool properties
@Data
@ConfigurationProperties("database.read-replicas.connection-pool")
public class ConnectionPoolProperties {
private int maximumPoolSize = 20;
private int minimumIdle = 5;
private long connectionTimeout = 30000;
private long idleTimeout = 600000;
private long maxLifetime = 1800000;
private String connectionTestQuery = "SELECT 1";
private boolean autoCommit = true;
}
Conclusion
This comprehensive read replica load balancing solution provides:
- Multiple load balancing strategies (Round Robin, Weighted, Least Connections, Latency-based)
- Health monitoring with automatic failover
- Comprehensive metrics and monitoring
- Spring Boot integration with easy configuration
- Connection pooling optimization
- Annotation-based routing for read/write separation
- Admin endpoints for management and monitoring
The system ensures high availability, improved read performance, and automatic failover while providing observability and easy management through comprehensive monitoring and administration capabilities.