Java面试中常问的问题

  1. Java HashMap 两次put会发生什么
  2. springboot的starter是如何实现自动装配的?
  3. redis为什么快?
  4. redis常用的数据结构是什么,
  5. redis如何实现分布式锁,
  6. redis实现分布式锁有什么问题
  7. redis如果系统错误释放锁和业务执行超时释放锁,会发生什么?
  8. 分布式事务如何保证一致性
  9. 服务发现是怎么回事
  10. kafka消息队列为何要引入组?解决了什么问题
  11. mysql ABC索引, 实际用到了AB,没用到C,是什么情况会发生?

1. Java HashMap 两次put会发生什么

我们要了解HashMap的结构,并了解put时候发生了什么。

HashMap是键值对结构, 计算key的hash值来找对对应桶的位置,然后放进去。所以put(key, value) 基本可以认为 先 hash(key), 找到位置,放进去,再put同样的 key, 位置是一样的, 那就把原有值覆盖掉。

如果是put的是不同的key, hash(key)后找到桶位置,如果桶里没有值,直接就放进去,有的话用equals一个个比较,找到就替换值,找不到就拼到链表结尾。

2. springboot的starter是如何实现自动装配的?

我们知道springboot由于有很多starter和注解的存在大大简化了spring的使用,减少了很多xml配置,那么它是如何做到的?

其实是由springboot自定义starter的配置来完成的,springboot 通过在WEB-INF目录下spring.factories 配置自己定义注解,然后引入到工程里。 在2.7.11 里, META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 改这个文件就可以达到相同的结果。

3. redis为什么快?

  1. 数据结构简单:Redis中的数据结构相对简单,只包含基本的数据结构,例如字符串、列表、集合、哈希表和有序集合等,相比其他的数据库,Redis需要更少的时间用于数据结构实现和维护。

  2. 内存操作:Redis是高速的内存数据库,数据存储在内存中,所有操作都是内存操作,不存在硬盘IO等慢速操作。

  3. 单线程模型:Redis采用单独的IO线程,使得应用程序并发访问时需要的锁或者同步操作少很多,提高了系统的吞吐量。

  4. 异步非阻塞IO:Redis采用异步非阻塞IO模型,使得服务器能够处理成百上千个客户端的请求,而且不会由于单个客户端请求阻塞而导致服务器无法处理其他请求。

综上所述,Redis之所以快就是因为其采用了简单的数据结构、内存操作、单线程模型和异步非阻塞IO等技术,从而大幅提升了数据读写的速度和系统吞吐量。

4. redis数据结构有哪些

  1. 字符串(Strings):存储字符串、整数或者浮点数。

  2. 列表(Lists):一个列表结构,通过一个键可以对一个字符串列表进行操作,支持从列表两端(左端/右端)添加或者删除数据。

  3. 集合(Sets):一个无序的、元素不重复的集合,支持对集合进行添加、删除和判断两个集合的交集、并集和差集等操作。

  4. 哈希表(Hashes):一个键值对的集合,键和值都是字符串类型,支持添加、修改、删除和查询具体的某个键值对。

  5. 有序集合(Sorted Sets):与集合类似,是一个无序的、元素不重复的集合,但不同的是,每一个元素都会附带一个权重(score),支持根据权重快速查询元素。

其中,字符串、列表、集合、哈希表和有序集合都具有很好的应用场景,应用广泛,并且在实现上都相对简单,易于理解和使用。

5. 如何用redis实现分布式锁

redis的原子操作 setnx来实现

6. redis实现分布式锁有什么问题

redis 分布式锁有以下几个问题需要注意:

  1. 锁失效 - 如果持有锁的客户端崩溃而没有主动释放锁,会导致锁失效。为了解决这个问题,通常会设置锁的过期时间。客户端申请锁之后需要在过期时间内完成操作并释放锁,否则锁会被自动删除,其他客户端可以再申请锁。.
  2. 锁的安全性 - 如果多个客户端同时申请一把锁,Redis 会随机将锁给一个客户端。这可能导致获取锁的客户端并不是最先申请锁的那个。为了解决这个问题,可以使用 Redis 的 Redlock 算法来提高获取锁的安全性。
  3. 死锁 - 如果一个客户端持有锁 A 并申请锁 B,而另一个客户端持有锁 B 并申请锁 A,这会导致两个客户端相互等待而产生死锁。为了避免这种情况,应该确保所有的客户端以同样的顺序申请多个锁。
  4. 锁的选举 - 在主从复制的环境下,如果客户端将锁申请在从节点上,当主节点下线时,新的主节点上不会存在这个锁,会导致锁的选举问题。为此,客户端应该始终将锁申请到主节点上,而不是从节点。
  5. 释放锁失败 - 当客户端持有锁并完成操作后,如果在释放锁时客户端崩溃,会导致锁一直无法释放而永久失效。为了避免这种情况,可以为锁设置过期时间,即使客户端崩溃,锁也会在一定时间后自动释放。

以上是 Redis 分布式锁需要考虑和解决的几个问题,通过设置锁的过期时间、使用 Redlock 算法、设置加锁的顺序、将锁申请到主节点等方式可以避免或者解决这些问题,实现安全高效的 Redis 分布式锁。

7. redis如果系统错误释放锁和业务执行超时释放锁,会发生什么?

如果应用在持有 Redis 分布式锁的过程中,锁因为超时而被自动释放,会发生以下问题:

  1. 资源被其他客户端修改 - 在锁释放之后,其他等待锁的客户端会获得到锁,并对共享资源进行修改。这会导致先前持有锁的客户端操作共享资源时,资源处于不一致的状态。
  2. 产生脏数据 - 如果一个客户端 half-done 就释放了锁,数据可能处于中间状态,这会产生脏数据。其他客户端看到这些脏数据,并基于这些数据进行决策,会造成错误。
  3. 并发操作混乱 - 多个客户端并发对同一个资源进行操作,顺序混乱,会导致最终结果错误。特别是一些需要顺序执行的操作。

为了解决超时释放锁带来的问题,可以采取以下措施:

  1. 锁续期 - 客户端在持有锁的过程中,需要不断地续期锁的有效期,防止锁被自动释放。当操作完成后,客户端主动释放锁。
  2. 操作超时 - 为整个业务操作设置一个超时时间,这个超时时间要比锁的超时时间长。如果在操作超时时间内,客户端未完成操作并释放锁,则认为该操作已经超时失败。这可以避免产生脏数据。
  3. 锁标识 - 每次获取锁时,都在 value 中保存一个锁标识,包括请求 ID、时间戳等。当锁超时释放后,如果 value 发生变化,表示该锁已经被其他客户端获取。原客户端需根据锁标识判断是否是自己持有的锁,如果不是则退出操作。
  4. 操作幂等 - 使共享资源操作变得幂等,这样无论客户端对资源操作多少次,结果都是一致的。这可以避免并发操作导致的结果错误。

综上,通过设置锁和业务操作的超时时间,使用锁标识判断锁的持有者,采取操作幂等设计等措施,可以尽量减少和避免Redis超时释放锁带来的问题

8. 分布式事务如何保证一致性

分布式事务要保证一致性,需要解决几个问题:

  1. 全局序列化 - 要保证所有节点在同一时间内只能有一个事务被执行,否则会发生写写冲突。通常使用分布式锁来实现全局序列化。
  2. 原子提交 - 要保证在所有节点上,事务的提交或者回滚是原子的,不会处于中间状态。通常使用两阶段提交协议来实现原子提交。
  3. 隔离级别 - 要确保事务内的读数据是事务开始时的最新数据版本(串行化),不会读取到其他并发事务修改的数据(避免脏读)。
  4. 补偿机制 - 如果在提交过程中,某些节点发生故障,需要补偿机制来确保所有节点数据的一致性。通常通过回滚已经执行的节点来实现补偿。
  5. 超时处理 - 要考虑在执行过程中某些节点超时的情况,并在超时后采取恢复措施保证一致性。

常见的一致性实现方案有:

  • 两阶段提交(2PC)协议:使用prepare和commit两个阶段来控制所有的节点要么全部commit,要么全部rollback。
  • 三阶段提交(3PC)协议:在两阶段提交的基础上增加了一个预提交确认阶段,可以避免脏写。
  • PAXOS 算法:PAXOS 是一个比较复杂的多轮投票过程,可以在分布式系统中达成一致并选择出一个值。
  • TCC 模式:TCC 模式分为 Try、Confirm、Cancel 三个阶段。Try 阶段执行业务操作,Confirm 阶段提交确认,Cancel 阶段执行补偿操作。
  • XA 接口:XA 接口是 JTA 规范中定义的分布式事务处理接口,其主要目的是统一接口,实现异构数据库之间的分布式事务处理。

总之,要实现分布式事务的一致性,需要遵循 ACID 原则,解决全局序列化、原子提交、隔离级别、补偿机制和超时处理等问题。并且需要选择两阶段提交、三阶段提交、PAXOS 算法、TCC 模式或者 XA 接口等算法和协议来控制分布式事务的执行。

9. 服务发现是怎么回事

微服务架构中,服务发现是很关键的一个部分。它的主要作用是管理各个微服务实例的信息,并将这些信息提供给其他微服务或者客户端使用。常见的服务发现机制有:

  1. 客户端服务发现 - 客户端直接查询注册中心获取服务实例信息。这种方式的实现简单,但是会造成注册中心的压力增大,并且一旦注册中心宕机,服务将不可用。
  2. 服务端服务发现 - 服务启动时直接从注册中心获取其它服务的实例信息,并缓存在本地。这种方式可以减轻注册中心压力,但是当服务实例发生变化时,需要更新所有依赖该服务的其它服务的缓存,较为麻烦。
  3. 客户端缓存服务发现 - 客户端先从注册中心获取服务实例信息并缓存在本地,后续会定期从注册中心更新数据。这种方式可以减轻注册中心压力,并且当注册中心宕机时,服务仍可用。这是一种较好的服务发现方式。

常用的服务注册中心有:

  • Eureka:Netflix 开源的一款服务注册中心,用于服务的注册、发现和负载均衡。
  • Consul:HashiCorp 开源的服务发现和配置管理系统。支持健康检查、KV 存储、多数据中心等功能。
  • Etcd:CoreOS 开源的一个分布式键值存储系统,可用于服务发现、共享配置以及分布式锁等。
  • Zookeeper:Apache 开源的一个分布式协调工具,支持服务发现、配置管理等功能。

具体的服务发现流程是:

  1. 服务启动时向注册中心注册自己的网络地址和端口等信息。
  2. 服务消费者从注册中心获取提供者的网络地址和端口等信息。
  3. 消费者使用提供者的信息与其建立连接,发起 RPC 请求。
  4. 注册中心定期检查各服务实例是否可用,如果某实例故障,则从注册列表中删除。
  5. 消费者定期从注册中心更新服务提供者的列表,并刷新本地缓存。
  6. 服务提供者定期向注册中心发送心跳,以续约在注册中心的注册信息。

以上就是微服务架构中的服务发现机制和基本流程,用来实现服务实例的动态注册与发现。

10. kafka消息队列为何要引入组?解决了什么问题

Kafka 引入消费组的主要目的是为了实现消息的负载均衡,以及在消费者不可用的情况下实现消息的高可用。

没有消费组的情况下,每条消息会被所有的消费者消费一次,导致消息被重复消费。而引入消费组后,属于同一个组的消费者会负责消费不同分区的数据,从而实现负载均衡,并且同一个消息也只会被组内的一个消费者消费,避免了消息重复消费的问题。

另外,当某个消费者不可用时,其所属分区的数据可以被 group 中其他消费者消费,保证消息不会丢失,实现了高可用。每个分区的数据只能被组内的一个消费者消费,而不是所有消费者。

举个例子,有 3 个 Topic,每个 Topic 有 3 个分区。有两个消费组,每个消费组有两个消费者。那么:- 消费组 1 - 消费者 1-1 消费 Topic 1 分区 0 和 Topic 2 分区 0 的数据 - 消费者 1-2 消费 Topic 1 分区 1 和 Topic 2 分区 1 的数据- 消费组 2 - 消费者 2-1 消费 Topic 1 分区 2 和 Topic 3 分区 0 的数据 - 消费者 2-2 消费 Topic 2 分区 2 和 Topic 3 分区 1 的数据

这样,每个 Topic 的每个分区的数据只会被一个消费组中的一个消费者消费,实现了负载均衡和消息高可用。

所以,Kafka 消费组的主要作用是:

  1. 实现消息的负载均衡,通过给不同的消费者分配不同的分区的数据,达到均衡的消费效果。
  2. 实现消息的高可用,当某个消费者不可用时,其分区的数据可以被组内其他消费者消费,保证消息不丢失。
  3. 避免消息被多个消费者重复消费的问题。

11. mysql ABC索引, 实际用到了AB,没用到C,是什么情况会发生?

B 是范围查询