분산 시스템을 위한 uuid

00_8_3·2023년 10월 11일

서비스 확장에 따라 db도 확장을 하게 되는데 스케일업만을 하기는 힘들다.
때문에 대규모 분산 시스템에서 db를 클러스터링 하게 되는데 (스케일아웃)
여러개의 db로 나뉘어져 있는 같은 이름의 테이블에 integer의 auto_increment Primary key를 사용하는 경우
중복되는 id로 인해 문제가 발생할 수 있다.

이 때 uuid를 사용하면 중복문제와 더불어 인덱싱 문제도 해결할 수 있는데
(모든 버전의 uuid가 정렬이 되는것은 아니다.)

장단점이 있으니 확인해보길 바란다.

참고: https://www.mimul.com/blog/id-generation-in-mysql/
https://www.linkedin.com/pulse/choosing-right-id-strategy-auto-increment-uuid-ulid-valluru/

mysql 8 버전 이후 uuid와 관련된 여러 함수가 지원되기 시작했는데
그 중 UUID_TO_BIN과 BIN_TO_UUID가 대표적이다.

UUID_TO_BIN 함수는 문자열을 binary(16)으로 변환 해준다.

CREATE TABLE test_uuid (
    uuid VARBINARY(16) PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

위와 같은 테이블을 생성 후

Insert Into test_uuid (uuid, name) values (UUID_TO_BIN("this is uuid"), 'kim');
을 넣어주면 된다.
저장된 값을 확인해보면 바이너리 타입이라 사람이 읽을수 없게 되어있다.
반대로 BIN_TO_UUID() 함수를 사용해서 확인 할 수 있다.

uuidv7

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          unix_ts_ms                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          unix_ts_ms           |  ver  |        counter        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        counter                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             rand                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

uuidv7의 bit layout이다.
가장 앞 자리(48bit)에 timestamp가 들어가 있어 시간의 흐름을 추적이 가능하다.

이 uuidv7과 prisma orm을 사용하여 mysql에 id가 uuid인 것을 만들어 보겠다.

실습

prisma

generator client {
    provider      = "prisma-client-js"
    binaryTargets = ["native"]
}

datasource db {
    provider = "mysql"
    url      = env("DATABASE_URL")
}

model Users {
    id        Bytes    @id @db.VarBinary(16)
    name      String   @db.VarChar(100)
    createdAt DateTime @default(now()) @map("created_at")

    @@map("users")
}
  • 1

프리즈마에서는 binary 타입은 buffer로 처리된다.

문자열 uuid를 buffer로 바꿔줘야 한다.

import { uuidv7obj, UUID } from "uuidv7";

const UUID_TO_BIN = (uuid: string) => Buffer.from(UUID.parse(uuid).bytes)
const BIN_TO_UUID = (buf: Buffer) => UUID.ofInner(buf).toString()

const id = uuidv7obj();        
        const users = await prisma.users.create({
            data: {
                id: UUID_TO_BIN(id.toString()),
                name: "kim"
            }
        })

uuidv7 라이브러리와 혼합하여 함수를 만들어 주었다.

  • 2
const users = await prisma.users.findMany({
            where: {
                id: {
                    gte: UUID_TO_BIN(id.toString())
                }
            },
            orderBy: {
                createdAt: "desc"
            }
        });

위와 같이 id(바이너리)를 range 기능을 사용해서 select하는 것을 생각 할수 있다.
하지만 prisma에서 BytesFiltergte lte를 지원하지 않는다.

참고 : https://github.com/prisma/prisma/discussions/21444

const users = await prisma.$queryRaw`select * from users where id > UUID_TO_BIN(${uuid})`;

위와 같이 query를 사용하자.

uuid_to_bin, bin_to_uuid

import { UUID } from "uuidv7";

function UUID_TO_BIN(uuidString: string, swapFlag = 0) {
    const buffer = Buffer.from(UUID.parse(uuidString).bytes);
    
    if (swapFlag === 1) {
        // swap time-low (first 4 bytes) and time-high parts (bytes 7 and 8)
        for (let i = 0; i < 4; i++) {
            const temp = buffer[i];
            buffer[i] = buffer[8 + i];
            buffer[8 + i] = temp;
        }
    }

    return buffer;
}

function BIN_TO_UUID(buffer: Buffer, swapFlag = 0) {
    if (swapFlag === 1) {
        // swap time-low and time-high parts back to their original positions
        for (let i = 0; i < 4; i++) {
            const temp = buffer[i];
            buffer[i] = buffer[8 + i];
            buffer[8 + i] = temp;
        }
    }

    return UUID.ofInner(buffer).toString();
}

// 사용 예제
const uuidString = '018b1d7a-f3a1-7e2d-80e4-ef5804f7733a';
const binary = UUID_TO_BIN(uuidString, 0);
console.log(binary);  // 바이너리 형식의 버퍼가 출력됩니다.

const convertedUuidString = BIN_TO_UUID(Buffer.from(binary), 0);
console.log(convertedUuidString);  // 원래의 UUID 문자열이 출력됩니다: 'f47ac10b-58cc-4372-a567-0e02b2c3d479'


function uuidV7TimestampToUnixMs(uuidString: string) {
    const buffer = Buffer.from(UUID.parse(uuidString).bytes);
    // const buffer = Buffer.from(parse(uuidString));
    
    // 처음 8 바이트에서 unix_ts_ms 추출
    const high = buffer.readUInt32BE(0);
    const low = buffer.readUInt16BE(4);
    
    const unixMs = (high * 0x10000) + low;
    
    return new Date(unixMs);
  }
  
//   const uuidString = 'YOUR_UUIDV7_STRING_HERE';
  console.log(uuidV7TimestampToUnixMs(uuidString));
  console.log(new Date())
  • uuidV7TimestampToUnixMs 함수는 맨앞의 48bit에서 timestamp로 변환하는 함수이다.
    48 = 32 + 16
    readUInt32BE 함수는 총 비트에서 32bit 단위로 나누어 offset 0에서 32bit를 가져오고
    readUInt16BE 함수는 총 비트에서 16 bit 단위로 나누어 offset 4에서 16bit를 가져와
    총 48bit로 유닉스 타임을 Date 객체로 변환한다.

  • BE는 빅 엔디안이다.

swap_flag는 문서에서 확인해보기 바란다.
https://dev.mysql.com/doc/refman/8.0/en/miscellaneous-functions.html#function_uuid-to-bin

결론

uuid v7을 사용해서 mysql prisma를 사용해서 저장하고 조회하는 방법에 대해 알아보았다.
uuid_to_bin 또는 byte filter에 대한 prisma에 미지원으로 인해 코드로 직접 구현도 해보았는데

queryRaw를 사용해서 직접 mysql 메소드를 사용하는게 조금 더 간단하고 보기 좋지 않았나 싶다.

0개의 댓글