서비스 확장에 따라 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() 함수를 사용해서 확인 할 수 있다.
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인 것을 만들어 보겠다.
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")
}
프리즈마에서는 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 라이브러리와 혼합하여 함수를 만들어 주었다.
const users = await prisma.users.findMany({
where: {
id: {
gte: UUID_TO_BIN(id.toString())
}
},
orderBy: {
createdAt: "desc"
}
});
위와 같이 id(바이너리)를 range 기능을 사용해서 select하는 것을 생각 할수 있다.
하지만 prisma에서 BytesFilter가 gte lte를 지원하지 않는다.
const users = await prisma.$queryRaw`select * from users where id > UUID_TO_BIN(${uuid})`;
위와 같이 query를 사용하자.
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 메소드를 사용하는게 조금 더 간단하고 보기 좋지 않았나 싶다.