fengling-risk-control/Fengling.RiskControl.Client/Counter/RedisCounterService.cs
Sam 293209b1dc feat: add Fengling.RiskControl.Client SDK
- Implement RedisCounterService for rate limiting
- Implement RuleLoader with timer refresh
- Implement RiskEvaluator for local rule evaluation
- Implement SamplingService for CAP events
- Implement CapEventPublisher for async event publishing
- Implement FailoverStrategy for Redis failure handling
- Add configuration classes and DI extensions
- Add unit tests (9 tests)
- Add NuGet publishing script
2026-02-06 00:16:53 +08:00

143 lines
4.2 KiB
C#

using Fengling.RiskControl.Domain.Aggregates.RiskRules;
using Fengling.RiskControl.Configuration;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
namespace Fengling.RiskControl.Counter;
public interface IRiskCounterService
{
Task<long> IncrementAsync(string memberId, string metric, int value = 1);
Task<int> GetValueAsync(string memberId, string metric);
Task<Dictionary<string, int>> GetAllValuesAsync(string memberId);
Task<bool> SetValueAsync(string memberId, string metric, int value);
Task RefreshTtlAsync(string memberId);
Task<bool> ExistsAsync(string memberId);
}
public class RedisCounterService : IRiskCounterService
{
private readonly IConnectionMultiplexer _redis;
private readonly RiskControlClientOptions _options;
private readonly ILogger<RedisCounterService> _logger;
private string MemberKey(string memberId) => $"{_options.Redis.KeyPrefix}{memberId}";
public RedisCounterService(
IConnectionMultiplexer redis,
RiskControlClientOptions options,
ILogger<RedisCounterService> logger)
{
_redis = redis;
_options = options;
_logger = logger;
}
public async Task<long> IncrementAsync(string memberId, string metric, int value = 1)
{
var db = _redis.GetDatabase();
var key = MemberKey(memberId);
try
{
var result = await db.HashIncrementAsync(key, metric, value);
await db.KeyExpireAsync(key, TimeSpan.FromSeconds(_options.Redis.DefaultTtlSeconds));
return result;
}
catch (RedisException ex)
{
_logger.LogError(ex, "Redis increment failed for member {MemberId}, metric {Metric}", memberId, metric);
throw;
}
}
public async Task<int> GetValueAsync(string memberId, string metric)
{
var db = _redis.GetDatabase();
var key = MemberKey(memberId);
try
{
var value = await db.HashGetAsync(key, metric);
return value.HasValue ? int.Parse(value) : 0;
}
catch (RedisException ex)
{
_logger.LogError(ex, "Redis get failed for member {MemberId}, metric {Metric}", memberId, metric);
throw;
}
}
public async Task<Dictionary<string, int>> GetAllValuesAsync(string memberId)
{
var db = _redis.GetDatabase();
var key = MemberKey(memberId);
try
{
var entries = await db.HashGetAllAsync(key);
return entries
.Where(e => e.Name != "_ttl")
.ToDictionary(
e => e.Name.ToString(),
e => int.Parse(e.Value)
);
}
catch (RedisException ex)
{
_logger.LogError(ex, "Redis get all failed for member {MemberId}", memberId);
throw;
}
}
public async Task<bool> SetValueAsync(string memberId, string metric, int value)
{
var db = _redis.GetDatabase();
var key = MemberKey(memberId);
try
{
await db.HashSetAsync(key, metric, value);
await db.KeyExpireAsync(key, TimeSpan.FromSeconds(_options.Redis.DefaultTtlSeconds));
return true;
}
catch (RedisException ex)
{
_logger.LogError(ex, "Redis set failed for member {MemberId}, metric {Metric}", memberId, metric);
throw;
}
}
public async Task RefreshTtlAsync(string memberId)
{
var db = _redis.GetDatabase();
var key = MemberKey(memberId);
try
{
await db.KeyExpireAsync(key, TimeSpan.FromSeconds(_options.Redis.DefaultTtlSeconds));
}
catch (RedisException ex)
{
_logger.LogError(ex, "Redis TTL refresh failed for member {MemberId}", memberId);
throw;
}
}
public async Task<bool> ExistsAsync(string memberId)
{
var db = _redis.GetDatabase();
var key = MemberKey(memberId);
try
{
return await db.KeyExistsAsync(key);
}
catch (RedisException ex)
{
_logger.LogError(ex, "Redis exists check failed for member {MemberId}", memberId);
throw;
}
}
}