Redis、Spring/SpringMVC、JVM、微服务(Spring Boot/Spring Cloud )(面试题)

Redis

1. 为什么要用 redis ?为什么要用缓存?主要从"高性能"和“高并发”这两点来描述这个问题。

高性能

  1. 内存存储:Redis将数据存储在内存中,相比磁盘存储,内存访问速度极快,从而显著提升了数据访问的性能。

  2. 数据结构丰富:Redis支持多种数据结构,如字符串、列表、集合、哈希等,能够更高效地处理各种复杂场景下的数据操作。

  3. 避免数据库瓶颈:通过将热点数据存储在Redis中,减少了对后端数据库的访问次数,避免了数据库成为性能瓶颈。

高并发

  1. 扩展性强:Redis支持主从复制和集群模式,可以轻松实现水平扩展,从而处理更高的并发请求。

  2. 缓解数据库压力:在高并发场景下,Redis作为缓存层可以承接大量读请求,减轻数据库压力。

  3. 分布式锁和限流:Redis支持分布式锁和限流策略,有助于在高并发环境中控制对共享资源的访问和防止系统过载。

2. redis和memcached 的区别

  1. 数据结构:Redis支持更丰富的数据结构,如list、set、sorted set、hash等,而Memcached主要支持简单的key-value存储。

  2. 内存管理:Redis支持数据的持久化和复制功能,而Memcached采用Slab Allocation机制,预分配内存块并根据对象大小存储,当内存不足时采用LRU算法淘汰旧数据。

  3. 扩展性:Redis支持主从复制和集群模式,具有更好的水平扩展性,而Memcached的每个进程间不通信,需要客户端提供分布式算法。

  4. 特性:Redis具有更丰富的功能,如键过期、发布订阅、Lua脚本支持等,而Memcached则更注重于简单的key-value存储和缓存功能。

3.redis 常见数据结构以及使用场景分析

数据结构

  1. String(字符串):用于存储字符串类型的数据,是最常用的数据类型,可以接受任何格式的数据。

  2. Hash(哈希):类似于字典或Map,用于存储键值对,适用于存储结构化数据。

  3. List(列表):有序的字符串列表,支持双向操作(如插入、删除),适用于实现消息队列、任务队列等。

  4. Set(集合):无序且不重复的字符串集合,适用于需要快速查找、判断元素是否存在的场景。

  5. ZSet(有序集合):每个元素都关联一个分数,按分数进行排序,适用于排行榜、计数器等场景。

使用场景

  1. 缓存:将热点数据存储在Redis中,减少数据库访问次数,提高系统性能。

  2. 分布式锁:利用Redis的原子操作实现高效可靠的分布式锁。

  3. 消息队列:利用List或Stream实现消息队列,生产者向队列中添加消息,消费者从队列中取出消息进行处理。

  4. 计数器:利用ZSet的有序特性实现计数器功能,如网站访问量统计、用户点赞数等。

  5. 会话管理:将用户会话信息存储在Redis中,实现跨服务器共享会话。

  6. 地理位置服务:利用Redis的geohash算法支持存储和查询地理位置信息。

  7. Web集群Session共享:在Web集群中,使用Redis存储和共享Session信息,确保用户在不同服务器间切换时Session信息保持一致。

4.请描述 redis 设置过期时间的含义和作用

Redis设置过期时间的含义是为一个key设置一个时间窗口,在这个时间窗口内,该key可以被访问和使用,到达时间窗口后,Redis会自动删除过期的key。

作用主要包括:

  1. 内存管理:自动清理不再需要的数据,避免数据无限期地占用内存资源,从而有效管理内存资源,防止内存泄漏和溢出。

  2. 数据新鲜度:对于时效性强的数据,设置过期时间可以确保缓存中的数据保持最新状态。过期后,Redis会自动删除过期数据,应用程序可以重新从原始数据源获取最新数据并重新缓存。

  3. 防止缓存问题

    • 缓存雪崩:通过设置过期时间的随机性,分散缓存的过期时间,避免大量缓存同时失效,减轻数据库的压力。

    • 缓存击穿:当热点数据过期后,由于设置了较短的过期时间,Redis可以迅速重新加载数据到缓存中,减少数据库压力。

  4. 实现特定功能:如通过Redis的过期时间实现分布式锁、限流等常见应用场景。

总的来说,设置过期时间有助于Redis更好地管理和利用内存资源,同时保证数据的时效性和系统的稳定性。

5.redis 内存淘汰机制(MySQL里有 2000w数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?)

Redis 的内存淘汰机制是为了在内存达到上限时,自动淘汰一些数据以释放空间。为了确保 Redis 中只存储热点数据,可以采取以下策略:

  1. 设置TTL(Time To Live):为 Redis 中的数据设置合理的过期时间,使不常访问的数据自动过期。这有助于保持 Redis 中的数据都是近期的热点数据。

  2. 使用淘汰策略

    • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key。

    • volatile-lru:根据 LRU 算法移除设置了过期时间的 key。

    • 考虑到 MySQL 中有 2000w 数据,而 Redis 中只存 20w 数据,可能更倾向于使用 volatile-lru 或与 LRU 相关的策略,因为这样可以确保那些最近被访问且设置了过期时间的 key 被保留。

  3. 数据预热:在系统启动或特定时间点,将预期的热点数据预先加载到 Redis 中,确保这些数据在需要时能被快速访问。

  4. 监控与分析:使用 Redis 的监控工具(如 Redis Insight)和分析技术来跟踪哪些数据是热点数据,哪些数据不常被访问。根据这些信息,可以调整数据的 TTL 和淘汰策略。

  5. 容量规划:根据业务需求和数据增长趋势,合理规划 Redis 的容量。如果 Redis 的容量不足以存储所有热点数据,可能需要考虑增加内存、使用 Redis 集群或优化数据结构以减少内存使用。

简写总结

  • 设置 TTL,确保数据不过期。

  • 使用 LRU 淘汰策略,保留热点数据。

  • 数据预热,预先加载热点数据。

  • 监控分析,调整策略以适应数据变化。

  • 合理规划 Redis 容量,确保足够存储热点数据。

6.redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)

Redis的持久化机制主要保证在Redis挂掉之后重启时数据可以进行恢复。它提供了两种主要的持久化方式:

  1. RDB(Redis DataBase):RDB是按照一定的时间间隔将内存中的数据集快照写入磁盘,生成一个dump.rdb文件。当Redis重启时,可以从这个文件中恢复数据。RDB是Redis的默认持久化方式,它提供了较快的恢复速度。

  2. AOF(Append Only File):AOF则是将Redis执行的所有写命令记录到日志文件中。当Redis重启时,它会重放AOF文件中的写命令来恢复数据。AOF提供了更高的数据安全性,但通常AOF文件较大,加载速度较慢。

7.请描述 redis 的 事务是如何实现的 ?

Redis的事务实现包含三个步骤:

  1. 开始事务:客户端使用MULTI命令来开启一个事务。

  2. 命令入队:客户端将事务中要执行的具体操作(如GET、SET等)发送给服务器。这些命令会被Redis暂存到一个命令队列中,并不会立即执行。

  3. 执行事务:客户端使用EXEC命令来提交事务,使Redis实际执行队列中的命令。如果服务器收到EXEC命令,则会按照命令队列中的顺序执行这些命令,并返回结果。

Redis的事务机制能保证命令的原子性,即要么全部执行,要么全部不执行,但不能保证像关系型数据库那样的ACID(原子性、一致性、隔离性、持久性)属性。

8.Redis 缓存使用过程当中,关于缓存穿透、缓存雪崩、缓存预热以及缓存降级这四点常见问题如何解决 ?

  1. 缓存穿透

    • 布隆过滤器:在查询缓存之前,使用布隆过滤器快速判断数据是否存在于缓存中,避免直接访问数据库。

    • 空值缓存:对于不存在的数据,也在缓存中保存一个空值或特殊标记,但设置较短的过期时间。

  2. 缓存雪崩

    • 缓存过期时间分散:避免大量缓存同时过期,将过期时间设置得随机一些。

    • 互斥锁:在缓存失效后,使用互斥锁或分布式锁保证只有一个线程去数据库加载数据,其他线程等待。

    • 异步加载:使用后台线程或队列来异步加载缓存,避免大量请求直接打到数据库。

  3. 缓存预热

    • 提前加载:在系统启动或低峰期时,将热点数据提前加载到缓存中。

    • 脚本工具:使用脚本或定时任务定期或按需加载数据到缓存。

  4. 缓存降级

    • 熔断机制:当缓存服务出现问题时,快速失败并降级到备用方案,如读取数据库或返回默认数据。

    • 限流降级:使用限流策略,如令牌桶或漏桶算法,控制对缓存的请求量,防止系统过载。

    • 关闭部分缓存:根据业务需要,关闭部分非关键的缓存功能,以减轻系统压力。

这些策略可以根据具体的业务场景和需求进行选择和组合使用。

9.请介绍“分布式锁"以及“分布式自增 ID"常见的应用场景

分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。常见的应用场景包括:

  1. 临界区互斥访问:防止多个节点同时修改共享数据,确保数据的一致性。

  2. 分布式环境同步访问:在分布式环境中同步访问共享资源,如分布式任务调度、分布式事务等。

分布式自增 ID

分布式ID是指在分布式环境下可用于对数据进行标识且易存储的全局唯一的ID标识。常见的应用场景包括:

  1. 订单号生成:在高并发的电商平台中,为了保证订单号的唯一性和顺序递增,使用分布式ID作为订单号。

  2. 数据库主键生成:在数据库分库分表后,为了避免多个节点插入相同的主键值,使用分布式ID生成不重复的主键。

  3. 数据消息队列的幂等性:在数据消息队列中,使用分布式ID确保多次消费同一条消息不会产生副作用。

这些应用场景都依赖于分布式锁和分布式ID来保证数据的一致性和唯一性。

10.请描述一下 Redis 的几种集群模式

  1. 主从复制模式(Master-Slave)

    • 原理:一个Redis实例作为主节点(Master),其他实例作为从节点(Slave)。主节点负责处理写操作,从节点负责处理读操作或作为主节点的备份。

    • 优点:读写分离,分担主节点的读写压力;方便容灾恢复。

    • 缺点:每台Redis服务器存储的数据相同,浪费内存;不具备自动容错和恢复功能,需人工介入;较难支持在线扩容。

  2. 哨兵模式(Sentinel)

    • 原理:哨兵是一个独立的进程,负责监控Redis主从节点的运行状态。当主节点宕机时,哨兵会自动将从节点中的一个提升为主节点,确保系统的高可用性。

    • 优点:自动故障转移,提供高可用性;支持在线扩容。

    • 缺点:相对复杂,需要部署和管理哨兵节点。

  3. 集群模式(Cluster)

    • 原理:将数据分散存储到多个Redis节点上,每个节点只存储一部分数据。每个节点都保存了数据和其他节点的映射关系,当访问数据时,会先查询映射关系,然后到相应的节点上获取数据。

    • 优点:支持在线扩容,数据分散存储,提高系统的扩展性和容错性。

    • 缺点:相对复杂,需要管理和维护多个节点;需要手动或自动进行数据的分片。

Spring/SpringMVC

1.Spring 事务如果失效了,可能是哪几种原因?

  1. 方法修饰符问题:如果@Transactional事务注解添加在不是public修饰的方法上,Spring的事务可能会失效。

  2. 类未被Spring管理:如果事务方法所在的类没有被加载到Spring IoC容器中,即该类没有被Spring管理,Spring无法实现代理,事务也会失效。

  3. 事务传播行为配置错误:如果内部方法的事务传播类型为不支持事务的传播类型,内部方法的事务在Spring中会失效。

  4. 数据源配置问题:未正确配置数据源的JDBC驱动类、URL、用户名和密码等信息,或数据源未配置事务管理器,也可能导致Spring事务失效。

  5. 事务拦截器配置错误:未正确配置事务拦截器,如未指定切入点或指定了错误的切入点。

  6. 事务超时配置错误:如果事务超时时间设置得太短,有可能在事务执行过程中出现超时,导致Spring事务失效。

  7. 方法调用错误:在代码中调用事务方法时,如果没有正确使用@Transactional注解或使用了错误的@Transactional注解,也可能导致Spring事务失效。

  8. 异常处理不当:注解中可以指定处理异常的类型,如果未指明或业务方法内自己try-catch异常,可能导致事务不能正常回滚。

  9. 数据库引擎不支持事务:如使用MyISAM引擎的数据库就不支持事务,这也会导致Spring事务失效。

这些原因可能导致Spring事务在应用中失效,需要针对具体情况进行排查和修复。

2、如何解决 POST 请求中文乱码问题,GET的又如何处理呢?

POST请求中文乱码

  1. 设置请求编码

    • 客户端(如HTML表单)发送POST请求时,需要确保表单的enctype设置为application/x-www-form-urlencoded(默认值),并设置accept-charsetUTF-8(或其他所需编码)。

    • 如果使用Java的Servlet接收POST请求,可以在doPost方法中,通过request.setCharacterEncoding("UTF-8")来设置请求体的编码。

  2. 后端处理

    • 后端在处理POST请求的参数时,应该使用与请求编码相同的编码来解析参数。

GET请求中文乱码

  1. URL编码

    • GET请求的参数直接附加在URL上,因此需要对参数进行URL编码。可以使用URLEncoder.encode(String s, String enc)方法将中文参数进行编码。

    • 在Java中,发送GET请求时,需要对URL中的参数部分进行编码。

  2. 后端处理

    • 后端在接收GET请求的参数时,通常不需要手动设置编码,因为参数已经过URL编码。但在解析参数时,如果后端框架或库支持,可以指定解码的字符集(如UTF-8)。

  3. 注意

    • 对于URL中的特殊字符(如?&=等),在URL编码时会被转换为对应的编码(如%3F%26%3D等),以确保URL的正确性和可读性。

总结

  • 对于POST请求,主要在客户端设置表单编码,并在后端设置请求体编码。

  • 对于GET请求,主要对URL中的参数进行URL编码,后端在解析参数时通常不需要额外设置编码。

确保整个请求-响应过程中使用的编码一致,是避免中文乱码的关键

3、请描述 Spring MVC 的主要组件?

Spring MVC的主要组件包括:

  1. DispatcherServlet:前端控制器,负责接收请求、调用其他组件处理请求并响应结果,相当于转发器和中央控制器。

  2. HandlerMapping:处理器映射器,根据请求的URL查找对应的处理器(Handler)。

  3. HandlerAdapter:处理器适配器,负责按照特定规则去执行Handler。在编写Handler时,需要按照HandlerAdapter的要求进行,以确保适配器能正确执行Handler。

  4. Handler(或Controller):控制器,接收处理前端控制器分发的请求,并将执行结果(ModelAndView)返回给前端控制器。

  5. ViewResolver:视图解析器,根据逻辑视图名解析出真正的视图(View)。

  6. View:视图,是一个接口,支持不同的视图类型(如JSP、Freemarker、PDF、Excel等),需要程序员根据业务需求进行开发。

这些组件共同协作,实现了Spring MVC的请求处理流程。

4、SpringMvc里面拦截器是怎么写的?

在Spring MVC中,编写拦截器通常涉及以下几个步骤:

  1. 实现HandlerInterceptor接口:

    你需要创建一个类并实现HandlerInterceptor接口。这个接口包含三个方法:

    • preHandle: 请求处理之前被调用。如果返回false,则请求会被拦截,不会继续执行后续操作。

    • postHandle: 请求处理之后,但在视图渲染之前被调用。

    • afterCompletion: 整个请求完成后被调用,即在视图渲染之后。

public class MyInterceptor implements HandlerInterceptor {  
    @Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
        // 逻辑处理,比如权限校验  
        return true; // 如果返回false,则请求被拦截  
    }  
  
    @Override  
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {  
        // 逻辑处理,比如日志记录  
    }  
  
    @Override  
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {  
        // 逻辑处理,比如资源清理  
    }  
}
  1. 配置拦截器: 接下来,你需要在Spring MVC的配置中注册你的拦截器。这可以通过Java配置或XML配置来完成。

Java配置(使用@ConfigurationWebMvcConfigurer):

@Configuration  
@EnableWebMvc  
public class WebConfig implements WebMvcConfigurer {  
  
    @Override  
    public void addInterceptors(InterceptorRegistry registry) {  
        registry.addInterceptor(new MyInterceptor())  
                .addPathPatterns("/**") // 拦截所有请求  
                .excludePathPatterns("/login", "/error"); // 排除某些请求  
    }  
  
    // 其他配置...  
}

XML配置(在spring-mvc-servlet.xml中):

<mvc:interceptors>  
    <mvc:interceptor>  
        <mvc:mapping path="/**" /> <!-- 拦截所有请求 -->  
        <mvc:exclude-mapping path="/login" /> <!-- 排除某些请求 -->  
        <mvc:exclude-mapping path="/error" />  
        <bean class="com.example.MyInterceptor" />  
    </mvc:interceptor>  
</mvc:interceptors>

注意:在实际应用中,通常会将拦截器定义为Spring Bean,并使用@Autowired或XML配置注入依赖。

  1. 定义拦截路径: 通过addPathPatterns方法定义哪些请求路径需要被拦截,通过excludePathPatterns方法定义哪些请求路径需要被排除。

  2. 使用拦截器: 配置完成后,Spring MVC会自动在请求处理过程中调用拦截器的方法。你可以根据需要在preHandlepostHandleafterCompletion方法中编写逻辑。

并发编程

1.Synchronized 用过吗,其原理是什么 ?

synchronized是Java中用来保证线程安全的一个关键字。你可以把它想象成一个“锁”或者一个“看门人”。当一个线程(我们称它为线程A)进入一个被synchronized修饰的方法或代码块时,这个“看门人”就会把这个“门”锁上,防止其他线程(比如线程B)进入。只有当线程A完成了这个方法或代码块里的所有操作,并且离开了这个区域后,“看门人”才会把“门”打开,让其他线程有机会进入。

这样做的好处是,可以确保同一时间只有一个线程在执行某个特定的代码段,从而避免了多线程环境下可能出现的混乱和错误。

简单来说,synchronized就是Java提供的一个工具,用来保证多线程环境下的代码安全执行。它像一个“看门人”,确保同一时间只有一个线程能进入某个“房间”(即被synchronized修饰的代码段)。

2.为什么说 Synchronized 是非公平锁 ?

Synchronized 被视为非公平锁,主要是因为当多个线程竞争同一个锁时,它不会按照线程请求锁的顺序来分配锁。换句话说,它不会让等待时间最长的线程先获得锁,而是让任何一个等待中的线程都有机会去尝试获取锁。这种“抢占式”的方式就是非公平锁的特性。

用口语化来说,Synchronized 就像是一个不讲“先来后到”的锁。当多个线程都在等待进入某个被 Synchronized 保护的区域时,它不会按照排队的顺序让线程一个个进,而是让“抢得最快”的线程先进入。所以,它就被称为非公平锁了。

3.为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性 ?

1.Synchronized 被视为悲观锁,是因为它总是假设最坏的情况——即每次数据操作都可能会有冲突,所以它会在一开始就“悲观”地加锁,确保数据在操作过程中不会被其他线程修改。这就像每次出门都担心会下雨,所以每次都带伞一样。

2.乐观锁则正好相反,它总是假设最好的情况——即数据在操作过程中不会发生冲突。因此,它不会一开始就加锁,而是在数据更新时进行检查。如果发现数据在操作过程中被其他线程修改了(即发生了冲突),它就会采取一些补偿措施,比如重新尝试更新数据。这就像每次出门都不带伞,但如果下雨了就找个地方躲雨或者买把伞。

3、CAS(Compare-and-Swap)是乐观锁实现的一种关键技术。简单来说,CAS 就是“比较并交换”的意思。它包含三个参数:内存地址 V、旧的预期值 A 和新值 B。如果内存地址 V 的值等于旧的预期值 A,那么就将内存地址 V 的值更新为新值 B。这个过程是原子的,即要么全部完成,要么全部不做。CAS 的主要特性包括:

  1. 原子性:CAS 是一条 CPU 指令,具有原子性,即在执行过程中不会被其他线程打断。

  2. 非阻塞性:CAS 是一种无锁算法,它不会让线程在等待锁的过程中阻塞,而是直接尝试进行操作。

  3. 高并发:由于 CAS 的非阻塞性,它可以支持较高的并发量。

4.请尽可能详尽地对比下 Synchronized 和 ReentrantLock 的异同。

相同点

  • 目的:它们都是为了解决多线程并发访问共享资源时可能出现的问题,保证同一时间只有一个线程能访问某个资源。

  • 可重入:都支持一个线程多次获得同一把锁,不会出现自己把自己锁死的情况。

不同点

  1. 实现方式

    • Synchronized:是Java的内置关键字,使用起来比较简单,不需要额外创建锁对象。

    • ReentrantLock:是JDK提供的API层面的锁,需要手动创建锁对象,并调用lock()unlock()方法来实现加锁和解锁。

  2. 公平性

    • Synchronized:它不管线程等待的顺序,哪个线程抢到了就是哪个线程的,属于非公平锁。

    • ReentrantLock:可以设置为公平锁,让等待时间最长的线程优先获得锁;也可以设置为非公平锁,和Synchronized一样抢锁。

  3. 锁的获取与释放

    • Synchronized:自动获取和释放锁,使用起来比较方便,但不够灵活。

    • ReentrantLock:需要手动获取和释放锁,虽然麻烦一些,但提供了更大的灵活性。

  4. 功能丰富性

    • ReentrantLockSynchronized提供了更多的功能,比如可以支持中断获取锁、尝试获取锁(如果获取不到就立即返回)、绑定多个条件等。

  5. 性能

    • 在早期版本的Java中,ReentrantLock的性能通常优于Synchronized。但随着Java版本的更新,Synchronized也经过了优化,两者在性能上的差异已经不那么明显了。

总的来说,SynchronizedReentrantLock各有优缺点,具体使用哪种同步机制取决于你的需求和场景。如果你需要更简单的同步方式,可以使用Synchronized;如果你需要更精细的控制和更多的功能,可以使用ReentrantLock

5.谈谈 ReadWriteLock 和 StampedLock。

ReadWriteLock(读写锁)和StampedLock(邮戳锁)在Java中都是用于处理多线程访问共享资源的锁机制,但它们各有特点和使用场景。

ReadWriteLock(读写锁)

  • 特点:读写锁将共享资源的访问权限分为两种:读和写。多个线程可以同时读取资源,但写操作是独占的,即同一时间只有一个线程可以写。

  • 适用场景:当读取操作远多于写入操作时,读写锁能显著提高并发性能。因为多个读线程可以并行工作,而不需要等待其他线程完成写操作。

  • 使用:它是一个接口,具体实现如ReentrantReadWriteLock。你需要显式地获取读锁或写锁,并在操作完成后释放它们。

StampedLock(邮戳锁)

  • 特点:StampedLock是ReadWriteLock的一个改进版本,它引入了乐观读策略,以进一步提高读操作的性能。乐观读意味着读操作不会阻塞写操作,除非写操作实际修改了数据。

  • 适用场景:在高并发读、低并发写且数据一致性要求不是非常严格的场景下,StampedLock通常能提供更好的性能。

  • 使用:StampedLock有三种模式:写模式、悲观读模式和乐观读模式。你需要通过调用相应的方法来获取和释放锁,并处理可能的冲突。

  • 注意事项:StampedLock是非重入的,这意味着同一个线程在持有锁的情况下不能再次获取该锁,否则会导致死锁。因此,在使用时需要格外小心。

总结

ReadWriteLock和StampedLock都是用于处理多线程访问共享资源的工具,但它们的实现和适用场景有所不同。ReadWriteLock提供了基本的读写锁机制,而StampedLock则通过引入乐观读策略来进一步提高性能。在选择使用哪种锁时,需要根据具体的业务场景和需求来权衡。

6.如何让 Java 的线程彼此同步?你了解过哪些同步器?请分别介绍下。

在Java中,线程同步主要是为了确保多个线程在访问共享资源时不会发生冲突,从而保持数据的一致性和完整性。以下是几种常用的Java线程同步器及其简短的介绍:

  1. synchronized关键字

    • 这是一个内置的关键字,用于实现线程同步。

    • 当一个线程进入一个synchronized方法或代码块时,会获取一个锁,其他线程必须等待该锁被释放后才能进入。

    • 使用简单,但功能相对基础。

  2. ReentrantLock

    • 是Java的java.util.concurrent.locks包中的一个类,提供了比synchronized更灵活的锁定机制。

    • 支持公平锁和非公平锁,可以尝试获取锁、定时获取锁、中断获取锁等。

    • 需要显式地创建锁对象,并调用lock()unlock()方法进行加锁和解锁。

  3. Semaphore(信号量)

    • 是一种计数信号量,可以控制同时访问共享资源的线程数量。

    • 维护一个许可证计数,线程可以获取和释放这些许可证。当许可证数量为零时,线程需要等待,直到其他线程释放许可证。

    • 适用于需要限制并发访问资源的场景。

  4. CountDownLatch

    • 是一个同步辅助类,允许一个或多个线程等待其他线程完成操作。

    • 通常用于并行计算中,当所有线程完成某个任务后一起开始下一个任务。

    • 可以通过调用countDown()方法来减少计数器的值,当计数器值到达零时,等待的线程将被唤醒。

  5. ReadWriteLock(读写锁)

    • 管理一组锁,包括一个读锁和一个写锁。

    • 允许多个线程同时读取共享数据,但写操作是独占的。

    • 适用于读多写少的场景,可以提高并发性能。

  6. StampedLock(邮戳锁)

    • 是ReadWriteLock的一个改进版本,引入了乐观读策略。

    • 在高并发读、低并发写且数据一致性要求不是非常严格的场景下,通常能提供更好的性能。

    • 使用时需要小心处理锁的获取和释放,以避免死锁等问题。

7.线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?

线程池中的线程创建过程通常不是一开始就随着线程池的启动就创建好的,而是在需要时才动态创建。不过,核心线程并不会在线程池创建时立即被创建,但可以通过调用prestartCoreThread()prestartAllCoreThreads()方法来为线程池初始化核心线程。

Java提供了几种线程池的创建方式,如newCachedThreadPoolnewFixedThreadPoolnewScheduledThreadPoolnewSingleThreadExecutor等,它们分别用于创建不同类型的线程池。这些线程池在创建时不会立即创建所有线程,而是根据任务的提交和线程池的配置来动态地创建和管理线程。

8.提到可以通过配置不同参数创建出不同的线程池,那么 Java 中默认实现好的线程池又有哪些呢?请比较它们的异同。

Java中默认实现好的线程池主要有四种,它们分别是:

  1. FixedThreadPool(固定线程池)

    • 特点:线程数量固定,核心线程数和最大线程数相等。

    • 适用场景:适用于任务量比较稳定,对系统资源消耗可控的场景。

    • 异同:线程数量固定,不会因为任务量增加而增加线程数。

  2. CachedThreadPool(可缓存线程池)

    • 特点:核心线程数为0,最大线程数无限大(实际上受限于JVM的最大线程数),当线程空闲超过指定时间后将被终止。

    • 适用场景:适用于执行大量短期异步任务的小程序,或者是负载较轻的服务器。

    • 异同:线程数量动态变化,根据任务量动态调整线程数。

  3. SingleThreadExecutor(单线程线程池)

    • 特点:只有一个线程,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)一个接一个地执行。

    • 适用场景:适用于需要保证任务按照顺序执行,或者任务之间有依赖关系,或者需要控制并发量的场景。

    • 异同:只有一个线程,任务按照顺序执行。

  4. ScheduledThreadPool(定时线程池)

    • 特点:支持定时和周期性地执行任务。

    • 适用场景:适用于需要定时或者周期性地执行任务的场景,如定时发送邮件、定时检查系统状态等。

    • 异同:除了可以执行普通任务外,还支持定时和周期性任务。

这些线程池各有特点,适用于不同的场景。在选择线程池时,需要根据实际任务的特点和需求来选择合适的线程池类型。

9.如何在 Java 线程池中提交线程 ?

在 Java 线程池中提交任务通常通过执行器的 execute() 方法或 submit() 方法来完成。这两个方法的主要区别在于 submit() 方法返回一个 Future 对象,可以用于获取任务的结果或检查任务是否完成。

使用 execute() 方法提交任务

ExecutorService executor = Executors.newFixedThreadPool(10); // 创建一个固定大小的线程池  
Runnable task = () -> {  
    // 任务的代码  
    System.out.println("Running task in thread: " + Thread.currentThread().getName());  
};  
executor.execute(task); // 提交任务  
// 等待所有任务完成(如果有必要的话)  
// executor.shutdown(); // 关闭线程池,不再接受新任务  
// executor.awaitTermination(timeout, unit); // 等待已提交的任务完成

使用 submit() 方法提交任务

ExecutorService executor = Executors.newFixedThreadPool(10); // 创建一个固定大小的线程池  
Callable<String> task = () -> {  
    // 任务的代码  
    return "Task result";  
};  
Future<String> future = executor.submit(task); // 提交任务并获取 Future 对象  
try {  
    String result = future.get(); // 获取任务结果,如果任务尚未完成,则阻塞直到完成  
    System.out.println("Task result: " + result);  
} catch (InterruptedException | ExecutionException e) {  
    // 处理异常  
    e.printStackTrace();  
}  
// 关闭线程池等操作与上例相同

请注意,当不再需要线程池时,应该调用 executor.shutdown() 方法来优雅地关闭它。这样可以确保已经提交的任务都能被执行完毕,而新的任务则不会被接受。如果需要等待所有任务都执行完毕,可以调用 executor.awaitTermination(timeout, unit) 方法。其中,timeout 是等待的时间,unit 是时间单位。

另外,Callable 接口与 Runnable 接口类似,但 Callable 可以返回结果,并且可以抛出异常。因此,当需要任务返回结果时,应该使用 Callable 接口和 submit() 方法。

10.请谈谈 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?

volatile关键字在Java中主要用于确保多线程环境中变量的可见性和有序性。以下是它的主要特点:

  1. 可见性:当一个线程修改了一个被volatile修饰的变量的值,新值会立即同步到主内存,并且其他线程会立即看到这个修改。这是因为volatile会禁止指令重排序,并确保每次读写操作都直接从主内存中读取或写入,而不是从线程的本地缓存中读取或写入。这确保了共享变量在所有线程之间的可见性。

  2. 有序性volatile关键字可以确保被修饰的变量的临界区代码的执行是有顺序的,即禁止指令重排序。这有助于防止在多线程环境中由于指令重排序导致的并发问题。

  3. 受限的原子性:虽然对于单个volatile变量的读/写操作具有原子性,但复合操作(如i++)并不具有原子性。这意味着在多线程环境中,对volatile变量的复合操作仍需要使用同步机制(如synchronized)来保证线程安全。

volatile能保证变量对所有线程的可见性的原因是,它强制每次读写操作都直接从主内存中读取或写入,而不是从线程的本地缓存中读取或写入。这样,当一个线程修改了volatile变量的值后,其他线程会立即从主内存中读取到这个新值,从而保证了变量的可见性。

11.请对比下 volatile 对比 Synchronized 的异同。

volatile和synchronized在Java中都是用于多线程同步的机制,但它们在实现方式和用途上有所不同。

相同点:

  • 两者都用于确保多线程环境下数据的正确性和一致性。

不同点:

volatile:

  • 是一个轻量级的同步机制,只能用于修饰变量。

  • 主要作用是保证变量的可见性和禁止指令重排序,但不保证原子性。

  • 在多线程访问volatile变量时不会发生阻塞。

synchronized:

  • 是一个重量级的同步机制,可以修饰方法或代码块。

  • 具有原子性、可见性、有序性和可重入性。

  • 在多线程访问被synchronized修饰的代码块或方法时可能会发生阻塞。

总结:

  • volatile适用于对变量的简单读写操作,而synchronized适用于对代码块或方法的复杂操作。

  • volatile的性能通常比synchronized要好,但在需要保证原子性时,synchronized是更好的选择。

  • volatile主要解决的是变量在多个线程之间的可见性问题,而synchronized解决的是多个线程之间访问公共资源的同步性问题。

12.请谈谈 ThreadLocal 是怎么解决并发安全的?

ThreadLocal通过为每个线程提供其自己的变量副本的方式来解决并发安全。每个线程都拥有自己独立的ThreadLocalMap(ThreadLocal的内部容器),其中存储着该线程对应的ThreadLocal变量值。当线程访问某个ThreadLocal变量时,它实际上是访问自己ThreadLocalMap中的那个变量,而不是访问共享变量。因此,不同线程之间不会相互干扰,从而实现了线程之间的数据隔离和并发安全。

简而言之,ThreadLocal为每个线程提供了独立的数据副本,避免了多线程对共享数据的并发访问,从而保证了并发安全。

JVM

1.Java 类加载过程?

Java类加载过程主要包括以下几个步骤:

  1. 加载(Loading)

    • 通过类的全限定名获取定义此类的二进制字节流。

    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

  2. 链接(Linking)

    • 验证(Verification):确保被加载的类的正确性和安全性。包括文件格式验证、元数据验证、字节码验证和符号引用验证。

    • 准备(Preparation):为类的静态变量分配内存,并将其初始化为默认值(零值或null)。

    • 解析(Resolution):把类中的符号引用转换为直接引用。

  3. 初始化(Initialization)

    • 为类的静态变量赋予正确的初始值(这些值是在Java代码中指定的)。

    • 执行类构造器<clinit>()方法。

在Java中,类加载器(ClassLoader)负责加载类的任务。主要有三种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader):主要负责加载Java的核心类库,如java.lang包下的类。

  2. 扩展类加载器(Extension ClassLoader):负责加载Java的扩展类库,一般对应JAVA_HOME/lib/ext目录中的JAR类包。

  3. 系统类加载器(System ClassLoader):也称为应用类加载器(Application ClassLoader),它负责加载应用程序的类路径(CLASSPATH)下的所有类库。

这些类加载器按照层次关系进行组织,并且每个类加载器都有自己独立的命名空间,保证了不同类加载器之间的隔离性。当Java虚拟机要加载一个类时,会按照父委托机制(Parent Delegation Model)进行加载,即先让父类加载器尝试加载该类,如果父类加载器无法加载该类,那么再由子类加载器尝试加载。这样可以确保Java核心类库的安全性,因为用户自定义的类加载器无法加载Java核心类库中的类。

2.描述-下 JVM 加载 Class 文件的原理机制?

JVM加载Class文件的原理机制主要包括以下几个步骤:

  1. 加载(Loading):

    • 通过类加载器将Class文件的二进制数据读入内存。

    • 在方法区创建对应的Class数据结构。

    • 在堆中生成一个代表该类的java.lang.Class对象。

  2. 链接(Linking):

    • 验证(Verification):确保Class文件符合JVM规范,不会危害JVM安全。

    • 准备(Preparation):为类的静态变量分配内存并设置默认初始值(如0或null)。

    • 解析(Resolution):将常量池中的符号引用替换为直接引用。

  3. 初始化(Initialization):

    • 如果类中有初始化语句(如静态变量赋值、静态代码块),则执行这些语句来初始化类的状态。

类加载器按照双亲委派模型(Parent Delegation Model)进行类加载,即先让父类加载器尝试加载该类,如果父类加载器无法加载,再由子类加载器尝试加载。这样可以确保Java核心类库的安全性和一致性。

整个加载过程中,加载、验证、准备和初始化这四个阶段的顺序是确定的,而解析阶段则可以在初始化阶段之后开始,以支持Java的动态绑定特性。

3.什么是类加器,类加器有哪些?

类加载器(ClassLoader)是Java虚拟机(JVM)的一部分,负责将类的字节码加载到内存中,并将其转换为可执行的Java对象

类加载器主要有以下几种类型:

  1. 启动类加载器(Bootstrap ClassLoader):也称为根类加载器,负责加载Java虚拟机的核心类库,如java.lang.Object等。这是虚拟机自身的类加载器,由C/C++实现,因此在Java中无法直接获取到。

  2. 扩展类加载器(Extension ClassLoader):用来加载Java的扩展库,如$JAVA_HOME/lib/ext目录下的jar包。这个类加载器由Java编写,是java.lang.ClassLoader的直接子类。

  3. 应用程序类加载器(Application ClassLoader):也称为系统类加载器,负责加载应用程序classpath下的类库。这个类加载器同样由Java编写,是java.lang.ClassLoader的直接子类。

  4. 自定义类加载器(Custom ClassLoader):根据需求自定义的类加载器,继承自java.lang.ClassLoader或其子类。

这些类加载器按照层次关系进行组织,并且每个类加载器都有自己独立的命名空间,保证了不同类加载器之间的隔离性。

4.简述 Java 垃圾回收机制。

Java垃圾回收机制是Java内存管理的重要部分,它自动释放不再使用的对象所占用的内存,以便让系统能够重新利用这些内存。以下是垃圾回收机制的简写概述:

  1. 标记-清除(Mark-Sweep):

    • 标记:从根对象(如静态变量、栈中的引用)开始,递归地访问对象的成员变量,并标记所有可达对象。

    • 清除:遍历整个堆内存,回收未被标记(即不可达)的对象所占用的内存空间。

  2. 复制(Copying):

    • 将内存划分为两个大小相等的区域,每次只使用其中一个区域。

    • 当该区域内存用尽时,将存活的对象复制到另一个区域,然后一次性清理掉原区域。

    • 这种方式适用于对象存活率较低的场景。

  3. 标记-整理(Mark-Compact):

    • 标记阶段与标记-清除算法相同。

    • 在清除阶段,将所有存活的对象移动到一端,然后直接清理掉边界以外的内存。

  4. 分代收集(Generational Collection):

    • 根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

    • 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

    • 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。

  5. 引用类型:

    • Java中的引用类型分为强引用、软引用、弱引用和虚引用四种,这四种引用类型直接影响了垃圾回收的行为。

  6. Stop-The-World:

    • 在进行垃圾回收时,Java会暂停所有的工作线程(Stop-The-World),待垃圾收集器完成内存回收后再恢复这些线程的工作。

  7. 垃圾回收器:

    • JVM提供了多种垃圾回收器,如Serial、ParNew、Parallel Scavenge、CMS、G1等,每种垃圾回收器都有其特定的应用场景和优缺点。

注意:Java程序员通常不需要(也不应该)直接控制垃圾回收,因为JVM会自动管理内存。但在某些情况下,可以通过调整JVM参数或使用特定的类(如WeakReferenceSoftReference等)来影响垃圾回收的行为。

5.如何判断一个对象是否存活?(或者 GC 对象的判定方法)

在Java中,判断一个对象是否存活(即是否应该被垃圾回收器回收)主要依赖于对象的可达性。GC(垃圾回收器)对象的判定方法主要基于以下两种算法:

  1. 引用计数法(Reference Counting)

    • 原理:为每个对象维护一个引用计数,当对象被引用时计数加一,引用被释放时计数减一。当引用计数为0时,对象就不可达,可以被回收。

    • 缺点:存在循环引用问题,即两个对象相互引用,但都不再被其他对象引用,导致它们无法被回收。

    注意:Java并没有使用引用计数法作为主要的垃圾回收判定方法,因为它无法解决循环引用问题。

  2. 可达性分析(Reachability Analysis)

    • 原理:从一组根对象(GC Roots)开始,递归地搜索所有可达的对象。搜索所经过的路径称为引用链,如果某个对象到GC Roots没有引用链相连,则该对象不可达,可以被回收。

    • GC Roots:通常包括虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(即一般说的Native方法)引用的对象。

    标记-清除(Mark-Sweep)算法

    • 标记阶段:从GC Roots开始,递归地访问对象的成员变量,并标记所有可达对象。

    • 清除阶段:遍历整个堆内存,回收未被标记(即不可达)的对象所占用的内存空间。

在可达性分析的基础上,JVM通常还采用一些辅助技术来优化垃圾回收过程,如分代收集、标记-整理(Mark-Compact)算法等。这些技术根据对象的存活周期和内存使用情况,选择最合适的垃圾回收策略。

6.垃圾回收的优点和原理。并者虑2种回收机制。

垃圾回收的优点

  1. 简化程序员工作:自动管理内存,减轻程序员手动管理内存的工作量,使程序员更专注于编写业务逻辑。

  2. 减少内存泄漏:自动回收不再使用的内存,避免内存泄漏问题,降低程序崩溃的风险。

  3. 提高程序可靠性:减少因内存管理错误导致的程序崩溃,提高程序的稳定性和可靠性。

  4. 改善程序性能:虽然垃圾回收本身会带来一定的性能开销,但通过避免内存泄漏等问题,总体上可以改善程序的性能。

垃圾回收的原理

基于自动内存管理机制,当程序运行时,系统会自动检测哪些内存已经不再使用,并回收这些内存,以便释放出更多的内存空间供程序使用。

两种回收机制

  1. 标记-清除(Mark-Sweep):

    • 标记阶段:从根对象开始,递归地访问对象的成员变量,并标记所有可达对象。

    • 清除阶段:遍历整个堆内存,回收未被标记(即不可达)的对象所占用的内存空间。

    • 缺点:可能产生内存碎片。

  2. 引用计数(Reference Counting):

    • 原理:为每个对象维护一个引用计数,当对象被引用时计数加一,引用被释放时计数减一。当引用计数为0时,对象就不可达,可以被回收。

    • 缺点:存在循环引用问题,即两个对象相互引用但都不再被其他对象引用时,它们无法被回收。Java并没有采用这种机制作为主要的垃圾回收方法。

7.垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收 ?

垃圾回收器的基本原理是通过自动检测并清除程序中不再使用的内存对象来管理内存。它主要基于可达性分析,从根对象(如全局变量、活动栈等)开始,遍历程序对象的引用链,将可达的对象标记为“存活”,然后清除未被标记的对象并释放其内存空间。

垃圾回收器不能马上回收内存。虽然可以通过调用System.gc()Runtime.getRuntime().gc()方法来建议虚拟机进行垃圾回收,但Java语言规范并不保证这些方法会立即触发垃圾回收。垃圾回收的具体时机和方式由JVM自主决定,取决于当前的内存使用情况和垃圾回收器的策略。

主动通知虚拟机进行垃圾回收的方法主要是使用System.gc()Runtime.getRuntime().gc()方法。但请注意,这只是向虚拟机发出一个建议,并不能保证立即执行垃圾回收。

8.Java 中会存在内存泄漏吗,请简单描述。

Java 中确实会存在内存泄漏。虽然 Java 提供了垃圾回收机制来自动管理内存,但在某些情况下,程序员仍然可能创建出内存泄漏的情况。

内存泄漏通常指的是程序中动态分配的内存由于某种原因无法被垃圾回收器回收,导致这部分内存始终被占用,直到程序结束。在 Java 中,内存泄漏通常是由于以下原因造成的:

  1. 静态集合类:当静态集合类如 HashMapLinkedList 等持有对象的引用,并且这些对象不再需要时,由于它们是静态的,垃圾回收器无法回收它们所占用的内存。

  2. 监听器、回调或内部类:如果某个对象被注册为监听器、回调或内部类,并且该对象持有对外部类的引用,那么即使外部类的实例不再需要,由于内部类/回调/监听器的存在,垃圾回收器也无法回收外部类实例所占用的内存。

  3. 数据库连接、文件句柄等:如果程序中打开了数据库连接、文件句柄等资源,但没有正确关闭它们,那么这些资源所占用的内存将无法被回收。

  4. 缓存:如果缓存中存储的对象不再需要,但由于缓存的清理策略不当或没有清理,这些对象所占用的内存将无法被回收。

  5. 线程:线程的生命周期比普通的对象长,如果线程中持有一些不再需要的对象的引用,那么这些对象所占用的内存将无法被回收,直到线程结束。

为了避免内存泄漏,程序员应该注意以下几点:

  • 避免在静态集合中引用对象。

  • 正确地管理监听器、回调和内部类,确保它们不再需要时能够被正确地清理。

  • 确保打开的资源(如数据库连接、文件句柄等)在使用完毕后能够被正确地关闭。

  • 设计合理的缓存清理策略,确保缓存中的对象在不再需要时能够被清理。

  • 合理地管理线程的生命周期,避免在线程中持有不再需要的对象的引用。

9.简述 Java 内存分配与回收策率以及 Minor GC 和 Major GC。

Java的内存分配与回收策略以及Minor GC和Major GC简述如下:

内存分配

  1. 对象优先在Eden区分配:新创建的对象首先尝试在新生代中的Eden区分配内存。

  2. 大对象直接进入老年代:如果对象占用内存较大(通常有一个阈值),会直接分配到老年代。

  3. 长期存活的对象进入老年代:在新生代中经过多次Minor GC后仍然存活的对象,会被移动到老年代。

内存回收

Java使用垃圾回收器自动管理内存,主要包括Minor GC和Major GC。

  1. Minor GC:发生在新生代中,特别是Eden区。当Eden区空间不足时,会触发Minor GC,清理不再使用的对象并回收其占用的内存。Minor GC通常执行得较快,因为新生代中对象的存活率较低。

  2. Major GC:发生在老年代中。当老年代空间不足时,会触发Major GC。Major GC的执行时间通常比Minor GC长,因为它需要整理老年代中的内存,并移动对象以减少碎片化。

需要注意的是,Minor GC和Major GC并不是完全独立的。在某些情况下,如老年代空间不足时,可能会先触发一次Minor GC来清理新生代中的对象,从而为老年代腾出更多空间。

此外,Full GC是对整个堆内存(包括年轻代和老年代)进行清理的一种垃圾回收操作,它是Major GC的一种特殊情况。Full GC的执行时间可能更长,因为它需要处理整个堆内存中的对象。

10.1标记-清除

标记-清除(Mark-Sweep) 是Java虚拟机(JVM)中垃圾回收算法的一种基础策略。该算法分为“标记”和“清除”两个阶段:

  1. 标记阶段(Mark Phase):

    • 从根对象(GC Roots)开始,递归地访问对象的成员变量,并标记所有可达的对象。

    • 标记过程结束后,所有可达对象都被“标记”了,而不可达对象则未被标记。

  2. 清除阶段(Sweep Phase):

    • 遍历整个堆内存,回收未被标记(即不可达)的对象所占用的内存空间。

    • 清除后,所有不可达对象的内存空间被释放,供后续内存分配使用。

标记-清除算法的缺点

  1. 效率问题:标记和清除两个阶段都需要遍历所有对象,效率较低。

  2. 空间碎片:清除阶段后,堆内存中可能产生不连续的内存碎片,导致大对象无法找到足够的连续空间进行分配,从而提前触发另一次垃圾收集。

为了解决这些问题,JVM还引入了其他更先进的垃圾回收算法,如标记-整理(Mark-Compact)算法,它在标记阶段结束后,将所有存活的对象移动到一端,然后直接清理掉边界以外的内存,从而解决空间碎片问题。

此外,复制(Copying)算法和分代收集(Generational Collection)策略也是JVM中常用的垃圾回收技术,它们分别根据对象的存活周期和内存使用情况,选择最合适的垃圾回收策略,以提高垃圾回收的效率和性能。

微服务(Spring Boot/Spring Cloud )

1.使用 Spring Cloud 有什么优势 ?

使用Spring Cloud的优势主要包括:

  1. 低耦合度:Spring Cloud的微服务架构使得每个服务都可以独立开发和部署,降低了模块之间的耦合度,提高了系统的可维护性和可扩展性。

  2. 降低开发成本:微服务架构允许开发团队并行开发不同的服务,减少了开发过程中的阻塞和等待时间,提高了开发效率,降低了开发成本。

  3. 支持跨平台开发:Spring Cloud的微服务架构可以使用不同的语言开发,这使得开发团队可以更加灵活地选择适合的技术栈,提高了开发的灵活性和效率。

  4. 灵活的数据库管理:每个微服务可以使用自己的数据库,也可以使用公共的数据库,这提供了更大的灵活性和可扩展性。

  5. 丰富的组件支持:Spring Cloud提供了丰富的组件支持,如服务发现、负载均衡、熔断器、分布式配置等,这些组件可以帮助开发者快速构建稳定、可靠的微服务应用。

  6. 易于集成:Spring Cloud可以与其他开源工具和云平台进行无缝集成,如Eureka、Consul、Zookeeper等服务注册与发现组件,Ribbon、Feign等负载均衡组件,Hystrix、Sentinel等断路器组件,以及Sleuth、Zipkin等分布式追踪组件。

  7. 提供一致的编程模型:Spring Cloud提供了一致的编程模型,使得开发人员可以通过简单的注解和配置,实现服务的注册与发现、服务的调用和负载均衡、断路器的配置和监控等功能,从而大大简化开发人员的工作,提高开发效率。

这些优势使得Spring Cloud成为构建微服务架构的热门选择之一。

2.服务注册和发现是什么意思?

服务注册和发现是构建分布式系统中的重要组成部分,主要帮助系统中的各个微服务相互发现和通信。

服务注册是指服务提供方在启动时,将自己的服务(包括服务的元数据信息,如服务名、IP地址、端口号等)注册到注册中心中,以便其他服务可以发现它。

服务发现是指客户端(即服务消费者)从注册中心中查找和选择可用的服务实例,并通过负载均衡策略来分配请求。也就是说,在分布式系统中,客户端可以通过注册中心快速地找到所需的已注册服务,并与之通信。

总的来说,服务注册和发现提供了一种机制,使得在微服务架构中,不同的服务可以相互发现和通信,从而实现了服务的解耦和可扩展性。

3.Spring Cloud 如何实现 ?

Spring Cloud通过服务注册中心来实现服务注册和发现。它提供了多种服务注册中心的实现,如Eureka、Consul和Zookeeper等。以下是Spring Cloud实现服务注册和发现的基本流程:

  1. 服务注册:服务提供者(如API、数据库服务等)在启动时,会将自己的服务信息(包括服务名称、IP地址、端口号等)注册到服务注册中心。注册中心会维护一个服务注册表,记录所有已注册的服务实例。

  2. 服务发现:服务消费者(如Web应用、移动应用等)在需要调用某个服务时,会通过服务注册中心查询该服务的可用实例信息。服务注册中心会根据负载均衡策略,返回一个或多个服务实例的地址给服务消费者。

Spring Cloud中,Eureka是最常用的服务注册中心之一。在项目中集成Eureka非常简单,只需要添加相应的依赖并配置Eureka服务器的地址即可。服务提供者启动时,会向Eureka服务器注册自己的信息;服务消费者则通过Eureka服务器获取服务提供者的信息,从而进行远程调用。Eureka还提供了服务健康检查机制,能够自动将不健康的服务实例从注册列表中移除。

此外,Consul和Zookeeper也是Spring Cloud支持的服务注册中心,可以根据项目需求选择使用。

4.什么是 Hystrix?它如何实现容错?

Hystrix是Netflix开源的一款针对分布式系统的延迟和容错库。它主要处理分布式系统中的服务间调用故障,通过以下功能实现容错:

  1. 断路器(Circuit Breaker):当服务调用失败时,Hystrix会开启断路器,暂停与服务器的通信,并记录这次失败。如果在一段时间内(如多次尝试)调用都失败,Hystrix会认为这个服务调用总是失败,并选择跳过这个服务调用。

  2. 隔离策略:通过线程池或信号量隔离策略,限制对某个依赖服务的并发请求数量,防止因单一依赖服务的故障导致整个系统资源耗尽。

  3. 熔断(Fallback):当断路器开启或请求失败时,Hystrix会提供一个降级操作(fallback)作为备选响应,确保系统在面临依赖服务故障时仍能提供基本服务。这个降级操作可以是静态数据、默认值,或是自定义逻辑生成的响应。

  4. 请求合并(Request Collapsing):Hystrix允许将多个请求合并为一个,并在所有请求都成功后才发送,这可以减少网络开销和资源占用。

  5. 实时监控和度量:Hystrix提供了实时监控和度量功能,可以对服务的执行情况进行监控和统计,包括错误率、响应时间、并发量等指标。通过监控数据,可以及时发现和解决服务故障或性能问题。

虽然Hystrix不再积极维护,但它依然是理解和学习微服务架构中服务治理理念的重要参考。

5.什么是 Netflix Feign?它的优点是什么 ?

Netflix Feign是一个声明式的Web服务客户端,它简化和优化了服务间的HTTP通信。以下是其主要的优点:

  1. 声明式API:Feign允许开发人员通过接口和注解定义服务间的API调用。这使得编写服务间的HTTP通信变得更加简单和优雅。

  2. 支持多种编解码器:Feign支持多种编解码器,如JSON、XML等,可以方便地处理不同数据格式的请求和响应。

  3. 负载均衡:Feign整合了Ribbon,具有负载均衡的能力,可以自动选择最优的服务实例进行调用。

  4. 熔断:Feign整合了Hystrix,具有熔断的能力,可以在服务调用失败时提供降级操作,确保系统的稳定性和可用性。

  5. 模板化调用:Feign类似于一种模板调用,所有的请求服务都存放在对应的FeignClient接口中,省去了大量冗余在配置文件中的URL配置项。

  6. 日志和监控:Feign支持日志和监控功能,可以直观地发现服务调用的过程及参数,便于排查问题。

这些优点使得Netflix Feign成为微服务架构中服务调用的理想选择之一。

6.Spring Boot 的核心配置文件有哪几个?它们的区别是什么?

Spring Boot 的核心配置文件主要有两个:application.propertiesapplication.yml(或 application.yaml)。它们的区别主要在于格式和可读性。

  1. application.properties:这是 Spring Boot 的默认配置文件,使用传统的键值对(key-value)格式。每个条目占一行,如 server.port=8080。这种格式简洁明了,但对于复杂的配置结构(如嵌套的键值对),可能会显得冗长和难以阅读。

  2. application.yml(或 application.yaml):这是一个 YAML 格式的配置文件,它使用缩进表示层级关系,更适合表示复杂的层次结构。YAML 格式的配置文件通常更易于阅读和维护,尤其是对于大型和复杂的配置。例如,你可以在 YAML 文件中方便地表示一个具有多个属性的对象或数组。

两者都可以用来配置应用程序的属性和环境,但 YAML 格式的配置文件在可读性和可维护性方面通常更有优势。因此,对于复杂的配置需求,建议使用 application.yml 文件。然而,如果你的配置需求相对简单,或者你的团队更熟悉 application.properties 格式,那么继续使用它也是可以的。

7.Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的 ?

Spring Boot 的核心注解是 @SpringBootApplication。这个注解主要由以下三个注解组成:

  1. @SpringBootConfiguration:这个注解是 @Configuration 注解的变体,用于标识该类是一个配置类,即该类可以用来定义 Bean。

  2. @EnableAutoConfiguration:这个注解允许 Spring Boot 自动装配,即根据当前类路径下的包或者类来配置 Spring Bean。

  3. @ComponentScan:这个注解用于启用组件扫描,自动扫描并加载符合条件的组件,包括 @Controller@Service@Repository 等。

在大多数情况下,我们只需要在 Spring Boot 的主类(通常是项目的入口类)上添加 @SpringBootApplication 注解,就可以启用 Spring Boot 的各项能力。这个注解包含了上述三个注解的功能,使得配置更加简洁和方便。

8.Spring Boot 中的监视器是什么?

在Spring Boot中,监视器(Monitor)通常指一种特殊的应用程序或组件,用于监控和管理整个应用程序的运行状态和性能。它提供了一种可视化的方式,可以实时跟踪和监视应用程序的各种指标,例如响应时间、CPU使用率、内存使用情况等。

Spring Boot提供了内置的监视器功能,其中最常用的是Spring Boot Actuator。Actuator是一个用于监控和管理应用程序的模块,提供了丰富的功能和端点(endpoints),可以帮助开发人员实时查看应用程序的运行状态、性能指标和健康状况,以及执行一些管理操作。这些端点包括但不限于查看应用程序的健康状态、信息、性能指标等。

除了Actuator之外,还有Spring Boot Admin这样的可视化管理控制台,它提供了一个用户界面来显示和管理多个Spring Boot应用程序的健康状态、详细信息、日志等。

总的来说,Spring Boot中的监视器提供了一种方便的方式来监控和管理应用程序的运行状态和性能,帮助开发人员及时发现潜在问题、优化性能和提高可靠性。

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值