带Redlock.net的分布式锁定

目录

为什么要锁定东西

发送通知

穷人的故障转移

确保消息处理一次准确无误

使用Redis

Redlock实现

Redlock安全吗?

代码

总结


为什么要锁定东西

如今,微服务架构被广泛采用。它提供的好处之一是水平扩展的可能性,这使我们能够显着提高应用程序的性能。但是,在某些情况下,多个服务实例面临对某些共享资源的争用。在这种情况下,其中一个实例将获得对此资源的锁定,声称对它具有独占访问权限。让我们看一下一些可能的用例,当这很有帮助时。

发送通知

让我们想象一个服务,它从数据库中获取一批通知并将其发送给客户。服务被设计为在给定时间段内运行一次的后台工作线程。如果我们想通过水平扩展来提高服务的性能,我们需要以某种方式发出信号,表明批处理被服务的给定实例使用。我们可以在数据库中创建一个标志,并在消息被worker实例占用时设置它。但是,这样的解决方案会污染我们的数据模型。相反,我们可以在分布式存储中为给定批次放置一个锁。

穷人的故障转移

另一个原因可能是穷人的故障转移方案,其中一个服务实例执行工作,而另一个实例处于空闲状态并等待,以防第一个实例由于某种原因失败。

确保消息处理一次准确无误

假设多个服务实例使用流中的消息。根据流实现,这可能会导致每个实例使用相同的消息。可能的解决方案之一是分片流,为每个专用实例创建一个子流。此解决方案的缺点是,每次升级/缩小服务时,我们都必须重新配置流。另一种解决方案是每个服务实际使用消息,但在处理消息之前尝试获取此消息的分布式锁。这样,每个服务将只处理一次。

使用Redis

作为分布式锁的存储,我们将使用RedisRedis具有许多优点:

  • Redis是内存中的键值存储,可提供额外的速度。
  • 它能够为密钥设置TTL值,从而允许我们过期锁定。我们稍后会看到这是算法的重要组成部分。

已经有一个在Redis上实现分布式锁定的。所以我们只需要利用它。

Redlock实现

假设我们将使用单个Redis作为存储锁。这样的解决方案有一个明显的缺点:它是单点故障。但是,无法通过简单地添加副本来缓解此问题,因为这会导致争用条件。想象一下以下场景:

  1. 客户端A获取主服务器中的锁。
  2. 在对密钥的写入传输到副本之前,主服务器崩溃。
  3. 副本将提升为主副本。
  4. 客户端B获取指向A已持有锁的同一资源的锁。

那么如何正确实现算法呢?

在算法的分布式版本中,我们假设我们有NRedis主节点。这些节点是完全独立的,所以我们不使用复制或任何其他隐式协调系统。我们已经描述了如何在单个实例中安全地获取和释放锁。我们理所当然地认为算法将使用此方法在单个实例中获取和释放锁。在我们的示例中,我们设置N=5,这是一个合理的值,因此我们需要在不同的计算机或虚拟机上运行5Redis主节点,以确保它们以几乎独立的方式失败。

为了获取锁,客户端执行以下操作:

  1. 它以毫秒为单位获取当前时间。
  2. 它尝试按顺序获取所有N个实例中的锁,在所有实例中使用相同的键名和随机值。在步骤2中,在每个实例中设置锁时,客户端使用与总锁自动释放时间相比较小的超时来获取它。例如,如果自动释放时间为10秒,则超时可能在~ 5-50毫秒范围内。这可以防止客户端长时间处于阻塞状态,尝试与已关闭的Redis节点通信:如果实例不可用,我们应该尽快尝试与下一个实例通信。
  3. 客户端通过从当前时间减去在步骤1中获得的时间戳来计算获取锁所经过的时间。当且仅当客户端能够在大多数实例(至少3个)中获取锁,并且获取锁所用的总时间小于锁有效期时,锁被视为已获取。
  4. 如果获取了锁,则其有效时间被视为初始有效时间减去经过的时间,如步骤3中计算的那样。
  5. 如果客户端由于某种原因(无法锁定N/2+1实例或有效时间为负)未能获取锁定,它将尝试解锁所有实例(甚至是它认为无法锁定的实例)。

Redlock安全吗?

为了被认为是安全的,Redlock算法必须满足以下属性:

  1. 安全属性:互斥。在任何给定时刻,只有一个客户端可以持有锁。
  2. 活动属性A:无死锁。最终,始终可以获取锁,即使锁定资源的客户端崩溃或分区也是如此。
  3. 活动属性B:容错。只要大多数Redis节点都已启动,客户端就可以获取和释放锁。

让我们检查各种方案,看看Redlock是否符合这些属性。

首先,让我们假设客户端能够在大多数实例中获取锁。所有实例都将包含一个生存时间相同的密钥。但是,密钥是在不同的时间设置的,因此密钥也会在不同的时间过期。但是,如果第一个密钥在最差时间T1(我们在联系第一个服务器之前采样的时间)被设置为最差,而最后一个密钥在时间T2(我们从最后一个服务器获得回复的时间)被设置为最差,我们确信在MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT期间,集合中第一个过期的密钥将至少存在。所有其他密钥将在稍后过期,因此我们确信至少这次将同时设置密钥。

在设置大多数密钥期间,另一个客户端将无法获取锁,因为如果N/2+1密钥已存在,N/2+1SET NX操作将无法成功。因此,如果获得了锁,则无法同时重新获取它(违反互斥属性)。

但是,我们还希望确保尝试同时获取锁的多个客户端不能同时成功。

如果客户端使用接近或大于锁定最大有效时间(基本上是我们用于SETTTL)的时间锁定了大多数实例,它将认为锁定无效并将解锁实例,因此我们只需要考虑客户端能够在小于有效时间的时间内锁定大多数实例的情况。在这种情况下,对于上面已经表达的参数,因为MIN_VALIDITY没有客户端应该能够重新获取锁。因此,仅当锁定多数的时间大于TTL时间时,多个客户端才能同时锁定N/2+1实例(时间是步骤2的结束),从而使锁定无效。

该系统的活度基于三个主要功能:

  1. 锁的自动释放(因为钥匙过期):最终钥匙可以再次锁定。
  2. 事实上,客户端通常会在未获取锁或获取锁且工作终止时合作移除锁,因此我们可能不必等待密钥过期才能重新获取锁。
  3. 事实上,当客户端需要重试锁时,它等待的时间比获取大多数锁所需的时间要长,以便在资源争用期间概率地使裂脑情况不太可能发生。

但是,我们在网络分区上支付与TTL时间相等的可用性损失,因此如果有连续分区,我们可以无限期地支付此罚款。每次客户端获取锁并在能够删除锁之前被分区掉时,都会发生这种情况。

基本上,如果有无限连续的网络分区,系统可能会在无限的时间内不可用。

代码

可以在 Github 上访问示例代码。让我们分解一下这里实际发生的事情。

通过分布式锁进行领导者选举背后的想法是,谁获得了共享资源的锁,谁就成为领导者。因此,自然而然地,我们有一个与内置C# lock构造非常相似的锁定键。

private const string _resource = "the-thing-we-are-locking-on";

显然,存储是单点故障,因此我们必须确保它是可靠的。我们用于案例RedLock.net允许我们使用多个Redis实例而不是单个实例以提高可靠性。

以下是我们在启动期间创建与Redis的连接的方法。

 var endPoints = new List<RedLockEndPoint>
{
    new DnsEndPoint("redis1", 6379)
    new DnsEndPoint("redis2", 6379)
    new DnsEndPoint("redis3", 6379)
};
_distributedLockFactory = RedLockFactory.Create(endPoints);

如果您需要提供密码,您可以利用RedLockEndPoint

var endpoint = new RedLockEndPoint(new DnsEndPoint("localhost", 49153));
endpoint.Password = "redispw";

每个实例尝试在给定时间段内获取一次锁。如果它成功了,它就会成为领导者。如果没有,它将在以后重试。CreateLockAsync具有接受重试间隔以及获取锁的间隔的重载。该方法将阻塞,直到获取锁或直到参数过期时提供的等待超时。

private async Task TryAcquireLock(CancellationToken token)
{
    if (token.IsCancellationRequested)
        return;

    var distributedLock = await _distributedLockFactory.CreateLockAsync(
        _resource,
        _expiry,
        _wait,
        _retry,
        token);
    if (distributedLock.IsAcquired)
    {
        DoLeaderJob();
    }
}

如您所见,锁在提供的时间后过期,这是RedLock算法自动释放机制的一部分。正如该算法的作者所指出的那样:

没有自动释放机制的分布式锁,锁所有者将无限期地持有它,基本上是无用的。如果持有锁的客户端崩溃并且无法在短时间内以完全状态恢复,则会创建一个死锁,其中分布式锁尝试保护的共享资源永远无法访问。这会产生在大多数情况下不可接受的活动问题,因此理智的分布式锁必须能够自动释放自身。

但是,您不需要在代码中显式重新获取锁,因为它具有自动扩展功能。第一次遇到RedLock.net时,这可能不直观,因此应该注意。简而言之,您可以将其视为RedLock算法中内置的一种领导者健康检查。

如上所述,一旦实例成为利用自动扩展功能的领导者,我们就会摆脱重新获取计时器。实例发生故障后,锁将被释放,并可供其他实例抓取。

总结

正如我们所看到的,通过分布式锁实现领导者选举非常简单。不过,应谨慎使用它,因为每个锁定都会增加微服务实例之间的争用,从而降低水平缩放的好处。

https://www.codeproject.com/Tips/5262384/Distributed-Locking-with-Redlock-net

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值