Note: see the link below for the English version of this article.
https://duongnt.com/stackexchangeredis
Đây là bài số 2 trong loạt 3 bài về distributed caching sử dụng Redis. Xin xem trang này để lấy đường link tới các phần còn lại.
Trong .NET Platform Extension có chứa một interface với tên gọi IDistributedCache. Interface này định nghĩa các hàm để cung cấp tính năng distributed caching cho dữ liệu đã được serialize. Ta có thể dùng interface này với Redis làm backend bằng cách sử dụng package Microsoft.Extensions.Caching.StackExchangeRedis. Ta cài package đó bằng lệnh sau.
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
Chú ý là package khác với package StackExchange.Redis.
Thiết lập kết nối
Đầu tiên, ta cần có một server Redis để chạy code trong bài hôm nay. Xin tham khảo phần trước để xem cách thiết lập Redis trong container Docker.
Nhờ StackExchangeRedis việc kết nối tới Redis trở nên rất đơn giản. Ta thiết lập địa chỉ tới server Redis bằng cách tạo đối tượng sau.
var configurationOptions = new ConfigurationOptions
{
EndPoints = { "localhost:6379" }, // Khác với khi dùng aioredis, ta không cần phần "redis://"
Ssl = false // Đổi giá trị này thành true nếu server Redis hỗ trợ SSL
};
ConfigurationOptions
còn chứa nhiều thiết lập khác, dưới đây là một số thiết lập đáng chú ý.
Password
: mật khẩu để đăng nhập vào Redis server.ConnectTimeout
: thời gian chờ tối đa khi kết nối tới Redis server.AsyncTimout
: thời gian tối đa cho một request không đồng bộ (async).SyncTimeout
: thời gian tối đa cho một request đồng bộ (sync).
Theo tôi, chúng ta nên đặt giá trị ConnectTimeout
và AsyncTimout/SyncTimeout
thấp. Bởi vì nếu Redis có sự cố thì việc đợi hết thời gian timeout mặc định (5 giây) sẽ khiến cả hệ thống trở nên quá chậm. Mà cache thì lẽ ra phải nhanh.
Sau đó, ta thêm service Redis vào tập các service của ứng dụng và tạo đối tượng IDistributedCache
như sau.
var serviceProvider = new ServiceCollection()
.AddStackExchangeRedisCache(options => options.ConfigurationOptions = configurationOptions)
.BuildServiceProvider();
var cache = serviceProvider.GetRequiredService<IDistributedCache>();
Bây giờ ta có thể dùng đối tượng cache
ở trên để tương tác với server Redis.
Dùng StackExchangeRedis để ghi dữ liệu vào Redis
Ta cần lưu ý là StackExchangeRedis không phải client Redis chuyên dụng cho C# .NET. Nó chỉ là implementation của IDistributedCache
với Redis là backend. Vì thế, nó không hỗ trợ hết các kiểu dữ liệu hay tính năng của Redis. StackExchangeRedis chỉ giúp ta đọc và ghi các byte array từ Redis. Ngoài ra, dù StackExchangeRedis có hàm Set
nhưng ta chỉ quan tâm tới hàm SetAsync
trong bài hôm nay.
Ta dùng hàm SetAsync
để ghi byte array vào Redis như sau.
var token = <code để tạo cancellation token> // có thể bỏ qua bước này nếu ta không cần hủy request
cacheOptions = new DistributedCacheEntryOptions
{
AbsoluteExpiration = new DateTimeOffset(new DateTime(2089, 10, 17, 07, 00, 00), TimeSpan.Zero),
SlidingExpiration = TimeSpan.FromSeconds(3600)
})
await cache.SetAsync("string_example", Encoding.UTF8.GetBytes("Example"), cacheOptions, token);
Ý nghĩa của từng thiết lập là như sau.
AbsoluteExpiration
: thời điểm mà giá trị trong cache sẽ hết hạn và bị xóa. Kiểu dữ liệu của thiết lập này làDateTimeOffset
. Nó lưu một giá trịDateTime
và chênh lệch củaDateTime
đó với UTC.SlidingExpiration
: nếu một giá trị trong cache không được đọc hay cập nhật trong khoảng thời gian lớn hơn giá trị này, giá trị đó sẽ bị xóa. Lưu ý làSlidingExpiration
không thể khiến giá trị tồn tại quá thời điểmAbsoluteExpiration
.
Chú ý: nếu có ai đọc bài này sau thời điểm 2089/10/17 07:00:00 UTC
thì hãy đặt giá trị mới hơn cho AbsoluteExpiration
. Nếu AbsoluteExpiration
là một thời điểm trong quá khứ thì sẽ xảy ra lỗi. Tôi không nghĩ là đến năm 2089 thì Redis vẫn còn được sử dụng, và có lẽ blog này cũng không còn tồn tại.
Nếu ta không muốn giá trị cache bị hết hạn thì sao?
Trong namespace Microsoft.Extensions.Caching.Abstractions
có nhiều hàm extension để giúp việc sử dụng IDistributedCache
trở nên dễ dàng hơn. Nếu ta không muốn giá trị trong cache bị hết hạn thì ta có thể dùng hàm này.
await cache.SetAsync("string_example_2", Encoding.UTF8.GetBytes("Example"), token);
Hàm đó sẽ tự tạo một đối tượng DistributedCacheEntryOptions
với giá trị mặc định. Có nghĩa là giá trị ta ghi vào cache sẽ không bao giờ hết hạn, bởi vì tất cả thời điểm hết hạn đều được đặt là -1
như ta thấy ở đây.
private const long NotPresent = -1;
// nhiều code khác
await cache.ScriptEvaluateAsync(SetScript, new RedisKey[] { _instance + key },
new RedisValue[]
{
absoluteExpiration?.Ticks ?? NotPresent,
options.SlidingExpiration?.Ticks ?? NotPresent,
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent,
value
});
Nếu ta chỉ muốn ghi string thì sao?
Nếu ta chỉ muốn ghi string vào Redis thì việc tự encode từng string sang byte array thật là phiền phức. Thật may là có một hàm extension cho phép ta viết code như sau.
await cache.SetStringAsync("string_example_3", "Example", cacheOptions, token);
Khi đọc code của SetStringAsync
, ta thấy là hàm đó sẽ encode giá trị string sang byte array rồi gọi hàm SetAsync
và truyền vào byte array đó.
return cache.SetAsync(key, Encoding.UTF8.GetBytes(value), options, token);
Cũng có cả hàm extension cho trường hợp ta muốn ghi một string vào cache mà không muốn string đó bị hết hạn.
await cache.SetStringAsync("string_example_4", "Example", token);
Redis lưu dữ liệu của ta dưới dạng nào?
Khi đọc code của StackExchangeRedis, ta thấy rằng nó dùng lệnh HMSET
để lưu dữ liệu vào Redis. Từ đó ta có thể đoán là dữ liệu của ta được lưu dưới dạng hash.
private const string SetScript = (@"
redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])
if ARGV[3] ~= '-1' then
redis.call('EXPIRE', KEYS[1], ARGV[3])
end
return 1");
Ta cũng có thể kiểm tra bằng redis-cli.
127.0.0.1:6379> type string_example
hash
Và ta có thể đọc giá trị từng trường trong hash.
127.0.0.1:6379> hgetall string_example
1) "absexp"
2) "659159676000000000"
3) "sldexp"
4) "36000000000"
5) "data"
6) "Example"
Có thể thấy là dữ liệu của ta được lưu trong trường data
. Và các thời điểm giá trị cache hết hạn được lưu trong absexp
và sldexp
. Ý nghĩa của 2 trường này như sau.
absexp
: đây là khoảng cách giữaAbsoluteExpiration
và thời điểm0001/01/01 00:00:00 UTC
với đơn vị là tick. Một giây bằng10.000.000
tick.sldexp
: đây là giá trị củaSlidingExpiration
cũng với đơn vị là tick.
Dùng StackExchangeRedis để đọc giá trị từ Redis
Giống như khi ta ghi dữ liệu vào Redis, lúc này ta cũng chỉ quan tâm tới hàm GetAsync
.
var token = <code để tạo cancellation token> // có thể bỏ qua bước này nếu ta không cần hủy request
var stringBytes = await cache.GetAsync("string", token);
Console.WriteLine(Encoding.UTF8.GetString(stringBytes)); // In ra "Example"
Nếu ta biết rằng dữ liệu của ta có dạng string thì ta có thể dùng hàm extension GetStringAsync
.
var stringValue = await cache.GetStringAsync("string", token);
Console.WriteLine(stringValue); // Print "Example"
Từ đoạn code này, ta có thể thấy GetStringAsync
gọi hàm GetAsync
, dùng UTF-8 để decode byte array trả về, rồi trả về string đã decode.
byte[] data = await cache.GetAsync(key, token).ConfigureAwait(false);
if (data == null)
{
return null;
}
return Encoding.UTF8.GetString(data, 0, data.Length);
Trong cả 2 trường hợp trên, nếu cache key không tồn tại thì kết quả trả về sẽ là null
.
Refresh một giá trị trong cache
Như đã nói ở phần trước, ta có thể đặt thiết lập SlidingExpiration để làm giá trong cache bị hết hạn nếu nó không được sử dụng trong một khoảng thời gian định trước. Đôi khi ta muốn reset đồng hồ đếm ngược này mà lại không muốn đọc hay cập nhật giá trị. Khi đó, ta có thể dùng hàm RefreshAsync
.
var token = <code để tạo cancellation token> // có thể bỏ qua bước này nếu ta không cần hủy request
await cache.RefreshAsync("string", token);
Nếu khi ghi giá trị vào cache ta không đặt SlidingExpiration thì hàm RefreshAsync
sẽ không có tác dụng gì.
Thử viết hàm GetOrSetAsync method
Giống như trong phần trước, ta sẽ viết hàm GetOrSetAsync
.
public async Task<byte[]> GetOrSetAsync(
this IDistributedCache cache,
string key,
Func<Task<byte[]>> factory,
DistributedCacheEntryOptions cacheOptions = default(DistributedCacheEntryOptions),
CancellationToken token = default(CancellationToken))
{
// Dùng key để lấy dữ liệu từ cache
var cacheValue = await cache.GetAsync(key, token);
// Nếu giá trị không phải là null
if (cacheValue is object)
{
// Thì key đã tồn tại, trả về giá trị đó
return cacheValue;
}
// Còn nếu key chưa tồn tại thì chạy delegate để tạo dữ liệu mới
cacheValue = await factory();
if (cacheOptions is null)
{
cacheOptions = new DistributedCacheEntryOptions();
}
// Lưu dữ liệu đó vào cache
await cache.SetAsync(key, cacheValue, cacheOptions, token);
// Và trả về dữ liệu mới đó
return cacheValue;
}
Ta dùng hàm extension này như sau.
var timeStamp = Encoding.UTF8.GetBytes(DateTime.Now.ToString());
await cache.GetOrSetAsync("GetOrSetAsync", () => Task.FromResult(timeStamp));
var rs = await cache.GetAsync("GetOrSetAsync");
Console.WriteLine(Encoding.UTF8.GetString(rs));
Dù ta có gọi hàm trên bao nhiêu lần chăng nữa, giá trị trả về cũng không đổi.
<Thời điểm ta gọi hàm lần đầu tiên>
Kết thúc
Trong bài hôm nay, chúng ta đã dùng thử package StackExchangeRedis và xem nó giúp ta thực hiện non-blocking request tới Redis server như thế nào. Trong phần 3, chúng ta sẽ trao đổi dữ liệu giữa ứng dụng Python và ứng dụng C# .NET thông qua Redis.
One Thought on “Dùng StackExchangeRedis để tích hợp Redis với ứng dụng C# .NET”