소개
이 문서에서는 .NET Core에서 Redis를 사용하여 분산 잠금을 생성하는 방법에 대해 설명합니다.
분산 시스템을 구축할 때 공유 리소스를 함께 처리하는 여러 프로세스에 직면하게 됩니다. 한 번에 둘 중 하나만 공유 리소스를 활용할 수 있기 때문에 예상치 못한 문제가 발생할 수 있습니다!
이 문제를 해결하기 위해 분산 잠금을 사용할 수 있습니다.
분산 잠금이 필요한 이유
평소와 마찬가지로 이 문제를 해결하기 위해 잠금 장치를 사용할 것입니다.
다음은 잠금 사용을 보여주는 몇 가지 샘플 코드를 보여줍니다.
public void SomeMethod()
{
// Do something...
lock (obj)
{
// Do ....
}
// Do something...
} 그러나 이러한 유형의 잠금은 문제를 해결하는 데 도움이 되지 않습니다! 공유 리소스가 있는 하나의 프로세스만 해결할 수 있는 프로세스 내 잠금입니다.
이것이 분산 잠금이 필요한 주된 이유이기도 합니다!
여기서는 Redis를 사용하여 간단한 분산 잠금을 생성하겠습니다.
그리고 이 작업을 수행하기 위해 Redis를 사용하는 이유는 무엇입니까? Redis의 단일 스레드 특성과 원자적 작업 수행 능력 때문입니다.
잠금을 만드는 방법
보여드릴 .NET Core 콘솔 애플리케이션을 만들어 보겠습니다.
다음 단계에 앞서 Redis 서버를 실행해야 합니다!
StackExchange.Redis는 .NET에서 가장 인기 있는 Reids 클라이언트이며 우리가 다음 작업을 수행하는 데 이를 사용할 것이라는 데는 의심의 여지가 없습니다.
먼저 Redis와의 연결을 생성합니다.
/// <summary>
/// The lazy connection.
/// </summary>
private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() =>
{
ConfigurationOptions configuration = new ConfigurationOptions
{
AbortOnConnectFail = false,
ConnectTimeout = 5000,
};
configuration.EndPoints.Add("localhost", 6379);
return ConnectionMultiplexer.Connect(configuration.ToString());
});
/// <summary>
/// Gets the connection.
/// </summary>
/// <value>The connection.</value>
public static ConnectionMultiplexer Connection => lazyConnection.Value; 공유 리소스에 대한 잠금을 요청하기 위해 다음을 수행합니다.
SET resource_name unique_value NX PX duration Resource_name은 애플리케이션의 모든 인스턴스가 공유하는 값입니다.
Unique_value는 애플리케이션의 각 인스턴스에 대해 고유해야 합니다. 그리고 이 고유한 값의 목적은 잠금을 해제(잠금 해제)하는 것입니다.
마지막으로 지속 시간(밀리초 단위)도 제공하며 그 이후에는 Redis가 잠금을 자동으로 제거합니다.
다음은 C# 코드로 구현한 것입니다.
/// <summary>
/// Acquires the lock.
/// </summary>
/// <returns><c>true</c>, if lock was acquired, <c>false</c> otherwise.</returns>
/// <param name="key">Key.</param>
/// <param name="value">Value.</param>
/// <param name="expiration">Expiration.</param>
static bool AcquireLock(string key, string value, TimeSpan expiration)
{
bool flag = false;
try
{
flag = Connection.GetDatabase().StringSet(key, value, expiration, When.NotExists);
}
catch (Exception ex)
{
Console.WriteLine($"Acquire lock fail...{ex.Message}");
flag = true;
}
return flag;
} 획득 잠금을 테스트하는 코드는 다음과 같습니다.
static void Main(string[] args)
{
string lockKey = "lock:eat";
TimeSpan expiration = TimeSpan.FromSeconds(5);
// 5 person eat something...
Parallel.For(0, 5, x =>
{
string person = $"person:{x}";
bool isLocked = AcquireLock(lockKey, person, expiration);
if (isLocked)
{
Console.WriteLine($"{person} begin eat food (with lock) at {DateTimeOffset.Now.ToUnixTimeMilliseconds()}.");
}
else
{
Console.WriteLine($"{person} cannot eat food due to not getting the lock.");
}
});
Console.WriteLine("end");
Console.Read();
} 코드를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

자물쇠는 한 사람만 얻을 수 있어요! 다른 사람들도 기다리고 있습니다.
잠금은 Redis에 의해 자동으로 제거되지만 공유 리소스를 제대로 활용하지 못합니다!
프로세스가 작업을 마치면 끝없이 기다리지 않고 다른 사람이 리소스를 사용할 수 있도록 해야 하기 때문입니다!
그래서 잠금도 해제해야 합니다.
잠금을 해제하는 방법
잠금을 해제하기 위해 Redis에서 해당 항목을 제거했습니다!
잠금을 생성할 때 취하는 내용은 리소스의 고유한 값과 일치해야 합니다. 이렇게 하면 올바른 잠금을 해제하는 것이 더 안전해집니다.
일치하면 잠금이 삭제됩니다. 이는 잠금 해제가 성공했음을 의미합니다. 그렇지 않으면 잠금 해제에 실패했습니다.
get 및 del 명령을 한 번에 실행해야 하므로 이를 위해 Lua 스크립트를 사용하겠습니다!
/// <summary>
/// Releases the lock.
/// </summary>
/// <returns><c>true</c>, if lock was released, <c>false</c> otherwise.</returns>
/// <param name="key">Key.</param>
/// <param name="value">Value.</param>
static bool ReleaseLock(string key, string value)
{
string lua_script = @"
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
redis.call('DEL', KEYS[1])
return true
else
return false
end
";
try
{
var res = Connection.GetDatabase().ScriptEvaluate(lua_script,
new RedisKey[] { key },
new RedisValue[] { value });
return (bool)res;
}
catch (Exception ex)
{
Console.WriteLine($"ReleaseLock lock fail...{ex.Message}");
return false;
}
} 프로세스가 완료되면 이 메소드를 호출해야 합니다.
프로세스가 잠금을 획득했지만 어떤 이유로 잠금을 해제하지 않으면 다른 프로세스는 잠금이 해제될 때까지 기다릴 수 없습니다. 이때 다른 절차가 진행되어야 합니다.
이 장면을 다루는 샘플은 다음과 같습니다.
Parallel.For(0, 5, x =>
{
string person = $"person:{x}";
var val = 0;
bool isLocked = AcquireLock(lockKey, person, expiration);
while (!isLocked && val <= 5000)
{
val += 250;
System.Threading.Thread.Sleep(250);
isLocked = AcquireLock(lockKey, person, expiration);
}
if (isLocked)
{
Console.WriteLine($"{person} begin eat food (with lock) at {DateTimeOffset.Now.ToUnixTimeMilliseconds()}.");
if (new Random().NextDouble() < 0.6)
{
Console.WriteLine($"{person} release lock {ReleaseLock(lockKey, person)} {DateTimeOffset.Now.ToUnixTimeMilliseconds()}");
}
else
{
Console.WriteLine($"{person} do not release lock ....");
}
}
else
{
Console.WriteLine($"{person} begin eat food (without lock) at {DateTimeOffset.Now.ToUnixTimeMilliseconds()}.");
}
}); 샘플을 실행한 후 다음과 같은 결과를 얻을 수 있습니다.
보시다시피 3인과 4인은 자물쇠 없이 진행됩니다.
내 GitHub 페이지에서 찾을 수 있는 소스 코드는 다음과 같습니다.
- RedisLock데모
요약
이 문서에서는 .NET Core에서 Redis를 사용하여 분산 잠금을 만드는 방법을 소개했습니다. 기본 버전이므로 비즈니스에 따라 개선할 수 있습니다.
이것이 도움이 되기를 바랍니다.