分布式锁的碎碎念

在分布式系统中,多个服务或节点通常会并发地访问共享资源,这可能导致数据冲突、并发问题或不一致性。为了解决这些问题,分布式锁应运而生。分布式锁能够确保同一时间内,只有一个客户端能够访问某个共享资源,从而避免资源竞争和数据不一致的问题。

分布式锁的概念

分布式锁是一种在分布式系统中控制访问共享资源的机制。它通过锁定资源来确保同一时刻只有一个进程或线程能执行某个操作,防止其他进程或线程同时访问同一资源。

分布式锁的特点

  • 原子性:分布式锁必须保证原子性,即某个操作一旦获得锁后,其他操作无法访问锁定资源。
  • 可重入性:锁应该能够被同一个客户端多次获取,避免死锁。
  • 可靠性:即使发生网络或系统故障,锁也应该能够正确释放,并保证不会导致死锁或资源无法访问。
  • 可扩展性:分布式锁通常用于多个服务或节点,能够支持大规模分布式系统。

常见的分布式锁实现方案

1. 基于 Redis 实现分布式锁

Redis 是常见的分布式锁实现技术之一。利用 Redis 的原子性操作(如 SETNXEXPIRE)可以轻松实现分布式锁。

使用 Redis SETNX 实现分布式锁

Redis 提供了 SETNX(SET if Not Exists)命令,它能保证当且仅当某个键不存在时才会成功设置键值。这可以用于在分布式系统中实现锁的功能。配合过期时间设置,可以防止死锁的发生。

Redis 分布式锁的实现步骤
  1. 尝试获取锁:客户端向 Redis 发送 SETNX 命令,将一个表示锁的键存入 Redis 中。如果该键已存在,则获取锁失败;如果不存在,则获取锁成功。
  2. 设置过期时间:为了防止因为客户端崩溃等原因导致锁无法释放,可以在设置锁时指定一个过期时间。如果在锁的持有者未释放锁的情况下,锁过期,其他客户端可以重新获取锁。
  3. 执行业务逻辑:获取锁后,客户端可以执行业务操作。
  4. 释放锁:业务执行完成后,客户端需要删除锁标识,以释放锁资源。
Redis 分布式锁的实现代码
public boolean acquireLock(String lockKey, String lockValue, long expireTime) {
    // 尝试设置锁
    if (jedis.setnx(lockKey, lockValue) == 1) {
        // 设置锁的过期时间
        jedis.expire(lockKey, expireTime);
        return true;
    }
    return false;
}

public boolean releaseLock(String lockKey, String lockValue) {
    // 仅当当前客户端的值与锁的值相等时,才删除锁
    if (lockValue.equals(jedis.get(lockKey))) {
        jedis.del(lockKey);
        return true;
    }
    return false;
}
  • 获取锁SETNX(lockKey, lockValue) 命令尝试在 Redis 中设置一个键 lockKey,并使用 lockValue 作为值。若键不存在,则返回 1,表示获取锁成功;否则,表示锁已经被占用。
  • 设置过期时间jedis.expire(lockKey, expireTime) 为锁设置一个过期时间,防止死锁。
  • 释放锁:通过判断当前客户端的锁值和 Redis 中存储的锁值是否一致,确保只有持有锁的客户端可以释放锁。

2. 基于 Redis Redisson 实现分布式锁

Redisson 是 Redis 官方提供的客户端库,它提供了更加高级和易用的分布式锁实现。Redisson 是通过 Redis 实现分布式锁、分布式集合等数据结构的工具。

Redisson 实现分布式锁的示例代码
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);

RLock lock = redisson.getLock("lockKey");
lock.lock();  // 获取锁

try {
    // 执行业务逻辑
    processBusinessLogic();
} finally {
    lock.unlock();  // 释放锁
}
  • Redisson RLock:通过 RLock 对象获取锁,lock.lock() 阻塞式地等待获取锁,lock.unlock() 释放锁。
  • 自动过期:Redisson 会自动设置锁的过期时间,以避免因为客户端异常崩溃等原因导致锁永远无法释放。

Redisson 使得分布式锁的实现变得更加简便,而且它还包括了超时控制、重试机制等功能,能够有效防止死锁。

3. 基于 Zookeeper 实现分布式锁

Zookeeper 作为一个分布式协调工具,除了用于配置管理、分布式注册中心等功能外,还可以用来实现分布式锁。Zookeeper 保证了分布式环境下的顺序一致性,因此可以使用 Zookeeper 的节点顺序特性来实现锁。

Zookeeper 实现分布式锁
  1. 创建临时顺序节点:每个客户端在 Zookeeper 中创建一个临时顺序节点,节点名称包含顺序号(例如,/lock-000000001)。
  2. 等待前一个节点释放锁:客户端会检查自己前面的节点(顺序号小于自己的节点),如果前面没有节点,表示可以获得锁;如果有前一个节点,则客户端等待前一个节点的删除。
  3. 释放锁:当客户端业务执行完毕后,删除自己的节点,从而释放锁。
Zookeeper 分布式锁的示例代码
// Zookeeper 客户端连接
ZooKeeper zk = new ZooKeeper("localhost:2181", 3000, null);

// 创建临时顺序节点
String lockPath = zk.create("/lock-", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

// 获取当前节点的顺序号
List<String> children = zk.getChildren("/", false);
Collections.sort(children);

// 判断是否是最小的节点
if (lockPath.equals("/" + children.get(0))) {
    // 获得锁
    processBusinessLogic();
    // 释放锁
    zk.delete(lockPath, -1);
} else {
    // 等待前一个节点的删除
    String prevNode = "/" + children.get(children.indexOf(lockPath) - 1);
    Stat stat = zk.exists(prevNode, true);  // 监视前一个节点
    if (stat == null) {
        // 前一个节点被删除,获取锁
        processBusinessLogic();
        zk.delete(lockPath, -1);
    }
}

Zookeeper 基于顺序节点实现分布式锁,能够确保在集群环境下只有一个节点能够持有锁。

4. 基于 Etcd 实现分布式锁

Etcd 是一个分布式键值存储,通常用于服务发现和配置管理,也可以用来实现分布式锁。Etcd 提供了类似 Zookeeper 的分布式一致性机制,通过利用其原子性操作和租约(lease)功能实现锁。

Etcd 分布式锁
  1. 创建租约:客户端首先请求创建一个租约,租约会在指定的时间内过期。
  2. 设置临时键:通过设置临时键来表示锁,键值为租约 ID。
  3. 检查锁:其他客户端尝试创建相同的临时键,若已存在,表示锁已被占用。

Etcd 实现分布式锁示例

Client client = Client.builder().endpoints("http://localhost:2379").build();
KV kv = client.getKVClient();

// 创建租约
Lease lease = client.getLeaseClient().grant(30); // 30 秒租约
ByteSequence lockKey = ByteSequence.from("lock", StandardCharsets.UTF_8);
kv.put(lockKey, ByteSequence.from("locked", StandardCharsets.UTF_8), PutOption.newBuilder().withLease(lease.getID()).build());

Etcd 锁机制提供了过期和自动释放功能,避免了死锁的发生。

分布式锁的常见问题

  1. 死锁:如果分布式锁没有正确释放(例如,客户端崩溃或超时未释放),可能会导致死锁。为避免死锁,需要合理设置锁的过期时间,或者通过租约机制来确保锁的自动释放。
  2. 锁泄露:在网络故障、客户端崩溃等情况下,可能导致锁未释放,需要有合理的异常处理和超时控制。
  3. 性能问题:分布式锁的实现涉及到网络通信,可能会带来性能上的开销。特别是在高并发场景下,锁竞争可能导致性能瓶颈。
全部评论

相关推荐

05-25 17:39
湖南大学 Java
一面1、使用消息队列,如何避免重复消费;&nbsp;&nbsp;&nbsp;&nbsp;生产者配置中启用了幂等性,kafka会为每一条消息分配唯一的序列号(Sequence&nbsp;Number)和生产者ID。Broker会拒绝重复的序号,确保同一分区内的消息不重复。消费者处理了消息会正确提交偏移量。2、mq主从复制,如果主节点挂了之后,会出现什么情况&nbsp;&nbsp;&nbsp;&nbsp;主节点Leader挂掉之后,会立即触发选举机制,从该分区的其他从节点中选举一个新的Leader来接管,在选举新的Leader的过程中,可能会出现短暂的延迟,导致消费者和生产者无法正确向分区发送和消费消息,但是一般是短暂的时间几百毫秒或者几秒。如果原来的Leader节点只是暂时的故障,很快恢复,它会重新加入分区,成为Follower节点进行数据同步。如果原来的Leader节点无法回复或者挂掉的时间过长,新的Leader节点将继续承担分区的读写操作,而原来的Leader上未复制到新的Leader的数据可能会丢失。3、在java类中可以通过反射来访问一类的私有成员吗?&nbsp;&nbsp;&nbsp;&nbsp;可以访问,可以通过getDeclaredFields方法获取类中所有声明的字段,包括私有字段,通过设置Field对象的setAccessible(true)方法取消java语言访问检查,从而可以访问并修改私有字段的值。4、开放题4.1有两个文件,一个文件A,大小200MB,文件B,大小10G,内存限制是265MB,求所有在文件A不在B的行。4.2给n个正整数,求最小的k个值,这里面n远大于k5、从输入一个页面到显示页面的过程5.1URL查询IP地址是通过什么协议&nbsp;&nbsp;&nbsp;&nbsp;DNS协议5.2DNS是属于什么层的协议&nbsp;&nbsp;&nbsp;&nbsp;DNS&nbsp;属于应用层协议&nbsp;:DNS&nbsp;协议运行在&nbsp;TCP/IP&nbsp;协议栈的应用层,它使用&nbsp;UDP&nbsp;或&nbsp;TCP&nbsp;协议进行通信。UDP&nbsp;通常用于简单的&nbsp;DNS&nbsp;查询,因为其速度快且开销小;而&nbsp;TCP&nbsp;则用于更复杂的&nbsp;DNS&nbsp;操作,如区域传输等。6、介绍一下操作系统的虚拟地址和物理地址&nbsp;&nbsp;&nbsp;&nbsp;虚拟地址:是程序运行时使用的地址空间,由操作系统和硬件共同管理。允许程序在逻辑上独立都物理内存进行编址。使得程序编写和移植更加方便。每个进程都有自己的虚拟地址空间,不同进程的虚拟地址空间是相互隔离的,这样可以保证各个进程的安全性和独立性。物理地址:是计算机实际的内存地址,由内存管理单元MMU负责将虚拟地址映射到物理地址。物理地址是唯一的,用于直接访问物理地址中的数据。操作系统通过页表等结构来维持虚拟地址和物理地址之间的转换。7、页中断是什么时候发生的&nbsp;&nbsp;&nbsp;&nbsp;访问未分配的页面:当进程试图访问一个尚未分配给他的虚拟地址会触发页中断。这可能是因为该页面从未被分配过,或者是因为内存紧张,该页面被操作系统临时调出磁盘交换空间中。权限问题:如果进程试图以不正确的权限访问某个页面,会导致页中断。操作系统会检查页面的访问权限,并在发现委会访问时触发页中断。硬件异常:某些一件故障或者异常情况也可能会导致也终端,例如内存控制器检测到内存错误。8、linux如何表示文件系统的权限&nbsp;&nbsp;&nbsp;&nbsp;符号表示法:r表示读、w表示写、x表示执行&nbsp;&nbsp;&nbsp;&nbsp;数字表示:读、写、执行权限分别用数字4,2,1表示9、linux文件系统中,在一个文件中,查找一个关键字上下10行的一个内容,如何查找&nbsp;&nbsp;&nbsp;&nbsp;可以使用grep命令结合上下文参数-A显示匹配行后n行,-B显示匹配行前n行-C显示匹配行前后各n行。
查看12道真题和解析
点赞 评论 收藏
分享
评论
1
1
分享

创作者周榜

更多
牛客网
牛客企业服务