Try   HackMD

應用Redis在Node.js中實作快取(Cache)- 筆記

tags: cache database Node.js Redis

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
本站筆記已同步更新到我的個人網站囉! 歡迎參觀與閱讀,體驗不同的視覺感受!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
簡介

當網站有些常常需要存取的資料、或是要依賴第三方API回傳資料時,經常需要花許多時間等待對方的伺服器回應,造成頁面載入時間過長;回想一下,當我們在瀏覽購物網站時,對於那些需要等待很久的網站頁面,會有什麼反應呢?耐心等它跑完、或是直接關掉?大部分的人應該是選擇後者吧!

因此,提升頁面載入速度可以有效改善使用者體驗,同時也可以減少伺服器的負擔,如果是使用第三方API,也不需要不斷對它發送請求(畢竟第三方API時常有存取次數限制),可以先將這些資料快取存起來,之後發送請求的時候就可以使用快取中的資料,直到快取過期。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
情境

做一個食譜的網站,透過串接spoonacular API取得食譜的資料,但是存取資料有次數限制(當然也是可以課金解決),另外就是等待回傳的時間稍慢,頁面載入時間長。因此,透過快取先把資料存起來,之後的請求可以先去快取中找資料,找不到的話再向第三方API發送請求獲得資料,可以有效減少發送重複請求的次數,提升載入的速度。

這裡使用Redis資料庫來儲存快取資料,Redis是一種非關聯式資料庫(NoSQL),主要以key-value pair的方式儲存資料在記憶體內(in-memory),效能極高,更多關於Redis的功能介紹可參考以下文章:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
實作分享

  1. 前置作業(環境設置)

  2. 初始化專案並安裝相關套件

mkdir node-redis-cache
cd node-redis-cache
npm init -y
npm i axios express ioredis dotenv
  • axios: HTTP client,用來向API發送請求
  • express: Node.js的輕量級框架
  • ioredis: Node.js的Redis client,用來和Redis資料庫溝通的工具
  • dotenv: 幫助載入.env中的環境變數的工具

專案架構如下:

node-redis-cache
├─ .env
├─ app.js
├─ package-lock.json
├─ package.json
└─ redis.js

  1. 建立Express伺服器
    建立一個基本的Express伺服器:
//app.js const express = require('express') const app = express() require('dotenv').config() const port = process.env.PORT || 3000 app.listen(port, () => { console.log(`Express server is running on port ${port}`) })
  1. 連線Redis資料庫
    建立一個檔案來管理Redis連線資訊。這裡const redis = new Redis()建立的Redis實例會連線到host:127.0.0.1和port:6379,也可以加入密碼或使用者名稱等設定,詳情參考ioredis的文件
// redis.js const Redis = require('ioredis') const redis = new Redis() redis.on('error', (error) => { console.log('Redis client error: ', error) }) redis.on('connect', () => { console.log('Connection Successful!!') }) module.exports = redis
  1. 加入第三方API,使用axios請求資料
    使用spoonacular API需要先取得一個API key,先註冊一個帳號,這個API內容十分豐富,文件也寫得非常清楚好懂,取得key之後可以先玩玩看
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    這裡我選擇Get Random Recipes來實作,在app.js中加入以下程式碼:(記得在.env中加入API_KEY
//app.js ... const axios = require('axios') app.get('/recipes', async (req, res, next) => { try{ const recipes = await axios({ method: 'get', url: 'https://api.spoonacular.com/recipes/random', headers: { 'Content-Type': 'application/json' }, params: { limitLicense: true, number: 60, tags: 'vegetarian', apiKey: process.env.API_KEY } }) // 處理一下回傳的data const recipesRawData = recipes.data.recipes.map(recipe => ({ id: recipe.id, dishName: recipe.title, vegetarian: recipe.vegetarian, glutenFree: recipe.glutenFree, dairyFree: recipe.dairyFree, cookingTime: recipe.readyInMinutes, servings: recipe.servings, image: recipe.image, instruction: Object.assign({}, recipe.analyzedInstructions[0]?.steps.map(s => s.step)), ingredient: Object.assign({}, recipe.extendedIngredients?.map(i => i.original) ), fullDetailsUrl: recipe.spoonacularSourceUrl })) } catch(error){ console.log(error) } })
  1. 寫入快取邏輯
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    思路:如果快取中有資料,就從快取回傳資料;如果快取中沒有資料,則對API發送請求,並將回傳的資料存入快取。

    因此,在GET /recipes 寫入以下邏輯:
//app.js ... // 引入Redis client const redis = require('./redis') app.get('/recipes', async (req, res, next) => { try { // 先到Redis中找資料,尋找key是recipes的資料 await redis.get('recipes', async (err, data) => { if (err) { console.log(err) } // 如果有此筆資料,則從Redis取得資料回傳 if (data) { const recipesData = JSON.parse(data) return res.json(recipesData) } else { // 如果Redis中找不到,則呼叫第三方API請求資料 const recipes = await axios({ ... }) const recipesRawData = recipes.data.recipes.map(recipe => ({ ... })) // 得到的資料存入Redis方便下次使用,設定key是recipes,並設定快取過期時間為86400秒(1天) await redis.set('recipes', JSON.stringify(recipesRawData), 'EX', 86400, err => { if (err) console.log(err) }) return res.json(recipesRawData) } }) } catch(error) { console.log(error) } })

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Points:

  • redis.setredis.get是Redis設置/取出key-value pair的方法,這裡使用JSON.parse()將字串轉換為物件和JSON.stringfy()將物件轉換成字串存入Redis。
  • 幫快取資料設置過期時間,過期後該筆資料會自動被清除,更多關於Redis過期資料參考這裡
  1. 使用Postman測試
    接著來測試加入快取後回傳的速度,先讓伺服器運作:
    node app.js
    接著到Postman設置環境並發送請求,第一次請求由於快取中還沒有資料,因此必須向第三方API請求資料,回傳時間比較長,花了1288ms:


    接著到Redis中查看資料是否成功存入:

    成功寫入了一筆key為recipes、value是回傳的60筆食譜資料轉成的字串。


    接著再發送第二次請求,這次是從Redis中取得資料:

    這次只花了17ms就回傳,速度大幅提升!

完整的app.js程式碼如下:

const express = require('express') const app = express() const redis = require('./redis') require('dotenv').config() const port = process.env.PORT || 3000 const axios = require('axios') app.get('/recipes', async (req, res, next) => { try { // Looking for data in redis store first await redis.get('recipes', async (err, data) => { if (err) { console.log(err) } if (data) { const recipesData = JSON.parse(data) return res.json(recipesData) } else { // 如果快取沒有,則從 API 獲取資料 const recipes = await axios.get('https://api.spoonacular.com/recipes/random', { headers: { 'Content-Type': 'application/json' }, params: { limitLicense: true, number: 60, tags: 'vegetarian', apiKey: process.env.API_KEY } }) // 處理一下API回應的data const recipesRawData = recipes.data.recipes.map(recipe => ({ id: recipe.id, dishName: recipe.title, vegetarian: recipe.vegetarian, glutenFree: recipe.glutenFree, dairyFree: recipe.dairyFree, cookingTime: recipe.readyInMinutes, servings: recipe.servings, image: recipe.image, instruction: Object.assign({}, recipe.analyzedInstructions[0]?.steps.map(s => s.step)), ingredient: Object.assign({}, recipe.extendedIngredients?.map(i => i.original)), fullDetailsUrl: recipe.spoonacularSourceUrl })) // save recipe data in cache(expiry time: 86400 sec = 1 day) await redis.set('recipes', JSON.stringify(recipesRawData), 'EX', 86400) return res.json(recipesRawData) } }) } catch (error) { console.error('Error fetching recipes:', error); return res.status(500).json({ error: 'Failed to fetch recipes' }); } }) app.listen(port, () => { console.log(`Express server is running on port ${port}`) })

小結

以上簡單分享應用Redis在Node.js中實作快取,實際上快取的設計還有很多細節需要考慮,例如當快取滿了清除資料的順序、如何將快取資料和關聯式資料庫的資料同步、快取資料失效或過期的處理等,往後再更進一步探討!


參考資料

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
 本站內容僅為個人學習記錄,如有錯誤歡迎留言告知、交流討論!