Redis分佈式鎖實現一直到Redis相關操作總結

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腳本,能夠一次性、連續地執行,網絡延遲開銷大大降低,性能也是槓槓的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章