DB/Redis

Redis를 활용한 다양한 시스템 설계

devson 2022. 10. 2. 15:07

Redis는 in-memory key-value storage로 주로 DB 레이어의 부하 분산과 빠른 응답을 위한 캐싱 레이어로 쓰인다.

하지만 Redis는 value로 다양한 데이터 타입을 지원하고 있으며, 다양한 모듈과 이 모듈들을 포함한 Redis Stack 또한 제공하고있다.

 

Redis에서 제공하는 데이터 타입들과 모듈을 활용하면 큰 노력 없이 다양한 종류의 시스템을 설계할 수 있다.

이번 포스팅에서는 Redis를 활용하여 다양한 시스템을 설계하는 방법에 대해 알아보도록 하겠다.

 

 

Rate Limit

API 사용이나 특정 기능에 있어서 Rate Limit를 해야하는 경우가 종종 있다.

- 하루에 한 번만 참여 가능한 이벤트
- 인증 문자를 받고나서 다음 인증 문자를 받으려면 1분의 대기시간을 주기

등의 기능을 예로 들 수 있다.

 

이때 Redis의 자료구조와 EXPIRE 기능을 활용하면 쉽게 구현 가능하다.

 

하루에 한 번만 참여 가능한 이벤트 기능

Redis의 SET 자료구조를 활용할 수 있다.

awesome-event:{yyyyMMdd}와 같은 패턴으로 key를 사용하는 SET에, User ID를 member로 사용하면

유저가 특정 날짜에 이벤트에 참여했는지를 알 수 있다.

redis:6379> SADD awesome-event:20220828 user_1
(integer) 1 # SET에 새로 추가된 member인 경우 1
redis:6379> SADD awesome-event:20220828 user_1
(integer) 0 # SET에 이미 존재하는 member인 경우 0

redis:6379> SADD awesome-event:20220828 user_2
(integer) 1
redis:6379> SADD awesome-event:20220828 user_3
(integer) 1

위 코드에서 처럼 SADD를 실행했을 때 리턴되는 값을 확인하면
하나의 커맨드를 통해 이전에 이벤트를 참여하였는지 아닌지를 확인할 수 있어 I/O나 Race Condition 측면에서 효율성을 가질 수 있다.

 

인증 문자를 받고나서 다음 인증 문자를 받으려면 1분의 대기시간을 주기 기능

Redis의 SET 커맨드의 option인 NX, EXPIRE를 활용한다.

auth-text:{User ID}와 같은 패턴으로 key를 사용하고, NX(Key가 존재하지 않으면 저장)와 EXPIRE(TTL 기능)를 사용하면

특정 시간 내에 유저가 인증 문자를 받았는지 확인할 수 있다.

redis:6379> SET auth-text:user_1 1 NX EX 60
OK # 새로 저장된 Key인 경우 OK
redis:6379> SET auth-text:user_1 1 NX EX 60
(nil) # 기존에 저장된 Key인 경우 nil(null)

redis:6379> SET auth-text:user_2 1 NX EX 60
OK
redis:6379> SET auth-text:user_3 1 NX EX 60
OK

이 역시 커맨드 실행 시 리턴되는 값을 통해 하나의 커맨드를 통해 시간 내에 인증 문자를 받았는지 아닌지를 확인할 수 있다.

 

물론 RDBMS와 같은 Database에 내역에 대한 저장하고 확인할 수 있지만 너무 많은 데이터가 쌓일 수 있고 Race Condition도 생길 여지가 있다.

Redis의 자료구조와 EXPIRE(TTL) 기능을 사용하면 계속해서 쌓이는 데이터의 압박과 Race Condition의 걱정을 줄이면서 쉽고 빠르게 Rate Limit 기능을 구현할 수 있다.

 

또한 토큰 버킷 알고리즘 기반 Java rate-limiting library인 Bucket4jRedis integration을 제공하기도 한다.

 

실시간 랭킹 시스템

실시간 랭킹 시스템은 Redis의 가장 잘 알려진 유스케이스 중 하나이다.

실시간 랭킹 시스템은 Redis의 Sorted Set(ZSET) 자료구조를 활용하면 쉽게 구현 가능하다.

redis:6379> ZINCRBY ranking 100 user_1 # user_1이 100점을 얻음
"100"
redis:6379> ZINCRBY ranking 50 user_1 # user_1이 50점을 얻음
"150"
redis:6379> ZINCRBY ranking 60 user_2 # user_2가 60점을 얻음
"60"
redis:6379> ZINCRBY ranking 200 user_3 # user_3이 200점을 얻음
"200"
redis:6379> ZINCRBY ranking 20 user_4 # user_4가 20점을 얻음
"20"
redis:6379> ZINCRBY ranking 20 user_5 # user_5가 20점을 얻음
"20"

# 상위 3등의 데이터를 가져옴 
redis:6379> ZREVRANGE ranking 0 2 WITHSCORES
1) "user_3"
2) "200"
3) "user_1"
4) "150"
5) "user_2"
6) "60"

 

또한 특정 유저의 등수를 파악하는 것도 커맨드 하나로 확인가능하다.

redis:6379> ZREVRANK ranking user_3
(integer) 0 # 1등

redis:6379> ZREVRANK ranking user_2
(integer) 2 # 3등

redis:6379> ZREVRANK ranking user_1
(integer) 1 # 2등

(score가 높은 순으로(역순)으로 확인하기 때문에 ZREVRANK를 사용한다)

 

추가적으로 Redis에서는 확률적 자료구조인 Top-K도 지원하기 때문에 수백만 이상의 데이터를 다뤄야하는 경우 Top-K의 사용을 고려할 수 있다.

참고: Sorted Set vs Top-K for Real-time Ranking System

 

좋아요 기능

좋아요는 SNS나 커뮤니티에서 자주 사용되는 기능으로써 특정 게시물 등에 좋아요 하기/취소하기누가 좋아요를 했는지, 좋아요 총 개수가 몇개인지를 보여주는 기능이다.

이 기능은 Redis에서 제공하는 SET 자료구조를 사용하면 쉽게 구현가능하다.

 

SET을 사용하는 이유는 SET 자료구조 특성 상 중복 데이터를 허용하지 않기 때문에,
짧은 시간에 요청이 두 번 가더라도 한 명의 유저가 같은 대상에 두 번 이상 좋아요를 하지 않게 막을 수 있기 때문이다.

 

좋아요 하기/취소하기

like:{resource_type}:{resource_id}와 같은 패턴으로 key를 사용하는 SET에, User ID를 member로 사용하면 간단한 좋아요 기능을 구현할 수 있다.

# 좋아요 하기
127.0.0.1:6379> SADD like:post:1 user_1 # 1번 게시물에 user_1이 좋아요를 함
(integer) 1
127.0.0.1:6379> SADD like:post:1 user_3 # 1번 게시물에 user_3이 좋아요를 함
(integer) 1
127.0.0.1:6379> SADD like:post:1 user_3 # 1번 게시물에 user_3이 좋아요를 함
(integer) 0 # 중복 값인 경우 0을 리턴한다

# 좋아요 취소하기
127.0.0.1:6379> SADD like:post:2 user_1 # 2번 게시물에 user_1이 좋아요를 함
(integer) 1
127.0.0.1:6379> SREM like:post:2 user_1 # 2번 게시물에 user_1이 좋아요를 취소함
(integer) 1
127.0.0.1:6379> SREM like:post:2 user_1 # 2번 게시물에 user_1이 좋아요를 취소함
(integer) 0 # 해당 key에 존재하지 않는 값인 경우 0을 리턴한다

위 커맨드에서 볼 수 있듯 SET 자료구조는 중복 처리를 막아주는 역할을 하기 때문에 데이터 정합성 측면에서 보다 안전하게 기능을 구현할 수 있다.

 

누가 좋아요를 했는지 확인하기

누가 좋아요를 했는지는 단순하게 SMEMBERS 커맨드를 통해 확인하면 된다.

# 유저가 좋아요를 함
127.0.0.1:6379> SADD like:post:1 user_1
(integer) 1
127.0.0.1:6379> SADD like:post:1 user_2
(integer) 1
127.0.0.1:6379> SADD like:post:1 user_5
(integer) 1
127.0.0.1:6379> SADD like:post:1 user_344
(integer) 1

# 좋아요를 한 유저 조회
127.0.0.1:6379> SMEMBERS like:post:1
1) "user_5"
2) "user_2"
3) "user_1"
4) "user_344"

 

SMEMBERS를 통해 누가 좋아요를 눌렀는지 확인할 수 있지만 하지만 순서를 보장해주지 않는다.

만약 좋아요를 누른 유저를 시간 순으로 나열해야한다면 Sorted Set(ZSET)을 사용하는 것을 고려할 수 있다.

아래와 같이 score를 좋아요를 한 timestamp로 저장한다면 좋아요를 누른 유저를 시간 순으로 나열할 수 있다.

물론 SET을 사용한 중복 데이터 방지의 이점 또한 그대로 가질 수 있다.

# 좋아요
127.0.0.1:6379> ZADD like:post:1 20220831213905 user_1
(integer) 1
127.0.0.1:6379> ZADD like:post:1 20220831213915 user_2
(integer) 1
127.0.0.1:6379> ZADD like:post:1 20220831214045 user_42
(integer) 1

# 조회
127.0.0.1:6379> ZRANGE like:post:1 0 -1
1) "user_1"
2) "user_2"
3) "user_42"

 

좋아요를 한 개수 조회하기

특정 대상이 얼마나 좋아요를 받았는지 확인하기 위해서는 SCARD를 사용하면된다.

(Sorted Set의 경우 ZCARD)

127.0.0.1:6379> SADD like:post:1 user_1
(integer) 1
127.0.0.1:6379> SADD like:post:1 user_2
(integer) 1
127.0.0.1:6379> SADD like:post:1 user_5
(integer) 1
127.0.0.1:6379> SADD like:post:1 user_344
(integer) 1

127.0.0.1:6379> SCARD like:post:1 # 1번 게시물의 좋아요 개수 조회
(integer) 4

 

유저가 좋아요를 한 대상 관리하기

앞서는 단순하게 유저가 특정 대상에 대해 좋아요를 한 목록에 대해서만 다뤘지만,
실제로는 유저가 어떤 대상에 좋아요를 했는지를 보여주고자하는 니즈도 크다.

그렇기에 결국 특정 대상에 좋아요를 하기 + 유저가 좋아요를 한 대상을 관리 기능을 둘 다 필요로 하는데

이 경우에 MULTI, EXEC 커맨드를 사용하여 트랜잭션을 보장하면서도, 하나의 요청에 여러 커맨드를 한 번에 실행할 수 있다.

127.0.0.1:6379> MULTI # Transaction 시작
OK
# 2번 게시물에 user_1이 좋아요를 함
127.0.0.1:6379> SADD like:post:2 user_1
QUEUED
# user_1이 좋아요를 한 게시물 목록에 2번 게시물을 추가함
127.0.0.1:6379> ZADD user-post-likes:user_1 20220831112233 post_2
QUEUED
127.0.0.1:6379> EXEC # 쌓인 명령어 실행
1) (integer) 1
2) (integer) 1

# user_1이 좋아요를 한 게시물 목록 조회
127.0.0.1:6379> ZRANGE user-post-likes:user_1 0 -1
1) "post_2"

 

또는 커맨드의 실행 결과를 사용하여 Redis에 저장된 다른 데이터를 변경해야하는 경우와 같이 조금은 복잡한 작업을 해야하는 경우 Lua script의 사용을 고려할 수 있다.

 

최근 본 상품 목록

커머스 서비스에서 자주 사용되는 최근 본 상품과 같이 시간과 관련된 데이터는 앞서 살펴본 것과 같이 Sorted Set(ZSET)을 사용하면 관리가 편리하다.

 

seen-products:{user_id}와 같은 패턴으로 key를 사용하는 SET에, score를 timestampProduct ID를 member로 사용하면 쉽게 조회한 상품을 저장하고 최근 본 상품을 나열할 수 있다.

# user_1이 2022-09-17 14:29:03에 product_253를 조회함
127.0.0.1:6379> ZADD latest-seen-products:user_1 20220917142903 product_253
(integer) 1
# user_1이 2022-09-17 14:31:53에 product_153를 조회함
127.0.0.1:6379> ZADD latest-seen-products:user_1 20220917143153 product_153
(integer) 1
# user_1이 2022-09-18 10:30:30에 product_685를 조회함
127.0.0.1:6379> ZADD latest-seen-products:user_1 20220918103030 product_685
(integer) 1

# user_1의 조회한 상품 중 최근 10개만 조회
127.0.0.1:6379> ZREVRANGE latest-seen-products:user_1 0 9
1) "product_685"
2) "product_153"
3) "product_253"

 

데이터가 계속 쌓이는 것을 막기위해 특정 RANK 아래의 데이터는 삭제하는 것을 생각할 수 있다.

(하나의 collection type에 계속 데이터를 쌓는 것은 해당 key를 사용 시 성능에 영향을 준다)

이때, 기획 변경 등의 이유로 보존하는 데이터의 양에 대해 약간의 버퍼를 두는 것도 고려할 수 있다.

아래 예제에서는 최근 본 상품 중 10개만 조회하지만, 데이터는 50개를 남겨두도록 하였다.

# 데이터 추가
127.0.0.1:6379> MULTI # Transaction 시작
OK

127.0.0.1:6379> ZADD latest-seen-products:user_1 20220917142903 product_10
QUEUED

# 최근 본 상품 50개만 남기고 삭제
# start: 0, stop: -(남기고자하는 갯수 + 1)
127.0.0.1:6379> ZREMRANGEBYRANK latest-seen-products:user_1 0 -51
QUEUED

127.0.0.1:6379> EXEC # 쌓인 명령어 실행
1) (integer) 1
2) (integer) 0

######################################################################

# 최근 본 상품 10개만 조회
127.0.0.1:6379> ZREVRANGE latest-seen-products:user_1 0 9
1) "product_10"
2) "product_9"
3) "product_8"
4) "product_7"
5) "product_6"
6) "product_5"
7) "product_4"
8) "product_3"
9) "product_2"
10) "product_1"

 

Job Queue

메인 로직과는 별개로 실행되어야하는 기능(e.g. 회원 가입 후 메일 보내기, 배송 완료 후 카카오톡 알림톡 보내기)이 있는 경우,

이 기능은 비동기로 진행하는 것이 요청-응답 시간을 줄이는데 도움을 준다.

 

멀티 쓰레딩을 기반으로 애플리케이션은 단순히 다른 Thread를 통해 실행하는 방식으로 비동기로 특정 기능을 실행할 수 있다.

하지만 서버 애플리케이션과는 별개인 worker에서 해당 작업을 실행하고자 하는 경우 Redis를 Job Queue로써 고려할 수 있다.

 

Ruby의 경우 Sidekiq가 대표적이고 Python은 RQ, Celery 가 Redis를 Job Queue로써 사용하는 대표적인 라이브러리이다.

또한 Java Redis client library인 Redisson 역시 이러한 기능을 제공한다.

(참고 - Distributed Tasks Execution and Scheduling in Java, Powered By Redis)

 

라이브러리를 사용하지 않고 Redis를 직접 사용하여 Job Queue 기능을 구현한다고하면 List를 사용할 수 있는데,
새로운 Job을 추가(enqueue)할 때는 RPUSH를 하고, Job을 가져올 때(dequeue)는 BLPOP을 하여 계속해서 Job을 polling 할 수 있다.

redis:6379> LPUSH job "{data1: 1, data2: 2}" // enqueue job into job queue
(integer) 1
redis:6379> LPUSH job "{data1: 2, data2: 3}" // enqueue job into job queue
(integer) 2

redis:6379> BLPOP job 0 // dequeue from job queue
1) "job"
2) "{data1: 2, data2: 3}"
redis:6379> BLPOP job 0 // dequeue from job queue
1) "job"
2) "{data1: 1, data2: 2}"

(혹은 Stream을 활용할 수 있다)

 

선착순

선착순 이벤트는 서비스 홍보의 목적으로 가끔씩 열릴 수 있는데,

유명한 서비스의 입소문이 난 이벤트라면 짧은 시간 내에 몰리는 트래픽을 감당할 수 있는 시스템을 구현하는 것이 까다로울 수 있다.

또한 선착순 인원 확인중복 참여 여부 확인이 필요한 기능 특성 상 데이터를 다룰 때 원자성이 중요하다.

Redis의 in-momerysingle threaded 라는 특성을 활용하면 빠르면서도 원자성을 보장할 수 있는 선착순 기능을 개발할 수 있다.

 

예제로 5,000개만 발급 가능하고, 중복 발급이 불가능한 선착순 쿠폰을 예로 들어보겠다.

쿠폰 발급 요청에 대해 아래와 같은 플로우로 쿠폰을 발급한다고 하자.

선착순 쿠폰 발급 플로우 차트

위 플로우 차트에서 모든 쿠폰이 발급 되었는지?이미 쿠폰을 발급 받았는지?에 대한 확인과 쿠폰 발급 처리를 하는데,
모든 operation에 대해 원자성이 지켜지지 않는다면 정확한 선착순 처리(쿠폰 초과 발급)가 되지 않을 수 있다.

 

Redis를 사용하되 원자성이 필요한 operation을 처리할 때는 Redis의 Lua scripting 기능을 활용할 수 있다.

Redis 커맨드와 마찬가지로 Lua script 역시 main thread에서 처리하기 때문에 operation의 원자성을 지킬 수 있다.

코드를 보기 전에 먼저 전체 시스템 구성에 대해 살펴보자.

 

전체적인 시스템은 아래와 같이 설계할 수 있는데,

Redis를 Job Queue로 사용하여 쿠폰 발급 여부를 확인하는 서버를 앞단에 두고 뒷단에 쿠폰을 발급하는 서버를 worker로 두었다.

이렇게 함으로써 갑작스러운 트래픽 증가에도 서비스 장애 전파를 막을 수 있다.

 

Coupon Event API

먼저 앞단에서 쿠폰 발급 여부를 확인하고 Job Queue를 통해 쿠폰 발급을 요청하는 API 서버를 살펴보자.

앞서 얘기한 것 처럼 쿠폰 발급 operation에 대한 원자성이 중요하기 때문에 Lua scripting 기능을 사용한다.

코드는 아래와 같이 작성할 수 있을 것이다.

const Redis = require("ioredis");

const redis = new Redis({
  host: "127.0.0.1",
  port: 6379
});

redis.defineCommand("publishEventCoupon", {
  numberOfKeys: 2, // user id, timestamp
  lua: `
      local userId = KEYS[1]
      local timestamp = KEYS[2]

      local maxNumberOfCoupon = 5000
      local couponKey = 'coupon:event'
      local numberOfPublishedCoupon = redis.call('ZCARD', couponKey)

      -- 쿠폰이 모두 발급되었는지 확인
      if maxNumberOfCoupon <= numberOfPublishedCoupon then
        return {"FINISHED"}
      end

      -- 유저 쿠폰 발급 시도
      local added = redis.call('ZADD', couponKey, 'NX', timestamp, userId)

      -- 유저가 이미 쿠폰을 발급 받았는지 확인
      if added < 1 then
        return {"ALREADY_PUBLISHED"}
      end

      -- enqueue 유저 쿠폰 발급 job
      local couponQueueName = 'queue:coupon:event'
      redis.call('RPUSH', couponQueueName, userId)
      return {"OK"}
    `
});

위와 같은 Lua script로 Redis를 사용한 여러 operation을 하나의 커맨드로 처리할 수 있다.

 

다만, Lua script는 원자성을 보장하는 operation을 수행할 수 있지만 유지 보수에 있어서 병목이 될 수 있는 사항이기 때문에

(너무 복잡한 script는 기능 변경이 필요할 때 손을 대기 힘들 수 있다)

적절한 수준에서 Lua script를 이용하도록 하는 것이 좋다.

 

전체 API 서버 코드

더보기
const Redis = require("ioredis");
const fastify = require("fastify")({ logger: true });

const redis = new Redis({
  host: "127.0.0.1",
  port: 6379
});

redis.defineCommand("publishEventCoupon", {
  numberOfKeys: 2, // user id, timestamp
  lua: `
      local userId = KEYS[1]
      local timestamp = KEYS[2]

      local maxNumberOfCoupon = 5000
      local couponKey = 'coupon:event'
      local numberOfPublishedCoupon = redis.call('ZCARD', couponKey)

      -- 쿠폰이 모두 발급되었는지 확인
      if maxNumberOfCoupon <= numberOfPublishedCoupon then
        return {"FINISHED"}
      end

      -- 유저 쿠폰 발급 시도
      local added = redis.call('ZADD', couponKey, 'NX', timestamp, userId)

      -- 유저가 이미 쿠폰을 발급 받았는지 확인
      if added < 1 then
        return {"ALREADY_PUBLISHED"}
      end

      -- 유저 쿠폰 발급 처리
      local couponQueueName = 'queue:coupon:event'
      redis.call('RPUSH', couponQueueName, userId)
      return {"OK"}
    `
});

// configure request router
fastify.post("/coupons/event", async (req, reply) => {
  const { userId } = req.body;
  const now = Date.now();

  const result = await redis.publishEventCoupon(userId, now);

  switch (result[0]) {
    case "OK":
      return { message: "발급 완료" };

    case "FINISHED":
      return { message: "쿠폰이 모두 소진되었습니다" };

    case "ALREADY_PUBLISHED":
      return { message: "쿠폰을 이미 발급 받았습니다" };

    default:
      throw Error("invalid response from Redis");
  }
});

// run webserver
const start = async () => {
  try {
    await fastify.listen({ port: 3000 });
  } catch (err) {
    fastify.log.error(err);
    redis.disconnect();
    process.exit(1);
  }
};
start();

 

Coupon Service Worker

Redis를 Job Queue로 사용하여 실제 쿠폰 발급 처리를하는 Worker 서버에 대해 살펴보자.

앞서 Coupon Event API에서 List를 Job Queue로써 사용했기 때문에 BLPOP 커맨드를 사용하도록 하였다.

const Redis = require("ioredis");

const redis = new Redis({
  host: "127.0.0.1",
  port: 6379
});

const worker = async () => {
  while (true) {
    const result = await redis.blpop("queue:coupon:event", 0);
    const [_key, userId] = result;
    console.log(`쿠폰 발급 완료 - 유저: ${userId}`);
  }
};

worker();

 

이런 식으로 Queue로부터 Job을 빼와서 쿠폰 발급을 처리할 수 있다.

 

대기열도 FIFO Queue 방식을 사용해야하기 때문에 위와 유사한 방식으로 처리될 수 있다.

참고:

- 실버바인 대기열 서버 설계 리뷰

- 프로모션을 대비한 대기열 시스템 구성하기 (Redis, WebSocket, Spring)

- [우아한테크토크] 선착순 이벤트 서버 생존기! 47만 RPM에서 살아남다?!

 

선착순과 같이 빠른 처리가 중요한 기능을 Redis를 사용하여 구현하는 경우, Redis 커맨드의 time complexity를 파악하는 것이 중요하다.

https://redis.io/commands/ 에 각 커맨드에 대한 time complexity도 나와있기 때문에 사용하는 커맨드의 성능에 대해 파악하는 것이 좋다.

 

또한, Redis는 eventual consistency 이기 때문에 Replication을 구성한 경우 Slave node로 데이터를 조회한다면 Master node의 변경 사항이 적용되지 않은 데이터를 참조할 수 있기 때문에

짧은 시간 내에 많은 트래픽이 들어오는 경우엔 정확한 선착순을 위해 Master node를 사용하여 조회하는 것이 좋다.

(누군가에게는 빼빼로데이. 누군가에게는?에서 아뿔싸...ㅠ 파트 참고)

 

마지막으로 참고가 될만한 링크들을 남겨놓는다.

- Redis&Kafka를 활용한 선착순 쿠폰 이벤트 개발기 (feat. 네고왕)

- 누군가에게는 빼빼로데이. 누군가에게는?

- 레디스를 이용한 기프티콘 선착순 이벤트 구현

- Redis를 활용한 트래픽 감당기

- 커머스서비스에서 동접자 대응을 위한 REDIS 도입기.

 

웹소켓 서버 클러스터링

Stateless한 API 서버의 경우 단순하게 인스턴스를 증가하는 식으로 Scaling out을 할 수 있지만,

클라이언트와 서버가 연결되어있는 Stateful한 웹소켓 서버의 경우, Scaling out을 했을 때 서로 다른 웹소켓 서버에 붙은 클라이언트 끼리 통신이 필요하기 때문에 클러스터링을 위한 인프라 구성이 추가로 필요하다.

 

(600k concurrent websocket connections on AWS using Node.js 에서 얘기하는 것과 같이 하나의 인스턴스에서 수많은 커넥션을 맺을 수 있는 웹소켓 서버를 한 대 운용한다해도 fault tolerance 한 시스템, 다운 타임 없는 기능 업데이트, 수평적 확장의 가능성을 위해서는 결국 클러스터링을 구축해 놓는 편이 좋다고 본다)

 

웹소켓 서버 간에 클러스터링을 하기 위해 주로 사용되는 솔루션은 Redis의 Pub/Sub이다.

출처: https://www.linkedin.com/pulse/distributed-websocket-based-messaging-system-sepehr-ghorbanpoor/

위에서 보여주는 시스템 처럼 다른 웹소켓 서버에 붙은 클라이언트에게 메세지를 전달하기 위해 주로 Redis Pub/Sub을 사용한다.

 

하지만 Redis의 Pub/Sub은 Publish 시에 모든 노드에 데이터를 전달하기 때문에  좀 더 대규모의 채팅 서비스를 운용한다면 Redis Pub/Sub 외에 다른 솔루션을 고려하는 것도 좋을 것이다.

(참고: Redis 7.x 에서의 ShardedPubSub채널톡 실시간 채팅 서버 개선 여정 - 2편 : Nats.io로 Redis 대체하기)

 

(하지만 Line에서는 잘 사용하고 있는 것 같다 - Redis Pub/Sub을 사용해 대규모 사용자에게 고속으로 설정 정보를 배포한 사례)

 

분산 락 (distributed lock)

개발을 하다보면 Race Condition을 해결해야만하는 경우가 있다.

커머스 상품의 재고 관리라던가 발급 개수가 정해져 있는 쿠폰과 같이 하나의 리소스에 여러 명이 동시에 접근할 수 있는 경우를 예로들 수 있다.

이와 같은 상황에서 분산 락의 사용을 고려할 수 있는데 분산 락을 사용하여 하나의 프로세스만 해당 리소스에 접근하도록 하여 Race Condition을 해결할 수 있다.

 

Redis는 Redis를 활용한 Redlock이라는 분산 락 알고리즘을 제시한다.

그리고 여러 언어에서 Redlock 및 Redis를 사용한 분산 락 구현체를 제공하기 때문에 이를 통해 분산 락을 사용하면 된다.

 

또한 Java Redis client library인 Redisson의 경우 Atomic 하면서 효율적이면서 방법으로 Lock을 제공하는데

Java를 사용한다면 Redisson을 통해 분산 락을 사용하는 것도 좋은 선택이 될 것이다.

(참고 - 레디스를 활용한 분산 락과 안전하고 빠른 락의 구현)