Files
Swarm/DysonNetwork.Shared/Cache/CacheService.cs
2025-12-02 22:38:47 +08:00

201 lines
5.7 KiB
C#

using Microsoft.Extensions.Caching.Distributed;
using RedLockNet;
using StackExchange.Redis;
namespace DysonNetwork.Shared.Cache;
public class CacheServiceRedis(
IDistributedCache cache,
IConnectionMultiplexer redis,
ICacheSerializer serializer,
IDistributedLockFactory lockFactory
)
: ICacheService
{
private const string GlobalKeyPrefix = "dyson:";
private const string GroupKeyPrefix = GlobalKeyPrefix + "cg:";
private const string LockKeyPrefix = GlobalKeyPrefix + "lock:";
private static string Normalize(string key) => $"{GlobalKeyPrefix}{key}";
// -----------------------------------------------------
// BASIC OPERATIONS
// -----------------------------------------------------
public async Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null)
{
key = Normalize(key);
var json = serializer.Serialize(value);
var options = new DistributedCacheEntryOptions();
if (expiry.HasValue)
options.SetAbsoluteExpiration(expiry.Value);
await cache.SetStringAsync(key, json, options);
return true;
}
public async Task<T?> GetAsync<T>(string key)
{
key = Normalize(key);
var json = await cache.GetStringAsync(key);
if (json is null)
return default;
return serializer.Deserialize<T>(json);
}
public async Task<(bool found, T? value)> GetAsyncWithStatus<T>(string key)
{
key = Normalize(key);
var json = await cache.GetStringAsync(key);
if (json is null)
return (false, default);
return (true, serializer.Deserialize<T>(json));
}
public async Task<bool> RemoveAsync(string key)
{
key = Normalize(key);
// Remove key from all groups
var db = redis.GetDatabase();
var groupPattern = $"{GroupKeyPrefix}*";
var server = redis.GetServers().First();
var groups = server.Keys(pattern: groupPattern);
foreach (var group in groups)
{
await db.SetRemoveAsync(group, key);
}
await cache.RemoveAsync(key);
return true;
}
// -----------------------------------------------------
// GROUP OPERATIONS
// -----------------------------------------------------
public async Task AddToGroupAsync(string key, string group)
{
key = Normalize(key);
var db = redis.GetDatabase();
var groupKey = $"{GroupKeyPrefix}{group}";
await db.SetAddAsync(groupKey, key);
}
public async Task RemoveGroupAsync(string group)
{
var groupKey = $"{GroupKeyPrefix}{group}";
var db = redis.GetDatabase();
var keys = await db.SetMembersAsync(groupKey);
if (keys.Length > 0)
{
foreach (var key in keys)
await cache.RemoveAsync(key.ToString());
}
await db.KeyDeleteAsync(groupKey);
}
public async Task<IEnumerable<string>> GetGroupKeysAsync(string group)
{
var groupKey = $"{GroupKeyPrefix}{group}";
var db = redis.GetDatabase();
var members = await db.SetMembersAsync(groupKey);
return members.Select(x => x.ToString());
}
public async Task<bool> SetWithGroupsAsync<T>(
string key,
T value,
IEnumerable<string>? groups = null,
TimeSpan? expiry = null)
{
var result = await SetAsync(key, value, expiry);
if (!result || groups == null)
return result;
var tasks = groups.Select(g => AddToGroupAsync(key, g));
await Task.WhenAll(tasks);
return true;
}
// -----------------------------------------------------
// DISTRIBUTED LOCK (RedLock wrapper)
// -----------------------------------------------------
private readonly TimeSpan _defaultRetry = TimeSpan.FromMilliseconds(100);
public async Task<IDistributedLock?> AcquireLockAsync(
string resource,
TimeSpan expiry,
TimeSpan? waitTime = null,
TimeSpan? retryInterval = null)
{
if (string.IsNullOrWhiteSpace(resource))
throw new ArgumentException("Resource cannot be null", nameof(resource));
var lockKey = $"{LockKeyPrefix}{resource}";
var redlock = await lockFactory.CreateLockAsync(
lockKey,
expiry,
waitTime ?? TimeSpan.Zero,
retryInterval ?? _defaultRetry
);
return !redlock.IsAcquired ? null : new RedLockAdapter(redlock, resource);
}
public async Task<bool> ExecuteWithLockAsync(
string resource,
Func<Task> action,
TimeSpan expiry,
TimeSpan? waitTime = null,
TimeSpan? retryInterval = null)
{
await using var l = await AcquireLockAsync(resource, expiry, waitTime, retryInterval);
if (l is null)
return false;
await action();
return true;
}
public async Task<(bool Acquired, T? Result)> ExecuteWithLockAsync<T>(
string resource,
Func<Task<T>> func,
TimeSpan expiry,
TimeSpan? waitTime = null,
TimeSpan? retryInterval = null)
{
await using var l = await AcquireLockAsync(resource, expiry, waitTime, retryInterval);
if (l is null)
return (false, default);
var result = await func();
return (true, result);
}
}
public class RedLockAdapter(IRedLock inner, string resource) : IDistributedLock
{
public string Resource { get; } = resource;
public string LockId => inner.LockId;
public ValueTask ReleaseAsync() => inner.DisposeAsync();
public async ValueTask DisposeAsync()
{
await inner.DisposeAsync();
GC.SuppressFinalize(this);
}
}