AI News Hub Logo

AI News Hub

Redis Caching Strategies for Video Content Platforms

DEV Community
ahmet gedik

TrendVidStream serves trending video content from 8 regions: US, GB, CH, DK, AE, BE, CZ, FI. The Nordic and Central European regions trend quietly, while UAE can spike hard when regional events drive traffic. A flat TTL for all regions wastes quota on quiet regions and serves stale data to spiking ones. Redis lets us handle that heterogeneity cleanly. The platform currently uses a 3-tier PHP file cache. Redis fits in as the middle tier: Request → Redis (sub-millisecond, in-memory) → PHP file cache (milliseconds, disk) → SQLite (single-digit ms, WAL mode) For a multi-worker PHP-FPM setup, Redis solves the shared-state problem: file-based caches are per-server. Redis is shared across all workers automatically. 5400, // 1.5h — UAE news/events spike 'GB' => 7200, // 2h 'US' => 7200, // 2h 'CH' => 10800, // 3h — stable Swiss market 'DK' => 10800, // 3h — stable Nordic 'BE' => 10800, // 3h 'CZ' => 14400, // 4h — Central EU slower cycle 'FI' => 14400, // 4h — Finland quietest ]; public function set(string $region, array $videos): void { $key = "tvs:trending:{$region}"; $ttl = self::REGION_TTL[$region] ?? 10800; $pipe = $this->r->pipeline(); $pipe->del($key); foreach ($videos as $video) { $pipe->rPush($key, json_encode([ 'id' => $video['video_id'], 'title' => $video['title'], 'thumb' => $video['thumbnail'], 'channel' => $video['channel_title'], 'views' => $video['view_count'], 'category' => $video['category_id'], ])); } $pipe->expire($key, $ttl); $pipe->execute(); } public function get(string $region, int $page = 1, int $perPage = 20): ?array { $key = "tvs:trending:{$region}"; $start = ($page - 1) * $perPage; $end = $start + $perPage - 1; $items = $this->r->lRange($key, $start, $end); if (empty($items)) { return null; } return array_map( fn($v) => json_decode($v, true), $items ); } public function count(string $region): int { return (int)$this->r->lLen("tvs:trending:{$region}"); } } TrendVidStream's home page surfaces videos trending in the most regions simultaneously — a pan-European trending feed: r->lRange($srcKey, 0, -1); if (empty($videos)) { continue; } $pipe = $this->r->pipeline(); $pipe->del($tempKey); foreach ($videos as $raw) { $v = json_decode($raw, true); $pipe->zAdd($tempKey, 1, $v['id']); } $pipe->execute(); $tempKeys[] = $tempKey; } if (empty($tempKeys)) { return; } // Union all region scores — video trending in 4 regions scores 4 $this->r->zUnionStore( 'tvs:global:trending', $tempKeys, array_fill(0, count($tempKeys), 1), 'SUM' ); $this->r->expire('tvs:global:trending', 10800); // Cleanup temp keys $this->r->del($tempKeys); } public function getTopGlobal(int $n = 20): array { // Highest score = most regions trending return $this->r->zRevRange('tvs:global:trending', 0, $n - 1, true); } } r->hMSet($key, [ 'title' => $meta['title'], 'description' => mb_substr($meta['description'], 0, 500), 'channel' => $meta['channel_title'], 'thumb_high' => $meta['thumbnail_high'], 'view_count' => $meta['view_count'], 'like_count' => $meta['like_count'], 'category_id' => $meta['category_id'], 'region' => $meta['region'], 'cached_at' => time(), ]); $this->r->expire($key, self::TTL); } public function get(string $videoId): ?array { $data = $this->r->hGetAll("tvs:video:{$videoId}"); return empty($data) ? null : $data; } public function mGet(array $videoIds): array { $pipe = $this->r->pipeline(); foreach ($videoIds as $id) { $pipe->hGetAll("tvs:video:{$id}"); } $results = $pipe->execute(); return array_combine( $videoIds, array_map(fn($r) => empty($r) ? null : $r, $results) ); } } youtube->fetchTrending($region); // 1. Persist to SQLite $this->db->upsertVideos($videos, $region); // 2. Write-through: update Redis immediately $this->trendingCache->set($region, $videos); // 3. Invalidate search results for this region $this->invalidateSearchCache($region); // 4. Invalidate PHP page cache for affected pages $this->clearPageCache($region); } private function invalidateSearchCache(string $region): void { // Use SCAN — never KEYS in production $cursor = null; do { [$cursor, $keys] = $this->r->scan( $cursor, ['match' => "tvs:search:{$region}:*", 'count' => 200] ); if ($keys) { $this->r->del($keys); } } while ($cursor !== 0); } } get($region); if ($data !== null) { $ttl = $cache->getTTL($region); // If less than 20% of TTL remains, trigger async refresh if ($ttl getTrending($region, 50); $cache->set($region, $fresh); }); } } return $data; } // Cache miss — fetch from SQLite $videos = $db->getTrending($region, 50); $cache->set($region, $videos); return array_slice($videos, 0, 20); } For 8 regions at TrendVidStream: Key type Count Avg size Total Trending lists 8 × 50 items ~600B/item ~240KB Video metadata hashes ~400 videos ~900B ~360KB Search results ~300 cached ~2KB ~600KB Global trending sorted set 1 ~15KB ~15KB Total ~1.2MB A 32MB Redis instance comfortably serves TrendVidStream with room for 25× growth. This is part of the "Building TrendVidStream" series, documenting the architecture behind a global video directory covering Nordic, Middle Eastern, and Central European regions.