Database Sharding ve Partitioning - Spring Boot Data Management
Database sharding ve partitioning, büyük ölçekli uygulamalarda veri yönetimini optimize etmek için kullanılan kritik tekniktlerdir. Bu yaklaşımlar, veritabanı performansını artırır, ölçeklenebilirlik sağlar ve sistem dayanıklılığını güçlendirir.
Hızlı Karar
| Durum | Sharding Kullan | Dikkat |
|---|---|---|
| Tek DB kapasite sınırında | Evet | Shard key kalıcı karardır |
| Sadece birkaç yavaş sorgu | Önce index/query optimize et | Sharding fazla pahalıdır |
| Tenant bazlı izolasyon | Tenant shard | Büyük tenant skew yaratabilir |
| Zaman bazlı büyük veri | Partitioning | Retention kolaylaşır |
Üretim Kontrol Listesi
- Problem: Sharding hangi kapasite veya izolasyon sınırını çözüyor?
- Çözüm: Shard key, routing, resharding, transaction sınırı ve backup stratejisi net mi?
- Trade-off: Kapasite artar; cross-shard query, transaction ve operasyon karmaşıklığı eklenir.
- Hata durumu: Hot shard, bad key, resharding failure, partial outage ve inconsistent backup ele alınmalı.
- Ölçüm: Per-shard load, skew, cross-shard query count, routing error ve migration duration izlenmeli.
- Güvenlik/maliyet: Tenant izolasyonu erişim kontrolüyle desteklenmeli; shard sayısı operasyon ve storage maliyetini artırır.
Sharding Overview
Sharding Strategies
Partitioning Types
Horizontal Sharding Strategies
Horizontal sharding, tabloları satır bazında farklı veritabanlarına bölme işlemidir. Bu strateji, veri hacmi arttıkça performansın korunmasını sağlar.
Shard Key Selection
Shard key seçimi, sharding stratejisinin başarısını belirleyen en kritik faktördür. Doğru shard key, verilerin eşit dağılımını ve cross-shard query'lerin minimizasyonunu sağlar.
Shard Key Kriterleri:
- User ID: Kullanıcı bazlı veri izolasyonu
- Geographic region: Coğrafi bölge bazlı sharding
- Tenant ID: Multi-tenant uygulamalar için
- Hash-based: Eşit dağılım garantisi
- Range-based: Zaman serileri için uygun
Spring Data JPA ile Sharding:
- @Entity annotations: Shard-aware entity tasarımı
- @Table(schema = "shard_1"): Statik shard assignment
- AbstractRoutingDataSource: Dinamik shard routing
- Custom repository implementations: Shard-aware data access
Temel Kavramlar
# Sharding Stratejileri
strategies:
range_based:
- key_range: "A-M"
shard: "shard_1"
- key_range: "N-Z"
shard: "shard_2"
hash_based:
- hash_function: "user_id % 4"
shards: ["shard_0", "shard_1", "shard_2", "shard_3"]
directory_based:
- lookup_service: "shard_coordinator"
mapping: "user_id -> shard_id"Spring Boot ile Sharding Implementasyonu
1. Custom Sharding Configuration
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.sharding.repository",
repositoryFactoryBeanClass = ShardingRepositoryFactoryBean.class
)
public class ShardingConfiguration {
@Bean
@Primary
public ShardingDataSource shardingDataSource() {
Map<String, DataSource> dataSourceMap = new HashMap<>();
// Shard 1 - Users A-M
dataSourceMap.put("shard1", createDataSource(
"jdbc:postgresql://shard1-db:5432/users",
"shard1_user", "shard1_pass"
));
// Shard 2 - Users N-Z
dataSourceMap.put("shard2", createDataSource(
"jdbc:postgresql://shard2-db:5432/users",
"shard2_user", "shard2_pass"
));
return new ShardingDataSource(dataSourceMap);
}
private DataSource createDataSource(String url, String username, String password) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
return new HikariDataSource(config);
}
}2. Sharding Strategy Interface
public interface ShardingStrategy {
String determineShard(Object shardingKey);
}
@Component
public class UserIdHashShardingStrategy implements ShardingStrategy {
private static final int SHARD_COUNT = 4;
@Override
public String determineShard(Object shardingKey) {
if (shardingKey instanceof Long) {
Long userId = (Long) shardingKey;
int shardIndex = Math.abs(userId.hashCode() % SHARD_COUNT);
return "shard" + shardIndex;
}
throw new IllegalArgumentException("Invalid sharding key type");
}
}
@Component
public class UserNameRangeShardingStrategy implements ShardingStrategy {
@Override
public String determineShard(Object shardingKey) {
if (shardingKey instanceof String) {
String username = (String) shardingKey;
char firstChar = Character.toLowerCase(username.charAt(0));
if (firstChar >= 'a' && firstChar <= 'm') {
return "shard1";
} else {
return "shard2";
}
}
throw new IllegalArgumentException("Invalid sharding key type");
}
}3. Sharding Aware Repository
@Repository
public class ShardedUserRepository {
private final Map<String, JdbcTemplate> shardTemplates;
private final ShardingStrategy shardingStrategy;
public ShardedUserRepository(
ShardingDataSource shardingDataSource,
@Qualifier("userIdHashShardingStrategy") ShardingStrategy shardingStrategy) {
this.shardingStrategy = shardingStrategy;
this.shardTemplates = new HashMap<>();
shardingDataSource.getDataSources().forEach((shardName, dataSource) -> {
shardTemplates.put(shardName, new JdbcTemplate(dataSource));
});
}
public User findById(Long userId) {
String shardName = shardingStrategy.determineShard(userId);
JdbcTemplate template = shardTemplates.get(shardName);
try {
return template.queryForObject(
"SELECT * FROM users WHERE id = ?",
new Object[]{userId},
new UserRowMapper()
);
} catch (EmptyResultDataAccessException e) {
return null;
}
}
public void save(User user) {
String shardName = shardingStrategy.determineShard(user.getId());
JdbcTemplate template = shardTemplates.get(shardName);
template.update(
"INSERT INTO users (id, username, email, created_at) VALUES (?, ?, ?, ?)",
user.getId(), user.getUsername(), user.getEmail(), user.getCreatedAt()
);
}
public List<User> findAll() {
List<User> allUsers = new ArrayList<>();
// Parallel execution across shards
List<CompletableFuture<List<User>>> futures = shardTemplates.entrySet()
.stream()
.map(entry -> CompletableFuture.supplyAsync(() -> {
return entry.getValue().query(
"SELECT * FROM users ORDER BY created_at DESC",
new UserRowMapper()
);
}))
.collect(Collectors.toList());
futures.forEach(future -> {
try {
allUsers.addAll(future.get());
} catch (Exception e) {
log.error("Error fetching from shard", e);
}
});
return allUsers.stream()
.sorted(Comparator.comparing(User::getCreatedAt).reversed())
.collect(Collectors.toList());
}
}Partitioning Stratejileri
1. Horizontal Partitioning (Sharding)
-- Range-based partitioning
CREATE TABLE users_2023 (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) PARTITION BY RANGE (created_at);
CREATE TABLE users_2023_q1 PARTITION OF users_2023
FOR VALUES FROM ('2023-01-01') TO ('2023-04-01');
CREATE TABLE users_2023_q2 PARTITION OF users_2023
FOR VALUES FROM ('2023-04-01') TO ('2023-07-01');2. Vertical Partitioning
// User temel bilgileri
@Entity
@Table(name = "user_profiles")
public class UserProfile {
@Id
private Long userId;
private String username;
private String email;
private LocalDateTime createdAt;
}
// User detay bilgileri (az erişilen)
@Entity
@Table(name = "user_details")
public class UserDetail {
@Id
private Long userId;
private String bio;
private String preferences;
private byte[] profileImage;
}Advanced Sharding Patterns
1. Consistent Hashing
@Component
public class ConsistentHashingStrategy implements ShardingStrategy {
private final TreeMap<Long, String> ring = new TreeMap<>();
private final int virtualNodes = 100;
@PostConstruct
public void initializeRing() {
List<String> shards = Arrays.asList("shard1", "shard2", "shard3", "shard4");
for (String shard : shards) {
for (int i = 0; i < virtualNodes; i++) {
String virtualNode = shard + ":" + i;
long hash = hashFunction(virtualNode);
ring.put(hash, shard);
}
}
}
@Override
public String determineShard(Object shardingKey) {
long hash = hashFunction(shardingKey.toString());
Map.Entry<Long, String> entry = ring.ceilingEntry(hash);
if (entry == null) {
entry = ring.firstEntry();
}
return entry.getValue();
}
private long hashFunction(String input) {
return Hashing.murmur3_128().hashString(input, StandardCharsets.UTF_8)
.asLong();
}
}2. Shard Coordinator Service
@Service
public class ShardCoordinatorService {
private final RedisTemplate<String, String> redisTemplate;
private final Map<String, DataSource> shardDataSources;
public String getShardForUser(Long userId) {
String cacheKey = "user:shard:" + userId;
String cachedShard = redisTemplate.opsForValue().get(cacheKey);
if (cachedShard != null) {
return cachedShard;
}
// Fallback to calculation
String shard = calculateShard(userId);
redisTemplate.opsForValue().set(cacheKey, shard, Duration.ofHours(24));
return shard;
}
public void reshardUser(Long userId, String fromShard, String toShard) {
try {
// Start transaction
User user = fetchUserFromShard(userId, fromShard);
saveUserToShard(user, toShard);
deleteUserFromShard(userId, fromShard);
// Update cache
String cacheKey = "user:shard:" + userId;
redisTemplate.opsForValue().set(cacheKey, toShard);
log.info("Successfully resharded user {} from {} to {}",
userId, fromShard, toShard);
} catch (Exception e) {
log.error("Failed to reshard user {}", userId, e);
throw new ShardingException("Resharding failed", e);
}
}
}Resharding Stratejisi
Monitoring ve Health Checks
Best Practices
Sharding, büyük ölçekli sistemlerde kaçınılmaz bir gereksinimdir, ancak complexity ekler. Doğru strateji ve implementasyon ile sistem performansını büyük ölçüde artırabilir.
Partitioning ve Rebalancing Kararı
| Strateji | Güçlü yön | Risk |
|---|---|---|
| Range partitioning | Sıralı aralık ve range scan | Hot range ve dengesiz büyüme |
| Hash partitioning | Daha dengeli dağılım | Range query ve yeniden dağıtım maliyeti |
| List/tenant partitioning | İzolasyon ve ownership | Büyük tenant veya cross-partition sorgu |
| Consistent hashing | Node değişiminde sınırlı hareket | Ring, virtual node ve metadata yönetimi |
Rebalancing sırasında veri yeni shard'lara kopyalanır, dual-read/dual-write veya change capture ile farklar takip edilir, doğrulama sonrası trafik kademeli taşınır. Hedef; kısa süreli bir migration değil, rollback edilebilir bir state machine'dir.
Rebalancing boyunca shard key değişmemeli veya eski key'den yeni key'e deterministik mapping bulunmalıdır. Cross-shard transaction, global unique key, pagination ve backup/restore davranışları migration öncesi test edilir. Replication, shard içindeki availability ve read scale'i artırır; sharding ise veriyi node'lara böler—aynı karar değildir.
