개발

redis-semaphore 사용해서 mutex 해결하기

박연호의 개발 블로그 2024. 1. 12. 19:52

CJ 택배사 API연동하는 과정에서 semaphore를 사용하여 mutex를 해결한 경험을 공유하고자 글을 작성합니다.

 

 

전체적인 흐름은 A 서버에서 B서버로 주문 등록 요청을 하면 B서버는 redis에서 토큰을 조회 후, 토큰과 함께 CJ API를 호출하게 됩니다.

 

A서버에서 B서버로 요청을 하면 B서버는 validation 이후, 토큰을 조회하는 함수를 실행합니다. getToken 함수에서는 토큰키를 사용하여 redis에서 토큰을 조회하고 존재하면 사용하고, 존재하지 않으면 CJ측에 토큰 재발급 요청 후, redis에 저장합니다. token의 ttl은 하루 입니다.

const getToken = () => {
  try {
    const token = await redis.get('토큰 키');

    if (token) {
      return token;
    } else {
      const result = await request('/getCjToken');
      await redis.set('토큰 키', result.token, result.ttl);
      return result.token;
    }
  } catch (err) {
    throw err;
  }
}

 

주문 등록 요청이 짧은 순간 여러번 오게 되면(A서버에서는 Promise.all로 한번에 많은 요청을 보냄), getToken이 여러번 실행하게 되고, 실행 시점에서는 redis의 토큰값이 없어 else문에서 N번의 cj 토큰 발급 요청 api를 실행하게 됩니다.

 

문제점은 짧은 시간안에 많은 cj 토큰 발급 api를 요청하게 되면 api 호출 권한이 회수 된다는 것입니다. 사실 N번의 토큰 발급 요청을 할 필요 없이 한번만 요청해 토큰을 레디스에 저장하면 됩니다. 

 

그렇기 위해서는 토큰 발급 api 요청 코드 부분을 임계영역으로 지정하여, lock을 획득하기 전까지 대기하도록 할 수 있습니다.

const getToken = () => {
  try {
    const token = await redis.get('토큰 키');

    if (token) {
      return token;
    } else {
      await mutex.acquire();   <-- lock key를 획득하기 전까지 busy wating
      
      const token = await redis.get('토큰 키');
     if(token) return token;

      const result = await request('/getCjToken');
      await redis.set('토큰 키', result.token, result.ttl);

      await mutex.release(); <-- 첫번째 요청이 토큰을 레디스에 저장하면 lock key release
      return result.token;
    }
  } catch (err) {
	throw err;
  } finally {
    await mutex.release();
  }
}

 

세마포어 구현은 redis-semaphore를 사용했습니다.

 

 

mutex.acquire()

 

위의 코드 덕분에 B서버가 여러 요청을 받았다 해도, 첫번째 요청만 토큰 재발급 요청을 하며, 이후의 요청은 await mutex.acquire() 코드에 메달려 있게 됩니다(spin lock). 또한 await mutex.acquire()가 짧은 순간 많이 호출된다 하더라도, redis는 single thread이기 때문에 문제가 없습니다.

 

첫번째 요청이 토큰을 재발급 받고 redis.set을 하면 await mutex.release()가 실행됩니다. redis에 저장된 key를 삭제함으로 써 세마포어를 반환하게 되며 이렇게 반환된 세마포어는 이후의 요청이 while문에서 획득하게 됩니다.

      const result = await request('/getCjToken');
      await redis.set('토큰 키', result.token, result.ttl);

      await mutex.release(); <-- 첫번째 요청이 토큰을 레디스에 저장하면 lock key release

 

 

release()에서 아래처럼 redis 내부에서 루아 스크립트를 실행시킨건 atomic때문 입니다. 

mutex.release()

 

이후의 요청은 토큰 재발급 요청을 하기 전에, 먼저 redis에 저장된 토큰값이 있는지 확인합니다. 물론, 첫번째 요청이 토큰을 저장했기 때문에 있겠죠. 해당 토큰을 반환하게 됩니다. 

      await mutex.acquire();   <-- lock key를 획득하기 전까지 busy wating
      const token = await redis.get('토큰 키');