99 Ngày Spring — Ngày 37: Redis
Ngày 36 ta đã làm cache chạy được với @Cacheable và Caffeine — nhanh, nhưng cache nằm trong tiến trình: restart là trắng, và chạy ba instance thì có ba cache lệch nhau. Hôm nay ta dời nó ra ngoài với Redis — một kho key-value trên RAM, dùng chung cho mọi instance, sống sót qua deploy. Điều đẹp nhất: nhờ lớp trừu tượng của Ngày 36, đổi Caffeine sang Redis gần như chỉ là vài dòng cấu hình.

Vì sao đổi Caffeine lấy Redis
Caffeine của Ngày 36 không hề sai — nó vẫn là cache nhanh nhất có thể vì dữ liệu nằm ngay trong heap. Vấn đề chỉ xuất hiện khi ứng dụng nhân bản: mỗi instance một bản cache riêng, không ai biết ai vừa xóa gì, và mỗi lần deploy là toàn bộ cache bốc hơi cùng lúc.
# Ngày 36 cache nằm TRONG tiến trình (Caffeine): restart là mất sạch,
# và mỗi instance giữ một bản riêng — 3 pod là 3 cache lệch nhau.
# Redis chuyển cache ra NGOÀI: một kho dùng chung, sống sót qua restart.
# docker-compose.yml
services:
redis:
image: redis:7-alpine
ports: ["6379:6379"]
# build.gradle / pom.xml
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
# application.yml
spring:
data:
redis:
host: localhost
port: 6379
cache:
type: redis # @Cacheable của Ngày 36 GIỜ ĐI THẲNG VÀO REDIS
redis:
time-to-live: 10m # TTL mặc định cho mọi cache
cache-null-values: falseDùng chung: ba instance nhìn cùng một kho, nên xóa cache ở instance này thì instance kia thấy ngay — hết cảnh dữ liệu cũ lởn vởn ở một pod.
Sống sót restart: deploy phiên bản mới không kéo theo một cơn bão truy vấn xuống database vì cache rỗng.
Đổi bằng cấu hình: đúng lời hứa của cache abstraction — code
@Cacheableviết hôm qua không phải sửa một chữ.Cái giá phải trả: thêm một thành phần phải vận hành, và mỗi lần đọc cache giờ là một vòng qua mạng chứ không còn là một lần tra bộ nhớ.
Serializer — đổi JDK sang JSON ngay từ đầu
Đây là quyết định nhỏ mà nhiều đội trả giá muộn. Mặc định Spring cất giá trị bằng JDK serialization: nhị phân, không đọc được, và ràng chặt vào chữ ký class — thêm một field là mọi entry cũ hỏng.
// Mặc định Spring dùng JDK serialization — giá trị trong Redis thành chuỗi nhị
// phân không ai đọc được, và đổi một field trong class là hỏng toàn bộ cache cũ.
// Gần như luôn nên đổi sang JSON:
@Bean
RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.serializeValuesWith(SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
}
// Vì sao JSON đáng đổi:
// - đọc được bằng redis-cli, debug production không phải đoán
// - thêm field mới KHÔNG làm nổ cache cũ (JDK serialization thì có)
// - ngôn ngữ khác cũng đọc được cùng kho cache
// Vẫn giữ nguyên tắc Ngày 36: cache DTO/record BẤT BIẾN, đừng cache entity JPA
// (entity kéo theo proxy lazy — serialize ra là gặp LazyInitializationException).Đổi sang
GenericJackson2JsonRedisSerializerđể giá trị đọc được bằng redis-cli — debug production không còn là trò đoán mò.Vẫn giữ nguyên kỷ luật của Ngày 36: cache record/DTO bất biến, tuyệt đối không cache entity JPA.
Đặt TTL ngay trong cấu hình, và tắt cache giá trị
nullnếu bạn không cố ý dùng nó để chặn cache penetration.
RedisTemplate — Redis không chỉ để cache
Cache chỉ là một cách dùng Redis. Bản chất nó là kho cấu trúc dữ liệu trên RAM với các thao tác nguyên tử — thứ giải quyết gọn ghẽ mấy bài toán mà làm bằng database thì vừa chậm vừa rắc rối.
// @Cacheable lo phần cache. Còn khi cần Redis như một CẤU TRÚC DỮ LIỆU
// (đếm, khóa, hàng đợi, xếp hạng) thì dùng thẳng template:
private final StringRedisTemplate redis;
// Đếm lượt xem — nguyên tử, không cần transaction:
redis.opsForValue().increment("post:" + id + ":views");
// Rate limit đơn giản: 100 request mỗi phút cho một IP
String key = "rate:" + ip;
Long dem = redis.opsForValue().increment(key);
if (dem == 1) redis.expire(key, Duration.ofMinutes(1)); // đặt hạn ở lần đầu
if (dem > 100) throw new TooManyRequestsException();
// Các họ thao tác hay dùng:
// opsForValue() chuỗi & số — cache, đếm, cờ
// opsForHash() bảng băm — lưu object theo field, sửa từng field
// opsForList() danh sách — hàng đợi đơn giản
// opsForSet() tập hợp — khử trùng, "ai đang online"
// opsForZSet() tập có điểm — bảng xếp hạng, top-K
// Quy ước ĐẶT TÊN KEY quan trọng hơn ta tưởng: "sanpham:42:chitiet".
// Redis là một không gian key PHẲNG dùng chung — không có tên miền thì
// hai service ghi đè nhau trong im lặng.incrementlà nguyên tử: đếm lượt xem hay đếm request không cần khóa, không cần transaction, và không mất số khi nhiều luồng cùng tăng.Chọn đúng họ thao tác đáng giá:
opsForZSetcho bảng xếp hạng,opsForHashkhi chỉ muốn sửa một field thay vì ghi đè cả object.Quy ước tên key là thứ phải chốt sớm — Redis chỉ có một không gian key phẳng dùng chung cho cả hệ thống.
Session store và bốn cái bẫy
Công dụng thứ hai hay gặp nhất là session dùng chung. Đây là chỗ Redis tỏa sáng nhất: chỉ thêm một dependency và một dòng cấu hình, ứng dụng đang chạy một máy bỗng chạy được nhiều máy mà không đá ai ra ngoài.
// Session store: bài toán kinh điển khi chạy nhiều instance.
// Session mặc định nằm trong RAM của từng server → user đăng nhập ở pod A,
// request kế tiếp rơi vào pod B là mất session. Redis giải quyết trong 2 bước:
implementation 'org.springframework.session:spring-session-data-redis'
spring:
session:
store-type: redis
timeout: 30m
// Xong. Không sửa một dòng controller nào — session đi ra Redis, mọi
// instance đọc chung, và deploy phiên bản mới không đá ai ra khỏi hệ thống.
// BỐN CÁI BẪY của Redis, gặp sớm hơn ta tưởng:
// 1. Redis chết thì sao? Mặc định lỗi cache LAN RA thành lỗi request.
// Khai báo CacheErrorHandler để bỏ qua lỗi cache và đi thẳng xuống DB.
// 2. Mọi thứ giờ đi qua MẠNG: Caffeine ~100 nanô giây, Redis ~1 mili giây.
// Vẫn nhanh hơn DB nhiều, nhưng vòng lặp gọi cache 1000 lần thì thấy ngay.
// 3. TTL là BẮT BUỘC, không phải tùy chọn — key không hạn dùng sẽ ở lại
// mãi mãi và ăn dần RAM cho tới khi Redis bắt đầu đuổi key bừa bãi.
// 4. Cache hai tầng nếu cần: Caffeine (L1, trong tiến trình) + Redis (L2,
// dùng chung) — nhanh nhất, đổi lại L1 của các instance có thể lệch nhau.Bẫy đau nhất là bẫy số một: Redis chết kéo cả ứng dụng chết theo. Cache là thứ để tăng tốc, hỏng thì phải rơi về database chứ không được biến thành lỗi 500 — hãy khai báo
CacheErrorHandlerngay từ đầu.Nhớ đổi đơn vị trong đầu: Caffeine tính bằng nanô giây, Redis tính bằng mili giây. Vẫn rẻ hơn DB rất nhiều, nhưng gọi trong vòng lặp thì vẫn đắt.
Key không TTL là rò rỉ bộ nhớ chạy chậm — đặt hạn dùng cho mọi key, kể cả key bạn nghĩ là tạm thời.
Cần cực nhanh thì làm hai tầng: Caffeine ở trong, Redis ở ngoài — nhưng phải chấp nhận L1 giữa các instance có thể lệch nhau trong vài giây.
Bài tập nhỏ
Bật Redis bằng
docker compose, đổispring.cache.typetừcaffeinesangredisvà xác nhận@Cacheablecủa Ngày 36 chạy y nguyên, không sửa code.Mở
redis-clichạyKEYS *vàTTL <key>để nhìn tận mắt cache của mình — rồi đổi serializer sang JSON và xem giá trị từ nhị phân thành đọc được.Chạy hai instance trên hai cổng khác nhau, sửa dữ liệu ở instance A và xác nhận instance B không còn trả bản cũ.
Viết rate limit 10 request mỗi phút bằng
increment+expire, rồi bắn 15 request để thấy ngưỡng chặn.Tắt hẳn container Redis trong lúc ứng dụng đang chạy: quan sát request lỗi, rồi thêm
CacheErrorHandlervà xác nhận ứng dụng rơi êm về database.
Kết lại
Bốn ý gói lại hôm nay: Redis biến cache từ riêng từng tiến trình thành dùng chung và sống sót restart, và nhờ lớp trừu tượng Ngày 36 thì đổi sang nó gần như chỉ là cấu hình; đổi serializer sang JSON ngay từ đầu để cache đọc được và không vỡ khi class đổi; RedisTemplate mở ra cả họ bài toán ngoài cache — counter, rate limit, xếp hạng — nhờ thao tác nguyên tử; và session store chỉ tốn một dependency nhưng phải nhớ TTL cho mọi key cùng đường lui khi Redis chết. Ngày 38 ta rời chuyện tốc độ để xử lý một bài toán cấu hình thật: nhiều datasource trong một ứng dụng — hai database, hai transaction manager, và @Primary. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


