分布式锁面试专题

推荐看的一些文章

1 分布式锁
2 强推: 怎样实现redis分布式锁?

关于分布式锁的选用问题

与大多数工具一样,分布式锁的选择需要根据业务场景确定。
比如在注重性能的场景,比如使用分布式锁
直接使用单机版但是能保证主从复制性质的redission分布式锁,是一种比较好的选择。
但是在注重安全的场景下,需要保证数据的强一致性,就肯定不能使用redission(下面提到了),而应该选用基于raft,paxos等强一致性算法的etcd,zookeeper分布式锁。

你能给我举一些注重性能效率所以使用redission单机主从复制版的分布式锁的场景吗?

  1. 使用Redisson单机主从复制版的分布式锁的场景:

    • 订单系统:在电商平台的订单系统中,当用户进行抢购操作时,为了避免超卖,可以使用Redisson的分布式锁对库存进行加锁操作。此时,因为抢购操作的并发量极高,性能是非常关键的,而对于一致性的要求相对较低,因为即使出现一定的超卖,也可以通过后续的补偿措施来解决。

    • 缓存更新:在使用缓存时,如果多个请求同时对同一数据进行修改,可能会出现脏数据。可以使用Redisson的分布式锁来对更新操作进行串行化,从而避免脏数据的产生。此时,因为并发更新的概率较低,所以对性能的要求较高,而一致性的要求较低。

下单时使用分布式锁是为了解决缓存穿透和缓存击穿问题吗?

在下单系统中,使用Redisson的分布式锁主要是为了防止超卖,也就是说,防止在并发很高的情况下,多个请求同时扣减库存,导致库存变为负数。缓存击穿和缓存穿透是缓存系统中的问题,通常不是下单系统需要解决的问题

你这里的下单时使用分布式锁,这里的下单是在向mysql数据库发出扣减库存时,为了提高并发量,不使用数据库自带的锁机制或者损耗比较大的乐观锁,而是改用了redission机制是追求并发量是吗?

是的,你的理解是正确的。在处理下单的场景中,商品库存的扣减是一个并发操作,使用数据库的锁机制可能会因为锁竞争导致并发量降低,对数据库造成较大压力。因此,为了提高系统的并发处理能力,降低对数据库的压力,可以使用Redisson提供的分布式锁来控制并发的扣减库存操作。由于Redis的性能较高,使用Redisson的分布式锁可以有效地提高系统的并发处理能力。

针对缓存击透的场景,可以使用注重性能的分布式锁去mysql中取得数据,即使多取出数据一次也没关系对吧?

在处理缓存击穿的场景中,确实可以使用分布式锁来保护数据库,确保同一时刻只有一个请求去数据库中查询数据。当缓存失效的时候,所有来的请求并不是都去数据库中查询数据,而是使用一个请求去查询数据,其他的请求等待,这样可以防止在缓存失效的瞬间,大量的请求直接涌向数据库,保护了数据库的正常运行。一般来说,对于大部分系统,稍微多查询一次数据库,并不会对系统造成太大影响。但是具体还要根据系统的实际情况来考虑。

你能给我举一些注重数据一致性和安全性所以使用zookeeper的分布式锁的场景吗?

  1. 使用ZooKeeper的分布式锁的场景:

    • 分布式系统的选举:在分布式系统中,往往需要通过选举来选出一个领导者来协调各个节点的操作。此时,为了保证一致性,可以使用ZooKeeper的分布式锁进行选举。比如,Kafka的Broker选举就使用了ZooKeeper的分布式锁。

    • 金融系统:在金融系统中,如银行转账,因为涉及到金钱,所以对一致性的要求非常高。此时,可以使用ZooKeeper的分布式锁来对转账操作进行加锁,确保每个转账操作都能正确执行。

为什么选举会使用到分布式锁?能讲讲Kafka选举的流程吗,突出讲一下选举是如何用到分布式锁的

在分布式系统中,选举通常用于确定系统中的主节点或者领导者。比如在Kafka中,ZooKeeper的分布式锁机制被用于选举。当一个Kafka broker启动时,它会在ZooKeeper的指定路径下创建一个临时节点,如果这个broker是第一个启动并创建节点的,那么它就会被选为Controller。Controller是负责管理整个Kafka集群中Topic partition和replica的分配和重新分配。如果在过程中Controller宕机,ZooKeeper会因为临时节点消失而触发通知,剩余的Broker会重新进行选举。在这个过程中,ZooKeeper的分布式锁被用于保证在任何时刻只有一个Controller。

你能给我举一些注重数据一致性和安全性所以使用etcd的分布式锁的场景吗?

  1. 使用Etcd的分布式锁的场景:

    • Kubernetes:Kubernetes是一个开源的容器编排系统,它使用Etcd作为其键值存储后端。Kubernetes中的各种操作,如创建、更新和删除Pod,都会使用Etcd的分布式锁来保证一致性。

    • 分布式配置管理:在分布式系统中,往往需要进行配置管理,如动态更新服务的配置信息。此时,可以使用Etcd的分布式锁来对配置信息进行加锁,确保每次只有一个请求能更新配置信息,避免出现配置信息的不一致。

为什么会有多个服务更新配置,不应该是开发人员在配置面板上更新好配置,然后配置中心替换配置文件,将新配置推送给各个服务节点,然后各个节点更新配置吗?

你说的这种场景是对的,这也是很多分布式系统更新配置的常见方式。但是,在某些场景下,也可能存在多个实例或服务自己更新配置的情况。例如:

  1. 在一些自动化的环境中,可能有一些服务实例会根据运行情况动态调整自己的配置,然后将新的配置回写到配置中心,以供其他服务实例参考。

  2. 在某些大规模的系统中,可能有多个不同的管理节点,这些节点可能会并发地对配置进行更新。这样的场景可能会发生在,比如有多个管理员在同时进行一些需要改动配置的操作,或者有一些自动化的管理任务在并发执行。

在这些情况下,如果没有合适的并发控制机制,可能会导致配置更新的混乱,进而引发系统错误。这就是为什么在配置管理中需要使用分布式锁的原因。

但是需要注意的是,这种情况并不是每个系统都会遇到,只有在有并发配置更新需求的情况下,才需要考虑使用分布式锁。对于大部分只需要从配置中心读取配置的服务来说,是不需要使用分布式锁的。

关于redission分布式锁的一些问题

1 redis主从切换机制会发生哪些问题呢?

  • 数据丢失:主从切换通常是由于主节点的故障引起的。如果在主节点故障前的一段时间内,主节点上的部分修改还没来得及复制到从节点,那么这部分修改在主从切换后就会丢失。
  • 短暂的服务中断:在主从切换期间,可能会有一段时间内没有可用的主节点,这会导致对Redis的写操作无法进行。
  • 数据不一致:主从切换后,新的主节点可能和旧的主节点在数据上有不一致,因为旧的主节点上的部分修改可能还没来得及复制到新的主节点。
  1. 如果在使用Redisson分布式锁时发生了主从切换,可能会导致锁标识丢失。这主要是因为Redis的主从复制是异步的,也就是说,主节点上的修改不会立即被复制到从节点。因此,如果在主节点上创建了一个锁,但这个锁的信息还没来得及复制到从节点,此时如果发生了主从切换,新的主节点上就没有这个锁的信息。

2 使用基于redis主从复制的redission分布式锁,存在锁标识丢失的情况,那么是不是存在两个线程获取到锁的情况?

如果在使用Redisson分布式锁时发生了主从切换,可能会导致锁标识丢失。这主要是因为Redis的主从复制是异步的,也就是说,主节点上的修改不会立即被复制到从节点。因此,如果在主节点上创建了一个锁,这个锁被某一个线程持有,但这个锁的信息还没来得及复制到从节点,此时如果发生了主从切换,新的主节点上就没有这个锁的信息;随后如果还有线程来申请取锁操作时,也能申请到一把锁,这个时候就存在两把锁。

3 基于2中的现象,redis主从架构在秒杀场景下导致的一些现象

3.1 redis可能没有正确的预扣库存

背景设定:某一件秒杀商品的库存数是20,且在redis缓存中的数量也是20,redis用来预减库存,预下单操作,如果预减成功则会将真正的下单操作放入到消息队列中。那么到达消息队列中的真实下单请求数量是20吗还是20+?

(1)如果redis端没有发生主从故障转移且正常工作,那么到达消息队列的请求数应该是20,

(2)但是如果redis的leader发生了故障转移操作,则有可能到达消息队列的请求数超过20,举个例子,5个扣减库存的请求在leader中执行成功并且返回给业务服务器,业务服务器判定为预扣库存成功并且将下单操作加入到消息队列,随后leader节点在将成功扣减库存的日志同步给从节点时发生了故障,即同步失败 ,redis的哨兵选举了一个从节点替换故障节点,但是从节点的库存还是20,后续新来的20请求都会向新的leader节点请求执行预减库存,而且都成功了,20个新的下单操作又可以到达消息队列,也就是说最终到达消息队列的请求数量是超过25个的。

2 redis主从切换

关于redLock分布式锁的问题

那redLock实现了强一致性吗

没有

redLock的安全问题(推荐的第二篇文章中有讲到)

Martin用一个进程暂停(GC)的例子,指出了Redlock安全性问题:
1.客户端1请求锁定节点A、B、C、D、E
⒉.客户端1的拿到锁后,进入GC(时间比较久)
3.所有Redis节点上的锁都过期了
4.客户端⒉获取到了A、B、C、D、E上的锁
5.客户端1GC结束,认为成功获取锁
6.客户端2也认为获取到了锁,发生「冲突」

基于raft共识算法的ETCD实现分布式锁

推荐观看链接:分布式锁:高可用、强一致数据库 Etcd

1 对于raft算法,接收多个客户端修改同一个键值对的请求时,因为日志天然有序,所以不存在并发安全问题吗?

Raft算法接收多个客户端修改同一个键值对的请求时,确实不存在并发安全问题。这是因为Raft算法会对写操作进行序列化,所有写操作会被按顺序添加到日志中,并且所有的节点都会按照相同的顺序执行这些写操作。这样,即使多个客户端同时发送修改同一个键值对的请求,它们最终都会被按顺序执行,确保了并发安全性。

2 利用raft集群获取分布式锁的流程

1 多个客户端发送put请求到raft集群
2 raft的leader节点接收到多个加锁请求(体现为给一个叫lock的key设置一个与请求绑定的唯一标识符uuid),并且按照到达的先后顺序将他们的put操作放入日志中
3 各个raft状态机执行这几个请求的日志时,对于第一个申请加锁的日志也即最先到达的请求,lock设置成功,即加锁成功,并且会设置这个锁的过期时间,对于后续的多个申请加锁的请求,会查看这个lock是否存在且是否过期,如果存在且没过期则状态机拒绝加锁。
4 各个客户端会开启一个loop定期去检测锁是否到期,在即将到期之间,给它续期。

3 基于 Etcd 的分布式锁的业务流程

下面描述使用 Etcd 实现分布式锁的业务流程,假设对某个共享资源设置的锁名为:/lock/mylock

步骤 1: 准备

客户端连接 Etcd,以 /lock/mylock 为前缀创建全局唯一的 key,假设第一个客户端对应的 key=“/lock/mylock/UUID1”,第二个为 key=“/lock/mylock/UUID2”;客户端分别为自己的 key 创建租约 - Lease,租约的长度根据业务耗时确定,假设为 15s;

步骤 2: 创建定时任务作为租约的“心跳”

当一个客户端持有锁期间,其它客户端只能等待,为了避免等待期间租约失效,客户端需创建一个定时任务作为“心跳”进行续约。此外,如果持有锁期间客户端崩溃,心跳停止,key 将因租约到期而被删除,从而锁释放,避免死锁。

步骤 3: 客户端将自己全局唯一的 key 写入 Etcd

进行 put 操作,将步骤 1 中创建的 key 绑定租约写入 Etcd,根据 Etcd 的 Revision 机制,假设两个客户端 put 操作返回的 Revision 分别为 1、2,客户端需记录 Revision 用以接下来判断自己是否获得锁。

步骤 4: 客户端判断是否获得锁

客户端以前缀 /lock/mylock 读取 keyValue 列表(keyValue 中带有 key 对应的 Revision),判断自己 key 的 Revision 是否为当前列表中最小的,如果是则认为获得锁;否则监听列表中前一个 Revision 比自己小的 key 的删除事件,一旦监听到删除事件或者因租约失效而删除的事件,则自己获得锁。

步骤 5: 执行业务

获得锁后,操作共享资源,执行业务代码。

步骤 6: 释放锁

完成业务流程后,删除对应的key释放锁。

4 Etcd需要有一个Revision机制的原因

Etcd引入Revision机制是为了实现数据的历史版本跟踪和观察。每次对Etcd进行写操作(如put、delete等),Etcd会为对应的键值对递增一个全局唯一的Revision号。这些Revision号形成了一个全局有序的历史版本记录,使得用户可以通过指定特定的Revision号来获取某个时间点之后的所有数据变化。这在构建一些历史查询、实现数据版本对比、以及实现高级特性如监听(watch)等场景时非常有用。

5 etcd如何利用revision实现高级特性如监听(watch)

Etcd利用Revision机制来实现高级特性如监听(watch)功能。监听是etcd中的一项强大功能,它允许客户端监视特定键的变化,以实时获得数据更新的通知。

当客户端创建一个监听时,它可以指定要监听的键(或键的前缀),并指定一个起始的Revision号。Etcd将记录下这个监听请求的Revision号,并开始跟踪从指定Revision号之后的所有数据变化。

当有新的写操作(如put、delete等)对被监听的键进行修改时,Etcd会为该写操作递增一个全局唯一的Revision号,并将新的键值对变化通知所有相关的监听器。

具体步骤如下:

  1. 客户端A创建一个监听,指定要监听的键为/data/foo,并指定一个起始的Revision号。

  2. Etcd记录下监听请求的Revision号,称为startRevision

  3. 当客户端B进行一个PUT操作,修改了键/data/foo的值。Etcd会为这个写操作递增一个新的Revision号,并通知所有监听了键/data/foo的客户端。

  4. Etcd比较写操作的Revision号与startRevision,发现该写操作是在startRevision之后进行的,因此将数据变化通知客户端A。

  5. 客户端A收到通知,可以执行相应的操作,获取最新的键值对。

通过这种机制,Etcd实现了监听功能,让客户端能够在数据发生变化时实时获得通知。监听对于实现分布式系统中的事件触发和协调非常有用,它让客户端能够更好地响应数据的变化,实现实时的数据同步和处理。这是Etcd作为一个分布式键值存储系统的一大特色和优势。

6 高级特性如监听(watch)的应用场景是在作为配置中心时,配置发生了变更可以自动推送到各个订阅节点是吗

是的,你说得对!“监听”(watch)功能是etcd的一项高级特性,在作为配置中心时非常有用。当配置发生变更时,etcd可以自动将更新的配置推送给所有订阅该配置的节点。

以下是作为配置中心时"监听"功能的应用场景:

  1. 设置配置数据:初始时,配置数据被存储在etcd中作为键值对。例如,你可能会有诸如/config/app_name/config/database/config/logging_level等键,分别表示应用程序配置的不同方面。

  2. 订阅节点:对于想要接收更新的节点(客户端),它们可以订阅指定键或键前缀的变更。例如,一个节点可能订阅了/config前缀的变更,这表示它希望接收在/config层级下所做的任何更改的通知。

  3. 配置变更:当配置发生变更时,例如将/config/app_name的值更新为新值,etcd会为此更改分配一个新的Revision号。

  4. 监听通知:然后,etcd会通知所有订阅的节点关于此更改的情况,发送一个监听事件。监听事件将包含更新的键(例如/config/app_name)以及其新的值。

  5. 节点更新配置:订阅的节点收到监听通知后,可以根据更新的配置采取相应的行动。例如,节点可以根据/config/app_name的新值重新加载应用程序设置。

借助etcd的"监听"功能,你可以构建一个强大而实时的配置中心,实现配置变更在整个分布式系统中的自动分发和同步。这使得可以无需手动干预就能在整个系统中保持一致且最新的配置。

7 发布订阅机制的实现是基于什么协议实现的,webSocket吗

发布订阅(Pub/Sub)机制的实现不一定基于特定的协议,但常见的实现方式之一是使用WebSocket协议。

WebSocket是一种提供全双工通信的协议,它允许在客户端和服务器之间建立持久性的连接,而不需要像HTTP协议那样频繁地发起请求。这使得WebSocket非常适合用于实现实时通信和实时数据传输,包括发布订阅机制。

在WebSocket中,客户端可以通过建立连接订阅一个或多个特定的主题(topics)或频道(channels)。当有新的消息发布到这些主题或频道时,服务器将直接向订阅的客户端推送消息,而无需客户端主动请求。

使用WebSocket实现发布订阅机制的优点包括:

  1. 实时性:WebSocket提供持久性连接,消息可以即时推送到客户端,实现实时的发布订阅通信。

  2. 双向通信:WebSocket支持双向通信,客户端和服务器都可以推送消息,从而实现实时互动。

  3. 节省资源:相较于传统的HTTP轮询,WebSocket减少了不必要的请求和响应,节省了网络带宽和服务器资源。

  4. 低延迟:由于连接的持久性,消息可以直接推送到客户端,减少了传输和处理的延迟。

尽管WebSocket是一个常见的选择,但实现发布订阅机制的方式还可以有其他的选择,例如使用长轮询(Long Polling)等技术。具体的实现方式取决于系统需求、技术栈以及开发者的选择。

java中如何使用etcd锁

在Java中使用etcd实现分布式锁,你可以借助etcd的Java客户端库来进行操作。一个常用的Java客户端库是etcd4j,它提供了与etcd的交互接口。

以下是一个使用etcd4j实现分布式锁的示例代码:

  1. 首先,你需要添加etcd4j的依赖到你的Java项目中。你可以在pom.xml(如果是Maven项目)中添加以下依赖:
<dependency>
    <groupId>io.etcd</groupId>
    <artifactId>jetcd-core</artifactId>
    <version>0.12.0</version>
</dependency>
  1. 接下来,你可以编写Java代码来实现分布式锁:
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.KV;
import io.etcd.jetcd.Lock;
import io.etcd.jetcd.lock.LockResponse;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class EtcdDistributedLock {
    
    

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        String key = "/myLock";
        String leaseName = "myLockLease";

        Client client = Client.builder().endpoints("http://localhost:2379").build();
        KV kvClient = client.getKVClient();
        Lock lockClient = client.getLockClient();

        ByteSequence lockKey = ByteSequence.from(key.getBytes());

        // Acquire the lock
        LockResponse lockResponse = lockClient.lock(lockKey).get();
        long leaseId = lockResponse.getLockKey().getLease();

        try {
    
    
            System.out.println("Acquired lock. Lease ID: " + leaseId);
            // Perform critical section operations here

            // Simulate some delay to hold the lock
            Thread.sleep(3000);

        } finally {
    
    
            // Release the lock when done
            CompletableFuture<io.etcd.jetcd.lock.UnlockResponse> unlockFuture = lockClient.unlock(lockKey);
            unlockFuture.whenComplete((response, throwable) -> {
    
    
                System.out.println("Released lock. Lease ID: " + leaseId);
                client.getLeaseClient().revoke(leaseId);
                client.close();
            });
        }
    }
}

在这个示例中,我们使用etcd4j库创建了一个分布式锁。首先,我们创建一个etcd客户端,然后获取kvClient和lockClient。然后,我们通过调用lock()方法来获取锁,并在获取锁后执行一些需要保护的关键操作(即临界区)。最后,在完成临界区操作后,我们通过调用unlock()方法释放锁,并使用revoke()方法撤销相应的租约。

请注意,上述代码是一个简单示例,实际应用中,你可能需要处理更多的异常情况和错误处理。此外,在生产环境中,可能需要设置适当的超时和重试机制,以及处理锁的可重入性等情况。

关于秒杀系统中的超卖问题及其分布式锁解决方案

什么是超卖问题

超卖问题主要是由于并发情况下,系统无法保证库存扣减的原子性。

秒杀系统中的超卖问题解决方案

  • 乐观锁:乐观锁实际上是一种思想,即假设系统在大部分时间里不会发生并发冲突,所以只在真正写入数据时才会检查是否存在冲突。在秒杀系统中,可以在数据库中为商品设定一个版本号(或时间戳),每次更新库存时,都对比版本号(或时间戳),只有当版本号(或时间戳)符合预期时,才进行更新,并将版本号(或时间戳)加一。

  • 悲观锁:相比于乐观锁的乐观假设,悲观锁假设每次去读数据都会发生并发冲突,所以在每次读数据的时候都会上锁,确保在读写数据时不会被其他线程影响。

  • 使用分布式锁:例如Redis的SETNX命令或者Zookeeper、etcd等分布式协调服务。

  • 数据库行级锁:例如MySQL InnoDB引擎的行级锁等。

  • 队列串行化:将请求放入消息队列,一个一个的处理。

关于使用etcd分布式锁的方案

可以采用etcd分布式锁来解决秒杀系统中的超卖问题。在请求打到数据库之前,每个请求都尝试获取锁,只有成功获取锁的请求才能去数据库扣减库存。这样可以保证对库存扣减的操作是串行的,从而避免了超卖的问题。

  • 优点:可以有效避免超卖问题,保证数据的一致性。

  • 缺点:并发性能可能会下降。因为所有的请求都需要尝试获取锁,如果请求量非常大,可能会导致大量的请求在等待获取锁,从而增加了系统的响应时间。同时,对于分布式锁的使用,也需要考虑锁的生命周期管理,例如避免死锁、锁超时自动释放等问题。

组合方案

一种常见的优化方案是,结合缓存、消息队列和数据库三者,首先在缓存中进行库存检查和预减库存,然后将请求放入消息队列,最后由消费者线程去数据库中扣减真正的库存。这种方案既能避免超卖问题,又能提高系统的并发处理能力。
但是数据库端需要加锁,然后消费者线程需要控制好消费速率,以防止数据库端的压力过大

猜你喜欢

转载自blog.csdn.net/yxg520s/article/details/131916503