Home > Blog > tech

Caching Strategy และ CDN คืออะไร? สอนออกแบบ Cache Layer สำหรับ Web Application 2026

caching strategies cdn guide
Caching Strategies CDN Guide 2026
2026-04-09 | tech | 3400 words

ในโลกของ Web Application ความเร็วคือทุกสิ่ง งานวิจัยจาก Google พบว่า 53% ของผู้ใช้จะออกจากเว็บไซต์ที่โหลดนานกว่า 3 วินาที ทุกๆ 100ms ที่เว็บช้าลง Amazon สูญเสียรายได้ 1% Caching เป็นเทคนิคที่สำคัญที่สุดในการลด latency และเพิ่มประสิทธิภาพของระบบ

บทความนี้จะสอนเรื่อง Caching Strategy อย่างครบถ้วน ตั้งแต่ Browser Cache, CDN, Application Cache ไปจนถึง Redis และการออกแบบ Multi-tier Caching Architecture ที่ใช้ในระบบ Production จริง

ทำไม Caching ถึงสำคัญ?

Caching คือการเก็บสำเนาของข้อมูลที่ใช้บ่อยไว้ในตำแหน่งที่เข้าถึงได้เร็วกว่าแหล่งข้อมูลต้นทาง เพื่อลดเวลาในการตอบสนองและลดภาระของระบบ การเพิ่ม Cache Layer ที่ถูกต้องสามารถลด Response Time จากหลายวินาทีเหลือเพียงไม่กี่มิลลิวินาที

ประโยชน์ของ Caching

ความเร็วในการเข้าถึงข้อมูลแต่ละระดับ

ระดับLatencyตัวอย่าง
L1 CPU Cache~1 nsCPU register
L2 CPU Cache~5 nsCPU cache
RAM~100 nsIn-memory cache (Redis)
SSD~100 usLocal disk cache
Network (same region)~1 msCDN edge, cache server
Network (cross region)~100 msOrigin server ต่างประเทศ
Database query~10-100 msPostgreSQL, MySQL

Caching Layers — ชั้นของ Cache

ระบบ Web Application มี Cache ได้หลายชั้น ตั้งแต่ Browser ของผู้ใช้ไปจนถึง Database แต่ละชั้นมีหน้าที่และลักษณะเฉพาะที่แตกต่างกัน

1. Browser Cache

Cache ที่อยู่ใน Browser ของผู้ใช้ เป็น Cache ที่ใกล้ผู้ใช้ที่สุด ไม่ต้อง request ไป server เลย ทำงานผ่าน HTTP Headers

2. CDN Cache (Edge Cache)

Cache ที่อยู่บน Server ของ CDN Provider กระจายอยู่ทั่วโลก ผู้ใช้จะเชื่อมต่อกับ edge server ที่ใกล้ที่สุด

3. Reverse Proxy Cache

Cache ที่อยู่หน้า Application Server เช่น Nginx, Varnish ทำหน้าที่กรอง request ก่อนถึง Application

4. Application Cache

Cache ในระดับ Application เช่น In-memory cache (Redis, Memcached) หรือ Local cache ใน process

5. Database Cache

Cache ในระดับ Database เช่น Query Cache ของ MySQL หรือ Shared Buffer ของ PostgreSQL

Browser Caching — HTTP Cache Headers

Browser Cache ควบคุมด้วย HTTP Response Headers เป็น Cache ที่มีประสิทธิภาพสูงสุดเพราะไม่ต้อง request ไป server เลย

Cache-Control Header

# Cache ได้ทั้ง Browser และ CDN เป็นเวลา 1 ชั่วโมง
Cache-Control: public, max-age=3600

# Cache ได้เฉพาะ Browser เป็นเวลา 1 ชั่วโมง
Cache-Control: private, max-age=3600

# ห้าม Cache (ข้อมูลส่วนตัว, หน้า dynamic)
Cache-Control: no-store

# ต้อง revalidate กับ server ทุกครั้ง
Cache-Control: no-cache

# Cache static assets ไว้นานมาก (ใช้กับ versioned files)
Cache-Control: public, max-age=31536000, immutable

# Stale-while-revalidate — ให้ cache เก่าไปก่อน ระหว่าง revalidate
Cache-Control: public, max-age=60, stale-while-revalidate=30

ETag และ Last-Modified

# Server ส่ง ETag (hash ของ content)
HTTP/1.1 200 OK
ETag: "abc123def456"
Content-Type: application/json

# Client ส่ง If-None-Match เพื่อถามว่าเปลี่ยนไหม
GET /api/products HTTP/1.1
If-None-Match: "abc123def456"

# ถ้าไม่เปลี่ยน → 304 Not Modified (ไม่ส่ง body กลับ)
HTTP/1.1 304 Not Modified

# Last-Modified / If-Modified-Since (ใช้วันที่แทน hash)
HTTP/1.1 200 OK
Last-Modified: Wed, 09 Apr 2026 10:00:00 GMT

GET /api/products HTTP/1.1
If-Modified-Since: Wed, 09 Apr 2026 10:00:00 GMT

ตัวอย่างการตั้ง Cache Headers ใน Nginx

# nginx.conf

# Static assets — cache นานมาก
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    add_header Vary "Accept-Encoding";
}

# HTML pages — revalidate ทุกครั้ง
location ~* \.html$ {
    add_header Cache-Control "no-cache";
    add_header ETag "";
}

# API responses — cache สั้นๆ
location /api/ {
    add_header Cache-Control "public, max-age=60, stale-while-revalidate=30";
    add_header Vary "Authorization, Accept";
}

# Private data — ห้าม cache
location /api/user/ {
    add_header Cache-Control "private, no-store";
}
กลยุทธ์ที่ดีที่สุด: ใช้ Cache Busting สำหรับ static assets โดยเพิ่ม hash ในชื่อไฟล์ เช่น app.a1b2c3.js แล้วตั้ง max-age=31536000, immutable เมื่อ content เปลี่ยน hash จะเปลี่ยน URL ใหม่จะถูก cache ใหม่อัตโนมัติ

CDN — Content Delivery Network

CDN (Content Delivery Network) คือเครือข่ายของ Server ที่กระจายอยู่ทั่วโลก (เรียกว่า Edge Location หรือ PoP — Point of Presence) ทำหน้าที่เก็บสำเนาของ content ไว้ให้ผู้ใช้เข้าถึงจาก server ที่ใกล้ที่สุด แทนที่จะต้องไปถึง Origin Server ที่อาจอยู่อีกฝั่งโลก

CDN ทำงานอย่างไร?

  1. ผู้ใช้ request ไปที่ CDN domain (เช่น cdn.example.com)
  2. DNS จะ resolve ไปยัง Edge Server ที่ใกล้ผู้ใช้ที่สุด (Anycast)
  3. ถ้า Edge Server มี cache (Cache HIT) จะส่งกลับทันที ไม่ต้องไป Origin
  4. ถ้าไม่มี cache (Cache MISS) Edge Server จะ fetch จาก Origin เก็บไว้ แล้วส่งให้ผู้ใช้
  5. Request ถัดไปจาก region เดียวกันจะได้ cached version ทันที

CDN Concepts ที่ต้องรู้

เปรียบเทียบ CDN Providers

Providerจุดเด่นราคาEdge Locations
CloudflareFree tier ดี, DDoS protection, WorkersFree — Enterprise310+ cities
AWS CloudFrontIntegration กับ AWS, Lambda@EdgePay per use450+ PoPs
FastlyInstant purge, VCL config, Compute@EdgePay per use90+ PoPs
Bunny CDNราคาถูก, ง่ายมาก, performance ดี$0.01/GB120+ PoPs
AkamaiEnterprise-grade, ใหญ่ที่สุดEnterprise pricing4,100+ PoPs

ตั้งค่า Cloudflare CDN

# 1. ชี้ DNS ไปที่ Cloudflare (เปลี่ยน nameserver)
# 2. ตั้ง Page Rules หรือ Cache Rules

# Cloudflare API — Purge cache
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
  -H "Authorization: Bearer API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"files": ["https://example.com/style.css", "https://example.com/app.js"]}'

# Purge ทั้งหมด
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
  -H "Authorization: Bearer API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"purge_everything": true}'

ตั้งค่า AWS CloudFront

# สร้าง CloudFront Distribution ด้วย AWS CLI
aws cloudfront create-distribution \
  --origin-domain-name my-bucket.s3.amazonaws.com \
  --default-root-object index.html

# Invalidation (purge cache)
aws cloudfront create-invalidation \
  --distribution-id E1234567890 \
  --paths "/images/*" "/css/*" "/index.html"

# CloudFront Function (edge computing)
# ตัวอย่าง: Redirect HTTP to HTTPS
function handler(event) {
    var request = event.request;
    var headers = request.headers;

    if (headers['cloudfront-viewer-protocol'] &&
        headers['cloudfront-viewer-protocol'].value === 'http') {
        return {
            statusCode: 301,
            headers: { location: { value: 'https://' + headers.host.value + request.uri } }
        };
    }
    return request;
}

Application-Level Caching Patterns

การเลือก Caching Pattern ที่เหมาะสมขึ้นอยู่กับรูปแบบการอ่าน/เขียนข้อมูลของแอปพลิเคชัน แต่ละ Pattern มีข้อดีข้อเสียที่แตกต่างกัน

1. Cache-Aside (Lazy Loading)

Pattern ที่ใช้บ่อยที่สุด Application จัดการ Cache เอง อ่านจาก Cache ก่อน ถ้าไม่มีค่อยไป Database แล้วเก็บผลลัพธ์ใน Cache

# Python — Cache-Aside Pattern
import redis
import json

cache = redis.Redis(host='localhost', port=6379, db=0)

def get_user(user_id: str) -> dict:
    # 1. อ่านจาก Cache ก่อน
    cached = cache.get(f"user:{user_id}")
    if cached:
        print("Cache HIT")
        return json.loads(cached)

    # 2. Cache MISS — ไป Database
    print("Cache MISS")
    user = db.query("SELECT * FROM users WHERE id = %s", user_id)

    # 3. เก็บผลลัพธ์ใน Cache (TTL 5 นาที)
    cache.setex(f"user:{user_id}", 300, json.dumps(user))

    return user

def update_user(user_id: str, data: dict):
    # อัปเดต Database
    db.execute("UPDATE users SET ... WHERE id = %s", user_id)
    # ลบ Cache (ไม่ใช่อัปเดต — เพื่อหลีกเลี่ยง race condition)
    cache.delete(f"user:{user_id}")

2. Read-Through Cache

Cache ทำหน้าที่โหลดข้อมูลจาก Database เอง Application ไม่ต้องจัดการ logic การโหลด

# Read-Through — Cache จัดการ data loading เอง
class ReadThroughCache:
    def __init__(self, cache_client, db_client, ttl=300):
        self.cache = cache_client
        self.db = db_client
        self.ttl = ttl

    def get(self, key: str, query_fn):
        # ลอง Cache ก่อน
        cached = self.cache.get(key)
        if cached:
            return json.loads(cached)

        # Cache ทำหน้าที่โหลดจาก DB เอง
        data = query_fn()
        self.cache.setex(key, self.ttl, json.dumps(data))
        return data

# ใช้งาน
cache = ReadThroughCache(redis_client, db_client)
user = cache.get(
    f"user:123",
    lambda: db.query("SELECT * FROM users WHERE id = 123")
)

3. Write-Through Cache

เขียนข้อมูลไปทั้ง Cache และ Database พร้อมกัน ข้อมูลใน Cache จะ sync กับ Database เสมอ

# Write-Through — เขียนทั้ง Cache และ DB
class WriteThroughCache:
    def put(self, key: str, value: dict):
        # เขียน Database ก่อน
        db.execute("INSERT INTO ... VALUES (...)", value)

        # แล้วเขียน Cache
        self.cache.setex(key, self.ttl, json.dumps(value))
        # ข้อมูลใน Cache จะ consistent กับ DB เสมอ

    def get(self, key: str):
        cached = self.cache.get(key)
        if cached:
            return json.loads(cached)
        # ถ้า Cache MISS ไป DB แล้วเก็บใน Cache
        data = db.query(key)
        self.cache.setex(key, self.ttl, json.dumps(data))
        return data

4. Write-Behind (Write-Back) Cache

เขียนข้อมูลเข้า Cache ก่อน แล้ว async เขียนลง Database ทีหลัง ได้ write performance สูงมาก แต่มีความเสี่ยงเรื่อง data loss

# Write-Behind — เขียน Cache ก่อน DB ตามทีหลัง
import asyncio
from collections import deque

class WriteBehindCache:
    def __init__(self):
        self.write_queue = deque()
        self.batch_size = 100
        self.flush_interval = 5  # วินาที

    def put(self, key: str, value: dict):
        # เขียน Cache ทันที (เร็วมาก)
        self.cache.set(key, json.dumps(value))

        # เพิ่มเข้า Queue เพื่อเขียน DB ทีหลัง
        self.write_queue.append((key, value))

    async def flush_to_db(self):
        """Background task ที่เขียนลง DB เป็น batch"""
        while True:
            await asyncio.sleep(self.flush_interval)
            batch = []
            while self.write_queue and len(batch) < self.batch_size:
                batch.append(self.write_queue.popleft())

            if batch:
                # Batch insert/update เข้า DB
                db.executemany("INSERT INTO ...", batch)
                print(f"Flushed {len(batch)} records to DB")

5. Refresh-Ahead Cache

Cache จะ refresh ตัวเองก่อนหมดอายุ โดยดูจาก access pattern ป้องกันไม่ให้ผู้ใช้ต้องรอ Cache MISS

# Refresh-Ahead — Cache refresh ก่อนหมดอายุ
import threading

class RefreshAheadCache:
    def __init__(self, ttl=300, refresh_ratio=0.8):
        self.ttl = ttl
        self.refresh_threshold = ttl * refresh_ratio  # refresh เมื่อเหลือ 20%

    def get(self, key: str, loader_fn):
        cached = self.cache.get(key)
        ttl_remaining = self.cache.ttl(key)

        if cached and ttl_remaining > 0:
            # ถ้าใกล้หมดอายุ — refresh ใน background
            if ttl_remaining < (self.ttl - self.refresh_threshold):
                threading.Thread(
                    target=self._refresh,
                    args=(key, loader_fn)
                ).start()
            return json.loads(cached)

        # Cache MISS — โหลดจาก DB
        data = loader_fn()
        self.cache.setex(key, self.ttl, json.dumps(data))
        return data

    def _refresh(self, key, loader_fn):
        data = loader_fn()
        self.cache.setex(key, self.ttl, json.dumps(data))
เลือก Pattern ไหนดี?
Cache-Aside: ใช้ได้ทั่วไป เหมาะกับ read-heavy workload
Read-Through: เหมาะเมื่อต้องการ abstraction ที่สะอาด
Write-Through: เมื่อต้องการ consistency สูง
Write-Behind: เมื่อต้องการ write performance สูงมาก (ยอมรับ risk ได้)
Refresh-Ahead: เมื่อต้องการ latency ต่ำสม่ำเสมอสำหรับ hot data

Redis เป็น Cache Layer

Redis เป็น In-memory Data Store ที่นิยมใช้เป็น Cache Layer มากที่สุด เพราะรองรับ data structure หลากหลาย มีความเร็วสูงมาก (100,000+ operations/sec) และมี feature ที่ช่วยจัดการ Cache ได้ดี

Redis TTL และ Eviction Policies

# ตั้ง TTL (Time to Live)
SET user:123 '{"name":"Bom"}' EX 300        # หมดอายุใน 300 วินาที
SET session:abc '{"user_id":123}' PX 1800000 # หมดอายุใน 30 นาที (milliseconds)
EXPIRE user:123 600                            # เปลี่ยน TTL เป็น 600 วินาที
TTL user:123                                   # ดูเวลาที่เหลือ
PERSIST user:123                               # ลบ TTL (ไม่มีวันหมดอายุ)

# Eviction Policies (เมื่อ memory เต็ม)
# ตั้งใน redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru   # ลบ key ที่ใช้นานสุดออก

# Policies ที่มีให้เลือก:
# noeviction      — ไม่ลบ, return error เมื่อเต็ม
# allkeys-lru     — ลบ key ที่ใช้นานสุดจากทุก key (แนะนำสำหรับ cache)
# allkeys-lfu     — ลบ key ที่ใช้น้อยสุดจากทุก key
# volatile-lru    — ลบ key ที่มี TTL และใช้นานสุด
# volatile-lfu    — ลบ key ที่มี TTL และใช้น้อยสุด
# volatile-ttl    — ลบ key ที่ TTL เหลือน้อยสุด
# allkeys-random  — ลบ random key

Cache Warming — อุ่น Cache ก่อนใช้งาน

# Cache Warming Script — โหลดข้อมูลยอดนิยมเข้า Cache ตอนเริ่มระบบ
import redis
import json

cache = redis.Redis()

def warm_cache():
    print("Warming cache...")

    # โหลดสินค้ายอดนิยม 1000 รายการ
    popular_products = db.query("""
        SELECT * FROM products
        ORDER BY view_count DESC
        LIMIT 1000
    """)
    for product in popular_products:
        cache.setex(
            f"product:{product['id']}",
            3600,
            json.dumps(product)
        )

    # โหลด Config/Settings
    settings = db.query("SELECT * FROM app_settings")
    cache.set("app:settings", json.dumps(settings))

    # โหลด Categories
    categories = db.query("SELECT * FROM categories WHERE active = 1")
    cache.set("app:categories", json.dumps(categories))

    print(f"Warmed {len(popular_products)} products + settings + categories")

# รันตอน application startup
warm_cache()

Memcached vs Redis

ด้านRedisMemcached
Data StructuresString, Hash, List, Set, Sorted Set, StreamString only
PersistenceRDB + AOFไม่มี (memory only)
ReplicationMaster-Replicaไม่มี built-in
ClusterRedis ClusterClient-side sharding
Pub/Subรองรับไม่รองรับ
Lua Scriptingรองรับไม่รองรับ
Max Key Size512 MB1 MB
Multi-threadingSingle-threaded (I/O threads ใน 6.0+)Multi-threaded
ใช้เมื่อไหร่ต้องการ feature หลากหลายCache ง่ายๆ ขนาดใหญ่

Cache Invalidation Strategies

คำกล่าวที่ว่า "There are only two hard things in Computer Science: cache invalidation and naming things" ของ Phil Karlton นั้นเป็นจริงมาก Cache Invalidation คือการทำให้ Cache เป็นปัจจุบันเมื่อข้อมูลต้นทางเปลี่ยน ถ้าทำไม่ดีจะเกิดปัญหา Stale Data

1. Time-Based Invalidation (TTL)

# วิธีง่ายที่สุด — ตั้ง TTL แล้วปล่อยให้หมดอายุเอง
cache.setex("product:123", 300, data)  # 5 นาที

# ข้อดี: ง่าย, ไม่ต้องจัดการ invalidation
# ข้อเสีย: ข้อมูลอาจ stale จนกว่า TTL จะหมด
# เหมาะกับ: ข้อมูลที่เปลี่ยนไม่บ่อย, ยอมรับ staleness ได้

2. Event-Based Invalidation

# ลบ Cache เมื่อข้อมูลเปลี่ยน
def update_product(product_id: str, data: dict):
    # อัปเดต Database
    db.execute("UPDATE products SET ... WHERE id = %s", product_id)

    # ลบ Cache ที่เกี่ยวข้องทั้งหมด
    cache.delete(f"product:{product_id}")
    cache.delete(f"product_list:category:{data['category']}")
    cache.delete("product_list:featured")

    # หรือ Publish event ให้ service อื่นลบ cache ด้วย
    redis_pubsub.publish("cache_invalidation", json.dumps({
        "type": "product_updated",
        "id": product_id,
    }))

3. Version-Based Invalidation

# ใช้ version number ใน cache key
# เมื่อ version เปลี่ยน cache key ก็เปลี่ยน ไม่ต้องลบ cache เก่า

def get_cache_version(entity: str) -> int:
    version = cache.get(f"version:{entity}")
    return int(version) if version else 1

def get_product(product_id: str):
    version = get_cache_version("products")
    key = f"product:v{version}:{product_id}"

    cached = cache.get(key)
    if cached:
        return json.loads(cached)

    data = db.query("SELECT * FROM products WHERE id = %s", product_id)
    cache.setex(key, 3600, json.dumps(data))
    return data

def invalidate_all_products():
    # แค่เพิ่ม version — cache key เก่าจะหมดอายุเอง
    cache.incr("version:products")

Cache Stampede Prevention

Cache Stampede (หรือ Thundering Herd) เกิดเมื่อ Cache หมดอายุพร้อมกัน ทำให้ request จำนวนมากไป Database พร้อมกัน อาจทำให้ Database ล่ม

1. Mutex/Locking

# ใช้ Lock ป้องกันไม่ให้หลาย request โหลดข้อมูลพร้อมกัน
def get_with_lock(key: str, loader_fn, ttl: int = 300):
    # ลอง Cache ก่อน
    cached = cache.get(key)
    if cached:
        return json.loads(cached)

    # ลอง acquire lock
    lock_key = f"lock:{key}"
    acquired = cache.set(lock_key, "1", nx=True, ex=10)  # Lock 10 วินาที

    if acquired:
        try:
            # ได้ Lock — โหลดจาก DB
            data = loader_fn()
            cache.setex(key, ttl, json.dumps(data))
            return data
        finally:
            cache.delete(lock_key)
    else:
        # ไม่ได้ Lock — รอแล้วลอง Cache อีกครั้ง
        import time
        time.sleep(0.1)
        cached = cache.get(key)
        if cached:
            return json.loads(cached)
        # ถ้ายังไม่มี ลอง Lock อีกครั้ง (recursive)
        return get_with_lock(key, loader_fn, ttl)

2. Probabilistic Early Expiration

# XFetch Algorithm — สุ่ม refresh ก่อนหมดอายุ
import random
import math

def xfetch(key: str, loader_fn, ttl: int = 300, beta: float = 1.0):
    cached = cache.get(key)
    remaining_ttl = cache.ttl(key)

    if cached and remaining_ttl > 0:
        # คำนวณว่าควร refresh หรือยัง
        # ยิ่งใกล้หมดอายุ ยิ่งมีโอกาส refresh สูง
        delta = ttl - remaining_ttl  # เวลาที่ผ่านไป
        random_value = -beta * math.log(random.random())

        if delta < random_value * ttl:
            return json.loads(cached)  # ยังไม่ refresh

    # Refresh cache
    data = loader_fn()
    cache.setex(key, ttl, json.dumps(data))
    return data

Stale-While-Revalidate Pattern

# ส่ง stale data ทันที แล้ว revalidate ใน background
import threading

def get_with_swr(key: str, loader_fn, ttl: int = 300, stale_ttl: int = 60):
    cached = cache.get(key)
    remaining_ttl = cache.ttl(key)

    if cached:
        if remaining_ttl <= 0:
            # Stale แต่ยังอยู่ใน stale period
            stale_data = cache.get(f"stale:{key}")
            if stale_data:
                # ส่ง stale data ทันที
                # Revalidate ใน background
                threading.Thread(
                    target=_revalidate,
                    args=(key, loader_fn, ttl, stale_ttl)
                ).start()
                return json.loads(stale_data)

        return json.loads(cached)

    # Cache MISS — โหลดจาก DB
    data = loader_fn()
    cache.setex(key, ttl, json.dumps(data))
    cache.setex(f"stale:{key}", ttl + stale_ttl, json.dumps(data))
    return data

def _revalidate(key, loader_fn, ttl, stale_ttl):
    data = loader_fn()
    cache.setex(key, ttl, json.dumps(data))
    cache.setex(f"stale:{key}", ttl + stale_ttl, json.dumps(data))

Caching สำหรับ API

REST API Caching

# Node.js + Express — API caching middleware
const redis = require('redis');
const client = redis.createClient();

function cacheMiddleware(ttl = 60) {
    return async (req, res, next) => {
        const key = `api:${req.originalUrl}`;
        const cached = await client.get(key);

        if (cached) {
            res.set('X-Cache', 'HIT');
            return res.json(JSON.parse(cached));
        }

        // Override res.json เพื่อ cache response
        const originalJson = res.json.bind(res);
        res.json = (data) => {
            client.setEx(key, ttl, JSON.stringify(data));
            res.set('X-Cache', 'MISS');
            originalJson(data);
        };
        next();
    };
}

// ใช้งาน
app.get('/api/products', cacheMiddleware(300), getProducts);
app.get('/api/products/:id', cacheMiddleware(600), getProductById);

// Invalidate เมื่อ data เปลี่ยน
app.post('/api/products', async (req, res) => {
    const product = await createProduct(req.body);
    // ลบ cache ที่เกี่ยวข้อง
    await client.del('api:/api/products');
    res.json(product);
});

GraphQL Caching

# GraphQL caching ซับซ้อนกว่า REST เพราะ query เปลี่ยนได้ตลอด

# 1. Persisted Queries — hash query แล้ว cache ตาม hash
# Client ส่ง hash แทน query string เต็ม
GET /graphql?extensions={"persistedQuery":{"sha256Hash":"abc123"}}

# 2. Response Caching — cache ตาม query + variables
import hashlib

def cache_graphql(query: str, variables: dict, ttl: int = 60):
    # สร้าง cache key จาก query + variables
    key_data = json.dumps({"query": query, "variables": variables}, sort_keys=True)
    key = f"graphql:{hashlib.sha256(key_data.encode()).hexdigest()}"

    cached = cache.get(key)
    if cached:
        return json.loads(cached)

    result = execute_query(query, variables)
    cache.setex(key, ttl, json.dumps(result))
    return result

# 3. Field-level caching with DataLoader
# ใช้ DataLoader เพื่อ batch + cache ในระดับ field

Edge Computing และ Edge Caching

Edge Computing คือการรัน logic บน Edge Server ของ CDN แทนที่จะส่ง request กลับไป Origin ทำให้ได้ latency ต่ำมากและลดภาระ Origin Server

# Cloudflare Workers — JavaScript ที่รันบน Edge
export default {
  async fetch(request, env) {
    const cache = caches.default;
    const url = new URL(request.url);

    // ลอง Cache ก่อน
    let response = await cache.match(request);
    if (response) {
      return response;
    }

    // Cache MISS — fetch จาก Origin
    response = await fetch(request);

    // Clone response เพื่อ cache
    const responseToCache = response.clone();

    // ตั้ง cache headers
    const headers = new Headers(responseToCache.headers);
    headers.set('Cache-Control', 'public, max-age=3600');

    const cachedResponse = new Response(responseToCache.body, {
      status: responseToCache.status,
      headers,
    });

    // เก็บใน Edge Cache
    await cache.put(request, cachedResponse);

    return response;
  },
};

Multi-tier Caching Architecture

# สถาปัตยกรรม Cache หลายชั้น

# Layer 1: In-Process Cache (fastest, smallest)
from functools import lru_cache
from cachetools import TTLCache

local_cache = TTLCache(maxsize=1000, ttl=60)

# Layer 2: Distributed Cache (Redis)
redis_cache = redis.Redis(host='redis-cluster', port=6379)

# Layer 3: CDN (for public content)
# ตั้ง Cache-Control headers

class MultiTierCache:
    def __init__(self):
        self.local = TTLCache(maxsize=1000, ttl=60)    # L1: 60 วินาที
        self.redis = redis.Redis()                       # L2: 5 นาที
        self.redis_ttl = 300

    def get(self, key: str, loader_fn):
        # L1: In-Process Cache
        if key in self.local:
            return self.local[key]

        # L2: Redis
        cached = self.redis.get(key)
        if cached:
            data = json.loads(cached)
            self.local[key] = data  # เก็บใน L1
            return data

        # L3: Database
        data = loader_fn()
        self.redis.setex(key, self.redis_ttl, json.dumps(data))  # เก็บใน L2
        self.local[key] = data  # เก็บใน L1
        return data

    def invalidate(self, key: str):
        self.local.pop(key, None)
        self.redis.delete(key)
        # Publish event ให้ instance อื่นลบ L1 cache ด้วย
        self.redis.publish("cache_invalidation", key)

Monitoring Cache Performance

Cache Hit Ratio

# ตัวเลขที่สำคัญที่สุดของ Cache คือ Hit Ratio
# Hit Ratio = Cache Hits / (Cache Hits + Cache Misses)

# Redis INFO stats
redis-cli INFO stats | grep keyspace
# keyspace_hits:1234567
# keyspace_misses:12345
# Hit Ratio = 1234567 / (1234567 + 12345) = 99.01%

# Custom monitoring
import time

class CacheMetrics:
    def __init__(self):
        self.hits = 0
        self.misses = 0
        self.latencies = []

    def record_hit(self, latency_ms: float):
        self.hits += 1
        self.latencies.append(latency_ms)

    def record_miss(self, latency_ms: float):
        self.misses += 1
        self.latencies.append(latency_ms)

    @property
    def hit_ratio(self) -> float:
        total = self.hits + self.misses
        return self.hits / total if total > 0 else 0

    @property
    def avg_latency(self) -> float:
        return sum(self.latencies) / len(self.latencies) if self.latencies else 0

    def report(self):
        print(f"Hit Ratio: {self.hit_ratio:.2%}")
        print(f"Hits: {self.hits}, Misses: {self.misses}")
        print(f"Avg Latency: {self.avg_latency:.2f} ms")

# เป้าหมาย:
# Cache Hit Ratio > 90% — ดี
# Cache Hit Ratio > 95% — ดีมาก
# Cache Hit Ratio > 99% — ยอดเยี่ยม

Common Caching Mistakes

1. Cache ทุกอย่าง

ไม่ใช่ทุกอย่างที่ควร cache ข้อมูลที่เปลี่ยนบ่อยมาก (เช่น real-time stock price) หรือข้อมูลที่เข้าถึงน้อยมาก (long-tail) ไม่ควร cache เพราะ hit ratio จะต่ำมาก ทำให้เปลือง memory โดยไม่ได้ประโยชน์ ควร cache เฉพาะ hot data ที่เข้าถึงบ่อย

2. TTL สั้นเกินไป หรือยาวเกินไป

TTL สั้นเกินไปทำให้ cache miss บ่อย ไม่ได้ประโยชน์ TTL ยาวเกินไปทำให้ข้อมูล stale นาน ต้องหา balance ที่เหมาะกับแต่ละ use case เช่น product catalog อาจใช้ 5-15 นาที ส่วน user session อาจใช้ 30 นาที ถึง 24 ชั่วโมง

3. ลืม Invalidate Cache

เมื่อ data เปลี่ยนแต่ไม่ invalidate cache ผู้ใช้จะเห็นข้อมูลเก่า ต้องมี strategy ที่ชัดเจนว่า cache key ไหนต้อง invalidate เมื่อไหร่ ควรใช้ naming convention ที่ดีเพื่อให้ invalidate ได้ถูกต้อง

4. Cache Key ไม่ดี

# BAD: Cache key ไม่ unique พอ
cache.set("users", data)  # ถ้ามี filter จะ cache ผิด

# GOOD: Cache key ที่ precise
cache.set(f"users:page={page}:limit={limit}:sort={sort}", data)

# GOOD: ใช้ hash สำหรับ key ยาว
import hashlib
params = json.dumps({"page": 1, "limit": 20, "filters": filters}, sort_keys=True)
key = f"users:{hashlib.md5(params.encode()).hexdigest()}"

5. ไม่ Monitor Cache Performance

ต้อง monitor hit ratio, memory usage, eviction rate เสมอ ถ้า hit ratio ต่ำกว่า 80% แสดงว่า caching strategy มีปัญหา อาจต้องปรับ TTL, key design, หรือ eviction policy

6. Single Point of Failure

ถ้า Cache Server ล่ม ทุก request จะไป Database ตรงๆ อาจทำให้ Database ล่มตาม ต้องมี fallback strategy เช่น Circuit Breaker ที่ degrade gracefully เมื่อ Cache ไม่พร้อมใช้งาน และใช้ Redis Cluster หรือ Sentinel สำหรับ High Availability

สรุป

Caching เป็นเทคนิคที่สำคัญที่สุดอย่างหนึ่งในการสร้าง Web Application ที่มีประสิทธิภาพสูง การเลือก Cache Layer ที่ถูกต้อง (Browser, CDN, Application, Database) การเลือก Caching Pattern ที่เหมาะสม (Cache-Aside, Read-Through, Write-Through, Write-Behind) และการจัดการ Cache Invalidation อย่างมีระบบ ล้วนเป็นทักษะที่นักพัฒนาทุกคนควรมี

เริ่มต้นด้วยการตั้ง HTTP Cache Headers ให้ถูกต้อง ใช้ CDN สำหรับ static content จากนั้นเพิ่ม Redis เป็น Application Cache สำหรับข้อมูลที่อ่านบ่อย และ monitor cache hit ratio อย่างสม่ำเสมอ เมื่อระบบเติบโตขึ้น ค่อยเพิ่ม Multi-tier Cache และ Edge Computing ตามความจำเป็น การ cache ที่ดีไม่ใช่แค่ทำให้เร็ว แต่ต้องทำให้ถูกต้องและ consistent ด้วย


Back to Blog | iCafe Forex | SiamLanCard | Siam2R