+9

⚡🧠 Các Loại Caching Phổ Biến – Khi Tốc Độ Phụ Thuộc Vào Cách Bạn Nhớ - Caching P4

Các Loại Caching Phổ Biến – Khi Tốc Độ Phụ Thuộc Vào Cách Bạn Nhớ

1. Cạm bẫy "Redis cho mọi thứ"

Trong cộng đồng kỹ sư backend hiện đại, có một phản xạ vô điều kiện đã trở thành một lối mòn tư duy: "Hệ thống chậm? Cài Redis làm cache đi!". Từ các dự án khởi nghiệp nhỏ, các trang blog cá nhân cho đến những hệ thống ERP nội bộ có lượng người dùng thưa thớt, Redis gần như là cái tên mặc định xuất hiện trong file docker-compose.yml hoặc trên sơ đồ kiến trúc đám mây của mọi lập trình viên.

Thế nhưng, việc mặc định lựa chọn một công cụ mạnh mẽ và phức tạp như Redis mà không cân nhắc kỹ bối cảnh thực tế chính là biểu hiện của việc thiếu tư duy hệ thống. Redis là một hệ thống phân tán tuyệt vời, nhưng nó đi kèm với chi phí: tài nguyên máy chủ độc lập, đường truyền mạng (network I/O), độ phức tạp trong vận hành (ops overhead), và chi phí tài chính thực tế cho các dịch vụ đám mây managed.

Khi tốc độ phản hồi của hệ thống phụ thuộc vào cách bạn "nhớ" dữ liệu, việc chọn sai công cụ lưu trữ cache có thể biến một cải tiến hiệu năng thành một gánh nặng tài chính và vận hành. Để thiết kế một hệ thống thực sự tối ưu, một kỹ sư dày dạn kinh nghiệm cần phải hiểu rõ các phương án thay thế nhẹ nhàng hơn như In-memory cache hay File-based cache, từ đó đưa ra lựa chọn phù hợp nhất với bối cảnh của mình.


2. Câu chuyện thực tế từ Production: Cụm Redis đắt đỏ cho một trang tin tức tĩnh

Hãy bắt đầu bằng một câu chuyện xương máu tại một công ty khởi nghiệp công nghệ. Đội ngũ kỹ sư lúc đó được giao nhiệm vụ xây dựng một cổng thông tin tuyển dụng và tin tức nội bộ cho một đối tác doanh nghiệp lớn. Để chứng tỏ năng lực kỹ thuật và chuẩn bị cho khả năng "scale vô hạn" trong tương lai, team đã thiết kế một hệ thống microservices hiện đại.

Để tối ưu hiệu năng tải trang, danh mục bài viết và danh sách phòng ban liên tục được cache lại. Và tất nhiên, công cụ được chọn không gì khác ngoài Redis. Team đã cấu hình một cụm Redis Cloud dạng Managed Service trên AWS với đầy đủ tính năng High Availability (Multi-AZ, Cluster Mode) để đảm bảo hệ thống không bao giờ sập.

Sau 3 tháng vận hành ổn định, bộ phận tài chính gửi một cảnh báo khẩn cấp: Hóa đơn AWS tăng vọt. Khi phân tích chi phí, cả team ngã ngửa khi phát hiện ra:

  • Cơ sở dữ liệu chính PostgreSQL (chạy RDS) chỉ tốn khoảng $30/tháng vì lượng dữ liệu thực tế không nhiều và được index tốt.
  • Cụm server ứng dụng chạy trên ECS Fargate tiêu tốn khoảng $45/tháng.
  • Cụm Redis Managed làm nhiệm vụ cache tiêu tốn đến $180/tháng – chiếm hơn một nửa tổng hóa đơn hạ tầng.

Khi kiểm tra kỹ hơn các thông số giám sát (metrics), lượng truy cập thực tế của cổng thông tin chỉ rơi vào khoảng 500-1000 lượt xem mỗi ngày, chủ yếu tập trung vào giờ hành chính. Dữ liệu tin tức và danh mục chỉ được cập nhật trung bình một lần mỗi tuần bởi phòng nhân sự. Việc duy trì một cụm Redis phân tán đắt đỏ chỉ để lưu trữ vài megabyte dữ liệu tĩnh, hầu như không thay đổi, là một sự lãng phí tài nguyên khủng khiếp.

Sau buổi họp rút kinh nghiệm (post-mortem), nhóm quyết định gỡ bỏ cụm Redis và thay thế bằng cơ chế In-memory cache kết hợp File-based cache ngay trên server ứng dụng. Kết quả? Hệ thống chạy nhanh hơn (vì giảm được độ trễ mạng tới Redis), hóa đơn cloud giảm ngay lập tức $180/tháng, và độ phức tạp trong source code cũng giảm đi đáng kể do không cần duy trì các kết nối mạng phức tạp tới Redis.


3. Bản chất của vấn đề: Network vs Local I/O và sự thiếu hụt tư duy thực dụng

Tại sao đội ngũ kỹ sư trong câu chuyện trên lại đưa ra quyết định sai lầm? Đó là vì họ đã bỏ qua một nguyên lý cơ bản của phần cứng máy tính: Kim tự tháp phân cấp bộ nhớ (Memory Hierarchy).

Khi ứng dụng của bạn giao tiếp với một Distributed Cache như Redis, dữ liệu phải đi qua một hành trình dài:

[App Process] 
     │ (1) Serialize dữ liệu thành JSON/Binary
     ▼
[Network Stack (OS)] 
     │ (2) Đóng gói gói tin TCP/IP
     ▼
[Network Cable / Switch / Virtual Network] (Độ trễ vật lý)
     ▼
[Redis Server Network Stack]
     │ (3) Giải nén gói tin TCP
     ▼
[Redis Process (RAM)] 
     │ (4) Tìm kiếm Key-Value
     ▼
[Hành trình ngược lại để trả dữ liệu về App]

Mỗi bước trong hành trình này đều thêm vào một khoảng trễ nhỏ (từ 1ms đến 5ms tùy thuộc vào khoảng cách vật lý giữa server ứng dụng và server Redis). Đối với các hệ thống lớn xử lý hàng vạn request mỗi giây, khoảng trễ này là hoàn toàn xứng đáng để giảm tải cho Database. Nhưng với các ứng dụng quy mô vừa và nhỏ, việc chịu thêm chi phí Network RTT (Round-Trip Time) và chi phí CPU cho việc tuần tự hóa dữ liệu (serialization) chỉ để lấy ra một chuỗi JSON tĩnh là một sự đánh đổi vô lý.

Tư duy kỹ sư thực dụng đòi hỏi chúng ta phải đặt câu hỏi: Chúng ta có thực sự cần một server trung gian để nhớ dữ liệu này không, hay bản thân máy chủ chạy ứng dụng đã tự nhớ được rồi?


4. Phân tích chi tiết 3 loại Caching phổ biến

Để đưa ra quyết định chính xác, chúng ta cần mổ xẻ ưu và nhược điểm của 3 giải pháp cache phổ biến nhất hiện nay.

4.1. In-memory Cache (Local Cache) - Tốc độ ánh sáng trong RAM của ứng dụng

In-memory cache (hay Local Cache) là hình thức lưu trữ dữ liệu trực tiếp trong không gian bộ nhớ RAM của chính tiến trình (process) đang chạy ứng dụng.

  • Cơ chế hoạt động: Ứng dụng sử dụng các cấu trúc dữ liệu dạng bảng băm (Hash Map) có sẵn của ngôn ngữ lập trình hoặc tích hợp các thư viện chuyên dụng như Caffeine/Guava (Java), Cachetools (Python), go-cache (Go), hoặc các class Singleton tự viết.
  • Ưu điểm:
    • Tốc độ tuyệt đối: Thời gian truy xuất dữ liệu cực kỳ nhanh (thường dưới một microsecond đến dưới 1ms) vì tiến trình ứng dụng đọc trực tiếp từ bộ nhớ RAM của chính nó thông qua con trỏ bộ nhớ. Không có chi phí truyền tải qua mạng, không cần chuyển đổi dữ liệu (serialization/deserialization).
    • Không tốn chi phí hạ tầng: Hoàn toàn miễn phí, không cần cài đặt hay duy trì bất kỳ dịch vụ hoặc server phụ nào.
  • Nhược điểm:
    • Vòng đời ngắn (Volatility): Dữ liệu cache sẽ biến mất hoàn toàn ngay khi tiến trình ứng dụng bị khởi động lại (restart để deploy code mới, hoặc bị OS tắt do thiếu tài nguyên).
    • Giới hạn bộ nhớ: Dung lượng cache bị ràng buộc bởi lượng RAM trống cấp phát cho tiến trình ứng dụng. Lưu quá nhiều dữ liệu có thể dẫn đến hiện tượng tràn bộ nhớ.
    • Không thể chia sẻ trạng thái (Isolation): Khi ứng dụng scale ngang (chạy nhiều node đằng sau Load Balancer), mỗi node sẽ có một vùng nhớ RAM độc lập. Dữ liệu cache ghi ở Node A thì Node B không thể đọc được.
  • Chính sách thu hồi (Eviction Policy): Vì tài nguyên RAM có hạn, Local Cache bắt buộc phải sử dụng các thuật toán như LRU (Least Recently Used) hoặc LFU (Least Frequently Used) để tự động xóa bớt dữ liệu cũ, nhường chỗ cho dữ liệu mới khi bộ nhớ đạt ngưỡng giới hạn.

4.2. File-based Cache - Lựa chọn bị lãng quên của thời đại SSD/NVMe

File-based cache là phương pháp serialize dữ liệu thành các file vật lý (thường là định dạng JSON, XML, hoặc Binary) và lưu trữ chúng trực tiếp trên ổ đĩa cứng của máy chủ chạy ứng dụng.

  • Cơ chế hoạt động: Khi ứng dụng truy vấn dữ liệu phức tạp từ database, thay vì giữ nó trên RAM, hệ thống ghi kết quả vào một file trong thư mục cache tạm thời (ví dụ: /tmp/cache/recent_posts.json). Request tiếp theo chỉ cần đọc trực tiếp file này.
  • Ưu điểm:
    • Tiết kiệm RAM: Giải phóng hoàn toàn RAM cho các tác vụ tính toán quan trạng khác của ứng dụng.
    • Bền bỉ (Persistence): Dữ liệu cache không bị mất đi khi ứng dụng restart hoặc crash.
    • Cực kỳ rẻ và dễ triển khai: Phù hợp với các dịch vụ hosting giá rẻ hoặc các VPS cấu hình thấp không có nhiều RAM để chạy Redis.
  • Nhược điểm:
    • Tốc độ trung bình: Đọc/ghi dữ liệu từ ổ đĩa (I/O Disk) luôn chậm hơn rất nhiều so với RAM. Tuy nhiên, trong thời đại của các ổ cứng SSD NVMe tốc độ cao (tốc độ đọc lên tới vài GB/s), tốc độ của File cache vẫn nhanh hơn gấp nhiều lần so với việc thực hiện các câu lệnh SQL JOIN phức tạp trên Database.
    • Độ phức tạp khi đồng bộ: Tương tự như Local Cache, File cache nằm cục bộ trên từng server. Nếu muốn chia sẻ file cache giữa các node, bạn phải sử dụng các giải pháp ổ đĩa mạng (như NFS, AWS EFS), điều này lại vô tình kéo theo độ trễ mạng và rủi ro lỗi đồng bộ file khóa (file locking issues).

4.3. Distributed Cache (Cache phân tán) - Trí nhớ tập thể của cụm Server

Distributed cache là một hệ thống lưu trữ cache độc lập, chạy trên một hoặc nhiều máy chủ chuyên dụng riêng biệt, cho phép toàn bộ cụm server ứng dụng kết nối và chia sẻ dữ liệu thông qua giao thức mạng TCP.

  • Cơ chế hoạt động: Các công cụ phổ biến như Redis hoặc Memcached đóng vai trò là một cơ sở dữ liệu key-value siêu tốc trong RAM. Các node ứng dụng gửi request truy xuất dữ liệu qua mạng TCP.
  • Ưu điểm:
    • Đồng nhất dữ liệu (Consistency): Toàn bộ các node ứng dụng đều nhìn thấy duy nhất một nguồn dữ liệu cache chung. Node A cập nhật dữ liệu, Node B lập tức đọc được dữ liệu mới.
    • Khả năng scale độc lập: Bạn có thể scale dung lượng cache (thêm RAM cho Redis cluster) mà không cần phải scale server ứng dụng, giúp tối ưu hóa việc phân bổ tài nguyên.
    • Tính năng nâng cao và độ bền cao: Hỗ trợ lưu trữ bền vững xuống đĩa (như cơ chế AOF/RDB của Redis), hỗ trợ các cấu trúc dữ liệu phức tạp (Lists, Sets, Sorted Sets) giúp thực hiện các logic nghiệp vụ nâng cao ngay trên tầng cache.
  • Nhược điểm:
    • Độ trễ mạng (Network Overhead): Chậm hơn In-memory cache do phải đi qua môi trường mạng và thực hiện serialization.
    • Chi phí cao và phức tạp: Cần kỹ năng quản trị hệ thống để thiết lập cấu hình Cluster, Sentinel (High Availability) nhằm tránh tình trạng sập toàn bộ hệ thống khi Redis server bị chết (Single Point of Failure). Chi phí thuê cloud cho các cụm Redis HA là rất lớn.

5. Bản đồ I/O và Mô hình kiến trúc

Để hình dung rõ hơn sự khác biệt về ranh giới vật lý và đường đi của dữ liệu giữa 3 loại cache này, hãy quan sát sơ đồ kiến trúc dưới đây:

                     +----------------------------------------+
                     |             APPLICATION SERVER         |
                     |                                        |
                     |  +----------------------------------+  |
                     |  |         Application Process      |  |
                     |  |                                  |  |
                     |  |  +----------------------------+  |  |
                     |  |  |    In-Memory Cache (RAM)   |  |  |
                     |  |  |    (Latency: <1 microsecond) |  |  |
                     |  |  +-------------+--------------+  |  |
                     |  |                |                 |  |
                     |  |                | Đọc/Ghi trực tiếp  |
                     |  |                v                 |  |
                     |  |  +-------------+--------------+  |  |
                     |  |  |   Core Application Logic   |  |  |
                     |  |  +------+--------------+------+  |  |
                     |  +---------|--------------|---------+  |
                     |            |              |            |
                     |    Đọc/Ghi |      Đọc/Ghi |            |
                     |    File    |      qua TCP |            |
                     |    (Local) |      Socket  |            |
                     |            v              |            |
                     |  +---------+----+         |            |
                     |  |  File Cache  |         |            |
                     |  | (Local SSD)  |         |            |
                     |  | (5-20ms)     |         |            |
                     |  +--------------+         |            |
                     +---------------------------|------------+
                                                 | (Mạng Nội Bộ - Intranet)
                                                 v (Latency: 1-5ms)
                                       +---------+----------+
                                       |  Distributed Cache |
                                       |   (Redis Cluster)  |
                                       +--------------------+

6. Giải pháp nâng cao: Multi-level Caching (Kiến trúc Cache L1/L2)

Trong thực tế thiết kế các hệ thống lớn có lượng truy cập cực khủng (như mạng xã hội, trang thương mại điện tử vào ngày flash sale), các kỹ sư cấp cao thường không chọn một loại cache duy nhất. Thay vào đó, họ kết hợp chúng thành một mô hình Kiến trúc Cache nhiều lớp (Multi-level Caching), tương tự như cơ chế cache L1, L2, L3 trên CPU máy tính.

  • Lớp 1 (L1 Cache - In-memory): Nằm ngay trên RAM của ứng dụng. Lớp này lưu trữ các dữ liệu cực kỳ "nóng" (hot keys) và có kích thước nhỏ với TTL rất ngắn (ví dụ: 10 giây). Lớp này giúp chặn đứng 90% lượng request ngay tại server ứng dụng mà không cần gửi truy vấn qua mạng.
  • Lớp 2 (L2 Cache - Distributed Cache / Redis): Đóng vai trò là nguồn dữ liệu chuẩn (Source of Truth) cho lớp L1. Khi lớp L1 bị cache miss hoặc hết hạn, ứng dụng sẽ truy vấn tới lớp L2. Lớp L2 lưu trữ dữ liệu với TTL dài hơn (ví dụ: 30 phút).

Dưới đây là một đoạn code mô phỏng logic truy xuất dữ liệu theo mô hình Multi-level Caching bằng Python (dành cho mục đích minh họa kiến trúc):

import time

class MultiLevelCache:
    def __init__(self, local_cache_client, redis_client, db_client):
        self.local_cache = local_cache_client  # L1: In-memory
        self.redis = redis_client              # L2: Distributed
        self.db = db_client                    # Database gốc

    def get_data(self, key):
        # 1. Thử đọc từ L1 Cache (In-memory) - Tốc độ cực nhanh
        data = self.local_cache.get(key)
        if data is not None:
            return data, "HIT_L1"

        # 2. L1 Miss -> Thử đọc từ L2 Cache (Redis) - Đi qua mạng
        data = self.redis.get(key)
        if data is not None:
            # Ghi ngược lại L1 với TTL ngắn để phục vụ các request tiếp theo
            self.local_cache.set(key, data, ttl=10)
            return data, "HIT_L2"

        # 3. Cả L1 và L2 đều Miss -> Đọc trực tiếp từ Database
        data = self.db.query_by_key(key)
        if data is not None:
            # Đồng bộ lại dữ liệu vào cả L2 (TTL dài) và L1 (TTL ngắn)
            self.redis.set(key, data, ttl=1800) # 30 phút
            self.local_cache.set(key, data, ttl=10) # 10 giây

        return data, "HIT_DB"

Mô hình này giúp giảm thiểu tối đa tải trọng mạng lên cụm Redis, đồng thời bảo vệ hệ thống khỏi hiện tượng Cache Stampede (nhiều request đồng thời đổ dồn vào database khi cache bị hết hạn cùng một lúc).


7. Ma trận quyết định kiến trúc (Decision Matrix)

Để giúp bạn dễ dàng đưa ra quyết định khi thiết kế hệ thống, hãy tham khảo ma trận so sánh dưới đây:

Tiêu chí In-memory Cache (L1) File-based Cache Distributed Cache (L2)
Tốc độ phản hồi (Latency) 🟢 Cực nhanh (<1ms) 🟡 Trung bình (5-20ms) 🟡 Nhanh (1-5ms)
Chi phí hạ tầng 🟢 Miễn phí (Dùng RAM app) 🟢 Rất rẻ (Dùng ổ đĩa app) 🔴 Đắt (Cần server riêng)
Khả năng Scale ngang 🔴 Kém (Không tự đồng bộ) 🔴 Kém (Bị cô lập trên ổ đĩa) 🟢 Xuất sắc (Cụm phân tán)
Độ phức tạp vận hành 🟢 Rất thấp 🟢 Thấp 🔴 Cao (Cần cấu hình HA, Cluster)
Độ bền dữ liệu khi Restart 🔴 Mất sạch 🟢 Còn nguyên 🟢 Hỗ trợ cấu hình lưu trữ đĩa
Trường hợp sử dụng phù hợp Dữ liệu tĩnh, cấu hình hệ thống, ứng dụng đơn node Website tĩnh, blog cá nhân, tài nguyên media ít cập nhật Session chung, giỏ hàng thương mại điện tử, hệ thống microservices

8. Những kịch bản lỗi chí mạng (Failure Cases)

Khi áp dụng các giải pháp cache cục bộ (In-memory) hoặc bỏ qua các nguyên tắc quản lý bộ nhớ, hệ thống của bạn sẽ đối mặt với những thảm họa thực tế sau:

Kịch bản 1: Sập máy chủ do cạn kiệt RAM (Out of Memory - OOM)

Đây là lỗi phổ biến nhất của các kỹ sư junior khi sử dụng In-memory cache. Họ viết một hàm lưu trữ kết quả tìm kiếm của khách hàng vào một Map tĩnh trong bộ nhớ.

// LỖI CHÍ MẠNG: Map này sẽ phình to vô hạn theo thời gian
var searchCache = make(map[string]SearchResult)

Khi ứng dụng chạy trong môi trường production có hàng triệu lượt tìm kiếm với các từ khóa khác nhau, Map này sẽ phình to không ngừng. Do không thiết lập dung lượng tối đa (Max Size) hoặc không có chính sách giải phóng các key cũ (Eviction Policy), tiến trình ứng dụng nhanh chóng ngốn sạch RAM của server vật lý. Hậu quả là OS kernel sẽ kích hoạt trình OOM Killer để tắt đột ngột tiến trình ứng dụng, gây gián đoạn dịch vụ hàng loạt.

Kịch bản 2: Dữ liệu "nhảy múa" do mất đồng bộ cache cục bộ (Cache Incoherence)

Hãy tưởng tượng bạn chạy ứng dụng với 3 instances (Node A, Node B, Node C) phía sau một Load Balancer chạy thuật toán Round-Robin. Bạn sử dụng In-memory cache để lưu trữ thông tin cấu hình khuyến mãi của sản phẩm (giảm giá 50%).

[Request 1] ---> [Load Balancer] ---> [Node A (Đã cache giảm giá 50%)] ---> Hiển thị: 50.000đ
[Request 2] ---> [Load Balancer] ---> [Node B (Chưa cập nhật cache - Giá gốc)] -> Hiển thị: 100.000đ
[Request 3] ---> [Load Balancer] ---> [Node C (Đã cache giảm giá 50%)] ---> Hiển thị: 50.000đ

Khi admin thay đổi giá bán sản phẩm, Node A nhận được lệnh cập nhật và xóa cache cục bộ để nạp lại giá mới. Tuy nhiên, Node B và Node C hoàn toàn không biết gì về sự thay đổi này và vẫn tiếp tục phục vụ dữ liệu cũ từ bộ nhớ RAM của chúng. Người dùng liên tục nhấn nút F5 làm request đi qua các node khác nhau, dẫn tới trải nghiệm kỳ quặc: giá sản phẩm lúc thì hiển thị 50.000đ, lúc lại biến thành 100.000đ. Đây là hậu quả trực tiếp của việc sử dụng local cache cho dữ liệu động trong một kiến trúc phân tán scale ngang.


9. Lời kết & Nhịp nối

Thiết kế hệ thống không phải là cuộc đua xem ai dùng công nghệ thời thượng hay đắt tiền hơn. Đó là nghệ thuật của sự cân bằng và thấu hiểu bối cảnh. Đừng để cái bóng của Redis che khuất các giải pháp đơn giản, hiệu quả và rẻ tiền hơn như In-memory cache hay File-based cache khi bạn chỉ cần giải quyết những bài toán ở quy mô nhỏ hoặc dữ liệu có độ động thấp.

Hãy luôn ghi nhớ:

  • Chọn In-memory cache khi cần tốc độ tối đa, dữ liệu nhỏ và chấp nhận mất khi restart.
  • Chọn File-based cache khi muốn tiết kiệm RAM, dữ liệu ít thay đổi và chạy trên hạ tầng chi phí thấp.
  • Chọn Distributed cache (Redis/Memcached) khi chạy hệ thống lớn, cần đồng bộ dữ liệu giữa nhiều node server hoặc cần các cấu trúc dữ liệu nâng cao.

Tuy nhiên, việc chọn đúng công cụ cache mới chỉ là bước khởi đầu. Caching giống như một liều thuốc giảm đau tức thời cho các vấn đề về latency và database bottleneck. Nhưng thuốc bổ dùng sai chỗ có thể trở thành thuốc độc. Có những loại dữ liệu và những kịch bản nghiệp vụ đặc thù mà việc đưa chúng vào cache – dù chỉ trong vòng 1 giây – cũng có thể dẫn tới những thảm họa nghiêm trọng về tài chính hoặc vi phạm bảo mật dữ liệu.

Ở tập tiếp theo, chúng ta sẽ cùng phân tích những vùng cấm kỵ này trong bài viết: "Khi Nào KHÔNG Nên Cache – Khi Độ Nhanh Trở Thành Con Dao Hai Lưỡi".


🎯 Dành cho những Developer muốn đi xa hơn

Viết được tính năng chỉ là điểm khởi đầu.

Khi hệ thống ngày càng lớn, những bài toán về hiệu năng, tính đúng đắn của dữ liệu, khả năng mở rộng và các trade-off trong kiến trúc mới là điều tạo nên sự khác biệt giữa một Developer và một System Engineer.

Nếu bạn muốn tiếp tục khám phá những chủ đề đó, hãy tham gia cùng TechCraft thông qua Dev Insider.

🚀 Dev Insider https://www.patreon.com/techcraft_official/posts/vi-sao-dev-ra-161163881?collection=2220113

📘 Facebook https://www.facebook.com/techcraft.official

🎥 YouTube https://www.youtube.com/@techcraft.official

🎵 TikTok https://www.tiktok.com/@techcraft.official

Build Systems. Not Just Features.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí