A C# managed Redis Library for doing caching, locking, and concurrency
- Install the library
<ItemGroup>
<PackageReference Include="Firebend.LitRedis.Core" />
</ItemGroup>or
dotnet add package Firebend.LitRedis.Core- In
Program.cs, add the Lit Redis configuration to theConfigureServicescallback inCreateHostBuilder
services
.AddLitRedis(redis => redis.WithCaching().WithLocking().WithConnectionString("localhost:6379,defaultDatabase=0"))
.AddHostedService<SampleHostedService>()
.AddLogging(o => o.AddSimpleConsole(c => c.TimestampFormat = "[yyy-MM-dd HH:mm:ss] "));Using the following SampleHostedService extending BackgroundService
public class SampleHostedService : BackgroundService
{
private readonly ILitRedisCacheStore _redisCacheStore;
private readonly ILitRedisDistributedLockService _redisDistributedLockService;
private readonly ILogger<SampleHostedService> _logger;
public SampleHostedService(ILogger<SampleHostedService> logger,
ILitRedisCacheStore redisCacheStore,
ILitRedisDistributedLockService redisDistributedLockService)
{
_logger = logger;
_redisCacheStore = redisCacheStore;
_redisDistributedLockService = redisDistributedLockService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
// execute
}
}Create a SampleCacheObject class defining the data structure to write to the cache
public class SampleCacheObject
{
public string Phrase { get; set; } = $"Cache me if you can! {DateTime.Now}";
}PutAsync(string key, SampleCacheObject model, TimeSpan? expiry, CancellationToken cancellationToken)
Write an object to the cache, providing the key to store data under as the first argument
try {
await _redisCacheStore.PutAsync("one", new SampleCacheObject(), TimeSpan.FromMinutes(5), stoppingToken);
}
catch (Exception ex) {
_logger.LogCritical(ex, "Error");
}Read a cached object from the store, providing the key data is stored under as the first argument
try {
var data = await _redisCacheStore.GetAsync<SampleCacheObject>("one", stoppingToken);
_logger.LogInformation($"Phrase: {data?.Phrase}");
}
catch (Exception ex) {
_logger.LogCritical(ex, "Error");
}Attempt to acquire a lock on a particular key, waiting until the lock is able to be acquired.
By default, LockIncrease is set to 30 seconds and RenewLockInterval is set to 10 seconds. The renewal interval should always be shorter than the lock increase so the lock is extended before it expires.
try {
var waitModel = RequestLockModel
.WithKey("lit-sample")
await using var locker = await _redisDistributedLockService.AcquireLockAsync(waitModel, stoppingToken);
if (locker.Succeeded)
{
_logger.LogInformation("Lock acquired");
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
catch (Exception ex) {
_logger.LogCritical(ex, "Error");
}Use WaitForever or set the lock model class's WaitTimeout to null
try {
var waitModel = RequestLockModel
.WithKey("lit-sample")
.WaitForever();
await using var locker = await _redisDistributedLockService.AcquireLockAsync(waitModel, stoppingToken);
if (locker.Succeeded)
{
_logger.LogInformation("Lock acquired");
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
catch (Exception ex) {
_logger.LogCritical(ex, "Error");
}Use WithLockIncrease, WithRenewLockInterval, and WithLockWaitTimeout or set the lock model class's LockIncrease, RenewLockInterval, and WaitTimeout to TimeSpans
try {
var waitModel = RequestLockModel
.WithKey("lit-sample")
.WithLockIncrease(TimeSpan.FromSeconds(3))
.WithRenewLockInterval(TimeSpan.FromSeconds(3))
.WithLockWaitTimeout(TimeSpan.FromSeconds(20));
await using var locker = await _redisDistributedLockService.AcquireLockAsync(waitModel, stoppingToken);
if (locker.Succeeded)
{
_logger.LogInformation("Lock acquired");
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
catch (Exception ex) {
_logger.LogCritical(ex, "Error");
}Use NoWait or set the lock model class's WaitTimeout to 0. If the lock fails to be acquired, it will simply exit
try {
var waitModel = RequestLockModel
.WithKey("lit-sample")
.NoWait();
await using var locker = await _redisDistributedLockService.AcquireLockAsync(waitModel, stoppingToken);
if (locker.Succeeded)
{
_logger.LogInformation("Lock acquired");
}
else {
_logger.LogInformation("No lock acquired");
}
}
catch (Exception ex) {
_logger.LogCritical(ex, "Error");
}If acquiring a lock fails, calling ThrowIfFailedToAcquire() will throw an AcquireLockFailedException
Once a lock is acquired, a background task keeps it alive by extending it on every RenewLockInterval. If a renewal ever fails (for example because Redis returned an error or another client took over the key after expiration), the lock is considered lost. When this happens the model exposes two ways to react so you can pick the one that fits your code style.
Statusreflects the current state of the lock:NotAcquired,Acquired, orLost.LockLostTokenis aCancellationTokenthat is cancelled when the lock is lost. It's safe to link it into your own work viaCancellationTokenSource.CreateLinkedTokenSource.ThrowOnLockLost()throws aLockLostExceptionif the lock has already been lost. Useful right before a critical section.
try {
var model = RequestLockModel
.WithKey("lit-sample")
.WithLockIncrease(TimeSpan.FromSeconds(5))
.WithRenewLockInterval(TimeSpan.FromSeconds(2));
await using var locker = await _redisDistributedLockService.AcquireLockAsync(model, stoppingToken);
if (!locker.Succeeded)
{
_logger.LogInformation("No lock acquired");
return;
}
// 1. Link the cancellation token into your work
using var workCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, locker.LockLostToken);
await DoWorkAsync(workCts.Token);
// 2. Throw at a critical section
locker.ThrowOnLockLost();
await CommitAsync(stoppingToken);
}
catch (LockLostException ex) {
_logger.LogCritical(ex, "Lock was lost mid-operation");
}
catch (Exception ex) {
_logger.LogCritical(ex, "Error");
}When the lock is lost, the release callback will not attempt to release the key on dispose, since another holder may already own it.
If the background renewal loop repeatedly fails to extend the lock the library will mark the lock as lost. The number of consecutive extension failures tolerated before marking the lock lost is configurable via RequestLockModel.MaxExtendRetries (default: 3). You can set it fluently with WithMaxExtendRetries(int) on the request model.
When a lock is marked lost the Status becomes Lost and LockLostToken will be cancelled so callers can react promptly.