ไม่มี code บรรทัดไหนของเราสร้างกุญแจ แล้วมันมีใบเดียวทั้งแอป
บทที่ 1 จบด้วยคำถามที่ยังไม่มีคำตอบ ผู้เช่าสองรายยิงคนละคำขอ แล้วทำไมทั้งคู่ถึงตกลงช่องเดียวกันใน cache
บทนี้เปิดกล่องแล้วอ่านกุญแจที่อยู่ข้างใน ก่อนจะไปแตะ code สักบรรทัด
เปิดกล่องแล้วนับกุญแจ
หัวข้อที่มีชื่อว่า “เปิดกล่องแล้วนับกุญแจ”ในทรง
cachedcache ชื่อ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 []]` ขณะที่สามคำขอทำให้ฐานข้อมูลถูกถามครั้งเดียว ค่าว่างให้ทุกคำขอ และค่าว่างของสองผู้เช่าก็เท่ากัน
@Serviceclass 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 ตรงๆ
@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 ของมันมาเป็นกุญแจ
@Serviceclass 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.” และของ attributekeyที่รับ SpEL -
ตัวเลขทุกตัวในบทนี้มาจาก
scripts/clinic-cache/probe.pyซึ่งสร้างโปรเจกต์ขึ้นใหม่ทีละทรง แล้วอ่านรายชื่อกุญแจกับจำนวนครั้งที่ฐานข้อมูลถูกถาม · ต้องสั่งscripts/clinic-cache/up.shก่อน ซึ่งยก container ของคอร์สนี้เองขึ้นที่ port 55436
เช็กความเข้าใจ — บทที่ 3
ข้อ 1 / 3ในทรง `cached` cache ชื่อ visits ถือกุญแจอะไรไว้ หลังคำขอของ clinic-a และหลังคำขอของ clinic-b