Что такое Redis
Redis — in-memory хранилище данных, используемое для кэширования, сессий, очередей и pub/sub. Скорость Redis измеряется микросекундами, что делает его идеальным для высоконагруженных приложений.
Подключение к Redis
import Redis from 'ioredis';
// Простое подключение
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
db: 0
});
// С retry стратегией
const redis = new Redis({
host: 'localhost',
port: 6379,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
maxRetriesPerRequest: 3
});
// Проверка подключения
redis.on('connect', () => console.log('Redis connected'));
redis.on('error', (err) => console.error('Redis error:', err));
Кэширование данных
// Простой кэш
async function getUser(id: string): Promise<User> {
const cacheKey = `user:${id}`;
// Проверяем кэш
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Загружаем из БД
const user = await prisma.user.findUnique({ where: { id } });
// Сохраняем в кэш на 1 час
await redis.setex(cacheKey, 3600, JSON.stringify(user));
return user;
}
// Универсальная функция кэширования
async function cached<T>(
key: string,
ttl: number,
fn: () => Promise<T>
): Promise<T> {
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
const data = await fn();
await redis.setex(key, ttl, JSON.stringify(data));
return data;
}
// Использование
const products = await cached(
'products:featured',
300, // 5 минут
() => prisma.product.findMany({ where: { featured: true } })
);
Инвалидация кэша
// Удаление конкретного ключа
async function updateUser(id: string, data: UpdateUserData) {
const user = await prisma.user.update({
where: { id },
data
});
// Инвалидируем кэш
await redis.del(`user:${id}`);
return user;
}
// Удаление по паттерну
async function invalidateUserCache(userId: string) {
const keys = await redis.keys(`user:${userId}:*`);
if (keys.length > 0) {
await redis.del(...keys);
}
}
// Тегированный кэш
async function setWithTags(key: string, value: any, tags: string[], ttl: number) {
const pipeline = redis.pipeline();
pipeline.setex(key, ttl, JSON.stringify(value));
for (const tag of tags) {
pipeline.sadd(`tag:${tag}`, key);
pipeline.expire(`tag:${tag}`, ttl);
}
await pipeline.exec();
}
async function invalidateByTag(tag: string) {
const keys = await redis.smembers(`tag:${tag}`);
if (keys.length > 0) {
await redis.del(...keys, `tag:${tag}`);
}
}
// Использование
await setWithTags(
`product:${id}`,
product,
['products', `category:${product.categoryId}`],
3600
);
// Инвалидация всех продуктов категории
await invalidateByTag(`category:${categoryId}`);
Сессии
import session from 'express-session';
import RedisStore from 'connect-redis';
// Express сессии в Redis
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 часа
}
}));
// Кастомное хранение сессий
class SessionManager {
private prefix = 'session:';
private ttl = 86400; // 24 часа
async create(userId: string, data: SessionData): Promise<string> {
const sessionId = crypto.randomUUID();
const key = this.prefix + sessionId;
await redis.setex(key, this.ttl, JSON.stringify({
userId,
...data,
createdAt: Date.now()
}));
return sessionId;
}
async get(sessionId: string): Promise<SessionData | null> {
const data = await redis.get(this.prefix + sessionId);
return data ? JSON.parse(data) : null;
}
async destroy(sessionId: string): Promise<void> {
await redis.del(this.prefix + sessionId);
}
async refresh(sessionId: string): Promise<void> {
await redis.expire(this.prefix + sessionId, this.ttl);
}
}
Pub/Sub
// Publisher
const publisher = new Redis();
async function publishEvent(channel: string, data: any) {
await publisher.publish(channel, JSON.stringify(data));
}
// Subscriber
const subscriber = new Redis();
subscriber.subscribe('orders', 'notifications');
subscriber.on('message', (channel, message) => {
const data = JSON.parse(message);
switch (channel) {
case 'orders':
handleNewOrder(data);
break;
case 'notifications':
sendNotification(data);
break;
}
});
// Использование
await publishEvent('orders', {
orderId: '123',
userId: 'user-1',
total: 1500
});
Очереди задач
import Bull from 'bull';
// Создание очереди
const emailQueue = new Bull('email', {
redis: {
host: 'localhost',
port: 6379
}
});
// Добавление задачи
await emailQueue.add('welcome', {
to: 'user@example.com',
subject: 'Welcome!',
template: 'welcome'
}, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000
},
removeOnComplete: true
});
// Обработка задач
emailQueue.process('welcome', async (job) => {
const { to, subject, template } = job.data;
await sendEmail(to, subject, template);
return { sent: true };
});
// События
emailQueue.on('completed', (job, result) => {
console.log(`Job ${job.id} completed`);
});
emailQueue.on('failed', (job, err) => {
console.error(`Job ${job.id} failed:`, err);
});
Rate Limiting
async function rateLimit(
key: string,
limit: number,
window: number
): Promise<boolean> {
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, window);
}
return current <= limit;
}
// Middleware
async function rateLimitMiddleware(req, res, next) {
const key = `ratelimit:${req.ip}`;
const allowed = await rateLimit(key, 100, 60); // 100 запросов в минуту
if (!allowed) {
return res.status(429).json({ error: 'Too many requests' });
}
next();
}
Заключение
Redis — незаменимый инструмент для высоконагруженных приложений. Кэширование ускоряет ответы, сессии масштабируются, очереди разгружают основной процесс. Правильная инвалидация кэша — ключ к консистентности данных.
Redis — это не просто кэш. Это универсальный инструмент для pub/sub, очередей, rate limiting и многого другого.
