주문 번호 생성에 대해서

업데이트:


| 주문번호에서의 요구 조건은 무엇일까?

  • 업무효율성 향상을 위해주문에 대한 대략적인 정보를 담고있어야 합니다
  • 고객실수를 방지하기 위해 길이가 고정되어있어야 합니다.
  • 길이가 최대한 짧으면 좋습니다.
  • 주문별로 고유한 값이어야 합니다.


| 다른 회사에서 주문번호는 어떻게 생성할까?

네이버 페이

숫자로만 구성 된 17글자의 주문번호

쿠팡

숫자로만 구성 된 13글자의 주문번호


| 주문번호에 담을 수 있는 것들은 뭐가 있을까?

  • 상품 분류코드
  • 혜택 정보
  • 환불 절차 코드
  • 위탁 상품 유무 코드
  • 주문번호를 잘못 소통했을때 첫글자와 마지막 숫자를 더한 값을 주문코드 마지막에 checksum 추가


| 하루에 주문양에 따라 주문번호를 정할 수 있습니다.

no규칙길이경우의 수Note
▼ 숫자만의 구성
1연월일(YYMMDD) + 랜덤숫자 3자리<br>예 : 2018111119자리하루당 1,000개🟢 적정값이
2연월일(YYMMDD) + 랜덤숫자 4자리<br>예 : 201811111110자리하루당 10,000개🟢 적정값이
3연월일(YYMMDD) + 랜덤숫자 5자리<br>예 : 2018111111111자리하루당 100,000개🟢 적정값이
4연월일(YYMMDD) + 랜덤숫자 6자리<br>예 : 20181111111112자리하루당 1,000,000개🟡 조금 긴 값이
5연월일(YYMMDD) + 랜덤숫자 7자리<br>예 : 201811111111113자리하루당 10,000,000개🔴 긴 값이
6연월일(YYMMDD) + 랜덤숫자 8자리<br>예 : 2018111111111114자리하루당 100,000,000개🔴 긴 값이
7연월일(YYMMDD) + 랜덤숫자 9자리<br>예 : 20181111111111115자리하루당 1,000,000,000개🔴 긴 값이
▼ 영문과 숫자의 무작위 제한된 영어+숫자로 구성
8연월일(YYMMDD) + '규칙'영문자 3자리<br>예 : 201811A1B9자리하루당 27,000개🟢 적정값이
9연월일(YYMMDD) + '규칙'영문자 4자리<br>예 : 201811A1B210자리하루당 810,000개🟢 적정값이
10연월일(YYMMDD) + '규칙'영문자 5자리<br>예 : 201811A1B2C11자리하루당 24,300,000개🟢 적정값이
11연월일(YYMMDD) + '규칙'영문자 6자리<br>예 : 201811A1B2C312자리하루당 729,000,000개🟡 조금 긴 값이
12연월일(YYMMDD) + '규칙'영문자 7자리<br>예 : 201811A1B2C3D13자리하루당 21,870,000,000개🔴 긴 값이
13연월일(YYMMDD) + '규칙'영문자 8자리<br>예 : 201811A1B2C3D13자리하루당 656,100,000,000개🔴 긴 값이
14연월일(YYMMDD) + '규칙'영문자 9자리<br>예 : 201811A1B2C3D13자리하루당 19,683,000,000,000개🔴 긴 값이
▼ 영어+숫자로 구성
15연월일(YYMMDD) + 모든 영문자 3자리<br>예 : 201811A1B9자리하루당 46,656개🟢 적정값이
16연월일(YYMMDD) + 모든 영문자 4자리<br>예 : 201811A1B210자리하루당 1,679,616개🟢 적정값이
17연월일(YYMMDD) + 모든 영문자 5자리<br>예 : 201811A1B2C11자리하루당 60,466,176개🟢 적정값이
18연월일(YYMMDD) + 모든 영문자 6자리<br>예 : 201811A1B2C312자리하루당 2,176,782,336개🟡 조금 긴 값이
19연월일(YYMMDD) + 모든 영문자 7자리<br>예 : 201811A1B2C3D13자리하루당 78,364,164,096개🔴 긴 값이
20연월일(YYMMDD) + 모든 영문자 8자리<br>예 : 201811A1B2C3D414자리하루당 2,821,109,907,456개🔴 긴 값이
21연월일(YYMMDD) + 모든 영문자 9자리<br>예 : 201811A1B2C3D4E15자리하루당 101,559,956,668,416개🔴 긴 값이
  1. 2 : E와 발음이 비슷
  2. 5 : O와 발음이 비슷
  3. E : 2와 발음이 비슷
  4. I : L과 모양이 비슷
  5. L : I와 모양이 비슷
  6. O : 5와 발음이 비슷 -> 해당 문자와 숫자는 제외시키고 만든 테이블입니다.

해당 테이블에서 보이는 것처럼 트래픽이나 유저 수에 따라서 주문 번호의 길이를 정할 수 있는데, 12글자로 정의를 해보겠습니다.


주문에서 UUID를 만드려면 동시성 때문에 중복이 될 수도 있다.

  • 여러 서버나 프로세스가 동시에 번호를 생성할 때 중복이 발생할 수 있습니다. 이는 데이터 일관성과 무결성을 해칠 수 있습니다.

| 스노우 플레이크란?

Twitter의 Snowflake 알고리즘은 분산 시스템에서 유일한 ID를 생성하는 데 매우 효과적인 방법입니다.

1.구조
64비트 정수로 구성됩니다.

2.구성 요소

  • 타임스탬프 (41비트): 밀리초 단위의 시간 정보
  • 데이터센터 ID (5비트): 최대 32개의 데이터센터 식별
  • 워커 ID (5비트): 각 데이터센터 내 최대 32개의 워커 식별
  • 시퀀스 번호 (12비트): 같은 밀리초 내에서 4096개의 고유 ID 생성 가능

3.장점:

  • 시간에 따라 정렬 가능
  • 분산 시스템에 적합
  • 높은 성능
  • ID의 의미를 쉽게 해석 가능


4.작동 방식:

  • 현재 시간을 기반으로 ID의 첫 부분을 생성
  • 데이터센터와 워커 정보를 추가
  • 같은 밀리초 내에서는 시퀀스 번호를 증가시켜 고유성 보장


5.사용 사례:

대규모 분산 시스템에서의 고유 ID 생성 데이터베이스 샤딩을 위한 키 생성 마이크로서비스 아키텍처에서의 트랜잭션 ID 생성

만들어보기

1️⃣    트위터 snowflake 이용

  • 날짜 부분 (5자리):

년도의 마지막 두 자리 (2자리) 해당 년도의 일 수 (3자리, 001-366)

  • 유니크 부분 (7자리): 스노우플레이크 ID의 하위 7자리

  • YY(년도 2자리)DDD(해당 년도의 일 수 3자리) 형식으로 반환

    private String generateCompressedDatePart(long snowflakeId) {
          // 스노우플레이크 ID에서 타임스탬프 추출
          long timestamp = (snowflakeId >> 22) + SnowflakeIdGenerator.TWEPOCH;
          LocalDate date = Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDate();
            
          // 년도의 마지막 두 자리
          int year = date.getYear() % 100;
          // 해당 년도의 일 수 (1-366)
          int dayOfYear = date.getDayOfYear();
            
          // YY(년도 2자리)DDD(해당 년도의 일 수 3자리) 형식으로 반환
          return String.format("%02d%03d", year, dayOfYear);
      }
    

import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;

public class OrderNumberGenerator {
    private final SnowflakeIdGenerator snowflake;

    public OrderNumberGenerator(long datacenterId, long workerId) {
        this.snowflake = new SnowflakeIdGenerator(datacenterId, workerId);
    }

    public String generateOrderNumber() {
        long snowflakeId = snowflake.nextId();
        // 날짜 부분 (5자리)과 고유 부분 (7자리)를 조합하여 12자리 주문번호 생성
        String datePart = generateCompressedDatePart(snowflakeId);
        String uniquePart = String.format("%07d", snowflakeId % 10000000L);
        return datePart + uniquePart;
    }

    private String generateCompressedDatePart(long snowflakeId) {
        // 스노우플레이크 ID에서 타임스탬프 추출
        long timestamp = (snowflakeId >> 22) + SnowflakeIdGenerator.TWEPOCH;
        LocalDate date = Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDate();
        
        // 년도의 마지막 두 자리
        int year = date.getYear() % 100;
        // 해당 년도의 일 수 (1-366)
        int dayOfYear = date.getDayOfYear();
        
        // YY(년도 2자리)DDD(해당 년도의 일 수 3자리) 형식으로 반환
        return String.format("%02d%03d", year, dayOfYear);
    }

    // 스노우플레이크 ID 생성기 내부 클래스
    private static class SnowflakeIdGenerator {
        // 2010-11-04 09:42:54.657 UTC - 스노우플레이크 시작 시간
        private static final long TWEPOCH = 1288834974657L;
        
        // 각 부분의 비트 수 정의
        private static final long WORKER_ID_BITS = 5L;
        private static final long DATACENTER_ID_BITS = 5L;
        private static final long SEQUENCE_BITS = 12L;
        
        // 최대값 계산
        private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
        private static final long MAX_DATACENTER_ID = -1L ^ (-1L << DATACENTER_ID_BITS);
        
        // 비트 시프트 값 계산
        private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
        private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
        private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
        
        private static final long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);
        
        private long workerId;
        private long datacenterId;
        private long sequence = 0L;
        private long lastTimestamp = -1L;

        public SnowflakeIdGenerator(long datacenterId, long workerId) {
            // 데이터센터 ID와 워커 ID의 유효성 검사
            if (workerId > MAX_WORKER_ID || workerId < 0) {
                throw new IllegalArgumentException("Worker ID can't be greater than " + MAX_WORKER_ID + " or less than 0");
            }
            if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
                throw new IllegalArgumentException("Datacenter ID can't be greater than " + MAX_DATACENTER_ID + " or less than 0");
            }
            this.workerId = workerId;
            this.datacenterId = datacenterId;
        }

        // 다음 ID 생성
        public synchronized long nextId() {
            long timestamp = timeGen();
            
            // 시계가 뒤로 갔는지 체크
            if (timestamp < lastTimestamp) {
                throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
            }
            
            // 같은 밀리초 내에서의 처리
            if (lastTimestamp == timestamp) {
                sequence = (sequence + 1) & SEQUENCE_MASK;
                if (sequence == 0) {
                    // 같은 밀리초 내에서 시퀀스를 모두 사용했으면 다음 밀리초까지 대기
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0L;
            }

            lastTimestamp = timestamp;

            // ID 조합: 타임스탬프 + 데이터센터 ID + 워커 ID + 시퀀스
            return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT) |
                    (datacenterId << DATACENTER_ID_SHIFT) |
                    (workerId << WORKER_ID_SHIFT) |
                    sequence;
        }

        // 다음 밀리초까지 대기
        private long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }

        // 현재 시간을 밀리초로 반환
        private long timeGen() {
            return System.currentTimeMillis();
        }
    }

    public static void main(String[] args) {
        OrderNumberGenerator generator = new OrderNumberGenerator(1, 1);
        String orderNumber = generator.generateOrderNumber();
        System.out.println("Generated Order Number: " + orderNumber);
    }
}


2️⃣     날짜 + 시간 + 상품 카테고리 + 시퀀스


public class OrderNumberGenerator {
    private static int sequence = 0;

    public synchronized String generateOrderNumber(int categoryCode) {
        LocalDateTime now = LocalDateTime.now();
        int dayOfYear = now.getDayOfYear();
        int secondOfDay = now.getHour() * 3600 + now.getMinute() * 60 + now.getSecond();
        
        sequence = (sequence + 1) % 100; // 0-99 사이의 시퀀스
        
        return String.format("%03d%05d%02d%02d", dayOfYear, secondOfDay, categoryCode, sequence);
    }
}


| 날짜를 포함하면 좋은 점들

  • 데이터 분산 및 중복 방지:
    날짜를 포함하면 시간에 따라 자연스럽게 데이터를 분산시킬 수 있습니다. 특정 파티션이나 샤드에 데이터 몰리는 것 방지 그리고 날짜로 인하여 생성될때 중복이 방지될 수 있습니다.

  • 데이터 관리 용이성:
    오래된 데이터를 쉽게 아카이브(압축해서 보관)하거나 삭제할 수 있습니다.
    예를 들어, 1년 이상 된 데이터를 별도 저장소로 이동하거나 삭제하는 작업이 간편해집니다.

| 파티션 및 샤딩?

대규모 데이터베이스를 관리하고 성능을 최적화하기 위한 기술.

1. 파티셔닝 (Partitioning)

파티셔닝은 단일 데이터베이스 내에서 테이블을 여러 부분으로 나누는 기술입니다.

주요 특징:
  • 논리적으로는 하나의 테이블이지만 물리적으로는 여러 부분으로 나뉩니다.
  • 각 파티션은 같은 스키마를 공유하지만 다른 저장 위치를 가질 수 있습니다.
  • 주로 대용량 테이블의 성능을 향상시키고 관리를 용이하게 하기 위해 사용됩니다.
파티셔닝을 사용할 경우
  • 테이블이 매우 크고 쿼리 성능이 저하될 때
  • 특정 열을 기준으로 데이터를 분리하고 싶을 때 (예: 날짜, 지역 등)
  • 데이터 유지 관리(아카이빙, 삭제 등)를 효율적으로 하고 싶을 때
2. 샤딩 (Partitioning)

샤딩은 데이터를 여러 데이터베이스 서버에 분산시키는 기술입니다.

주요 특징:
  • 데이터를 여러 물리적 서버에 분산시킵니다.
  • 각 샤드(조각)는 독립적인 데이터베이스처럼 작동할 수 있습니다.
  • 수평적 확장을 가능하게 하여 시스템의 처리량을 증가시킵니다.
샤딩을 사용할 경우
  • 단일 데이터베이스 서버의 용량이나 성능 한계에 도달했을 때
  • 지리적으로 분산된 사용자를 지원해야 할 때
  • 고가용성과 내결함성을 높이고 싶을 때

참고사이트

https://velog.io/@dochis/CS%EC%B2%98%EB%A6%AC%EC%97%90-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%EC%A3%BC%EB%AC%B8%EB%B2%88%ED%98%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%A3%BC%EB%AC%B8%EB%B2%88%ED%98%B8-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98

https://velog.io/@disdos0928/DB%EC%97%90-%EC%A0%91%EA%B7%BC%ED%95%98%EC%A7%80-%EC%95%8A%EC%9C%BC%EB%A9%B4%EC%84%9C-%EC%A3%BC%EB%AC%B8%EB%B2%88%ED%98%B8%EB%A5%BC-%EC%83%9D%EC%84%B1%ED%95%B4%EC%95%BC%ED%95%9C%EB%8B%A4


https://popcorn-overflow.tistory.com/10

댓글남기기