Биз жөнүндөБлогБайланыш
Backend иштеп чыгуу2026-ж., 17-апрель 8 мин 126Жаңыланды: 2026-ж., 22-июнь

Redis маалымат структуралары Node.js мисалдары менен: String, Hash, List, Sorted Set

AunimedaAunimeda
📋 Мазмуну

Redis'ти кэш катары гана билген иштеп чыгуучулар анын потенциалынын 20%ын гана колдонот. Ал маалымат базасы, message broker, rate limiter, job queue, session store, real-time leaderboard болушу мүмкүн - баары RAM'да, баары миллисекундтарда.

Бул макалада Redis'тин 4 негизги маалымат структурасын карайбыз: ар бири үчүн Redis буйруктары, Node.js коду (ioredis аркылуу) жана реалдуу колдонмолор.


Орнотуу

npm install ioredis
// src/lib/redis.ts
import Redis from 'ioredis';

const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: Number(process.env.REDIS_PORT) || 6379,
  password: process.env.REDIS_PASSWORD,
  retryStrategy(times) {
    // 3 жолу аракет, андан кийин error
    if (times > 3) return null;
    return Math.min(times * 200, 1000);
  },
  maxRetriesPerRequest: 3,
});

redis.on('error', (err) => console.error('Redis error:', err));
redis.on('connect', () => console.log('Redis connected'));

export default redis;

1. String - Rate Limiter

String - эң жөнөкөй структура. Маани бир баалуулук: текст, сан, JSON сериализацияланган объект.

Маанилүү буйруктар:

SET key value EX 60        # 60 секундтан кийин жок болот
GET key                    # Маанини алуу
INCR key                   # Атомдуу +1
INCRBY key 5               # Атомдуу +5
TTL key                    # Канча секунд калды
EXISTS key                 # Барбы?

Реалдуу колдонмо: Rate Limiter

API endpoint'те IP же user боюнча суроо санын чектөө - спам жана DDoS'тон коргоо.

// src/middleware/rateLimiter.ts
import { Request, Response, NextFunction } from 'express';
import redis from '../lib/redis';

interface RateLimitOptions {
  windowSeconds: number;  // убакыт терезеси
  maxRequests: number;    // макс суроо саны
  keyPrefix?: string;
}

export function createRateLimiter(options: RateLimitOptions) {
  const { windowSeconds, maxRequests, keyPrefix = 'rl' } = options;

  return async (req: Request, res: Response, next: NextFunction) => {
    // IP боюнча же authenticated user'дын ID'си боюнча
    const identifier = req.user?.id || req.ip;
    const key = `${keyPrefix}:${identifier}`;

    try {
      // INCR атомдуу: маани жок болсо 0'дан баштайт, анан +1
      const current = await redis.incr(key);

      if (current === 1) {
        // Биринчи суроо - TTL коюу
        await redis.expire(key, windowSeconds);
      }

      // Response headers'та limit маалыматы
      const ttl = await redis.ttl(key);
      res.setHeader('X-RateLimit-Limit', maxRequests);
      res.setHeader('X-RateLimit-Remaining', Math.max(0, maxRequests - current));
      res.setHeader('X-RateLimit-Reset', Date.now() + ttl * 1000);

      if (current > maxRequests) {
        return res.status(429).json({
          error: 'Too Many Requests',
          message: `${windowSeconds} секундда ${maxRequests} суродон ашырдыңыз`,
          retryAfter: ttl,
        });
      }

      next();
    } catch (err) {
      // Redis жеткиликсиз болсо - блоктобой өткөрүп жибер
      console.error('Rate limiter error:', err);
      next();
    }
  };
}

// Колдонуу:
// app.use('/api/auth/login', createRateLimiter({ windowSeconds: 60, maxRequests: 5 }));
// app.use('/api/', createRateLimiter({ windowSeconds: 60, maxRequests: 100 }));

2. Hash - Объект кэштөө

Hash - бул бир key'дин астындагы field-value жуптарынын жыйнагы. JSON stringify/parse'сыз, тике объект сыяктуу.

Маанилүү буйруктар:

HSET user:42 name "Азамат" email "azamat@example.com" role "admin"
HGET user:42 name           # "Азамат"
HGETALL user:42             # Бардык fields
HMGET user:42 name email    # Бир нече fields
HDEL user:42 role           # Бир field'ды жок кылуу
HEXISTS user:42 email       # Field барбы?
HSET user:42 loginCount 0
HINCRBY user:42 loginCount 1  # Атомдуу +1

Реалдуу колдонмо: Колдонуучу профилин кэштөө

// src/services/userCacheService.ts
import redis from '../lib/redis';
import { User } from '../models/User';

const USER_CACHE_TTL = 3600; // 1 саат
const USER_KEY = (id: number) => `user:${id}`;

interface CachedUser {
  id: number;
  name: string;
  email: string;
  role: string;
  avatarUrl: string;
}

export async function getCachedUser(userId: number): Promise<CachedUser | null> {
  const key = USER_KEY(userId);

  // Hash'тан бардык fields'ты алуу
  const data = await redis.hgetall(key);

  // Бош объект → кэш жок
  if (!data || Object.keys(data).length === 0) {
    return null;
  }

  return {
    id: Number(data.id),
    name: data.name,
    email: data.email,
    role: data.role,
    avatarUrl: data.avatarUrl,
  };
}

export async function setCachedUser(user: CachedUser): Promise<void> {
  const key = USER_KEY(user.id);

  // Pipeline: бир network round-trip'та бир нече команда
  const pipeline = redis.pipeline();

  pipeline.hset(key, {
    id: user.id,
    name: user.name,
    email: user.email,
    role: user.role,
    avatarUrl: user.avatarUrl,
  });
  pipeline.expire(key, USER_CACHE_TTL);

  await pipeline.exec();
}

export async function invalidateUserCache(userId: number): Promise<void> {
  await redis.del(USER_KEY(userId));
}

// Middleware: User маалыматын кэштен же DB'ден алуу
export async function getUserWithCache(userId: number): Promise<CachedUser> {
  // 1. Кэштен текшер
  const cached = await getCachedUser(userId);
  if (cached) return cached;

  // 2. DB'ден алуу
  const user = await User.findByPk(userId, {
    attributes: ['id', 'name', 'email', 'role', 'avatarUrl'],
  });
  if (!user) throw new Error('User not found');

  const userData: CachedUser = {
    id: user.id,
    name: user.name,
    email: user.email,
    role: user.role,
    avatarUrl: user.avatarUrl,
  };

  // 3. Кэшке жаз
  await setCachedUser(userData);

  return userData;
}

Эмне үчүн JSON.stringify ордуна Hash?

Hash'та жеке полелерди жаңыртсаңыз болот - бүтүндөй объектти кайра жаздырбай:

// Мисалы, avatar гана өзгөрдү:
await redis.hset(USER_KEY(userId), 'avatarUrl', newAvatarUrl);
// JSON approach'та: db'ден окуп, stringify, кайра SET - 3 операция

3. List - Job Queue

List - маанилердин тизилген жыйнагы (linked list). FIFO queue (биринчи кирген - биринчи чыккан) же LIFO stack катары иштейт.

Маанилүү буйруктар:

RPUSH queue:emails '{"to":"user@kg","subject":"..."}'  # Оңго кош
LPUSH queue:emails '{"priority":true,...}'              # Солго кош (башка)
LPOP queue:emails         # Солдон алып чык (FIFO)
RPOP queue:emails         # Оңдон алып чык (LIFO)
BLPOP queue:emails 30     # Blocking: маани пайда болгунча 30с күт
LLEN queue:emails         # Узундугу
LRANGE queue:emails 0 -1  # Баарын карау (debug)

Реалдуу колдонмо: Email Job Queue

// src/services/emailQueue.ts
import redis from '../lib/redis';

interface EmailJob {
  to: string;
  subject: string;
  template: string;
  data: Record<string, unknown>;
  attempts?: number;
}

const QUEUE_KEY = 'queue:emails';
const DEAD_LETTER_KEY = 'queue:emails:failed';
const MAX_ATTEMPTS = 3;

// Producer: queue'га кош
export async function enqueueEmail(job: EmailJob): Promise<void> {
  const payload = JSON.stringify({ ...job, attempts: 0, createdAt: Date.now() });
  await redis.rpush(QUEUE_KEY, payload);
  console.log(`Email queued to ${job.to}`);
}

// Consumer: queue'дан алып иштет
export async function startEmailWorker(): Promise<void> {
  console.log('Email worker started, waiting for jobs...');

  while (true) {
    try {
      // BLPOP: 30 секунд күт, маани пайда болсо алып чык
      // [queueName, value] форматта кайтарат
      const result = await redis.blpop(QUEUE_KEY, 30);

      if (!result) {
        // Timeout - кайра күт
        continue;
      }

      const [, payload] = result;
      const job: EmailJob & { attempts: number; createdAt: number } = JSON.parse(payload);

      try {
        await sendEmail(job);
        console.log(`Email sent to ${job.to}`);
      } catch (sendError) {
        console.error(`Failed to send email to ${job.to}:`, sendError);

        // Retry логикасы
        if (job.attempts < MAX_ATTEMPTS - 1) {
          const retryJob = { ...job, attempts: job.attempts + 1 };
          // 5 секунд кийин кайра аракет (exponential backoff)
          setTimeout(async () => {
            await redis.rpush(QUEUE_KEY, JSON.stringify(retryJob));
          }, 5000 * Math.pow(2, job.attempts));
        } else {
          // Dead letter queue: 3 аракет ийгиликсиз болсо
          await redis.rpush(DEAD_LETTER_KEY, payload);
          console.error(`Job moved to dead letter queue: ${job.to}`);
        }
      }
    } catch (err) {
      console.error('Worker error:', err);
      // 1 секунд күтүп кайра баштоо
      await new Promise(r => setTimeout(r, 1000));
    }
  }
}

async function sendEmail(job: EmailJob): Promise<void> {
  // Nodemailer же башка email провайдер менен жиберүү
  // ...
}

4. Sorted Set - Рейтинг Системасы

Sorted Set - ар бир маани score (сан) менен байланышкан уникалдуу маанилердин жыйнагы. Score боюнча автоматтык сорттолот.

Маанилүү буйруктар:

ZADD leaderboard 1500 "user:42"      # Кош/жаңырт
ZINCRBY leaderboard 100 "user:42"    # +100 балл
ZRANK leaderboard "user:42"          # Орун (0-индекс, аз→чоңго)
ZREVRANK leaderboard "user:42"       # Орун (чоң→аз, 1-орун = 0)
ZSCORE leaderboard "user:42"         # Score'у
ZREVRANGE leaderboard 0 9            # Топ-10 (чоң→аз)
ZREVRANGE leaderboard 0 9 WITHSCORES # Score менен
ZRANGE leaderboard 0 -1 WITHSCORES  # Баары
ZCARD leaderboard                    # Жалпы катышуучу саны

Реалдуу колдонмо: Оюн рейтинги (Leaderboard)

// src/services/leaderboardService.ts
import redis from '../lib/redis';

const LEADERBOARD_KEY = 'leaderboard:game:weekly';
const LEADERBOARD_TTL = 7 * 24 * 3600; // 1 жума

interface LeaderboardEntry {
  userId: string;
  username: string;
  score: number;
  rank: number;
}

// Балл кошуу/жаңыртуу
export async function addScore(userId: string, points: number): Promise<void> {
  // ZINCRBY - атомдуу, race condition жок
  const newScore = await redis.zincrby(LEADERBOARD_KEY, points, `user:${userId}`);
  await redis.expire(LEADERBOARD_KEY, LEADERBOARD_TTL);
  console.log(`User ${userId} new score: ${newScore}`);
}

// Топ-N алуу
export async function getTopPlayers(count: number = 10): Promise<LeaderboardEntry[]> {
  // ZREVRANGE: чоңдон кичигине, WITHSCORES: score менен
  const results = await redis.zrevrange(LEADERBOARD_KEY, 0, count - 1, 'WITHSCORES');

  // Results форматы: ['user:42', '1500', 'user:7', '1200', ...]
  const entries: LeaderboardEntry[] = [];

  for (let i = 0; i < results.length; i += 2) {
    const userId = results[i].replace('user:', '');
    const score = Number(results[i + 1]);
    const rank = i / 2 + 1;

    // Username'ди кэштен алуу (Hash'тан)
    const username = await redis.hget(`user:${userId}`, 'name') || `User ${userId}`;

    entries.push({ userId, username, score, rank });
  }

  return entries;
}

// Конкреттүү колдонуучунун орду
export async function getUserRank(userId: string): Promise<{
  rank: number | null;
  score: number | null;
  totalPlayers: number;
}> {
  const key = `user:${userId}`;

  const [rankResult, scoreResult, totalPlayers] = await Promise.all([
    redis.zrevrank(LEADERBOARD_KEY, key),  // null эгер жок болсо
    redis.zscore(LEADERBOARD_KEY, key),
    redis.zcard(LEADERBOARD_KEY),
  ]);

  return {
    rank: rankResult !== null ? rankResult + 1 : null, // 0-indexed → 1-indexed
    score: scoreResult !== null ? Number(scoreResult) : null,
    totalPlayers,
  };
}

// API endpoint'те колдонуу:
// GET /api/leaderboard/top → getTopPlayers(10)
// GET /api/leaderboard/me  → getUserRank(req.user.id)
// POST /api/game/complete  → addScore(req.user.id, earnedPoints)

Pipeline: бир нече буйрукту бир жолу жиберүү

Ар бир Redis буйругу - бир network round-trip. Жүздөгөн буйрукту жиберишиңиз керек болсо, pipeline колдонуңуз:

// Жай жол: 100 жолу network'ка барат
for (const userId of userIds) {
  await redis.hgetall(`user:${userId}`);
}

// Ылдам жол: 1 жолу network'ка барат
const pipeline = redis.pipeline();
for (const userId of userIds) {
  pipeline.hgetall(`user:${userId}`);
}
const results = await pipeline.exec();
// results: [[error, value], [error, value], ...]

Aunimeda - Redis, Node.js жана scalable backend архитектурасын иштеп чыгат. Тиркемеңизди миңдеген колдонуучуга чейин масштабдуу кылуу боюнча кеңешебиз.

Долбооруңузду талкуулайлы же WhatsApp аркылуу жазыңыз.

Ошондой эле окуңуз

Express.js менен REST API сервер кантип жасоо: нөлдөн production'го чейин (2015)aunimeda
Backend иштеп чыгуу

Express.js менен REST API сервер кантип жасоо: нөлдөн production'го чейин (2015)

Node.js 4 LTS + Express.js 4 - 2015-жылда PHP'га альтернатива катары пайда болду. Биз Бишкекте мобилдик тиркеме үчүн API сервер жасадык. Роутинг, middleware, валидация, JWT авторизация, MySQL - бардыгы бир жерде. Иштеген код мисалдары.

WebSockets vs SSE vs Long Polling: realtime технологиясын кантип тандоо керекaunimeda
Веб-иштеп чыгуу

WebSockets vs SSE vs Long Polling: realtime технологиясын кантип тандоо керек

Чат, кабарлар, заказ статусу - булардын баары реалдуу убакытта жаңыртууну талап кылат. WebSocket, Server-Sent Events жана Long Polling ар башка иштейт. Кайсын качан колдонуу керегин Node.js код мисалдары менен карайбыз.

Node.js vs Bun vs Deno 2026: кайсы JavaScript runtime тандоо керекaunimeda
Веб-иштеп чыгуу

Node.js vs Bun vs Deno 2026: кайсы JavaScript runtime тандоо керек

Bun 1.x продакшн стабилдүү. Deno 2.0 npm колдойт. Node.js 22 TypeScript'ти нативдүү иштетет. Реалдуу benchmark'тар, экосистема салыштыруусу жана Кыргызстандагы долбоорлор үчүн конкреттүү сунуштамалар.

Бизнесиңизге IT иштеп чыгуу керекпи?

Веб-сайттарды, мобилдик тиркемелерди жана AI чечимдерин иштеп чыгабыз. Акысыз консультация.

Консультация алуу Бардык макалалар