Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Dockerfile.module
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# node_modules를 가지고 있는 이미지
# 이 이미지를 기반으로 각 workspace 별 이미지를 만들면
# yarn install 레이어를 공유하게 된다.
FROM node:20-alpine

WORKDIR /app

# 호이스팅을 위해
COPY package.json yarn.lock ./
COPY apps/backend/package.json ./apps/backend/
COPY apps/frontend/package.json ./apps/frontend/
COPY apps/websocket/package.json ./apps/websocket/

# 의존성 설치
RUN yarn install
112 changes: 66 additions & 46 deletions apps/backend/src/redis/redis.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import Redis, { Command } from 'ioredis';
import Redis from 'ioredis';
const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT';

export type RedisPage = {
Expand Down Expand Up @@ -33,6 +33,54 @@ export class RedisService {
constructor(
@Inject(REDIS_CLIENT_TOKEN) private readonly redisClient: Redis,
) {}
async acquireLock(
redisClient: Redis,
key: string,
retryCount = 10,
retryDelay = 100,
) {
key = 'lock:' + key;
// retryCount만큼 시도
for (let i = 0; i < retryCount; i++) {
const value = Date.now().toString();
Logger.log(`redis lock info ${key} : ${value}`);
const acquireResult = await redisClient.set(key, value, 'EX', 10, 'NX');
Logger.log(acquireResult);

// 락 획득 성공
if (acquireResult == 'OK') {
Logger.log(`시도 횟수 : ${i}`);
const release = async () => {
Logger.log(`release 하려는 key : ${key}`);
Logger.log(`release 하려는 value : ${value}`);
const releaseResult = await redisClient.eval(
releaseScript,
1,
key,
value,
);
// 락 해제 성공
Logger.log(`락 해제 결과 : ${releaseResult}`);
if (releaseResult === 1) {
Logger.log('락 해제 성공');
return true;
}
// 락 해제 실패
Logger.log('락 해제 실패');
return false;
};
return release;
}

// 락 획득 실패하면 retryDelay이후 다시 획득 시도
await new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, retryDelay);
});
}
throw new Error('락 획득 실패');
}

async getAllKeys(pattern) {
return await this.redisClient.keys(pattern);
Expand All @@ -51,16 +99,28 @@ export class RedisService {

async set(key: string, value: object) {
// 락 획득할 수 있을 때만 set
const release = await this.acquireLock(key);
const release = await this.acquireLock(this.redisClient, key);
await this.redisClient.hset(key, Object.entries(value));

// 락 해제
await release();
}

async setFields(key: string, map: Record<string, string>) {
// 락 획득할 수 있을 때만 set
const release = await this.acquireLock(this.redisClient, key);
// fieldValueArr 배열을 평탄화하여 [field, value, field, value, ...] 형태로 변환
const flattenedFields = Object.entries(map).flatMap(([field, value]) => [
field,
value,
]);
// 락 해제
await release();
// hset을 통해 한 번에 여러 필드를 설정
return await this.redisClient.hset(key, ...flattenedFields);
}
async setField(key: string, field: string, value: string) {
// 락 획득할 수 있을 때만 set
const release = await this.acquireLock(key);
const release = await this.acquireLock(this.redisClient, key);
const result = await this.redisClient.hset(key, field, value);

// 락 해제
Expand All @@ -70,51 +130,11 @@ export class RedisService {

async delete(key: string) {
// 락 획득할 수 있을 때만 set
const release = await this.acquireLock(key);
const release = await this.acquireLock(this.redisClient, key);
const result = await this.redisClient.del(key);

// 락 해제
await release();
return result;
}
private async acquireLock(key: string, retryCount = 10, retryDelay = 100) {
// retryCount만큼 시도
for (let i = 0; i < retryCount; i++) {
// mili초 단위 timestamp + 랜덤 숫자
const value = Date.now().toString() + Math.random().toString();
const acquireResult = await this.redisClient.set(
'user:' + key,
value,
'EX',
10,
'NX',
);

// 락 획득 성공
if (acquireResult == 'OK') {
const release = async () => {
const releaseResult = await this.redisClient.eval(
releaseScript,
1,
'user:' + key,
value,
);
// 락 해제 성공
if (releaseResult !== 1) {
// 락 해제 실패
throw new Error('락 해제 실패');
}
};
return release;
}

// 락 획득 실패하면 retryDelay이후 다시 획득 시도
await new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, retryDelay);
});
}
throw new Error('락 획득 실패');
}
}
23 changes: 13 additions & 10 deletions apps/websocket/src/redis/redis.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import Redis, { Command } from 'ioredis';
import Redis from 'ioredis';
const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT';
const RED_LOCK_TOKEN = 'RED_LOCK';

export type RedisPage = {
title?: string;
Expand Down Expand Up @@ -40,33 +39,37 @@ export class RedisService {
retryCount = 10,
retryDelay = 100,
) {
key = 'lock:' + key;
// retryCount만큼 시도
for (let i = 0; i < retryCount; i++) {
// mili초 단위 timestamp + client id
const clientId = await this.redisClient.sendCommand(
new Command('client id'),
);
const value = Date.now().toString();
console.log(`redis key : ${value}`);
Logger.log(`redis lock info ${key} : ${value}`);
const acquireResult = await redisClient.set(key, value, 'EX', 10, 'NX');
Logger.log(acquireResult);

// 락 획득 성공
if (acquireResult == 'OK') {
console.log(`시도 횟수 : ${i}`);
return async function release() {
Logger.log(`시도 횟수 : ${i}`);
const release = async () => {
Logger.log(`release 하려는 key : ${key}`);
Logger.log(`release 하려는 value : ${value}`);
const releaseResult = await redisClient.eval(
releaseScript,
1,
key,
value,
);
// 락 해제 성공
Logger.log(`락 해제 결과 : ${releaseResult}`);
if (releaseResult === 1) {
Logger.log('락 해제 성공');
return true;
}
// 락 해제 실패
Logger.log('락 해제 실패');
return false;
};
return release;
}

// 락 획득 실패하면 retryDelay이후 다시 획득 시도
Expand Down
54 changes: 27 additions & 27 deletions compose.init.yml
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
version: "3.8"

services:
nginx:
image: nginx:alpine
restart: always
ports:
- "80:80"
volumes:
- ./services/nginx/conf.d/prod_nginx_init.conf:/etc/nginx/conf.d/default.conf
- ./data/certbot/www:/var/www/certbot
networks:
- frontend

certbot:
image: certbot/certbot:latest
volumes:
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot
- ./data/certbot/log:/var/log/letsencrypt
command: >
certonly --webroot
--webroot-path=/var/www/certbot
--email hihj070914@icloud.com
--agree-tos
--no-eff-email
-d octodocs.site
-d www.octodocs.site
nginx:
image: nginx:alpine
restart: always
ports:
- "80:80"
volumes:
- ./services/nginx/conf.d/prod_nginx_init.conf:/etc/nginx/conf.d/default.conf
- ./data/certbot/www:/var/www/certbot
networks:
- frontend

certbot:
image: certbot/certbot:latest
volumes:
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot
- ./data/certbot/log:/var/log/letsencrypt
command: >
certonly --webroot
--webroot-path=/var/www/certbot
--email growth_s@naver.com
--agree-tos
--no-eff-email
-d www.octodocs.shop
depends_on:
- nginx
networks:
frontend:
driver: bridge
frontend:
driver: bridge
40 changes: 16 additions & 24 deletions compose.local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ services:
retries: 3
start_period: 10s
timeout: 5s
frontend:
build:
context: .
dockerfile: ./services/frontend/Dockerfile.local
image: frontend:latest
env_file:
- .env
volumes:
- .env:/app/.env
- ./apps/frontend:/app/apps/frontend

networks:
- net
ports:
- "5173:5173" # Vite dev server

backend:
build:
Expand All @@ -45,10 +60,6 @@ services:
volumes:
- .env:/app/.env
- ./apps/backend:/app/apps/backend
- ./apps/frontend:/app/apps/frontend
- root_node_modules:/app/node_modules
- backend_app_node_modules:/app/apps/backend/node_modules
- frontend_app_node_modules:/app/apps/frontend/node_modules
depends_on:
postgres:
condition: service_healthy
Expand All @@ -57,13 +68,7 @@ services:
networks:
- net
ports:
- "5173:5173" # Vite dev server
- "3000:3000" # 백엔드 API 포트
entrypoint: |
sh -c "
yarn install &&
yarn dev
"

websocket:
build:
Expand All @@ -75,8 +80,6 @@ services:
volumes:
- .env:/app/.env
- ./apps/websocket:/app/apps/websocket
- root_node_modules:/app/node_modules
- websocket_app_node_modules:/app/apps/websocket/node_modules
depends_on:
postgres:
condition: service_healthy
Expand All @@ -86,11 +89,6 @@ services:
- net
ports:
- "4242:4242" # WebSocket 포트
entrypoint: |
sh -c "
yarn install &&
yarn dev
"

nginx:
build:
Expand All @@ -111,16 +109,10 @@ services:
bind:
create_host_path: true
propagation: rprivate
- ./services/nginx/conf.d:/etc/nginx/conf.d
- ./services/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf

networks:
net:

volumes:
postgres_data:
root_node_modules:
backend_node_modules:
backend_app_node_modules:
frontend_app_node_modules:
websocket_node_modules:
websocket_app_node_modules:
Loading
Loading