مصطفی جعفرزاده
مصطفی جعفرزاده
خواندن ۱۰ دقیقه·۱۱ روز پیش

افزایش سرعت و کارایی با سیستم کش پیشرفته در NestJS: چگونه از درخت AVL و Redis استفاده کنیم؟

در دنیای امروز، سرعت و کارایی در پاسخگویی به درخواست‌ها برای سیستم‌های بزرگ و پر ترافیک از اهمیت بالایی برخوردار است. سیستم‌های آنلاین مانند فروشگاه‌های اینترنتی، شبکه‌های اجتماعی و خدمات بانکی با حجم بالایی از داده‌ها و درخواست‌های کاربران مواجه هستند. این تقاضای بالا نه تنها بار سنگینی بر سرورها و پایگاه‌های داده ایجاد می‌کند، بلکه می‌تواند تجربه کاربری را نیز به طور قابل توجهی تحت تأثیر قرار دهد. در این راستا، پیاده‌سازی یک سیستم کش می‌تواند راه‌حلی مؤثر برای بهبود عملکرد و کاهش بار منابع باشد.

در این مقاله، ما به بررسی پیاده‌سازی یک سیستم کش پیشرفته می‌پردازیم که ترکیبی از درخت‌های AVL و Redis است. این سیستم شامل مکانیزم‌های امنیتی، مدیریت TTL (زمان انقضا) و ادغام با Redis برای بهبود عملکرد و انعطاف‌پذیری می‌باشد. هدف این است که با استفاده از این ترکیب، بتوانیم از مزایای هر دو فناوری بهره‌مند شویم و نقاط ضعف آن‌ها را برطرف کنیم.

*نکته مهم:این مفاله با کمک هوش مصنوعی توسعه داده شده است

مزایا و معایب ترکیب سیستم کش مبتنی بر درخت AVL با Redis

مزایا:

1. بهبود کارایی حافظه:

- مدیریت هوشمندانه TTL: با استفاده از درخت AVL برای مدیریت انقضای داده‌ها، می‌توان مصرف حافظه را بهینه‌تر کرد و از نگهداری داده‌های منسوخ جلوگیری نمود. این امر به ویژه در مواردی که داده‌ها به سرعت تغییر می‌کنند و نیاز به انقضای دقیق دارند، مفید است.

2. افزایش امنیت:

- اعتبارسنجی توکن: اضافه کردن مکانیزم اعتبارسنجی مبتنی بر توکن به امنیت Redis افزوده می‌شود. این لایه امنیتی اضافی از دسترسی غیرمجاز به کش جلوگیری می‌کند و امنیت کلی سیستم را تقویت می‌کند.

3. مدیریت TTL پیشرفته‌تر:

- سیاست‌های انقضای سفارشی: درخت AVL امکان پیاده‌سازی سیاست‌های انقضای پیچیده‌تر و متناسب با نیازهای خاص برنامه را فراهم می‌کند که ممکن است Redis به صورت پیش‌فرض ارائه ندهد.

4. تنوع ساختارهای داده‌ای:

- ساختار درخت متوازن: درخت AVL به عنوان یک ساختار داده‌ای متوازن، می‌تواند برای برخی کاربردها که نیاز به جستجوهای سریع و مرتب‌سازی دارند، عملکرد بهتری نسبت به ساختارهای داده‌ای پیش‌فرض Redis ارائه دهد.

5. افزایش انعطاف‌پذیری و سفارشی‌سازی:

- سفارشی‌سازی بیشتر: ترکیب دو سیستم امکان سفارشی‌سازی بیشتری را فراهم می‌کند و می‌توان راه‌حل‌هایی دقیق‌تر و متناسب با نیازهای خاص توسعه داد.

معایب:

1. افزایش پیچیدگی معماری:

- مدیریت دو سیستم کش: استفاده همزمان از Redis و سیستم کش مبتنی بر درخت AVL، پیچیدگی معماری را افزایش می‌دهد و نیاز به مدیریت هماهنگ بین دو سیستم دارد.

2. افزایش سربار (Overhead) زمانی:

- تاخیر اضافی: افزودن یک لایه کشینگ اضافی ممکن است تاخیری ایجاد کند. باید اطمینان حاصل شود که مزایای عملکرد از این تاخیرها بیشتر است.

3. نگهداری و هماهنگی داده‌ها:

- تداوم داده‌ها: حفظ تداوم و هماهنگی بین Redis و درخت AVL برای جلوگیری از ناسازگاری داده‌ها نیازمند مکانیزم‌های هماهنگی پیچیده است.

4. هزینه توسعه و نگهداری:

- افزایش هزینه‌ها: توسعه و نگهداری دو سیستم کش نیازمند منابع بیشتر و تخصص‌های متفاوت است که ممکن است هزینه‌های کلی پروژه را افزایش دهد.

5. پیچیدگی امنیتی:

- هماهنگی سیاست‌های امنیتی: اطمینان از اینکه سیاست‌های امنیتی به درستی و هماهنگ در هر دو سیستم پیاده‌سازی شده‌اند، می‌تواند چالش‌برانگیز باشد.

پیاده‌سازی سیستم کش با استفاده از درخت AVL و Redis

در ادامه، به معرفی پیاده‌سازی حرفه‌ای این سیستم کش می‌پردازیم. این پیاده‌سازی شامل درخت AVL برای مدیریت داده‌ها با قابلیت TTL و Redis برای ذخیره‌سازی سریع داده‌ها می‌باشد.

1. درخت AVL با TTL

ابتدا، پیاده‌سازی درخت AVL با قابلیت مدیریت TTL را انجام می‌دهیم.

// src/utils/avltree.ts
export class AVLNode {
key: string;
value: any;
ttl: number; // زمان انقضا به میلی‌ثانیه
height: number;
left: AVLNode | null;
right: AVLNode | null;
constructor(key: string, value: any, ttl: number) {
this.key = key;
this.value = value;
this.ttl = Date.now() + ttl;
this.height = 1;
this.left = null;
this.right = null;
}
isExpired(): boolean {
return Date.now() > this.ttl;
}
}
export class AVLTree {
private root: AVLNode | null;
constructor() {
this.root = null;
}
private getHeight(node: AVLNode | null): number {
return node ? node.height : 0;
}
private updateHeight(node: AVLNode): void {
node.height = 1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));
}
private rotateRight(y: AVLNode): AVLNode {
const x = y.left!;
y.left = x.right;
x.right = y;
this.updateHeight(y);
this.updateHeight(x);
return x;
}
private rotateLeft(x: AVLNode): AVLNode {
const y = x.right!;
x.right = y.left;
y.left = x;
this.updateHeight(x);
this.updateHeight(y);
return y;
}
private getBalance(node: AVLNode): number {
return node ? this.getHeight(node.left) - this.getHeight(node.right) : 0;
}
insert(key: string, value: any, ttl: number): void {
this.root = this.insertNode(this.root, key, value, ttl);
}
private insertNode(node: AVLNode | null, key: string, value: any, ttl: number): AVLNode {
if (!node) return new AVLNode(key, value, ttl);
if (key < node.key) {
node.left = this.insertNode(node.left, key, value, ttl);
} else if (key > node.key) {
node.right = this.insertNode(node.right, key, value, ttl);
} else {
node.value = value;
node.ttl = Date.now() + ttl;
return node;
}
this.updateHeight(node);
const balance = this.getBalance(node);
// تنظیم تعادل درخت
if (balance > 1 && key < node.left!.key) return this.rotateRight(node);
if (balance < -1 && key > node.right!.key) return this.rotateLeft(node);
if (balance > 1 && key > node.left!.key) {
node.left = this.rotateLeft(node.left!);
return this.rotateRight(node);
}
if (balance < -1 && key < node.right!.key) {
node.right = this.rotateRight(node.right!);
return this.rotateLeft(node);
}
return node;
}
search(key: string): any {
let node = this.root;
while (node) {
if (node.isExpired()) {
this.delete(key);
return null;
}
if (key === node.key) return node.value;
node = key < node.key ? node.left : node.right;
}
return null;
}
delete(key: string): void {
this.root = this.deleteNode(this.root, key);
}
private deleteNode(node: AVLNode | null, key: string): AVLNode | null {
if (!node) return null;
if (key < node.key) {
node.left = this.deleteNode(node.left, key);
} else if (key > node.key) {
node.right = this.deleteNode(node.right, key);
} else {
if (!node.left || !node.right) return node.left || node.right;
let minLargerNode = node.right;
while (minLargerNode.left) minLargerNode = minLargerNode.left;
node.key = minLargerNode.key;
node.value = minLargerNode.value;
node.ttl = minLargerNode.ttl;
node.right = this.deleteNode(node.right, minLargerNode.key);
}
this.updateHeight(node);
const balance = this.getBalance(node);
if (balance > 1 && this.getBalance(node.left!) >= 0) return this.rotateRight(node);
if (balance < -1 && this.getBalance(node.right!) <= 0) return this.rotateLeft(node);
if (balance > 1 && this.getBalance(node.left!) < 0) {
node.left = this.rotateLeft(node.left!);
return this.rotateRight(node);
}
if (balance < -1 && this.getBalance(node.right!) > 0) {
node.right = this.rotateRight(node.right!);
return this.rotateLeft(node);
}
return node;
}
}

2. سرویس کش (CacheService) با ادغام Redis

در این بخش، سرویس کش را پیاده‌سازی می‌کنیم که از درخت AVL و Redis برای مدیریت کش استفاده می‌کند. همچنین مکانیزم اعتبارسنجی توکن را نیز اضافه می‌کنیم.

// src/cache/cache.service.ts
import { Injectable, UnauthorizedException, InternalServerErrorException } from '@nestjs/common';
import { AVLTree } from '../utils/avltree';
import { InjectRedis, Redis } from '@nestjs-modules/ioredis';
@Injectable()
export class CacheService {
private avlTree: AVLTree;
private authorizedTokens: Set<string> = new Set(['your_authorized_token']); // توکن‌های مجاز
constructor(@InjectRedis() private readonly redis: Redis) {
this.avlTree = new AVLTree();
}
validateToken(token: string): void {
if (!this.authorizedTokens.has(token)) {
throw new UnauthorizedException('Invalid access token');
}
}
async set(key: string, value: any, ttl: number, token: string): Promise<void> {
this.validateToken(token);
try {
// ذخیره در Redis
await this.redis.set(key, JSON.stringify(value), 'PX', ttl);
// ذخیره در AVL Tree
this.avlTree.insert(key, value, ttl);
} catch (error) {
throw new InternalServerErrorException('Failed to set cache');
}
}
async get(key: string, token: string): Promise<any> {
this.validateToken(token);
try {
// ابتدا تلاش می‌کنیم از Redis دریافت کنیم
const redisValue = await this.redis.get(key);
if (redisValue) {
return JSON.parse(redisValue);
}
// اگر در Redis نبود، از AVL Tree دریافت می‌کنیم
const avlValue = this.avlTree.search(key);
if (avlValue) {
// ذخیره مجدد در Redis برای سرعت بیشتر
// فرض می‌کنیم TTL باقی‌مانده را در AVL Tree نگه داشته‌ایم
// برای ساده‌سازی، TTL جدیدی قرار می‌دهیم
const newTtl = 60000; // 60 ثانیه به عنوان مثال
await this.redis.set(key, JSON.stringify(avlValue), 'PX', newTtl);
return avlValue;
}
return null;
} catch (error) {
throw new InternalServerErrorException('Failed to get cache');
}
}
async delete(key: string, token: string): Promise<void> {
this.validateToken(token);
try {
// حذف از Redis
await this.redis.del(key);
// حذف از AVL Tree
this.avlTree.delete(key);
} catch (error) {
throw new InternalServerErrorException('Failed to delete cache');
}
}
}

3. API (CacheController)

کنترلر برای مدیریت درخواست‌های API به سرویس کش.

// src/cache/cache.controller.ts
import { Controller, Get, Post, Delete, Body, Param, Query, HttpCode, HttpStatus } from '@nestjs/common';
import { CacheService } from './cache.service';
class SetCacheDto {
key: string;
value: any;
ttl: number; // میلی‌ثانیه
token: string;
}
@Controller('cache')
export class CacheController {
constructor(private readonly cacheService: CacheService) {}
@Post('set')
@HttpCode(HttpStatus.CREATED)
async setCache(@Body() body: SetCacheDto) {
await this.cacheService.set(body.key, body.value, body.ttl, body.token);
return { message: 'Data cached successfully' };
}
@Get('get/:key')
async getCache(@Param('key') key: string, @Query('token') token: string) {
const value = await this.cacheService.get(key, token);
return value ? { value } : { message: 'Key not found or expired' };
}
@Delete('delete/:key')
@HttpCode(HttpStatus.NO_CONTENT)
async deleteCache(@Param('key') key: string, @Query('token') token: string) {
await this.cacheService.delete(key, token);
return { message: 'Key deleted successfully' };
}
}

4. ماژول کش (CacheModule)

تعریف ماژول کش که سرویس و کنترلر را به هم متصل می‌کند و Redis را تزریق می‌کند.

// src/cache/cache.module.ts
import { Module } from '@nestjs/common';
import { CacheService } from './cache.service';
import { CacheController } from './cache.controller';
import { RedisModule } from '@nestjs-modules/ioredis';
@Module({
imports: [
RedisModule.forRoot({
config: {
host: 'localhost',
port: 6379,
// سایر تنظیمات Redis
},
}),
],
providers: [CacheService],
controllers: [CacheController],
})
export class CacheModule {}

5. پیکربندی Redis

برای استفاده از Redis در پروژه NestJS، از بسته `@nestjs-modules/ioredis` استفاده می‌کنیم. ابتدا این بسته را نصب کنید:

npm install @nestjs-modules/ioredis ioredis

سپس تنظیمات Redis را در CacheModule به صورت بالا انجام دهید. اگر نیاز به پیکربندی پیشرفته‌تری دارید، می‌توانید از فایل‌های پیکربندی جداگانه استفاده کنید.

6. مکانیزم اعتبارسنجی توکن

برای مدیریت توکن‌ها و اعتبارسنجی آن‌ها، می‌توان از استراتژی‌های مختلفی استفاده کرد. در این پیاده‌سازی ساده، توکن‌ها در یک مجموعه ثابت نگهداری می‌شوند. برای پروژه‌های بزرگ‌تر، توصیه می‌شود از JWT یا سایر روش‌های امنیتی پیشرفته‌تر استفاده کنید.

7. مدیریت خطاها و اعتبارسنجی ورودی‌ها

در این پیاده‌سازی، از کلاس‌های DTO برای اعتبارسنجی ورودی‌ها و مدیریت خطاها استفاده شده است. همچنین، در سرویس کش از مدیریت خطاهای عمومی استفاده شده است تا از بروز مشکلات ناخواسته جلوگیری شود.

8. فایل اصلی ماژول برنامه (AppModule)

در نهایت، ماژول کش را به ماژول اصلی برنامه اضافه می‌کنیم.

// src/app.module.ts
import { Module } from '@nestjs/common';
import { CacheModule } from './cache/cache.module';
@Module({
imports: [CacheModule],
controllers: [],
providers: [],
})
export class AppModule {}

9. فایل اصلی برنامه (main.ts)

فایل اصلی برنامه که NestJS را اجرا می‌کند.

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// استفاده از ValidationPipe برای اعتبارسنجی ورودی‌ها
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
await app.listen(3000);
}
bootstrap();

10. تست و اجرا

پس از پیاده‌سازی تمامی قسمت‌ها، می‌توانید برنامه را اجرا کنید و از عملکرد آن اطمینان حاصل کنید.

npm run start:dev

11. نمونه درخواست‌ها

تنظیم کش:

POST http://localhost:3000/cache/set
Content-Type: application/json
{
"key": "user123",
"value": { "name": "Ali", "age": 30 },
"ttl": 60000, // 60 ثانیه
"token": "your_authorized_token"
}

دریافت کش:

GET http://localhost:3000/cache/get/user123?token=your_authorized_token

حذف کش:

DELETE http://localhost:3000/cache/delete/user123?token=your_authorized_token

موارد کاربردی مناسب برای ترکیب Redis و سیستم کش درخت AVL

1. سیستم‌های بانکی و مالی:

- مدیریت نشست‌ها و تراکنش‌های حساس: امنیت بالا و مدیریت دقیق TTL برای داده‌های حساس مالی ضروری است. ترکیب امنیت توکن و مدیریت هوشمند TTL می‌تواند در این حوزه بسیار مفید باشد.

2. پلتفرم‌های تجارت الکترونیک با ترافیک بالا:

- ذخیره‌سازی داده‌های محصول و مدیریت سبد خرید: بهینه‌سازی حافظه و افزایش سرعت دسترسی به داده‌ها برای تجربه کاربری بهتر در فروشگاه‌های آنلاین بزرگ مانند آمازون بسیار مهم است.

3. برنامه‌های پیام‌رسان و شبکه‌های اجتماعی:

  • ذخیره‌سازی وضعیت کاربران در زمان واقعی: نیاز به دسترسی سریع و مدیریت دقیق داده‌ها برای نمایش وضعیت آنلاین/آفلاین کاربران و پیام‌ها.

4. اپلیکیشن‌های هواشناسی و تبادل ارز:

- کشینگ API برای کاهش بار درخواست‌ها: ذخیره‌سازی نتایج محاسبات پیچیده و داده‌های زنده با مدیریت دقیق انقضای داده‌ها برای ارائه اطلاعات به‌روز و سریع به کاربران.

5. سیستم‌های مدیریت محتوا و پلتفرم‌های رسانه‌ای:

- کشینگ صفحات و محتواهای پرمخاطب: بهینه‌سازی دسترسی به محتوای پر بازدید و کاهش بار سرور برای ارائه تجربه کاربری روان‌تر.

6. اپلیکیشن‌های تحلیلی و داشبوردهای بلادرنگ:

- ذخیره‌سازی نتایج تحلیل‌های فوری: ارائه داده‌های تحلیلی سریع و به‌روز با استفاده از کش‌های متعدد برای بهبود کارایی و دقت نتایج.

نتیجه‌گیری

در این مقاله، یک سیستم کش پیشرفته با استفاده از درخت AVL و Redis در فریم‌ورک NestJS پیاده‌سازی شد. این سیستم با ارائه مدیریت TTL پیشرفته، امنیت با توکن، و ادغام با Redis، یک راه‌حل قوی و انعطاف‌پذیر برای برنامه‌های با تقاضای بالا فراهم می‌کند. ترکیب این دو فناوری، مزایای هر دو را به همراه دارد و می‌تواند ضعف‌های Redis را برطرف کرده و عملکرد کلی کشینگ را بهبود بخشد.

nodejsredis
برنامه نویس علاقه مند به طراحی الگوریتم
شاید از این پست‌ها خوشتان بیاید