서론
전통적인 애플리케이션 설계는 단일 데이터베이스를 저장 솔루션으로 사용하는 경우가 많습니다. 하지만 인터넷의 급속한 발전과 애플리케이션 데이터 양의 증가에 따라 데이터베이스는 데이터 양이 증가함에 따라 전체 애플리케이션 프레임워크의 성능 병목 현상이 될 수 있습니다.
첫째, 관계형 데이터베이스는 대부분 B+Tree 유형의 인덱스를 사용합니다. 데이터 양이 특정 임계값을 초과하면 인덱스 깊이가 증가하며, 이는 디스크 IO 작업 횟수에 직접적인 영향을 미쳐 데이터베이스 쿼리 성능에 영향을 줍니다.
둘째, 사용자 수 증가에 따라 고동시성 데이터베이스 요청도 증가합니다. 단일 노드 데이터베이스의 연결 수, TPS 및 저장 용량에는 상한선이 존재하며, 동시성 수량이 특정 수준에 도달하거나 데이터 양이 단일 노드 저장 용량을 초과하면 데이터베이스 성능이 전체 시스템의 병목이 됩니다.
마지막으로, 데이터 양이 매우 클 때 데이터베이스 백업 및 마이그레이션은 점점 더 어려워지며, 시간 비용과 난이도는 데이터 양의 증가와 함께 증가합니다.
따라서 단일 노드 관계형 데이터베이스가 인터넷 애플리케이션 시나리오를 충족시킬 수 없을 때, NoSQL 데이터베이스를 일부 부하를 분담하는 데 사용할 수 있습니다. 하지만 NoSQL은 관계형 데이터베이스의 특성을 완전히 대체할 수 없으므로, 본질적으로 관계형 데이터베이스를 대체할 수 없습니다. 따라서 관계형 데이터베이스 자체에서 해결책을 찾아야 합니다.
단일 데이터베이스가 문제를 해결할 수 없다면, 다중 지점으로 전환하는 것을 고려할 수 있습니다. 데이터 샤딩 방식을 통해 단일 노드 데이터를 특정 규칙에 따라 여러 조각으로 나누어 여러 노드에 저장함으로써 단일 데이터베이스의 성능 병목 현상을 해결하고 시스템의 가용성을 향상시킬 수 있습니다.
데이터 샤딩
데이터 샤딩이란 큰 데이터 세트를 여러 작은 데이터 세트로 분할하는 것을 의미합니다. 특정 기준에 따라 단일 데이터베이스에 존재하는 데이터를 여러 데이터베이스, 여러 데이터 테이블, 여러 저장 영역에 분산 저장하여 성능 병목 현상을 해결하는 방법입니다.
1.1 데이터 샤딩 방식
데이터 샤딩 방식은 일반적으로 수직 분할과 수평 분할 두 가지가 있습니다.
수직 분할
수직 분할은 비즈니스에 따라 분하는 방식으로, 핵심 컨셉은 전용 데이터베이스 사용입니다. 일반적으로 하나의 데이터베이스에는 여러 데이터 테이블이 포함되며, 다른 테이블은 다른 비즈니스를 나타냅니다. 비즈니스에 따라 동일한 비즈니스의 테이블을 동일한 데이터베이스에 배치하고, 다른 비즈니스는 다른 데이터베이스에 저장함으로써 데이터베이스를 여러 데이터베이스로 분할할 수 있습니다. 이를 통해 다른 비즈니스의 데이터베이스 작업이 다른 데이터베이스로 분산됩니다. 또한 특정 데이터 테이블의 필드가 많은 경우, 데이터 테이블의 필드를 기준으로 나눌 수도 있습니다. 예를 들어, 사용자 정보 테이블에 기본 사용자 정보와 상세 정보가 모두 포함되어 있다면, 중요도에 따라 수직 분할하여 기본 사용자 정보 테이블과 상세 사용자 정보 테이블로 나눌 수 있습니다. 이렇게 데이터의 사용 빈도도에 따라 분할 처리할 수 있습니다.
일반적으로 수직 분할은 시스템 아키텍처 설계를 조정해야 하며, 데이터베이스 동시성 처리 능력을 향상시키지만 단일 테이블 데이터 양이 많은 문제를 해결할 수는 없습니다. 하나의 테이블을 두 개의 테이블로 분할했더라도 두 테이블의 필드는 줄었지만 데이터 양은 줄지 않았으므로 쿼리 효율성이 낮은 문제를 해결할 수 없습니다.
수평 분할
수평 분할은 데이터를 가로로 자르는 방식으로, 테이블의 특정 또는 몇 개의 필드에 대한 특정 규칙에 따라 데이터를 여러 데이터베이스나 테이블에 분산시킵니다. 예를 들어, 연도별로 분할하여 다른 연도의 데이터를 다른 연도의 테이블에 저장하거나, 데이터 기본 키를 통해 모듈러스 알고리즘을 사용하여 저장할 테이블을 선택하는 방식 등이 있습니다. 수평 분할은 다른 테이블에 다른 데이터를 저장함으로써 데이터 분산 저장 효과를 달성하여 단일 테이블 데이터 양이 너무 큰 문제를 해결할 수 있습니다.
수평 분할은 일반적으로 비즈니스 레벨에서 인지할 필요가 없으며 시스템 아키텍처 설계를 조정할 필요도 없으므로, 수평 분할 방식이 일반적으로 수직 분할보다 우선시되며, 수직 분할은 시스템 아키텍처 설계 초기에 계획을 세워야 합니다.
1.2 데이터 샤딩 구현
데이터 샤딩은 일반적으로 파티셔닝, 데이터베이스 분할, 테이블 분할 세 가지 방식이 있습니다.
1.2.1 파티셔닝
MySQL 데이터베이스의 데이터는 파일로 디스크에 저장되며, 하나의 테이블은 세 개의 파일이 생성됩니다. .frm 파일은 테이블 구조를 저장하고, .myd 파일은 테이블의 데이터를 저장하며, .myi 파일은 테이블의 인덱스를 저장합니다. 테이블에 저장된 데이터가 많아지면 .myd 파일과 .myi 파일이 커집니다. 이는 데이터를 쿼리할 때 효율이 비교적 낮아지는 원인이 됩니다. MySQL 데이터베이스는 자체적으로 데이터 파티셔닝 기능을 제공하며, 이를 통해 디스크의 파일을 여러 하위 파일로 분할할 수 있습니다. 이는 테이블 구조를 수정하지 않고 원본 데이터를 분산 저장하는 효과를 얻을 수 있습니다. 파티셔닝 후 논리적으로는 하나의 테이블이지만 물리적 저장 시에는 이미 여러 테이블로 분할되었습니다.
파티셔닝은 데이터의 지정된 필드를 특정 영역에 수직으로 분할하거나, 다른 데이터를 다른 영역에 수평으로 분할할 수 있습니다.
1.2.2 데이터베이스 분할
하나의 데이터베이스를 여러 개의 상호 독립적인 데이터베이스로 분할합니다. 각 데이터베이스는 서로 독립적이며 각자의 저장 공간을 독점합니다. 수직 분할은 비즈니스를 데이터베이스 레벨에서 분리할 수 있으며, 수평 분할은 데이터를 분산 저장할 수 있습니다. 이를 통해 데이터베이스 동시성 처리 능력을 향상시키고 단일 데이터베이스의 성능 문제를 해결할 수 있습니다.
1.2.3 테이블 분할
하나의 데이터베이스 테이블을 여러 개의 상호 독립적인 테이블로 분할합니다. 수직 분할은 실제로 하나의 부모 테이블을 여러 자식 테이블로 분하는 것이며, 수평 분할은 데이터 양이 큰 테이블을 여러 데이터 양이 작은 테이블로 분하는 것입니다. 이를 통해 단일 테이블 데이터 양이 많아서 발생하는 쿼리 효율성 저하 문제를 해결하고 동시에 데이터 테이블의 동시성 처리 능력을 향상시킬 수 있습니다.
데이터베이스 및 테이블 분할 방안
파티셔닝 기능은 MySQL 데이터베이스의 고 버전에 기본적으로 포함되어 있으며, 데이터베이스가 제공하는 API를 통해 테이블 데이터를 동적으로 파티셔닝할 수 있습니다. 데이터 파티셔닝은 비즈니스 레벨에 완전히 투명하며 사용자에게는 인지되지 않습니다.
하지만 데이터베이스 및 테이블 분할은 데이터베이스 자체로 구현할 수 없으며, 사용자가 직접 해결 방안을 생각해야 합니다. 해결 방안은 중간 소프트웨어를 사용하는 것이며, 중간 소프트웨어는 구성 요소 형태와 프록시 서비스 두 가지 형태로 존재할 수 있습니다. 구성 요소 형태는 애플리케이션에 통합되어 애플리케이션 JDBC 기능을 강화하는 역할을 합니다. 프록시 서비스 방식은 애플리케이션과 데이터베이스 사이에 프록시 서비스 레이어를 추가하는 방식으로, 애플리케이션은 SQL 작업을 프록시 서비스에 전달하고 프록시 서비스가 여러 데이터 노드에 접근하여 작업을 수행한 후 결과를 애플리케이션에 반환합니다. 하지만 독립적인 서비스 배포가 필요합니다.
ShardingJDBC의 사용
ShardingJDBC는 구성 요소 방식으로 애플리케이션에 통합되며, 경량 구성 요소 도구로定位됩니다. Java 애플리케이션의 JDBC 레이어에서 향상된 기능을 제공합니다. 애플리케이션이 데이터베이스에 직접 연결할 수 있으며 추가적인 배포 및 종속성이 필요하지 않으며, 증강된 버전의 JDBC와 같습니다. ShardingJDBC의 아키텍처는 구성 요소 방식의 아키텍처와 유사합니다.
3.1 ShardingJDBC의 핵심 개념
- 논리 테이블: 수평 분할된 데이터베이스/테이블은 동일한 테이블 구조와 논리를 가집니다. 예를 들어, user 테이블이 user_01, user_02 등으로 수평 분할된 경우, 논리적으로는 모두 user 테이블로 통칭됩니다.
- 실제 테이블: 실제 데이터를 저장하는 물리적 테이블입니다. 예를 들어, user 테이블이 user_01, user_02 테이블로 수평 분할된 경우, user_01과 user_02가 실제 테이블입니다.
- 데이터 노드: 데이터 샤딩의 최소 단위로, 데이터 소스와 데이터 테이블로 구성됩니다. 예를 들어, db0.user_01 테이블, db1.user_02 테이블 등이 있습니다.
- 바인딩 테이블: 샤딩 규칙이 일치하는 부모 테이블과 자식 테이블입니다. 예를 들어, order 테이블과 order_detail 테이블이 모두 orderId를 기준으로 샤딩되는 경우, 두 테이블은 바인딩 테이블 관계입니다. 바인딩 관계를 설정하면 부모 테이블과 자식 테이블의 연관 쿼리에서 데카르트 곱 연관이 발생하지 않습니다.
- 브로드캐스트 테이블: 모든 샤딩에 테이블 구조와 데이터가 완전히 동일한 테이블이 존재합니다. 일반적으로 데이터 딕셔너리나 각 데이터베이스에서 공통으로 사용되는 데이터를 저장하는 데 사용되며, 데이터 양은 작지만 빈번하게 사용됩니다.
- 샤딩 키: 샤딩에 사용되는 데이터베이스 필드입니다. 예를 들어, 수평 분할 시 user 테이블의 userId를 모듈러스 알고리즘으로 할당하여 userId%10의 나머지에 따라 데이터가 어떤 노드에 있는지 판단합니다.
- 샤딩 알고리즘: 샤딩 키를 특정 알고리즘에 따라 샤딩하는 데 사용됩니다. 주요 알고리즘은 다음과 같습니다:
- 정확 샤딩 알고리즘(PreciseShardingAlgorithm): 단일 키를 샤딩 키로 사용하는 = 및 IN 시나리오를 처리합니다. StandardShardingStrategy와 함께 사용해야 합니다.
- 범위 샤딩 알고리즘(RangeShardingAlgorithm): 단일 키를 샤딩 키로 사용하는 between, and, >, <, >=, <= 등의 샤딩 시나리오를 처리합니다. StandardShardingStrategy와 함께 사용해야 합니다.
- 복합 샤딩 알고리즘(ComplexKeysShardingAlgorithm): 여러 키를 샤딩 키로 사용하여 샤딩하는 시나리오를 처리합니다. 여러 샤딩 키 간의 논리가 복잡하므로 ComplexShardingStrategy와 함께 사용해야 합니다.
- 힌트 샤딩 알고리즘(HintShardingAlgorithm): 힌트 행 샤딩 시나리오를 처리합니다. HintShardingStrategy와 함께 사용해야 합니다.
- 샤딩 전략: 샤딩 키와 샤딩 알고리즘이 결합된 것입니다. 주요 샤딩 전략은 다음과 같습니다:
- 표준 샤딩 전략: StandardShardingStrategy에 해당합니다. SQL 문장의 =, >, <, >=, <=, IN, AND, BETWEEN 등 샤딩 작업을 지원합니다. StandardShardingStrategy는 단일 키 샤딩만 지원하며, PreciseShardingAlgorithm과 RangeShardingAlgorithm 두 가지 샤딩 알고리즘을 제공합니다. PreciseShardingAlgorithm은 필수이며, = 및 IN 샤딩을 처리합니다. RangeShardingAlgorithm은 선택 사항이며, BETWEEN AND, >, <, >=, <= 샤딩을 처리합니다. 구성하지 않으면 SQL의 BETWEEN AND는 전체 데이터베이스 라우팅으로 처리됩니다.
- 복합 샤딩 전략: ComplexShardingStrategy에 해당합니다. SQL 문장의 =, >, <, >=, <=, IN 및 BETWEEN AND 샤딩 작업을 지원합니다. ComplexShardingStrategy는 다중 샤딩 키를 지원하며, 다중 샤딩 키 간의 관계가 복잡하므로 과도한 캡슐화 없이 샤딩 키 값과 샤딩 연산자를 샤딩 알고리즘에 직접 전달하여 응용 프로그램 개발자가 구현할 수 있도록 최대한의 유연성을 제공합니다.
- 행 표현식 샤딩 전략: InlineShardingStrategy에 해당합니다. Groovy 표현식을 사용하며, SQL 문장의 = 및 IN 샤딩 작업을 지원합니다. 단일 샤딩 키만 지원합니다. 간단한 샤딩 알고리즘의 경우 간단한 구성을 통해 사용하여 복잡한 Java 코드 개발을 피할 수 있습니다. 예: t_user_$->{u_id % 8}은 t_user 테이블이 u_id를 8로 나누어 8개의 테이블로 분할되며, 테이블 이름은 t_user_0부터 t_user_7입니다.
- 힌트 샤딩 전략: HintShardingStrategy에 해당합니다. SQL에서 샤딩 값을 추출하는 대신 힌트를 통해 샤딩 값을 지정하는 방식의 샤딩 전략입니다.
- 비샤딩 전략: NoneShardingStrategy에 해당합니다. 샤딩하지 않는 전략입니다.
3.2 ShardingJDBC 구성
다음은 Spring Boot와 ShardingJDBC를 통합하는 방식에 대한 ShardingJDBC 구성 설명입니다.
데이터 소스 구성:
sharding.jdbc.datasource.names= # 데이터 소스 이름, 여러 데이터 소스는 쉼표로 구분
sharding.jdbc.datasource.<data-source-name>.type= # 데이터베이스 연결 풀 클래스 이름
sharding.jdbc.datasource.<data-source-name>.driver-class-name= # 데이터베이스 드라이버 클래스 이름
sharding.jdbc.datasource.<data-source-name>.url= # 데이터베이스 URL 연결
sharding.jdbc.datasource.<data-source-name>.username= # 데이터베이스 사용자 이름
sharding.jdbc.datasource.<data-source-name>.password= # 데이터베이스 비밀번호
sharding.jdbc.datasource.<data-source-name>.xxx= # 데이터베이스 연결 풀의 기타 속성
# 논리 테이블에 대한 실제 데이터 노드 구성
sharding.jdbc.config.sharding.tables.<logic-table-name>.actual-data-nodes= # 데이터 소스 이름 + 테이블 이름으로, 점으로 구분. 여러 테이블은 쉼표로 구분되며, inline 표현식 지원. 생략 시 알려진 데이터 소스와 논리 테이블 이름으로 데이터 노드 생성. 브로드캐스트 테이블(연관 쿼리에 사용되는 각 데이터베이스에 동일한 테이블이 필요한 경우, 대부분 딕셔너리 테이블) 또는 데이터베이스만 분할하고 테이블은 분할하지 않으며 모든 데이터베이스의 테이블 구조가 완전히 동일한 경우에 사용
# 데이터베이스 샤딩 전략, 생략 시 기본 샤딩 전략 사용. 다음 샤딩 전략 중 하나만 선택 가능
# 단일 샤딩 키를 위한 표준 샤딩 시나리오
sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.standard.sharding-column= # 샤딩 열 이름
sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.standard.precise-algorithm-class-name= # 정확 샤딩 알고리즘 클래스 이름, = 및 IN에 사용. 해당 클래스는 PreciseShardingAlgorithm 인터페이스를 구현하고 매개 변수 없는 생성자를 제공해야 함
sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.standard.range-algorithm-class-name= # 범위 샤딩 알고리즘 클래스 이름, BETWEEN에 사용, 선택 사항. 해당 클래스는 RangeShardingAlgorithm 인터페이스를 구현하고 매개 변수 없는 생성자를 제공해야 함
# 다중 샤딩 키를 위한 복합 샤딩 시나리오
sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.complex.sharding-columns= # 샤딩 열 이름, 여러 열은 쉼표로 구분
sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.complex.algorithm-class-name= # 복합 샤딩 알고리즘 클래스 이름. 해당 클래스는 ComplexKeysShardingAlgorithm 인터페이스를 구현하고 매개 변수 없는 생성자를 제공해야 함
# 행 표현식 샤딩 전략
sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.inline.sharding-column= # 샤딩 열 이름
sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.inline.algorithm-expression= # 샤딩 알고리즘 행 표현식, groovy 구문에 맞아야 함
# 힌트 샤딩 전략
sharding.jdbc.config.sharding.tables.<logic-table-name>.database-strategy.hint.algorithm-class-name= # 힌트 샤딩 알고리즘 클래스 이름. 해당 클래스는 HintShardingAlgorithm 인터페이스를 구현하고 매개 변수 없는 생성자를 제공해야 함
# 테이블 샤딩 전략, 데이터베이스 샤딩 전략과 동일
sharding.jdbc.config.sharding.tables.<logic-table-name>.table-strategy.xxx= # 생략
sharding.jdbc.config.sharding.tables.<logic-table-name>.key-generator-column-name= # 자동 증가 열 이름, 생략 시 자동 증가 기본 키 생성기 사용 안 함
sharding.jdbc.config.sharding.tables.<logic-table-name>.key-generator-class-name= # 자동 증가 열 값 생성기 클래스 이름, 생략 시 기본 자동 증가 열 값 생성기 사용. 해당 클래스는 매개 변수 없는 생성자를 제공해야 함
sharding.jdbc.config.sharding.tables.<logic-table-name>.logic-index= # 논리 인덱스 이름, 분할된 Oracle/PostgreSQL 데이터베이스의 DROP INDEX XXX 문은 구성된 논리 인덱스 이름을 통해 실행되는 SQL의 실제 분할 테이블을 지정
sharding.jdbc.config.sharding.binding-tables[0]= # 바인딩 테이블 규칙 목록
sharding.jdbc.config.sharding.binding-tables[1]= # 바인딩 테이블 규칙 목록
sharding.jdbc.config.sharding.binding-tables[x]= # 바인딩 테이블 규칙 목록
sharding.jdbc.config.sharding.broadcast-tables[0]= # 브로드캐스트 테이블 규칙 목록
sharding.jdbc.config.sharding.broadcast-tables[1]= # 브로드캐스트 테이블 규칙 목록
sharding.jdbc.config.sharding.broadcast-tables[x]= # 브로드캐스트 테이블 규칙 목록
sharding.jdbc.config.sharding.default-data-source-name= # 샤딩 규칙이 구성되지 않은 테이블은 기본 데이터 소스를 통해 위치 지정
sharding.jdbc.config.sharding.default-database-strategy.xxx= # 기본 데이터베이스 샤딩 전략, 데이터베이스 샤딩 전략과 동일
sharding.jdbc.config.sharding.default-table-strategy.xxx= # 기본 테이블 샤딩 전략, 테이블 샤딩 전략과 동일
sharding.jdbc.config.sharding.default-key-generator-class-name= # 기본 자동 증가 열 값 생성기 클래스 이름, 생략 시 io.shardingsphere.core.keygen.DefaultKeyGenerator 사용. 해당 클래스는 KeyGenerator 인터페이스를 구현하고 매개 변수 없는 생성자를 제공해야 함
sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.master-data-source-name= # 주-부 데이터베이스 부분 참조
sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[0]= # 주-부 데이터베이스 부분 참조
sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[1]= # 주-부 데이터베이스 부분 참조
sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.slave-data-source-names[x]= # 주-부 데이터베이스 부분 참조
sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.load-balance-algorithm-class-name= # 주-부 데이터베이스 부분 참조
sharding.jdbc.config.sharding.master-slave-rules.<master-slave-data-source-name>.load-balance-algorithm-type= # 주-부 데이터베이스 부분 참조
sharding.jdbc.config.config.map.key1= # 주-부 데이터베이스 부분 참조
sharding.jdbc.config.config.map.key2= # 주-부 데이터베이스 부분 참조
sharding.jdbc.config.config.map.keyx= # 주-부 데이터베이스 부분 참조
sharding.jdbc.config.props.sql.show= # SQL 표시 여부, 기본값: false
sharding.jdbc.config.props.executor.size= # 작업 스레드 수, 기본값: CPU 코어 수
sharding.jdbc.config.config.map.key1= # 사용자 정의 구성
sharding.jdbc.config.config.map.key2= # 사용자 정의 구성
sharding.jdbc.config.config.map.keyx= # 사용자 정의 구성
3.3 ShardingJDBC 사용 사례
1. Maven 의존성 구성
<dependency>
<groupId>io.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
2. 테스트 데이터베이스 및 테이블 생성
새로운 테스트 데이터베이스 shard_db0와 shard_db1을 생성하고, 구조가 동일한 테스트 테이블 log_info0, log_info1, log_info2를 각각 생성합니다. 각 테이블에는 log_id와 log_content 두 개의 필드만 포함됩니다.
3. Spring Boot의 ShardingJDBC 관련 구성
## sharding-jdbc
sharding:
jdbc:
# 데이터 소스
datasource:
names: db0,db1
db0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/shard_db0
username: root
password: password
db1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/shard_db1
username: root
password: password
# 샤딩
config:
sharding:
# 데이터베이스 샤딩 전략
default-database-strategy:
inline:
sharding-column: log_id
algorithm-expression: db${log_id % 2}
# 테이블 샤딩 전략
tables:
log_info:
actual-data-nodes: db$->{0..1}.log_info${0..2}
table-strategy:
inline:
sharding-column: log_id
algorithm-expression: log_info${log_id % 3}
여기서 두 개의 데이터 소스는 각각 데이터베이스 shard_db0와 shard_db1에 해당하며, 데이터베이스 샤딩 전략은 log_id를 기준으로 모듈러스 연산을 사용하여 샤딩합니다. 테이블 샤딩 역시 log_id를 기준으로 모듈러스 알고리즘을 사용하여 데이터 테이블을 할당합니다.
4. 데이터 일괄 삽입 테스트 코드
ID가 1부터 시작하여 50까지 총 50개의 데이터를 삽입합니다.
@Resource
private LogInfoMapper logInfoMapper;
public void insertLogsTest(){
for(long i=1; i <= 50; i++){
logInfoMapper.insertLog(i, ("logContent:" + i));
}
}
log_id를 샤딩 키로 사용하여 데이터베이스를 분할하므로, log_id가 홀수인 데이터는 데이터베이스 shard_db1에 저장되고, 짝수인 데이터는 shard_db0에 저장됩니다. 또한 log_id를 기준으로 테이블 분할 작업을 수행하며, log_info0에는 log_id가 3의 배수인 데이터가 저장되고, log_info1에는 3의 배수+1인 데이터가 저장되며, log_info2에는 log_id가 3의 배수+2인 데이터가 저장됩니다.
5. 결과 검증
데이터베이스 shard_db0의 세 개의 log_info 테이블에서 각각 다음 쿼리를 실행합니다:
SELECT * FROM shard_db0.log_info0;
SELECT * FROM shard_db0.log_info1;
SELECT * FROM shard_db0.log_info2;
데이터베이스 shard_db1의 세 개의 log_info 테이블에서 각각 다음 쿼리를 실행합니다:
SELECT * FROM shard_db1.log_info0;
SELECT * FROM shard_db1.log_info1;
SELECT * FROM shard_db1.log_info2;
이렇게 하면 50개의 데이터가 두 개의 데이터베이스의 6개 테이블에 분산 저장됩니다. 물론 더 많은 데이터베이스와 테이블을 추가하여 매우 큰 데이터 양의 샤딩 효과를 구현할 수 있습니다.
3.4 데이터베이스 및 테이블 분할의 고급 사용법
이전 섹션에서 ShardingJDBC의 데이터베이스 및 테이블 분할에 대한 기본 작업을 살펴보았지만, 실제 비즈니스 시나리오의 복잡성은 사례의 단일 테이블 두 필드만큼 간단하지 않으며, 다음과 같은 여러 문제를 해결해야 합니다.
3.4.1 데이터베이스 및 테이블 분할 자동 증가 기본 키 구현
단일 테이블의 경우 주로 기본 키 자동 증가 방식을 통해 고유 ID를 유지하지만, 데이터베이스 및 테이블 분할의 경우 데이터 테이블의 자동 증가 ID를 사용하면 문제가 발생합니다. 각 테이블은 자체적인 자동 증가 ID 세트를 가지고 있으며, 다른 테이블에 동일한 기본 키가 발생할 가능성이 높습니다. 이는 먼저 해결해야 할 큰 문제입니다. ShardingJDBC가 데이터베이스 및 테이블 분할 솔루션을 제공하므로, 이 문제도 고려하여 해결책을 제공합니다.
다음과 같이 구성하여 고유 ID를 자동으로 생성할 수 있습니다:
sharding.jdbc.config.sharding.tables.<logic-table-name>.key-generator-column-name= # 자동 증가 열 이름, 생략 시 자동 증가 기본 키 생성기 사용 안 함
sharding.jdbc.config.sharding.tables.<logic-table-name>.key-generator-class-name= # 자동 증가 열 값 생성기 클래스 이름, 생략 시 기본 자동 증가 열 값 생성기 사용. 해당 클래스는 매개 변수 없는 생성자를 제공해야 함
여기서 key-generator-column-name은 자동으로 고유 ID를 생성할 필드 이름을 설정할 수 있으며, key-generator-class-name는 사용할 생성기 유형을 나타냅니다. 기본적으로 UUID와 눈송이 알고리즘 두 가지 모드를 제공하며, 각각 UUID/SNOWFLAKE입니다. 물론 사용자 정의 고유 ID 생성기도 만들 수 있습니다.
public class CustomKeyGenerator implements KeyGenerator {
private AtomicLong atomicLong = new AtomicLong(0);
@Override
public Number generateKey() {
return atomicLong.incrementAndGet();
}
}
사용자 정의 CustomKeyGenerator 클래스는 KeyGenerator 인터페이스를 구현하고 generatorKey 메서드를 재정의하며, 이렇게 하면 각 데이터 삽입 시 해당 메서드를 실행하여 구성된 필드에 기본 키 값을 설정합니다.
3.4.2 바인딩 테이블
비즈니스에 부모 테이블과 자식 테이블이 관련될 때 부모 테이블과 자식 테이블의 연관 쿼리가 발생합니다. 데이터베이스 및 테이블 분할을 사용하면 부모 테이블과 자식 테이블이 다른 데이터 노드에 분산될 수 있으며, 이로 인해 부모 테이블과 자식 테이블의 연관 문제가 발생할 수 있습니다. 예를 들어, 전자상거래 시스템의 주문(order) 테이블과 주문 상세(order_detail) 테이블이 있습니다.
각 데이터 노드에 order 테이블과 order_detail 테이블을 각각 생성합니다.
두 테이블을 함께 바인딩하면 order_detail 테이블의 데이터가 order 테이블의 관련 데이터와 동일한 노드에 저장됩니다. 다음과 같이 구성합니다:
sharding.jdbc.config.sharding.tables.order.actual-data-nodes=db$->{0..1}.order${0..2}
sharding.jdbc.config.sharding.tables.order.table-strategy.inline.sharding-column=order_id
sharding.jdbc.config.sharding.tables.order.table-strategy.inline.algorithm-expression=order${order_id % 3}
sharding.jdbc.config.sharding.tables.order_detail.actual-data-nodes=db$->{0..1}.order_detail${0..2}
sharding.jdbc.config.sharding.tables.order_detail.table-strategy.inline.sharding-column=order_id
sharding.jdbc.config.sharding.tables.order_detail.table-strategy.inline.algorithm-expression=order_detail${order_id % 3}
# order와 order_detail 테이블 바인딩
sharding.jdbc.config.sharding.binding-tables[0]=order,order_detail
3.4.3 브로드캐스트 테이블
비즈니스 시스템에 일부 데이터 딕셔너리가 포함된 경우, 예를 들어 시도구역 정보, 사용자 역할의 공통 정보 등이 있습니다. 데이터베이스 및 테이블 분할 후에는 각 데이터 노드에 이러한 데이터의 사본이 필요합니다. 이 경우 각 데이터 노드에 동일한 테이블을 생성해야 하며, 데이터가 업데이트될 때 모든 데이터 노드의 데이터를 동기화 업데이트해야 합니다.
코드로 구현하면 상당히 복잡하므로, ShardingJDBC는 브로드캐스트 테이블 구성 방식을 사용하여 이 문제를 해결합니다. 다음과 같이 구성합니다:
# 브로드캐스트 테이블 구성: 사용자 역할 테이블
sharding.jdbc.config.sharding.broadcast-tables[0]=user_role
구성 후 user_role 테이블의 데이터를 업데이트하면 모든 노드의 데이터가 동기화 업데이트됩니다.
3.4.4 페이징 쿼리
ShardingJDBC는 MySQL, Oracle 데이터베이스의 페이징 쿼리를 지원합니다.
3.4.5 트랜잭션
ShardingJDBC는 테이블 및 데이터베이스 분할의 트랜잭션을 지원하지만, SQL이 라우팅되는 데이터베이스는 동일해야 한다는 전제 조건이 있습니다. 즉, 단일 데이터베이스 내의 SQL 실행은 트랜잭션을 보장할 수 있습니다.
ShardingJDBC의 구현
ShardingJDBC의 핵심 프로세스는 주로 다음과 같은 6단계로 나뉩니다: **SQL 구문 분석 -> SQL 최적화 -> SQL 라우팅 -> SQL 재작성 -> SQL 실행 -> 결과 병합**
4.1 SQL 구문 분석
어휘 분석과 구문 분석으로 나뉩니다. 먼저 어휘 분석기를 통해 SQL을 더 이상 분할할 수 없는 단어들로 나눕니다. 그런 다음 구문 분석기를 사용하여 SQL을 이해하고 최종적으로 구문 분석 컨텍스트를 도출합니다. 구문 분석 컨텍스트에는 테이블, 선택 항목, 정렬 항목, 그룹화 항목, 집계 함수, 페이징 정보, 쿼리 조건 및 수정이 필요한 플레이스홀더 표시가 포함됩니다.
SQL 구문 분석은 ShardingJDBC의 구문 분석 엔진이 담당합니다.
4.2 SQL 최적화
샤딩 조건을 병합하고 최적화합니다.
4.3 SQL 라우팅
구문 분석 컨텍스트에 따라 데이터베이스 및 테이블 샤딩 전략을 일치시키고 라우팅 경로를 생성합니다. 샤딩 키를 포함하는 SQL의 경우, 샤딩 키의 다양성에 따라 단일 라우팅(샤딩 키의 연산자가 등호인 경우), 다중 라우팅(샤딩 키의 연산자가 IN인 경우) 및 범위 라우팅(샤딩 키의 연산자가 BETWEEN인 경우)으로 나눌 수 있습니다. 샤딩 키를 포함하지 않는 SQL은 브로드캐스트 라우팅을 사용합니다.
4.4 SQL 재작성
SQL을 실제 데이터베이스에서 올바르게 실행할 수 있는 문장으로 재작성합니다. SQL 재작성은 정확성 재작성과 최적화 재작성으로 나뉩니다.
SQL 재작성의 주요 시나리오는 다음과 같습니다:
- 실제 테이블 이름의 재작성: 논리 테이블을 쿼리하는 SQL이 select * from user where user_id = 1000인 경우, 실제 테이블의 SQL로 재작성해야 합니다: select * from user_0 where user_id = 1000
- 페이징 매개변수의 재작성: 두 개의 실제 테이블에 데이터가 1,2,3,4; 5,6,7,8로 분할된 경우에 select * from user order by user_id limit 2,2를 실행하면 재작성하지 않으면 각 실제 테이블에서 가져온 결과는 3,4와 7,8이며, 결과를 병합한 후 최종 결과는 7과 8이 됩니다. 이는 실제로 두 번째 페이지의 두 개의 데이터는 3과 4여야 한다는 점에서 명백히 잘못된 데이터입니다. 따라서 SQL을 select * from user order by user_id limit 0,4로 재작성해야 합니다. 즉, 첫 번째 페이지와 두 번째 페이지의 모든 데이터를 가져온 후 메모리에서 다시 정렬하여 두 번째 페이지의 데이터를 가져옵니다. (limit 오프셋 값이 클수록 효율이 낮아집니다)
- 일괄 분할: 일괄 작업을 수행할 때, 예를 들어 IN 작업의 경우 user_id의 홀수와 짝수에 따라 두 테이블로 분할된다고 가정하면 SQL이 select * from user where user_id in (1,2,3,4)인 경우 분할하지 않으면 이 SQL이 두 테이블에서 모두 실행되어 필터링된 데이터가 많아지고 성능이 저하됩니다. 결과에는 영향이 없지만, 일괄 삽입 작업의 경우 반드시 분해해야 하며, 그렇지 않으면 여러 테이블에 동일한 데이터가 존재하게 됩니다.
4.5 SQL 실행
SQL 실행은 실행 엔진을 통해 처리됩니다. ShardingJDBC는 자동화된 실행 엔진을 사용하며, 라우팅과 재작성이 완료된 실제 SQL을 안전하고 효율적으로 하위 데이터 소스에 실행하는 역할을 담당합니다. 단순히 SQL을 JDBC를 통해 직접 데이터 소스에 실행하는 것이 아니며, 실행 요청을 스레드 풀에 직접 배치하여 동시에 실행하는 것도 아닙니다. 데이터 소스 연결 생성 및 메모리 사용량이 발생하는 소비를 균형 있게 조정하고, 동시성을 최대한 합리적으로 활용하는 데 더 중점을 둡니다. 실행 엔진의 목표는 자동화된 자원 제어와 실행 효율의 균형을 맞추는 것입니다.
4.5.1 연결 모드
먼저 데이터베이스 연결 측면에서 분석하면, 하나의 SQL 조건에 여러 데이터베이스 및 여러 테이블의 데이터가 포함된 경우, 각 실제 SQL이 하나의 연결을 차지하면 데이터베이스 연결 수가 부족해져 다른 SQL 실행에 심각한 영향을 미칠 수 있습니다. 하지만 하나의 SQL이 많은 실제 SQL로 분해된 경우, 하나의 연결을 사용하면 병렬 효과를 달성할 수 없습니다. 예를 들어, 하나의 SQL이 10개의 실제 SQL로 분해된 경우, 하나의 연결로 순차적으로 처리하면 하나의 스레드가 10번의 데이터베이스 쿼리 작업을 순차적으로 실행하게 되어 명백히 효율이 크게 저하됩니다.
따라서 ShardingJDBC는 사용자가 선택할 수 있는 두 가지 데이터베이스 연결 모드를 제공합니다. 메모리 제한 모드와 연결 수 제한 모드입니다.
- 메모리 제한 모드: 각 실제 SQL에 하나의 연결을 할당하여 SQL의 병렬 실행을 달성하고 효율을 극대화합니다.
- 연결 수 제한 모드: 각 데이터베이스에 하나의 연결만 할당하며, 데이터베이스 분할의 경우 여러 연결이 할당되고, 테이블 분할의 경우 하나의 연결만 할당하여 순차적으로 실행됩니다.
ShardingJDBC는 자동화된 실행 엔진을 통해 다양한 SQL 특성에 따라 최적의 모드를 선택하도록 동적으로 선택합니다. 사용자는 maxConnectionSizePerQuery를 통해 한 번의 쿼리 작업에 최대 몇 개의 연결을 할당할 수 있는지 설정하여 연결 수를 제한할 수 있습니다.
4.6 결과 병합
각 데이터 노드에서 가져온 다중 데이터 결과 세트를 하나의 결과 세트로 결합하고 요청 클라이언트에 올바르게 반환하는 것을 결과 병합이라고 합니다.
ShardingSphere는 기능적으로 순회, 정렬, 그룹화, 페이징 및 집계 5가지 유형의 결과 병합을 지원하며, 이들은 조합 관계이며 상호 배제되지 않습니다. 구조적으로는 스트림 병합, 메모리 병합 및 장식자 병합으로 나눌 수 있습니다. 스트림 병합과 메모리 병합은 상호 배제적이며, 장식자 병합은 스트림 병합과 메모리 병합 위에서 추가 처리를 할 수 있습니다.
데이터베이스에서 반환된 결과 세트는 한 번에 하나씩 반환되므로 모든 데이터를 한 번에 메모리에 로드할 필요가 없습니다. 따라서 결과 병합 시 데이터베이스의 반환 방식을 따르면 메모리 소비를 크게 줄일 수 있으며, 이는 병합 방식의 우선 선택입니다.
스트림 병합은 결과 세트에서 가져온 각 데이터가 단일 데이터를 올바르게 반환하는 방식으로 가져올 수 있으며, 이는 데이터베이스의 기본 반환 방식과 가장 일치합니다. 순회, 정렬 및 스트림 그룹화는 모두 스트림 병합의 일종입니다.
메모리 병합은 결과 세트의 모든 데이터를 순회하여 메모리에 저장한 후 통합된 그룹화, 정렬 및 집계 계산을 수행한 후, 이를 단일 액세스 데이터 결과 세트로 캡슐화하여 반환합니다.
장식자 병합은 모든 결과 세트 병합에 대한 통합된 기능 향상이며, 현재 장식자 병합에는 페이징 병합과 집계 병합 두 가지 유형이 있습니다.
ShardingSphere는 페이징 쿼리에 대해 두 가지 측면에서 최적화합니다.
- 스트림 처리 + 병합 정렬 방식을 사용하여 메모리 과도 사용을 방지합니다. SQL 재작성은 불가피하게 추가 대역폭을 차지하지만 메모리 폭주를 유발하지는 않습니다. 직관과 달리, 대부분의 사람들은 ShardingSphere가 1,000,010 * 2개의 레코드를 모두 메모리에 로드하여 메모리 부족으로 인해 메모리 오버플로우가 발생할 것이라고 생각합니다. 그러나 각 결과 세트의 레코드는 정렬되어 있으므로, ShardingSphere는 각 분할의 현재 결과 세트 레코드를 비교할 때마다 메모리에 상주하는 레코드는 현재 라우팅된 분할의 결과 세트의 현재 커서만 가리킵니다. 이미 정렬된 정렬 대상의 경우, 병합 정렬의 시간 복잡도는 O(n)에 불과하므로 성능 손실이 매우 작습니다.
- 단일 분할에만 해당하는 쿼리에 대한 추가 최적화. 단일 분할 쿼리 요청은 레코드의 정확성을 보장하기 위해 SQL을 재작성할 필요가 없으므로, ShardingSphere는 이 경우 SQL을 재작성하지 않아 대역폭을 절약합니다.