Redis實現分布式鎖(悲觀鎖/樂觀鎖)
對鎖的概念和應用場景在此就不闡述了,網上搜索有很多解釋,只是我搜索到的使用C#利用Redis的SetNX命令實現的鎖雖然能用,但是都不太適合我需要的場景。
基於ServiceStack.Redis寫了一個幫助類
Redis連接池
public static PooledRedisClientManager RedisClientPool = CreateManager();
private static PooledRedisClientManager CreateManager()
{
var redisHosts = System.Configuration.ConfigurationManager.AppSettings["redisHosts"];
if (string.IsNullOrEmpty(redisHosts))
{
throw new Exception("AppSetting redisHosts no found");
}
string[] redisHostarr = redisHosts.Split(new string[] { ",", "," }, StringSplitOptions.RemoveEmptyEntries);
return new PooledRedisClientManager(redisHostarr, redisHostarr, new RedisClientManagerConfig
{
MaxWritePoolSize = 1000,
MaxReadPoolSize = 1000,
AutoStart = true,
DefaultDb = 0
});
}
使用Redis的SetNX命令實現加鎖,
/// <summary>
/// 加鎖
/// </summary>
/// <param name="key">鎖key</param>
/// <param name="selfMark">自己標記</param>
/// <param name="lockExpirySeconds">鎖自動過期時間[默認10](s)</param>
/// <param name="waitLockMilliseconds">等待鎖時間(ms)</param>
/// <returns></returns>
public static bool Lock(string key, out string selfMark, int lockExpirySeconds = 10, long waitLockMilliseconds = long.MaxValue)
{
DateTime begin = DateTime.Now;
selfMark = Guid.NewGuid().ToString("N");//自己標記,釋放鎖時會用到,自己加的鎖除非過期否則只能自己打開
using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient())
{
string lockKey = "Lock:" + key;
while (true)
{
string script = string.Format("if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then redis.call('PEXPIRE',KEYS[1],{0}) return 1 else return 0 end", lockExpirySeconds * 1000);
//循環獲取取鎖
if (redisClient.ExecLuaAsInt(script, new[] { lockKey }, new[] { selfMark }) == 1)
{
return true;
}
//不等待鎖則返回
if (waitLockMilliseconds == 0)
{
break;
}
//超過等待時間,則不再等待
if ((DateTime.Now - begin).TotalMilliseconds >= waitLockMilliseconds)
{
break;
}
Thread.Sleep(100);
}
return false;
}
}
- 參數key:鎖的key
- 參數selfMark:在設置鎖的時候會產生一個自己的標識,在釋放鎖的時候會用到,所謂解鈴還須系鈴人。防止鎖被誤釋放,導致鎖無效.
- 參數lockExpirySeconds:鎖的默認過期時間,防止被永久死鎖.
- 參數waitLockMilliseconds:循環獲取鎖的等待時間.
如果設置為0,為樂觀鎖機制,獲取不到鎖,直接返回未獲取到鎖.
默認值為long最大值,為悲觀鎖機制,約等於很多很多天,可以理解為一直等待.
釋放鎖
/// <summary>
/// 釋放鎖
/// </summary>
/// <param name="key">鎖key</param>
/// <param name="selfMark">自己標記</param>
public void UnLock(string key, string selfMark)
{
using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient())
{
string lockKey = "Lock:" + key;
var script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisClient.ExecLuaAsString(script, new[] { lockKey }, new[] { selfMark });
}
}
- 參數key:鎖的key
- 參數selfMark:在設置鎖的時候返回的自己標識,用來解鎖自己加的鎖(此值不能隨意傳,必須是加鎖時返回的值)
調用方式
- 悲觀鎖方式
int num = 10;
string lockkey = "xianseng";
//悲觀鎖開啟20個人同時拿寶貝
for (int i = 0; i < 20; i++)
{
Task.Run(() =>
{
string selfmark = "";
try
{
if (PublicLockHelper.Lock(lockkey, out selfmark))
{
if (num > 0)
{
num--;
Console.WriteLine($"我拿到了寶貝:寶貝剩余{num}個\t\t{selfmark}");
}
else
{
Console.WriteLine("寶貝已經沒有了");
}
Thread.Sleep(100);
}
}
finally
{
PublicLockHelper.UnLock(lockkey, selfmark);
}
});
}
- 樂觀鎖方式
int num = 10;
string lockkey = "xianseng";
//樂觀鎖開啟10個線程,每個線程拿5次
for (int i = 0; i < 10; i++)
{
Task.Run(() =>
{
for (int j = 0; j < 5; j++)
{
string selfmark = "";
try
{
if (PublicLockHelper.Lock(lockkey, out selfmark, 10, 0))
{
if (num > 0)
{
num--;
Console.WriteLine($"我拿到了寶貝:寶貝剩余{num}個\t\t{selfmark}");
}
else
{
Console.WriteLine("寶貝已經沒有了");
}
Thread.Sleep(1000);
}
else
{
Console.WriteLine("沒有拿到,不想等了");
}
}
finally
{
PublicLockHelper.UnLock(lockkey, selfmark);
}
}
});
}
單機只能用多線模擬使用分布式鎖了
此鎖已經可以滿足大多數場景了,若有不妥,還請多多指出,以免誤別人!
()