掘金 后端 ( ) • 2024-05-07 09:45

highlight: a11y-light theme: mk-cute

引子

在上文的结尾中我提到了redis分布式锁在“主从架构”下失效的情况:比如当redis执行相应命令时,主节点挂掉了,从节点被选为新的主节点,但命令还没来得及同步到从节点,因此高并发场景下,新的请求又会拿到锁,但前一个锁并没有手动释放掉,到过期时间后,就把新请求的锁给释放掉了,那么就又出现并发问题了,本篇文章就将以解决这个问题作为开端来展开。

解决问题

在解决问题之前,我们先要认识一个名词-"CAP"。它是 Consistency(一致性)、Availability(可用性)和 Partition tolerance(分区容忍性)的缩写,这是一个在设计分布式系统时必须考虑的理论。CAP理论指出,一个分布式系统不可能同时满足这三个特性。在不存在网络失败的情况下(分布式系统正常运行时),C和A能够同时保证。只有当网络发生分区或失败时,才会在C和A之间做出选择。对于一个分布式系统而言,P是前提,必须保证,因为只要有网络交互就一定会有延迟和数据丢失,这种状况我们必须接受,必须保证系统不能挂掉。所以只剩下C、A可以选择。

为什么要先科普这个理论呢?因为redis在“主从架构”的前提下遵循的是AP,即保证高可用,也就是说主节点在收到命令执行后,会立即返回执行结果,而不是先往从节点同步数据,这也就是为什么redis主从架构下“锁”依然可能失效的原因。既然问题找到了,我们怎么解决呢?最简单的办法不用redis。

1.zookeeper分布式锁

zookeeper就不过多介绍了,不知道的同学自行百度,使用它的原因在于它追求的是CP,即数据一致性。和redis不同的是:它在接收到线程的命令后,并不会立即返回,而是先给从节点同步数据,从节点同步成功后会返回给主节点,主节点会统计从节点同步数据成功的个数,只有当半数以上的从节点同步成功,主节点才会返回“加锁成功”。

而当主节点挂掉后,从节点重新选举则依靠ZAB机制(Ps:用于实现分布式一致的协议),它通过底层的“选举算法”可以保证选举出来的从节点一定是同步过数据的。具体来说:

在ZooKeeper中,每个事务请求都会被赋予一个全局唯一的事务id,也就是zxid(ZooKeeper Transaction Id)。当主节点向从节点同步数据时,这些数据变更会作为一个事务被赋予一个zxid。 因此,如果一个从节点成功地从主节点那里接收并应用了这些数据变更,那么它的zxid就会更新。这就意味着,同步过数据的节点和未同步过数据的节点的zxid是不一样的。而在选举新的主节点时,ZooKeeper会选择zxid最大的节点,也就是数据最新的节点作为新的主节点。因为zxid最大的节点最有可能是已经同步过最新数据的节点。

2.Redlock算法

Redlock算法是Redis社区提出的一种解决Redis分布式锁失效问题的算法,在使用多个独立Redis实例的情况下,能够提供更高的可靠性和安全性。 Redlock算法的核心思想是:使用多个独立的Redis实例作为锁服务器,当客户端获取锁时,需要在大多数(如3个或5个)独立的Redis实例上设置锁,并在释放锁时需在所有实例上进行操作。只有当大多数实例都设置或释放锁成功时,才认为操作成功。 以下是一个简单的基于Redlock算法的Redis分布式锁的代码demo:

import redis
from redlock import RedLock
def acquire_lock(lock_name, retry_times=3, retry_delay=0.1):
redlocks = RedLock(lock_name, retry_times, retry_delay) for _ in range # 创建3个RedLock实例
acquired_locks = lock.acquire() for lock in redlocks # 在各个实例上尝试获取锁
if acquired_locks.count(True) >= 2: # 大多数实例获取锁成功
return True
else:
release_lock(lock_name)
return False

def release_lock(lock_name):
redlocks = RedLock(lock_name) for _ in range
lock.release() for lock in redlocks # 在所有实例上释放锁
f acquire_lock(“my_lock”):
try:
# 获取到锁后执行需要加锁的操作
print(“Do something…”)
finally:
release_lock(“my_lock”)
else:
print(“Failed to acquire lock”)

以上代码使用了Python Redis客户端及Redlock库,通过创建多个RedLock实例来实现锁的设置和释放。在获取锁时,需要在大多数实例上设置锁,并在释放锁时需在所有实例上进行操作,以保证操作的可靠性。