LinkedHashMap แบบ Access Order: ทำ LRU Cache เล็กให้เข้าใจได้
cache ขนาดเล็กใน process ไม่จำเป็นต้องเริ่มจากระบบใหญ่เสมอครับ LinkedHashMap มีโหมด access order ที่ย้าย entry ซึ่งเพิ่ง get หรือ put ไปท้ายลำดับ แล้วเปิด hook removeEldestEntry ให้ตัดตัวเก่าสุดเมื่อเกินความจุ
WHY: Bound สำคัญกว่า Cache Hit ในวันแรก
HashMap ธรรมดาไม่มีลำดับ eviction หากเก็บไปเรื่อย ๆ memory จะโตตาม key cardinality LRU หรือ least recently used จำกัดขนาดและเก็บของที่เพิ่งถูกใช้ คล้ายชั้นวางหนังสือจำนวนช่องคงที่ หนังสือที่เพิ่งอ่านกลับไปท้ายแถว ส่วนเล่มหน้าสุดออกเมื่อของใหม่มา
แต่ “เพิ่งใช้” ไม่เท่ากับ “มีคุณค่า” เสมอ scan ใหญ่ครั้งเดียวอาจไล่ของร้อนออกทั้งหมด และ cache stampede ยังเกิดเมื่อหลาย thread miss key เดียวกัน ดังนั้นตัวอย่างนี้เหมาะกับ metadata ขนาดเล็กและ single-threaded ownership ไม่ใช่คำตอบ production ทุกกรณี
import java.util.LinkedHashMap;
import java.util.Map;
public class Main {
static final class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
LruCache(int capacity) {
super(capacity + 1, 0.75f, true);
if (capacity < 1) throw new IllegalArgumentException("capacity must be positive");
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
public static void main(String[] args) {
LruCache<String, Integer> cache = new LruCache<>(2);
cache.put("a", 1);
cache.put("b", 2);
assert cache.get("a") == 1;
cache.put("c", 3);
assert cache.containsKey("a") : "recently used entry should remain";
assert !cache.containsKey("b") : "least recently used entry should leave";
assert cache.containsKey("c");
System.out.println("lru tests passed");
}
}
HOW: ห่อ Collection ไม่ให้ Contract รั่ว
อย่าส่ง Map mutable นี้ให้ caller เพราะ caller อาจเรียก iteration, putAll หรือ null key โดยไม่ผ่าน policy สร้างคลาส cache ที่เปิด get, put, invalidate และ metrics เท่าที่จำเป็น กำหนดชัดว่า null value หมายถึง miss หรือเป็นข้อมูลที่ cache ได้
LinkedHashMap ไม่ thread-safe และใน access-order mode แม้ get ก็เปลี่ยนโครงสร้างลำดับ การครอบ synchronizedMap ช่วย mutual exclusion แต่ computation ที่เกิดนอก lock ยังแข่งกันได้ หากมี concurrency, expiry, weight-based eviction หรือ async load ให้ใช้ library cache ที่ออกแบบมาเฉพาะและผ่าน benchmark
capacity แบบจำนวน entry ใช้ไม่ได้เมื่อ value แต่ละตัวขนาดต่างกันมาก cache รูป 10 รูปอาจหนักกว่าข้อความหมื่นชิ้น หาก memory คือข้อจำกัดจริง ให้กำหนด weight หรือวัด retained heap รวมถึงตั้ง metric hit, miss, eviction และ load latency
Recap
- accessOrder=true ทำให้ get ขยับ entry ไปท้ายลำดับ
- removeEldestEntry ใช้ enforce capacity หลัง insert
- ห่อ Map เพื่อคุม null, invalidation และ metrics
- get เปลี่ยนโครงสร้าง จึงต้องคิดเรื่อง thread safety
- ใช้ cache library เมื่อมี expiry, weight หรือ concurrent loading
ทดลอง cache read-only metadata หนึ่งจุดด้วย capacity ต่ำและเปิด eviction metric ก่อนขยายครับ