分布式锁

229 阅读29分钟

一、分布式锁的概念

1.1 从单机锁到分布式锁的演进

在单机应用中,多线程环境下为了保证数据的一致性和完整性,我们常常使用单机锁,如 Java 中的 synchronized 关键字和 ReentrantLock。它们就像在同一屋檐下为不同房间分配钥匙,确保同一时间只有一个线程能进入特定的 “房间”(临界区)访问共享资源 。例如,在一个简单的单机订单处理系统中,当多个线程同时尝试创建订单时,单机锁可以防止订单数据的重复插入或错误更新。

然而,随着业务的增长和技术的发展,分布式系统逐渐成为主流。在分布式系统中,多个服务实例可能分布在不同的服务器上,甚至跨越不同的机房。此时,单机锁就显得力不从心了。因为它们的作用范围仅限于单个 JVM 进程内,无法对跨多个 JVM 的资源访问进行有效控制。这就好比多个不同建筑物中的房间需要统一管理钥匙,单机锁的 “钥匙管理系统” 无法覆盖到这么大的范围。于是,分布式锁应运而生,它就像是一个能够统一管理所有建筑物房间钥匙的系统,确保在分布式环境下,对共享资源的访问能够被有序控制 。

1.2 分布式锁的定义

分布式锁是一种用于分布式系统的同步机制,它的主要作用是在多个服务实例之间协调对共享资源的访问 。其核心在于实现不同节点(服务器)之间对共享资源的互斥访问,确保在任意时刻,只有一个服务实例能够获取到锁,并对共享资源进行操作,其他实例则需要等待锁的释放 。

与单机锁相比,分布式锁的作用范围更广,它跨越了多个进程和服务器,解决了分布式环境下的并发控制问题。例如,在一个分布式电商系统中,商品库存是共享资源,分布式锁可以保证在高并发的抢购场景下,只有一个服务实例能够成功扣减库存,避免超卖现象的发生 。

二、分布式锁的使用场景

2.1 电商系统中的库存管理

在电商系统中,库存管理是一个至关重要的环节,尤其是在秒杀、抢购等活动中,高并发的请求对库存数据的准确性和一致性提出了极高的挑战 。以双十一抢购活动为例,当一款热门商品上架时,可能瞬间会有数十万甚至数百万的用户同时发起购买请求。如果没有有效的并发控制机制,就很容易出现超卖的情况,即卖出的商品数量超过了实际库存 。

假设某商品的库存为 1000 件,有 10000 个用户同时尝试购买。在高并发环境下,如果多个服务实例同时读取库存,都判断库存大于 0,然后进行扣减操作,就会导致库存被多次扣减,最终可能出现超卖。而分布式锁可以有效地解决这个问题。在抢购开始前,每个服务实例在尝试扣减库存前,首先尝试获取分布式锁。只有成功获取到锁的实例,才被允许进行库存扣减操作。其他未获取到锁的实例则需要等待锁的释放,然后再尝试获取锁和扣减库存 。这样就确保了在同一时刻,只有一个服务实例能够对库存进行操作,从而避免了超卖现象的发生 。

2.2 任务调度系统

在分布式任务调度系统中,确保每个任务仅被一个工作节点执行是非常关键的,否则可能会导致任务重复执行,产生数据不一致或其他错误 。例如,在一个电商系统中,每天凌晨需要执行一次订单统计任务,统计前一天的订单数量、金额等信息,并生成报表 。如果没有分布式锁的控制,当有多个工作节点同时运行这个任务调度程序时,可能会出现多个节点同时执行订单统计任务的情况。这不仅会浪费系统资源,还可能导致统计数据不准确,因为多个节点可能会重复统计相同的订单 。

引入分布式锁后,每个工作节点在执行任务前,首先尝试获取分布式锁。如果获取成功,说明该节点获得了执行任务的权限,可以开始执行订单统计任务。在任务执行过程中,锁一直被该节点持有,其他节点无法获取到锁,也就不能执行该任务。当任务执行完毕后,该节点释放锁,其他节点才有机会获取锁并执行任务 。这样就保证了订单统计任务在分布式环境下只会被一个工作节点执行一次,确保了任务执行的准确性和数据的一致性 。

2.3 分布式系统中的资源分配

在分布式系统中,多个进程或线程可能同时需要访问共享资源,如数据库连接池、文件系统中的共享文件等。如果没有适当的同步机制,就可能会出现资源冲突和死锁的问题 。例如,在一个分布式数据库访问系统中,多个服务实例可能同时需要从数据库连接池中获取数据库连接。如果没有分布式锁的控制,可能会出现多个实例同时获取到同一个数据库连接的情况,这会导致数据访问错误和数据库连接池的混乱 。

分布式锁可以通过互斥访问的方式,避免资源冲突和死锁的发生。在上述数据库连接池的例子中,每个服务实例在获取数据库连接前,先尝试获取分布式锁。只有获取到锁的实例,才能从数据库连接池中获取连接。在使用完连接后,该实例释放锁,其他实例才可以获取锁并获取连接 。这样就保证了在同一时刻,只有一个服务实例能够访问数据库连接池中的共享连接,避免了资源冲突 。同时,合理的分布式锁设计和使用,可以避免死锁的发生,确保系统的稳定性和可靠性 。

三、分布式锁的操作原理

3.1 核心操作步骤

分布式锁的操作主要包括三个核心步骤:获取锁、占有资源、释放锁

获取锁是分布式锁操作的第一步,当一个服务实例需要访问共享资源时,它会向分布式锁服务发送获取锁的请求。这个过程就像是在一个共享的钥匙管理系统中申请一把特定的钥匙。以基于 Redis 的分布式锁为例,通常会使用 SETNX(SET if Not eXists) 命令来尝试获取锁。比如,执行 SETNX lock_key value,其中 lock_key 是锁的唯一标识,value 可以是一些与请求相关的信息,如请求 ID。如果 SETNX 命令执行成功,返回值为 1,表示获取锁成功,该服务实例获得了访问共享资源的权限;如果返回值为 0,则表示锁已被其他实例持有,获取锁失败,该实例需要等待或进行重试 。

一旦服务实例成功获取到锁,就可以占有共享资源并进行相应的操作。这就如同拿到钥匙后可以打开对应的房间,对房间内的物品进行使用或修改。在这个阶段,其他未获取到锁的服务实例无法访问该共享资源,只能等待锁的释放。例如,在电商系统的库存管理中,获取到锁的服务实例可以对库存数据进行扣减操作,而其他实例则需要等待,直到该实例完成操作并释放锁 。

当服务实例完成对共享资源的操作后,需要及时释放锁,以便其他实例能够获取锁并访问共享资源。释放锁的过程就像是将钥匙归还到钥匙管理系统中。对于基于 Redis 的分布式锁,通常使用 DEL 命令来删除锁的键值对,即执行 DEL lock_key,从而释放锁资源。确保锁的及时释放非常重要,如果锁没有被正确释放,可能会导致其他实例无法获取锁,从而造成资源的浪费和业务的阻塞 。

3.2 关键特性

分布式锁具有多个关键特性,这些特性对于保证分布式系统中共享资源的正确访问和系统的稳定性至关重要 。

互斥性是分布式锁最核心的特性,它确保在同一时刻,只有一个服务实例能够获取到锁并访问共享资源,从而避免多个实例同时对共享资源进行操作导致的数据不一致问题。例如,在分布式数据库访问中,如果多个实例同时对同一数据进行写入操作,可能会导致数据的覆盖和丢失,而互斥性可以有效防止这种情况的发生 。

一致性要求加锁和释放锁的过程能够准确地反映锁的状态,并且在分布式环境中的各个节点上保持一致。这意味着无论从哪个节点获取锁的状态信息,都应该得到相同的结果。例如,在一个跨多个数据中心的分布式系统中,不同数据中心的节点对锁的状态认知应该是一致的,否则可能会出现某个节点认为锁已释放,而其他节点认为锁仍被持有的情况,从而导致并发访问问题 。

可重入性是指同一个服务实例在持有锁的情况下,可以再次获取同一个锁,而不会被阻塞。这在一些递归调用或需要多次访问共享资源的场景中非常重要。例如,在一个递归的文件处理方法中,每次递归调用都可能需要访问共享的文件资源,如果没有可重入性,可能会导致自己被自己阻塞。以 Java 中的 ReentrantLock 为例,它是可重入的,在分布式锁中也需要具备类似的特性 。

锁租期是指锁的有效时间,为了防止服务实例在获取锁后出现异常而导致锁无法释放,从而造成死锁的情况,通常会为分布式锁设置一个过期时间。例如,在基于 Redis 的分布式锁中,可以在设置锁时指定过期时间,如SET lock_key value EX 10,表示锁的过期时间为 10 秒。如果在这个时间内服务实例没有完成操作并释放锁,锁会自动过期并被释放,其他实例就可以获取锁 。

在分布式系统中,大量的服务实例可能会频繁地获取和释放锁,因此分布式锁的性能至关重要。高效的分布式锁应该能够快速地处理锁的获取和释放请求,减少等待时间,提高系统的并发处理能力。例如,基于缓存(如 Redis)的分布式锁通常比基于数据库的分布式锁具有更好的性能,因为缓存的读写速度更快 。

四、Redis 自定义实现分布式锁

4.1 基于 SET 命令的加锁实现

在 Redis 中,我们可以使用SET命令来实现分布式锁的加锁操作 。SET命令具有丰富的参数选项,通过合理配置这些参数,能够确保加锁操作的原子性和有效性 。

以下是使用SET命令加锁的 Java 代码示例,这里使用 Jedis 库来操作 Redis :

import redis.clients.jedis.Jedis;

public class RedisDistributedLock {
    private static final String LOCK_KEY = "my_distributed_lock";
    private static final String LOCK_VALUE = "unique_value";
    private static final int EXPIRE_TIME = 10; // 锁的过期时间,单位秒

    public boolean lock(Jedis jedis) {
        String result = jedis.set(LOCK_KEY, LOCK_VALUE, "NX", "EX", EXPIRE_TIME);
        return "OK".equals(result);
    }
}

在上述代码中,jedis.set(LOCK_KEY, LOCK_VALUE, "NX", "EX", EXPIRE_TIME) 这行代码执行了加锁操作 。其中,LOCK_KEY 是锁的键,它在整个分布式系统中必须是唯一的,用于标识这把特定的锁;LOCK_VALUE 是锁的值,这里设置为一个唯一值,用于区分不同的加锁请求,在实际应用中,可以使用 UUID 等方式生成唯一值;"NX" 参数表示 SET if Not eXists,即只有当 LOCK_KEY 不存在时,才会设置键值对,这保证了在同一时刻,只有一个客户端能够成功设置锁,从而实现了互斥性;"EX" 参数用于设置键的过期时间,后面跟着的 EXPIRE_TIME 表示锁的过期时间为 10 秒,这是为了防止因程序异常等原因导致锁无法释放,从而造成死锁的情况 。

如果 SET 命令执行成功,返回值为 "OK",表示加锁成功;如果 LOCK_KEY 已经存在,SET 命令不会执行任何操作,返回值为 null,表示加锁失败 。通过这种方式,我们利用 Redis 的 SET 命令实现了分布式锁的加锁操作,并且保证了加锁过程的原子性,避免了在高并发场景下可能出现的竞争条件和数据不一致问题 。

4.2 使用 Lua 脚本解锁

解锁操作同样需要保证原子性,以避免在多线程或多客户端环境下出现误解锁的情况 。在 Redis 中,我们可以借助 Lua 脚本的原子性执行特性来实现安全的解锁操作 。

下面是一个使用 Lua 脚本解锁的 Java 代码示例,同样基于 Jedis 库 :

import redis.clients.jedis.Jedis;
import java.util.Arrays;
import java.util.List;

public class RedisDistributedLock {
    private static final String LOCK_KEY = "my_distributed_lock";
    private static final String LOCK_VALUE = "unique_value";

    public boolean unlock(Jedis jedis) {
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        List<String> keys = Arrays.asList(LOCK_KEY);
        List<String> args = Arrays.asList(LOCK_VALUE);
        Object result = jedis.eval(luaScript, keys, args);
        return 1L.equals(result);
    }
}

在这段代码中,luaScript 定义了一个 Lua 脚本,其逻辑为:首先通过 redis.call('get', KEYS[1]) 获取锁的键对应的值,然后将其与传入的 ARGV[1](即加锁时设置的LOCK_VALUE)进行比较 。如果两者相等,说明当前客户端是锁的持有者,此时执行 redis.call('del', KEYS[1]) 删除锁的键值对,从而实现解锁操作,并返回 1;如果两者不相等,说明锁已被其他客户端持有或者锁的状态已发生变化,此时不执行删除操作,返回 0 。

jedis.eval(luaScript, keys, args) 这行代码用于在 Redis 中执行 Lua 脚本 。其中,keys 参数传递了锁的键 LOCK_KEY,args 参数传递了加锁时设置的 LOCK_VALUE 。执行该脚本后,返回的结果 result 会被判断,如果结果为 1L,表示解锁成功;否则,表示解锁失败 。

通过使用 Lua 脚本,我们确保了解锁操作的原子性,即检查锁的持有者和删除锁这两个步骤是作为一个不可分割的整体执行的,避免了在检查锁的持有者之后、删除锁之前,锁的状态被其他客户端改变的情况,从而保证了解锁操作的安全性和可靠性 。

五、Redisson 分布式锁的使用

5.1 Redisson 简介

Redisson 是一个基于 Redis 的 Java 驻留客户端,它在分布式锁领域占据着重要地位 。与其他分布式锁实现方案相比,Redisson 具有诸多显著优势 。它对 Redis 进行了深度封装和扩展,不仅提供了简单易用的分布式锁功能,还支持多种复杂的数据结构和分布式服务,如分布式集合、分布式对象、分布式队列等 。这使得开发者在构建分布式系统时,能够更加便捷地利用 Redis 的强大功能,而无需花费大量时间和精力去处理底层的细节 。

Redisson 的设计目标是简化分布式系统的开发,它提供了丰富的 API,这些 API 设计得非常直观,易于理解和使用。通过 Redisson,开发者可以轻松地创建分布式锁对象,并进行加锁、解锁等操作,就像在使用本地锁一样自然 。此外,Redisson 还支持多种锁模式,如可重入锁、公平锁、读写锁等,能够满足不同场景下的并发控制需求 。

5.1 Redisson 简介

Redisson 是一个基于 Redis 的 Java 驻留客户端,它在分布式锁领域占据着重要地位 。与其他分布式锁实现方案相比,Redisson 具有诸多显著优势 。它对 Redis 进行了深度封装和扩展,不仅提供了简单易用的分布式锁功能,还支持多种复杂的数据结构和分布式服务,如分布式集合、分布式对象、分布式队列等 。这使得开发者在构建分布式系统时,能够更加便捷地利用 Redis 的强大功能,而无需花费大量时间和精力去处理底层的细节 。

Redisson 的设计目标是简化分布式系统的开发,它提供了丰富的 API,这些 API 设计得非常直观,易于理解和使用。通过 Redisson,开发者可以轻松地创建分布式锁对象,并进行加锁、解锁等操作,就像在使用本地锁一样自然 。此外,Redisson 还支持多种锁模式,如可重入锁、公平锁、读写锁等,能够满足不同场景下的并发控制需求 。

5.2 基本使用方法

在使用 Redisson 获取分布式锁时,首先需要引入 Redisson 的依赖 。以 Maven 项目为例,在 pom.xml 文件中添加如下依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.15.6</version>
</dependency>

接下来,配置 Redisson 客户端 。可以通过代码方式进行配置,如下所示:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonConfig {
    public static RedissonClient getRedissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}

上述代码中,config.useSingleServer().setAddress("redis://[127.0.0.1:6379](http://127.0.0.1:6379)") 表示使用单机模式连接 Redis,地址为 [127.0.0.1:6379](http://127.0.0.1:6379)。在实际应用中,可根据 Redis 的部署情况,选择合适的连接方式,如集群模式、哨兵模式等 。

获取分布式锁的示例代码如下:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

public class RedissonLockExample {
    public static void main(String[] args) {
        RedissonClient redissonClient = RedissonConfig.getRedissonClient();
        RLock lock = redissonClient.getLock("my_distributed_lock");
        try {
            // 尝试获取锁,等待时间为3秒,锁的过期时间为10秒
            boolean isLocked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (isLocked) {
                // 获取锁成功,执行业务逻辑
                System.out.println("获取锁成功,开始执行业务逻辑");
                // 模拟业务操作
                Thread.sleep(5000);
            } else {
                // 获取锁失败
                System.out.println("获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                System.out.println("释放锁成功");
            }
        }
        redissonClient.shutdown();
    }
}

在上述代码中,redissonClient.getLock("my_distributed_lock") 通过 Redisson 客户端获取一个名为 my_distributed_lock 的锁对象 。lock.tryLock(3, 10, TimeUnit.SECONDS) 尝试获取锁,其中第一个参数3表示最大等待时间为 3 秒,第二个参数10表示锁的过期时间为 10 秒 。如果在 3 秒内成功获取到锁,tryLock 方法返回 true,此时可以执行业务逻辑;如果在 3 秒内未能获取到锁,tryLock 方法返回 false 。

在业务逻辑执行完毕后,需要释放锁 。通过 lock.unlock() 方法释放锁,但在释放锁之前,需要先判断当前线程是否持有该锁,即 lock.isHeldByCurrentThread(),以避免误释放其他线程持有的锁 。

常见的配置参数包括连接地址、密码、数据库编号、连接池大小等 。例如,在配置文件中可以进行如下配置:

redisson:
  singleServerConfig:
    address: "redis://127.0.0.1:6379"
    password: "your_password"
    database: 0
    connectionPoolSize: 100
    connectionMinimumIdleSize: 10

其中,address 指定 Redis 的连接地址,password 为 Redis 的密码(如果有的话),database 指定使用的 Redis 数据库编号,connectionPoolSize 设置连接池的大小,connectionMinimumIdleSize 设置连接池的最小空闲连接数 。合理配置这些参数,能够提高 Redisson 客户端与 Redis 的连接性能和稳定性,以适应不同的业务场景需求 。

六、Redisson 的底层实现原理

6.1 加锁机制

Redisson 的加锁机制主要依赖于 Redis 的 Lua 脚本和数据结构 。当一个客户端尝试获取锁时,Redisson 会向 Redis 发送一段 Lua 脚本,这段脚本会执行一系列操作 。

假设我们有一个名为my_lock的锁,对应的 Lua 脚本逻辑大致如下:

if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hset', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
return redis.call('pttl', KEYS[1]);

在这段脚本中,KEYS[1] 表示锁的键,即 my_lock;ARGV[1] 表示锁的过期时间,默认为 30 秒;ARGV[2] 是一个由客户端生成的唯一标识,通常包含客户端的 UUID 和线程 ID,用于标识加锁的客户端和线程 。

脚本首先通过 exists 命令检查锁的键是否存在 。如果不存在,说明锁未被占用,此时使用 hset 命令在 Redis 中创建一个哈希表,以锁的键为哈希表的键,以 ARGV[2] 为字段,值设为 1,表示该客户端获取到了锁 。然后使用 pexpire 命令设置锁的过期时间,防止因客户端异常导致锁无法释放 。

如果锁的键已经存在,脚本会通过 hexists 命令检查哈希表中是否存在 ARGV[2] 这个字段 。如果存在,说明是同一个客户端再次尝试获取锁,即可重入加锁的情况,此时使用 hincrby 命令将该字段的值加 1,表示重入次数增加 。同样,也会使用 pexpire 命令更新锁的过期时间 。

如果锁已被其他客户端持有,脚本会返回锁的剩余生存时间,客户端可以根据这个时间决定是否继续等待或进行重试 。通过这种方式,Redisson 利用 Redis 的原子操作和 Lua 脚本的原子性执行特性,确保了加锁过程的原子性和可靠性 。

6.2 锁互斥机制

Redisson 的锁互斥机制是其实现分布式锁的关键特性之一,它确保在同一时间只有一个客户端能够成功获取到锁 。

在加锁过程中,当一个客户端尝试获取锁时,如上述 Lua 脚本所示,首先会检查锁的键是否存在 。如果锁的键不存在,该客户端可以成功获取锁,并设置相应的哈希表和过期时间 。此时,其他客户端尝试获取同一把锁时,由于锁的键已经存在,通过 hexists 检查发现哈希表中的字段不是自己的唯一标识,从而判断锁已被其他客户端持有,获取锁失败 。

例如,假设有客户端 A 和客户端 B 同时尝试获取名为 my_lock 的锁 。客户端 A 首先执行加锁操作,成功获取到锁,并在 Redis 中创建了以 my_lock 为键的哈希表,字段为客户端 A 的唯一标识,值为 1 。当客户端 B 尝试获取锁时,Lua 脚本执行到 exists 命令,发现 my_lock 键已存在,再通过 hexists 检查发现哈希表中的字段不是自己的唯一标识,所以客户端 B 获取锁失败,只能等待锁的释放 。

这种基于 Redis 数据结构和 Lua 脚本的互斥机制,保证了在分布式环境下,同一把锁在同一时间只能被一个客户端持有,有效地避免了并发访问共享资源时可能出现的数据不一致问题 。

6.3 Watch Dog 自动延期机制

Redisson 的 Watch Dog 自动延期机制是为了解决在业务处理过程中,锁的过期时间设置不当可能导致的问题 。当一个客户端获取锁时,如果没有显式指定锁的过期时间,Redisson 会启动 Watch Dog 机制 。

Watch Dog 机制会在客户端持有锁的过程中,定期检查锁的剩余有效期 。默认情况下,Watch Dog 会每隔 10 秒检查一次锁的状态 。当锁的剩余有效期小于 15 秒(默认过期时间 30 秒的一半)时,Watch Dog 会自动向 Redis 发送命令,延长锁的过期时间,使其重新回到 30 秒 。

假设一个客户端获取锁后,开始执行复杂的业务逻辑,预计需要 40 秒才能完成 。如果没有 Watch Dog 机制,在 30 秒后锁会自动过期,可能会导致其他客户端获取到锁,从而引发并发问题 。但由于有了 Watch Dog 机制,在第 20 秒时,Watch Dog 检查到锁的剩余有效期小于 15 秒,会自动延长锁的过期时间,使得该客户端能够在 40 秒内安全地持有锁,完成业务逻辑 。

通过这种自动延期机制,Redisson 在保证系统安全性的同时,提高了系统的灵活性和可靠性,避免了因业务处理时间超出预期而导致锁提前释放的问题 。

6.4 可重入加锁机制

Redisson 的可重入加锁机制允许同一个客户端在持有锁的情况下,多次获取同一把锁,而不会被阻塞 。这一机制主要通过 Redis 的哈希表数据结构来实现 。

在加锁的 Lua 脚本中,当检查到锁的键已经存在时,会进一步通过 hexists 命令检查哈希表中是否存在当前客户端的唯一标识 。如果存在,说明是同一个客户端再次尝试获取锁,即可重入加锁的情况 。此时,会使用 hincrby 命令将该字段的值加 1,表示重入次数增加 。

例如,有一个递归的业务方法,每次递归调用都需要获取分布式锁 。当第一次调用获取锁时,在 Redis 中创建了哈希表,字段为客户端的唯一标识,值为 1 。在递归调用中,再次尝试获取锁时,Lua 脚本检测到锁的键已存在,且哈希表中存在自己的唯一标识,于是将该字段的值加 1,变为 2 。这表明该客户端对这把锁进行了两次重入 。

在释放锁时,Redisson 会相应地减少重入次数 。当重入次数减为 0 时,才会真正删除锁的键值对,释放锁资源 。例如,上述递归方法执行完毕后,在释放锁的过程中,每次调用解锁操作,都会将哈希表中对应字段的值减 1 。当重入次数变为 0 时,执行del命令删除锁的键,从而完成锁的释放 。通过这种方式,Redisson 实现了可重入加锁机制,满足了在一些需要递归或多次访问共享资源场景下的需求 。

七、ZK 实现分布式锁的实现

7.1 ZK 节点特性与分布式锁的结合

Zookeeper(简称 ZK)是一个分布式协调服务,它提供了丰富的功能,其中节点特性为实现分布式锁提供了坚实的基础 。

ZK 中有四种类型的节点:持久节点、持久顺序节点、临时节点和临时顺序节点 。在实现分布式锁时,临时顺序节点发挥了关键作用 。临时顺序节点具有两个重要特性:一是临时性,当客户端与 ZK 的会话结束或连接断开时,该临时顺序节点会自动被删除;二是顺序性,当多个客户端在同一父节点下创建临时顺序节点时,ZK 会为这些节点分配一个单调递增的序号 。

我们可以利用这些特性来实现分布式锁。例如,假设有多个客户端同时竞争一把分布式锁,它们会在 ZK 的某个指定父节点(如/lock)下创建临时顺序节点 。由于节点的顺序性,每个节点都有一个唯一的序号,通过比较这些序号,最小序号的节点对应的客户端就可以获得锁 。而其他客户端创建的节点序号较大,无法获取锁,它们会监听比自己序号小的前一个节点 。当持有锁的客户端完成操作后,对应的临时顺序节点会被删除(由于临时性),此时监听该节点的客户端会收到通知,然后重新检查自己是否可以获取锁 。

7.2 加锁与解锁流程

使用 ZK 实现分布式锁的加锁流程如下:

  1. 创建持久节点:首先,在 ZK 中创建一个持久节点,例如/lock,这个节点作为所有锁竞争的根节点,它会一直存在于 ZK 中 。
  1. 创建临时顺序节点:每个需要获取锁的客户端在/lock节点下创建一个临时顺序节点 。例如,客户端 A 创建了/lock/lock - 0000000001,客户端 B 创建了/lock/lock - 0000000002,这些节点的序号是根据创建时间顺序分配的 。
  1. 判断是否获取锁:客户端创建完临时顺序节点后,获取/lock节点下的所有子节点,并按照序号进行排序 。如果自己创建的节点序号最小,那么该客户端就获取到了锁,可以开始执行临界区的业务逻辑 。例如,客户端 A 创建的节点序号最小,它就获得了锁 。
  1. 监听前序节点:如果客户端创建的节点序号不是最小的,说明锁已被其他客户端获取,此时该客户端需要监听比自己序号小的前一个节点 。例如,客户端 B 创建的节点序号大于客户端 A,客户端 B 就需要监听客户端 A 创建的节点/lock/lock - 0000000001 。当这个前序节点被删除时(即持有锁的客户端释放锁),监听的客户端会收到通知,然后重新回到步骤 3,再次判断自己是否可以获取锁 。

解锁流程相对简单:

  1. 删除临时顺序节点:当持有锁的客户端完成业务逻辑后,它会删除自己创建的临时顺序节点 。例如,客户端 A 完成业务后,删除/lock/lock - 0000000001节点 。
  1. 通知监听客户端:由于其他客户端在等待锁的过程中监听了前序节点,当持有锁的客户端删除节点后,监听该节点的客户端会收到 ZK 的通知 。这些客户端会重新获取/lock节点下的所有子节点并排序,判断自己是否可以获取锁 。例如,客户端 B 收到通知后,发现自己创建的节点现在是序号最小的,于是客户端 B 获取到锁,开始执行自己的业务逻辑 。

通过这种方式,利用 ZK 的临时顺序节点特性和事件监听机制,实现了分布式锁的加锁和解锁操作,确保了在分布式环境下对共享资源的互斥访问 。

八、总结

分布式锁在分布式系统中扮演着至关重要的角色,它是保障系统数据一致性和业务正确性的关键工具 。在电商、任务调度、资源分配等众多场景中,分布式锁的应用有效地解决了高并发环境下的资源竞争问题,确保了系统的稳定运行 。

不同的分布式锁实现方式各有优缺点 。基于 Redis 自定义实现分布式锁,具有简单直接、性能较高的特点,但在锁的管理和复杂场景支持方面存在一定局限性 。Redisson 分布式锁则对 Redis 进行了深度封装,提供了丰富的功能和良好的性能,如可重入锁、Watch Dog 自动延期机制等,适用于大多数分布式场景,但需要引入额外的依赖 。ZK 实现的分布式锁,利用其节点特性和事件监听机制,保证了锁的强一致性和可靠性,尤其适用于对一致性要求极高的场景,但其性能相对较低,实现也较为复杂 。

展望未来,随着分布式系统的不断发展和应用场景的日益丰富,分布式锁的发展趋势也值得关注 。一方面,在性能优化上,会不断探索更高效的算法和数据结构,以提高锁的获取和释放速度,满足高并发场景下对系统性能的严格要求 。例如,研究如何进一步优化 Redis 的 Lua 脚本执行效率,或者探索新的分布式缓存技术来实现更快速的锁操作 。另一方面,在功能扩展方面,会更加注重对复杂业务场景的支持,如分布式事务中的锁管理、跨多个数据中心的分布式锁一致性等 。同时,随着云计算、大数据等新兴技术的发展,分布式锁也需要更好地与这些技术融合,以适应新的架构和应用需求 。例如,在云原生环境中,如何实现分布式锁的自动化部署、动态配置和弹性扩展,将是未来研究的重要方向 。