feat: Redis 중복 발송 방지 구현 (#108)
This commit is contained in:
parent
639069972b
commit
4292f57ad1
|
|
@ -22,6 +22,11 @@
|
||||||
"PrefetchCount": 10,
|
"PrefetchCount": 10,
|
||||||
"MessageTtl": 86400000
|
"MessageTtl": 86400000
|
||||||
},
|
},
|
||||||
|
"Redis": {
|
||||||
|
"ConnectionString": "",
|
||||||
|
"InstanceName": "spms_dev_",
|
||||||
|
"DuplicateTtlHours": 24
|
||||||
|
},
|
||||||
"CredentialEncryption": {
|
"CredentialEncryption": {
|
||||||
"Key": ""
|
"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.Application.Settings;
|
||||||
using SPMS.Domain.Interfaces;
|
using SPMS.Domain.Interfaces;
|
||||||
using SPMS.Infrastructure.Auth;
|
using SPMS.Infrastructure.Auth;
|
||||||
|
using SPMS.Infrastructure.Caching;
|
||||||
using SPMS.Infrastructure.Messaging;
|
using SPMS.Infrastructure.Messaging;
|
||||||
using SPMS.Infrastructure.Persistence;
|
using SPMS.Infrastructure.Persistence;
|
||||||
using SPMS.Infrastructure.Push;
|
using SPMS.Infrastructure.Push;
|
||||||
|
|
@ -46,6 +47,11 @@ public static class DependencyInjection
|
||||||
// File Storage
|
// File Storage
|
||||||
services.AddSingleton<IFileStorageService, LocalFileStorageService>();
|
services.AddSingleton<IFileStorageService, LocalFileStorageService>();
|
||||||
|
|
||||||
|
// Redis
|
||||||
|
services.Configure<RedisSettings>(configuration.GetSection(RedisSettings.SectionName));
|
||||||
|
services.AddSingleton<RedisConnection>();
|
||||||
|
services.AddSingleton<IDuplicateChecker, DuplicateChecker>();
|
||||||
|
|
||||||
// RabbitMQ
|
// RabbitMQ
|
||||||
services.Configure<RabbitMQSettings>(configuration.GetSection(RabbitMQSettings.SectionName));
|
services.Configure<RabbitMQSettings>(configuration.GetSection(RabbitMQSettings.SectionName));
|
||||||
services.AddSingleton<RabbitMQConnection>();
|
services.AddSingleton<RabbitMQConnection>();
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.2" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||||
<PackageReference Include="RabbitMQ.Client" Version="7.2.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" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user