一、分布式锁
1、分布式系统
(1)、概述
分布式系统是由多个独立的计算机(或节点)组成的系统。这些计算机之间通过网络相互通信协作,以实现共同的目标。从外部来看,分布式系统的行为类似于一个单一的、集中式的系统,但实际上它是由多个物理上分离的组件协同工作的。
(2)、分布式系统的特征
- 多个节点 :分布式系统由多个独立的计算节点组成,每个节点都可以执行任务并处理数据。
- 松耦合 :节点之间通常是松耦合的,它们通过网络进行通信,而不是通过共享内存或直接的硬件连接。
- 并发性 :多个节点可以同时执行任务,因此分布式系统具有并发处理的能力。
- 容错性 :分布式系统通常设计为高可用的,能够容忍部分节点的故障而不影响整体系统的正常运行。
- 透明性 :从用户或应用程序的角度来看,分布式系统的行为应该是透明的,即用户不需要知道系统内部的具体结构和节点之间的通信细节。
- 可扩展性:分布式系统可以通过增加更多的节点来扩展其处理能力和存储容量。
(3)、分布式系统的常见应用场景
- 分布式数据库 :如Cassandra、MongoDB等,允许多个节点存储和管理数据,提供高可用性和水平扩展能力。
- 分布式缓存 :如Redis 集群、Memcached,用于在多个节点之间分布缓存数据,提高读取性能。
- 分布式消息队列 :如Kafka、RabbitMQ,用于在多个生产者和消费者之间传递消息,确保消息的可靠传输。
- 分布式计算框架 :如Hadoop、Spark,用于在多个节点上并行处理大规模数据集。
- 微服务架构 :将应用程序拆分为多个独立的服务,每个服务可以部署在不同的节点上,通过API进行通信。
- Nginx代理的多节点服务等。
2、分布式锁
(1)、概述
在分布式系统中,多个节点可能需要在同一时刻访问和修改共享资源。为了确保这些操作的原子性和一致性,通常需要使用分布式锁(简单说:系统外部的锁)来协调各个节点之间的访问。
分布式锁是一种用于在分布式系统中协调多个节点对共享资源进行互斥访问的机制。它的主要目的是确保在同一时间只有一个节点能够执行某些关键操作,从而避免数据竞争和不一致问题。
(2)、为什么需要分布式锁?
在分布式系统中,多个节点可能同时访问和修改共享资源(如数据库、文件系统、缓存等)。
如果没有适当的同步机制,可能会导致以下问题:
**- 数据竞争:**多个节点同时修改同一资源,导致数据不一致。
- 重复执行 :多个节点同时执行相同的任务,浪费资源并可能导致错误结果。
- 死锁:如果锁没有正确释放,可能会导致其他节点无法获取锁,进而引发死锁。
为了确保这些操作的原子性和一致性,分布式锁则是一种有效的解决方案。
(3)、分布式锁要求
一个理想的分布式锁应该满足以下基本要求:
**- 互斥性:**同一时刻只能有一个客户端持有锁。
**- 安全性:**只有持有锁的客户端才能释放锁,其他客户端不能篡改或删除他人的锁。
- 原子性 :锁的获取、续期和释放操作应该是原子性的,避免竞态条件。
- 自动过期 :如果持有锁的客户端崩溃或超时,锁应该能够自动释放,防止死锁。
- 高可用性:锁服务应该是高可用的,即使部分节点不可用,锁服务仍然可以正常工作。
(4)、分布式锁的实现方式
分布式锁的实现方式有很多种,常见的实现方式包括:
1、基于Redis的分布式锁
Redis是一种高性能的内存数据库,常用于实现分布式锁。Redis提供了丰富的命令集,可以方便地实现锁的获取、释放和续期操作。
- 获取锁:
- 使用SET命令的NX和EX选项来实现。NX确保只有当键不存在时才会设置键,EX设置锁的过期时间。
java
SET lock_key "client_id" NX EX expire_time
- 释放锁:
使用EVAL命令执行Lua脚本,确保只有持有锁的客户端才能释放锁。
java
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
- 续期锁:
使用SET命令的XX和EX选项来延长锁的过期时间。XX确保只有当键已经存在时才会更新其过期时间。
java
SET lock_key "client_id" XX EX expire_time
- Redlock(红锁)算法:
- Redis官方推荐的一种分布式锁算法,通过多个Redis实例来提高锁的可靠性和可用性。
2、基于数据库的分布式锁
一些关系型数据库(如:MySQL)也可以用于实现分布式锁。通常通过在数据库中创建一个唯一的记录来表示锁,并使用事务来确保锁的互斥性。
**- 获取锁:**客户端尝试插入一条记录到数据库中。如果插入成功,则表示获取到了锁;如果插入失败,则表示锁已经被其他客户端持有。
**- 释放锁:**客户端通过删除记录来释放锁。
**- 续期锁:**可以通过定期更新记录的时间戳来延长锁的有效期。
(5)、分布式锁的最佳实践
在使用分布式锁时,建议遵循以下最佳实践,以确保锁的安全性和可靠性:
**- 设置合理的过期时间:**过期时间不宜过短,否则可能会导致锁提前释放;过期时间也不宜过长,否则可能会导致锁长时间无法释放,尤其是在客户端崩溃或超时的情况下。
**- 使用唯一标识符:**每个客户端在获取锁时应使用唯一的标识符(如:UUID),以确保只有持有锁的客户端才能释放锁。
**- 避免死锁:**通过设置合理的过期时间和自动续期机制,确保锁不会因客户端崩溃或超时而导致死锁。
**- 处理网络分区:**在分布式系统中,网络分区是一个常见问题。使用Redlock算法或类似机制可以提高锁的可靠性和可用性。
**- 监控和报警:**监控锁的使用情况,及时发现潜在的问题。如果锁的获取或释放失败,可以设置报警机制,及时通知管理员进行处理。
(6)、分布式锁总结
分布式锁是分布式系统中用于协调多个节点对共享资源进行互斥访问的机制。常见的实现方式包括基于Redis、数据库(有些还有ZooKeeper、Etcd)等。无论选择哪种实现方式,都应确保锁的互斥性、安全性、原子性和高可用性。通过合理设置过期时间、使用唯一标识符和自动续期机制,可以有效避免死锁和误删锁等问题。
二、redis实现分布式锁
为什么Redis可以实现分布式锁?
Redis是一个内存数据库,具有以下特性,使其非常适合实现分布式锁:
- 高并发性能:Redis是单线程模型,所有命令都是原子性的,适合处理高并发场景。
- 持久化支持:Redis支持AOF和RDB持久化,可以确保锁的状态不会因为Redis实例重启而丢失。
- 网络延迟低:Redis通常部署在本地网络中,网络延迟较低,适合快速获取和释放锁。
- 丰富的命令集:Redis提供了SET、GET、DEL、EXPIRE等命令,可以方便地实现锁的获取、释放和过期机制。
Redis实现加锁的几种命令:
redis能用的的加锁命令分表是INCR、SETNX、SET。
最常见的实现方式是使用Redis的SET命令,并结合NX(Not Exist)、EX(Expire)和 PX(毫秒级过期)选项来实现。
1、INCR实现(不常用)
(1)、概述
INCR命令用于对键的值进行自增操作。这种加锁的思路是,key不存在,执行INCR操作,返回值为1,则表示成功获取锁。其它用户在执行INCR操作,返回的数大于1,说明这个锁正在被使用当中,则获取锁失败。
(2)、示例过程
- 1、客户端A请求服务器获取key的值为1,表示获取了锁。
- 2、客户端B也去请求服务器获取key的值为2,表示获取锁失败。
- 3、客户端A执行代码完成,删除锁。
- 4、客户端B在等待一段时间后在去请求的时候获取key的值为1表示获取锁成功。
- 5、客户端B执行代码完成,删除锁。
(3)、操作命令
获取锁:
java
INCR lock_key
释放锁:
java
DEL lock_key
(4)、INCR总结
这种方式简单易懂,但缺乏唯一标识符,无法区分不同的客户端。这意味着任何客户端都可以释放锁,存在误删锁的风险。同时多个客户端可能在同一时间点竞争锁,导致性能下降,尤其是在高并发场景下。
2、SETNX实现(一般)
(1)、概述
SETNX(Set if Not Exists)命令用于在键不存在时设置键的值。如果键已经存在,则不会执行任何操作。通过这种方式,可以确保只有第一个尝试获取锁的客户端能够成功获取锁。
(2)、示例过程
- 1、客户端A请求服务器设置key的值,如果设置成功就表示加锁成功。
- 2、客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败。
- 3、客户端A执行代码完成,删除锁。
- 4、客户端B在等待一段时间后在去请求设置key的值,设置成功。
- 5、客户端B执行代码完成,删除锁。
(3)、操作命令
获取锁
java
SETNX lock_key "lock_123"
expire lock_key 10
- 如果返回值为 1,表示成功获取锁。
- 如果返回值为 0,表示锁已经被其他客户端持有。
释放锁:
释放锁时,不能直接使用 DEL 命令删除键,因为这可能会误删其他客户端的锁。正确的做法是验证锁的持有者(即:client_id),确保只有持有锁的客户端才能删除锁。通常使用Lua 脚本来实现:
java
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
(4)、SETNX总结
SETNX确保了只有当键不存在时才会设置键,保证了锁的互斥性。通过设置唯一的client_id,确保只有持有锁的客户端才能释放锁,避免了误删锁的风险。可以结合EX或PX选项设置锁的过期时间,确保锁不会因客户端崩溃或超时而导致死锁。
但是,SETNX本身没有提供续期功能,如果需要续期锁,必须额外调用EXPIRE或SET命令,增加了代码复杂性。如果在SETNX和EXPIRE之间发生网络延迟或客户端崩溃,可能会导致锁没有正确设置过期时间,从而引发问题。
3、SET实现(最常用)
(1)、概述
SET命令的NX和EX选项可以用于实现分布式锁。NX确保只有当键不存在时才会设置键,EX设置锁的过期时间。通过这种方式,可以一次性完成锁的设置和过期时间的设置,避免了SETNX和EXPIRE之间的竞态条件。
(2)、示例操作
- 1、客户端A请求服务器设置key的值,如果设置成功就表示加锁成功。
- 2、客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败。
- 3、客户端A执行代码过程中,通过XX给锁续期。
- 4、客户端A执行代码完成,删除锁。
- 5、客户端B在等待一段时间后在去请求设置key的值,设置成功。
- 6、客户端B执行代码完成,删除锁。
(3)、操作命令
获取锁:
java
SET lock_key "lock_123" NX EX 10
- 如果返回值为 OK,表示成功获取锁。
- 如果返回值为 nil,表示锁已经被其他客户端持有或者锁已经过期失效。
释放锁:
释放锁时,同样需要验证锁的持有者(即 client_id),确保只有持有锁的客户端才能删除锁。通常使用Lua 脚本来实现:
java
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
续期锁:
续期锁时,可以使用SET命令的XX选项,确保只有当键已经存在时才会更新其过期时间。
java
SET lock_key "lock_123" XX EX 10
(4)、SET总结
优点
- 原子性:SET 命令可以一次性完成键的设置和过期时间的设置,避免了 SETNX 和 EXPIRE 之间的竞态条件。
- 互斥性:NX 确保了只有当键不存在时才会设置键,保证了锁的互斥性。
- 安全性:通过设置唯一的 client_id,确保只有持有锁的客户端才能释放锁,避免了误删锁的风险。
- 自动过期:EX 或 PX 选项可以设置锁的过期时间,确保锁不会因客户端崩溃或超时而导致死锁。
- 续期简单:SET 命令的 XX 选项可以方便地实现锁的续期,确保锁不会因过期而被误释放。
简单来说:
SET命令通过可选项NX,EX结合,一行命令就实现了SETNX和EXPIRE两行命令的操作,进而表现的无懈可击。
4、三种方式的对比总结
5、总结
- INCR:适用于低并发场景,且需要实现可重入锁。但由于缺乏唯一标识符和安全性较弱,不推荐在高并发或对安全性要求较高的场景中使用。
- SETNX:适用于中等并发场景,提供了较好的互斥性和安全性,但需要额外处理续期逻辑,可能会引入竞态条件。
- SET:是目前最推荐的实现方式,特别是在高并发场景中。它提供了良好的互斥性、安全性和自动过期功能,并且续期操作简单。SET命令结合NX和EX选项是实现分布式锁的最佳选择。(推荐)
学海无涯苦作舟!!!