มาลองทำ Caching ด้วย Node.js และ Redis กันดีกว่า

มาลองทำ Caching ด้วย Node.js และ Redis กันดีกว่า

990฿คอร์สสอนสร้างเว็บไซต์ HTML/CSS สำหรับมือใหม่ + พร้อม Workshop


สวัสดีครับ วันนี้เราจะมาลองทำการ Caching เพื่อเพิ่มความเร็วให้กับเว็บไซต์ของเราครับ ด้วย Redis

และสำหรับคนชอบเวอร์ชั่น Video Tutorial ผมอัพโหลดเป็น Youtube อีกช่องทางครับ

ข้อดีของการ Caching คือ เวลาที่เราทำการเรียก Service เดิมซ้ำๆ แทนที่จะต้องไป Query ฐานข้อมูล หรือ ไปดึงข้อมูลจาก Service อื่นๆมา ทั้งๆที่มันก็เหมือนเดิม ทำไมเราไม่ caching มันซะเลยหละ ทุกครั้งที่ข้อมูลเดิมถูกเรียก เราก็ใช้ได้เลย ไม่ต้องไป query ใหม่

ซึ่งการ Caching จริงๆมันมีหลาย Level ครับ Caching ที่ระดับ client ก็ได้ พวก web browser หรือ caching ผ่าน api server แต่สำหรับบทความนี้ จะพาทำการ caching ฝั่ง server ครับ ด้วย Node.js + Redis กัน

Redis logo

Step 1 : Redis คืออะไร แบบ Overview

Redis คือ Database ตัวนึงครับ แต่เป็น Database ที่เก็บข้อมูลใน memory ก็คือเก็บข้อมูลใน RAM นั่นเอง โดยข้อมูลที่เก็บจะเป็น Key Value อาจจะมองเป็น NoSQL ก็ได้เช่นกัน

ซึ่งการเก็บข้อมูลแบบ In memory ก็เหมือน RAM เลยคือข้อมูลจะบันทึกแค่ตอนที่เครื่องทำงาน ถ้า restart หรือปิด เปิดใหม่ ข้อมูลก็จะหายครับ (ซุึ่งจริงๆ เราสามารถ config ให้ข้อมูลไม่หาย หลังจาก reboot/restart ได้ครับ)

อ้าว แล้วมันดียังไง? เก็บแล้วข้อมูลหาย

ซึ่ง Redis มันไม่ได้ออกแบบมา ให้เก็บข้อมูลในระยะยาวอยู่แล้วครับ และ in memory ข้อดีคือความเร็ว ตัว Redis มันมาตอบโจทย์การเก็บข้อมูลที่เราใช้บ่อยๆ เอามาไว้ทำ caching หรือเก็บพวก temp file ใช้งานไม่นาน เป็นต้น

โดย Redis อย่างที่บอก เก็บข้อมูลแบบ Key Value ก็แค่ระบุว่า key ชื่อนี้ จะเก็บข้อมูลอะไรบ้าง? ซึ่งตัวข้อมูลที่เก็บ ก็เป็น String, Set, Hash เป็นต้นครับ (หลักๆ ก็มองเป็น String ก็ได้ครับ)

สำหรับใครอยากลองเล่น Redis สามารถไปลองเล่นได้ที่นี่ครับ https://try.redis.io/

Step 2 : ติดตั้ง Redis

สำหรับบทความนี้ผมติดตั้งบน Mac OS ด้วย Homebrew นะครับ

brew update
brew install redis

และสั่ง start redis ด้วย

# start redis
brew services start redis

# verify version
redis-cli --version

หรือจะ Install ด้วย Binary ก็ได้เช่นกันครับ (สำหรับ Linux)

wget http://download.redis.io/releases/redis-5.0.5.tar.gz
tar xzf redis-5.0.5.tar.gz
cd redis-5.0.5
make

สำหรับ Windows ลองดูบล็อคของ redis ครับ https://redislabs.com/blog/redis-on-windows-8-1-and-previous-versions/

เมื่อติดตั้ง และ start redis เรียบร้อยแล้ว ก็ลอง เข้าผ่าน CLI ครับ

redis-cli

127.0.0.1:6379>

ลองพิมพ์

ping

การเก็บค่า ใช้คำสั่ง SET <NAME> <VALUE> เช่น

SET name "Devahoy"

ดึงข้อมูลจาก key ที่เราบันทึกไว้ด้วยคำสั่ง GET <NAME> เช่น

GET name

"Devahoy"

เราสามารถ set expire ให้ key

# set expire ตอนกำหนด name ถ้าไม่ใส่จะไม่มี expire
SET name Devahoy 60

# set expire ทีหลัง
EXPIRE name 360

รวมถึงสามารถดูเวลา expire ของ key ได้ เช่น TTL <KEY>

# ดู expire time
TTL name

โดย -1 คือ ไม่มี expire, -2 expire แล้ว และค่าอื่นๆ คือเวลาที่ยังเหลืออยู่ (วินาที)

คร่าวๆ ก็แบบนี้ก่อนละกันครับ จริงๆมีการเก็บแบบ HSET, HGET หรืออื่นๆ อีก สามารถอ่านเพิ่มได้ครับ

Step 3 : เริ่มต้น Project

ตัวโปรเจ็คที่จะสร้าง จะเป็น Node.js และจะเรียก request เพื่อไปดึง Github API อีกทีนะครับ

mkdir node-redis-example
cd node-redis-example

npm init -y

จากนั้นผมทำการสร้างไฟล์ app.js ขึ้นมา มี endpoint แค่ root ครับ

const express = require('express');
const axios = require('axios');

const app = express();

app.get((req, res) => {
  res.json({
    message: 'OK'
  });
});

app.listen(9000, () => {
  console.log('App is running on port 9000');
});

ต่อมาก็ให้มันไปดึง github username จาก query string ครับ เป็นแบบนี้

app.get(async (req, res) => {
  const username = req.query.username || 'devahoy';
  const url = `https://api.github.com/users/${username}`;

  const response = axios.get(url);
  res.json(response.data);
})

ลองทดสอบ start server ขึ้นมา จะได้หน้าเว็บ http://localhost:9000?username=devahoy เป็น response จาก github api ครับ (ลองเปลี่ยน username เป็นชื่ออื่นๆดู)

node app.js

ลองทดสอบด้วย Postman ดู (จริงๆ ก็สามารถทำผ่านหน้าเว็บได้) แต่ว่าเราจะดู response time ครับ บน Postman มันเห็นง่ายชัดเจนดีครับ

จะเห็นว่าทุกๆ ครั้งที่เรา request ไม่ว่าจะ username ซ้ำ ตัว api มันก็ไปดึงข้อมูล github api ตลอด ทั้งที่ข้อมูลเดิม ไม่จำเป็นต้องไปดึงใหม่ก็ได้

ซึ่งจริงๆแล้ว redis ก็สามารถเอาไปใช้กรณี user หรือข้อมูลซ้ำๆ แบบนี้ก็ได้เช่นกันครับ อาจจะไม่ใช้ดึง api อีก service อาจจะเป็นการ query database ที่อาจจะใช้เวลานานก็ได้ เป็นต้น

Postman response time

Step 5 : Caching ด้วย Redis

เอาละ ลองใช้ Redis cache แทนดีกว่า เราใช้ node-redis ครับ ติดตั้งผ่าน npm ได้เลย

npm install redis

จากนั้นทำการเพิ่ม redis ดังนี้

const redis = require('redis');
const redisClient = redis.createClient(); // default port 6379

จำได้มั้ยครับ เราสามารถ get และ set ค่า ตัว node-redis มันก็ wrap มาเป็น function ให้เราแล้วครับ

redisClient.set('key', 'value');

redisClient.get('key');

// set แบบ expire time
redisClient.setex('key', 360, 'value');

ทีนี้มาที่โค๊ด ผมแก้ไขนิดหน่อย เป็นแบบนี้ เมื่อ request เข้ามา ที่ endpoint GET /

  1. เช็คก่อนว่า มี key เก็บไว้ใน redis มั้ย?
  2. ถ้ามี ก็ return ค่านั้นไปเลย (เร็ว ไม่ต้อง query หรือ fetch ค่า)
  3. ถ้าไม่มี ก็ดึงข้อมูล ปกติครับ เมื่อได้ response ก็ เก็บค่าลง redis ด้วย key ที่กำหนด
  4. เรียบร้อย
const BASE_URL = 'https://api.github.com/users';

app.get('/', (req, res) => {
  const username = req.query.username || 'devahoy';

  redisClient.get(username, async (error, data) => {
    if (error) {
      res.json({
        message: 'Something went wrong!',
        error
      });
    }

    if (data) {
      return res.json(JSON.parse(data));
    }

    const url = `${BASE_URL}/${username}`;
    const response = await axios.get(url);

    // set แบบมี expire ด้วย (เก็บไว้ 60วินาที)
    redisClient.setex(username, 60, JSON.stringify(response.data));
    res.json(response.data);
  });
});

จากโค๊ดด้านบน จะเห็นว่า

  1. redisClient.get() เป็นแบบ callback function นะครับ
  2. key ที่ผมจะเก็บคือ username ของ github เลย (จริงๆเราเก็บเป็น user:<NAME> ก็ได้ครับ จะได้แบ่งแยก และดูเป็นหมวดหมู่)
  3. และก็สังเกตว่าผมเก็บค่า json เป็นแบบ string เลยต้องใช้ JSON.stringify() และตอนแปลงจาก string เป็น json ก็ใช้ JSON.parse() ครับ

ทีนี้ลอง stop/start server ใหม่ และลองเปิด Postman เพื่อดูข้อมูลอีกครั้ง

จะเห็นว่าเวลาเรียกข้อมูลที่เคยดึงไปแล้ว และถูก cache นั้น response time ไวมากครับ

Postman after caching

Step 6 : เรียก node-redis แบบ Promise

จากโค๊ดด้านบน เราเห็นการเรียกแบบ callback function ทีนี้ redis อยากใช้แบบ Promise ทำได้มั้ย (Node v8 ขึ้นไป)

ซึ่งจริงๆสามารถใช้ได้ครับ เพียงแค่ใช้ module utils ครับ

const { promisify } = require('utils');

// ใช้ `promisify` และส่ง method ที่ต้องการใช้หเป็น Promise ไป
const asyncGet = promisify(redisClient.get).bind(redisClient);

app.get('/', async (req, res) => {
  const username = req.query.username || 'devahoy';

  const cached = await asyncGet(username);
  if (cached) {
    return res.json(JSON.parse(cached));
  }

  const url = `${BASE_URL}/${username}`;
  const response = await axios.get(url);

  // set แบบมี expire ด้วย (เก็บไว้ 60วินาที)
  redisClient.setex(username, 60, JSON.stringify(response.data));
  res.json(response.data);
});

สุดท้ายไฟล์ app.js แบบ Promise ด้วย promisify จะได้แบบนี้ครับ

const express = require('express');
const axios = require('axios');
const redis = require('redis');
const { promisify } = require('utils');

const client = redis.createClient();
const getAsync = promisify(client.get).bind(client);

const app = express();

const BASE_URL = 'https://api.github.com/users';

app.get('/', async (req, res) => {
  const username = req.query.username || 'devahoy';

  const cached = await getAsync(username);

  if (cached) {
    return res.json(JSON.parse(cached));
  }

  const url = `${BASE_URL}/${username}`;
  const response = await axios.get(url);

  client.setex(username, 60, JSON.stringify(response.data));
  res.json(response.data);
});

app.listen(9000, () => {
  console.log('app running');
});

เทียบกับแบบ callback

const express = require('express');
const axios = require('axios');
const redis = require('redis');

const client = redis.createClient();

const app = express();

const BASE_URL = 'https://api.github.com/users';

app.get('/', (req, res) => {
  const username = req.query.username || 'devahoy';

  client.get(username, async (error, data) => {
    if (error) {
      res.json({
        message: 'Something went wrong!',
        error
      });
    }

    if (data) {
      return res.json(JSON.parse(data));
    }

    const url = `${BASE_URL}/${username}`;
    const response = await axios.get(url);

    client.setex(username, 60, JSON.stringify(response.data));

    res.json(response.data);
  });
});

app.listen(9000, () => {
  console.log('app running');
});

สรุป

ก็จบไปแล้วสำหรับ Example สำหรับการทำ Caching ด้วย Nodejs + Redis ซึ่งจะเห็นว่าไม่ยากเลยครับ และแน่นอนเราควรจะทำการ caching ให้เป็นพื้นฐานไปเลยครับ แต่ไม่ใช่จะ cache ทุกอย่างนะครับ เราต้องดูเป็นกรณีๆไปครับ และนอกจาก Redis จริงๆ ก็มีตระกูล cache อื่นๆ อีกเยอะครับ ไม่ว่าจะเป็น Memcached หรือแม้กระทั่ง MongoDB ก็ตาม หวังว่าบทความนี้จะมีประโยชน์กับคนที่สนใจ redis กันนะครับ

Happy Coding

Source Code

สำหรับ Reference อ่านเพิ่มเติม

Chai Phonbopit

Chai Phonbopit: Software Engineer แห่งหนึ่ง • ผู้ชายธรรมดาๆ ที่ชื่นชอบ Node.js, JavaScript, React และ Open Source มีงานอดิเรกเป็น Acoustic Guitar และ Football นอกจากเขียนบล็อคที่เว็บนี้แล้ว ก็มีเขียนที่ https://medium.com/@Phonbopit ครับ