이번 시간엔 쿠폰 발급 시스템 디자인을 진행해보고자 한다. 해당 시스템은 어떻게 디자인할까?
이번에 만들고자 하는 쿠폰 발급 시스템의 기능 요구사항은 다음과 같다.
1. 1000장의 쿠폰을 발행하여 사용자에게 제공한다.
2. 1인당 1개의 쿠폰만 발행한다.
map_coupons_users
테이블을 하나 생성하여 할당해도 좋다. 필자의 경우는 MySQL8 버전에서 지원하는 Skip Locked 잠금을 적용해보고자 coupons 데이터 모델에서 매핑관계까지 전체 관리하는 방향으로 디자인하겠다.POST /{user_id}/coupons
{
"name" : "test",
...
}
---
{
"coupon_id" : "BBSS123XE",
"expired_at" : 12314313,
...
}
쿠폰을 미리 1000개 발행하느냐 마느냐
이 두 가지 측면에서 접근이 가능하다. 각각의 상황에 따라 서로 다른 아키텍처가 그려지기에 먼저 쿠폰을 미리 발급하지않고 사용자 요청 시 발급하는 방향으로 디자인해보겠다.SETNX
명령어로 Spin Lock 개념을 도입하여 분산락을 적용할 수 있다. 처음에 설정한 [key, value]에서 value 유효성 검증을 선행한 후 사용자에게 쿠폰 한 개를 발급한다. 이때, RDB 분산락을 활용한다. MySQL에서는 Named Lock이라고 하는 분산락으로 1인당 정확하게 1개의 쿠폰을 할당하도록 데이터 일관성을 보장한다. Redis에서 Batch Job으로 남은 쿠폰 개수를 동기화한다면 데이터 일관성을 높일 수 있다. 여기서 TPS가 만약 더 증가하게 된다면 Scale-out으로 대응하되 그럼에도 커버하기 힘든 수준의 트래픽이 몰릴 경우 메시지 큐를 중간에 도입하여 비동기 처리한다. 아키텍처는 다음과 같다.SELECT COUNT(*)
FROM coupons
WHERE status = 'UNUSED';
UPDATE coupons
SET user_id = :id, status = 'UNUSED'
WHERE coupon_id = (
SELECT coupon_id
FROM coupons
WHERE status = 'UNUSED'
LIMIT 1
FOR UPDATE SKIP LOCKED
);
하지만, 이렇게 할 경우 트랜잭션 Lock 범위가 넓어져 데드락에 빠질 위험이 있으며 불필요한 트랜잭션으로 어플리케이션 성능 저하가 발생할 수 있다. 따라서, RDB + Redis 분산락 조합으로 Data Consistency와 Availability를 보장하는 방향으로 설계하는 것이 베스트다.