feat: Redis 중복 발송 방지 구현 (#108)
This commit is contained in:
parent
639069972b
commit
4292f57ad1
|
|
@ -22,6 +22,11 @@
|
|||
"PrefetchCount": 10,
|
||||
"MessageTtl": 86400000
|
||||
},
|
||||
"Redis": {
|
||||
"ConnectionString": "",
|
||||
"InstanceName": "spms_dev_",
|
||||
"DuplicateTtlHours": 24
|
||||
},
|
||||
"CredentialEncryption": {
|
||||
"Key": ""
|
||||
},
|
||||
|
|
|
|||
6
SPMS.Application/Interfaces/IDuplicateChecker.cs
Normal file
6
SPMS.Application/Interfaces/IDuplicateChecker.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace SPMS.Application.Interfaces;
|
||||
|
||||
public interface IDuplicateChecker
|
||||
{
|
||||
Task<bool> IsDuplicateAsync(string requestId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
10
SPMS.Application/Settings/RedisSettings.cs
Normal file
10
SPMS.Application/Settings/RedisSettings.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace SPMS.Application.Settings;
|
||||
|
||||
public class RedisSettings
|
||||
{
|
||||
public const string SectionName = "Redis";
|
||||
|
||||
public string ConnectionString { get; set; } = "localhost:6379";
|
||||
public string InstanceName { get; set; } = "spms_dev_";
|
||||
public int DuplicateTtlHours { get; set; } = 24;
|
||||
}
|
||||
49
SPMS.Infrastructure/Caching/DuplicateChecker.cs
Normal file
49
SPMS.Infrastructure/Caching/DuplicateChecker.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SPMS.Application.Interfaces;
|
||||
using SPMS.Application.Settings;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace SPMS.Infrastructure.Caching;
|
||||
|
||||
public class DuplicateChecker : IDuplicateChecker
|
||||
{
|
||||
private readonly RedisConnection _redis;
|
||||
private readonly RedisSettings _settings;
|
||||
private readonly ILogger<DuplicateChecker> _logger;
|
||||
|
||||
public DuplicateChecker(
|
||||
RedisConnection redis,
|
||||
IOptions<RedisSettings> settings,
|
||||
ILogger<DuplicateChecker> logger)
|
||||
{
|
||||
_redis = redis;
|
||||
_settings = settings.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> IsDuplicateAsync(string requestId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = $"{_settings.InstanceName}duplicate:{requestId}";
|
||||
var ttl = TimeSpan.FromHours(_settings.DuplicateTtlHours);
|
||||
|
||||
try
|
||||
{
|
||||
var db = await _redis.GetDatabaseAsync();
|
||||
var wasSet = await db.StringSetAsync(key, "1", ttl, When.NotExists);
|
||||
|
||||
if (!wasSet)
|
||||
{
|
||||
_logger.LogWarning("중복 메시지 감지: requestId={RequestId}", requestId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Redis 중복 체크 실패: requestId={RequestId} — 중복 아닌 것으로 처리", requestId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
70
SPMS.Infrastructure/Caching/RedisConnection.cs
Normal file
70
SPMS.Infrastructure/Caching/RedisConnection.cs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SPMS.Application.Settings;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace SPMS.Infrastructure.Caching;
|
||||
|
||||
public class RedisConnection : IAsyncDisposable
|
||||
{
|
||||
private readonly RedisSettings _settings;
|
||||
private readonly ILogger<RedisConnection> _logger;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
private ConnectionMultiplexer? _connection;
|
||||
|
||||
public RedisConnection(IOptions<RedisSettings> settings, ILogger<RedisConnection> logger)
|
||||
{
|
||||
_settings = settings.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IDatabase> GetDatabaseAsync()
|
||||
{
|
||||
if (_connection is { IsConnected: true })
|
||||
return _connection.GetDatabase();
|
||||
|
||||
await _semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (_connection is { IsConnected: true })
|
||||
return _connection.GetDatabase();
|
||||
|
||||
_connection?.Dispose();
|
||||
|
||||
_logger.LogInformation("Redis 연결 시도: {ConnectionString}",
|
||||
MaskConnectionString(_settings.ConnectionString));
|
||||
|
||||
_connection = await ConnectionMultiplexer.ConnectAsync(_settings.ConnectionString);
|
||||
|
||||
_logger.LogInformation("Redis 연결 성공");
|
||||
return _connection.GetDatabase();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Redis 연결 실패");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static string MaskConnectionString(string connectionString)
|
||||
{
|
||||
var parts = connectionString.Split(',');
|
||||
return parts.Length > 0 ? parts[0] + ",..." : connectionString;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_connection != null)
|
||||
{
|
||||
await _connection.CloseAsync();
|
||||
_connection.Dispose();
|
||||
}
|
||||
|
||||
_semaphore.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using SPMS.Application.Interfaces;
|
|||
using SPMS.Application.Settings;
|
||||
using SPMS.Domain.Interfaces;
|
||||
using SPMS.Infrastructure.Auth;
|
||||
using SPMS.Infrastructure.Caching;
|
||||
using SPMS.Infrastructure.Messaging;
|
||||
using SPMS.Infrastructure.Persistence;
|
||||
using SPMS.Infrastructure.Push;
|
||||
|
|
@ -46,6 +47,11 @@ public static class DependencyInjection
|
|||
// File Storage
|
||||
services.AddSingleton<IFileStorageService, LocalFileStorageService>();
|
||||
|
||||
// Redis
|
||||
services.Configure<RedisSettings>(configuration.GetSection(RedisSettings.SectionName));
|
||||
services.AddSingleton<RedisConnection>();
|
||||
services.AddSingleton<IDuplicateChecker, DuplicateChecker>();
|
||||
|
||||
// RabbitMQ
|
||||
services.Configure<RabbitMQSettings>(configuration.GetSection(RabbitMQSettings.SectionName));
|
||||
services.AddSingleton<RabbitMQConnection>();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.2" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="7.2.0" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.10.14" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user