在本文中,我们将讨论如何在.NET Core中使用Redis创建分布式锁。
当我们构建分布式系统时,我们将面临多个进程一起处理共享资源,由于其中只有一个可以一次使用共享资源,因此会导致一些意外问题!
我们可以使用分布式锁来解决这个问题。
为什么分布式锁?
首先在非集群单体应用下,我们使用锁来处理这个问题。
以下显示了一些演示锁的使用的示例代码。
代码语言:javascript复制public void SomeMethod()
{
//do something...
lock(obj)
{
//do ....
}
//do something...
}
但是,这种类型的锁不能帮助我们很好地解决问题!这是一个进程内锁,只能用共享资源解决一个进程。
这也是我们需要分布式锁的主要原因!
我将使用Redis在这里创建一个简单的分布式锁。
为什么我使用Redis来完成这项工作?由于Redis的单线程特性及其执行原子操作的能力。
如何创建一个锁?
我将创建一个.NET Core Console应用程序来向您展示大概流程。
在下一步之前,我们应该运行Redis服务器!
StackExchange.Redis是.NET中最受欢迎的Reids客户端,我们将使用它来完成以下工作。
首先与Redis建立联系。
代码语言:javascript复制/// <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;
为了请求锁定共享资源,我们执行以下操作:
代码语言:javascript复制SET resource_name unique_value NX PX duration
resource_name是应用程序的所有实例将共享的值。
unique_value必须对应用程序的每个实例都是唯一的。而他的主要目的是取消锁定(解锁)。
最后,我们还提供一个持续时间(以毫秒为单位),之后Redis将自动删除锁定。
这是C#代码中的实现。
代码语言:javascript复制/// <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;
}
这是测试获取锁定的代码。
代码语言:javascript复制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} can not eat food due to don't get the lock.");
}
});
Console.WriteLine("end");
Console.Read();
}
运行代码后,我们可能会得到以下结果。
只有一个人可以获得锁定!其他人等待。
虽然Redis会自动删除锁,但它也没有很好地利用共享资源!
因为当一个进程完成它的工作时,应该让其他人使用该资源,而不是无休止地等待!
所以我们也需要释放锁。
如何释放锁定?
要释放锁,我们只需删除Redis中对应的key/value!
正如我们在创建锁中所做的那样,我们需要匹配资源的唯一值,这样可以更安全地释放正确的锁。
匹配时,我们将删除锁定,这意味着解锁成功。否则,解锁不成功。
我们需要一次执行get和del命令,因此我们将使用lua脚本来执行此操作!
代码语言:javascript复制/// <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;
}
}
我们应该在进程完成后调用此方法。
当进程获得锁定并且由于某些原因而未释放锁定时,其他进程不能等到它被释放。此时,其他流程应该继续进行。
这是一个处理这个场景的示例。
代码语言:javascript复制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在无锁情况下运行。