Mở đầu — vì sao bài này quan trọng
Ở bài trước chúng ta đã nói về các mẫu caching (cache-aside, write-through, write-behind...) như những chiến lược ở tầng tư duy. Nhưng khi bước vào giai đoạn triển khai thực tế, bạn — với vai trò một kiến trúc sư — sẽ phải trả lời một câu hỏi rất cụ thể và rất "đắt tiền": Chúng ta dùng công cụ nào để làm distributed cache? Redis hay Memcached?
Đây không phải câu hỏi nhỏ. Một khi cache layer đã đi vào production và hàng chục service phụ thuộc vào nó, việc đổi từ Redis sang Memcached (hay ngược lại) tốn hàng tháng công sức migration, chưa kể rủi ro downtime. Quyết định này thuộc loại "khó đảo ngược" (irreversible decision) — đúng loại quyết định mà một ADR (Architecture Decision Record) ra đời để ghi lại.
Vấn đề là rất nhiều đội ngũ chọn Redis một cách mặc định vì "ai cũng dùng Redis", mà không thực sự hiểu Memcached mạnh ở đâu, Redis đắt ở đâu, và khi nào lựa chọn "đơn giản hơn" lại là lựa chọn đúng. Bài này giúp bạn ra quyết định có căn cứ, chứ không theo phong trào. Đây là một distributed cache — nghĩa là cache nằm ngoài process ứng dụng, chia sẻ chung cho nhiều instance/service qua mạng — nên lựa chọn công cụ ảnh hưởng trực tiếp tới độ trễ, chi phí hạ tầng và cả độ phức tạp vận hành.
Khái niệm cốt lõi
Cả Redis và Memcached đều là in-memory data store — lưu dữ liệu trong RAM để truy xuất cực nhanh (thường dưới 1ms), truy cập qua mạng, và dùng phổ biến làm cache dùng chung cho cụm ứng dụng. Nhưng triết lý thiết kế của hai công cụ này khác nhau căn bản.
Memcached — sự đơn giản có chủ đích
Memcached ra đời năm 2003, được thiết kế với một mục tiêu duy nhất: làm một key-value cache đơn giản, nhanh, dễ scale ngang. Bạn SET một key với một giá trị (chuỗi bytes), và GET nó ra. Hết. Giá trị với Memcached chỉ là một khối bytes mờ đục (opaque blob) — nó không hiểu gì bên trong.
Điểm mạnh cốt lõi của Memcached:
- Multi-threaded: Memcached tận dụng được nhiều core CPU trong một node. Trên một máy 16 core, nó dùng được cả 16 core cho việc xử lý request.
- Kiến trúc bộ nhớ slab: quản lý memory theo các "slab class" cố định, giảm phân mảnh bộ nhớ, rất ổn định khi lưu hàng triệu object kích thước tương tự nhau.
- Đơn giản để vận hành: ít tính năng nghĩa là ít thứ để hỏng, ít thứ để cấu hình sai.
Redis — chiếc dao đa năng của kỹ sư backend
Redis (2009) cũng là key-value store nhưng đi theo hướng "data structure server". Giá trị trong Redis không chỉ là chuỗi bytes — nó là những cấu trúc dữ liệu có ngữ nghĩa mà server hiểu và thao tác được trực tiếp:
- String: chuỗi/số, hỗ trợ
INCR,DECR— lý tưởng cho bộ đếm. - Hash: như một object/map, ví dụ lưu profile người dùng theo từng field.
- List: danh sách hai đầu, làm queue hoặc feed đơn giản.
- Set và Sorted Set (ZSET): tập hợp, đặc biệt sorted set dùng làm bảng xếp hạng (leaderboard) cực mạnh nhờ
ZRANGE,ZRANK. - Stream: log append-only, nền tảng cho hàng đợi tin nhắn nhẹ.
- Bitmap và HyperLogLog: đếm số lượng phần tử duy nhất (unique count) trên hàng chục triệu phần tử chỉ với vài KB bộ nhớ.
- Persistence: hai cơ chế. RDB chụp snapshot toàn bộ dataset định kỳ ra đĩa (gọn, khôi phục nhanh, nhưng có thể mất vài phút dữ liệu cuối). AOF (Append-Only File) ghi lại từng lệnh ghi (bền hơn, mất tối đa ~1 giây nếu dùng
everysec, nhưng file lớn hơn). Thực tế nhiều đội bật cả hai. - Replication và High Availability: replica đọc, cùng với Redis Sentinel để tự động failover, hoặc Redis Cluster để sharding dữ liệu trên nhiều node.
- Pub/Sub và Keyspace notifications: cho phép Redis đóng vai trò kênh phát tin nhẹ.
- Lua scripting và Transactions (
MULTI/EXEC): chạy nhiều lệnh atomically. - TTL linh hoạt và các chính sách eviction đa dạng (
allkeys-lru,volatile-lru,allkeys-lfu...).
So sánh nhanh để định hình tư duy
| Tiêu chí | Memcached | Redis |
|---|---|---|
| Mô hình dữ liệu | Key-value đơn giản (blob) | Nhiều cấu trúc dữ liệu |
| Đa luồng | Có (multi-threaded) | Chủ yếu single-thread cho lệnh |
| Persistence | Không | RDB + AOF |
| Replication/HA | Không tích hợp | Sentinel, Cluster |
| Pub/Sub, scripting | Không | Có |
| Kích thước value tối đa | 1MB (mặc định) | 512MB |
| Độ phức tạp vận hành | Rất thấp | Trung bình–cao |
Tình huống thực tế
Ví dụ 1 — Sàn thương mại điện tử "ChợViệt" và bài toán session cache
ChợViệt là một sàn TMĐT giả định tại TP.HCM, khoảng 2 triệu người dùng hoạt động hằng ngày. Ban đầu đội backend dùng Memcached làm cache cho hai thứ: session người dùng và cache kết quả truy vấn sản phẩm (product detail JSON).
Trong hai năm đầu, Memcached hoạt động hoàn hảo. Nó multi-threaded nên trên cụm 3 node 8-core, throughput rất cao. Session chỉ là blob JSON, GET/SET đơn giản. Chi phí vận hành gần như bằng không — không ai phải lo về persistence vì mất session lúc restart chỉ khiến người dùng đăng nhập lại, không mất dữ liệu quan trọng.
Vấn đề nảy sinh khi marketing yêu cầu tính năng "giỏ hàng bị bỏ quên" (abandoned cart): cần biết người dùng đã thêm gì vào giỏ và giữ lại kể cả khi họ đóng trình duyệt. Đội thử nhét giỏ hàng vào Memcached, nhưng vấp phải hai giới hạn: mỗi lần cập nhật một món hàng phải đọc toàn bộ blob giỏ, sửa, ghi lại (read-modify-write) — dễ xảy ra race condition khi người dùng mở nhiều tab; và khi Memcached restart để bảo trì, toàn bộ giỏ hàng "biến mất".
Bài học rút ra: Memcached là lựa chọn đúng cho session và product cache — những dữ liệu ephemeral, opaque, có thể tái tạo. Nhưng khi yêu cầu chuyển sang dữ liệu có cấu trúc (giỏ hàng = một hash/set) và cần độ bền tương đối, nó không còn phù hợp. Đội đã đúng khi không migrate toàn bộ sang Redis, mà chỉ thêm một cụm Redis nhỏ riêng cho giỏ hàng (dùng Hash + persistence AOF everysec), giữ Memcached cho phần còn lại. Chọn công cụ theo tính chất dữ liệu, không theo "một công cụ cho tất cả".
Ví dụ 2 — Ứng dụng gọi xe "GoNhanh" và bài toán leaderboard tài xế
GoNhanh (giả định, mô phỏng các nền tảng như Grab/Gojek tại Đông Nam Á) cần một bảng xếp hạng tài xế theo điểm thưởng, cập nhật gần như realtime cho chương trình "Top tài xế tuần". Có khoảng 200.000 tài xế hoạt động, điểm thay đổi liên tục sau mỗi cuốc xe.
Nếu dùng Memcached, họ sẽ phải kéo toàn bộ danh sách về ứng dụng, sắp xếp trong bộ nhớ app, mỗi lần cần rank — cực kỳ tốn kém và không realtime. Với Redis, bài toán này gói gọn trong vài lệnh: mỗi cuốc xe hoàn thành, ZINCRBY leaderboard 5 driver:8842; muốn top 10, ZREVRANGE leaderboard 0 9 WITHSCORES; muốn biết hạng của một tài xế, ZREVRANK. Sorted Set của Redis được thiết kế đúng cho việc này, độ phức tạp O(log N).
Đội cũng tận dụng thêm HyperLogLog để đếm số hành khách duy nhất mỗi tài xế phục vụ trong tuần mà không cần lưu từng ID — tiết kiệm bộ nhớ khổng lồ.
Bài học rút ra: Khi bài toán bản chất là một cấu trúc dữ liệu (xếp hạng, đếm unique, hàng đợi), Redis không chỉ tiện mà còn giúp bạn đẩy tính toán xuống tầng data store thay vì làm ở tầng ứng dụng. Đây là lúc Redis thắng tuyệt đối, và Memcached đơn giản không phải là ứng viên.
Ví dụ 3 — Fintech "PayVN" và cái bẫy "dùng Redis làm database"
PayVN là một ví điện tử giả định. Đội kỹ thuật, vì đã có sẵn cụm Redis phục vụ cache, quyết định dùng luôn Redis làm nơi lưu số dư ví tạm trước khi ghi xuống PostgreSQL, để "giảm tải database". Họ bật AOF nên nghĩ dữ liệu đã an toàn.
Sự cố xảy ra khi một node Redis master crash đúng lúc failover chưa kịp hoàn tất. Redis replication là asynchronous — replica có thể chậm hơn master vài mili giây tới vài giây. Khi Sentinel promote replica lên làm master, một số giao dịch vừa ghi vào master cũ mà chưa kịp sync đã mất. Với cache thì không sao, nhưng đây là số dư tiền. Kết quả: vài trăm giao dịch bị lệch, phải đối soát thủ công.
Bài học rút ra: Persistence và replication của Redis khiến nó trông giống một database, nhưng nó không đảm bảo durability mạnh như một RDBMS transactional. Redis là cache tuyệt vời; đừng biến nó thành nguồn sự thật (source of truth) cho dữ liệu tài chính. Ranh giới giữa "cache có persistence" và "primary datastore" là ranh giới cần một ADR rõ ràng để cả team không vượt qua trong lúc vội.
Hướng dẫn từng bước
Đây là quy trình mình dùng để ra quyết định Redis vs Memcached cho một hệ thống mới:
- Phân loại dữ liệu sẽ cache. Nó là blob opaque (JSON, HTML fragment, session) hay là cấu trúc có ngữ nghĩa (counter, set, ranking, queue)? Nếu chỉ toàn blob đơn giản
GET/SET— Memcached đủ và nhẹ hơn.
- Xác định có cần persistence không. Nếu mất sạch cache khi restart là chấp nhận được (chỉ tăng miss tạm thời rồi warm lại) — Memcached ổn. Nếu cần warm-restart nhanh hoặc giữ trạng thái qua khởi động lại — Redis với RDB/AOF.
- Xác định yêu cầu High Availability. Cần tự động failover, replica đọc, sharding có sẵn? Redis Cluster/Sentinel cho bạn sẵn. Với Memcached bạn tự lo ở tầng client (consistent hashing) hoặc dùng dịch vụ managed.
- Đo mô hình truy cập (access pattern). Value lớn (>1MB)? Redis (tối đa 512MB/value) linh hoạt hơn Memcached (mặc định 1MB). Cần thao tác atomic phức tạp trên phía server? Redis với Lua/
MULTI.
- Cân nhắc CPU và throughput trên mỗi node. Nếu workload cực kỳ đơn giản nhưng khối lượng khổng lồ và bạn muốn vắt kiệt CPU đa nhân trên ít node — Memcached multi-threaded có lợi thế. Với Redis, bạn scale bằng thêm shard.
- Ưu tiên managed service khi có thể. Trên cloud, chọn giữa AWS ElastiCache (hỗ trợ cả Redis và Memcached), Google Memorystore, hay các dịch vụ tương đương. Managed giúp bạn khỏi tự lo replication, backup, patching.
- Ghi ADR. Ghi lại bối cảnh, các phương án, quyết định và hệ quả — đặc biệt ghi rõ ranh giới sử dụng (ví dụ: "Redis chỉ dùng làm cache, không làm source of truth cho dữ liệu giao dịch").
Lỗi thường gặp & mẹo
Lỗi: Mặc định chọn Redis vì "nó xịn hơn". Redis nhiều tính năng hơn thật, nhưng mỗi tính năng là một thứ cần cấu hình, giám sát và có thể hỏng. Nếu bạn chỉ cần GET/SET blob với throughput cao, Memcached cho bạn sự đơn giản đáng giá.
Lỗi: Không đặt maxmemory và eviction policy cho Redis. Redis mặc định có thể ăn hết RAM rồi bị OOM killer giết. Luôn đặt maxmemory và chọn policy phù hợp (thường allkeys-lru cho cache thuần).
Lỗi: Nhầm persistence là durability tuyệt đối. Như ví dụ PayVN — AOF everysec vẫn có thể mất tới ~1 giây dữ liệu, và replication async có thể mất dữ liệu khi failover. Đừng dùng Redis làm nguồn sự thật cho dữ liệu không được phép mất.
Lỗi: Lệnh nặng chặn cả server. Redis single-threaded cho việc xử lý lệnh, nên một lệnh KEYS hay SMEMBERS trên set khổng lồ sẽ chặn* mọi client khác. Dùng SCAN thay KEYS, tránh thao tác O(N) lớn trên hot path.
Mẹo: Cache stampede. Khi một key nóng hết hạn, hàng nghìn request cùng lúc dội xuống database. Dùng TTL có jitter (thêm ngẫu nhiên vài giây), hoặc cơ chế lock/single-flight để chỉ một request đi tính lại. Điều này đúng cho cả Redis lẫn Memcached.
Mẹo: Tách cụm theo mục đích. Đừng nhét session ephemeral chung cụm với dữ liệu cần persistence. Tách cụm giúp bạn cấu hình eviction, persistence và scaling độc lập.
Mẹo: Bật monitoring hit ratio. Một cache có hit ratio thấp (dưới ~80%) thường báo hiệu TTL sai, key thiết kế kém, hoặc đang cache sai thứ — quan trọng hơn cả việc chọn Redis hay Memcached.
Bài tập thực hành
- Ma trận quyết định. Cho ba tình huống sau, hãy chọn Redis hay Memcached và giải thích trong 2–3 câu mỗi ý: (a) cache fragment HTML trang chủ báo điện tử; (b) bộ đếm rate-limit theo IP với
INCRvà TTL; (c) hàng đợi tác vụ gửi email nhẹ.
- Thiết kế leaderboard. Viết ra các lệnh Redis (dùng Sorted Set) để: cộng 10 điểm cho
user:123, lấy top 5, và lấy thứ hạng củauser:123. Sau đó giải thích tại sao Memcached không phù hợp cho bài này.
- Viết một ADR ngắn (khoảng nửa trang) cho quyết định "Chọn Memcached cho session cache của một ứng dụng web nội bộ 50.000 người dùng". Nêu context, các phương án cân nhắc (Redis, Memcached, DB session), quyết định, và ít nhất hai hệ quả (consequences) — cả tích cực lẫn tiêu cực.
- Tình huống sự cố. Team của bạn báo Redis thỉnh thoảng "đứng hình" vài giây vào giờ cao điểm. Liệt kê 3 nguyên nhân khả dĩ liên quan đến những gì đã học (gợi ý: lệnh chặn, persistence, eviction) và cách kiểm chứng từng nguyên nhân.
Tóm tắt
Redis và Memcached đều là in-memory distributed cache, nhưng khác nhau về triết lý. Memcached là key-value cache đơn giản, multi-threaded, không persistence — chọn nó khi bạn cần cache blob opaque với throughput cao và muốn vận hành cực nhẹ. Redis là "data structure server" giàu tính năng — string, hash, list, set, sorted set, stream, bitmap, hyperloglog — kèm persistence (RDB + AOF), replication/HA (Sentinel, Cluster), pub/sub và scripting; chọn nó khi bài toán bản chất là một cấu trúc dữ liệu (leaderboard, counter, queue, unique-count) hoặc cần warm-restart và HA tích hợp.
Ba nguyên tắc mang về: (1) chọn công cụ theo tính chất dữ liệu và yêu cầu vận hành, không theo phong trào; (2) nhiều tính năng hơn cũng đồng nghĩa nhiều thứ cần vận hành và có thể hỏng hơn — đơn giản có giá trị riêng; (3) persistence của Redis không biến nó thành database — đừng dùng nó làm nguồn sự thật cho dữ liệu không được phép mất. Và cuối cùng, hãy ghi lại quyết định này thành một ADR, kèm ranh giới sử dụng rõ ràng, vì đây là một lựa chọn khó đảo ngược.