在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问。现在公司都是流行分布式架构,在分布式环境下,如何保证不同节点的线程同步执行呢?
实际上,对于分布式场景,我们可以使用分布式锁,它是控制分布式系统之间互斥访问共享资源的一种方式。对于每个锁,最好有一个唯一id,保证不会错误解锁。(例如 :A锁与B锁的key相同,在A锁过期的一瞬间,B锁进行解锁,若不校验锁id,会导致A锁被解锁);Redis提供了SETNX(set if not exists),仅在key不存在时插入value;Redis在2.6.12版本提供了SET函数的重载,支持仅在key不存在时插入带有过期时间的value;虽然Redis没有提供仅在value相同时删除的命令,但是在2.6.0版本提供了EXAL用于执行脚本,通过该脚本可以;仅在value相同时删除这一功能。
下面介绍实现分布式锁。
引入依赖
using ServiceStack.Redis;
锁实现过程
private static readonly string ScriptSetIfAbsent = "return redis.call('SET',KEYS[1],ARGV[1],'EX',ARGV[2],'NX')";
private static readonly string ScriptDeleteIfEqualValue = @"if redis.call('GET',KEYS[1]) == ARGV[1] then
return redis.call('DEL',KEYS[1])
else
return 'FALSE'
end";
/// <summary>
/// 加锁
/// </summary>
/// <param name="key">锁key</param>
/// <param name="lockToken">锁令牌,用于释放锁</param>
/// <param name="lockExpirySeconds">锁自动超时时间(秒)</param>
/// <param name="waitLockSeconds">等待锁时间(秒)</param>
/// <returns>加锁成功</returns>
public bool Lock(string key, out string lockToken, int lockExpirySeconds = 10, double waitLockSeconds = 0)
{
int waitIntervalMs = 1000;
string lockKey = GetLockKey(key);
DateTime begin = DateTime.Now;
string uuid = Guid.NewGuid().ToString();
//循环获取取锁
while (true)
{
string result;
using (var client = GetNativeClient())
{
//返回SET操作结果,为OK时成功
result = client.EvalStr(ScriptSetIfAbsent, 1,
System.Text.Encoding.UTF8.GetBytes(lockKey),
System.Text.Encoding.UTF8.GetBytes(uuid),
System.Text.Encoding.UTF8.GetBytes(lockExpirySeconds.ToString()));
}
if (result == "OK")
{
lockToken = uuid;
return true;
}
//超过等待时间,则不再等待
if ((DateTime.Now - begin).TotalSeconds >= waitLockSeconds) break;
Thread.Sleep(waitIntervalMs);
}
lockToken = null;
return false;
}
/// <summary>
/// 释放锁,执行完代码以后调用
/// </summary>
/// <param name="key">锁Key</param>
/// <param name="lockToken">锁令牌</param>
/// <returns>释放锁成功</returns>
public bool DelLock(string key, string lockToken)
{
if (string.IsNullOrWhiteSpace(lockToken))
{
throw new Exception("参数lockToken不能为空");
}
string lockKey = GetLockKey(key);
using (var client = GetNativeClient())
{
//返回删除的行数,为1时成功
string result = client.EvalStr(ScriptDeleteIfEqualValue, 1,
System.Text.Encoding.UTF8.GetBytes(lockKey),
System.Text.Encoding.UTF8.GetBytes(lockToken));
return result == "1";
}
}
锁使用过程
if (RedisManager.Lock(key, out tokenLock))
{
try
{
IRedisClient rdsclient = null;
try
{
}
finally
{
rdsclient?.Dispose();
}
}
finally
{
RedisManager.DelLock(key, tokenLock);
}
}