Khả năng mở rộng cho người mới - Phần 3: Bộ nhớ đệm (Cache)
Vấn đề còn lại
Sau khi làm theo Phần 2, giờ bạn đã có một giải pháp database mở rộng được. Bạn không còn sợ phải lưu hàng terabyte nữa, và thế giới trông thật đẹp đẽ.
Nhưng chỉ đẹp với riêng bạn thôi. Người dùng của bạn vẫn phải chịu đựng những trang tải chậm mỗi khi có nhiều dữ liệu cần lấy từ database.
Giải pháp là triển khai một cache (bộ nhớ đệm).
Cache là gì, và đặt ở đâu
Khi nói "cache", tôi luôn muốn nói tới in-memory cache (bộ đệm nằm trong RAM) như Memcached hoặc Redis.
Xin đừng bao giờ dùng cache dựa trên file. Nó khiến việc nhân bản (cloning) và tự động mở rộng (auto-scaling) server của bạn trở thành cực hình.
Quay lại với in-memory cache. Cache là một kho key-value đơn giản, và nó nên nằm như một tầng đệm giữa ứng dụng và kho dữ liệu của bạn.
Bất cứ khi nào ứng dụng cần đọc dữ liệu, trước hết nó phải thử lấy dữ liệu từ cache. Chỉ khi không có trong cache thì mới đi lấy từ nguồn dữ liệu chính.
Tại sao phải làm vậy
Vì cache nhanh như chớp. Nó giữ mọi tập dữ liệu trong RAM và xử lý request nhanh nhất mức kỹ thuật cho phép.
Ví dụ, Redis chạy trên một server tiêu chuẩn có thể thực hiện vài trăm nghìn thao tác đọc mỗi giây. Ghi cũng vậy, đặc biệt là phép tăng giá trị (increment), nhanh vô cùng.
Đây vẫn là mô hình cache được dùng phổ biến nhất. Mỗi khi bạn chạy một truy vấn tới database, bạn lưu tập kết quả vào cache. Khóa cache là một bản băm (hash) của chính câu truy vấn đó. Lần sau chạy lại truy vấn này, bạn kiểm tra cache trước xem đã có kết quả sẵn chưa.
Mô hình này có vài vấn đề. Vấn đề chính là chuyện hết hạn (expiration).
Rất khó xóa một kết quả đã cache khi bạn cache một truy vấn phức tạp - mà ai chẳng có truy vấn phức tạp? Khi một mẩu dữ liệu thay đổi (ví dụ một ô trong bảng), bạn phải xóa tất cả các truy vấn đã cache có thể có chứa ô đó. Bạn hiểu vấn đề rồi chứ?
Mô hình 2 - Cache đối tượng (Cached Objects)
Đây là mô hình tôi hết sức khuyến nghị và luôn ưu tiên dùng.
Nói chung, hãy nhìn dữ liệu của bạn như một đối tượng, đúng như cách bạn vẫn làm trong code (class, instance, v.v.). Để class của bạn lắp ráp một tập dữ liệu từ database, rồi lưu toàn bộ instance của class đó, hoặc tập dữ liệu đã lắp ráp xong, vào cache.
Nghe có vẻ lý thuyết, tôi biết, nhưng cứ nhìn vào cách bạn code bình thường:
Bạn có một class tên Product, trong đó có thuộc tính data. Nó là một mảng chứa giá, mô tả, hình ảnh và đánh giá của khách hàng về sản phẩm. Thuộc tính data được lấp đầy bởi nhiều phương thức trong class, mỗi phương thức chạy vài truy vấn database - những truy vấn rất khó cache, vì nhiều thứ liên quan chằng chịt với nhau.
Giờ hãy làm thế này: khi class của bạn lắp ráp xong mảng dữ liệu, hãy lưu thẳng mảng đó - hoặc tốt hơn nữa, lưu cả instance của class - vào cache!
Cách này cho phép bạn dễ dàng vứt bỏ đối tượng mỗi khi có gì đó thay đổi, và làm cho toàn bộ hoạt động của code vừa nhanh hơn vừa hợp logic hơn.
Mô hình 1 lưu mỗi câu hỏi thành một mảnh riêng, khó dọn khi dữ liệu đổi. Mô hình 2 lưu nguyên một object, đổi dữ liệu chỉ cần xóa đúng object đó.
Và phần hay nhất: xử lý bất đồng bộ
Nó mở đường cho xử lý bất đồng bộ (asynchronous processing)!
Hãy tưởng tượng một đội quân worker server chuyên đi lắp ráp các đối tượng giúp bạn. Ứng dụng chỉ việc tiêu thụ đối tượng đã cache mới nhất, và gần như không bao giờ phải chạm vào database nữa!
Vài gợi ý về đối tượng nên cache
session của người dùng (đừng bao giờ dùng database cho việc này!)
bài blog đã render hoàn chỉnh
luồng hoạt động (activity stream)
quan hệ bạn bè giữa các user
Lời cuối
Như bạn có lẽ đã nhận ra, tôi là một fan cuồng của caching. Nó dễ hiểu, cực kỳ đơn giản để triển khai, và kết quả thì lúc nào cũng làm người ta nín thở.
Nhìn chung tôi thích Redis hơn Memcached, vì tôi mê những tính năng thiên về database của Redis như tính bền vững (persistence) và các cấu trúc dữ liệu có sẵn như list và set. Với Redis cộng thêm cách đặt khóa khôn ngoan, biết đâu bạn còn có cơ hội bỏ luôn được database.
Nhưng nếu bạn chỉ cần cache thôi, hãy chọn Memcached, vì nó mở rộng ngọt như đường.