MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Zookeeper 与 Redis 分布式锁的对比

2023-10-061.9k 阅读

Zookeeper 分布式锁原理

Zookeeper 是一个分布式协调服务框架,其分布式锁的实现基于其树形目录结构和节点特性。在 Zookeeper 中,每个节点被称为 Znode,Znode 可以分为持久节点、临时节点和顺序节点。利用这些特性实现分布式锁的过程如下:

创建锁节点

客户端在 Zookeeper 的指定路径下创建一个临时顺序节点。例如,假设有一个锁路径为/locks,客户端 A 创建的临时顺序节点可能是/locks/lock-0000000001。顺序节点的特点是其节点名称会按照创建顺序自动编号,编号最小的节点代表当前持有锁的客户端。

获取锁

客户端创建完临时顺序节点后,获取/locks路径下所有的子节点,并判断自己创建的节点是否是编号最小的节点。如果是,则获取到锁;如果不是,则监听比自己编号小的前一个节点。例如,客户端 A 创建的节点为/locks/lock-0000000001,客户端 B 创建的节点为/locks/lock-0000000002,那么客户端 B 会监听/locks/lock-0000000001节点。当/locks/lock-0000000001节点被删除(即客户端 A 释放锁)时,Zookeeper 会通知客户端 B,客户端 B 重新获取/locks路径下所有子节点,并判断自己是否是编号最小的节点,如果是则获取到锁。

释放锁

当持有锁的客户端完成任务后,删除自己创建的临时顺序节点。由于是临时节点,在客户端与 Zookeeper 断开连接时,该节点也会自动被删除,从而确保锁能被正确释放。

Redis 分布式锁原理

Redis 是一个基于内存的高性能键值存储数据库,其分布式锁的实现主要依赖于它的单线程原子操作特性。

SETNX 命令实现基本锁

Redis 的SETNX(SET if Not eXists)命令可以实现基本的分布式锁。SETNX key value 命令会在键key不存在时,为键设置指定的值value,并返回 1;如果键key已经存在,则不做任何操作,返回 0。在分布式锁场景中,客户端尝试使用SETNX命令设置一个特定的键值对,例如SETNX lock_key unique_value,其中unique_value是客户端生成的唯一标识(如 UUID)。如果返回 1,说明设置成功,客户端获取到锁;如果返回 0,说明锁已被其他客户端持有,获取锁失败。

锁的释放

客户端完成任务后,需要删除锁对应的键来释放锁。但是在删除锁时,需要确保只有持有锁的客户端才能删除,否则可能会出现误删其他客户端锁的情况。可以在删除锁时,通过判断键的值是否与自己设置的unique_value一致来确保安全性,如先使用GET lock_key获取键的值,然后判断是否与自己的unique_value相等,相等则使用DEL lock_key删除锁。

锁的续约与超时

为了防止持有锁的客户端出现异常导致锁一直无法释放(死锁),通常会为锁设置一个过期时间。在 Redis 中,可以在设置锁时使用SET key value EX seconds命令,其中EX seconds表示设置键的过期时间为seconds秒。另外,为了避免业务执行时间过长导致锁过期,一些场景下还会引入锁续约机制,即客户端在持有锁的过程中,定时检查锁是否即将过期,如果即将过期则重新设置过期时间。

Zookeeper 与 Redis 分布式锁对比

可靠性

  1. Zookeeper:Zookeeper 基于其强一致性的特性,保证了分布式锁的可靠性。由于 Zookeeper 采用了 Zab 协议,在集群环境下,即使部分节点出现故障,也能保证数据的一致性。在锁的获取和释放过程中,Zookeeper 通过监听机制,确保只有前一个持有锁的节点释放锁后,后续节点才能获取锁,不会出现锁的误判或丢失情况。
  2. Redis:Redis 本身是单线程模型,在单个实例下通过原子操作保证了锁操作的原子性。然而,在 Redis 集群环境下,由于数据同步存在一定的延迟,可能会出现短暂的数据不一致。例如,在主从复制模式下,当主节点设置了锁后,还未同步到从节点时主节点故障,从节点晋升为主节点,此时新的主节点可能会允许其他客户端获取相同的锁,导致锁的可靠性受到一定影响。

性能

  1. Zookeeper:Zookeeper 的性能相对较低,因为每次锁的操作(创建节点、获取子节点、监听节点变化等)都需要与 Zookeeper 集群进行网络通信,并且 Zookeeper 采用的 Zab 协议在数据同步时会有一定的开销。尤其在高并发场景下,频繁的网络通信和数据同步可能会导致性能瓶颈。
  2. Redis:Redis 基于内存操作,并且单线程模型避免了线程切换的开销,性能非常高。在分布式锁场景中,通过简单的SETNX等原子操作就能快速完成锁的获取和释放,非常适合高并发场景。不过,随着集群规模的扩大,数据同步和一致性维护的开销可能会对性能产生一定影响,但总体来说,在性能方面 Redis 优于 Zookeeper。

实现复杂度

  1. Zookeeper:Zookeeper 分布式锁的实现相对复杂,需要理解 Zookeeper 的树形结构、节点特性以及监听机制等。代码实现过程中,需要处理节点创建、获取子节点、监听节点变化以及异常处理等多个环节,对开发人员的要求较高。
  2. Redis:Redis 分布式锁的实现相对简单,基本的锁实现只需要使用SETNX命令即可。虽然在实际应用中可能需要考虑锁的释放安全、锁的续约等问题,但总体来说,代码实现相对简洁,开发成本较低。

应用场景

  1. Zookeeper:适用于对可靠性要求极高,对性能要求不是特别苛刻的场景,如分布式系统中的配置管理、选举机制等。在这些场景中,数据的一致性和可靠性是首要考虑因素,Zookeeper 的特性能够很好地满足需求。
  2. Redis:适用于高并发场景下对性能要求较高的分布式锁应用,如电商的秒杀活动、抢红包等场景。在这些场景中,需要快速地获取和释放锁,Redis 的高性能能够满足高并发的需求。

代码示例

Zookeeper 分布式锁代码示例(Java)

以下是使用 Curator 框架实现 Zookeeper 分布式锁的示例代码:

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class ZookeeperDistributedLockExample {
    private static final String ZOOKEEPER_SERVERS = "127.0.0.1:2181";
    private static final String LOCK_PATH = "/locks/my_lock";

    public static void main(String[] args) {
        CuratorFramework client = CuratorFrameworkFactory.builder()
               .connectString(ZOOKEEPER_SERVERS)
               .retryPolicy(new ExponentialBackoffRetry(1000, 3))
               .build();
        client.start();

        InterProcessMutex lock = new InterProcessMutex(client, LOCK_PATH);

        try {
            if (lock.acquire(10, java.util.concurrent.TimeUnit.SECONDS)) {
                try {
                    System.out.println("获取到锁,开始执行业务逻辑");
                    // 模拟业务逻辑
                    Thread.sleep(5000);
                } finally {
                    lock.release();
                    System.out.println("释放锁");
                }
            } else {
                System.out.println("获取锁失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            client.close();
        }
    }
}

Redis 分布式锁代码示例(Java)

以下是使用 Jedis 客户端实现 Redis 分布式锁的示例代码:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisDistributedLockExample {
    private static final String REDIS_SERVER = "127.0.0.1";
    private static final int REDIS_PORT = 6379;
    private static final String LOCK_KEY = "my_lock";
    private static final String UNIQUE_VALUE = "unique_value_123";
    private static final int EXPIRE_TIME = 10; // 锁过期时间,单位秒

    public static void main(String[] args) {
        JedisPoolConfig config = new JedisPoolConfig();
        JedisPool jedisPool = new JedisPool(config, REDIS_SERVER, REDIS_PORT);

        Jedis jedis = jedisPool.getResource();

        try {
            // 使用 SETNX 和 EX 命令获取锁
            String result = jedis.set(LOCK_KEY, UNIQUE_VALUE, "NX", "EX", EXPIRE_TIME);
            if ("OK".equals(result)) {
                System.out.println("获取到锁,开始执行业务逻辑");
                // 模拟业务逻辑
                Thread.sleep(5000);
                // 释放锁
                if (UNIQUE_VALUE.equals(jedis.get(LOCK_KEY))) {
                    jedis.del(LOCK_KEY);
                    System.out.println("释放锁");
                }
            } else {
                System.out.println("获取锁失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            jedis.close();
            jedisPool.close();
        }
    }
}

通过以上对比和代码示例,可以看出 Zookeeper 和 Redis 分布式锁各有优劣,在实际应用中需要根据具体的业务场景和需求来选择合适的分布式锁方案。