문제 상황
실시간으로 베팅이 이루어지는 서비스이다. "만약 많은 사용자가 동시에 베팅을 진행할 경우, 동시성 문제가 일어나 데이터가 부정확해지는 것은 아닐까?" 고민을 하게 되었다.
구체적인 상황은 아래와 같다.
1. 베팅을 하면 카운트가 올라간다. (베팅 참여자, 베팅한 금액)
2. 카운트를 올리기 위해 데이터를 조회한다.
3. 조회한 데이터에 증가 연산을 수행한다.
4. 업데이트 된 데이터를 저장한다.
위 경우, 만약 두 명의 사용자가 데이터가 1인 시점에 접근해 각각 1씩 올려도 3이 아니라 2가 되는 동시성 문제가 발생하게 되는 것이다.
결론부터 이야기하면, 레디스와 레디스의 HINCRBY 명령어를 통해 원자성을 보장할 수 있었다.
HINCRBY 명령어와 가상의 테스트 시나리오를 통해, 어떻게 동시성 문제를 해결했는지 정리해보았다.
레디스의 특징과 Redis HINCRBY 명령
Redis는 단일 스레드 기반으로 동작하여 모든 명령어를 순차적으로 처리한다. 즉 한번에 하나의 요청을 처리한다.
이런 특성 덕분에 Redis는 많은 동시 요청을 처리할 때에도 원자성을 보장할 수 있다.
HINCRBY
명령어는 그 자체로 원자성을 가진다. 아래의 메커니즘이 하나의 원자성으로 실행된다.
1. 키 조회 및 변경
지정된 해시 키(hash key)와 필드(field)를 조회한다.
2. 증가 연산
조회한 값을 메모리에서 바로 증가 연산한다.
3. 변경사항 저장
증가된 값을 다시 지정된 필드에 저장한다.
4. 응답 반환
위 동작이 하나의 명령어로 처리되고, 레디스는 단일 스레드로 동작하기 때문에 다른 명령어가 끼어들거나 같은 키에 대해 동시에 작업하지 못한다.
그림으로 쉽게 표현해보면 위와 같다. 하나의 저장소에 동시 접근이 불가능하고 HINCRBY 명령이 원자적으로 수행된다.
동시성 테스트
설계
이것을 확인하기 위해서 실제 레디스에 접근해 테스트를 진행했다.
베팅 옵션 업데이트
async updateBetOption(roomId: string, option: string, betAmount: number) {
await Promise.all([
this.client.hincrby(`test:${roomId}:${option}`, 'currentBets', betAmount),
this.client.hincrby(`test:${roomId}:${option}`, 'participants', 1),
]);
}
베팅을 업데이트하는 메서드는 위와 같다. 이것을 promise all을 통해 병렬에 가깝게 호출하여 테스트를 진행했다.
Redis.concurrency.ts
async function run() {
... 레디스 초기화
const option = "option1"
const betAmount = 10;
const updatePromises:Promise<void>[] = [];
const updateCount = 1000000;
// 100만 번
for (let i = 0; i < updateCount; i++) {
updatePromises.push(redisManager.updateBetOption(roomID, option, betAmount));
}
await Promise.all(updatePromises);
const result = await redisManager.getChannelData(roomID);
console.log('Fetching channel data:', result);
const expectedBets = updateCount * betAmount;
const expectedParticipants = updateCount;
if (result && result[option]) {
const { currentBets, participants } = result[option];
console.log("Test Result:", {
expectedBets,
actualBets: parseInt(currentBets, 10),
expectedParticipants,
actualParticipants: parseInt(participants, 10),
});
if (
parseInt(currentBets, 10) === expectedBets &&
parseInt(participants, 10) === expectedParticipants
) {
console.log("테스트 통과");
console.log("");
} else {
console.error("테스트 실패: 동시성 문제");
}
}
... 레디스 삭제 및 종료
}
run().catch(console.error);
10만 번의 카운트를 통해 업데이트를 실행하고 결과를 확인했다.
기대 값은 expectBetAmount = 100,000*10
, expectParticipants = 100,000*1
이다
결과
기대값과 일치하는 것을 확인할 수 있다.
'Backend > Redis' 카테고리의 다른 글
Redis 메모리 초과로 서버 다운된 경험과 해결 (2) | 2024.12.01 |
---|---|
레디스에서 O(N) 관련 커맨드는 주의하기 (0) | 2024.11.19 |