운영 중인 서비스에 약 7천 명을 대상으로 하여
선착순으로 상품을 지급하는 이벤트를 진행하기로 했다.
선착순 이벤트 구현의 중요한 부분은
순서에 따라서 정확히 제한 인원 내에 들어온 요청에만
성공 응답을 내려주고,
그 이후에 들어온 요청은 선착순 마감 응답을 내려줘야 한다는 점이다.
1. Redis의 싱글 스레드 이용하기?
처음에는 Redis의 싱글 스레드 특성을 이용하여
선착순 이벤트를 구현하려고 했다.
[이벤트 신청 API]
1. 선착순 키값 조회
2. 키 값이 정해진 인원보다 크거나 같으면 에러 응답
3. 키 값이 정해진 인원보다 작으면 redis increment 실행
그러나 Redis의 값이 유실되거나,
Redis 서버가 다운되는 상황을 항상 대비하는 것이
서비스의 그라운드 룰이었기 때문에
그러한 상황에 어떻게 대응할 지 대책이 필요했다.
Redis의 싱글 스레드의 이점을 이용하는 동시에
Redis 값이 유실되는 상황에도 대비하는 방법이 도저히 떠오르지 않았다.
2. RDBMS의 Auto Increment Id를 이용하기
Redis가 다운됐을 때 대비하는 방법을 생각해보려다가,
Redis를 사용하지 않고 RDBMS만을 사용하여
선착순 이벤트를 구현하는 방식을 만들어보기로 했다.
이 서비스는 PostgreSQL을 사용하고 있는데,
MySQL, PostgreSQL 등 웬만한 RDBMS에는 모두 있는
Auto Increment id를 사용하는 것이다.
[이벤트 신청 내역 테이블]
table: EventApplications
컬럼:
id Int auto_increment
userId Int
createdAt Date
[이벤트 신청 API]
1. 이벤트 신청 내역 테이블에 새로운 로그를 insert 한다.
2. insert 하면 생성된 로그의 id를 받아온다.
3. 해당 id보다 작거나 같은 id를 가진 로그의 수를 count 한다.
4. 그 count가 정해진 선착순 제한 인원보다 크다면 선착순 마감 에러를 반환한다.
5. 그 count가 정해진 선착순 제한 인원보다 작거나 같다면 성공 응답을 반환한다.
6. 이벤트가 마감된 후 정해진 선착순 제한 인원 이상으로 생성된 이벤트 신청 내역 로그는 삭제한다.
별도의 트랜잭션을 걸 필요도 없고,
[row insert > count > 검증]
3 단계를 거치기만 하면 된다.
다만 사후에 제한 인원보다 많이 생성된 신청 내역은
최종 신청으로 처리되지 않도록 삭제 등의 조치가 필요하다.
동시에 많은 요청이 몰리더라도
RDBMS의 Auto Increment Id는
같은 값을 생성하지 않고,
생성하더라도 바로 에러를 발생시키므로
제한 인원보다 더 많은 인원이 성공 응답을 받거나 하는 등의
이슈 상황은 발생하지 않을 것이라 판단했다.
3. 부하 테스트
컨트롤러에서 Promise.all로 비동기적으로 많은 요청을 동시에 보내는 상황을 시뮬레이션 해보았는데,
별다른 이슈 없이 정상 동작했다.
정원보다 많은 레코드가 생성되긴 했으나,
운영 팀에서 정원 초과된 레코드는 무시하면 되니 문제가 되지 않았다.
응답은 정원에 맞는 인원에게만 성공 응답이 반환됐고,
그 이후 요청에 대해서는 에러 응답이 반환됐다.
4. 실전 후기
허무하게도 실전에서는 트래픽이 그다지 몰리지 않아서
무난하게 끝났다.
이번 작업으로
RDBMS만으로도 선착순 로직이 구현 가능하다는 걸 배웠다.