Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Redis

ใช้ Serverless Redis สร้าง SvelteKit Applications ที่ขยายได้

บทนำ

SvelteKit เป็น Full-Stack Application Framework ที่กำลังจะมาสำหรับ Svelte UI Framework ที่ Build App ที่ Compile Time ให้ JavaScript ขนาดเล็กและเร็วขึ้น แม้ SvelteKit จะให้เขียน Server-side Logic ด้วย Endpoints ได้ แต่การ Persist ข้อมูลแอปขึ้นอยู่กับคุณ

ในบทความนี้ จะแสดงวิธีเก็บข้อมูลด้วย Redis ใน SvelteKit Application ใช้ Redis Cache API Responses ของ Movie API และแสดง Random Movie จาก The Movie Database (TMDB) API

คุณต้องการ Redis Connection String หรือรัน Redis Local หากยังไม่มี Instance แนะนำ Upstash เหมือน SvelteKit ที่ Optimize สำหรับ Serverless Apps และ ฟรีหาก Request ไม่มาก (Cap 10k/day ขณะเขียน) ไม่ว่าจะได้ Redis จากที่ไหน ควรอยู่ใกล้ที่ Deploy App เพื่อลด Latency

Prerequisites

  • ความรู้พื้นฐาน SvelteKit (Pages vs Endpoints, Loading, Query/Params Retrieval)
  • TMDB API Key และ Redis Instance (เช่น Upstash) หากจะ Run/Deploy Demo

Starter Overview

Clone Starter Repo Branch main มี End Result Checkout Branch initial เพื่อติดตามบทความ (Fork ก่อนหากจะ Push Changes)

git clone https://github.com/geoffrich/movie-search-redis.git
cd movie-search-redis
git checkout initial

นี่คือ SvelteKit App ขนาดเล็ก สำหรับค้นหาภาพยนตร์และดูรายละเอียดด้วย TMDB API ใช้ TypeScript สำหรับโต้ตอบกับ API Responses (ไม่บังคับสำหรับ Redis หรือ SvelteKit) Routes ที่มีอยู่ (ไฟล์ใน src/routes):

  • / - Home Page
  • /search - Search Results List รับ Query Params query และ page (เช่น ?query=star wars&page=3)
  • /search.json - Server Endpoint Query TMDB API Return Results (Query Params เดียวกับ /search)
  • /movie/[id] - Movie Details ด้วย ID (เช่น /movie/11)
  • /movie/[id].json - Server Endpoint Return Movie Details จาก TMDB API (ปรับ Response ให้ Subset ของข้อมูล)

ดู Demo บน Netlify TMDB API Calls เกิดขึ้นเฉพาะ Server Endpoints เพื่อไม่ Expose API Key ให้ Client

รัน Local: สร้าง .env ใน Root Project ใส่ TMDB API Key และ Redis Connection String แล้ว npm install และ npm run dev

TMDB_API_KEY=KEY_GOES_HERE
REDIS_CONNECTION=CONNECTION_GOES_HERE

เข้าถึงค่าได้ด้วย process.env['TMDB_API_KEY'] หรือ process.env['REDIS_CONNECTION'] อ่านจาก .env ผ่าน dotenv ใน hooks.ts

Cache API Response ใน Redis

ปรับปรุงโปรเจกต์: Cache API Response สำหรับ Movie แต่ละเรื่องใน Redis ปัจจุบัน App Request TMDB API ทุกครั้งที่โหลดหน้า Movie ตอน Request ครั้งแรก เก็บ Response ไว้ใน Redis เพื่อครั้งต่อไปไม่ต้อง Request TMDB ซ้ำ

แม้ TMDB API จะเร็วและไม่จำเป็นต้อง Cache แต่เทคนิคนี้มีประโยชน์สำหรับ API ที่ตอบช้า หรือมี Rate Limits และให้ Resiliency หาก API Down

เราเพิ่ม ioredis เป็น Dependency แล้ว (Redis Client สำหรับ Node)

สร้างไฟล์ src/lib/redis.ts Initialize Redis Client Export ใช้งาน และ Helper Functions สำหรับ Keys:

import Redis from "ioredis";

const connectionString = process.env["REDIS_CONNECTION"];
export const MOVIE_IDS_KEY = "movie_ids";

/** Return the key used to store movie details for a given ID in Redis */
export function getMovieKey(id): string {
  return `movie:${id}`;
}

export default connectionString ? new Redis(connectionString) : new Redis();

Implementation นี้สร้าง Single Redis Client ต่อ Serverless Instance อีกทางเลือกคือสร้างและปิด Connection ต่อ Request Upstash มี REST API ที่ไม่ต้อง Initialize Connection ทางเลือกไหนดีขึ้นอยู่กับ Latency และ Memory Requirements ของ App

ไปที่ /src/routes/movie/[id].json.ts Import Redis Client และ getMovieKey จาก redis.ts:

import redis, { getMovieKey } from "$lib/redis";

ดู Function getMovieDetailsFromApi เรียก TMDB API ดึง Movie Details และ Credits Return กลับ ก่อน Return เราอยากเก็บใน Redis Cache เพื่อครั้งหน้าเอา Cached Version แทนการ Call API เพิ่ม Function cacheMovieResponse:

async function cacheMovieResponse(id: number, movie, credits) {
  try {
    const cache: MovieDetails = { movie, credits };
    // Store movie response for 24 hours
    await redis.set(getMovieKey(id), JSON.stringify(cache), "EX", 24 * 60 * 60);
  } catch (e) {
    console.log("Unable to cache", id, e);
  }
}

แต่ละ Movie ID เก็บใน Key ต่างกัน เช่น ID 11 -> Key movie:11 Argument สองตัวสุดท้ายของ redis.set คือ Cache 24 ชั่วโมง (86400 วินาที) ไม่ควร Cache ถาวร (ละเมีย Terms of Use และข้อมูลจะ Stale) ต้อง Stringify JS Object ก่อนเก็บ Cache Catch Exceptions เพื่อ Resiliency หาก Cache ไม่ได้ ยังคง Return API Data แทนการ Throw Unhandled Exception

ใช้ cacheMovieResponse ใน getMovieDetailsFromApi:

async function getMovieDetailsFromApi(id: number) {
  const [movieResponse, creditsResponse] = await Promise.all([
    getMovieDetails(id),
    getCredits(id),
  ]);

  if (movieResponse.ok) {
    const movie = await movieResponse.json();
    const credits = await creditsResponse.json();
    // Add this line
    await cacheMovieResponse(id, movie, credits);
    return { movie, credits };
  }
  return { status: movieResponse.status };
}

ตอนนี้เก็บ Cache แล้ว แต่ยังต้อง Retrieve Cached Data เพิ่ม Function อ่าน Movie Details จาก Cache:

async function getMovieDetailsFromCache(id: number): Promise<MovieDetails | Record<string, never>> {
  try {
    const cached: string = await redis.get(getMovieKey(id));
    if (cached) {
      const parsed: MovieDetails = JSON.parse(cached);
      console.log(`Found ${id} in cache`);
      return parsed;
    }
  } catch (e) {
    console.log("Unable to retrieve from cache", id, e);
  }
  return {};
}

ข้อมูลเก็บเป็น String ต้อง Parse ให้เป็น Object Catch Exceptions เช่นเดียวกัน ไม่ให้ Escape ออกมา สามารถ Fallback ไปดึงจาก API ได้

สุดท้าย เรียก Caching Function ใน Main Request Handler หากเจอใน Cache Return ทันที มิฉะนั้น Fallback ไป API:

export const get: RequestHandler = async function ({ params }) {
  const { id: rawId } = params;
  const id = parseInt(rawId);
  if (isNaN(id)) { return { status: 400 }; }

  // Add these lines
  const { movie, credits } = await getMovieDetailsFromCache(id);
  if (movie && credits) {
    return { body: adaptResponse(movie, credits) };
  }

  // Fallback to the API
  const result = await getMovieDetailsFromApi(id);
  // ...
};

ดู Final Code ใน Demo Repo ลองไปที่หน้า Movie Refresh หน้า ควรเห็น "Found id in cache" ใน Console Log แสดงว่า Store และ Retrieve สำเร็จ

ดึง Random Movie

Redis ทำได้มากกว่าแค่ Cache API Responses มาดูวิธีสร้าง Route Redirect ไป Random Movie

ไม่ได้แค่สุ่มตัวเลข 1-300000 เป็น Movie ID เพราะไม่ทุกตัวเลขมี Movie (เช่น ID 1 หรือ 1000 ไม่มี Movie) ยังยากที่จะ Track Max ID เพราะเปลี่ยนตลอดเมื่อมี Movie ใหม่ วิธีที่ดีกว่า: 2 Steps Process

  1. เมื่อ Search Query เกิดขึ้น ให้ใส่ IDs ทั้งหมดที่ Return มาเข้า Redis Set
  2. เมื่อ Request /movie/random ให้สุ่ม Member จาก Set นั้นและ Redirect ไป Movie Detail Page

Random Movies เริ่มน้อย แต่มากขึ้นตาม Searches ที่ทำ

Update /src/routes/search.json.ts เพื่อ Populate Random Set:

import type { RequestHandler } from "@sveltejs/kit";
import type { SearchResponse } from "$lib/types/tmdb";
import redis, { MOVIE_IDS_KEY } from "$lib/redis";

const VOTE_THRESHOLD = 20;

export const get: RequestHandler = async function ({ query }) {
  const searchQuery = query.get("query");
  const page = query.get("page") ?? 1;

  const response = await fetch(
    `https://api.themoviedb.org/3/search/movie?api_key=${process.env["TMDB_API_KEY"]}&page=${page}&include_adult=false&query=${searchQuery}`
  );

  const parsed: SearchResponse = await response.json();

  // Add these lines
  const filteredMovies = parsed.results.filter((movie) => movie.vote_count >= VOTE_THRESHOLD);
  if (filteredMovies.length > 0) {
    try {
      await redis.sadd(MOVIE_IDS_KEY, ...filteredMovies.map((r) => r.id));
    } catch (e) {
      console.log(e);
    }
  }

  return { body: parsed };
};

ไม่ได้เพิ่ม ทุก Movie ที่ Return มา กรอง Movies ที่ Vote น้อยออก เพราะน่าจะไม่ได้ผ่านการตรวจสอบจาก User ปรับ VOTE_THRESHOLD ได้ตามต้องการ

Search ภาพยนตร์เพื่อ Populate Set เช่น Star Wars, Lion King, Spider Man หลัง Search จะมี Set ของ Random IDs ใน Redis สร้าง Routes สำหรับ /movie/random

src/routes/movie/random.json.ts

import type { RequestHandler } from "@sveltejs/kit";
import redis, { MOVIE_IDS_KEY } from "$lib/redis";

export const get: RequestHandler = async function () {
  const randomId = await redis.srandmember(MOVIE_IDS_KEY);
  return { body: randomId };
};

Server Endpoint นี้ใช้ SRANDMEMBER สุ่ม ID จาก Movie ID Set

src/routes/movie/random.svelte

Svelte Page นี้ต้องการแค่ Load Function ไม่ Render UI เรียก Server Endpoint แล้ว Redirect ไป Movie Page ที่ตรงกัน

เสร็จแล้ว! ไปที่ https://localhost:3000/movie/random จะ Redirect ไป Random Movie จาก Searches ก่อนหน้า เพิ่ม Link ใน Navigation ได้ที่ /src/routes/__layout.svelte

<header>
  <nav>
    <a href="/">Search</a>
    <a href="/movie/random">Random</a>
  </nav>
</header>

ดู Live Demo ได้ที่ Live Demo

สรุป

ยังมีหลายวิธีใช้ Redis แต่หวังว่าบทความนี้ให้ความเข้าใจพื้นฐานการผสานรวม Redis ใน SvelteKit App ดู Final Code บน GitHub และ Live Demo บน Netlify

มีคำถาม? ติดต่อได้ที่ Twitter อ่านบทความ Svelte อื่นๆ ได้ที่ My Blog