Distributed Caching – Một sự lựa chọn hoàn hảo về caching cho microservice

Distributed Caching – Một sự lựa chọn hoàn hảo về caching cho microservice

December 28, 2021 1 By Nam Vu

Bài viết trước mình có đề cập đến In-memory Caching và cách sử dụng nó với net core, nhưng phải nhấn mạnh một điều là In-memory caching chỉ thực sự hữu dụng khi ứng dụng của bạn được deploy trên một server thôi nhé. 

Nếu bạn có ý định triển khai ứng dụng trên nhiều servers hoặc microservices thì bạn cần nghĩ tới giải pháp tập trung caching lại một service. 

Đó chính là giải pháp mà bài viết này mình sẽ đi chi tiết ngay dưới đây. Distributed Caching!

Distributed Caching là gì?

Distributed cache được chia sẻ bởi nhiều app servers. Thông tin trong cache không được lưu trong bộ nhớ của một web server riêng biệt và dự liệu được cache là có sẵn trong tất cả các server của ứng dụng. 

Distributed cache có thể cải thiện một cách đáng kể khả năng phản hồi của ứng dụng, từ đó data có thể được nhận từ cache sẽ nhanh hơn nhiều so với relational database (hoặc web service).

Distributed cache cung cấp dữ liệu giống nhau và nhất quán trên nhiều servers hay services nếu một server bị trục trặc hay khởi động lại thì dữ liệu được lưu trữ vẫn còn nằm trên những server khác.

Ngoài ra nó còn có thể mở rộng được (scalability). Ví dụ khi bạn triển khai mô hình microservices bạn muốn caching dữ liệu lại trên mỗi cluster, bạn có thể dễ dàng mở rộng các dung lượng size hoặc gắn thêm nhiều node mới cho service caching này của bạn. Khắc phục được hạn chế dung lương của memory cache.

Ưu điểm của distributed caching?

Có lẽ Distributed caching sinh ra để khắc phục những nhược điểm vốn có của In-memory caching. Cụ thể là:

– Liên lạc (Communicate): Dữ liệu trong cache được liên lạc trên tất cả các services. Người dùng không thấy sự khác biệt kết quả cho dù service nào xử lý request của họ. Ngoài ra nó còn là một kênh giao tiếp thông tin trong microservices nữa.

– Sẵn sàng (Availability): Sử dụng cache còn giúp đỡ về tính sẵn sàng cho toàn hệ thống. Dữ liệu trong cache vẫn tồn tại khi bất kỳ service restart và deployments. 

– Mở rộng (Scalability) Một service riêng biệt có thể gỡ bỏ hoặc thêm mới vào mà không ảnh hưởng đến cache service. Không những đáp ứng được về khả năng mở rộng, Distributed cache còn có thể mở rộng nhanh chóng, không ảnh hưởng về mặt tính năng

– Hiệu năng (Performance): Không thể phủ nhận là khi sử dụng cache, phần hiệu năng cho end user được tăng lên một cách rõ rệt. Trải nghiệm tuyệt vời hơn, không còn thời gian chờ đợi. Ngoài ra distributed caching còn giữ nguyên tất cả các lợi ích của In-memory caching như: giảm thiểu connection tới database, tăng tốc độ xử lý, cải thiện hiệu suất…

Vậy nhược điểm của distributed caching thì sao?

– Chi phí (Cost): Có lẽ đây là vấn đề đầu tiên được nhắc đến. Khi xây dựng một service tổng hợp caching riêng biệt thì chi phí xây dựng và vận hành nó cũng phải cân nhắc đầu tiên. Tuy nhiên chi phí bỏ ra để xây dựng hoặc thuê các cloud service sẽ giúp ứng dụng của bạn linh hoạt và tiện dụng hơn.

– Thời gian trao đổi (Response time): Tuy rằng về hiệu suất Distributed cache vẫn có khả năng đáp ứng cao nhưng khi so với In-memory cache nó vẫn chậm hơn vì bạn phải dùng 1 cache server riêng biệt thông qua một số giao thức đặc biệt để lấy và trao đổi dữ liệu cho service trước khi trả cho request. 

– Triển khai (Deployment): Nếu bạn tự xây dựng một service cache thì bạn cũng tốn khá nhiều công sức để triển khai vận hành nó. Còn nếu bạn sử dụng hàng xây dựng sẵn bạn cũng phải setup tích hợp với cloud service như Azure, AWS, Google (Mình sẽ có bài viết khác viết về Distributed cache in cloud). Mà mỗi dịch vụ của bên thứ ba này có cấu hình khác nhau, bạn phải xây dựng một ứng dụng linh hoạt để có thể thay đổi khi thay đổi bên thứ ba.

Quản lý distributed caching

Thông thường thì hệ thống sẽ đánh dấu thời điểm hết hạn (expire time) của dữ liệu, nếu quá thời hạn thì cache sẽ bị xóa đi như In-memory cache mình đã đề cập ở bài viết trước.

Tuy nhiên đối với Distributed cache thì bạn sẽ xử lý tốt hơn khi tính toán expire time của cache và nó cũng là một bài toán khá đau đầu tùy vào logic của hệ thống đang phát triển.

Có 6 cache xóa cache cơ bản sau:

– First In First Out (FIFO): Các dữ liệu được ghi vào cache trước thì sẽ được ưu tiên xóa đi trước mà không quan tâm tới tần suất hay số lượng truy cập của nó.

– Last In First Out (LIFO): Sẽ ưu tiên xóa những dữ liệu được ghi cũ nhất mà không quan tâm tới tần suất hay số lượng truy cập của nó.

– Least Recently Used (LRU): Sẽ xóa những dữ liệu ít được truy cập

– Most Recently Used (MRU): Sẽ xóa những dữ liệu được truy cập thường xuyên  gần đây nhất.

– Least Frequently Used (LFU): Sẽ xóa những dữ liệu ít được truy cập nhất.

– Random Recently (RR): Sẽ chọn ra một dữ liệu bất kỳ trên cache để thực hiện thao tác xóa.

Các loại distributed caching trong net core

Thực tế distributed caching chỉ là một ý tưởng chung và nó có rất nhiều các bên thứ ba thực hiện hóa nó. Và mỗi ngôn ngữ hay framework cũng đều có những thư viên hỗ trợ vấn đề thực hiện hóa này. 

Đối với dotnet nói riêng thì mình biết 3 thư viện hỗ trợ cho 3 loại distributed caching khác nhau và tất nhiên nó cũng có ưu và nhược điểm riêng của nó. 

– SQL Server distributed cache: Package Microsoft.Extensions.Caching.SqlServer

– Redis distributed cache: Package Microsoft.Extensions.Caching.StackExchangeRedis

– NCache distributed cache: Package NCache.Microsoft.Extensions.Caching.OpenSource

Mình sẽ hẹn bạn chi tiết sử dụng từng loại thư viện ở một bài viết khác. Với bài viết này mình sẽ nói cụ thể về thư viện thứ 2 bên trên để làm ví dụ minh họa nhé. Và đây cũng là một trong những loại distributed caching phổ biến nhất hiện nay.

Cách sử dụng distributed cache

Trước khi bắt đầu đi cụ thể về Redis distributed cache thì mình đi sơ qua một interface hữu dụng để làm việc khi làm việc với distributed cache trong dot net core.

Để thực hiện hóa bất kỳ loại nào bên trên thì dotnet đã trừu tượng hóa một interface để có thể tương tác linh hoạt dễ dàng cho anh em lập trình viên. Đó chính là IDistributedCache được tích hợp với một package common là Microsoft.Extensions.Caching.Distributed, package này cung cấp các phương thức chính như:

– Get(), GetAsync(): Với tham số là một key kiểu string và nhận về một item đã được cache như một mảng byte[] nếu được tìm thấy trong cache.

– Set(), SetAsync(): Thêm một item (mảng byte[]) tới cache sử dụng một string key.

– Refresh(), RefreshAsync(): Làm mới một item trong cache dựa trên key của nó, đặt lại thời hạn của timeout.

– Remove(), RemoveAsync(): Gỡ bỏ một item cache dựa trên key của nó.

Với interface này bạn có thể tung hoành thực hiện ý tưởng của mình với distributed cache nhé.Để có thể đi tiếp thì bạn lại phải hiểu Redis là gì và Redis cache là gì thì mới có thể sử dụng được nó một cách hiệu quả.

Redis cache là gì?

Redis là một database open-source in-memory, cái mà thường xuyên được sử dụng như một distributed cache. Đừng nghĩ redis chỉ sử dụng làm caching nhé, nó còn có thể làm message bus, pub/sub (Mình cũng sẽ có bài viết khác nói về chủ đề này) hay sử dụng làm distributed lock request (Đó chính là redlock, mình cũng sẽ có bài viết riêng nói về chủ đề này).

Redis được viết bằng ngôn ngữ C cung cấp các kiểu data như strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes, và streams (Đừng chủ quan với các kiểu data type này vì nó chính là cái bạn sử dụng để lưu trữ đó. Cụ thể từng datatype thì bạn có thể đọc thêm ở trang chủ redis nhé).

Về mặt cơ bản thì nó là một dạng kiểu key-value (key là khóa để bạn có thể truy vấn và nó là duy nhất, value là dữ liệu bạn lưu trữ với datatype tương ứng ở trên). 

Nó cũng được sử dụng trong nhiều website lớn như Twitter, Github, Pinterest, Snapchat, Flickr, Digg, Stackoverflow… Đặc biệt hơn nữa là nó có thể sử dụng trên rất nhiều ngôn ngữ phổ biết khác nhau (Hiện tại là 54 ngôn ngữ)

Một số nhà phát triển cloud cũng đã cung cấp các dịch vụ hỗ trợ redis trên dịch vụ đám mấy của họ để có thể dễ dàng triển khai trên nền tàng tương ứng như Azure Cache for RedisAmazon ElastiCache for Redis và Memorystore

Ở bài viết này mình sẽ hướng dẫn các bạn build redis local để bạn có thể hiểu bản chất vấn đề, còn thực tế bạn dùng dịch vụ bên thứ ba nào thì có thể triển khai tương tự.

Có 2 cách setup redis trên local đó là:

– Cài đặt Redis Server, bạn download trên trang chủ và cài đặt bình thường với hệ điều hành tương ứng. Sau khi download về thì bạn chạy redis-server.exe đây chính là cache service mà bạn sử dụng làm server lưu trữ tập trung cache data của bạn. Khi ứng dụng này tắt nghĩa là cache server của bạn ngừng hoạt động và bạn sẽ mất hết dữ liệu trong cache. Cẩn thận khi dùng cách host trực tiếp này nhé!
Để test lưu và truy vấn dữ liệu thì trong file zip bạn download được có chưa cả file redis-cli.exe, đây chỉnh là CLI dùng để thao tác với redis phía client thông qua câu lệnh.  Khi chạy nó lên thì nó sẽ kết nối với cache server trên để giúp bạn test hoặc truy vấn giá trị của cache.

– Cách thứ 2 là dùng docker, vì cách đầu tiên mình không nghĩ nó hữu dụng khi deploy do luôn phải đảm bảo nó running và không thể tắt bật tắt bật nếu không muốn dữ liệu của bạn bị mất. Redis có hỗ trợ docker image và mình có tạo sẵn file docker-composer trong demo của mình, bạn có thể lấy sử dụng. Việc của bạn là chỉ cần chạy câu lệnh này để host redis image lên trên docker mà thôi


docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d --build redis

Khi đã có Redis cache service rồi, để thao tác với dữ liệu bằng GUI thì bạn sử dụng tool redis-desktop-manager hoặc bản portable mình cũng để trong source code demo

Đến đây bạn có thể kết nỗi và check xem redis local của bạn đã thực sự hoạt động bằng cách connect qua tool nhé!

Address: 127.0.0.1
Port: 6379

Tích hợp Redis Cache với AspNet Core

Khi đã có cache service rồi, đảm bảo nó chạy thành công rồi thì bạn có thể sẵn sàng tích hợp với ứng dụng của bạn.

Tương tự như các bước sử dụng của In-memory cache thì Redis cache tích hợp cũng khá đơn giản.

Bước 1: Thêm connection vào appseting.json

"RedisCacheServerUrl": "127.0.0.1:6379",

Bước 2: Config connection trong startup.cs

services.AddStackExchangeRedisCache(options =>
            {
                options.Configuration = Configuration["RedisCacheServerUrl"];
            });

Bước 3: Sử dụng dependence injection để inject IDistributedCache

public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
    private readonly IDistributedCache _cache;
    private readonly DataContext _dataContext;
    
    public IndexModel(ILogger<IndexModel> logger, IDistributedCache cache, DataContext dataContext)
    {
        _logger = logger;
        _cache = cache;
        _dataContext = dataContext;
    }
}

Bước 4: Set hoặc SetAsync data cho cache bao gồm key và value

// Add data in cache
await _cache.SetAsync(cacheKey, newDataToCache, options);

Bước 5: Lấy dữ liệu từ cache ta dùng phương thức Get hoặc GetAsync

// Get data from cache
var cachedData = await _cache.GetAsync(cacheKey);

Bước 6: Sử dụng Refresh hoặc RefreshAsync nếu cần cập nhật giá trị

Bước 7: Sử dụng Remove hoặc RemoveAsync nếu cần xóa cache nào đó

Cũng khá đơn giản phải không nào, còn về các options setting như AbsoluteExpiration và SlidingExpiration thì tương tự như In-memory cache bạn có thể đọc lại bài viết trước để hiểu nó là gì và khi nào sử dụng option này.

// set cache options
var options = new DistributedCacheEntryOptions()
             .SetAbsoluteExpiration(DateTime.Now.AddMinutes(2))
             .SetSlidingExpiration(TimeSpan.FromMinutes(1));

Nếu bạn lưu data vào cache dưới dạng json thì sử dụng hàm SerializeObject và DeserializeObject khi đưa vào và lấy ra sử dụng.

var cacheKey = "GET_ALL_PRODUCTS";
List<Product> products = new List<Product>();


// Get data from cache
var cachedData = await _cache.GetAsync(cacheKey);
if (cachedData != null)
{
    // If data found in cache, encode and deserialize cached data
    var cachedDataString = Encoding.UTF8.GetString(cachedData);
    products = JsonConvert.DeserializeObject<List<Product>>(cachedDataString);
}
else
{
    // If not found, then fetch data from database
    products = await _dataContext.Products.ToListAsync();


    // serialize data
    var cachedDataString = JsonConvert.SerializeObject(products);
    var newDataToCache = Encoding.UTF8.GetBytes(cachedDataString);


    // set cache options
    var options = new DistributedCacheEntryOptions()
        .SetAbsoluteExpiration(DateTime.Now.AddMinutes(2))
        .SetSlidingExpiration(TimeSpan.FromMinutes(1));


    // Add data in cache
    await _cache.SetAsync(cacheKey, newDataToCache, options);
}

Cũng khá dễ dàng đúng không nào!

Trên đây là những kiến thức cơ bản về Distributed Cache và ví dụ minh họa với Redis Cache. Hi vọng bài viết có thể giúp ích được cho bạn phần nào trong quá trình sử dụng.

Theo dõi mình để đọc thêm những bài viết hay ho khác nhé!

#ntechdevelopers