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

สัญญา​สี่​ข้อ​ของ​ชั้น​ที่​จำ​คำ​ตอบ และ​ทรง​ที่​ผ่าน​ครบ​ทุก​ช่อง

เจ็ด​บท​ก่อน​หน้า​วัด​ทรง​ของ​ชั้น​ที่​จำ​คำ​ตอบ​มา​ที​ละ​ทรง บท​นี้​ไม่​วัด​อะไร​ใหม่​สัก​ช่อง

สิ่ง​ที่​บท​นี้​ทำ​คือ​เอา​ผล​ที่​วัด​ไว้​แล้ว​มา​เขียน​เป็น​สัญญา​สี่​ข้อ แต่ละ​ข้อ​มี​บท​ที่​วัด​มัน​ไว้​อยู่​ข้าง​หลัง

สี่​ข้อ​นี้​มา​จาก​บท​ที่ 3 บท​ที่ 5 บท​ที่ 6 และ​บท​ที่ 7 ตาม​ลำดับ ไม่มี​ข้อ​ไหน​เป็น​ของ​ใหม่

  1. กุญแจ​ต้อง​มี​ผู้​เรียก​อยู่​ใน​นั้น
  2. สิ่ง​ที่​เก็บ​ต้อง​เป็น​ค่า ไม่ใช่ entity ที่​คน​อื่น​แก้​ได้
  3. การ​เติม​กลับ​ต้อง​เกิด​หลัง commit
  4. ชุด​เทสต์​ต้อง​รัน​ใน​ทรง​ที่ deploy จริง

🔁 บท​ที่ 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 แปล​ว่า​ยื่น​สิทธิ์​แก้​ของ​กลาง​ให้​ผู้​เรียก ทาง​ที่​ปิด​รู​นี้​คือ​คืน​ค่าที่​แก้​ไม่​ได้ ซึ่ง​ต้อง​มี​รูป​สำหรับ​คืน​เพิ่ม​ขึ้น​มา

🔁 บท​ที่ 6 วัด​รูป​ที่ code จริง​เขียน​กัน คือ​เขียน​แล้ว​ล้าง cache แล้ว​มี​คน​อ่าน​ต่อ ทั้งหมด​อยู่​ข้าง​ใน @Transactional ตัว​เดียวกัน

คำขอ​นั้น​คืน 500 และ transaction ถูก rollback แต่​การ​ยิง /visits ต่อ​ได้ 3 แถว รวม​แถว​ชื่อ ผีไม่มี transaction ส่วน​การ​ถาม​ฐาน​ข้อ​มูล​ตรงๆ ได้ 2 แถว

การ​อ่าน​ที่​เกิด​ข้าง​ใน transaction ที่​ยัง​ไม่ commit เขียน​ผล​ลง​ช่อง​ที่​คน​ข้าง​นอก​อ่าน​ได้ ConcurrentMapCacheManager ไม่รู้​จัก transaction จึง​ไม่มี​อะไร​ถอน​ค่า​นั้น​ออก​ตอน rollback

beforeInvocation ช่วย​ข้อ​นี้​ไม่​ได้ ปัญหา​ไม่​ได้​อยู่​ที่​ล้าง​ก่อน​หรือ​ล้าง​หลัง มัน​อยู่​ที่​การ​เติม​กลับ​เกิด​ขึ้น​ก่อน commit ราคา​ของ​ข้อ​นี้​คือ​การ​เติม​กลับ​ต้อง​ผูก​กับ commit ไม่ใช่​ผูก​กับ​การ​ที่ method เดิน​จบ

🔁 บท​ที่ 7 เล่า​เรื่อง​นี้​จน​จบ สิ่ง​ที่​วัด​ได้​คือ​ทรง cached ทำให้ IsolationTest ของ​คอร์ส​ก่อน​หน้า แดง จริง

การ​เติม spring.cache.type=none ลงใน properties ของ​เทสต์ ทำให้​เทสต์​กลับ​มา​เขียว โดย code ของ​แอป​ไม่​ขยับ​สัก​ตัว​อักษร cacheManager ใน​ชุด​เทสต์​กลาย​เป็น NoOpCacheManager และ​ฐาน​ข้อมูล​ถูก​ถาม 3 ครั้ง

ของ​ที่​ขึ้น production ยัง​รั่ว​เหมือน​เดิม เพราะ​ทรง​ที่​เทสต์​รัน​ไม่ใช่​ทรง​ที่ deploy ราคา​ของ​ข้อ​นี้​คือ​ชุด​เทสต์​ต้อง​ยิง​สอง​ผู้​เช่า​โดย​เปิด cache ไว้​ตาม​จริง ซึ่ง​แดง​ตอน​แรก​แน่นอน

ทรง cached-keyed คือ​ทรง​เดียว​ใน​คอร์ส​ที่​ทั้ง​ลด​การ​ถาม​ฐาน​ข้อมูล และ​คืน​แถว​ของ​ผู้​เช่า​ถูก​ราย

ทรง cached-keyed — ต่าง​จาก​ทรง cached ที่ key ตัว​เดียว
@Service
class 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 ซึ่ง​ไม่มี​อยู่​ใน​สี่​อย่าง​นั้น

การ​ประกาศ​ขอบเขต​แบบ​นี้​มี​ราคา คุณ​ปิด​คอร์ส​โดย​ยัง​ไม่รู้​ว่า​ค่าที่​หมด​อายุ​แล้ว​มีหน้า​ตา​อย่างไร สิ่ง​ที่​ได้​แลก​มา​คือ​ทุก​ข้อ​อ้าง​ใน​คอร์ส​นี้​ยิง​ซ้ำ​แล้ว​ได้​ผล​เดิม

ซี​รีส์​คลินิก​มี​สี่​คอร์ส และ​สัญญา​ของ​แต่ละ​คอร์ส​อ่าน​ต่อ​กัน​เป็น​เส้น​เดียว

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


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • 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 อีกครั้ง ฐานข้อมูลถูกถามกี่ครั้ง และกุญแจในนั้นมีอะไรบ้าง