Skip to content

Commit 94414c0

Browse files
committed
refactor: red lock 제거 후 redis 낙관적 락 적용
1 parent 5446971 commit 94414c0

File tree

11 files changed

+109
-159
lines changed

11 files changed

+109
-159
lines changed

apps/backend/src/app.module.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { WorkspaceModule } from './workspace/workspace.module';
2020
import { RoleModule } from './role/role.module';
2121
import { TasksModule } from './tasks/tasks.module';
2222
import { ScheduleModule } from '@nestjs/schedule';
23-
import { RedLockModule } from './red-lock/red-lock.module';
2423

2524
@Module({
2625
imports: [
@@ -54,7 +53,6 @@ import { RedLockModule } from './red-lock/red-lock.module';
5453
WorkspaceModule,
5554
RoleModule,
5655
TasksModule,
57-
RedLockModule,
5856
],
5957
controllers: [AppController],
6058
providers: [AppService],

apps/backend/src/page/page.module.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ import { Page } from './page.entity';
66
import { PageRepository } from './page.repository';
77
import { NodeModule } from '../node/node.module';
88
import { WorkspaceModule } from '../workspace/workspace.module';
9-
import { RedLockModule } from '../red-lock/red-lock.module';
109

1110
@Module({
1211
imports: [
1312
TypeOrmModule.forFeature([Page]),
1413
forwardRef(() => NodeModule),
1514
WorkspaceModule,
16-
RedLockModule,
1715
],
1816
controllers: [PageController],
1917
providers: [PageService, PageRepository],

apps/backend/src/page/page.service.ts

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { UpdatePageDto } from './dtos/updatePage.dto';
88
import { UpdatePartialPageDto } from './dtos/updatePartialPage.dto';
99
import { PageNotFoundException } from '../exception/page.exception';
1010
import { WorkspaceNotFoundException } from '../exception/workspace.exception';
11-
import Redlock from 'redlock';
1211

1312
const RED_LOCK_TOKEN = 'RED_LOCK';
1413

@@ -18,20 +17,13 @@ export class PageService {
1817
private readonly pageRepository: PageRepository,
1918
private readonly nodeRepository: NodeRepository,
2019
private readonly workspaceRepository: WorkspaceRepository,
21-
@Inject(RED_LOCK_TOKEN) private readonly redisLock: Redlock,
2220
) {}
2321
/**
2422
* redis에 저장된 페이지 정보를 다음 과정을 통해 주기적으로 데이터베이스에 반영한다.
2523
*
2624
* 1. redis에서 해당 페이지의 title과 content를 가져온다.
2725
* 2. 데이터베이스에 해당 페이지의 title과 content를 갱신한다.
2826
* 3. redis에서 해당 페이지 정보를 삭제한다.
29-
*
30-
* 만약 1번 과정을 진행한 상태에서 page가 삭제된다면 오류가 발생한다.
31-
* 위 과정을 진행하는 동안 page 정보 수정을 막기 위해 lock을 사용한다.
32-
*
33-
* 동기화를 위해 기존 페이지에 접근하여 수정하는 로직은 RedLock 알고리즘을 통해 락을 획득할 수 있을 때만 수행한다.
34-
* 기존 페이지에 접근하여 연산하는 로직의 경우 RedLock 알고리즘을 사용하여 동시 접근을 방지한다.
3527
*/
3628
async createPage(dto: CreatePageDto): Promise<Page> {
3729
const { title, content, workspaceId, x, y, emoji } = dto;
@@ -62,42 +54,29 @@ export class PageService {
6254
}
6355

6456
async deletePage(id: number): Promise<void> {
65-
// 락을 획득할 때까지 기다린다.
66-
const lock = await this.redisLock.acquire([`user:${id.toString()}`], 1000);
67-
try {
68-
// 페이지를 삭제한다.
69-
const deleteResult = await this.pageRepository.delete(id);
70-
71-
// 만약 삭제된 페이지가 없으면 페이지를 찾지 못한 것
72-
if (!deleteResult.affected) {
73-
throw new PageNotFoundException();
74-
}
75-
} finally {
76-
// 락을 해제한다.
77-
await lock.release();
57+
// 페이지를 삭제한다.
58+
const deleteResult = await this.pageRepository.delete(id);
59+
60+
// 만약 삭제된 페이지가 없으면 페이지를 찾지 못한 것
61+
if (!deleteResult.affected) {
62+
throw new PageNotFoundException();
7863
}
7964
}
8065

8166
async updatePage(id: number, dto: UpdatePageDto): Promise<Page> {
82-
// 락을 획득할 때까지 기다린다.
83-
const lock = await this.redisLock.acquire([`user:${id.toString()}`], 1000);
84-
try {
85-
// 갱신할 페이지를 조회한다.
86-
// 페이지를 조회한다.
87-
const page = await this.pageRepository.findOneBy({ id });
88-
89-
// 페이지가 없으면 NotFound 에러
90-
if (!page) {
91-
throw new PageNotFoundException();
92-
}
93-
// 페이지 정보를 갱신한다.
94-
const newPage = Object.assign({}, page, dto);
95-
96-
// 변경된 페이지를 저장
97-
return await this.pageRepository.save(newPage);
98-
} finally {
99-
await lock.release();
67+
// 갱신할 페이지를 조회한다.
68+
// 페이지를 조회한다.
69+
const page = await this.pageRepository.findOneBy({ id });
70+
71+
// 페이지가 없으면 NotFound 에러
72+
if (!page) {
73+
throw new PageNotFoundException();
10074
}
75+
// 페이지 정보를 갱신한다.
76+
const newPage = Object.assign({}, page, dto);
77+
78+
// 변경된 페이지를 저장
79+
return await this.pageRepository.save(newPage);
10180
}
10281

10382
async updateBulkPage(pages: UpdatePartialPageDto[]) {

apps/backend/src/red-lock/red-lock.module.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

apps/backend/src/redis/redis.module.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import { Module, forwardRef } from '@nestjs/common';
1+
import { Module } from '@nestjs/common';
22
import { ConfigModule, ConfigService } from '@nestjs/config';
33
import { RedisService } from './redis.service';
44
import Redis from 'ioredis';
5-
import { RedLockModule } from '../red-lock/red-lock.module';
65

76
// 의존성 주입할 때 redis client를 식별할 토큰
87
const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT';
98

109
@Module({
11-
imports: [ConfigModule, forwardRef(() => RedLockModule)], // ConfigModule 추가
10+
imports: [ConfigModule], // ConfigModule 추가
1211
providers: [
1312
RedisService,
1413
{

apps/backend/src/redis/redis.service.ts

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Injectable } from '@nestjs/common';
22
import { Inject } from '@nestjs/common';
33
import Redis from 'ioredis';
4-
import Redlock from 'redlock';
54
const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT';
65
const RED_LOCK_TOKEN = 'RED_LOCK';
76

@@ -27,7 +26,6 @@ export type RedisEdge = {
2726
export class RedisService {
2827
constructor(
2928
@Inject(REDIS_CLIENT_TOKEN) private readonly redisClient: Redis,
30-
@Inject(RED_LOCK_TOKEN) private readonly redisLock: Redlock,
3129
) {}
3230

3331
async getAllKeys(pattern) {
@@ -46,32 +44,14 @@ export class RedisService {
4644
}
4745

4846
async set(key: string, value: object) {
49-
// 락을 획득할 때까지 기다린다.
50-
const lock = await this.redisLock.acquire([`user:${key}`], 1000);
51-
try {
52-
await this.redisClient.hset(key, Object.entries(value));
53-
} finally {
54-
lock.release();
55-
}
47+
await this.redisClient.hset(key, Object.entries(value));
5648
}
5749

5850
async setField(key: string, field: string, value: string) {
59-
// 락을 획득할 때까지 기다린다.
60-
const lock = await this.redisLock.acquire([`user:${key}`], 1000);
61-
try {
62-
return await this.redisClient.hset(key, field, value);
63-
} finally {
64-
lock.release();
65-
}
51+
return await this.redisClient.hset(key, field, value);
6652
}
6753

6854
async delete(key: string) {
69-
// 락을 획득할 때까지 기다린다.
70-
const lock = await this.redisLock.acquire([`user:${key}`], 1000);
71-
try {
72-
return await this.redisClient.del(key);
73-
} finally {
74-
lock.release();
75-
}
55+
return await this.redisClient.del(key);
7656
}
7757
}

apps/backend/src/tasks/tasks.service.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class TasksService {
2424
@InjectDataSource() private readonly dataSource: DataSource,
2525
) {}
2626

27-
@Cron(CronExpression.EVERY_10_SECONDS)
27+
@Cron(CronExpression.EVERY_30_SECONDS)
2828
async handleCron() {
2929
this.logger.log('스케줄러 시작');
3030
// 시작 시간
@@ -56,6 +56,8 @@ export class TasksService {
5656
}
5757

5858
async migratePage(key: string) {
59+
// 낙관적 락 적용
60+
await this.redisClient.watch(key);
5961
const data = await this.redisClient.hgetall(key);
6062
const redisData = Object.fromEntries(
6163
Object.entries(data).map(([field, value]) => [field, value]),
@@ -110,6 +112,8 @@ export class TasksService {
110112
}
111113

112114
async migrateNode(key: string) {
115+
// 낙관적 락 적용
116+
await this.redisClient.watch(key);
113117
const data = await this.redisClient.hgetall(key);
114118
const redisData = Object.fromEntries(
115119
Object.entries(data).map(([field, value]) => [field, value]),
@@ -163,6 +167,8 @@ export class TasksService {
163167
}
164168

165169
async migrateEdge(key: string) {
170+
// 낙관적 락 적용
171+
await this.redisClient.watch(key);
166172
const data = await this.redisClient.hgetall(key);
167173
const redisData = Object.fromEntries(
168174
Object.entries(data).map(([field, value]) => [field, value]),

apps/websocket/src/red-lock/red-lock.module.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

apps/websocket/src/redis/redis.module.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import { Module, forwardRef } from '@nestjs/common';
1+
import { Module } from '@nestjs/common';
22
import { ConfigModule, ConfigService } from '@nestjs/config';
33
import { RedisService } from './redis.service';
44
import Redis from 'ioredis';
5-
import { RedLockModule } from '../red-lock/red-lock.module';
65

76
// 의존성 주입할 때 redis client를 식별할 토큰
87
const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT';
98

109
@Module({
11-
imports: [ConfigModule, forwardRef(() => RedLockModule)], // ConfigModule 추가
10+
imports: [ConfigModule], // ConfigModule 추가
1211
providers: [
1312
RedisService,
1413
{
Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { Injectable } from '@nestjs/common';
22
import { Inject } from '@nestjs/common';
33
import Redis from 'ioredis';
4-
import Redlock from 'redlock';
54
const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT';
6-
const RED_LOCK_TOKEN = 'RED_LOCK';
75

86
type RedisPage = {
97
title?: string;
@@ -14,7 +12,6 @@ type RedisPage = {
1412
export class RedisService {
1513
constructor(
1614
@Inject(REDIS_CLIENT_TOKEN) private readonly redisClient: Redis,
17-
@Inject(RED_LOCK_TOKEN) private readonly redisLock: Redlock,
1815
) {}
1916

2017
createStream() {
@@ -29,40 +26,22 @@ export class RedisService {
2926
}
3027

3128
async set(key: string, value: object) {
32-
// 락을 획득할 때까지 기다린다.
33-
const lock = await this.redisLock.acquire([`user:${key}`], 1000);
34-
try {
35-
await this.redisClient.hset(key, Object.entries(value));
36-
} finally {
37-
lock.release();
38-
}
29+
await this.redisClient.hset(key, Object.entries(value));
3930
}
4031

4132
async setFields(key: string, map: Record<string, string>) {
42-
// 락을 획득할 때까지 기다린다.
43-
const lock = await this.redisLock.acquire([`user:${key}`], 1000);
44-
try {
45-
// return await this.redisClient.hset(key, );
46-
// fieldValueArr 배열을 평탄화하여 [field, value, field, value, ...] 형태로 변환
47-
const flattenedFields = Object.entries(map).flatMap(([field, value]) => [
48-
field,
49-
value,
50-
]);
51-
52-
// hset을 통해 한 번에 여러 필드를 설정
53-
return await this.redisClient.hset(key, ...flattenedFields);
54-
} finally {
55-
lock.release();
56-
}
33+
// return await this.redisClient.hset(key, );
34+
// fieldValueArr 배열을 평탄화하여 [field, value, field, value, ...] 형태로 변환
35+
const flattenedFields = Object.entries(map).flatMap(([field, value]) => [
36+
field,
37+
value,
38+
]);
39+
40+
// hset을 통해 한 번에 여러 필드를 설정
41+
return await this.redisClient.hset(key, ...flattenedFields);
5742
}
5843

5944
async delete(key: string) {
60-
// 락을 획득할 때까지 기다린다.
61-
const lock = await this.redisLock.acquire([`user:${key}`], 1000);
62-
try {
63-
return await this.redisClient.del(key);
64-
} finally {
65-
lock.release();
66-
}
45+
return await this.redisClient.del(key);
6746
}
6847
}

0 commit comments

Comments
 (0)