分布式锁怎么实现
分布式锁,是一种跨进程的跨机器节点的互斥锁,它可以用来保证多机器节点对于共享资源访问的排他性。
我觉得分布式锁和线程锁本质上是一样的,线程锁的生命周期是单进程多线程,分布式锁的生命周期是多进程多机器节点。
在本质上,他们都需要满足锁的几个重要特性:
-
排他性,也就是说,同一时刻只能有一个节点去访问共享资源。
-
可重入性,允许一个已经获得锁的线程,在没有释放锁之前再次重新获得锁。
-
锁的获取、释放的方法
-
锁的失效机制,也就是避免死锁的问题
所以,我认为,只要能够满足这些特性的技术组件都能够实现分布式锁。
-
关系型数据库,可以使用唯一约束来实现锁的排他性,如果要针对某个方法加锁,就可以创建一个表包含方法名称字段,并且把方法名设置成唯一的约束。
那抢占锁的逻辑就是:往表里面插入一条数据,如果已经有其他的线程获得了某个方法的锁,那这个时候插入数据会失败,从而保证了互斥性。
这种方式虽然简单啊,但是要实现比较完整的分布式锁,还需要考虑重入性、锁失效机制、没抢占到锁的线程要实现阻塞等,就会比较麻烦。
-
Redis,它里面提供了SETNX命令可以实现锁的排他性,当key不存在就返回1,存在就返回0。然后还可以用expire命令设置锁的失效时间,从而避免死锁问题。当然有可能存在锁过期了,但是业务逻辑还没执行完的情况。所以这种情况,可以写一个定时任务对指定的key进行续期。
Redisson这个开源组件,就提供了分布式锁的封装实现,并且也内置了一个WatchDog机制来对key做续期。
我认为Redis里面这种分布式锁设计已经能够解决99%的问题了,当然如果在Redis 搭建了高可用集群的情况下出现主从切换导致key失效,这个问题也有可能造成多个线程或者进程抢占到同一个锁资源的情况,所以Redis官方也提供了一个RedLock的解决办法,但是实现会相对复杂一些。
在我看来,分布式锁应该是一个CP模型,而Redis是一个AP模型,所以在集群架构下由于数据的一致性问题导致极端情况下出现多个线程抢占到锁的情况很难避免。
那么基于CP模型又能实现分布式锁特性的组件,我认为可以选择Zookeeper或者etcd,
-
在数据一致性方面,zookeeper用到了zab协议来保证数据的一致性,etcd用到了raft算法来保证数据一致性。
-
在锁的互斥方面,zookeeper可以基于有序节点再结合Watch机制实现互斥和唤醒,etcd可以基于Prefix机制和Watch实现互斥和唤醒。
以上就是我对于分布式锁的理解!
Redis分布式锁
分布式锁,是一种跨进程的跨机器节点的互斥锁,它可以用来保证多机器节点对于共享资源访问的排他性。
我使用Redis相关组件Redisson来实现分布式,以特征字符串+设备ID为锁对象,它比直接使用Redis中的setNX方式实现更加优越,(加锁成功时使用UUID+线程ID作为key,解决线程处理完释放其他线程锁的情况)内部具备一个看门狗Watch dog机制,每隔一定时间会对锁状态检查,如果继续持有,则续期,这就避免在锁过期时,线程还在处理业务。
分布式锁和本地锁区别
最直接的就是他们的生命周期不一致,本地锁也就是线程锁,是单进程多线程,而分布式锁的生命周期是多进程多机器节点。
分布式锁应用于分布式系统环境,能够解决多个机器节点对共享资源产生冲突的问题,而本地锁是单台机器节点下多线程环境竞争共享资源产生冲突的问题。他们在实现方面也有所不同,但核心都在于适用于他们的业务场景,比如本地锁,那就要本台机器每个线程都能访问到判断该锁,分布式锁就需要多台机器都能访问到该锁,判断锁状态
Redis内部命令实现分布式锁
一、SETNX 命令实现锁获取
-
SETNX 命令介绍:SETNX(SET if Not eXists)是 Redis 的一个基本命令,它的作用是在指定的键不存在时,设置键的值。如果键已经存在,则不做任何操作。这个特性使得它非常适合用于获取分布式锁,因为我们可以将锁看作是一个特定的键,当一个节点试图获取锁时,就是尝试用 SETNX 命令设置这个锁键的值,如果设置成功,说明该节点成功获取到了锁;如果设置失败,说明锁已经被其他节点持有。
-
示例用法:假设我们要获取一个名为 "lock:resource" 的分布式锁,其中 "resource" 表示要锁定的具体资源(比如一个库存资源、一个文件等)。可以使用以下 Redis 命令来尝试获取锁:
SETNX lock:resource <node_id>
这里的<node_id>
是当前节点的唯一标识(比如节点的 IP 地址和端口号组合、或者一个在分布式系统中唯一的字符串标识等)。如果命令返回 1,表示成功获取到了锁,此时该节点就可以对对应的资源进行操作;如果返回 0,表示锁已经被其他节点持有,该节点需要等待或者采取其他策略(比如重试获取锁等)。
二、设置锁的过期时间
-
为什么要设置过期时间:在分布式环境中,如果一个节点获取到了锁之后,由于某些意外情况(比如进程崩溃、网络故障等)无法正常释放锁,那么这个锁就会一直被占用,导致其他节点永远无法获取到锁来访问相应资源。为了避免这种情况,我们需要给锁设置一个过期时间,即使节点出现问题不能主动释放锁,当过期时间到达后,锁也会自动释放,使得资源可以被其他节点访问。
-
使用 EXPIRE 命令设置过期时间:在成功获取到锁(即 SETNX 命令返回 1)之后,我们可以立即使用 EXPIRE 命令来设置锁的过期时间。例如,假设我们想给刚才获取的 "lock:resource" 锁设置 30 秒的过期时间,可以使用以下命令:
EXPIRE lock:resource 30
这样,即使持有锁的节点出现问题,30 秒后锁也会自动释放。不过,这里存在一个潜在的问题,就是在执行 SETNX 命令和 EXPIRE 命令之间可能会出现故障(比如进程崩溃),导致无法设置过期时间,从而使得锁可能永远不会自动释放。为了解决这个问题,可以使用 Redis 的 SET 命令的扩展形式,它可以在设置键值的同时设置过期时间,下面会详细介绍。
三、使用 SET 命令的扩展形式优化锁获取和过期时间设置
-
SET 命令扩展形式介绍:Redis 的 SET 命令有一个扩展形式,它可以在设置键值的同时设置过期时间,并且还具有原子性,即设置键值和设置过期时间这两个操作是同时完成的,不会出现像上面使用 SETNX 和 EXPIRE 命令那样可能因为中间故障而导致的问题。其语法如下:
SET lock:resource <node_id> EX <seconds> NX
其中,<node_id>
是当前节点的唯一标识,<seconds>
是要设置的过期时间(以秒为单位),"NX" 表示只有当键不存在时才设置键值。使用这个命令就可以在一次操作中完成获取锁并设置过期时间的任务,例如:
SET lock:resource <node_id> EX 30 NX
如果命令返回 OK,表示成功获取到了锁并且设置了 30 秒的过期时间,此时该节点就可以对对应的资源进行操作;如果返回 (nil),表示锁已经被其他节点持有,该节点需要等待或者采取其他策略(比如重试获取锁等)。
四、锁释放操作
-
DEL 命令实现锁释放:当节点完成对相应资源的操作后,需要释放锁,以便其他节点能够获取到锁来访问资源。在 Redis 中,可以使用 DEL 命令来释放锁。假设我们之前获取的锁是 "lock:resource",当要释放锁时,可以使用以下命令:
DEL lock:resource
需要注意的是,在释放锁之前,应该先确认当前节点确实持有该锁,否则可能会错误地释放其他节点持有的锁。一种常见的确认方法是在获取锁时,将当前节点的唯一标识存储在锁键的值中,在释放锁时,先获取锁键的值,查看是否与当前节点的唯一标识相同,如果相同则说明当前节点持有该锁,可以放心地使用 DEL 命令释放锁。
通过以上使用 Redis 内部命令 SETNX、EXPIRE、SET(扩展形式)以及 DEL 等的操作,可以实现一个简单的基于 Redis 的分布式锁机制,不过在实际应用中,还需要考虑一些其他因素,比如锁的重试策略、锁超时处理的优化等,以确保分布式锁的可靠性和有效性。