ข้าม​ไป​ยัง​เนื้อหา

ไม่มี code บรรทัด​ไหน​ของ​เรา​สร้าง​กุญแจ แล้ว​มัน​มี​ใบ​เดียว​ทั้ง​แอป

บท​ที่ 1 จบ​ด้วย​คำถาม​ที่​ยัง​ไม่มี​คำ​ตอบ ผู้​เช่า​สอง​ราย​ยิง​คนละ​คำขอ แล้ว​ทำไม​ทั้ง​คู่​ถึง​ตกลง​ช่อง​เดียวกัน​ใน cache

บท​นี้​เปิด​กล่อง​แล้ว​อ่าน​กุญแจ​ที่​อยู่​ข้าง​ใน ก่อน​จะ​ไป​แตะ code สัก​บรรทัด

ใน​ทรง cached cache ชื่อ visits มี​กุญแจ​ใบ​เดียว และ​มัน​เป็น​ใบ​เดิม​ทั้ง​ก่อน​และ​หลัง​คำขอ​ของ​ผู้​เช่า​ราย​ที่​สอง

probe อ่าน​ราย​ชื่อ​กุญแจ​สอง​ครั้ง ครั้ง​แรก​หลัง​คำขอ​ของ clinic-a ครั้ง​ที่​สอง​หลัง​คำขอ​ของ clinic-b ผล​ที่​ได้​เหมือน​กัน​ทั้ง​สอง​ครั้ง

ช่วง​ที่​อ่านกุญแจ​ใน cache ชื่อ visits
หลัง​คำขอ​ของ clinic-a[SimpleKey []]
หลัง​คำขอ​ของ clinic-b[SimpleKey []]

คำขอ​ของ clinic-b ไม่​ได้​สร้าง​กุญแจ​ใบ​ใหม่ มัน​ไป​เจอ​ใบ​เดิม​ที่​คำขอ​แรก​วาง​ไว้ แล้ว​รับ​ค่า​ใน​นั้น​กลับ​ไป นี่​คือ​กุญแจ​ใบ​เดียว​ทั้ง​แอป ไม่ใช่​ใบ​เดียว​ต่อ​ผู้​เช่า

ใคร​เป็น​คน​ตัดสิน​ว่า​สอง​คำขอ​นี้​ถาม​เรื่อง​เดียวกัน

หัวข้อ​ที่​มีชื่อ​ว่า “ใคร​เป็น​คน​ตัดสิน​ว่า​สอง​คำขอ​นี้​ถาม​เรื่อง​เดียวกัน”

ค่า​เริ่ม​ต้น​ของ Spring สร้าง​กุญแจ​จาก​รายการ argument ของ method และ method ของ​เรา​ไม่มี argument สัก​ตัว

กุญแจ​ใบ​นั้น​เรียก​ว่า cache keycache keyค่าที่ cache ใช้​แยก​คำ​ตอบ​หนึ่ง​ออก​จาก​อีก​คำ​ตอบ · กุญแจ​ที่​ไม่​นับ​ผู้​เช่า ทำให้​คำขอ​ของ​สอง​ผู้​เช่า​ตกลง​ช่อง​เดียวกัน และ​คำ​ตอบ​ของ​ราย​แรก​กลาย​เป็น​คำ​ตอบ​ของ​ราย​ที่​สอง และ​ไม่มี code บรรทัด​ไหน​ที่​เรา​เขียน​สร้าง​มัน​ขึ้น​มา คน​สร้าง​คือ SimpleKeyGeneratorSimpleKeyGeneratorตัว​สร้าง​กุญแจ​ที่ Spring ใช้​เมื่อ​ผู้​เขียน​ไม่​ได้​กำหนด​กุญแจ​เอง · มัน​อ่าน​เฉพาะ argument ของ method และ​ไม่รู้​จัก​สิ่ง​ที่​อยู่​นอก​รายการ​นั้น​เลย ซึ่ง​เป็น​ตัว​สร้าง​กุญแจ​ที่​ทำงาน​ให้​เมื่อ​เรา​ไม่​ได้​ระบุ​กุญแจ​เอง

เอกสาร​ของ Spring Framework บอก​กฎ​ของ​มัน​ไว้​ตรงๆ — “If no parameters are given, return SimpleKey.EMPTY.”

visits() ไม่​รับ parameter สัก​ตัว เพราะ​ผู้​เช่า​เข้า​มา​ทาง SecurityContext ไม่​ได้​เข้า​มา​ทาง รายการ argument กฎ​ข้าง​บน​จึง​คืน SimpleKeySimpleKeyกุญแจ​ที่ SimpleKeyGenerator สร้าง​ให้ method ที่​ไม่มี argument · วัด​แล้ว​ว่า​ทั้ง cache มี​ใบ​เดียว​คือ `[SimpleKey []]` ขณะ​ที่​สาม​คำขอ​ทำให้​ฐาน​ข้อมูล​ถูก​ถาม​ครั้ง​เดียว ค่า​ว่าง​ให้​ทุก​คำขอ และ​ค่า​ว่าง​ของ​สอง​ผู้​เช่า​ก็​เท่า​กัน

❌ version ดิบ · ทรง cached · กุญแจ​ไม่มี​อะไร​ให้​ต่าง​กัน
@Service
class VisitService(private val repo: VisitRepo, private val tenant: CurrentTenant) {
@Cacheable("visits")
fun visits(): List<Visit> = repo.findByTenantId(tenant.id)
}

บรรทัด @Cacheable("visits") ข้าง​บน​คือ​รูป​ที่​รั่ว​ใน​บท​ที่ 1 มัน​ไม่​ได้​เขียน​ผิด​ไวยากรณ์ และ​ไม่มี​อะไร​ใน​นั้น​บอก​ว่า​กุญแจ​จะ​ซ้ำ

เมื่อ​กุญแจ​มา​จาก​รายการ argument รูป​ที่​ยัด​ผู้​เช่า​เข้า​มา​เป็น parameter จะ​ได้​กุญแจ​คนละ​ใบ ส่วน​รูป​ที่​ซ่อน​ผู้​เช่า​ไว้​ใน context ได้​ใบ​เดียว

ลอง​นึกถึง​รูป​ที่​รับ tenantId เข้า​มา​เป็น parameter ตรงๆ

รูป​เทียบ · เขียน​ขึ้น​เพื่อ​อธิบาย ไม่​ได้​วัด​ใน probe
@Cacheable("visits")
fun visits(tenantId: String): List<Visit> = repo.findByTenantId(tenantId)

รูป​นี้​ผู้​เช่า​อยู่​ใน​รายการ argument กุญแจ​ของ clinic-a กับ​ของ clinic-b จึง​ต่าง​กัน​โดยที่​ไม่มี​ใคร ตั้งใจ​ให้​ต่าง มัน​รอด​เพราะ​บังเอิญ ไม่ใช่​เพราะ​มี​ใคร​ออกแบบ​ไว้

ส่วน​รูป​ที่​ย้าย​ผู้​เช่า​เข้าไป​อยู่​ใน SecurityContext เป็น​รูป​ที่​สะอาด​กว่า ผู้​เรียก​ไม่​ต้อง​ส่ง​ผู้​เช่า​มา​เอง และ​ไม่มี​ทาง​ส่ง​ผิด​ราย แต่​ค่าที่​ทำให้​กุญแจ​ต่าง​กัน​ก็​หาย​ไป​พร้อม​กัน

ราคา​ของ​รูป​ที่​สะอาด​กว่า​อยู่​ตรง​นี้ ยิ่ง​ซ่อน context ไว้​ลึก ตัว​สร้าง​กุญแจ​ก็​ยิ่ง​ไม่​เห็น​ค่าที่​เป็น​ตัว​กำหนด​คำ​ตอบ

@Cacheable มี attribute ชื่อ key ที่​รับ SpEL ทาง​แก้​คือ​บอก​กุญแจ​ไป​ตรงๆ ว่า​ให้​ใช้​ผู้​เช่า​ราย​ไหน

เอกสาร​เดียวกัน​ระบุ​ว่า attribute key รับ SpELSpring Expression Languageภาษา expression ที่​เขียน​ค่า​ของ​กุญแจ​ได้ · เป็น​ที่​ที่​ผู้​เขียน​ใส่​ผู้​เช่า​เข้าไป​ใน​กุญแจ​เอง ผล​ที่​วัด​ได้​คือ​กุญแจ​สอง​ใบ `[clinic-a, clinic-b]` และ​ฐาน​ข้อมูล​ถูก​ถาม​สอง​ครั้ง ซึ่ง​เป็น​ภาษา expression ของ Spring เรา​จึง​ชี้​ไป​ที่ bean ชื่อ currentTenant แล้ว​อ่าน​ค่า id ของ​มัน​มา​เป็น​กุญแจ

ทรง cached-keyed · ยก​มา​ตรง​ตัว​จาก probe
@Service
class VisitService(private val repo: VisitRepo, private val tenant: CurrentTenant) {
@Cacheable("visits", key = "@currentTenant.id")
fun visits(): List<Visit> = repo.findByTenantId(tenant.id)
}

@currentTenant ใน expression คือ​ชื่อ bean ที่​ประกาศ​ไว้​ตั้งแต่​ต้น​คอร์ส​ด้วย @Component("currentTenant") ตัว​งาน​ข้าง​ใน visits() ไม่​ขยับ​สัก​ตัว​อักษร สิ่ง​ที่​เพิ่ม​มา​มี​แค่ attribute เดียว

พอ​อ่าน​กุญแจ​ใหม่​หลัง​คำขอ​ของ clinic-b ราย​ชื่อ​กลาย​เป็น [clinic-a, clinic-b] สอง​ใบ​แยก​กัน clinic-b ได้ วีระ หนึ่ง​แถว​ของ​ตัว​เอง​กลับ​ไป

ทรง​เดิม​มี​ช่อง​เดียว​ให้​ทั้ง​สอง​ผู้​เช่า ทรง​ที่​ใส่​ผู้​เช่า​ลง​ใน​กุญแจ​มี​ช่อง​ละ​ราย

flowchart TB
  subgraph S1["ทรง cached · กุญแจใบเดียว"]
    A1["คำขอของ clinic-a"] --> K1["กุญแจ SimpleKey ค่าว่าง"]
    B1["คำขอของ clinic-b"] --> K1
    K1 --> V1["พิมพ์ใจ, สมชาย"]
  end
  subgraph S2["ทรง cached-keyed · กุญแจสองใบ"]
    A2["คำขอของ clinic-a"] --> K2["กุญแจ clinic-a"]
    B2["คำขอของ clinic-b"] --> K3["กุญแจ clinic-b"]
    K2 --> V2["พิมพ์ใจ, สมชาย"]
    K3 --> V3["วีระ"]
  end

คำ​บรรยาย​ภาพ: ด้าน​บน​คือ cache ที่​มี​ช่อง​เดียว คำขอ​ของ​สอง​ผู้​เช่า​จึง​ชี้​ไป​ที่​ค่า​เดียวกัน ด้าน​ล่าง​คือ cache ที่​กุญแจ​มีชื่อ​ผู้​เช่า​อยู่​ข้าง​ใน คำขอ​สอง​ใบ​จึง​แยก​ไป​คนละ​ช่อง

กุญแจ​ที่​แยก​ตาม​ผู้​เช่า​ทำให้​ฐาน​ข้อมูล​ถูก​ถาม​เพิ่ม​จาก 1 ครั้ง​เป็น 2 ครั้ง แต่​คำขอ​ที่​สาม​ยัง​ไม่​ลง​ไป​ถึง​ฐาน

การ​วัด​ทุก​ทรง​ยิง​สาม​คำขอ ตาม​ลำดับ clinic-a แล้ว clinic-b แล้ว clinic-a อีก​ครั้ง

ช่อง​ที่​วัดทรง scopedทรง cachedทรง cached-keyed
clinic-a เห็นพิมพ์​ใจ, สมชายพิมพ์​ใจ, สมชายพิมพ์​ใจ, สมชาย
clinic-b เห็นวีระพิมพ์​ใจ, สมชายวีระ
กุญแจ​ใน cacheไม่มี bean ของ CacheManager[SimpleKey []][clinic-a, clinic-b]
ฐาน​ข้อมูล​ถูก​ถาม3 ครั้ง1 ครั้ง2 ครั้ง

ตัวเลข 1 ใน​ทรง cached ไม่ใช่​กำไร มัน​คือ​คำ​ตอบ​ของ​ผู้​เช่า​ราย​หนึ่ง​ที่​ถูก​ส่ง​ต่อ​ให้​อีก​ราย เลข 2 คือ​ราคา​จริง​ของ​คำ​ตอบ​ที่​ถูก​ต้อง คือ​หนึ่ง​ครั้ง​ต่อ​ผู้​เช่า​หนึ่ง​ราย

คำขอ​ที่​สาม​ซึ่ง​เป็น​ของ clinic-a อีก​ครั้ง ยัง​ได้​ค่า​จาก cache เหมือน​เดิม กุญแจ​ที่​แยก​ตาม​ผู้​เช่า​ไม่​ได้​ทำให้​ผู้​เช่า​ราย​เดิม​เสีย​ประโยชน์​ที่​เคย​ได้

กุญแจ​ถูก​ต้อง​แล้ว แต่​ยัง​ไม่มี​อะไร​รับ​ประกัน​ว่า​คำ​เรียก​ทุก​ใบ​จะ​เดิน​มา​ถึง​ชั้น​ที่​ดู​กุญแจ

สิ่ง​ที่​บท​นี้​เพิ่ม​เข้าไป​คือ​กฎ​ข้อ​เดียว คือ​ถ้า​คำ​ตอบ​ขึ้น​อยู่​กับ​ผู้​เช่า กุญแจ​ต้อง​มี​ผู้​เช่า​อยู่​ใน​นั้น กฎ​ข้อ​นี้​กลับ​มา​อีก​ครั้ง​เป็น​ข้อ​แรก​ของ​สัญญา​ใน บท​ที่ 8

แต่​กุญแจ​ที่​ถูก​ต้อง​มี​เงื่อนไข​ที่​ยัง​ไม่​ได้​ตรวจ คือ​คำ​เรียก​ต้อง​เดิน​ผ่าน​ชั้น​ที่​อ่าน​กุญแจ​จริงๆ บท​ที่ 4 วัด​คำ​เรียก​ที่​ไม่​เคย​ไป​ถึง​ชั้น​นั้น และ​อาการ​เดียว​ที่​มัน​แสดงออก​คือ​ตัวเลข​ที่​ควร​ลด กลับ​ไม่​ลด


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Spring Framework — Annotation-based Caching (เข้า​ถึง 2026-09-04) — ที่มา​ของ​กฎ “If no parameters are given, return SimpleKey.EMPTY.” และ​ของ attribute key ที่​รับ SpEL

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก scripts/clinic-cache/probe.py ซึ่ง​สร้าง​โปร​เจ​กต์​ขึ้น​ใหม่​ที​ละ​ทรง แล้ว​อ่าน​ราย​ชื่อ​กุญแจ​กับ​จำนวน​ครั้ง​ที่​ฐาน​ข้อมูล​ถูก​ถาม · ต้อง​สั่ง scripts/clinic-cache/up.sh ก่อน ซึ่ง​ยก container ของ​คอร์ส​นี้​เอง​ขึ้น​ที่ port 55436

เช็กความเข้าใจ — บทที่ 3

ข้อ 1 / 3

ในทรง `cached` cache ชื่อ visits ถือกุญแจอะไรไว้ หลังคำขอของ clinic-a และหลังคำขอของ clinic-b