ในโลกของ 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 — ผู้ใช้ได้รับข้อมูลเร็วขึ้น 10-1000 เท่า ขึ้นอยู่กับ cache layer
- ลดภาระ Database — Query ที่ซ้ำกันไม่ต้องไปถึง Database ทุกครั้ง ช่วยให้ Database รับ load ได้มากขึ้น
- ลดค่าใช้จ่าย — ใช้ Bandwidth น้อยลง Server ทำงานน้อยลง ไม่ต้องเพิ่ม instance
- เพิ่ม Scalability — ระบบรองรับ Traffic เพิ่มขึ้นได้โดยไม่ต้องเพิ่ม Infrastructure มากนัก
- เพิ่ม Availability — แม้ Origin Server ล่ม CDN ยังสามารถ serve cached content ได้
ความเร็วในการเข้าถึงข้อมูลแต่ละระดับ
| ระดับ | Latency | ตัวอย่าง |
|---|---|---|
| L1 CPU Cache | ~1 ns | CPU register |
| L2 CPU Cache | ~5 ns | CPU cache |
| RAM | ~100 ns | In-memory cache (Redis) |
| SSD | ~100 us | Local disk cache |
| Network (same region) | ~1 ms | CDN edge, cache server |
| Network (cross region) | ~100 ms | Origin server ต่างประเทศ |
| Database query | ~10-100 ms | PostgreSQL, 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";
}
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 ทำงานอย่างไร?
- ผู้ใช้ request ไปที่ CDN domain (เช่น cdn.example.com)
- DNS จะ resolve ไปยัง Edge Server ที่ใกล้ผู้ใช้ที่สุด (Anycast)
- ถ้า Edge Server มี cache (Cache HIT) จะส่งกลับทันที ไม่ต้องไป Origin
- ถ้าไม่มี cache (Cache MISS) Edge Server จะ fetch จาก Origin เก็บไว้ แล้วส่งให้ผู้ใช้
- Request ถัดไปจาก region เดียวกันจะได้ cached version ทันที
CDN Concepts ที่ต้องรู้
- Origin Server — Server ต้นทางที่เก็บข้อมูลจริง CDN จะ fetch ข้อมูลจากที่นี่เมื่อ Cache Miss
- Edge Location / PoP — จุดให้บริการของ CDN ที่กระจายอยู่ทั่วโลก ผู้ใช้จะเชื่อมต่อกับ PoP ที่ใกล้ที่สุด
- TTL (Time to Live) — ระยะเวลาที่ Cache ยัง valid ก่อนจะ stale และต้อง revalidate กับ Origin
- Cache Key — ตัวระบุว่า request ไหนจะ match กับ cache ไหน ปกติใช้ URL + query string
- Cache Invalidation — การลบ Cache ออกก่อนหมดอายุ เช่น เมื่อ content เปลี่ยน
- Purge — การสั่งลบ Cache ของ URL เฉพาะออกจากทุก Edge Location
เปรียบเทียบ CDN Providers
| Provider | จุดเด่น | ราคา | Edge Locations |
|---|---|---|---|
| Cloudflare | Free tier ดี, DDoS protection, Workers | Free — Enterprise | 310+ cities |
| AWS CloudFront | Integration กับ AWS, Lambda@Edge | Pay per use | 450+ PoPs |
| Fastly | Instant purge, VCL config, Compute@Edge | Pay per use | 90+ PoPs |
| Bunny CDN | ราคาถูก, ง่ายมาก, performance ดี | $0.01/GB | 120+ PoPs |
| Akamai | Enterprise-grade, ใหญ่ที่สุด | Enterprise pricing | 4,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))
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
| ด้าน | Redis | Memcached |
|---|---|---|
| Data Structures | String, Hash, List, Set, Sorted Set, Stream | String only |
| Persistence | RDB + AOF | ไม่มี (memory only) |
| Replication | Master-Replica | ไม่มี built-in |
| Cluster | Redis Cluster | Client-side sharding |
| Pub/Sub | รองรับ | ไม่รองรับ |
| Lua Scripting | รองรับ | ไม่รองรับ |
| Max Key Size | 512 MB | 1 MB |
| Multi-threading | Single-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 ด้วย
