Spring/ETC
@Tsid 커스텀 생성 후 적용하기 (수정)
YoonJong
2024. 6. 3. 13:11
728x90
사용이유
- 대규모 분산 시스템에서의 병렬처리 가능 및 고유성 보장
- 8byte 로 공간 효율성 향상
- 타임스탬프 값을 비트 가장 앞에 배치하여 정렬 순서 보장되어 데이터 삽입, 조회 시 성능 향상
As-Is
- 기존 @GeneratedValue(strategy = GenerationType.IDENTITY) 사용
- 동시성 문제 발생
- 국가별 프로젝트의 모든 테이블을 하나의 테이블로 관리 시, ID 중복 발생
To-Be
- TSID 를 이용하여 분산 환경에서 PK 고유 생성 보장.
- 8bite 로 공간 효율
- 순차적으로 증가(timestamp) 정렬 보장하여 성능 효율
- UUID 생성 또한 고려하였지만, 선택 X
- 36자리 문자열로 구성되어 있어 공간 효율성 문제
- 데이터 삽입, 조회 시 성능 문제 (인덱스 비효율)
Gradle 의존성 추가
// https://mvnrepository.com/artifact/com.github.f4b6a3/tsid-creator
implementation group: 'com.github.f4b6a3', name: 'tsid-creator', version: '5.2.6'
Entity 클래스
@Entity
@Table(name = "member")
@NoArgsConstructor
public class MemberEntity {
@Id
@RimanTsid
private Long id;
}
@RimanTsid 커스텀 어노테이션
@IdGeneratorType(RimanTsidGenerator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface RimanTsid {
Class<? extends Supplier<TsidFactory>> value() default FactorySupplierCustom.class;
@Slf4j
public static class FactorySupplierCustom implements Supplier<TsidFactory> {
private static int nodeBits = Integer.parseInt(System.getenv("ENV_NODE_BITS"));
private static String clock = System.getenv("ENV_TZ");
public static final FactorySupplierCustom INSTANCE = new FactorySupplierCustom();
private final TsidFactory tsidFactory;
public FactorySupplierCustom() {
this.tsidFactory = TsidFactory.builder()
.withNodeBits(nodeBits)
.withClock(Clock.system(ZoneId.of(clock)))
.withRandomFunction(() -> ThreadLocalRandom.current().nextInt())
.build();
}
@Override
public TsidFactory get() {
return this.tsidFactory;
}
}
}
RimanTsidGenerator 클래스
public class RimanTsidGenerator implements IdentifierGenerator {
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) {
RimanTsid.FactorySupplierCustom factorySupplier = RimanTsid.FactorySupplierCustom.INSTANCE;
return factorySupplier.get().create().toLong();
}
}
테스트 진행
- 테스트 환경
- 로컬 테스트 진행 ( 1ms 당 스레드가 접근하는 개수는 랜덤 )
- 1000 개의 스레드가 동시에 접근
@Test
@DisplayName("멀티스레드 환경에서도 싱글톤으로 생성된다.")
void multiThreadSingleTonTest() throws InterruptedException {
Set<Object> setSingleTonList = ConcurrentHashMap.newKeySet();
int threadCount = 1000; // 병렬로 실행할 스레드 수
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.execute(() -> {
setSingleTonList.add(FactorySupplierCustom.INSTANCE.hashCode());
});
}
// 스레드 풀 종료 대기
executorService.shutdown();
executorService.awaitTermination(10, TimeUnit.SECONDS);
assertEquals(setSingleTonList.size(), 1);
}
@RepeatedTest(50)
@DisplayName("[Custom @RimanTsid Test] 스레드 n개가 동시에 접근할 경우에도 ID가 중복되지 않는다.")
void multiThreadTest2() throws InterruptedException {
Set<Long> setIdList = ConcurrentHashMap.newKeySet();
int threadCount = 1000; // 싱글 스레드 요청 개수
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.execute(() -> {
long customTsid = FactorySupplierCustom.INSTANCE.get().create().toLong();
setIdList.add(customTsid);
});
}
// 스레드 풀 종료 대기
executorService.shutdown();
executorService.awaitTermination(10, TimeUnit.SECONDS);
assertEquals(setIdList.size(), threadCount);
}
@RepeatedTest(50)
@DisplayName("[Custom @RimanTsid Generate Entity Test] 스레드 n개가 동시에 접근할 경우에도 ID가 중복되지 않는다.")
void multiThreadTest() throws InterruptedException {
Set<Long> setIdList = ConcurrentHashMap.newKeySet();
int threadCount = 1000; // 병렬로 실행할 스레드 수
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.execute(() -> {
MemberEntity memberEntity = new MemberEntity();
memberRepository.save(memberEntity);
setIdList.add(memberEntity.getId());
});
}
// 스레드 풀 종료 대기
executorService.shutdown();
executorService.awaitTermination(30, TimeUnit.SECONDS);
System.out.println("setList.size() = " + setIdList.size());
assertEquals(setIdList.size(), threadCount);
}
@Test
@DisplayName("TsidFactory 커스텀 시, WithNodeBits 설정이 정상 적용된다.")
void multiThreadTest3() throws InterruptedException {
Set<Long> setList = ConcurrentHashMap.newKeySet();
Map<Long, Integer> timestampCounts = new ConcurrentHashMap<>();
int threadCount = 1000; // 싱글 스레드 요청 개수
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.execute(() -> {
long customTsid = FactorySupplierCustom.INSTANCE.get().create().toLong();
setList.add(customTsid);
long timestamp = customTsid >> 22; // 상위 42bits 추출 - TSID 는 64bits
timestampCounts.merge(timestamp, 1, Integer::sum);
});
}
// 스레드 풀 종료 대기
executorService.shutdown();
executorService.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("setList.size() = " + setList.size());
assertEquals(setList.size(), threadCount);
// DateTimeFormatter를 사용하여 타임스탬프를 보기 좋게 변환
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
.withZone(ZoneId.systemDefault());
// 타임스탬프가 동일한 경우 출력
timestampCounts.forEach((timestamp, count) -> {
if (count > 0) {
String formattedTimestamp = formatter.format(Instant.ofEpochMilli(timestamp));
System.out.println("Timestamp: " + formattedTimestamp + ", Count: " + count);
}
});
}
728x90