Налаштування Redis для кешування веб-додатку
Redis — це не просто кеш. Це структури даних у пам'яті: рядки, хеші, списки, множини, відсортовані множини, потоки, pub/sub. Правильний вибір структури часто дає чистіше рішення, ніж спроба зробити те ж саме в реляційній базі.
Встановлення Redis 7.x
# Ubuntu 22.04+
apt install -y lsb-release curl gpg
curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" > /etc/apt/sources.list.d/redis.list
apt update && apt install -y redis
/etc/redis/redis.conf — ключові параметри
# Мережа
bind 127.0.0.1 ::1
port 6379
requirepass your_strong_password_here
# Пам'ять
maxmemory 2gb
maxmemory-policy allkeys-lru # витіснити найменш часто використовувані ключі
maxmemory-samples 10
# Персистентність для кешу — лише AOF з мінімальним fsync
save "" # вимкнути RDB снімки
appendonly yes
appendfsync everysec # fsync щосекунди
no-appendfsync-on-rewrite yes
# Продуктивність
tcp-keepalive 300
hz 20
lazyfree-lazy-eviction yes # асинхронне витіснення
lazyfree-lazy-expire yes
io-threads 4 # для Redis 6+ на багатоядерних серверах
io-threads-do-reads yes
Шаблони кешування
Cache-aside — найпоширеніший:
// cache.service.ts
import { Redis } from 'ioredis'
export class CacheService {
constructor(private redis: Redis) {}
async getOrSet<T>(
key: string,
ttlSeconds: number,
factory: () => Promise<T>
): Promise<T> {
const cached = await this.redis.get(key)
if (cached !== null) {
return JSON.parse(cached) as T
}
const value = await factory()
await this.redis.setex(key, ttlSeconds, JSON.stringify(value))
return value
}
async invalidate(pattern: string): Promise<void> {
// Використовуємо SCAN, не KEYS — не блокує Redis
let cursor = '0'
do {
const [nextCursor, keys] = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100)
cursor = nextCursor
if (keys.length > 0) {
await this.redis.unlink(...keys) // асинхронне видалення
}
} while (cursor !== '0')
}
}
// Використання
const product = await cache.getOrSet(
`product:${id}`,
300, // 5 хвилин
() => db.products.findOne({ id })
)
// Після оновлення
await cache.invalidate(`product:${id}`)
await cache.invalidate(`products:category:${product.categoryId}:*`)
Write-through — запис у кеш і БД синхронно:
async updateProduct(id: string, data: UpdateProductDto) {
const updated = await db.products.update(id, data)
await redis.setex(`product:${id}`, 600, JSON.stringify(updated))
return updated
}
Сесії користувачів
// session.service.ts
const SESSION_TTL = 86400 * 7 // 7 днів
export class SessionService {
constructor(private redis: Redis) {}
async create(userId: string, metadata: SessionMeta): Promise<string> {
const sessionId = crypto.randomUUID()
const key = `session:${sessionId}`
await this.redis.hset(key, {
userId,
createdAt: Date.now(),
ip: metadata.ip,
userAgent: metadata.userAgent
})
await this.redis.expire(key, SESSION_TTL)
// Додати в список сесій користувача
await this.redis.zadd(`user:${userId}:sessions`, Date.now(), sessionId)
await this.redis.expire(`user:${userId}:sessions`, SESSION_TTL)
return sessionId
}
async get(sessionId: string): Promise<SessionData | null> {
const data = await this.redis.hgetall(`session:${sessionId}`)
if (!Object.keys(data).length) return null
await this.redis.expire(`session:${sessionId}`, SESSION_TTL) // скользящее вікно
return data as SessionData
}
async destroyAll(userId: string): Promise<void> {
const sessionIds = await this.redis.zrange(`user:${userId}:sessions`, 0, -1)
if (sessionIds.length) {
const keys = sessionIds.map(id => `session:${id}`)
await this.redis.unlink(...keys, `user:${userId}:sessions`)
}
}
}
Rate limiting з ковзаючим вікном
async function rateLimit(
redis: Redis,
key: string,
limit: number,
windowMs: number
): Promise<{ allowed: boolean; remaining: number; resetAt: number }> {
const now = Date.now()
const windowStart = now - windowMs
const pipeline = redis.pipeline()
pipeline.zremrangebyscore(key, '-inf', windowStart)
pipeline.zadd(key, now, `${now}-${Math.random()}`)
pipeline.zcard(key)
pipeline.pexpire(key, windowMs)
const results = await pipeline.exec()
const count = results![2][1] as number
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
resetAt: now + windowMs
}
}
Pub/Sub для подій
// publisher.ts
async function publishOrderEvent(event: OrderEvent) {
await redis.publish('orders:events', JSON.stringify(event))
}
// subscriber.ts
const subscriber = redis.duplicate()
await subscriber.subscribe('orders:events')
subscriber.on('message', (channel, message) => {
const event = JSON.parse(message) as OrderEvent
eventBus.emit(event.type, event.payload)
})
Моніторинг
# Поточне навантаження
redis-cli --latency-history -i 1
redis-cli info stats | grep -E "instantaneous_ops|rejected_connections|evicted_keys"
# Використання пам'яті
redis-cli info memory | grep -E "used_memory_human|mem_fragmentation_ratio"
# Повільні команди
redis-cli config set slowlog-log-slower-than 10000 # 10ms
redis-cli slowlog get 10
Sentinel для високої доступності
# sentinel.conf
sentinel monitor mymaster 10.0.0.1 6379 2
sentinel auth-pass mymaster your_password
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
Терміни
Базове налаштування Redis з кешуванням та конфігурацією сесій для додатку: 1 день. Налаштування Sentinel з автоматичною відмовостійкістю: 1–2 дні. Redis Cluster для горизонтального масштабування з шардуванням: 2–3 дні.







