반응형

들어가며

Sharding이란?

  • 한 RDBMS에 데이터가 많아질 경우 용량 초과 이슈가 발생할 수 있고, 데이터 조회 속도가 느려질 수 있다.
  • 이를 해결하기 위해 데이터를 여러 RDBMS에 쪼개 관리하는 파티셔닝 기술을 샤딩이라고 한다.

sharding-jdbc-core

설명

  • com.dangdang:sharding-jdbc-core 라이브러리를 활용하여 샤딩 처리를 수행하는 예제
  • MySQL과 JPA를 활용하므로 필요하면 아래 링크 참고

테이블 추가

DROP TABLE IF EXISTS User;
CREATE TABLE User
(
    userSeq BIGINT UNSIGNED NOT NULL,
    name    VARCHAR(200),
    type    CHAR(10),
    PRIMARY KEY (userSeq)
);

User

@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userSeq;
    private String name;
    @Enumerated(EnumType.STRING)
    private Type type;

    public enum Type {
        KAKAO, GMAIL, BASIC
    }
}

UserRepository

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

build.gradle

dependencies {
    ...
    implementation 'com.dangdang:sharding-jdbc-core:1.5.4.1'
}

ShardDBConfig

@Configuration
public class ShardDBConfig {
    @Bean
    public DataSource shardDataSource() throws SQLException {
        Map<String, DataSource> dataSources = Map.of(
            "shard-0", createDataSource("jdbc:mysql://localhost:13306/test", "ubuntu", "123456"),
            "shard-1", createDataSource("jdbc:mysql://localhost:23306/test", "ubuntu", "123456")
        );

        DataSourceRule dataSourceRule = new DataSourceRule(dataSources);

        SingleKeyDatabaseShardingAlgorithm<?> shardingAlgorithm = new CustomShardingAlgorithm<>(dataSources.size());

        ShardingRule shardingRule = new ShardingRule.ShardingRuleBuilder()
            .dataSourceRule(dataSourceRule)
            .tableRules(Arrays.asList(
                createTableRule("User", "userSeq", "type", dataSourceRule, shardingAlgorithm)
            ))
            .build();

        return new ShardingDataSource(shardingRule);
    }

    private DataSource createDataSource(String url, String username, String password) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName(com.mysql.jdbc.Driver.class.getName());
        dataSource.setJdbcUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    private TableRule createTableRule(String tableName, String primaryKeyName, String shardingKeyName, DataSourceRule dataSourceRule, SingleKeyDatabaseShardingAlgorithm<?> shardingAlgorithm) {
        return TableRule.builder(tableName)
            .generateKeyColumn(primaryKeyName)
            .databaseShardingStrategy(new DatabaseShardingStrategy(shardingKeyName, shardingAlgorithm))
            .dataSourceRule(dataSourceRule)
            .build();
    }

    @Slf4j
    @RequiredArgsConstructor
    private static class CustomShardingAlgorithm<T extends Comparable<?>> implements SingleKeyDatabaseShardingAlgorithm<T> {
        private final int shardSize;

        @Override
        public String doEqualSharding(Collection<String> shardNames, ShardingValue<T> shardingValue) {
            String shardName = "shard-" + Math.abs(crc32(String.valueOf(shardingValue.getValue()))) % shardSize;

            log.info("[DO_SHARDING] shardName : {}, shardingValue : {}", shardName, shardingValue);

            if (shardNames.contains(shardName)) {
                return shardName;
            }

            throw new UnsupportedOperationException();
        }

        @Override
        public Collection<String> doInSharding(Collection<String> shardNames, ShardingValue<T> shardingValue) {
            return null;
        }

        @Override
        public Collection<String> doBetweenSharding(Collection<String> shardNames, ShardingValue<T> shardingValue) {
            return null;
        }

        // 간단하게 value.hashCode()를 사용할 수도 있지만
        // 환경에 따라 hashCode() 값을 동일하게 반환해주지 않을 수 있으므로 샤딩에서 사용하지 않을 것을 권장
        // 따라서 여기서는 CRC32를 활용하여 hash 계산
        private long crc32(String value) {
            CRC32 crc32 = new CRC32();
            crc32.update(value.getBytes());
            return crc32.getValue();
        }
    }
}

Test

@Rollback(false)
@SpringBootTest
public class ShardingTest {
    @Autowired
    private UserRepository userRepository;

    @Transactional
    @Test
    public void insertUser() {
        IntStream.range(0, 100).forEach(i -> userRepository.save(new User(null, "john-" + i, User.Type.values()[i % User.Type.values().length])));
    }

    @Transactional(readOnly = true)
    @Test
    public void selectUsers() {
        System.out.println(userRepository.findAll(PageRequest.of(0, 5)).getContent());
        System.out.println(userRepository.findAll(PageRequest.of(5, 5)).getContent());
    }
}

ShardingSphere

설명

  • ShardingSphere란 RDBMS의 샤딩 처리를 지원하는 아파치 오픈소스 프로젝트를 말한다.
  • 위의 sharding-jdbc-core 예제와 거의 동일하게 진행하고, 디펜던시와 설정만 조금 다르므로 아래를 참고한다.

build.gradle

dependencies {
    ...
    implementation 'org.apache.shardingsphere:sharding-jdbc-core:4.1.1'
}

ShardDBConfig

@Configuration
public class ShardDBConfig {
    @Bean
    public DataSource shardDataSource() throws SQLException {
        Map<String, DataSource> dataSources = Map.of(
            "shard-0", createDataSource("jdbc:mysql://localhost:13306/test", "ubuntu", "123456"),
            "shard-1", createDataSource("jdbc:mysql://localhost:23306/test", "ubuntu", "123456")
        );

        PreciseShardingAlgorithm<?> shardingAlgorithm = new CustomShardingAlgorithm(dataSources.size());

        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.setTableRuleConfigs(Arrays.asList(
            createTableRuleConfig("User", "userSeq", "type", shardingAlgorithm)
        ));

        return ShardingDataSourceFactory.createDataSource(dataSources, shardingRuleConfig, new Properties());
    }

    private DataSource createDataSource(String url, String username, String password) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName(com.mysql.jdbc.Driver.class.getName());
        dataSource.setJdbcUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    private TableRuleConfiguration createTableRuleConfig(String tableName, String primaryKeyName, String shardingKeyName, PreciseShardingAlgorithm shardingAlgorithm) {
        Properties properties = new Properties();
        properties.setProperty("worker.id", "123"); // 프로세스별로 다른 ID값으로 지정

        TableRuleConfiguration tableRuleConfig = new TableRuleConfiguration(tableName);
        tableRuleConfig.setKeyGeneratorConfig(new KeyGeneratorConfiguration("SNOWFLAKE", primaryKeyName, properties));
        tableRuleConfig.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(shardingKeyName, shardingAlgorithm));

        return tableRuleConfig;
    }

    @Slf4j
    @RequiredArgsConstructor
    public static class CustomShardingAlgorithm<T extends Comparable<?>> implements PreciseShardingAlgorithm<T> {
        private final int shardSize;

        @Override
        public String doSharding(final Collection<String> shardNames, final PreciseShardingValue<T> shardingValue) {
            String shardName = "shard-" + Math.abs(crc32(String.valueOf(shardingValue.getValue()))) % shardSize;

            log.info("[DO_SHARDING] shardName : {}, shardingValue : {}", shardName, shardingValue);

            if (shardNames.contains(shardName)) {
                return shardName;
            }

            throw new UnsupportedOperationException();
        }

        private long crc32(String value) {
            CRC32 crc32 = new CRC32();
            crc32.update(value.getBytes());
            return crc32.getValue();
        }
    }
}

참고

반응형

'Development > Spring' 카테고리의 다른 글

[Spring] @ConfigurationProperties  (0) 2021.08.02
[Spring] @Value  (0) 2021.08.02
[Spring] Thymeleaf  (0) 2021.06.26
[Spring] Model Mapping  (0) 2021.06.13
[Spring] Spring Security  (0) 2021.05.29

+ Recent posts