- 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
143 lines
4.2 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|