วิธี Cache ข้อมูล Ecommerce โดยไม่แสดงราคาหรือสต็อกที่ล้าสมัย

การ Caching คือหนึ่งในวิธีที่เร็วที่สุดในการเพิ่มประสิทธิภาพร้านค้าออนไลน์ — และในขณะเดียวกันก็เป็นหนึ่งในวิธีที่ง่ายที่สุดในการทำลายความไว้วางใจของลูกค้า ลองนึกภาพลูกค้าที่กดเพิ่มสินค้าลงตะกร้าในราคา ฿1,290 แต่ถูกเรียกเก็บเงิน ฿1,790 ตอน checkout — โอกาสที่เขาจะกลับมาซื้ออีกครั้งนั้นแทบจะเป็นศูนย์ หรือปุ่ม "เพิ่มลงตะกร้า" บนสินค้าที่หมดสต็อกไปแล้วตั้งแต่สามชั่วโมงก่อน ก็คือ Ticket แจ้งปัญหาที่รอวันเกิดขึ้น

คู่มือนี้จะอธิบายวิธี cache ข้อมูลที่ถูกต้อง ด้วย TTL ที่เหมาะสม และกลยุทธ์การ Invalidation ที่ดี — เพื่อให้ได้ประโยชน์ด้านความเร็วโดยไม่เสียความแม่นยำ

บริบทตลาดไทย: แพลตฟอร์มอย่าง Lazada, Shopee, LINE Shopping และ TikTok Shop ปรับราคาและโปรโมชันแบบ Real-time ตลอดเวลา โดยเฉพาะช่วงแคมเปญ 11.11, 12.12 หรือ Hardsale ของ Shopee ปริมาณ Traffic และการเปลี่ยนแปลงราคาในช่วงนั้นสูงมาก การจัดการ Cache ที่ดีจึงไม่ใช่แค่เรื่อง "ดีถ้ามี" — แต่เป็นสิ่งจำเป็น


ทำไม Ecommerce Caching ถึงแตกต่างจากกรณีอื่น

คู่มือ Caching ส่วนใหญ่มองว่าข้อมูลล้าสมัย (Staleness) เป็นเรื่องที่ยอมรับได้ แต่สำหรับ Ecommerce ข้อมูลบางประเภทห้ามล้าสมัยเด็ดขาด:

  • ราคาสินค้า — เปลี่ยนตามโปรโมชัน กฎภาษี หรืออัตราแลกเปลี่ยน (โดยเฉพาะร้านที่ขายในหลายสกุลเงิน เช่น THB/USD)
  • จำนวนสต็อก — สำคัญมากสำหรับสินค้า Limited Edition หรือช่วง Flash Sale บน Shopee/Lazada
  • โค้ดส่วนลด — หมดอายุหรือถูกใช้ครบจำนวนได้ระหว่าง Session
  • ค่าจัดส่ง — ขึ้นอยู่กับขนส่ง (Kerry, Flash Express, J&T, ไปรษณีย์ไทย) และปลายทาง

ข้อมูลที่รับ Staleness ได้มากกว่า:

  • รูปภาพและรายละเอียดสินค้า
  • โครงสร้างหมวดหมู่และ Navigation
  • รีวิวและคะแนนสินค้า
  • สินค้าแนะนำ

หลักสำคัญคือ ต้องรู้ว่าข้อมูลแต่ละประเภทอยู่ในกลุ่มไหน — แล้วใช้กลยุทธ์ Caching ที่ต่างกันในแต่ละกลุ่ม


ขั้นตอนที่ 1: จำแนกข้อมูลตามระดับความทนต่อความล้าสมัย

ก่อนเขียน Logic Cache ใดๆ ให้สร้างตารางจำแนกข้อมูลสำหรับร้านของคุณ นี่คือ Template เริ่มต้น:

ประเภทข้อมูล ทนต่อ Staleness TTL แนะนำ สิ่งที่ Trigger การ Invalidate
รูปภาพสินค้า สูง 24 ชม.–7 วัน อัปเดตไฟล์ Asset
รายละเอียดสินค้า ปานกลาง 1–4 ชม. แก้ไขเนื้อหา
หมวดหมู่ / Navigation ปานกลาง 1–2 ชม. เปลี่ยนโครงสร้างแคตตาล็อก
ราคาปกติ ต่ำ 5–15 นาที เปลี่ยน Price Rule
ราคาโปรโมชัน ต่ำมาก 1–2 นาที เริ่ม/จบแคมเปญ
จำนวนสต็อก ต่ำมาก 30–60 วินาที มีออเดอร์ / อัปเดตสต็อก
ความถูกต้องของโค้ดส่วนลด ไม่มีเลย ห้าม Cache ตรวจสอบ Live เสมอ
ยอดรวมตะกร้า ไม่มีเลย ห้าม Cache คำนวณ Live เสมอ

หลักการง่ายๆ: ถ้าข้อมูลล้าสมัยแล้วอาจทำให้เกิดความไม่ตรงกันทางการเงิน หรือทำให้ลูกค้าได้รับประสบการณ์ที่ผิดพลาด ห้าม Cache — หรือใช้กลยุทธ์ Write-through พร้อม Immediate Invalidation


ขั้นตอนที่ 2: ใช้สถาปัตยกรรม Cache แบบหลายชั้น

Cache ชั้นเดียวสร้างจุดล้มเหลวเดียวและให้ TTL เดียวกับทุกอย่าง ให้ใช้สามชั้นที่มีหน้าที่ต่างกันแทน:

Request จากลูกค้า
  │
  ▼
┌─────────────────────────┐
│   CDN / Edge Cache      │  ← Static Assets, หน้าหมวดหมู่ที่ Render แล้ว
└─────────────────────────┘
  │ miss
  ▼
┌─────────────────────────┐
│  Application Cache      │  ← ข้อมูลสินค้า, รายการราคา, Snapshot สต็อก
│  (Redis / Memcached)    │
└─────────────────────────┘
  │ miss
  ▼
┌─────────────────────────┐
│   Origin / Database     │  ← Source of Truth: ราคาจริง, สต็อกจริง
└─────────────────────────┘

ชั้นที่ 1: CDN / Edge Cache

Cache HTML ที่ Render แล้วสำหรับหน้าหมวดหมู่และหน้าสินค้าที่ ไม่มี Personalisation ใช้ Surrogate Keys (รองรับโดย Cloudflare, Fastly, AWS CloudFront) เพื่อ Invalidate ทุกหน้าที่อ้างถึงสินค้าชิ้นนั้นได้ทันทีเมื่อมีการเปลี่ยนแปลง

ห้าม Cache ที่ CDN Level:

  • หน้าที่แสดงราคาของผู้ใช้ที่ Login แล้ว (เช่น ราคา Reseller หรือราคา Corporate)
  • หน้าที่แสดงสถานะตะกร้า
  • หน้าที่แสดงสต็อก Real-time ("เหลือสินค้าเพียง 2 ชิ้น!")

ระวัง Header Vary: Cookie ที่ตั้งค่าผิด — อาจทำให้ CDN Cache ถูก Bypass สำหรับผู้ใช้ที่ Login ทั้งหมดโดยไม่ตั้งใจ

ชั้นที่ 2: Application Cache (Redis)

นี่คือ Layer หลักที่ทำงานหนักที่สุด ใช้ Cache Price List ที่ Resolve แล้ว, Inventory Snapshot, Product Attribute Set และทุกอย่างที่ต้อง Join หลาย Table

# ตัวอย่าง: ดึงราคาสินค้าด้วย Cache-Aside Pattern และ TTL สั้น
def get_price(product_id: str, customer_group: str) -> Decimal:
    cache_key = f"price:{product_id}:{customer_group}"
    cached = redis.get(cache_key)

    if cached:
        return Decimal(cached)

    price = db.query_price(product_id, customer_group)
    redis.setex(cache_key, ttl=60, value=str(price))  # TTL 60 วินาที
    return price

ใช้ Namespaced Keys (price:, stock:, product:) เพื่อให้ Flush ข้อมูลทั้งหมวดได้ระหว่าง Bulk Update โดยไม่กระทบ Cache อื่น

ชั้นที่ 3: Origin / Database

ห้าม Cache ที่ Layer นี้ — มันคือ Source of Truth การ Caching ใดๆ ในชั้นนี้ควรให้ Database จัดการเองผ่าน Query Cache หรือ Read Replica ไม่ใช่ Application Logic


ขั้นตอนที่ 3: ใช้ Event-Driven Cache Invalidation

TTL Expiry เป็นแค่ Safety Net ไม่ใช่กลยุทธ์ สำหรับราคาและสต็อก คุณต้องการ Event-Driven Invalidation — ล้าง Cache ทันทีที่ข้อมูลต้นทางเปลี่ยน

Pattern: Publish on Write, Invalidate on Consume

อัปเดตราคาใน ERP / ระบบหลังบ้าน
        │
        ▼
  Message Broker
  (Kafka / AWS SQS / Redis Pub/Sub)
        │
        ▼
  Cache Invalidation Service
        │
        ├── ลบ Redis key: price:{product_id}:*
        └── Purge CDN Surrogate Key: product-{product_id}

วิธีนี้ทำให้ Cache ไม่แสดงราคาเก่าเกินไปกว่าไม่กี่วินาทีหลังจากการเปลี่ยนแปลงถูก Commit — ไม่ว่า TTL จะตั้งไว้เท่าไรก็ตาม

ข้อมูลที่ควรส่งใน Event

Event อัปเดตราคา/สต็อกควรมีบริบทเพียงพอสำหรับการ Invalidate อย่างแม่นยำ:

{
  "event": "price_updated",
  "product_id": "SKU-12345",
  "affected_customer_groups": ["retail", "reseller"],
  "effective_at": "2026-03-08T10:00:00+07:00"
}

Event ที่แม่นยำ = การ Invalidate ที่แม่นยำ หลีกเลี่ยงการ "ล้าง Cache ทั้งหมด" ในช่วง Traffic สูง — มันทำให้เกิดปัญหา Thundering Herd ที่ทุก Cache Miss พุ่งเข้า Database พร้อมกัน


ขั้นตอนที่ 4: ป้องกัน Thundering Herd เมื่อ Cache Miss

เมื่อ Cache Key ยอดนิยมหมดอายุ Request หลายร้อยรายการอาจพุ่งเข้า Database พร้อมกัน นี่คือปัญหา Thundering Herd ที่สามารถล่มระบบได้ในช่วง Flash Sale

วิธีที่ 1: Probabilistic Early Expiry (TTL Jitter)

เพิ่ม Jitter สุ่มให้กับ TTL เพื่อให้ Cache ของสินค้าที่คล้ายกันไม่หมดอายุพร้อมกัน:

import random

BASE_TTL = 60  # วินาที
jitter = random.randint(0, 10)
redis.setex(cache_key, ttl=BASE_TTL + jitter, value=data)

วิธีที่ 2: Request Coalescing (Single-Flight)

ให้แน่ใจว่ามีเพียง Request เดียวที่คำนวณค่าใหม่เมื่อ Cache Miss ส่วนที่เหลือรอ:

# ใช้ Distributed Lock ป้องกันการคำนวณซ้ำพร้อมกัน
lock_key = f"lock:{cache_key}"

if redis.set(lock_key, "1", nx=True, ex=5):  # nx = ตั้งค่าเฉพาะเมื่อยังไม่มี Key นี้
    # Request นี้ได้ Lock — คำนวณและเติม Cache
    value = db.fetch(...)
    redis.setex(cache_key, ttl=60, value=value)
    redis.delete(lock_key)
else:
    # Request อื่นกำลังคำนวณอยู่ — รอสักครู่แล้ว Retry
    time.sleep(0.05)
    value = redis.get(cache_key)

วิธีที่ 3: Stale-While-Revalidate

ส่งค่า Cache เก่าทันทีในขณะที่คำนวณใหม่แบบ Asynchronous ในเบื้องหลัง เหมาะสำหรับรายละเอียดสินค้าและ Navigation — ไม่เหมาะกับราคาที่ต้องการความแม่นยำ


ขั้นตอนที่ 5: ออกแบบ Checkout Path ให้ใช้ข้อมูล Live เสมอ

ไม่ว่าหน้าสินค้าจะ Cache ได้ดีแค่ไหน ขั้นตอน Checkout ต้องใช้ข้อมูล Live สำหรับทุกการคำนวณราคาและสต็อก

สิ่งที่ต้องตรวจสอบ Live ที่ Checkout (ห้ามข้าม):

  1. คำนวณราคาตะกร้าใหม่ เมื่อเริ่ม Checkout — ดึงราคาปัจจุบัน คำนวณโปรโมชันใหม่ คำนวณ VAT 7% ใหม่
  2. จอง Stock ที่ Order Confirmation ไม่ใช่ตอน "เพิ่มลงตะกร้า" — ใช้ช่วงเวลาจอง (เช่น 15 นาที) และปล่อยคืนถ้าไม่ชำระเงิน
  3. ตรวจสอบโค้ดส่วนลด Live ณ จุดที่ใช้ — ตรวจวันหมดอายุ จำนวนที่ใช้ได้ และเงื่อนไขสิทธิ์แบบ Real-time
  4. คำนวณค่าจัดส่งใหม่ ที่ขั้นตอนสุดท้าย — อัตราค่าขนส่ง (Kerry, Flash, J&T, ไปรษณีย์ไทย) อาจเปลี่ยนได้ระหว่าง Session ที่ยาวนาน

Pattern สำหรับการจอง Stock:

ลูกค้ากด "สั่งซื้อ"
        │
        ▼
  พยายามจอง Stock (ลดจำนวนพร้อมตรวจสอบ Floor)
        │
  ┌─────┴──────┐
  │ สำเร็จ     │ ล้มเหลว (stock = 0)
  │            │
  ▼            ▼
ดำเนินการ    คืนค่า "สินค้าหมดแล้ว"
ชำระเงิน     ก่อนที่จะพยายามเรียกเก็บเงิน

ห้ามเรียกเก็บเงินลูกค้าสำหรับสินค้าที่ยังไม่ได้ยืนยันว่ามีอยู่จริง


ขั้นตอนที่ 6: ติดตาม Cache Health ใน Production

ปัญหา Cache คือ Silent Failure เพิ่ม Monitoring เพื่อตรวจพบก่อนลูกค้าเจอ

Metric ที่ต้องติดตาม:

Metric บอกอะไร เกณฑ์แจ้งเตือน
Cache Hit Rate ประสิทธิภาพ Cache โดยรวม < 80% ต้องตรวจสอบ
Stale Read Rate ความถี่ที่ข้อมูล TTL-Expired ถูกส่งออก ติดตาม Trend แจ้งเตือนเมื่อพุ่งขึ้น
Invalidation Lag เวลาระหว่างข้อมูลเปลี่ยนและ Cache ถูกล้าง > 30 วินาทีสำหรับราคาถือว่ามีปัญหา
Cache Eviction Rate Cache เล็กเกินไปหรือ Key ใหญ่เกินไป Eviction สูง = ขยายหรือตัดแต่ง
Price Discrepancy Events ราคาในตะกร้า ≠ ราคา Order จริง ทุกครั้งที่เกิดต้องตรวจสอบ

ตั้ง Canary Check: ทุก 60 วินาที ดึงราคาสินค้าที่รู้จักจากทั้ง Cache และ Database ถ้าต่างกันเกิน Tolerance ที่ยอมรับได้ ให้ส่ง Alert ทันที — เพื่อให้รู้ก่อนลูกค้าเจอปัญหา


ขั้นตอนที่ 7: จัดการ Flash Sale และ Peak Event แบบพิเศษ

Flash Sale ทำลายสมมติฐาน Caching ทั่วไป ราคาเปลี่ยนตามเวลา สต็อกหมดในไม่กี่วินาที และ Traffic พุ่งขึ้น 10–100 เท่า

สำหรับตลาดไทยโดยเฉพาะ: แคมเปญ 11.11, 12.12, Shopee Payday, Lazada Birthday Sale และ Flash Sale บน LINE Shopping เกิดขึ้นบ่อยและมีผู้ใช้งานพร้อมกันจำนวนมาก ต้องเตรียมระบบให้พร้อมล่วงหน้าเสมอ

Pre-warm Cache ก่อน Event เริ่ม — เติมข้อมูลราคาและสินค้าใน Redis ไม่กี่นาทีก่อน Go-live เพื่อให้ Traffic ระลอกแรกไม่โดน Cold Cache

ใช้ Dedicated Inventory Service สำหรับการอัปเดต Stock ที่มีปริมาณสูง Redis Counter พร้อม Atomic Decrement (DECR) ทำงานได้เร็วกว่า Database Row Lock มากภายใต้ภาระงานพร้อมกัน:

# Atomic Stock Decrement — คืนค่าสต็อกที่เหลือหลังจากลด
remaining = redis.decr(f"stock:{product_id}")

if remaining < 0:
    # Oversold — ยกเลิกและปฏิเสธ
    redis.incr(f"stock:{product_id}")
    raise OutOfStockError()

Degrade อย่างสง่างามภายใต้ภาระงาน ถ้า Cache Layer ล่ม Fallback ควรเป็นการตอบสนองแบบเรียบง่าย ("กรุณาตรวจสอบสินค้าที่ Checkout") แทนที่จะให้ Database รับ Request ที่ไม่มี Cache ทั้งหมด


Checklist ก่อน Production

  • [ ] สร้างตารางจำแนกข้อมูลครบ — ทุกประเภทมี TTL และกลยุทธ์ Invalidation แล้ว
  • [ ] สถาปัตยกรรม Cache แบบสามชั้นพร้อม (CDN + Application Cache + Origin)
  • [ ] ใช้ Namespaced Redis Keys สำหรับ Targeted Invalidation
  • [ ] Event-Driven Invalidation สำหรับการเปลี่ยนราคาและสต็อก
  • [ ] ใช้ TTL Jitter ป้องกัน Thundering Herd
  • [ ] ยืนยันว่า Checkout Path ใช้ข้อมูล Live เท่านั้น
  • [ ] ใช้ Stock Reservation Logic ที่ Order Confirmation
  • [ ] ตรวจสอบโค้ดส่วนลด Live เสมอ
  • [ ] ติดตาม Cache Hit Rate, Invalidation Lag และ Price Discrepancy แล้ว
  • [ ] เขียนและทดสอบ Runbook สำหรับ Flash Sale แล้ว

สรุป

Ecommerce Caching ที่ทั้งเร็วและแม่นยำไม่ได้อยู่ที่การเลือกอย่างใดอย่างหนึ่ง — แต่คือการใช้กลยุทธ์ที่ถูกต้องกับข้อมูลที่ถูกต้อง Cache Static Content อย่างก้าวร้าว ใช้ TTL สั้นและ Event-Driven Invalidation สำหรับราคาและสต็อก ห้าม Cache ยอดรวมตะกร้าหรือความถูกต้องของโค้ดส่วนลด และสร้าง Checkout Path ที่ถือว่าข้อมูล Live เป็นสิ่งที่ต้องใช้เสมอ

ทีมที่จัดการเรื่องนี้ได้ถูกต้องไม่ได้แค่มีร้านค้าที่เร็วกว่า — พวกเขามีร้านค้าที่ลูกค้าไว้วางใจ


ต้องการให้ผู้เชี่ยวชาญรีวิว Ecommerce Caching Architecture ของคุณไหม? จองคำปรึกษาฟรีกับ Simplico


Get in Touch with us

Chat with Us on LINE

iiitum1984

Speak to Us or Whatsapp

(+66) 83001 0222

Related Posts

Our Products