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 аркылуу жазыңыз.