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脚本,能够一次性、连续地执行,网络延迟开销大大降低,性能也是杠杠的

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