FreeRedis 實現源碼
/// <summary>
/// 開啓分佈式鎖,若超時返回null
/// </summary>
/// <param name="name">鎖名稱</param>
/// <param name="timeoutSeconds">超時(秒)</param>
/// <param name="autoDelay">自動延長鎖超時時間,看門狗線程的超時時間爲timeoutSeconds/2 , 在看門狗線程超時時間時自動延長鎖的時間爲timeoutSeconds。除非程序意外退出,否則永不超時。</param>
/// <returns></returns>
public LockController Lock(string name, int timeoutSeconds, bool autoDelay = true)
{
name = $"RedisClientLock:{name}";
var startTime = DateTime.Now;
while (DateTime.Now.Subtract(startTime).TotalSeconds < timeoutSeconds)
{
var value = Guid.NewGuid().ToString();
if (SetNx(name, value, timeoutSeconds) == true)
{
double refreshSeconds = (double)timeoutSeconds / 2.0;
return new LockController(this, name, value, timeoutSeconds, refreshSeconds, autoDelay);
}
Thread.CurrentThread.Join(3);
}
return null;
}
使用SETNX命令可以將一個鍵值對設置到Redis中,但只有在該鍵不存在時纔會設置成功
if (SetNx(name, value, timeoutSeconds) == true)
將鎖對應的鍵設置爲某個固定的值,如果設置成功則表示獲取到了鎖
然後進去看看具體類做了什麼
延長鎖
/// <summary>
/// 延長鎖時間,鎖在佔用期內操作時返回true,若因鎖超時被其他使用者佔用則返回false
/// </summary>
/// <param name="milliseconds">延長的毫秒數</param>
/// <returns>成功/失敗</returns>
public bool Delay(int milliseconds)
{
var ret = _client.Eval(@"local gva = redis.call('GET', KEYS[1])
if gva == ARGV[1] then
local ttlva = redis.call('PTTL', KEYS[1])
redis.call('PEXPIRE', KEYS[1], ARGV[2] + ttlva)
return 1
end
return 0", new[] { _name }, _value, milliseconds)?.ToString() == "1";
if (ret == false) _autoDelayTimer?.Dispose(); //未知情況,關閉定時器
return ret;
}
中間一大段lua腳本,大意就是 Redis 中與 KEYS[1] 對應的鍵的值等於 ARGV[1] 時,設置該鍵的過期時間爲 ARGV[2] + ttlva,並返回操作成功;否則返回操作失敗
其中local ttlva = redis.call('PTTL', KEYS[1])
調用 Redis 的 PTTL命令獲取與 KEYS[1] 對應的鍵的剩餘過期毫秒單位時間,並將其賦給變量 ttlva
執行成功返回true,失敗是false,如果是false則代表未知情況
刷新鎖
/// <summary>
/// 刷新鎖時間,把key的ttl重新設置爲milliseconds,鎖在佔用期內操作時返回true,若因鎖超時被其他使用者佔用則返回false
/// </summary>
/// <param name="milliseconds">刷新的毫秒數</param>
/// <returns>成功/失敗</returns>
public bool Refresh(int milliseconds)
{
var ret = _client.Eval(@"local gva = redis.call('GET', KEYS[1])
if gva == ARGV[1] then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1
end
return 0", new[] { _name }, _value, milliseconds)?.ToString() == "1";
if (ret == false) _autoDelayTimer?.Dispose(); //未知情況,關閉定時器
return ret;
}
當 Redis 中與 KEYS[1] 對應的鍵的值等於 ARGV[1] 時,設置該鍵的過期時間爲 ARGV[2],並返回操作成功;否則返回操作失敗,其他和上一個類似
釋放鎖
/// <summary>
/// 釋放分佈式鎖
/// </summary>
/// <returns>成功/失敗</returns>
public bool Unlock()
{
_autoDelayTimer?.Dispose();
return _client.Eval(@"local gva = redis.call('GET', KEYS[1])
if gva == ARGV[1] then
redis.call('DEL', KEYS[1])
return 1
end
return 0", new[] { _name }, _value)?.ToString() == "1";
}
當 Redis 中與 KEYS[1] 對應的鍵的值等於 ARGV[1] 時,刪除該鍵,並返回操作成功
琢磨
如上述所見,主要是利用redis的eval命令執行lua腳本,問題來了了,相同的操作我直接普通的 get set 腳本也可以實現,爲啥一定要用eval寫lua腳本呢
回顧一下redis的機制,由於redis是單線程,也就意味着redis 服務器在任意時刻只能執行一個命令,如果我在客戶端get set去判斷
首先,原子性無法保證,原子性無法保證就會導致髒數據,數據不一致,還有爭搶之類的一堆巴啦啦的問題,其次就是網絡開銷,問我這get set一來一回,網絡開銷成本肯定指數級上升延遲也會變的幾乎肉眼可見
像葉老這樣通過直接eval執行lua腳本,能夠一次性、連續地執行,網絡延遲開銷大大降低,性能也是槓槓的