Time-Sorted Unique Identifiers (TSID).

 - https://github.com/f4b6a3/tsid-creator

NanoID

 - https://www.daleseo.com/nanoid/

snowflake

 - ex) https://github.com/callicoder/java-snowflake

instagram
 - 개인적으로 과거에 해당 방식으로 종종 개발
 - https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c

기존에는 Sqlyog 유료 버전을 사용해서 GUI로 편하게 사용했었는데, datagrip에는 GUI가 없는 듯해서 메모 목적으로 정리합니다.

 

-- Profiling 활성화 확인: 0이면 비 활성화
select @@profiling;

-- 활성화
set profiling=1;

-- profile 목록 보기
show profiles;

-- profile 목록에서 실행했던 쿼리 26의 내용 보기
-- 26 쿼리는 커버링 인덱스로 변경한 쿼리
select state, format(duration, 6) as duration
from information_schema.PROFILING
where QUERY_ID=26 order by seq;


-- 샘플로 커버링 인덱스로 튜닝했을 때의 효과를 확인하는 쿼리입니다.

-- 커버링 인덱스 쿼리
SELECT m.* FROM  (
-- 데이터 조회(드라이빙)
SELECT	id
	FROM	테스트 테이블
	WHERE  date_at  >=  '2024-04-01' AND date_at  <  ADDDATE('2024-04-30', 1)	
	ORDER BY id DESC
	LIMIT 720000, 15
) AS d, 테스트 테이블 AS m
WHERE m.id=d.id
;


-- 기존 쿼리: 커버링 인덱스 X
SELECT	컬럼들 블라블라
FROM	테스트 테이블
WHERE  date_at  >=  '2024-04-01' AND date_at  <  ADDDATE('2024-04-30', 1)

ORDER BY id DESC
LIMIT 720000, 15
;


-- profile 목록에서 실행했던 쿼리 50의 내용 보기
-- 50 쿼리는 커버링 인덱스 미 사용 쿼리
select state, format(duration, 6) as duration
from information_schema.PROFILING
where QUERY_ID=50 order by seq;

 

커버링 인덱스로 튜닝전 쿼리의 Profile

 

튜닝 후 쿼리의 Profile

필요 라이브러리

<!-- https://mvnrepository.com/artifact/commons-validator/commons-validator -->
        <dependency>
            <groupId>commons-validator</groupId>
            <artifactId>commons-validator</artifactId>
            <version>1.8.0</version>
        </dependency>


코드(java ver)

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.EmailValidator;

public class CheckEmailDomainSample {

  /** 알고있는 유효한 이메일 호스트명  */
  private static final Set<String> CACHED_VALID_HOSTNAME = Sets.newLinkedHashSet(Arrays.asList("gmail.com", "naver.com"));
  private static final String targetFilePath = "검사할 도메인이 저장된 파일 경로(개행 구분)";

  private static final EmailValidator emailValid = EmailValidator.getInstance();

  public static void main(String[] args) throws IOException {

 
    List<String> emailDomainList = FileUtils.readLines(new File(targetFilePath));

    for (String hostname : emailDomainList) {
      System.out.println(String.format("%s : %s'", hostname, isValidHostname(hostname)));
    }
  }


  public static boolean isValidHostname(String hostname) {

    try {

      if (CACHED_VALID_HOSTNAME.contains(StringUtils.lowerCase(hostname))) {
        return true;
      }

      final String mxStr = "MX";
      Hashtable<String, String> hashtable = new Hashtable<>();

      hashtable.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
      hashtable.put("com.sun.jndi.dns.timeout.initial", "5000"); /* quite short... too short? */
      hashtable.put("com.sun.jndi.dns.timeout.retries", "1");

      DirContext ictx = new InitialDirContext(hashtable);

      Attributes attrs = ictx.getAttributes(hostname, new String[]{mxStr});
      Attribute attr = attrs.get(mxStr);

      if (attr == null || attr.size() == 0) {
        return false; //없음
      }

      NamingEnumeration e = attr.getAll();

      while (e.hasMore()) {
        String mxs = String.valueOf(e.next());
        String[] mx = mxs.split("\\s+");

        for (String mxString : mx) {
          if (StringUtils.endsWith(mxString, ".")) {
            return true;
          }
        }
      }

    } catch (NamingException ne) {
      return false;
    }

    return false;
  }

}
  1. 가끔 분산 DB, DB통합 등을 고려해서 auto inc가 아니라 uuid 등으로 PK를 생성해야하는 경우가 존재
  2. 이때 몇가지를 고려해서 코드레벨에서 준비해야하는게 있었는데, mysql 8기준으로 이제는 native function에서 지원이 잘됨

 

관련해서 잘 정리된 글이 있어서 메모 목적으로 링크

핵심 부분은 'UUID_TO_BIN(UUID(),1)' 를 이용해서 uuid ver 1으로 생성된 uuid에서, 시간 관련된 필드를 조정 & 저장 용량을 줄이기 위해서 BINARY(16)로 저장
 - Clustered Index를 사용하는 Mysql이 index 조정 오버헤드를 줄이기 위해서

 

-- 테이블 생성 : UUID_TO_BIN(UUID(),1)
create table tb_test3 ( 
uuid binary(16) default (UUID_TO_BIN(UUID(),1)) primary key, 
first_name varchar(15), emp_no int unsigned)
engine=innodb default charset=utf8mb4 collate=utf8mb4_general_ci;


-- 데이터 입력
insert into tb_test3(first_name,emp_no) values ('Georgi',10001);
insert into tb_test3(first_name,emp_no) values ('Mary',10002);
insert into tb_test3(first_name,emp_no) values ('Saniya',10003);
insert into tb_test3(first_name,emp_no) values ('Sumant',10004);
insert into tb_test3(first_name,emp_no) values ('Duangkaew',10005);
insert into tb_test3(first_name,emp_no) values ('Patricio',10006);


-- 데이터 조회
mysql> select hex(uuid),first_name,emp_no
from tb_test3;
+----------------------------------+------------+--------+
| hex(uuid)                        | first_name | emp_no |
+----------------------------------+------------+--------+
| 11ED4F5A366CD3C3A20708002722A50F | Georgi     |  10001 |
| 11ED4F5A89BD4D38A20708002722A50F | Mary       |  10002 |
| 11ED4F5A8ED8C0C8A20708002722A50F | Saniya     |  10003 |
| 11ED4F5C16AAD10DA20708002722A50F | Sumant     |  10004 |
| 11ED4F5D5B56C1E8A20708002722A50F | Duangkaew  |  10005 |
| 11ED4F5E2CDE8B13A20708002722A50F | Patricio   |  10006 |
+----------------------------------+------------+--------+

좋은 글 참고 링크

 - 상세 사용 방법 등은 코드는 추후 해당 글에 추가 예정

spring boot 3.2 릴리즈 일정 확인 중에 알게된 링크

https://calendar.spring.io/

메모 목적으로 작성한 글이라서 생략된 부분이 많습니다.

 

  1.  목적/배경
    1. 현재 JDK 21의 VirtualThread기반으로 작업 중
    2. webflux를 이용해서 논블럭을 작성할 필요가 없어짐
      1. 유지보수, 읽기좋은 소스 등의 관점에서 기존 동기방식 스타일 코드 작성이 유리
      2. IO 블럭킹에 대한 성능 문제는 VirtualThread가 blocking 코드를 만나면 잠시 대기/큐잉 등 의 형태로 커버됨
    3. 다만, 통신용 모듈이 webclient로 기존에 작성되어 있음
      1. spring 디펜더시 문제 등의 이유로 23년 11월 23일에 spring boot 3.2가 릴리즈될때 rest client가 포함되서 해결 예정
      2. 지금 당장 webclient로 작성된 코드도 필요
    4. webclient를 block()으로 호출해서 임시 사용

 

------

샘플 소스

/**
 * Webclient로 외부 API를 호출
 *  - block()을 사용하여 결과를 받아옴
 *  - 500 에러가 발생하면 재시도
 *
 * @author 
 */
@Slf4j
public class WebClientBlockRetriveRequestSample {

  public static void main(String[] args) {

    final String reqUri = "http://localhost:87/delay/2"; //테스트 대상 URL
    final Duration timeoutDuration = Duration.ofSeconds(1); //Timeout

    String apiResponse = null;
    try {
      apiResponse = getRequestExcute(reqUri, timeoutDuration); //요청 실행
    } catch (BadWebClientRequestException e) {
      log.error("BadWebClientRequestException발생\n\n\t{}", e.getMessage(), e);
      throw e;
    } catch (WebClientTimeoutException te) {
      log.error("WebClientTimeoutException발생\n\n\t{}", te.getMessage(), te);
    }

    log.info("apiResponse: {}", apiResponse);

  }

  /**
   * 외부 HTTP 요청 실행
   *
   * @param reqUri
   * @param timeoutDuration
   * @return
   */
  public static String getRequestExcute(String reqUri, Duration timeoutDuration) {

    WebClient webClient = WebClient.builder()
        //.defaultHeader("Content-Type", "application/json")
        .build();

    String apiResponse = webClient.mutate().build().get()
        .uri(reqUri)
        .retrieve()
        .onStatus(httpStatus -> httpStatus.is4xxClientError() || httpStatus.is5xxServerError(),
            clientResponse -> handleErrorResponse(reqUri, clientResponse)
        ).bodyToMono(String.class)
        .timeout(timeoutDuration)
        .doOnError(throwable -> {

          if (throwable instanceof java.util.concurrent.TimeoutException) { //타임아웃 발생한 경우 핸들링을 위해서 예외 클래스 변경 처리
            log.error("TimeoutException: " + throwable.getMessage());
            throw new WebClientTimeoutException(String.format("Steam API no response whthin %s(millis)", timeoutDuration.toMillis()));
          }

        })
        .retryWhen(Retry.backoff(2, Duration.ofSeconds(2)).maxBackoff(Duration.ofSeconds(3)).jitter(0.5)
            .filter(throwable -> throwable instanceof WebClientNeedRetryException)) //특정 예외인 경우 재 시도
        .block(); //동기 방식으로 호출(virtual thread사용하기 때문에 문제 없음)

    return apiResponse;
  }


  public static Mono<? extends Throwable> handleErrorResponse(String uri, ClientResponse response) {

    if (response.statusCode().is4xxClientError()) {
      String errMsg = String.format("'%s' 4xx ERROR. statusCode: %s, response: %s, header: %s", uri, response.statusCode().value(), response.bodyToMono(String.class), response.headers().asHttpHeaders());
      log.error(errMsg);
      return Mono.error(new BadWebClientRequestException(response.statusCode().value(), errMsg));
    }

    if (response.statusCode().is5xxServerError()) { //5xx에러인 경우 재 시도 처리를 위해서 재 시도 필요 예외를 리턴
      String errMsg = String.format("'%s' 5xx ERROR. %s", uri, response.toString());
      log.error(errMsg);
      return Mono.error(new WebClientNeedRetryException(response.statusCode().value(), errMsg));
    }

    String errMsg = String.format("'%s' ERROR. statusCode: %s, response: %s, header: %s", uri, response.statusCode().value(), response.bodyToMono(String.class), response.headers().asHttpHeaders());
    log.error(errMsg);
    return Mono.error(new RuntimeException(errMsg));

  }

 

사용하는 커스텀 개발된 예외들

/**
 * 잘못된 파라미터로 요청시 발생하는 Exception
 *
 * @author 
 */
@Getter
public class BadWebClientRequestException extends RuntimeException {

  private static final long serialVersionUID = 2241080498857315158L;

  private final int statusCode;

  private String statusText;

  public BadWebClientRequestException(int statusCode) {
    super();
    this.statusCode = statusCode;
  }

  public BadWebClientRequestException(int statusCode, String msg) {
    super(msg);
    this.statusCode = statusCode;
  }

  public BadWebClientRequestException(int statusCode, String msg, String statusText) {
    super(msg);
    this.statusCode = statusCode;
    this.statusText = statusText;
  }
}

 

/**
 * Webclient로 호출 중 재 시도가 필요한 경우에 사용하는 Exception
 *
 * @author 
 */
@Getter
public class WebClientNeedRetryException extends RuntimeException {

  private static final long serialVersionUID = 3238789645114297396L;

  private final int statusCode;

  private String statusText;

  public WebClientNeedRetryException(int statusCode) {
    super();
    this.statusCode = statusCode;
  }

  public WebClientNeedRetryException(int statusCode, String msg) {
    super(msg);
    this.statusCode = statusCode;
  }

  public WebClientNeedRetryException(int statusCode, String msg, String statusText) {
    super(msg);
    this.statusCode = statusCode;
    this.statusText = statusText;
  }
#bash 쉘프롬프트 변경(필요시)
sudo su
echo 'PS1="[\u@\h \$PWD \D{%T}]\\$ "' >> /etc/bashrc && source /etc/bashrc


# Amazon Corretto JDK 21을 ~/apps 디렉토리 하위에 설치하는 명령어
# https://docs.aws.amazon.com/corretto/latest/corretto-21-ug/downloads-list.html 에서 다운로드 URL확이 ㄴ가능

# 디렉토리 생성(apps 디렉토리 하위에 생성 예정)
mkdir apps

# 다운로드
wget 'https://corretto.aws/downloads/latest/amazon-corretto-21-x64-linux-jdk.tar.gz' -O ~/apps/amazon-corretto-21-x64-linux-jdk.tar.gz


# 압축해제 및 삭제
cd ~/apps && tar -xzf amazon-corretto-21-x64-linux-jdk.tar.gz && rm -f amazon-corretto-21-x64-linux-jdk.tar.gz

# 심볼릭 링크 (필요시) 심볼릭 링크가 존재한다면, 삭제 후 재생성
cd ~/apps && rm jdk_21 && ln -s amazon-corretto-21.0.1.12.1-linux-x64 jdk_21

## DNS TTL 무제한 -> 10초로 수정 (어플리케이션마다 달라야 할 수 있음)
echo 'networkaddress.cache.ttl=10' >> ~/apps/jdk_21/conf/security/java.security

# 환경변수 및 디폴트 jdk 설정
echo 'export JAVA_21_HOME=~/apps/jdk_21' >> ~/.bashrc && echo 'export PATH=$JAVA_21_HOME/bin:$PATH' >> ~/.bashrc && source ~/.bashrc

# 기타 - jdk 버전 확인
$JAVA_HOME/bin/java -version
$JAVA_21_HOME/bin/java -version

+ Recent posts