สัญญาสี่ข้อของชั้นที่จำคำตอบ และทรงที่ผ่านครบทุกช่อง
เจ็ดบทก่อนหน้าวัดทรงของชั้นที่จำคำตอบมาทีละทรง บทนี้ไม่วัดอะไรใหม่สักช่อง
สิ่งที่บทนี้ทำคือเอาผลที่วัดไว้แล้วมาเขียนเป็นสัญญาสี่ข้อ แต่ละข้อมีบทที่วัดมันไว้อยู่ข้างหลัง
สัญญาสี่ข้อของชั้นที่จำคำตอบ
หัวข้อที่มีชื่อว่า “สัญญาสี่ข้อของชั้นที่จำคำตอบ”สี่ข้อนี้มาจากบทที่ 3 บทที่ 5 บทที่ 6 และบทที่ 7 ตามลำดับ ไม่มีข้อไหนเป็นของใหม่
- กุญแจต้องมีผู้เรียกอยู่ในนั้น
- สิ่งที่เก็บต้องเป็นค่า ไม่ใช่ entity ที่คนอื่นแก้ได้
- การเติมกลับต้องเกิดหลัง commit
- ชุดเทสต์ต้องรันในทรงที่ deploy จริง
ข้อ 1 · กุญแจต้องมีผู้เรียกอยู่ในนั้น
หัวข้อที่มีชื่อว่า “ข้อ 1 · กุญแจต้องมีผู้เรียกอยู่ในนั้น”🔁 บทที่ 3 เปิดกล่องแล้วเจอว่ากุญแจในทรง
cached คือ [SimpleKey []] ใบเดียวทั้งแอป ทั้งหลังคำขอของ clinic-a และหลังคำขอของ
clinic-b
visits() ไม่รับ parameter สักตัว เพราะผู้เช่าเดินทางมาใน
SecurityContext ไม่ใช่ในรายการ parameter ตัวสร้างกุญแจตัวเริ่มต้น
จึงไม่มีอะไรให้แยกคำขอสองใบนี้ออกจากกัน
กฎที่เขียนได้จากตรงนั้นมีข้อเดียว ถ้าคำตอบขึ้นกับผู้เช่า กุญแจต้องมีผู้เช่า
และกุญแจไม่ได้มาเอง ต้องระบุมันด้วย key
ราคาคือฐานข้อมูลถูกถาม 2 ครั้งแทน 1 ครั้ง ผู้เช่าสองรายไม่แบ่งช่องกันแล้ว จึงไม่มีทางที่คำขอของรายหนึ่งจะประหยัดการถามฐานให้อีกราย
ข้อ 2 · สิ่งที่เก็บต้องเป็นค่า ไม่ใช่ entity ที่คนอื่นแก้ได้
หัวข้อที่มีชื่อว่า “ข้อ 2 · สิ่งที่เก็บต้องเป็นค่า ไม่ใช่ entity ที่คนอื่นแก้ได้”🔁 บทที่ 5 ให้ผู้เรียกแก้ชื่อบน
อ็อบเจ็กต์ที่ได้รับมา โดยไม่เขียนลงฐานเลยสักบรรทัด ผู้เรียกคนถัดไปได้ ["REDACTED", "สมชาย"]
ฐานข้อมูลไม่ถูกถามเพิ่มเลย 3 ครั้งก่อนแก้ และ 3 ครั้งหลังแก้ ส่วนในฐานข้อมูลยังเป็น
พิมพ์ใจ เหมือนเดิม cache กับฐานข้อมูลเล่าคนละเรื่องแล้ว
ข้อนี้วัดบน ConcurrentMapCacheManager ซึ่งเป็นตัวเริ่มต้น มันเก็บอ็อบเจ็กต์ตัวเดียวกับที่
ยื่นออกไป cache ที่ serialize ค่าจะได้สำเนา ขอบเขตของข้อนี้จึงอยู่ที่ตัวเริ่มต้น ไม่ใช่ทุกตัว
ราคาคือชั้น service เลิกคืน entity ตรงๆ การคืน entity แปลว่ายื่นสิทธิ์แก้ของกลางให้ผู้เรียก ทางที่ปิดรูนี้คือคืนค่าที่แก้ไม่ได้ ซึ่งต้องมีรูปสำหรับคืนเพิ่มขึ้นมา
ข้อ 3 · การเติมกลับต้องเกิดหลัง commit
หัวข้อที่มีชื่อว่า “ข้อ 3 · การเติมกลับต้องเกิดหลัง commit”🔁 บทที่ 6 วัดรูปที่ code
จริงเขียนกัน คือเขียนแล้วล้าง cache แล้วมีคนอ่านต่อ ทั้งหมดอยู่ข้างใน @Transactional
ตัวเดียวกัน
คำขอนั้นคืน 500 และ transaction ถูก rollback แต่การยิง /visits ต่อได้ 3 แถว
รวมแถวชื่อ ผีไม่มี transaction ส่วนการถามฐานข้อมูลตรงๆ ได้ 2 แถว
การอ่านที่เกิดข้างใน transaction ที่ยังไม่ commit เขียนผลลงช่องที่คนข้างนอกอ่านได้
ConcurrentMapCacheManager ไม่รู้จัก transaction จึงไม่มีอะไรถอนค่านั้นออกตอน rollback
beforeInvocation ช่วยข้อนี้ไม่ได้ ปัญหาไม่ได้อยู่ที่ล้างก่อนหรือล้างหลัง
มันอยู่ที่การเติมกลับเกิดขึ้นก่อน commit ราคาของข้อนี้คือการเติมกลับต้องผูกกับ commit
ไม่ใช่ผูกกับการที่ method เดินจบ
ข้อ 4 · ชุดเทสต์ต้องรันในทรงที่ deploy จริง
หัวข้อที่มีชื่อว่า “ข้อ 4 · ชุดเทสต์ต้องรันในทรงที่ deploy จริง”🔁 บทที่ 7 เล่าเรื่องนี้จนจบ
สิ่งที่วัดได้คือทรง cached ทำให้ IsolationTest ของคอร์สก่อนหน้า แดง จริง
การเติม spring.cache.type=none ลงใน properties ของเทสต์ ทำให้เทสต์กลับมาเขียว
โดย code ของแอปไม่ขยับสักตัวอักษร cacheManager ในชุดเทสต์กลายเป็น NoOpCacheManager
และฐานข้อมูลถูกถาม 3 ครั้ง
ของที่ขึ้น production ยังรั่วเหมือนเดิม เพราะทรงที่เทสต์รันไม่ใช่ทรงที่ deploy ราคาของข้อนี้คือชุดเทสต์ต้องยิงสองผู้เช่าโดยเปิด cache ไว้ตามจริง ซึ่งแดงตอนแรกแน่นอน
ทรงที่ผ่านครบทุกช่อง
หัวข้อที่มีชื่อว่า “ทรงที่ผ่านครบทุกช่อง”ทรง
cached-keyedคือทรงเดียวในคอร์สที่ทั้งลดการถามฐานข้อมูล และคืนแถวของผู้เช่าถูกราย
@Serviceclass VisitService(private val repo: VisitRepo, private val tenant: CurrentTenant) { @Cacheable("visits", key = "@currentTenant.id") fun visits(): List<Visit> = repo.findByTenantId(tenant.id)}ยิงสามคำขอชุดเดิม ตามลำดับ clinic-a แล้ว clinic-b แล้ว clinic-a อีกครั้ง
| ช่องที่วัด | ทรง scoped | ทรง cached | ทรง cached-keyed |
|---|---|---|---|
clinic-a เห็น | พิมพ์ใจ, สมชาย | พิมพ์ใจ, สมชาย | พิมพ์ใจ, สมชาย |
clinic-b เห็น | วีระ | พิมพ์ใจ, สมชาย | วีระ |
| กุญแจ | ไม่มี bean ของ CacheManager | [SimpleKey []] | [clinic-a, clinic-b] |
| ฐานข้อมูลถูกถาม | 3 ครั้ง | 1 ครั้ง | 2 ครั้ง |
IsolationTest | เขียว | แดง | เขียว |
flowchart TB
A["คำขอที่ 1 · ผู้เรียกของ clinic-a"] --> KA{"กุญแจ clinic-a อยู่ใน cache แล้วหรือยัง"}
C["คำขอที่ 3 · ผู้เรียกของ clinic-a อีกครั้ง"] --> KA
B["คำขอที่ 2 · ผู้เรียกของ clinic-b"] --> KB{"กุญแจ clinic-b อยู่ใน cache แล้วหรือยัง"}
KA -->|"ยังไม่มี · คำขอที่ 1"| D1[("ฐานข้อมูล · ถูกถามครั้งที่ 1")]
KB -->|"ยังไม่มี · คำขอที่ 2"| D2[("ฐานข้อมูล · ถูกถามครั้งที่ 2")]
D1 --> RA["พิมพ์ใจ, สมชาย"]
D2 --> RB["วีระ"]
KA -->|"มีแล้ว · คำขอที่ 3"| RA
คำบรรยายภาพ: กุญแจสองใบแยกคำขอของสองผู้เช่าออกจากกัน ฐานข้อมูลจึงถูกถาม 2 ครั้งจากสามคำขอ และคำขอที่สามยังได้ค่าจาก cache
ตัวเลขที่ลดลงยังอยู่ 2 ครั้งน้อยกว่า 3 ครั้งของทรง scoped สิ่งที่หายไปคือแถวของผู้เช่ารายหนึ่ง
ที่เคยเดินไปหาอีกราย
สิ่งที่คอร์สนี้ไม่ได้ตอบ
หัวข้อที่มีชื่อว่า “สิ่งที่คอร์สนี้ไม่ได้ตอบ”สามข้อนี้อยู่นอกขอบเขตที่ประกาศไว้ตั้งแต่บทแรก และเหตุผลของทั้งสามข้อเป็นเหตุผลเดียวกัน
- cache ที่อยู่นอก process ค่าเดินข้ามเครือข่ายไปอยู่ที่อื่น จังหวะที่มันไปถึงกับจังหวะที่ ผู้เรียกอ่าน จึงแยกกันได้ และการวัดส่วนต่างนั้นต้องใช้หน่วยเวลาหรือ race
- การหมดอายุ ต้องบอกได้ว่าค่าอยู่ได้นานเท่าไร ซึ่งเป็นหน่วยเวลา
- ผู้เรียกหลายรายพร้อมกัน ต้องวัด race ระหว่างผู้เรียก ไม่ใช่วัดลำดับที่กำหนดเอง
หน่วยของคอร์สนี้มีสี่อย่าง คือจำนวนแถว จำนวนครั้งที่ถามฐานข้อมูล ชื่อกุญแจ และรหัส HTTP ทั้งสามข้อข้างบนต้องการหน่วยเวลาหรือ race ซึ่งไม่มีอยู่ในสี่อย่างนั้น
การประกาศขอบเขตแบบนี้มีราคา คุณปิดคอร์สโดยยังไม่รู้ว่าค่าที่หมดอายุแล้วมีหน้าตาอย่างไร สิ่งที่ได้แลกมาคือทุกข้ออ้างในคอร์สนี้ยิงซ้ำแล้วได้ผลเดิม
เดินต่อจากที่นี่
หัวข้อที่มีชื่อว่า “เดินต่อจากที่นี่”ซีรีส์คลินิกมีสี่คอร์ส และสัญญาของแต่ละคอร์สอ่านต่อกันเป็นเส้นเดียว
- Kotlin + Spring Boot + Clean Architecture วางกฎทิศทาง ที่ compiler บังคับให้
- ขอบ HTTP ของคลินิก ทำให้ผู้เรียกปลายสายอ่านคำตอบออก
- คัดกรองผู้เรียกของคลินิก ตอบว่าใครเรียกได้ และเห็นข้อมูลของใคร
คอร์สนี้ต่อจากคอร์สที่สาม และถามข้อเดียว คือกฎที่คอร์สนั้นวางไว้ยังยืนอยู่ไหม เมื่อมีชั้นที่จำคำตอบมาคั่นกลาง คำตอบที่วัดได้คือยืนอยู่ ก็ต่อเมื่อกุญแจรู้จักผู้ถาม
-
Spring Framework — Annotation-based Caching (เข้าถึง 2026-09-04) — ที่มาของ
keyที่รับ SpEL และของbeforeInvocation -
Spring Boot — Caching (เข้าถึง 2026-09-04) — ที่มาของ
spring.cache.type=noneและของตัวเริ่มต้นที่ใช้ConcurrentHashMap -
Spring Framework — Proxying Mechanisms (เข้าถึง 2026-09-04) — ที่มาของกลไก self invocation ที่บทที่ 4 วัด
-
ตัวเลขทุกตัวในบทนี้มาจาก
scripts/clinic-cache/probe.pyซึ่งสร้างโปรเจกต์ขึ้นใหม่ทีละทรง แล้วเทียบผลทีละช่อง · ต้องสั่งscripts/clinic-cache/up.shก่อน ซึ่งยก container ของคอร์สนี้เองขึ้นที่ port 55436
เช็กความเข้าใจ — บทที่ 8
ข้อ 1 / 3ในทรง `cached-keyed` ที่ยิงสามคำขอตามลำดับ clinic-a แล้ว clinic-b แล้ว clinic-a อีกครั้ง ฐานข้อมูลถูกถามกี่ครั้ง และกุญแจในนั้นมีอะไรบ้าง