Giới thiệu
Caching là một kỹ thuật không thể thiếu để xây dựng các hệ thống hiệu suất cao và mở rộng. Tuy nhiên, việc triển khai một hệ thống cache hỗ trợ nhiều chiến lược ghi, chính sách loại bỏ, và cơ chế vô hiệu hóa không phải là điều đơn giản. Bài viết này sẽ hướng dẫn bạn qua một thiết kế Cấp Thấp (LLD) hoàn chỉnh của một hệ thống cache trong Java, sử dụng các mẫu thiết kế đã được chứng minh để làm cho nó trở nên mô-đun, có thể mở rộng và sẵn sàng cho sản xuất.
Mục Tiêu Thiết Kế
Chúng ta cần một hệ thống cache hỗ trợ:
- Nhiều chiến lược ghi: write-through, write-back, write-around
- Nhiều chính sách loại bỏ: LRU, LFU, TTL
- Cơ chế vô hiệu hóa: thông báo dựa trên listener
- Trừu tượng hóa lưu trữ: bộ nhớ tạm, cơ sở dữ liệu hoặc kho lưu trữ bên ngoài
- Thống kê: hit và miss của cache
- Khả năng mở rộng thông qua các giao diện và mẫu thiết kế sạch
Các Mẫu Thiết Kế Sử Dụng
- Mẫu Chiến Lược → cho các chiến lược loại bỏ và ghi
- Mẫu Factory + Builder → cho việc tạo cache với cấu hình linh hoạt
- Mẫu Observer → cho các listener vô hiệu hóa
- Mẫu Singleton → cho thống kê
- Mẫu Decorator → cho việc loại bỏ dựa trên TTL và bao quanh các chính sách khác
Các Giao Diện Chính
Chúng ta định nghĩa các hợp đồng chính:
Cache<K, V>→ các thao tác chính của cacheEvictionPolicy<K>→ cách các mục bị loại bỏWriteStrategy<K, V>→ cách ghi được duy trìPersistence<K, V>→ trừu tượng cho lưu trữ cơ bảnInvalidationListener<K>→ observer cho các sự kiện loại bỏ/vô hiệu hóa
Chính Sách Loại Bỏ
- LRU (Least Recently Used): Loại bỏ các mục ít được truy cập nhất trước.
- LFU (Least Frequently Used): Loại bỏ các mục ít được sử dụng nhất.
- TTL Decorator: Bao quanh chính sách khác và thực thi thời gian sống.
Chính sách này sử dụng Mẫu Chiến Lược để bạn có thể thay đổi chính sách mà không cần thay đổi logic chính.
Chiến Lược Ghi
- Write-Through: Ghi đồng thời vào cache và lưu trữ.
- Write-Back (Write-Behind): Ghi vào cache trước, lưu trữ không đồng bộ.
- Write-Around: Ghi trực tiếp vào lưu trữ; cache chỉ được cập nhật khi đọc.
Điều này cho phép bạn điều chỉnh giữa tính nhất quán và hiệu suất.
Vô Hiệu Hóa & Người Quan Sát
Chúng tôi hỗ trợ listeners để thông báo khi các khóa bị vô hiệu hóa (ví dụ, bị loại bỏ). Điều này hữu ích cho việc giữ cho nhiều cache đồng bộ hoặc cho ghi log bên ngoài.
Lớp Lưu Trữ
Chúng tôi trừu tượng hóa lưu trữ để bạn có thể cắm vào:
- Bản đồ trong bộ nhớ (cho demo/testing)
- Cơ sở dữ liệu
- Cache phân phối như Redis
Điểm Nổi Bật Trong Triển Khai
Dưới đây là mã đã được cấu trúc (tổng quan đơn giản):
java
interface Cache<K, V> { V get(K key); void put(K key, V value); void remove(K key); }
interface EvictionPolicy<K> { void recordAccess(K key); K evictKey(); }
interface WriteStrategy<K, V> { void write(K key, V value, Persistence<K, V> p); }
interface Persistence<K, V> { void write(K k, V v); Optional<V> read(K k); }
class SimpleCache<K, V> implements Cache<K, V> { /* kết nối các chiến lược & chính sách */ }
class LruEvictionPolicy<K> implements EvictionPolicy<K> { /* sử dụng linked hash set */ }
class TtlEvictionDecorator<K> implements EvictionPolicy<K> { /* thêm thời gian hết hạn */ }
class WriteThroughStrategy<K, V> implements WriteStrategy<K, V> { /* đồng bộ */ }
class WriteBackStrategy<K, V> implements WriteStrategy<K, V> { /* không đồng bộ */ }
class InMemoryPersistence<K, V> implements Persistence<K, V> { /* sử dụng bản đồ */ }
Factory & Cấu Hình
Chúng tôi thêm một CacheFactory với một builder CacheConfig để đơn giản hóa việc tạo:
java
CacheConfig cfg = new CacheConfig();
cfg.capacity = 1000;
cfg.policy = new LruEvictionPolicy<>(cfg.capacity);
cfg.writeStrategy = new WriteThroughStrategy<>();
cfg.persistence = new InMemoryPersistence<>();
Cache<String, String> cache = CacheFactory.create(cfg);
Điều này đảm bảo một API sạch cho khách hàng.
Ví Dụ Sử Dụng
java
cache.put("a", "1");
cache.put("b", "2");
System.out.println(cache.get("a")); // hit
System.out.println(cache.get("c")); // miss → tải từ lưu trữ
Chúng ta cũng có thể gán listeners:
java
((SimpleCache<String, String>) cache).addInvalidationListener(key ->
System.out.println("Vô hiệu hóa: " + key));
Thống Kê
Chúng tôi theo dõi hits và misses với một lớp Singleton CacheMetrics:
java
System.out.println("Hits=" + CacheMetrics.get().hits());
System.out.println("Misses=" + CacheMetrics.get().misses());
Tổng Hợp
- Sử dụng LRU + Write-Through cho tính nhất quán mạnh.
- Sử dụng LFU + Write-Back cho hiệu suất trong khối lượng đọc nặng.
- Sử dụng TTL + Write-Around cho dữ liệu nhạy cảm với độ mới.
Kết Luận
Bằng cách áp dụng các mẫu thiết kế và trừu tượng hóa mô-đun, chúng ta đã xây dựng một hệ thống cache có thể cắm vào và mở rộng. Hệ thống này cho phép bạn:
- Thay đổi các chính sách loại bỏ
- Chọn các chiến lược ghi dựa trên tính nhất quán và hiệu suất
- Thêm các listener cho việc vô hiệu hóa
- Mở rộng các lớp lưu trữ
Thiết kế này có thể mở rộng từ một cache trong bộ nhớ đơn giản đến một hệ thống phân phối với lưu trữ trong cơ sở dữ liệu hoặc Redis.
👉 Bước tiếp theo: chúng ta có thể mở rộng điều này với hỗ trợ cache phân phối, triển khai LFU thực sự, và tích hợp Spring Boot với Redis.