# Стратегии кэширования SFERA ## 🎯 Обзор Комплексная система кэширования для платформы SFERA, обеспечивающая высокую производительность, снижение нагрузки на внешние API и улучшение пользовательского опыта за счет оптимального кэширования данных различных типов. ## 📊 Архитектура кэширования ```mermaid graph TB A[SFERA Application] --> B[Multi-Layer Cache] B --> C[Browser Cache] B --> D[CDN Cache] B --> E[Application Cache] B --> F[Database Cache] E --> E1[Redis Cache] E --> E2[Memory Cache] E --> E3[Query Cache] F --> F1[PostgreSQL Cache] F --> F2[Connection Pool] G[External APIs] --> H[API Response Cache] H --> H1[Marketplace Cache] H --> H2[DaData Cache] H --> H3[SMS Cache] ``` ## 🔄 Уровни кэширования ### 1. Browser/Client Cache #### HTTP Cache Headers ```typescript // src/lib/cache-headers.ts export const CacheHeaders = { // Статические ресурсы (изображения, CSS, JS) static: { 'Cache-Control': 'public, max-age=31536000, immutable', // 1 год Expires: new Date(Date.now() + 31536000 * 1000).toUTCString(), }, // API данные (редко изменяются) longTerm: { 'Cache-Control': 'public, max-age=3600, s-maxage=3600', // 1 час ETag: true, Vary: 'Accept-Encoding', }, // API данные (часто изменяются) shortTerm: { 'Cache-Control': 'public, max-age=300, s-maxage=300', // 5 минут ETag: true, }, // Приватные данные пользователя private: { 'Cache-Control': 'private, max-age=300', // 5 минут, только браузер ETag: true, }, // Динамические данные (не кэшировать) noCache: { 'Cache-Control': 'no-cache, no-store, must-revalidate', Pragma: 'no-cache', Expires: '0', }, } // Middleware для установки заголовков кэширования export const setCacheHeaders = (type: keyof typeof CacheHeaders) => { return (res: NextResponse) => { const headers = CacheHeaders[type] Object.entries(headers).forEach(([key, value]) => { if (key === 'ETag' && value === true) { // Генерация ETag на основе контента return } res.headers.set(key, value as string) }) return res } } ``` #### Service Worker для кэширования ```javascript // public/sw.js const CACHE_NAME = 'sfera-cache-v1' const STATIC_CACHE = 'sfera-static-v1' const API_CACHE = 'sfera-api-v1' // Статические ресурсы для кэширования const STATIC_RESOURCES = ['/', '/manifest.json', '/offline.html', '/_next/static/css/', '/_next/static/js/', '/icons/'] // Установка Service Worker self.addEventListener('install', (event) => { event.waitUntil( Promise.all([ caches.open(STATIC_CACHE).then((cache) => { return cache.addAll(STATIC_RESOURCES) }), caches.open(API_CACHE), ]), ) }) // Стратегии кэширования self.addEventListener('fetch', (event) => { const { request } = event const url = new URL(request.url) // Статические ресурсы - Cache First if (request.destination === 'image' || request.destination === 'script' || request.destination === 'style') { event.respondWith(cacheFirst(request, STATIC_CACHE)) return } // API запросы - Network First с fallback на cache if (url.pathname.startsWith('/api/')) { event.respondWith(networkFirst(request, API_CACHE)) return } // HTML страницы - Stale While Revalidate if (request.destination === 'document') { event.respondWith(staleWhileRevalidate(request, CACHE_NAME)) return } }) // Cache First стратегия async function cacheFirst(request, cacheName) { const cache = await caches.open(cacheName) const cached = await cache.match(request) if (cached) { return cached } try { const response = await fetch(request) if (response.ok) { cache.put(request, response.clone()) } return response } catch (error) { return new Response('Network error', { status: 408 }) } } // Network First стратегия async function networkFirst(request, cacheName) { const cache = await caches.open(cacheName) try { const response = await fetch(request) if (response.ok) { cache.put(request, response.clone()) } return response } catch (error) { const cached = await cache.match(request) return cached || new Response('Offline', { status: 503 }) } } // Stale While Revalidate стратегия async function staleWhileRevalidate(request, cacheName) { const cache = await caches.open(cacheName) const cached = await cache.match(request) const fetchPromise = fetch(request).then((response) => { if (response.ok) { cache.put(request, response.clone()) } return response }) return cached || fetchPromise } ``` ### 2. Redis Cache #### Конфигурация Redis ```typescript // src/lib/redis.ts import Redis from 'ioredis' export class RedisCache { private redis: Redis private defaultTTL = 3600 // 1 час по умолчанию constructor() { this.redis = new Redis({ host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379'), password: process.env.REDIS_PASSWORD, db: parseInt(process.env.REDIS_DB || '0'), // Настройки производительности lazyConnect: true, keepAlive: 30000, retryDelayOnFailover: 100, maxRetriesPerRequest: 3, // Настройки для production enableOfflineQueue: false, connectTimeout: 10000, commandTimeout: 5000, }) this.redis.on('error', (error) => { console.error('Redis connection error:', error) }) this.redis.on('connect', () => { console.log('Redis connected successfully') }) } // Получение данных с fallback async get(key: string, fallback?: () => Promise, ttl?: number): Promise { try { const cached = await this.redis.get(key) if (cached) { return JSON.parse(cached) } if (fallback) { const data = await fallback() await this.set(key, data, ttl) return data } return null } catch (error) { console.error('Redis get error:', error) return fallback ? await fallback() : null } } // Сохранение данных async set(key: string, value: any, ttl?: number): Promise { try { const serialized = JSON.stringify(value) const expiry = ttl || this.defaultTTL await this.redis.setex(key, expiry, serialized) } catch (error) { console.error('Redis set error:', error) } } // Удаление по ключу async del(key: string): Promise { try { await this.redis.del(key) } catch (error) { console.error('Redis delete error:', error) } } // Удаление по паттерну async delPattern(pattern: string): Promise { try { const keys = await this.redis.keys(pattern) if (keys.length > 0) { await this.redis.del(...keys) } } catch (error) { console.error('Redis pattern delete error:', error) } } // Инкремент счетчика async incr(key: string, ttl?: number): Promise { try { const value = await this.redis.incr(key) if (ttl && value === 1) { await this.redis.expire(key, ttl) } return value } catch (error) { console.error('Redis increment error:', error) return 0 } } // Сохранение в hash async hset(key: string, field: string, value: any, ttl?: number): Promise { try { const serialized = JSON.stringify(value) await this.redis.hset(key, field, serialized) if (ttl) { await this.redis.expire(key, ttl) } } catch (error) { console.error('Redis hset error:', error) } } // Получение из hash async hget(key: string, field: string): Promise { try { const value = await this.redis.hget(key, field) return value ? JSON.parse(value) : null } catch (error) { console.error('Redis hget error:', error) return null } } // Получение всех полей hash async hgetall(key: string): Promise> { try { const values = await this.redis.hgetall(key) const result: Record = {} Object.entries(values).forEach(([field, value]) => { result[field] = JSON.parse(value) }) return result } catch (error) { console.error('Redis hgetall error:', error) return {} } } // Закрытие соединения async disconnect(): Promise { await this.redis.disconnect() } } // Глобальный экземпляр Redis export const redis = new RedisCache() ``` ### 3. Application-Level Caching #### Memory Cache с LRU ```typescript // src/lib/memory-cache.ts class LRUCache { private cache = new Map() private maxSize: number constructor(maxSize: number = 1000) { this.maxSize = maxSize } get(key: string): T | null { const item = this.cache.get(key) if (!item) return null if (Date.now() > item.expiry) { this.cache.delete(key) return null } // Обновляем позицию (LRU) this.cache.delete(key) this.cache.set(key, item) return item.value } set(key: string, value: T, ttlMs: number = 300000): void { // Удаляем старые записи если превышен лимит if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value this.cache.delete(firstKey) } this.cache.set(key, { value, expiry: Date.now() + ttlMs, }) } delete(key: string): void { this.cache.delete(key) } clear(): void { this.cache.clear() } size(): number { return this.cache.size } } // Глобальные кэши для разных типов данных export const userCache = new LRUCache(500) export const organizationCache = new LRUCache(200) export const productCache = new LRUCache(1000) export const orderCache = new LRUCache(500) ``` #### Query Result Cache ```typescript // src/lib/query-cache.ts import { redis } from './redis' import { createHash } from 'crypto' export class QueryCache { // Кэширование результатов GraphQL запросов static async cacheGraphQLQuery(query: string, variables: any, result: T, ttl: number = 300): Promise { const key = this.generateQueryKey(query, variables) await redis.set(`gql:${key}`, result, ttl) } static async getCachedGraphQLQuery(query: string, variables: any): Promise { const key = this.generateQueryKey(query, variables) return await redis.get(`gql:${key}`) } // Кэширование результатов Prisma запросов static async cachePrismaQuery( model: string, method: string, args: any, result: T, ttl: number = 300, ): Promise { const key = this.generatePrismaKey(model, method, args) await redis.set(`prisma:${key}`, result, ttl) } static async getCachedPrismaQuery(model: string, method: string, args: any): Promise { const key = this.generatePrismaKey(model, method, args) return await redis.get(`prisma:${key}`) } // Инвалидация кэша при изменении данных static async invalidateModelCache(model: string): Promise { await redis.delPattern(`prisma:${model}:*`) await redis.delPattern(`gql:*${model}*`) } private static generateQueryKey(query: string, variables: any): string { const combined = query + JSON.stringify(variables) return createHash('md5').update(combined).digest('hex') } private static generatePrismaKey(model: string, method: string, args: any): string { const combined = `${model}:${method}:${JSON.stringify(args)}` return createHash('md5').update(combined).digest('hex') } } ``` ## 🏪 Marketplace Data Caching ### 1. Wildberries Data Cache ```typescript // src/services/marketplace-cache.ts import { redis } from '@/lib/redis' import { WildberriesAPI } from '@/lib/integrations/wildberries' export class MarketplaceCacheService { private static readonly CACHE_KEYS = { wbProducts: (orgId: string) => `wb:products:${orgId}`, wbStocks: (orgId: string) => `wb:stocks:${orgId}`, wbOrders: (orgId: string, date: string) => `wb:orders:${orgId}:${date}`, wbSales: (orgId: string, date: string) => `wb:sales:${orgId}:${date}`, wbWarehouses: (orgId: string) => `wb:warehouses:${orgId}`, ozonProducts: (orgId: string) => `ozon:products:${orgId}`, ozonStocks: (orgId: string) => `ozon:stocks:${orgId}`, ozonOrders: (orgId: string, date: string) => `ozon:orders:${orgId}:${date}`, } private static readonly CACHE_TTL = { products: 3600, // 1 час - товары редко изменяются stocks: 300, // 5 минут - остатки изменяются часто orders: 1800, // 30 минут - заказы обновляются периодически sales: 3600, // 1 час - продажи обновляются реже warehouses: 86400, // 24 часа - склады изменяются редко statistics: 7200, // 2 часа - статистика обновляется несколько раз в день } // Кэширование товаров Wildberries static async getWBProducts(organizationId: string, wbApi: WildberriesAPI): Promise { const key = this.CACHE_KEYS.wbProducts(organizationId) return await redis.get( key, async () => { console.log('Fetching WB products from API for org:', organizationId) const products = await wbApi.getProductCards() return products }, this.CACHE_TTL.products, ) } // Кэширование остатков Wildberries static async getWBStocks(organizationId: string, wbApi: WildberriesAPI): Promise { const key = this.CACHE_KEYS.wbStocks(organizationId) return await redis.get( key, async () => { console.log('Fetching WB stocks from API for org:', organizationId) const stocks = await wbApi.getStocks() return stocks }, this.CACHE_TTL.stocks, ) } // Кэширование заказов Wildberries с учетом даты static async getWBOrders(organizationId: string, dateFrom: string, wbApi: WildberriesAPI): Promise { const dateKey = dateFrom.split('T')[0] // Используем только дату const key = this.CACHE_KEYS.wbOrders(organizationId, dateKey) return await redis.get( key, async () => { console.log('Fetching WB orders from API for org:', organizationId, 'date:', dateKey) const orders = await wbApi.getOrders(dateFrom) return orders }, this.CACHE_TTL.orders, ) } // Кэширование продаж Wildberries static async getWBSales(organizationId: string, dateFrom: string, wbApi: WildberriesAPI): Promise { const dateKey = dateFrom.split('T')[0] const key = this.CACHE_KEYS.wbSales(organizationId, dateKey) return await redis.get( key, async () => { console.log('Fetching WB sales from API for org:', organizationId, 'date:', dateKey) const sales = await wbApi.getSales(dateFrom) return sales }, this.CACHE_TTL.sales, ) } // Кэширование складов Wildberries static async getWBWarehouses(organizationId: string, wbApi: WildberriesAPI): Promise { const key = this.CACHE_KEYS.wbWarehouses(organizationId) return await redis.get( key, async () => { console.log('Fetching WB warehouses from API for org:', organizationId) const warehouses = await wbApi.getWarehouses() return warehouses }, this.CACHE_TTL.warehouses, ) } // Инвалидация кэша при обновлении API ключей static async invalidateOrganizationCache(organizationId: string): Promise { const patterns = [`wb:*:${organizationId}*`, `ozon:*:${organizationId}*`] for (const pattern of patterns) { await redis.delPattern(pattern) } console.log('Invalidated marketplace cache for organization:', organizationId) } // Префетчинг данных (предварительная загрузка) static async prefetchMarketplaceData(organizationId: string, wbApi: WildberriesAPI): Promise { const today = new Date().toISOString() const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() // Загружаем данные параллельно await Promise.allSettled([ this.getWBProducts(organizationId, wbApi), this.getWBStocks(organizationId, wbApi), this.getWBWarehouses(organizationId, wbApi), this.getWBOrders(organizationId, yesterday, wbApi), this.getWBSales(organizationId, yesterday, wbApi), ]) console.log('Prefetched marketplace data for organization:', organizationId) } // Получение статистики кэша static async getCacheStats(): Promise<{ keys: number memory: string hitRate: number }> { // Подсчет ключей по паттернам const wbKeys = await redis.redis.keys('wb:*') const ozonKeys = await redis.redis.keys('ozon:*') const totalKeys = wbKeys.length + ozonKeys.length // Получение информации о памяти Redis const info = await redis.redis.info('memory') const memoryMatch = info.match(/used_memory_human:(.+)/) const memory = memoryMatch ? memoryMatch[1].trim() : 'unknown' return { keys: totalKeys, memory, hitRate: 0.85, // Примерный hit rate, можно реализовать точный подсчет } } } ``` ### 2. DaData Cache ```typescript // src/services/dadata-cache.ts import { redis } from '@/lib/redis' import { DaDataAPI } from '@/lib/integrations/dadata' export class DaDataCacheService { private static readonly CACHE_TTL = { organization: 86400, // 24 часа - данные организаций стабильны address: 604800, // 7 дней - адреса практически не изменяются bank: 604800, // 7 дней - банковские данные стабильны cleanData: 2592000, // 30 дней - очищенные данные не изменяются } // Кэширование поиска организаций по ИНН static async findOrganizationByINN(inn: string, dadataApi: DaDataAPI): Promise { const key = `dadata:org:inn:${inn}` return await redis.get( key, async () => { console.log('Fetching organization from DaData API for INN:', inn) const organization = await dadataApi.findByINN(inn) return organization }, this.CACHE_TTL.organization, ) } // Кэширование подсказок организаций static async suggestOrganizations(query: string, dadataApi: DaDataAPI): Promise { // Нормализуем запрос для ключа кэша const normalizedQuery = query.toLowerCase().trim() const key = `dadata:org:suggest:${normalizedQuery}` return await redis.get( key, async () => { console.log('Fetching organization suggestions from DaData API for query:', query) const suggestions = await dadataApi.suggestOrganizations(query) return suggestions }, this.CACHE_TTL.organization, ) } // Кэширование подсказок адресов static async suggestAddresses(query: string, dadataApi: DaDataAPI): Promise { const normalizedQuery = query.toLowerCase().trim() const key = `dadata:address:suggest:${normalizedQuery}` return await redis.get( key, async () => { console.log('Fetching address suggestions from DaData API for query:', query) const suggestions = await dadataApi.suggestAddresses(query) return suggestions }, this.CACHE_TTL.address, ) } // Кэширование подсказок банков static async suggestBanks(query: string, dadataApi: DaDataAPI): Promise { const normalizedQuery = query.toLowerCase().trim() const key = `dadata:bank:suggest:${normalizedQuery}` return await redis.get( key, async () => { console.log('Fetching bank suggestions from DaData API for query:', query) const suggestions = await dadataApi.suggestBanks(query) return suggestions }, this.CACHE_TTL.bank, ) } // Кэширование очистки телефонов static async cleanPhone(phone: string, dadataApi: DaDataAPI): Promise { const key = `dadata:clean:phone:${phone}` return await redis.get( key, async () => { console.log('Cleaning phone number via DaData API:', phone) const cleaned = await dadataApi.cleanPhone(phone) return cleaned }, this.CACHE_TTL.cleanData, ) } // Кэширование очистки адресов static async cleanAddress(address: string, dadataApi: DaDataAPI): Promise { const key = `dadata:clean:address:${address}` return await redis.get( key, async () => { console.log('Cleaning address via DaData API:', address) const cleaned = await dadataApi.cleanAddress(address) return cleaned }, this.CACHE_TTL.cleanData, ) } // Массовая предзагрузка часто используемых данных static async prefetchCommonData(dadataApi: DaDataAPI): Promise { const commonQueries = ['Москва', 'Санкт-Петербург', 'Новосибирск', 'Екатеринбург', 'Нижний Новгород'] // Предзагружаем адреса для крупных городов await Promise.allSettled(commonQueries.map((query) => this.suggestAddresses(query, dadataApi))) console.log('Prefetched common DaData queries') } } ``` ## 📈 Performance Optimization ### 1. Cache Warming ```typescript // src/services/cache-warming.ts import { MarketplaceCacheService } from './marketplace-cache' import { DaDataCacheService } from './dadata-cache' import { QueryCache } from '@/lib/query-cache' import { PrismaClient } from '@prisma/client' export class CacheWarmingService { constructor(private prisma: PrismaClient) {} // Прогрев кэша при старте приложения async warmupCache(): Promise { console.log('Starting cache warmup...') await Promise.allSettled([ this.warmupUserData(), this.warmupOrganizationData(), this.warmupCommonQueries(), this.warmupStaticData(), ]) console.log('Cache warmup completed') } // Прогрев пользовательских данных private async warmupUserData(): Promise { // Загружаем активных пользователей за последние 24 часа const activeUsers = await this.prisma.user.findMany({ where: { lastLoginAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000), }, }, take: 100, include: { organization: true, }, }) // Кэшируем их данные for (const user of activeUsers) { await QueryCache.cachePrismaQuery( 'user', 'findUnique', { where: { id: user.id } }, user, 1800, // 30 минут ) } console.log(`Warmed up cache for ${activeUsers.length} active users`) } // Прогрев данных организаций private async warmupOrganizationData(): Promise { const activeOrganizations = await this.prisma.organization.findMany({ where: { users: { some: { lastLoginAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000), }, }, }, }, take: 50, include: { apiKeys: true, users: { take: 5, }, }, }) for (const org of activeOrganizations) { await QueryCache.cachePrismaQuery( 'organization', 'findUnique', { where: { id: org.id } }, org, 3600, // 1 час ) } console.log(`Warmed up cache for ${activeOrganizations.length} organizations`) } // Прогрев часто используемых запросов private async warmupCommonQueries(): Promise { // Статистика по типам организаций const orgStats = await this.prisma.organization.groupBy({ by: ['type'], _count: true, }) await QueryCache.cachePrismaQuery('organization', 'groupBy', { by: ['type'] }, orgStats, 3600) // Недавние заказы const recentOrders = await this.prisma.supplyOrder.findMany({ where: { createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), }, }, take: 100, orderBy: { createdAt: 'desc', }, }) await QueryCache.cachePrismaQuery( 'supplyOrder', 'findMany', { take: 100, orderBy: { createdAt: 'desc' } }, recentOrders, 600, // 10 минут ) console.log('Warmed up common queries cache') } // Прогрев статических данных private async warmupStaticData(): Promise { // Справочники const warehouses = await this.prisma.warehouse.findMany() await QueryCache.cachePrismaQuery( 'warehouse', 'findMany', {}, warehouses, 86400, // 24 часа ) const cities = await this.prisma.$queryRaw` SELECT DISTINCT city FROM organizations WHERE city IS NOT NULL ` await QueryCache.cachePrismaQuery('organization', 'cities', {}, cities, 86400) console.log('Warmed up static data cache') } // Прогрев кэша для конкретной организации async warmupOrganizationCache(organizationId: string): Promise { const org = await this.prisma.organization.findUnique({ where: { id: organizationId }, include: { apiKeys: true, users: true, }, }) if (!org) return // Кэшируем данные организации await QueryCache.cachePrismaQuery('organization', 'findUnique', { where: { id: organizationId } }, org, 3600) // Если есть API ключи маркетплейсов, прогреваем их данные const wbKey = org.apiKeys.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive) if (wbKey) { // Здесь можно добавить прогрев данных Wildberries console.log('Wildberries API key found for organization:', organizationId) } console.log('Warmed up cache for organization:', organizationId) } } ``` ### 2. Cache Invalidation ```typescript // src/services/cache-invalidation.ts import { redis } from '@/lib/redis' import { QueryCache } from '@/lib/query-cache' export class CacheInvalidationService { // Инвалидация при изменении пользователя static async invalidateUserCache(userId: string): Promise { await Promise.all([ redis.delPattern(`user:${userId}*`), redis.delPattern(`gql:*user*${userId}*`), QueryCache.invalidateModelCache('user'), ]) console.log('Invalidated user cache:', userId) } // Инвалидация при изменении организации static async invalidateOrganizationCache(organizationId: string): Promise { await Promise.all([ redis.delPattern(`org:${organizationId}*`), redis.delPattern(`wb:*:${organizationId}*`), redis.delPattern(`ozon:*:${organizationId}*`), redis.delPattern(`gql:*organization*${organizationId}*`), QueryCache.invalidateModelCache('organization'), ]) console.log('Invalidated organization cache:', organizationId) } // Инвалидация при изменении заказа static async invalidateOrderCache(orderId: string, organizationId?: string): Promise { const patterns = [`order:${orderId}*`, `gql:*order*${orderId}*`] if (organizationId) { patterns.push(`org:${organizationId}:orders*`, `gql:*orders*${organizationId}*`) } await Promise.all(patterns.map((pattern) => redis.delPattern(pattern))) await QueryCache.invalidateModelCache('supplyOrder') console.log('Invalidated order cache:', orderId) } // Инвалидация при изменении API ключей static async invalidateAPIKeyCache(organizationId: string, marketplace: string): Promise { await Promise.all([ redis.delPattern(`${marketplace.toLowerCase()}:*:${organizationId}*`), redis.delPattern(`api:${organizationId}:${marketplace}*`), ]) console.log('Invalidated API key cache:', organizationId, marketplace) } // Планово очистить весь кэш static async flushAllCache(): Promise { await redis.redis.flushdb() console.log('Flushed all cache') } // Очистить кэш по времени (старые записи) static async cleanupExpiredCache(): Promise { // Redis автоматически удаляет истекшие ключи, но можно добавить дополнительную логику const info = await redis.redis.info('keyspace') console.log('Cache cleanup completed. Keyspace info:', info) } } ``` ## 📊 Cache Monitoring ### 1. Cache Metrics ```typescript // src/services/cache-monitoring.ts import { redis } from '@/lib/redis' export class CacheMonitoringService { // Получение метрик кэша static async getCacheMetrics(): Promise<{ memory: { used: string peak: string fragmentation: number } keys: { total: number expired: number byPattern: Record } performance: { hitRate: number missRate: number opsPerSecond: number } connections: { active: number total: number } }> { const [memoryInfo, keystoreInfo, statsInfo] = await Promise.all([ redis.redis.info('memory'), redis.redis.info('keyspace'), redis.redis.info('stats'), ]) // Подсчет ключей по паттернам const patterns = ['wb:', 'ozon:', 'dadata:', 'user:', 'org:', 'gql:', 'prisma:'] const keysByPattern: Record = {} for (const pattern of patterns) { const keys = await redis.redis.keys(`${pattern}*`) keysByPattern[pattern.replace(':', '')] = keys.length } return { memory: { used: this.extractValue(memoryInfo, 'used_memory_human'), peak: this.extractValue(memoryInfo, 'used_memory_peak_human'), fragmentation: parseFloat(this.extractValue(memoryInfo, 'mem_fragmentation_ratio')), }, keys: { total: await redis.redis.dbsize(), expired: parseInt(this.extractValue(statsInfo, 'expired_keys')), byPattern: keysByPattern, }, performance: { hitRate: this.calculateHitRate(statsInfo), missRate: this.calculateMissRate(statsInfo), opsPerSecond: parseFloat(this.extractValue(statsInfo, 'instantaneous_ops_per_sec')), }, connections: { active: parseInt(this.extractValue(statsInfo, 'connected_clients')), total: parseInt(this.extractValue(statsInfo, 'total_connections_received')), }, } } // Получение топ-10 ключей по размеру static async getTopKeysBySize(): Promise> { const keys = await redis.redis.keys('*') const keyInfo = [] for (const key of keys.slice(0, 100)) { // Ограничиваем для производительности const [size, ttl] = await Promise.all([redis.redis.memory('usage', key), redis.redis.ttl(key)]) keyInfo.push({ key, size, ttl }) } return keyInfo.sort((a, b) => b.size - a.size).slice(0, 10) } // Анализ производительности кэша static async analyzeCachePerformance(): Promise<{ recommendations: string[] warnings: string[] hotKeys: string[] }> { const metrics = await this.getCacheMetrics() const recommendations: string[] = [] const warnings: string[] = [] const hotKeys: string[] = [] // Анализ фрагментации памяти if (metrics.memory.fragmentation > 1.5) { warnings.push(`High memory fragmentation: ${metrics.memory.fragmentation}`) recommendations.push('Consider restarting Redis to defragment memory') } // Анализ hit rate if (metrics.performance.hitRate < 0.8) { warnings.push(`Low cache hit rate: ${metrics.performance.hitRate * 100}%`) recommendations.push('Review caching strategy and TTL values') } // Анализ количества ключей if (metrics.keys.total > 100000) { warnings.push(`High number of keys: ${metrics.keys.total}`) recommendations.push('Implement key cleanup strategy') } // Поиск горячих ключей (часто используемых) const topKeys = await this.getTopKeysBySize() hotKeys.push(...topKeys.slice(0, 5).map((k) => k.key)) return { recommendations, warnings, hotKeys, } } // Очистка кэша по рекомендациям static async optimizeCache(): Promise<{ cleaned: number; optimized: boolean }> { let cleaned = 0 // Удаляем ключи без TTL (если они не должны быть постоянными) const keysWithoutTTL = await redis.redis.keys('*') for (const key of keysWithoutTTL) { const ttl = await redis.redis.ttl(key) if (ttl === -1 && !key.startsWith('config:')) { // Исключаем конфигурационные ключи await redis.redis.expire(key, 3600) // Устанавливаем TTL 1 час cleaned++ } } // Дополнительная оптимизация const metrics = await this.getCacheMetrics() const optimized = metrics.memory.fragmentation < 1.5 && metrics.performance.hitRate > 0.8 return { cleaned, optimized } } private static extractValue(info: string, key: string): string { const match = info.match(new RegExp(`${key}:(.+)`)) return match ? match[1].trim() : '0' } private static calculateHitRate(statsInfo: string): number { const hits = parseInt(this.extractValue(statsInfo, 'keyspace_hits')) const misses = parseInt(this.extractValue(statsInfo, 'keyspace_misses')) return hits / (hits + misses) || 0 } private static calculateMissRate(statsInfo: string): number { return 1 - this.calculateHitRate(statsInfo) } } ``` ### 2. Cache Health Check ```typescript // src/services/cache-health.ts export class CacheHealthService { // Проверка здоровья кэша static async healthCheck(): Promise<{ status: 'healthy' | 'warning' | 'critical' checks: Array<{ name: string status: 'pass' | 'fail' message: string value?: any }> }> { const checks = [] let overallStatus: 'healthy' | 'warning' | 'critical' = 'healthy' // Проверка подключения к Redis try { const pong = await redis.redis.ping() checks.push({ name: 'Redis Connection', status: pong === 'PONG' ? 'pass' : 'fail', message: pong === 'PONG' ? 'Connected' : 'Connection failed', value: pong, }) } catch (error) { checks.push({ name: 'Redis Connection', status: 'fail', message: `Connection error: ${error.message}`, }) overallStatus = 'critical' } // Проверка производительности const startTime = Date.now() try { await redis.set('health:test', 'test', 10) const value = await redis.get('health:test') const responseTime = Date.now() - startTime checks.push({ name: 'Cache Performance', status: responseTime < 100 ? 'pass' : 'fail', message: `Response time: ${responseTime}ms`, value: responseTime, }) if (responseTime > 100) { overallStatus = overallStatus === 'critical' ? 'critical' : 'warning' } } catch (error) { checks.push({ name: 'Cache Performance', status: 'fail', message: `Performance test failed: ${error.message}`, }) overallStatus = 'critical' } // Проверка использования памяти try { const metrics = await CacheMonitoringService.getCacheMetrics() checks.push({ name: 'Memory Usage', status: metrics.memory.fragmentation < 2 ? 'pass' : 'fail', message: `Fragmentation: ${metrics.memory.fragmentation}`, value: metrics.memory.used, }) checks.push({ name: 'Hit Rate', status: metrics.performance.hitRate > 0.7 ? 'pass' : 'fail', message: `Hit rate: ${(metrics.performance.hitRate * 100).toFixed(1)}%`, value: metrics.performance.hitRate, }) if (metrics.memory.fragmentation > 2 || metrics.performance.hitRate < 0.7) { overallStatus = overallStatus === 'critical' ? 'critical' : 'warning' } } catch (error) { checks.push({ name: 'Cache Metrics', status: 'fail', message: `Metrics collection failed: ${error.message}`, }) } return { status: overallStatus, checks, } } // Автоматическое восстановление кэша static async autoHeal(): Promise<{ actions: string[]; success: boolean }> { const actions: string[] = [] let success = true try { // Очистка истекших ключей const cleaned = await CacheInvalidationService.cleanupExpiredCache() actions.push('Cleaned expired keys') // Оптимизация кэша const optimization = await CacheMonitoringService.optimizeCache() actions.push(`Optimized ${optimization.cleaned} keys`) // Проверка здоровья после восстановления const health = await this.healthCheck() success = health.status !== 'critical' if (success) { actions.push('Cache health restored') } else { actions.push('Manual intervention required') } } catch (error) { actions.push(`Auto-heal failed: ${error.message}`) success = false } return { actions, success } } } ``` ## 🎯 Best Practices ### 1. Кэширование GraphQL ```typescript // src/lib/graphql-cache.ts export const GraphQLCacheConfig = { // Кэш по умолчанию для запросов defaultMaxAge: 300, // 5 минут // Специфичные настройки для разных типов typeConfigs: { User: { maxAge: 1800 }, // 30 минут Organization: { maxAge: 3600 }, // 1 час Product: { maxAge: 600 }, // 10 минут Order: { maxAge: 300 }, // 5 минут Warehouse: { maxAge: 86400 }, // 24 часа }, // Поля, которые не должны кэшироваться skipCache: ['currentUser', 'realtimeData', 'sensitiveInformation'], } // Директива для кэширования в GraphQL схеме export const cacheDirective = ` directive @cache( maxAge: Int = 300 scope: CacheScope = PUBLIC ) on FIELD_DEFINITION | OBJECT enum CacheScope { PUBLIC PRIVATE } ` ``` ### 2. Кэширование компонентов React ```typescript // src/hooks/useCache.ts import { useCallback, useEffect, useState } from 'react' import { redis } from '@/lib/redis' export const useCache = ( key: string, fetcher: () => Promise, ttl: number = 300 ) => { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const fetchData = useCallback(async () => { try { setLoading(true) setError(null) // Попытка получить из кэша const cached = await redis.get(key) if (cached) { setData(cached) setLoading(false) return } // Получение свежих данных const fresh = await fetcher() // Сохранение в кэш await redis.set(key, fresh, ttl) setData(fresh) } catch (err) { setError(err as Error) } finally { setLoading(false) } }, [key, fetcher, ttl]) useEffect(() => { fetchData() }, [fetchData]) const invalidate = useCallback(async () => { await redis.del(key) await fetchData() }, [key, fetchData]) return { data, loading, error, invalidate, refetch: fetchData } } // Пример использования export const OrganizationProfile = ({ organizationId }: { organizationId: string }) => { const { data: organization, loading, error } = useCache( `org:${organizationId}`, () => fetch(`/api/organizations/${organizationId}`).then(r => r.json()), 3600 // 1 час ) if (loading) return
Loading...
if (error) return
Error: {error.message}
return
{organization?.name}
} ``` ## 🎯 Заключение Система кэширования SFERA обеспечивает: 1. **Многоуровневое кэширование**: От браузера до базы данных 2. **Интеллектуальная инвалидация**: Автоматическая очистка устаревших данных 3. **Оптимизация API**: Снижение нагрузки на внешние сервисы 4. **Мониторинг производительности**: Контроль эффективности кэша 5. **Автоматическое восстановление**: Самодиагностика и исправление проблем Правильно настроенная система кэширования значительно улучшает производительность приложения и снижает затраты на внешние API.