字节狂问1小时,小伙offer到手,太狠了!(字节面试真题)

文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :

免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取


字节狂问1小时,小伙offer到手,太狠了!(字节面试真题)

前言:

在尼恩的(50+)读者社群中,经常有小伙伴,需要面试 头条、美团、阿里、京东等大厂。

下面是一个小伙伴成功拿到字节飞书offer,通过一小时拷问的面试经历,就两个字:

  • 深: 问的很深
  • 宽: 范围很宽

总之,就是 头条的面试官,功底还是挺牛的。

但是,咱们的候选人,也不是吃素的。

下面,从小伙的面试经历看看,收个飞书Offer需要学点啥?当然,这个小伙伴是 中间开发,但是对于中高级开发来说,这些面试题,也有参考意义。

这里也把题目以及参考答案,收入咱们的《尼恩Java面试宝典》 V69,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请到公众号 【技术自由圈】获取

本文目录

飞书面试正题:

1、什么是Spring循环依赖? Spring是怎么解决的?

Spring循环依赖是指两个或多个Bean之间相互依赖,形成一个环形依赖的情况。例如,Bean A依赖于Bean B,而Bean B又依赖于Bean A,这样就形成了一个循环依赖。

Spring解决循环依赖的方式是使用“提前暴露”和“三级缓存”机制。

具体来说,Spring在创建Bean时,会将正在创建的Bean先放入“一级缓存”中,然后检查这个Bean是否有依赖其他Bean,如果有,Spring会将这个Bean的依赖关系放入“二级缓存”中,并且创建这些依赖的Bean。

如果依赖的Bean中又有依赖当前Bean的,Spring会将这些依赖关系放入“三级缓存”中,并且创建这些依赖的Bean。

当所有Bean都创建完成后,Spring会将这些Bean从“三级缓存”中取出,并且将它们注入到相应的Bean中,完成循环依赖的解决。

此外,Spring还提供了多种解决方案来避免循环依赖问题,例如使用构造函数注入、使用setter方法注入、使用@Lazy注解等。

需要注意的是,Spring解决循环依赖的机制并不是完美的,因为它需要使用“三级缓存”机制,会占用一定的内存空间。

同时,如果循环依赖的Bean中存在复杂的依赖关系,可能会导致Spring无法解决循环依赖的问题,从而导致程序出现异常。因此,在编写代码时,应该尽量避免循环依赖的情况。

2、怎么样高性能的计算最小公共字符串

最小公共字符串问题是一个经典的计算问题,其目标是在两个字符串中找到最短的子字符串,该子字符串同时出现在两个字符串中。这个问题可以用多种算法来解决,其中一些算法可以实现高性能的计算。

其中一种高性能算法是后缀树算法

后缀树是一种特殊的数据结构,它可以用来表示一个字符串的所有后缀。在后缀树中,每个节点表示一个字符串的后缀,而每个边表示一个字符。

通过在后缀树中搜索两个字符串的公共子串,可以找到最小公共字符串。

另一种高性能算法是基于动态规划的算法

这种算法使用一个二维数组来记录两个字符串的所有子串的公共长度。通过在这个数组中搜索最小公共字符串,可以找到最小公共字符串。

无论使用哪种算法,都可以通过使用并行计算来提高性能。

例如,可以将字符串分成多个子串,并在多个处理器上并行计算子串之间的公共字符串。这种方法可以大大提高计算速度,并且可以很好地扩展到处理大量数据的情况。

最小公共字符串问题可以用以下步骤解决:

  1. 统计字符串集合中每个字符串出现的次数,保存到一个字典中。
  2. 对于每个字符串,找到它最长的公共前缀,即其他字符串的最小公共字符串。
  3. 如果当前字符串和它的最长公共前缀相同,则增加它的出现次数,否则更新最长公共前缀。
  4. 最终得到的最小公共字符串即为所有字符串中最长的公共前缀。

以下是一个使用后缀树算法实现最小公共字符串的Java代码示例:

public class SuffixTree {
    private final Node root;

    public SuffixTree(String s) {
        root = new Node();
        for (int i = 0; i < s.length(); i++) {
            insertSuffix(s.substring(i), i);
        }
    }

    private void insertSuffix(String suffix, int index) {
        Node node = root;
        for (char ch : suffix.toCharArray()) {
            if (!node.containsKey(ch)) {
                node.put(ch, new Node());
            }
            node = node.get(ch);
        }
        node.addIndex(index);
    }

    public String findLCS(String s1, String s2) {
        String lcs = "";
        Node node = root;
        int i = 0, j = 0;
        while (i < s1.length() && j < s2.length()) {
            char ch1 = s1.charAt(i);
            char ch2 = s2.charAt(j);
            if (node.containsKey(ch1) && node.containsKey(ch2)) {
                node = node.get(ch1);
                i++;
                j++;
            } else {
                break;
            }
            if (node.hasMultipleIndexes()) {
                String candidate = findLCS(s1.substring(i - node.getIndexList().get(0), i), s2.substring(j - node.getIndexList().get(0), j));
                if (candidate.length() > lcs.length()) {
                    lcs = candidate;
                }
            }
        }
        return lcs;
    }

    private static class Node {
        private final Map<Character, Node> children = new HashMap<>();
        private final List<Integer> indexList = new ArrayList<>();

        public void put(char ch, Node node) {
            children.put(ch, node);
        }

        public boolean containsKey(char ch) {
            return children.containsKey(ch);
        }

        public Node get(char ch) {
            return children.get(ch);
        }

        public void addIndex(int index) {
            indexList.add(index);
        }

        public boolean hasMultipleIndexes() {
            return indexList.size() > 1;
        }

        public List<Integer> getIndexList() {
            return indexList;
        }
    }
}

使用这个后缀树实现类,可以通过以下方式找到两个字符串的最小公共字符串:

String s1 = "abcdefg";
String s2 = "bcdefgh";
SuffixTree suffixTree = new SuffixTree(s1 + "#" + s2);
String lcs = suffixTree.findLCS(s1, s2);
System.out.println(lcs); // 输出 "bcdef"

这个算法的时间复杂度为O(m+n),其中m和n分别是两个字符串的长度。由于后缀树的构建和搜索都可以使用高效的算法实现,因此这个算法可以实现高性能的计算。

3、RPC框架

1)什么是RPC框架?RPC框架中, RPC通讯 协议怎么设计的

RPC(Remote Procedure Call)是一种远程调用协议,它允许一个计算机程序调用另一个计算机程序的子程序,而不需要程序员显式编写远程调用的代码。RPC框架是一种实现RPC协议的软件框架,它提供了一种简单的方法来实现跨网络的通信。

完整RPC架构图

在RPC框架中,RPC通讯协议的设计通常包括以下几个方面:

  1. 传输协议:RPC框架需要选择一种可靠的传输协议来保证数据的传输。常用的传输协议有TCP和UDP。
  2. 序列化协议:RPC框架需要选择一种序列化协议来将数据序列化为二进制格式,以便在网络上传输。常用的序列化协议有JSON、XML、Protobuf等。
  3. 服务注册与发现:RPC框架需要提供服务注册与发现的功能,以便客户端可以找到可用的服务提供者。常用的服务注册与发现工具有Zookeeper、Consul等。
  4. 负载均衡:RPC框架需要提供负载均衡的功能,以便将请求均衡地分配给不同的服务提供者。常用的负载均衡算法有轮询、随机等。
  5. 安全认证:RPC框架需要提供安全认证的功能,以确保通信的安全性。常用的安全认证方式有SSL、Token认证等。
  6. 对等节点:RPC框架通常需要支持多个对等节点,每个节点都可以提供远程服务。节点之间可以通过网络通信来交互消息,并在对等节点之间传递响应。
  7. 调用流程:RPC框架通常提供一组调用流程,以便程序员方便地进行远程调用。调用流程通常包括请求、响应和错误处理等部分。

在设计RPC通讯协议时,需要考虑通信的可靠性、效率和安全性等因素,同时需要根据具体的应用场景选择合适的协议。

2)设计一个RPC框架,需要考虑哪些问题

设计一个RPC框架需要考虑以下问题:

  1. 通信协议:选择一种合适的通信协议来实现客户端和服务端之间的通信,常用的通信协议包括HTTP、TCP、UDP等,以及序列化协议,例如JSON或是Protobuf。
  2. 服务注册与发现:需要实现服务注册与发现机制,使得客户端可以自动发现可用的服务提供者。
  3. 负载均衡:需要实现负载均衡机制,确保请求能够均衡地分配给不同的服务提供者。常用的负载均衡算法包括轮询、随机和最少连接数等。
  4. 安全认证:为了保证系统的安全性,需要对请求进行身份验证和权限控制,以防止恶意攻击和非法操作。常用的安全认证方式包括基于令牌的身份验证和基于SSL/TLS的加密通信等。
  5. 异常处理:需要考虑异常处理机制,例如网络异常、超时等情况的处理方式。
  6. 高可用性:需要考虑如何保证系统的高可用性,例如实现服务降级、容错等机制。
  7. 性能优化:在高并发的情况下,需要对RPC框架进行性能优化,以确保请求能够快速响应。常用的性能优化方式包括缓存、异步处理和线程池等机制。
  8. 日志与监控:需要实现日志记录和监控机制,以便及时发现和解决问题。
  9. 兼容性:需要考虑不同语言、不同平台之间的兼容性问题,例如实现跨语言调用的机制。
  10. 扩展性:需要考虑如何实现系统的扩展性,例如支持动态添加和删除服务提供者。

3)RPC框架中,序列化算法的对比与优缺点分析

在RPC框架中,序列化算法是非常重要的一环,它直接影响到系统的性能和可扩展性。下面是几种常见的序列化算法的对比分析:

1.Java原生序列化:是Java自带的序列化方式,可以将对象序列化为字节流,也可以将字节流反序列化为对象。

优点

  • 使用方便,不需要额外的依赖

缺点

  • 序列化后的字节流较大,序列化和反序列化的性能较差
  • 只能在Java平台上使用

2.JSON序列化:是一种轻量级的数据交换格式,可以将对象序列化为JSON字符串,也可以将JSON字符串反序列化为对象。

优点:

  • JSON是一种轻量级的数据交换格式,序列化后的数据较小,易于阅读和编写。
  • JSON支持嵌套对象和数组,方便表示复杂的数据结构。
  • JSON可以通过HTTP协议进行传输,适用于Web应用程序。

缺点:

  • 序列化和反序列化的性能较差,不支持二进制数据,只能序列化JavaBean等简单的数据结构。
  • JSON的解析速度相对较慢,对于大量数据的处理可能会有性能问题。

3.XML序列化:XML序列化是一种基于XML格式的序列化方式,可以将对象序列化为XML字符串,也可以将XML字符串反序列化为对象。

优点:

  • XML是一种通用的数据格式,可以用于多种应用程序之间的数据交换。
  • 序列化后的数据易于阅读和调试,支持复杂的数据结构
  • XML支持命名空间和属性,方便表示复杂的数据结构。
  • XML可以通过HTTP协议进行传输,适用于Web应用程序。

缺点:

  • XML相对于JSON来说较为冗长,数据较大,序列化和反序列化的效率较低。
  • XML不支持二进制数据,无法直接序列化和反序列化二进制数据。

4.Protobuf序列化:Protobuf是一种高效的二进制序列化协议,可以将对象序列化为二进制数据,也可以将二进制数据反序列化为对象。

优点:

  • Protobuf支持快速序列化和反序列化数据,性能非常高,序列化后的数据较小。
  • Protobuf支持多种编程语言,包括C++、Java、Python等,支持跨语言调用。
  • Protobuf可以定义自定义的消息类型,方便表示复杂的数据结构。

缺点:

  • Protobuf相对于JSON和XML来说较为复杂,学习和使用成本较高。
  • Protobuf不支持HTML和XML这样的标记语言,无法直接在Web应用程序中使用。
  • 需要定义IDL文件,不支持动态添加字段等操作

综上所述,不同的序列化算法各有优缺点,需要根据具体的应用场景选择适合的序列化算法。

如果需要高性能的序列化和反序列化化二进制数据,可以选择Protobuf;

如果需要易于阅读和调试的序列化格式,可以选择JSON或XML;

如果需要在Web应用程序中使用RPC框架,可以选择JSON;

如果需要在多种应用程序之间进行数据交换,XML可能更适合;

如果需要Java平台原生支持的序列化方式,可以选择Java原生序列化。

4)了解过 gRPC 吗?gRPC 的原理是什么

gRPC是一个高性能、开源和通用的RPC框架,由Google开发。

它使用Protocol Buffers作为接口描述语言,可以在多种语言中使用,包括Java、Python、C++等。gRPC支持多种传输协议和序列化协议,可以在不同的环境中使用,如云、移动设备、浏览器等。

grpc的原理是基于HTTP/2和protobuf协议,利用protobuf序列化和反序列化技术,实现远程过程调用。

protobuf是一种轻量级、高效、可扩展的数据序列化格式,由谷歌开发并开源。它允许在不同的平台和语言之间传递和解析数据,支持类型定义和版本控制,具有数据压缩效率高和序列化和反序列化速度快的优点。

gRPC通过在客户端和服务器之间建立一个protobuf序列化/反序列化通道,实现远程过程调用。客户端将请求序列化为字节流并发送到服务器,服务器将响应反序列化为字节流并发送给客户端。gRPC还支持负载均衡、服务发现机制、认证和授权、监控和日志等功能,提高了RPC框架的可靠性和可扩展性。

4、了解过 Dubbo 吗?Dubbo 的原理是什么

Dubbo是一种高性能、轻量级的分布式服务框架,由阿里巴巴集团开发。

它采用了分布式服务框架的核心理念,提供了基于RPC(远程过程调用)的分布式服务治理解决方案,支持多种协议和注册中心,可以方便地实现微服务架构,帮助开发者快速构建分布式应用。

Dubbo的原理是基于Java的远程调用框架,使用了Java的反射机制和动态代理技术。

它采用了基于SOA(面向服务架构)的思想,将业务逻辑封装成服务,然后通过RPC协议进行远程调用。基于RPC协议,采用了一种简化的序列化和反序列化方式,即基于字节数组的序列化和反序列化方式。它通过在网络中建立一个负载均衡器,将请求分发到多个提供者,并通过一组超时机制和重试策略来保证高可用和稳定性。

Dubbo还提供了多种负载均衡策略和路由策略,可以根据不同的场景进行配置。

Dubbo还支持多种注册中心,包括Zookeeper、Redis和Multicast等,可以实现服务的自动注册和发现。此外,Dubbo还提供了丰富的监控和管理功能,可以方便地对服务进行监控和管理。

Dubbo的原理可以概括为:

  1. 定义接口:使用Java接口定义服务接口,包括请求和响应的消息格式、参数和返回值类型等信息。
  2. 生成代码:使用Dubbo插件将Java接口转换为Dubbo接口,并生成对应的Dubbo服务接口文件。
  3. 实现服务:在服务端实现服务接口,并根据需要添加错误处理和其他功能。
  4. 配置注册中心:配置Dubbo的注册中心,如Zookeeper或Nacos等,用于管理服务的注册和发现。
  5. 启动服务提供者:在服务提供者上启动Dubbo服务,监听指定的端口,等待客户端的请求。
  6. 发送请求:客户端调用服务接口的方法,并将请求消息发送给Dubbo服务器。
  7. 负载均衡:Dubbo会根据一定的负载均衡策略选择合适的服务提供者进行处理。
  8. 解析请求:Dubbo服务器接收到请求后,将其解析为相应的方法调用,并将请求消息转发给服务提供者。
  9. 执行方法:服务提供者执行相应的方法,并将结果消息发送给Dubbo服务器。
  10. 解析响应:Dubbo服务器接收到响应后,将其解析为相应的结果消息,并将响应消息转发给客户端。

由于Dubbo使用了高效的通信协议和负载均衡算法,因此具有较高的性能和可靠性。此外,Dubbo还支持集群部署、动态代理等功能,可以满足不同场景下的需求。

5、简单介绍一下 Hystrix 原理,他如何实现熔断的

Hystrix是一个开源的、容错和延迟容忍的库,由Netflix开发。

它提供了一种可以在高并发场景中使用的限流框架。它通过在系统中添加一些额外的组件,例如限流器和令牌桶,来避免系统因为过度的并发而崩溃。它可以帮助开发者处理分布式系统中的延迟和故障问题,提高系统的可用性和稳定性。

Hystrix高层示意图

Hystrix的原理是基于断路器模式,它可以监控服务调用的延迟和错误率,并根据预设的阈值进行自动熔断。

当服务调用失败或超时时,Hystrix会自动切换到备用的服务或者返回预设的默认值,避免了服务的级联故障。Hystrix还提供了实时的监控和统计功能,可以帮助开发者了解系统的运行状况和性能瓶颈。

Hystrix实现熔断的过程如下:

  1. 当服务调用失败或超时时,Hystrix会记录这个事件,并根据预设的阈值进行判断。
  2. 如果失败或超时的事件达到了预设的阈值,Hystrix会自动打开断路器,停止对该服务的调用。
  3. 在断路器打开的状态下,Hystrix会自动切换到备用的服务或者返回预设的默认值。
  4. 在一段时间内,Hystrix会定期地尝试调用服务,如果调用成功,则会关闭断路器,否则继续保持打开状态。

通过熔断机制,Hystrix可以避免服务的级联故障,提高系统的可用性和稳定性。

6、如果让你去设计一个熔断器,你会怎么去设计他的熔断逻辑

设计一个熔断器的熔断逻辑需要考虑以下几个方面:

1.熔断门限:熔断器的门限应该是可配置的,可以根据系统的实际情况来设定一个合理的阈值。这个阈值应该可以根据系统的压力动态调整。

1)定义熔断条件:熔断器需要定义触发熔断的条件,例如:错误率达到一定阈值、请求超时率达到一定阈值等等。

2)熔断器状态:熔断器需要有三种状态:关闭、开启和半开状态。关闭状态下,请求会正常通过;开启状态下,请求会被熔断器拦截;半开状态下,熔断器会尝试发送一部分请求,如果请求成功,则熔断器进入关闭状态,否则进入开启状态。

2.熔断时间:熔断器应该能够在规定的时间内快速响应,这个时间应该足够短,以保证系统能够快速恢复正常。

1)熔断器的熔断时间:熔断器需要定义一个熔断时间,在这段时间内,所有请求都会被熔断器拦截,直到熔断时间结束。在熔断时间内,熔断器会记录所有失败的请求,以便后续分析和处理。

2)熔断器的恢复时间:熔断器需要定义一个恢复时间,在这段时间内,熔断器处于半开状态。在半开状态下,熔断器会尝试发送一部分请求,如果请求成功,则熔断器进入关闭状态,否则进入开启状态。恢复时间结束后,熔断器会重新进入关闭状态。

3.判断是否熔断:在服务端接收到请求后,先检查当前请求是否在熔断范围内。如果是,则直接返回预设的错误信息或者默认的响应结果;如果不是,则继续执行后续操作。

4.熔断方式:熔断器应该能够以多种方式触发熔断,例如超过阈值、超过指定时间等。

5.监控与报警:通过监控系统对服务的可用性和性能进行实时监测,一旦发现服务出现异常或者负载过高,立即触发熔断机制,并向管理员发送报警信息。

6.熔断重试:熔断器应该支持熔断后的自动重试,以便系统能够尽快恢复正常。如果服务正常运行,但是由于网络等原因导致请求失败,可以设置一个重试机制。当请求失败时,可以尝试重新发送请求,直到成功为止。

7.错误处理:熔断器应该能够处理错误,例如当系统出现异常时,熔断器应该能够自动退出并进行错误处理,而不是简单地将请求重新路由到另一个系统。

8.可靠性:熔断器应该是可靠的,即使系统出现异常,熔断器也应该能够正常工作,避免系统的崩溃。

9.动态调整:根据系统的实际情况和用户反馈,动态调整阈值和重试机制等参数,以提高系统的稳定性和可靠性。

熔断器只是一种保护机制,不能完全替代容错和恢复能力。

因此,在设计熔断器的同时,还需要考虑其他方面的优化措施,如增加缓存、优化算法等,以提高系统的性能和可靠性。

7、说说什么是 Redis 的缓存穿透、击穿、雪崩 ,该如何去解决?

Redis缓存穿透、缓存击穿和缓存雪崩都是缓存常见的问题,需要针对不同的问题采取不同的解决方案。

  1. 缓存穿透

缓存穿透是指查询一个不存在的数据,由于缓存中没有该数据,导致请求穿透到数据库,从而对数据库造成压力。解决缓存穿透问题的方法有:

  • 布隆过滤器:使用布隆过滤器对请求进行过滤,如果请求的数据不存在,则直接返回,避免请求穿透到数据库。
  • 缓存空对象:将不存在的数据缓存起来,设置过期时间,下次请求时直接从缓存中返回空数据,避免请求穿透到数据库。
  1. 缓存击穿

缓存击穿是指一个热点数据在缓存中过期或者被清除,导致大量请求同时访问数据库,从而对数据库造成压力。解决缓存击穿问题的方法有:

  • 热点数据永不过期:将热点数据设置为永久不过期,避免缓存失效导致请求穿透到数据库。
  • 加锁:在缓存失效的时候,使用分布式锁或者互斥锁,避免大量请求同时访问数据库。
  • 限流:使用限流算法对请求进行限制,避免大量请求同时访问数据库。
  1. 缓存雪崩

缓存雪崩是指缓存中大量的数据在同一时间失效,导致大量请求同时访问数据库,从而对数据库造成压力。解决缓存雪崩问题的方法有:

  • 数据过期时间随机:将缓存数据的过期时间设置为随机时间,避免大量缓存同时失效。
  • 数据预热:在系统启动时,将热点数据加载到缓存中,避免缓存失效时大量请求访问数据库。
  • 分布式部署:将缓存部署在多个节点上,避免某个节点失效导致缓存雪崩。

总之,为了解决 Redis 缓存的故障问题,需要综合考虑多个因素,包括缓存设计、访问模式、数据结构、算法等等。在实际应用中,可以根据具体情况选择合适的技术和方案进行优化和调整。

8、说说 Redis 分布式锁如何实现 ? 需要考虑哪些问题

Redis分布式锁的实现可以通过使用setnx(set if not exists)命令和expire命令来实现。

具体来说,可以利用Redis的单线程特性,通过setnx命令设置一个锁,如果返回值是1,则表示成功获取到锁,否则表示锁已经被其他客户端获取。然后可以使用expire命令设置锁的过期时间,以防止锁一直被占用而不被释放。

需要考虑的问题包括:

  1. 锁的粒度:锁的粒度应该尽量小,以避免出现锁竞争的情况,同时也要避免出现死锁的情况。

  2. 锁的超时时间:锁的超时时间应该根据业务需求来设置,以避免锁一直被占用而不被释放。

  3. 锁的可重入性:如果在同一个线程中多次获取同一个锁,应该保证可以成功获取锁,而不是一直等待。

  4. 锁的释放:在释放锁的时候,应该先判断锁是否属于当前客户端,以避免误释放其他客户端的锁。

  5. 锁的容错性:在获取锁的时候,应该考虑到网络延迟等因素,避免因为一次获取锁失败就导致整个业务流程失败。

  6. 锁的实现方式:Redis分布式锁的实现方式有多种,如使用Lua脚本实现、使用Redlock算法实现等,需要根据实际情况选择最合适的实现方式。

  7. 分布式环境下的数据一致性:在分布式环境下,多个节点可能会同时请求锁,因此需要保证分布式锁的实例在分布式环境下仍然能够正确地工作,并确保分布式环境下的数据一致性。

  8. 高并发性:分布式锁需要支持高并发的访问,并确保在高并发的情况下仍然能够正确地工作。

  9. 死锁风险:在分布式环境下,由于多个节点可能会互相请求锁,因此需要考虑如何避免死锁的发生。

  10. 授权控制:分布式锁需要支持授权控制,即能够控制哪些节点能够获得锁,并能够在授权控制的基础上实现锁的撤销。

  11. 故障恢复:分布式锁需要支持故障恢复,即在节点故障或网络异常的情况下,锁能够自动恢复正常工作。

  12. 性能:分布式锁需要考虑性能问题,例如锁的响应时间、锁的粒度等。

综上所述,实现Redis分布式锁需要考虑多个方面的问题,包括数据一致性、高并发性、死锁风险、可重入性、授权控制、故障恢复和性能等。

9、Zookeeper 除了注册中心还有什么其他的用处

除了作为注册中心之外,Zookeeper还有以下的用处:

  1. 配置管理:可以使用Zookeeper来存储和管理应用程序的配置信息,当配置发生变化时,可以通过Zookeeper通知应用程序进行相应的更新。

  2. 分布式锁:可以使用Zookeeper来实现分布式锁,避免多个客户端同时访问共享资源的问题。

  3. 分布式队列:可以使用Zookeeper来实现分布式队列,用于协调多个节点之间的任务调度。

  4. 集群管理:可以使用Zookeeper来管理集群中的节点信息,如节点的状态、健康状况等。

  5. 分布式协调:可以使用Zookeeper来实现分布式协调,如选举算法、分布式事务等。

  6. 存储状态信息: Zookeeper 可以用于存储状态信息,例如订单的状态、库存的数量等。

  7. 实现服务发现: Zookeeper 可以用于实现服务发现,例如在分布式系统中,可以使用 Zookeeper 来查找服务的地址。

  8. 支持动态添加和删除节点: Zookeeper 可以支持动态添加和删除节点,这使得它成为了一个非常灵活和可扩展的工具。

  9. 支持负载均衡: Zookeeper 可以用于实现简单的负载均衡,例如在分布式系统中,可以使用 Zookeeper 来实现对不同服务的负载均衡。

  10. 命名服务:Zookeeper可以提供全局唯一的命名服务,使得不同的应用程序可以通过名称来访问相同的资源。

总之,Zookeeper作为一个分布式协调服务,可以用于解决分布式系统中的各种协调问题,提高系统的可用性、可靠性和可扩展性。

10、Zookeeper 的分布式锁是如何实现的?

Zookeeper 的分布式锁是基于一种叫做 "Zookeeper Watch" 的机制实现的。

在 Zookeeper 中,一个节点可以注册一个 Watch 监听其他节点的变化。当一个节点的状态发生变化时,该节点会通知所有注册了该 Watch 的节点,这样就可以保证所有节点都能够及时地获取到变化的信息。

在 Zookeeper 的分布式锁中,节点会使用一个 Watch 监听其他节点的状态,如果发现其他节点的状态发生变化,就可以认为该节点已经被其他节点获取了。

一旦一个节点获取了锁,其他节点就无法再获取锁了。

Zookeeper实现分布式锁的过程可以分为以下几个步骤:

  1. 创建一个临时有序节点:每个客户端在Zookeeper上创建一个临时有序节点,节点的名称为lock,例如/lock/lock-0001。

  2. 获取所有的子节点:客户端通过Zookeeper的API获取/lock节点下的所有子节点,并按照节点名称的序号从小到大进行排序。

  3. 判断自己是否获得锁:如果客户端创建的节点是所有子节点中序号最小的节点,则表示客户端获得了锁,可以执行相应的业务逻辑;否则,客户端需要监听比自己序号小的节点的删除事件,当比自己序号小的节点被删除时,再次执行步骤2和步骤3,直到获得锁。

  4. 释放锁:当客户端执行完业务逻辑后,需要删除自己创建的节点,以释放锁。

需要注意的是,Zookeeper实现分布式锁还需要考虑以下问题:

  1. 节点名称的唯一性:如果多个客户端同时创建了相同名称的节点,可能会导致锁的竞争,需要保证节点名称的唯一性。

  2. 节点删除的时机:如果客户端在执行业务逻辑时,节点被意外删除,可能会导致其他客户端获取到了锁,需要考虑节点删除的时机。

  3. 网络延迟和故障:如果网络延迟或者Zookeeper节点故障,可能会导致客户端无法获取到锁,需要考虑如何处理这些异常情况。

Zookeeper 的分布式锁机制是非常可靠和安全的,因为在获取锁的过程中,需要对所有节点进行验证,只有满足一定条件的节点才能够获取锁。同时,Zookeeper 的分布式锁也支持动态加锁和解锁,这使得它成为了一个非常灵活和可扩展的分布式锁工具。

总之,Zookeeper实现分布式锁的过程相对比较复杂,需要考虑多种情况,但是通过Zookeeper实现分布式锁可以避免多个客户端同时访问共享资源的问题,提高系统的可用性和稳定性。

11、说说 AQS 原理

AQS(AbstractQueuedSynchronizer)是一种分布式锁算法,是由 Eric Brewer 等人在 2000 年提出的。

AQS 能够保证分布式系统中多个节点对共享资源的访问是有序的和互斥的,即要么所有节点都可以访问共享资源,要么所有节点都不能访问共享资源。

AQS是Java中用于实现锁和同步器的基础框架,它提供了一种实现阻塞锁和相关同步器的通用机制,如ReentrantLock、CountDownLatch、Semaphore等。

AQS的核心是一个双向链表,用于存储等待线程。每个节点代表一个等待线程,节点中包含了线程的状态、等待时间、前驱节点和后继节点等信息。AQS通过CAS(Compare and Swap)操作来实现对状态的原子更新和线程的阻塞和唤醒。

AQS的状态是一个int类型的变量,它表示了同步器的状态。在Lock实现中,状态通常表示锁的持有者或者锁的重入次数。在CountDownLatch和Semaphore等同步器中,状态表示可用资源的数量。

AQS提供了两种模式:独占模式和共享模式。独占模式只允许一个线程获取锁,共享模式允许多个线程同时获取锁。在独占模式下,AQS使用一个FIFO队列来存储等待线程,在共享模式下,AQS使用一个CLH队列来存储等待线程。

AQS的实现基于模板方法设计模式,它定义了一些抽象方法,如tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared等,这些方法由具体的同步器实现。在使用AQS实现同步器时,我们只需要继承AQS类,实现这些抽象方法即可。

AQS 的基本思想是:

在分布式系统中,每个节点都维护一个计数器,这个计数器表示该节点对共享资源的请求的状态。当一个节点对共享资源进行请求时,该节点会将计数器递增 1。当某个节点成功地获得了共享资源时,该节点会将计数器递减 1。

AQS 中有三个重要的概念:节点、资源和状态。

  • 节点是分布式系统中的一个独立的计算机实例,它可以是任何类型的计算机实例,例如服务器、工作站等。

  • 资源是共享的资源,例如共享文件、共享数据库连接等。

  • 状态是指资源的当前状态,例如一个文件的读取状态、一个数据库连接的空闲状态等。

在 AQS 中,节点之间通过异步方式进行通信,节点不会直接访问其他节点。

节点请求资源时,会将请求信息发送到其他节点,这些节点会对请求进行处理,并返回请求结果。如果所有节点都可以访问共享资源,则节点会将请求信息转发给所有节点,并等待所有节点对该请求的响应都返回。

如果有节点不能访问共享资源,则节点会将请求信息放入一个队列中,等待其他节点可以访问共享资源时再处理该请求。

AQS 中有一些重要的操作,例如获取锁、释放锁、尝试获取锁等。

  • 获取锁是指一个节点请求获得对共享资源的访问权限。

  • 释放锁是指一个节点释放已经获得的对共享资源的访问权限。

  • 尝试获取锁是指一个节点请求获得对共享资源的访问权限,如果获取锁失败,则尝试获取锁的节点会释放已经获得的访问权限。

AQS 是一种非常可靠和安全的分布式锁算法,它可以在分布式系统中实现多个节点对共享资源的有序和互斥访问。

总之,AQS是Java中用于实现锁和同步器的基础框架,它提供了一种通用的机制来实现阻塞锁和相关同步器。AQS的核心是一个双向链表,通过CAS操作来实现对状态的原子更新和线程的阻塞和唤醒。AQS提供了独占模式和共享模式,以及一些抽象方法,可以方便地实现各种同步器。

12、说说 JUC 中,公平锁和非公平锁如何实现

在JUC(Java Util Concurrent)中,公平锁和非公平锁的实现方式不同。

公平锁是指多个线程按照申请锁的顺序来获取锁。也就是先来先得的原则,线程获取锁的顺序是按照线程加锁的顺序来分配的。

公平锁的实现方式是通过维护一个FIFO队列来实现的。在公平锁的实现中,当一个线程请求获取锁时,如果发现队列中已经有等待的线程,那么当前线程就会被加入到队列的末尾,等待前面的线程获取锁并释放后再尝试获取锁。

公平锁的实现方式虽然保证了锁的公平性,但是由于加锁和释放锁的操作需要频繁地操作队列,因此在高并发场景下,公平锁的性能会比非公平锁低。

非公平锁是指多个线程获取锁的顺序是不确定的,有可能后申请的线程比先申请的线程先获取到锁。

非公平锁的实现方式是在锁释放时,直接将锁分配给当前申请的线程,而不是先将线程加入到等待队列中。这种方式可以减少线程上下文切换的次数,提高锁的性能。但是由于非公平锁的获取顺序是不确定的,因此有可能会导致某些线程一直获取不到锁,出现“饥饿”现象。

Java 内置的锁(synchronized)是一种非公平锁机制,即所有线程都会获得相同的锁。如果多个线程同时请求同一个锁,那么只有一个线程能够获得锁,而其他线程则需要等待。这种锁机制无法保证对共享资源的访问是有序和互斥的,也就是说,多个线程可能会同时访问同一个共享资源,导致数据不一致性。 Java 中还提供了一些其他的锁机制,包括 ReentrantLock(重入锁)、ReadWriteLock(读写锁)等,这些锁机制可以更好地保证数据的正确性和多线程的同步访问。

下面我们以 ReentrantLock(重入锁)为例,介绍 JUC 中的公平锁和非公平锁的实现。

公平锁:

public class ReentrantLock {
    private boolean isLocked = false;
    private Thread lockedBy = null;
    private int waitCount = 0;

    public synchronized void lock() throws InterruptedException {
        Thread callingThread = Thread.currentThread();
        while (isLocked && lockedBy != callingThread) {
            wait();
        }
        isLocked = true;
        lockedBy = callingThread;
    }

    public synchronized void unlock() {
        if (Thread.currentThread() == lockedBy) {
            isLocked = false;
            notify();
        }
    }
}

在 ReentrantLock 中,实现了 lock() 和 unlock() 方法。当一个线程需要获得锁时,首先判断锁是否被其他线程获得(isLocked),如果是则等待(wait()),直到锁可用为止。获得锁后,将锁的持有者(lockedBy)设置为当前线程,并唤醒其他等待线程(notify())。

非公平锁:

public class NonfairLock {
    private boolean isLocked = false;
    private Thread lockedBy = Thread.currentThread();

    public void lock() throws InterruptedException {
        while (isLocked) {
            wait();
        }
        isLocked = true;
        lockedBy = Thread.currentThread();
    }

    public void unlock() {
        isLocked = false;
    }
}

在 NonfairLock 中,实现了 lock() 和 unlock() 方法。当一个线程需要获得锁时,直接调用 wait() 方法,直到锁可用为止。获得锁后,将锁的持有者设置为当前线

需要注意的是,公平锁模式可能会导致线程饥饿问题,因为某些线程可能会一直等待其他线程释放锁。因此,在选择公平锁模式时需要仔细考虑应用程序的需求和性能要求。

总之,公平锁和非公平锁的实现方式不同。公平锁通过维护一个FIFO队列来保证锁的公平性,而非公平锁则直接将锁分配给当前申请的线程,不保证锁的公平性。公平锁的性能较低,但保证了锁的公平性;非公平锁的性能较高,但可能会导致某些线程“饥饿”。在实际应用中,我们需要根据实际情况选择合适的锁类型。

说在最后:

在尼恩的(50+)读者社群中,很多、很多小伙伴需要进大厂、拿高薪。

尼恩团队,会持续结合一些大厂的面试真题,给大家梳理一下学习路径,看看大家需要学点啥?

前面用一篇文章,给大家介绍一篇滴滴真题:

收个滴滴Offer:从小伙三面经历,看看需要学点啥?

这些真题,都会收入到 史上最全、持续升级的 PDF电子书 《尼恩Java面试宝典》。

本文收录于 《尼恩Java面试宝典》 V69版。

基本上,把尼恩的 《尼恩Java面试宝典》吃透,大厂offer很容易到滴。

另外,下一期的 大厂面经大家有啥需求,可以发消息给尼恩。

技术自由的实现路径:

实现你的 架构自由:

吃透8图1模板,人人可以做架构

10Wqps评论中台,如何架构?B站是这么做的!!!

阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了

峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?

100亿级订单怎么调度,来一个大厂的极品方案

2个大厂 100亿级 超大流量 红包 架构方案

… 更多架构文章,正在添加中

实现你的 响应式 自由:

响应式圣经:10W字,实现Spring响应式编程自由

这是老版本 《Flux、Mono、Reactor 实战(史上最全)

实现你的 spring cloud 自由:

Spring cloud Alibaba 学习圣经》 PDF

分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)

实现你的 linux 自由:

Linux命令大全:2W多字,一次实现Linux自由

实现你的 网络 自由:

TCP协议详解 (史上最全)

网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!

实现你的 分布式锁 自由:

Redis分布式锁(图解 - 秒懂 - 史上最全)

Zookeeper 分布式锁 - 图解 - 秒懂

实现你的 王者组件 自由:

队列之王: Disruptor 原理、架构、源码 一文穿透

缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)

缓存之王:Caffeine 的使用(史上最全)

Java Agent 探针、字节码增强 ByteBuddy(史上最全)

实现你的 面试题 自由:

4000页《尼恩Java面试宝典 》 40个专题

免费获取11个技术圣经PDF:

posted @ 2023-05-30 22:05  疯狂创客圈  阅读(256)  评论(0编辑  收藏  举报