今日5道java八股文面试题

文章探讨了JavaString的不可变性设计原因,包括安全性、线程安全和性能优化,以及在设计高并发系统时的策略,如使用缓存、MQ和手动创建线程池。此外,还解释了阿里巴巴规定不推荐使用Executors创建线程池的原因,以及如何通过主从复制、Sentinel和保证幂等性来提高Redis的高并发和高可用性。
摘要由CSDN通过智能技术生成

1、你知道为什么String被设计为是不可变的?

 首先我们要知道为什么String类型可以做到不可改变,首先就是要知道String底层代码

  1. 字符数组和final修饰符: String类内部使用一个final修饰符修饰的字符数组来存储字符串的字符内容。final修饰符保证了字符数组在创建后不能再被改变,从而确保字符串的内容不会被修改。
  2. 没有公开的修改方法: String类没有提供用于修改字符串内容的公开方法。所有看似修改字符串的方法,比如substringreplace等,实际上都是返回一个新的字符串对象,而不会直接修改原始字符串对象。
  3. 字符串常量池: Java中的字符串常量池(String Pool)是一个存放字符串字面值的特殊内存区域。当创建一个字符串时,如果字符串常量池中已经存在相同内容的字符串,则会返回常量池中的字符串对象,而不会创建新的对象。这样可以避免重复创建相同内容的字符串,节省内存空间,并且确保字符串的不可变性。
  4. String类被final修饰:String这个类被final所修饰的,所以也不会被继承而扩展使得破坏掉String的不可变性。

 Java中的String被设计为不可变的有多个原因:

  1. 安全性:在Java中,字符串常常被用作Map的key,因为字符串的不可变性可以保证哈希值的稳定性。如果字符串是可变的,当字符串被修改时,它的哈希值也会发生变化,导致在哈希表中无法正确查找或删除元素。不可变的字符串可以确保在哈希表等数据结构中的正确性。
  2. 线程安全: 字符串不可变性使得字符串对象在多线程环境中是线程安全的。多个线程可以同时共享一个字符串对象,而不需要额外的同步措施。这在并发编程中非常重要,可以避免线程安全问题。
  3. 性能优化: 字符串的不可变性可以带来性能上的优势。由于字符串是不可变的,编译器或运行时环境可以在需要的时候进行优化,例如在内存中共享字符串字面值,而不必为每个使用相同字符串的地方都分配新的内存。这样可以减少内存占用,并提高运行时效率。
  4. 缓存哈希值: 字符串的不可变性使得可以在创建字符串时计算并缓存其哈希值。这样,当需要字符串的哈希值时,不必每次都重新计算,提高了性能。
  5. 传递性: 字符串的不可变性确保在传递参数时不会意外修改字符串的内容,避免了潜在的副作用。

虽然字符串不可变性带来了上述优点,但在某些场景下,频繁操作字符串可能会导致性能问题,因为每次修改字符串都会创建一个新的字符串对象。在这种情况下,可以使用StringBuilderStringBuffer类来处理可变字符串,这两个类提供了修改字符串内容的方法,但它们没有字符串的不可变性所带来的优势。在大多数情况下,Java的设计者认为字符串的不可变性带来的好处更加重要,因此选择了将String设计为不可变的。

2、如何设计一个高并发的系统?

系统拆分

将一个系统拆分为多个子系统,用 dubbo 、springcloud、springcloudalibaba来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以扛高并发么。
缓存
缓存,必须得用缓存。大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了,毕竟人家 redis 轻轻松松单机几万的并发。所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发。
MQ
MQ,必须得用 MQ。可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改,那高并发绝对搞挂你的系统,你要是用 redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU了,数据格式还无比简单,没有事务支持。所以该用mysgl 还得用 mysal 啊。那你昨办?用 MQ 吧,大量的写请求灌入 MQ 里,排队慢慢玩儿,后边系统消费后慢慢写,控制在 mysal 承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。MQ 单机抗几万并发也是 ok的,这个之前还特意说过。
分库分表
分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来扛更高的并发,然后将单表拆分为多个表,每个表的数据量保持少一点,提高 sql 跑的性能。
读写分离
读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库。
ElasticSearch
Elasticsearch,简称es。es 是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来打更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用 es 来承载,还有一些全文搜索类的操作,也可以考虑用 es 来承载。

上面的6点,基本就是高并发系统肯定要干的一些事儿,大家可以仔细结合之前讲过的知识考虑一下,到时候你可以系统的把这块闸述一下,然后每个部分要注意哪些问题,之前都讲过了,你都可以阐述阐述,表明你对这块是有点积累的。

 3、为什么阿里巴巴规定不能使用Executors去创建线程池?

当我们需要在程序中使用线程池来处理多个任务时,通常会使用Java提供的ExecutorService来创建线程池。在Java中,有一个Executors工具类,它提供了一些简单的方法来创建线程池,比如newFixedThreadPoolnewCachedThreadPool等。

然而,阿里巴巴的规约建议不要使用Executors提供的这些简单方法,而是要手动创建线程池。

这是因为Executors提供的方法可能在某些情况下不是最合适的。它们通常使用默认的配置,可能会创建太多线程或者使用无界队列,导致系统资源的浪费或者内存溢出。

相反,手动创建线程池能够更好地控制线程池的大小和行为。我们可以根据实际需求来设置线程池的核心线程数、最大线程数和队列的大小等参数。这样,我们可以根据系统的硬件资源和并发需求,让线程池运行得更高效,避免资源的浪费和系统崩溃。

所以,阿里巴巴的建议是使用ThreadPoolExecutor类手动创建线程池,并合理配置线程池的参数。这样可以确保线程池在高并发场景下能够更好地工作,保证系统的稳定性和性能。

4、你们是如何保证Redis高并发、高可用的呢?

  1. 主从复制:使用Redis的主从复制功能,将主服务器上的数据复制到多个从服务器。这样,从服务器可以处理读取请求,减轻主服务器的负载,提高读取性能,并增加数据冗余,提高可用性。

  2. Sentinel哨兵:Sentinel是Redis的高可用性解决方案,它监控主服务器的状态,并在主服务器故障时自动将从服务器提升为新的主服务器,确保Redis服务的高可用性。

  3. 集群模式:Redis集群模式将数据分片存储在多个节点上,每个节点负责一部分数据。这样可以水平扩展Redis,实现更好的负载均衡和并发性能。

  4. 数据持久化:通过开启RDB快照和AOF日志,将数据持久化到磁盘,以便在Redis重启后可以恢复数据,保证数据的可靠性和持久性。

  5. 合理设计数据结构:根据业务需求,选择合适的数据结构和Redis命令,避免不必要的计算和内存占用,提高Redis的性能和效率。

  6. 定期监控和优化:使用监控工具,对Redis的性能、内存使用情况、QPS等进行定期监控和优化,及时发现并解决潜在问题。

  7. 使用连接池:在应用程序中使用连接池,有效管理与Redis的连接,避免频繁创建和关闭连接,提高性能。

  8. 硬件升级:如果Redis服务器性能不足,可以考虑升级硬件,增加内存和处理能力。

请注意,高并发和高可用性往往需要综合考虑硬件、软件和架构等多方面因素。具体的实施策略可能会因应用程序的具体需求和环境而有所不同。建议在部署和配置Redis时,根据实际情况选择合适的策略和措施。

4.1、下面是主从复制的实现流程:

主从架构图:

主从复制的核心原理:

  1. 启用主从复制:首先,在Redis配置文件中配置主从复制的相关参数。打开主服务器的配置文件(redis.conf)并添加以下配置:
# 在主服务器配置文件中添加
slaveof <master-ip> <master-port>

这会将当前服务器设置为从服务器,并将其连接到指定的主服务器(master-ip和master-port为主服务器的IP地址和端口号)。

  1. 启动Redis服务器:分别启动主服务器和从服务器。

  2. 数据同步:从服务器启动后,会自动连接到主服务器,并开始进行初次全量同步。主服务器将所有数据发送给从服务器,使其数据与主服务器一致。

  3. 增量同步:一旦初次全量同步完成,主服务器将持续监视自己的数据更改,并将更改的数据发送给从服务器,以便保持数据的一致性。

在Redis的主从复制中,确实可能会遇到网络断开的情况。当网络连接出现问题时,从服务器与主服务器之间的通信可能会中断。这种情况下,Redis会根据配置和策略采取不同的措施来处理。

  1. 断线重连:从服务器会尝试重新连接到主服务器。Redis从服务器会定期尝试重新连接主服务器,以确保在网络故障后能够恢复与主服务器的连接。默认情况下,从服务器会在连接丢失后每隔一秒尝试重新连接一次,可以通过配置选项来调整重试频率。

  2. 容错机制:Redis从服务器会记录连接失败的次数。如果从服务器无法重新连接到主服务器,它会记录连接失败的次数。一旦连接失败次数达到一定阈值,从服务器将认为主服务器不可用,并在此时触发故障转移(failover)。
  3. 故障转移:当主服务器出现故障或被宣告不可用时,Redis Sentinel或其他监控机制会检测到这个状态。在这种情况下,Redis从服务器可能会被晋升为新的主服务器,从而确保Redis服务的高可用性。故障转移后,应用程序需要重新配置连接信息,将连接指向新的主服务器。

在主从复制中,网络断开是一个需要特别关注的问题。为了确保高可用性和数据一致性,建议采取以下措施:

  1. 配置合理的连接超时和重试策略:确保连接超时不过长,以便及时发现网络故障并进行断线重连。

  2. 使用监控工具:使用监控工具,如Redis Sentinel,来实时监测Redis服务器的状态,以便及时发现主服务器故障或网络连接问题。

通过合理的配置和监控机制,可以有效地处理主从复制中可能遇到的网络断开问题,从而确保Redis服务的高可用性和数据一致性。

  • 备份数据:定期对主服务器进行数据备份,以防止数据丢失。可以使用Redis的持久化机制来进行数据备份。

  • 处理故障转移:在故障转移发生后,及时处理应用程序的连接信息,将连接指向新的主服务器。

  • 主从复制的断点续传
    从 Redis2.8 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
    master node 会在内存中维护一个backlog, master 和 slave 都会保存一个replica offset (标志位)还有一个 master run id,offset 就是保存在 backlog中的。如果 master 和 slave 网络连接断掉了,slave 会 master 从上次 replica offset 开始继续复制,如果没有找到对应的 offtset,那么就会执行一次 resynchronization (在同步)。
    如果根据 host+ip 定位 master node,是不靠谱的,如果 master node 重启或者数据出现了变化,那么 slave node 应该根据不同的 runid 区分。

  • 复制的完整流程

  • slave node 启动时,会在自己本地保存 master node 的信息,包括 master node 的 host 和 ip,但是复制流程没开始。
    slave node 内部有个定时任务,每秒检查是否有新的 master node 要连接和复制,如果发现,就跟 master node 建立 socket 网络连接。然后slave node 发送 ping 命令给 master node如果 master 设置了 requirepass(密码认证),那么 slave node 必须发送 masterauth 的口令过去进行认证。master node 第一次执行全量复制,将所有数据发给 slave node。而在后续,master node 持续将写命令,异步复制给 slave node.

  •  4.2、Sentinel哨兵

Sentinel(哨兵)是Redis的高可用性解决方案之一,它是一个独立的进程,用于监控和管理Redis服务器集群的状态。Sentinel的主要作用是实时监控Redis主服务器和从服务器的状态,当主服务器出现故障或变得不可用时,自动进行故障转移,将从服务器升级为新的主服务器,以确保Redis集群的高可用性。

Sentinel的主要特点和功能包括:

  1. 监控:Sentinel定期检查Redis服务器的健康状态,包括主服务器和从服务器,确保它们处于正常运行状态。

  2. 自动故障转移:当主服务器故障或不可用时,Sentinel会自动进行故障转移。它会选举一个从服务器作为新的主服务器,并通知其他从服务器和应用程序,将它们的连接信息更新为新的主服务器。

  3. 主观下线和客观下线:Sentinel使用主观下线和客观下线来判断服务器的状态。主观下线是指一个Sentinel认为某个服务器不可用,客观下线是指多数Sentinel节点都认为该服务器不可用。只有当一个服务器被多数Sentinel节点标记为客观下线时,Sentinel才会触发故障转移。

  4. 选举机制:Sentinel使用Raft算法进行主服务器的选举,确保选出的新主服务器是合法且可靠的。

  5. 配置监控:Sentinel可以监控多个Redis集群,并可以通过配置文件指定监控的规则和故障转移策略。

  6. 通知机制:Sentinel可以向管理员发送警报通知,以便及时处理故障和维护。

使用Sentinel,您可以构建一个高可用性的Redis集群,即使主服务器发生故障,也能保持Redis服务的可用性。Sentinel通常以多个节点运行,以避免单点故障,并通过多数原则来确保集群的健壮性。通过结合主从复制和Sentinel,可以实现Redis集群的高可用性和负载均衡,提供稳定可靠的服务。

5、MQ又遇到过重复消费的问题吗?怎么解决的呢?

面试官问你这个问题本质还是想问你使用消息队列如何保证幂等性

首先,比如说RabbitMQ、RocketMQ和kafka,都可能出现重复消费的问题,因为这个问题不是MQ能保证的,是由我们开发来保证的。

就比如kafka,实际上是有一个offset的概念,就是每次消费写进去,都有一个offset,代表消息的序号,然后消费者消费了之后,每隔一段时间,会把自己消费过的消息提交一下,表示“我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的offset来继续消费”。

但是还是会有例外的呢,就比如突然断电,这会导致消费者有些消息处理了,但是没有提交offset,那些被处理过的消息就会被重复消息一次。

举个例子:

有这么个场景。数据 1/2/3 依次进入 Kaka,Kafka 会给这三条数据每条分配一个 offset,代表这条数据的序号,我们就假设分配的 offset 依次是152/153/154。消费者从Kafka 去消费的时候,也是按照这个顺序去消费,假如当消费者消费了 offset=153 的这条数据,刚准备去提交offset 到 Zookeeper,此时消费者进程被重启了。那么此时消费过的数据 1/2 的 ofset 并没有提交,Kaka 也就不知道你已经消费了offset=153 这条数据。那么重启之后,消费者会找 Kaka 拿把上次我消费到消息后面的数据继续传过来。由于之前的 offset 没有提交成功,那么数据 1/2 会再次传过来,如果此时消费者没有去重的话,那么就会导致重复消费。注意: 新版的 Kafka 已经将 offset 的存储从 Zookeeper 转移至 Kafka brokers,并使用内部位移主题consumer_offsets 进行存储。

如果消费者干的事儿是拿一条数据就往数据库里写一条,会导致说,你可能就把数据 1/2 在数据库里插入了 2次,那么数据就错啦。其实重复消费不可怕,可怕的是你没考虑到重复消费之后,怎么保证幂等性。举个例子吧。假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性,一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错,所以第二个问题来了,怎么保证消息队列消费的幂等性?

其实还是得结合业务来思考,我这里给几个思路:

  • 比如你拿个数据要写库,你先根据主键查一下,如果这数据有了,你就别插入了,update 一下好吧。
  • 比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。
  • 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个id 去比如 Redis 里查一下,之前消费过吗? 如果没有消费过,你就处理,然后这个id 写Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
  • 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值