Làm Chủ Redis Từ Con Số 0 Đến Cách Quản Lý Giỏ Hàng Thực Tế
Hello anh em! Lại là mình đây. Dạo gần đây mình đang cắm đầu cắm cổ cày cuốc cho xong cái đồ án tốt nghiệp để kịp ra trường. Trong lúc làm đến chức năng Giỏ hàng (Cart), mình nhận ra nếu cứ mỗi lần khách hàng click thêm cái áo hay tăng số lượng mà mình lại bắt database PostgreSQL chạy query cập nhật xuống ổ cứng thì sớm muộn gì hệ thống cũng sập nguồn, lag lòi mắt.
Thế là mình quyết định mò mẫm sang dùng Redis để làm cache cứu net. Thấy cái này cực kỳ hay ho mà lúc vấn đáp bảo vệ thế nào các thầy cô cũng xoáy sâu vào hỏi. Vì vậy, mình viết luôn bài này vừa để lưu trữ kiến thức cho bản thân học thuộc lòng trước khi lên thớt, vừa chia sẻ cho ae nào cũng đang lơ tơ mơ về Redis như mình lúc đầu. Chiến luôn nhé!
1. Redis là cái gì và vì sao DB chính lại phải "sợ" nó?
Để hiểu vì sao cần Redis, chúng ta phải hiểu vấn đề của các cơ sở dữ liệu quan hệ truyền thống (như PostgreSQL, MySQL).
Mấy ông DB truyền thống ghi dữ liệu trực tiếp xuống ổ cứng (Disk) để chắc chắn dữ liệu không bị bay màu khi mất điện. Nhưng ngặt nỗi, tốc độ đọc ghi của ổ cứng (dù là SSD xịn sò) vẫn là rùa bò so với RAM (RAM nhanh hơn ổ cứng từ 100 đến 1000 lần).
Hãy tưởng tượng thế này:
- PostgreSQL giống như cái kho chứa đồ dưới tầng hầm: Lưu được cực kỳ nhiều thứ, rất an toàn nhưng mỗi lần muốn lấy gì bạn phải đi thang máy xuống, tìm kiếm mỏi mắt rồi mới mang lên được.
- Redis giống như cái bàn làm việc ngay trước mặt bạn: Chỉ để được vài món đồ hay dùng, nhưng cần một phát là với tay lấy được ngay lập tức.
Định nghĩa ngắn gọn: Redis là hệ thống lưu trữ dữ liệu dạng Key - Value hoạt động hoàn toàn trên RAM. Nhờ thế, tốc độ đọc ghi của nó nhanh khủng khiếp, lên tới cả trăm nghìn request mỗi giây.
Ae xem bảng so sánh tốc độ truy xuất này là tự hiểu vì sao ta phải dùng Redis làm cache liền:
+------------------------------------------------------------------+
| THIẾT BỊ | TỐC ĐỘ TRUY XUẤT (Gần đúng) |
+------------------------------------------------------------------+
| RAM (Bộ nhớ) | ~ 50 - 100 Nano-giây (Nhanh như chớp) |
| SSD (Ổ cứng) | ~ 50 - 150 Micro-giây (Chậm hơn RAM 1000 lần) |
| HDD (Ổ đĩa cơ) | ~ 5 - 10 Mili-giây (Chậm kinh hoàng) |
+------------------------------------------------------------------+
* Lưu ý nhẹ: 1 Mili-giây (ms) = 1.000 Micro-giây (µs) = 1.000.000 Nano-giây (ns)
2. Ủa thế sao Redis chạy đơn luồng (Single-thread) mà vẫn nhanh như gió?
HÚ HÚ! Câu này đi phỏng vấn hay lúc vấn đáp các thầy rất thích hỏi để bẫy học sinh nè: "Bình thường code đa luồng (Multi-thread) mới chạy nhanh chứ, sao thằng Redis đơn luồng mà lại gánh được hàng triệu request???"
Ae cứ tự tin trả lời ba ý này là các thầy gật gù ngay:
- Không tốn thời gian quản lý luồng: Chạy đơn luồng giúp Redis né được việc tranh chấp dữ liệu, không lo bị nghẽn (Deadlock) và đặc biệt là không tốn tài nguyên CPU để chuyển đổi qua lại giữa các luồng (Context Switching).
- RAM quá nhanh: Vì dữ liệu nằm trên RAM nên CPU không phải là điểm nghẽn. Đơn luồng xử lý tuần tự trên RAM vẫn là quá nhanh so với tốc độ mạng và ổ cứng.
- Cơ chế I/O Multiplexing (Non-blocking): Tuy luồng xử lý lệnh chỉ có một, nhưng luồng nhận yêu cầu kết nối lại rất nhiều. Đứa nào gửi dữ liệu sẵn sàng thì luồng chính mới nhảy vào xử lý, đứa nào chưa gửi xong thì đứng xếp hàng đợi, không làm nghẽn luồng chính.
3. Các cấu trúc dữ liệu trong Redis (Kèm lệnh thực tế để nghịch)
Redis không chỉ lưu chuỗi String nhàm chán đâu, nó có tận 5 kiểu dữ liệu cực kỳ bá đạo. Ae mở terminal gõ redis-cli rồi test thử các lệnh này xem sao:
3.1 String (Kiểu chuỗi)
Dùng để lưu chuỗi chữ, số hoặc cả ảnh mã hóa Base64.
SET hoten "Hai Hoan"
GET hoten
# Kết quả trả về: "Hai Hoan"
3.2 Hash (Bảng băm)
Giống như một Object trong Javascript hay một class Entity trong Java. Dùng để gom nhóm các thuộc tính của một đối tượng.
HSET nguoidung:1 name "Hoan" role "USER" age 22
HGET nguoidung:1 name
# Kết quả trả về: "Hoan"
3.3 List (Danh sách liên kết)
Mảng dữ liệu sắp xếp theo thứ tự thời gian thêm vào.
LPUSH email_queues "Gửi email giảm giá cho User 1"
LPUSH email_queues "Gửi email giảm giá cho User 2"
RPOP email_queues
# Kết quả trả về: "Gửi email giảm giá cho User 1" (Đứa nào vào trước thì được gửi trước)
- Ứng dụng: Làm hàng đợi tin nhắn hoặc danh sách thông báo mới nhất.
3.4 Set (Tập hợp không trùng lặp)
Tập hợp các phần tử không trùng nhau và không có thứ tự cụ thể.
SADD thong_tin "Java"
SADD thong_tin "Java" # Trùng lặp nên Redis sẽ ngó lơ đứa này
SADD thong_tin "Redis"
SMEMBERS thong_tin
# Kết quả chỉ có: "Java", "Redis"
3.5 Sorted Set (Tập hợp có sắp xếp)
Giống Set nhưng mỗi phần tử có thêm điểm số (score) đi kèm để tự động sắp xếp.
ZADD top_ban_chay 100 "ProductA"
ZADD top_ban_chay 250 "ProductB"
ZADD top_ban_chay 50 "ProductC"
ZRANGE top_ban_chay 0 -1 WITHSCORES
# Kết quả: ProductC (50), ProductA (100), ProductB (250)
4. Cách mình áp dụng Redis làm Giỏ Hàng (Cart) trong đồ án
Trong đồ án của mình, tính năng Giỏ hàng được thiết kế chạy trên Redis thay vì ghi trực tiếp vào DB chính PostgreSQL.
Vì sao lại thế? Giỏ hàng là chỗ người dùng thêm, xóa, đổi số lượng sản phẩm liên tục. Nếu mỗi lần họ click chuột ta lại cập nhật xuống DB chính thì ổ cứng sẽ gào thét vì quá tải. Cho nên, mình đưa giỏ hàng lên Redis lưu tạm thời trên RAM. Khi nào người dùng chốt mua và bấm nút "Thanh toán" tạo đơn hàng, mình mới ghi thông tin chính thức xuống PostgreSQL và xóa giỏ hàng trên Redis đi.
Dưới đây là đoạn code thực tế của mình:
4.1 File cấu hình kết nối Redis (RedisConfig.java)
Để lưu được Object Java (CartResponse) vào Redis, mình cần chuyển đổi nó thành chuỗi JSON (Serialization). Ae nhớ cấu hình serializer đàng hoàng, đừng để các sếp chê là NGUU khi lưu trực tiếp đối tượng Java thô xuống Redis nhé (nó sẽ biến thành một đống ký tự nhị phân loằng ngoằng cực kỳ dễ lỗi).
package com.haihoan2874.techhub.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 1. Cấu hình Key lưu dạng String
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 2. Cấu hình Value tự động chuyển sang chuỗi JSON nhờ Jackson
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
4.2 Xử lý giỏ hàng ở tầng Service (CartService.java)
Đoạn code đọc ghi giỏ hàng tương tác với Redis của mình trông như thế này:
@Service
@RequiredArgsConstructor
public class CartService {
private final RedisTemplate<String, Object> redisTemplate;
private static final String CART_PREFIX = "cart:"; // Key trong Redis sẽ có dạng: cart:userId
private static final long CART_TTL = 7; // Hạn dùng giỏ hàng là 7 ngày
// Lấy thông tin giỏ hàng
public CartResponse getCart(String userId) {
String key = CART_PREFIX + userId;
Object cachedCart;
try {
// Mò tìm giỏ hàng trên RAM của Redis
cachedCart = redisTemplate.opsForValue().get(key);
} catch (Exception ex) {
// Nếu lỗi kết nối Redis, kích hoạt fallback dùng RAM server cứu net!
return getFallbackCart(userId);
}
if (cachedCart == null) {
return createEmptyCart(userId);
}
return (CartResponse) cachedCart;
}
// Lưu thông tin giỏ hàng
private void saveCart(CartResponse cart) {
String key = CART_PREFIX + cart.getUserId();
try {
// Lưu giỏ hàng kèm thời gian sống (TTL) là 7 ngày
redisTemplate.opsForValue().set(key, cart, CART_TTL, TimeUnit.DAYS);
} catch (Exception ex) {
// Nếu lỗi, lưu dự phòng tạm thời vào RAM của App Server
fallbackCarts.put(cart.getUserId(), cart);
}
}
}
5. Cơ chế Fallback dự phòng: Phòng khi Redis "lăn đùng ra chết"
Nếu đi vấn đáp mà các thầy hỏi: "Lỡ tay tắt Redis container hoặc Redis bị sập đột ngột thì app của em sập theo luôn à?" thì đây là lúc bạn lôi cơ chế Fallback ra để ghi điểm tuyệt đối.
Trong code CartService.java của mình:
- Mình định nghĩa một
ConcurrentHashMaplưu tạm thời trên bộ nhớ RAM của Server chạy Spring Boot:private final Map<String, CartResponse> fallbackCarts = new ConcurrentHashMap<>(); - Bọc toàn bộ các thao tác gọi tới Redis trong khối lệnh
try-catch. - Cách hoạt động: Khi xảy ra lỗi kết nối với Redis, hệ thống sẽ tự động chuyển hướng lưu trữ và đọc ghi giỏ hàng qua
fallbackCarts. Khách hàng hoàn toàn không cảm nhận được giỏ hàng bị lỗi mà vẫn thêm sản phẩm và thanh toán bình thường.
6. Persistence: Tắt máy có bị mất sạch giỏ hàng không?
Vì dữ liệu Redis lưu trên RAM nên khi tắt máy hoặc restart server là bay màu sạch dữ liệu. Để giải quyết, Redis hỗ trợ ghi dữ liệu xuống đĩa cứng (Persistence) qua 2 cơ chế:
- RDB (Snapshot): Cứ sau một khoảng thời gian (ví dụ 5 phút), Redis chụp một tấm ảnh toàn bộ bộ nhớ hiện tại và lưu vào file nhị phân
dump.rdb. Cách này chạy nhanh, file gọn nhưng nếu sập nguồn đột ngột trước khi chụp ảnh mới thì dữ liệu của 5 phút đó sẽ mất. - AOF (Append Only File): Ghi lại mọi câu lệnh thay đổi dữ liệu (
SET,DEL...) vào fileappendonly.aof. Khi khởi động lại, Redis chạy lại từ đầu đến cuối các lệnh đó. Cách này an toàn tuyệt đối nhưng file log sẽ rất to.
- Áp dụng trong đồ án: Trong file
docker-compose.yml, container Redis của mình chạy lệnh:redis-server --appendonly yesTức là mình đã kích hoạt tính năng AOF để đảm bảo dữ liệu giỏ hàng của khách hàng an toàn nhất.
7. Cẩm nang câu hỏi vấn đáp Redis (Từ Dễ đến Nâng cao)
Để chuẩn bị tốt nhất cho buổi bảo vệ đồ án, ae học thuộc lòng bộ câu hỏi "bắt bài" này nha:
🌟 NHÓM CÂU HỎI DỄ (DÙNG ĐỂ KHỞI ĐỘNG)
❓ Câu 1: Redis chạy ở cổng mặc định bao nhiêu và lưu dữ liệu ở đâu?
- Trả lời: Dạ, cổng mặc định của Redis là 6379 và nó lưu dữ liệu hoàn toàn trên bộ nhớ RAM (In-memory).
❓ Câu 2: Làm thế nào để em kiểm tra xem một Key có tồn tại trong Redis không?
- Trả lời: Dạ, em dùng lệnh
EXISTS <tên_key>. Kết quả trả về1là có,0là không.
❓ Câu 3: Làm sao để em xóa toàn bộ dữ liệu đang lưu trong Redis?
- Trả lời: Dạ, em dùng lệnh
FLUSHALLđể xóa sạch toàn bộ dữ liệu của tất cả các database trong Redis.
❓ Câu 4: TTL là gì và tại sao trong giỏ hàng em lại đặt TTL?
- Trả lời: TTL là Time To Live (Thời gian sống của key). Em đặt TTL cho giỏ hàng để sau một khoảng thời gian (ví dụ 7 ngày) nếu khách hàng không quay lại mua thì Redis tự động xóa đi để giải phóng RAM, tránh đầy bộ nhớ.
❓ Câu 5: Để chạy Redis nhanh nhất dưới máy local khi code đồ án, em làm cách nào?
- Trả lời: Dạ, em sử dụng Docker để chạy container Redis thông qua file
docker-compose.yml, cực kỳ nhanh gọn và không cần cài trực tiếp vào hệ điều hành.
🌟 NHÓM CÂU HỎI TRUNG BÌNH & KHÓ (LẤY ĐIỂM CAO 8-9-10)
❓ Câu 6: Phân biệt Cache Hit và Cache Miss?
- Trả lời:
- Cache Hit: Khi request yêu cầu dữ liệu và dữ liệu đó đã có sẵn trong Cache (Redis). Hệ thống lấy luôn từ Cache trả về cho client mà không cần truy vấn DB chính.
- Cache Miss: Khi dữ liệu yêu cầu chưa có trong Cache. Hệ thống bắt buộc phải xuống DB chính để tìm, sau đó ghi ngược dữ liệu tìm được vào Cache để lần sau truy cập nhanh hơn.
❓ Câu 7: Tại sao em phải Serialize dữ liệu trước khi lưu vào Redis?
- Trả lời: Vì Java thao tác trên các đối tượng (Object), còn Redis chỉ hiểu dạng chuỗi byte hoặc chuỗi ký tự String/JSON. Việc Serialize (chuyển đổi Object sang chuỗi JSON) giúp Redis lưu trữ được, và khi lấy dữ liệu ra ta thực hiện Deserialize (chuyển JSON về lại Object Java) để code xử lý tiếp.
❓ Câu 8: Nếu hàng nghìn người truy cập và server Redis bị sập, hệ thống của em xử lý thế nào?
- Trả lời: Dạ không sao. Hệ thống của em có cơ chế Fallback. Khi kết nối tới Redis bị ngắt, khối lệnh
try-catchtrongCartServicesẽ bắt được lỗi, tự động chuyển hướng dữ liệu giỏ hàng của người dùng sang lưu trữ tạm thời trên RAM cục bộ của server ứng dụng (ConcurrentHashMap). Hệ thống vẫn hoạt động bình thường cho đến khi kết nối Redis được khôi phục.
❓ Câu 9: Trình bày sự khác nhau giữa Cache Breakdown và Cache Avalanche (Tuyết lở)?
- Trả lời:
- Cache Breakdown: Xảy ra khi một Key cực kỳ "hot" (lượng truy cập siêu lớn) vừa mới hết hạn TTL. Lúc này hàng vạn request đồng thời ập vào tìm key này bị hụt, đâm thẳng xuống DB chính để đọc dữ liệu gây nghẽn DB.
- Cache Avalanche: Xảy ra khi một lượng lớn các key trong Cache cùng hết hạn tại một thời điểm hoặc do server Redis bị sập hoàn toàn. Khi đó toàn bộ hệ thống bị Cache Miss, tất cả request đổ dồn xuống DB chính làm sập DB ngay lập tức.
- Cách chống: Em thiết lập thời hạn hết hạn TTL cho các key cộng thêm một khoảng thời gian ngẫu nhiên (Random Jitter) để tránh việc các key cùng hết hạn một lúc, đồng thời sử dụng cơ chế dự phòng Fallback.
THU CUỐI
Vậy là chúng ta đã đi qua bộ tài liệu tự học và ôn tập Redis cực kỳ chi tiết từ lý thuyết cơ bản, cách Redis hoạt động đơn luồng, cho đến cách áp dụng code giỏ hàng thực tế và các câu hỏi vấn đáp cốt lõi.
Hy vọng bài viết này sẽ giúp các bạn (và cả mình trong tương lai khi đọc lại) tự tin hơn khi bảo vệ đồ án tốt nghiệp.
Nếu thấy bài viết hữu ích, đừng ngại để lại 1 upvote để mình có động lực chia sẻ tiếp nhé! Chúc mọi người code thật mượt mà!
All rights reserved