Java项目遇到的问题

实际项目中,Java项目遇到的问题,包含MQ、SpringBoot、Redis、MySQL、Kafka 等…

MQ 相关

MQ 大量未订阅消息处理

  • 产生原因:消费端宕机、消费端消费能力不足、生产端流量过大。
  • 解决方案
    1. 加机器,如果是线上事故,尽可能多的申请服务器,尽量短时间将 MQ 消费掉。
    2. 在 MQ 配置中增加最大消费数量和每次从消息队列中读取的消费数量。
    3. 上线专门记录消息的队列,将其持久化到数据库中,后续慢慢处理。

若因为积压时间太久,导致消息丢失如何处理

  • 先找到丢失的消息:如果有备份,直接从备份里恢复;如果没有,则尝试从生产者的生产记录里找到,让生产者重新发送。
  • 分析问题出现原因
    1. 首先看消息是不是积压时间过长丢失的,如果是,延长这个时间。
    2. 然后看是不是队列空间打满了,如果是,扩大空间。
    3. 最后增强消费能力。
  • 后续优化
    1. 采用高可用架构,配置集群,保证一台服务器宕掉的时候,其他依然可用。
    2. 进行数据复制,防止单点丢失数据。

若积压太多,导致 MQ 满了如何处理

  • 直接写程序将新收到的 MQ 丢弃,在流量低峰期再找回来。

如果 MQ 和 MySQL 同时挂了,怎么处理

  • MQ 持久化,写数据库、写磁盘、写日志。恢复后从里面读取,重新发送。

RabbitMQ 的原理

  • 生成者发送消息之后并不会直接到消费者,而是会把消息发送到交换机(Exchange),在交换机中将其放在合适的队列(Queue),然后才是消费者进行消费。

消息丢失如何处理

  • 消息丢失分三种情况:生产者消息丢失、队列消息丢失、消费者消息丢失。
    • 生产者消息丢失:用 Confirm 模式,将生产者发送消息时,生成一个唯一 ID,在消费队列接收后返回一个 ACK(包含唯一
      ID),如果接收失败,就返回 NACK 重新发送即可。
    • 队列消息丢失:同样的方式,将消息做持久化,持久化完成后再返回 ACK。
    • 消费者消息丢失:一般是采用了自动确认功能,改为手动确认,在消费者消费完成后再返回成功。而不是接收后自动返回成功。

Redis 相关

缓存穿透、缓存击穿和缓存雪崩

  • 缓存穿透:大量不存在的查询读缓存读不到,直接去读取数据库。造成服务崩溃的情况。
    • 解决方案
      1. 在缓存中增加 value 为空的缓存,但这样会加大内存消耗,可以设置一个较短的过期时间。如果在缓存时间内在数据库中增加了这个
        key 对象,会造成缓存和实际不一致,可以加库后发 MQ 更新缓存。
      2. 如果实时性要求不高,可以采用布隆过滤器,它可以减少使用缓存空间。
  • 缓存击穿:若一个热点 key 的缓存失效了,此时新建缓存比较复杂,同时多个请求来新建该缓存,导致服务崩溃。
    • 解决方案
      1. 加锁排队,同一个 key 同时只有一个在新建。风险:如果新建时间较长,会有死锁风险,会导致吞吐量降低,但可以较好的保证数据一致性。
      2. 永不过期的缓存,缓存本身无物理过期时间,添加缓存逻辑过期时间,若超过则更新缓存,更新期间还读以前的缓存。更新后读新缓存。不会有死锁风险,但会有数据不一致的情况出现。
  • 缓存雪崩:若一个缓存一段时间内失效,此时有大量请求进来,所有请求落在服务器上,会引起缓存雪崩,服务宕机。
    • 解决方案
      1. Redis 二级缓存,设置过期时间不同。
      2. 分布式部署多个机房,提高缓存可用性。
      3. 数据预热,提前加载好缓存数据。
      4. 加锁排队,一个 key 同时只能有一个线程访问,其他排队等待。

Redis 的数据类型

  • String(字符串)、List(列表)、Set(集合)、Hash(哈希)、Zset(有序集合)。

Redis 的数据结构

  • SDS:简单动态字符串,对应数据类型中的 String。
    • Redis 是用 C 语言实现的,但在 C 语言里面的 char 类型字符串存在缺陷,所以 Redis 封装了一个新的简单动态字符串。
    • C 语言中 char 的缺陷:
      1. 字符数组结束位置会插入 \0,所以出现 \0 时 C 语言就会认为字符串已经结束了。导致字符串不能全量读取。这种限制导致
        C 语言的字符串只能读取文本,不能读取音频、视频等二进制数据。
      2. CHAR 类型字符串不会计算自身缓冲区大小,有缓冲区溢出的风险。
    • SDS 数据结构:len(字符串长度)、alloc(分配的空间长度)、flags(SDS 类型)、buf[](字节数组)。
    • 优势:
      1. C 语言中获取字符串长度需要遍历,时间复杂度为 O(N),而简单动态字符串可以直接获取,时间复杂度为 O(1)。
      2. 二进制安全,不需要 \0 来控制结尾,同时还支持了文本、视频、音频等的存储。
      3. 不会发生缓冲区溢出,有 alloc-len 来获取剩余空间大小,若缓冲区大小不够用,Redis 会自动扩张 SDS 空间(小于 1M
        的翻倍扩容,大于 1M 的按 1M 扩容)。
      4. 节省内存空间,Redis 共设计了五种类型(sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64)。
  • 双向链表:List 常用数据结构,但因链表不连续,无法很好利用 CPU 缓存且内存开销大,因此数据量较小时使用压缩列表,数据量大才会使用双向链表。
  • 压缩列表:List、Hash 和 Zset 数据量较少时使用,连续紧凑,但不能保存过多元素,否则查询效率低,查询第一个和最后一个字段的时间复杂度是
    O(1),但查询中间的数字需要遍历,时间复杂度为 O(N)。
    • 压缩列表在更新时会有连锁更新的现象(原因:列表节点的 prevlen 属性会根据前一个节点的长度进行不同的空间大小分配)。直接影响压缩列表的访问性能。
  • 哈希表:保存键值对(key-value)的数据结构。有可能存在 hash 冲突,Redis 采用链式 hash 来解决冲突(哈希冲突:当有两个或两个以上的
    key 分配到同一个哈希桶,称之为 hash 冲突)。
    • 链式哈希通过 next 指针,在发生 hash 冲突时,将其指向一个新的哈希桶。但这样会导致哈希长度增加,所以又采用了 rehash 来解决。
    • rehash 的触发条件:负载因子 = 哈希表已保存节点数量 / 哈希表大小。
    • 若负载因子大于等于 1,且 Redis 没有执行 RDB 快照和 AOF 重写的情况下,会进行 rehash 操作。
    • 负载因子大于等于 5 时,无论在做什么,都会强制进行 rehash 操作。
  • 跳表:只有在 Zset 的底层采用使用跳表,能支持 O(logN) 复杂度的节点查找。
  • 整数集合:Set 的底层实现之一、多层有序链表如果 Set 只包含数值,会使用整数集合。
  • Quicklist:双向链表 + 压缩列表。整体是链表形式,而每个元素又是一个压缩列表,控制压缩列表的大小和元素个数来规避连锁更新的风险。
  • Listpack:每个节点不再包含前一个节点的长度,新加元素不会影响其他节点,解决 Quicklist 问题。

Redis 的持久化

  • Redis 有两种持久化方式:RDB、AOF。
    • RDB 为默认持久化方式,保存路径在 redis.conf 配置文件下,会生成 dump.rdb。会在指定时间对数据进行持久化存储。
    • 如果手动开启持久化功能,会生成 AOF 持久化文件,用来记录对服务器的读写操作,当服务重启时会重新执行这些命令来恢复原始数据。
    • 因为 RDB 出现意外 down 掉的话,会丢失最后一次快照的所有内容,而 AOF 不会丢失超过 2 秒的数据,因此官方建议 RDB 和 AOF
      同时开启,两者同时开启,优先 AOF 作为持久化机制。

关于 Redlock

  • Redlock 是 Redis 官方提出的一种分布式锁算法,用于实现分布式系统里面的可靠锁机制。核心思想是通过多个独立的 Redis
    实例来确保锁的可靠性和一致性。
  • 背景:在分布式系统中,单点的 Redis 实现的锁可能存在以下问题:
    1. 单点故障:如果 Redis 实例宕机,锁就会失效。
    2. 时钟漂移:如果 Redis 实例的时钟不同步,可能导致锁的过期时间计算错误。
  • 核心思想:在多个独立的 Redis 实例上同时获取锁,只有大多数实例(N/2+1)成功获取锁,才算真正获取到锁。
  • 步骤
    1. 获取当前时间:记录获取锁的开始时间。
    2. 依次向多个 Redis 实例请求锁;(1. 使用相同的 key 和随机值作为锁的值,2. 设置锁的过期时间)。
    3. 计算获取锁的时间:如果获取锁的时间小于锁的过期时间,并且大多数实例(N/2+1)成功获取锁,则认为锁获取成功。
    4. 释放锁:向所有 Redis 实例发送删除锁的请求。
  • 优点:避免单点故障,部分实例宕机依然可用,且基于 Redis 实现,易于集成。
  • 缺点:需要在多个 Redis 实例上获取锁,增加了网络通信和计算开销。如果 Redis
    实例的时钟不同步,可能会导致锁的过期时间计算错误。而且可靠性存在争议,不能完全解决分布式锁的问题。
  • 使用场景:分布式任务调度:确保同一时间只有一个节点执行任务;资源竞争控制:防止多节点同时操作共享资源。
  • 替代方案:Zookeeper,Etcd 分布式锁,数据库分布式锁。

Etcd 分布式锁

  • 特性
    1. 租约:Etcd 里面的时间绑定机制,为键值对设置一个有效期,租约过期时,之前设置的键值对自动删除。
    2. 事务:支持事务操作,保证原子性。
    3. Watch 机制:监听某个键的变化,用来实现锁的等待和通知。
  • 实现方案
    1. 创建租约,设置过期时间。
    2. 客户端尝试通过事务写入一个键值对(锁的唯一标识),失败表示锁已经被其他人持有。
    3. 监听锁释放:如果获取失败,可以通过 Watch 机制,监听锁的释放。
    4. 释放锁:客户端删除键值对,或者租约过期来释放锁。

Redis 的过期策略和淘汰机制

  • 过期策略
    1. 惰性删除:访问时检查,并删除过期键。
    2. 定期删除:定期随机检查,并删除过期键。
  • 淘汰机制
    1. 不淘汰,默认策略。当有新的写入进来发现内存不足,直接报错。适用于不允许数据丢失的场景。
    2. 从设置了过期时间的键中,删除最近最少使用的。
    3. 从设置了过期时间的键中,删除最不经常使用的。
    4. 从设置了过期时间的键中,随机淘汰。
    5. 从设置了过期时间的键中,淘汰剩余时间最短的键。
    6. 从所有键中,淘汰最近最少使用的。
    7. 从所有键中,淘汰最不经常使用的。
    8. 从所有键中,随机淘汰。

ES 相关

  • ES 本身使用倒排索引。

Spring 相关

Spring Boot 常用组件

  • Starter:提供各种功能的依赖管理。
  • Web(spring-boot-starter-web):集成 Spring MVC 和 Tomcat。核心注解:@RestController,@Service 等。
  • JPA(spring-boot-starter-data-jpa):简化数据库操作。核心注解:@Entity @Query。
  • Test(spring-boot-starter-test):提供测试支持,核心注解:@Junit,@Test。

Spring Boot 启动过程中 URL 冲突,原理是什么,为什么会报错

  • Spring Boot 启动时会初始化上下文,加载所有的 Bean,在加载控制器时,Spring MVC 会加载所有映射,如果 URL 冲突,则直接报错。异常信息为:
    IllegalStateException(Ambiguous mapping. Cannot map 'xxxController' method ... to {xxx}: There is already 'yyyController' bean method ... mapped.)

Spring、Spring MVC、Spring Boot 的区别

  • Spring:是一个家族,里面包含了各种各样衍生产品,但他们都实现了 IOC(依赖注入)和 AOP(面向切面)。然后在这个基础上进行延伸,提供其他高级功能。Spring
    是一个引擎。
  • Spring MVC:是一个 Web 框架,提供轻耦合的方式开发 Web 应用,是 Spring 的一个模块,通过解决问题领域是网站应用程序或服务开发。是基于
    Spring 的 MVC 框架。
  • Spring Boot:实现了 Auto-configuration
    自动配置(另外三大神器:Starter(依赖)、CLI(命令行接口)、Auctutor(监控)),大幅度减少配置,几乎相当于开箱即用。是一套快速开发整合包。

Spring 的启动原理

  1. 开始启动。
  2. 构造一个 Spring 应用,进行模块的初始化,包括配置 source、配置是否为 Web
    环境、创建初始化构造器(获取工厂对象,生成工厂实例)、创建应用监听器(获取工厂对象,生成工厂实例)、配置应用的主方法所在类。
  3. 启动该应用:启动监听器模块、启动配置环境模块、启动应用上下文(创建上下文对象、基本属性配置、更新应用上下文:准备环境所需
    Bean 工厂和通过工厂产生环境所需的 Bean)。

Spring Boot 的启动类,使用 @SpringBootApplication 注解。

Spring Boot 比较重要的注解

  • @SpringBootConfiguration:继承 @Configuration,标识当前是注解类。
    • 按照原来 XML 配置文件的形式,Spring Boot 大多采用配置类来解决配置问题:
      1. XML 中进行配置,指向类路径。
      2. 通过 @Bean 可注入。任何一个标注了 @Bean 的方法,其返回值将作为 Bean 定义注册到 Spring 的 IOC 容器。方法名将默认成该
        Bean 定义的 ID。
  • @EnableAutoConfiguration:开启 Spring Boot 注解功能。
  • @ComponentScan:扫描路径设置。
    1. 对应 XML 配置中的元素。
    2. 自动扫描并加载符合条件的组件。
    3. 将 Bean 定义加载到 IOC 容器中。

BeanFactory 和 ApplicationContext 的区别

  • 两者都是接口,ApplicationContext 间接继承了 BeanFactory。
  • BeanFactory 是最底层接口,只提供了实例化对象和获取对象的功能。
  • ApplicationContext 是 Spring 的一个更高级容器,提供更多有用功能,包括:Bean 的详细信息,国际化,统一加载资源、事件机制和
    Web 应用的支持等等。
  • BeanFactory 采用延迟加载的形式来注入 Bean,ApplicationContext 相反,是在 IOC 启动时一次性创建所有
    Bean,这么好处是可以及时发现配置文件的错误,缺点是造成浪费。

BeanFactory 和 FactoryBean 的区别和联系

  • 两者都是接口
  • BeanFactory 是用来创建 Bean 和获取 Bean 的。
  • FactoryBean 跟普通 Bean 不同,其返回的对象不是指定类的一个实例,而是 FactoryBean 的 getObject 方法所返回的实例。
  • 通过 BeanFactory 和 beanName 获取 Bean 时,如果 beanName 不加 & 则获取到对应 Bean 的实例;如果 beanName 加上 &
    ,则获取到 FactoryBean 本身的实例。
  • FactoryBean 通常是用来创建比较复杂的 Bean(如创建 MyBatis 的 SqlSessionFactory 很复杂),一般的 Bean 直接用 XML
    配置即可,但如果创建一个 Bean 的创建过程中涉及到很多其他的 Bean 和复杂的逻辑,用 XML 配置比较困难,这时可以考虑用
    FactoryBean。

Spring 框架是如何解决 Bean 之间的循环依赖的

  • Spring 先用构造函数进行实例化,然后填充属性,再对其进行附加操作和其他初始化,正是这样的生命周期,才有了 Spring
    解决循环依赖。这样的解决机制是根据 Spring 框架内定义的三级缓存来实现的。
  • 三级缓存
    1. singletonObjects:第一级缓存,里面放置的是实例化好的单例对象。
    2. earlySingletonObjects:第二级缓存,里面存放的是提前曝光的单例对象。
    3. singletonFactories:第三级缓存,里面存放的是要被实例化的对象的对象工厂。
  • 所以当一个 Bean 调用构造函数进行实例化后,即使属性还未填充,就可以通过三级缓存向外暴露依赖的引用值(所以循环依赖问题的解决也是基于
    Java 的引用传递),这也说明了另外一点,基于构造函数的注入,如果有循环依赖,Spring 是不能够解决的。还要说明一点,Spring 默认的
    Bean Scope 是单例的,而三级缓存中都包含 singleton,可见是对于单例 Bean 之间的循环依赖的解决,Spring 是通过三级缓存来实现的。

Spring 的 @Import 注解的作用

  1. 将没有使用 @Component 注解的普通 class 加入到 Spring 容器,由 Spring 管理。
  2. 导入一个 Configuration 类(比如你想组合多个 Java Config 类到一个 Java Config 类,或者你引入的第三方 jar 包中的 Java
    Config 类没在你 Spring Boot 程序的子包下,即没有被扫描进 Spring 容器)。
  3. 通过实现了 ImportSelector 接口的类,导入多个 class 到 Spring 容器(Spring Boot 的自动装配
    @EnableAutoConfiguration)。
  4. 通过实现 ImportBeanDefinitionRegistrar 接口的方式(MyBatis 整合 Spring:MapperScannerRegistrar.java
    @MapperScan 注解。
  5. 使用@Import搭配@Configuration导入到Spring容器。

同一个类中调用 @Transaction注解的方法会有事务效果吗?

没有,可以Autowired注入自己,然后再调用注入的类中的方法,即自己依赖自己,循环依赖

springboot启动过程中如何加载META-INF/spring.factories

1
List<String> s = SpringFactoriesLoader.loadFactoryNames(type, classLoader);

IOC和AOP的原理和应用

  1. IOC本质就是依赖注入,在开发过程中,A如果需要引用的B,那就需要new B()
    ;但这样耦合度较高,如果B做了调整,那A也需要相应调整。在spring引入ioc之后,可以直接通过工厂模式加反射机制,将其托管给spring容器(常见的dto的构造器原理)。这样就降低了代码的耦合程度。

AOP本质是面向切面编程,除了spring中的应用之外,更多算是一种思想,将代码以切面的形式区分为核心切面和横切面。开发人员在开发过程中只需要考虑自己的核心业务逻辑,横切逻辑无需考虑在内。在spring中的应用就是将公用的逻辑提取出来,在代码执行时加载,如权限控制,日志管理,事务COMMIT等。这个思想在开发过程中也可以继续沿用。

SpringMVC的大致实现过程

  1. 用户发起request请求,如果有多个servlet,则通过servletmapping来指定具体的servlet。
  2. 通过servlet指定的url去需要指定的controller。
  3. 通过controller去调用modelandview,返回指定视图view。
  4. 将返回model组装返回给response。

Spring事务的传播级别

  1. spring的默认级别,判断上下文是否存在事务,若存在则加入当前事务,若不存在,则新建事务。
  2. 判断上下文是否存在事务,若不存在,则以非事务方式运行
  3. 判断上下文是否存在事务,若存在,则抛出异常
  4. 判断上下文是否存在事务,若不存在,则抛出异常
  5. 每次执行都会创建新事务,同事挂起上下文中的事务,执行完成后再恢复上下文中的事务,子事务的执行不影响父事务的执行和回滚
  6. 判断上下文中是否存在事务,若存在,则挂起,直到当前事务执行完成再恢复(降低事务大小,将非核心逻辑包裹执行)。
  7. 嵌套事务,若上下文中存在事务则嵌套执行,若没有,则新建事务。

Spring如何自定义注解

  1. @Target(ElementType.TYPE)注解,是说这个注解可以作用于类,接口或枚举类型;
  2. @Component 表示这个注解是spring组件注解,会被spring bean加载
  3. @Documented 表示这个注解会被放在javadoc中
  4. @Retention(RetentionPolicy.RUNTIME) 表示注解运行时保留,可以通过反射读取
  5. @interface 接口属性

除了eurka的其他注册中心?

  1. 市面主流的注册中心有很多,根据自身需要去选用即可
  2. eureka特点是易于集成到Spring colud生态,高可用,支持多节点复制。缺点是一致性较弱,功能也比较简单。
  3. zookeeper拥有强一致性,但配置比较复杂,同时写的性能也比较差
  4. consul支持服务发现和多数据中心,但配置也比较复杂,强一致性也会影响性能。
  5. Nacos,没有eurkea和consul来的成熟,
  6. etcd,拥有高可用,强一致性,但配置较为复杂,且功能也比较单一,主要用于键值存储

Spring Boot 和Spring Cloud的区别

  1. Spring Boot是快速开发和运行Spring的工具,适合单体应用和微服务里面的单个服务。
  2. Spring Cloud是构建分布式系统和微服务的工具集合,适合复杂的分布式场景。

Mysql相关

mybatis方言处理

  1. 定义mapper.xml文件,在里面加入方言支持(只需要列出必要字段)
  2. 因为mapper XML 文件不支持继承,(一个接口中的方法在XML文件中必须有实现-否则启动报错),所以采用 Mapper.java 接口类继承的方式

数据库事务特性(ACID原则):

  1. 原子性:一个事务多个操作,要么同时成功提交,要么同时失败回滚。
  2. 一致性:一个事务执行前后,必须从一个一致状态变成另一个一致状态,比如A和B一共有400元,两者无论互相转账几次,都应该是400元。
  3. 隔离性:一个事务内部操作和使用数据,对其他同时进行的事务,都是隔离的
  4. 持久性:数据库修改后应该是持久修改。后面就算数据库出现问题,数据恢复后,数据也不应该发生变化

数据库可能出现的问题:

  1. 脏读:事务A读取到了事务B尚未提交的数据
  2. 不可重复读:事务A第一次读取数据之后,事务B提交了数据,事务A第二次读取时候发现数据和第一次读取不一致。
  3. 幻读:事务A修改了表里面的所有数据但没有提交,这时事务B对这个表新增了一行,然后提交了,事务A提交之后发现有一行没有修改。这种现象叫幻读。若要解决,需要在修改前进行锁表

数据库隔离级别:

  1. 读未提交,
  2. 读已提交
  3. 可重复读
  4. 串行化

mysql的mvcc的并发控制的问题

概念:MVCC又叫多版本并发控制,提供访问数据库时,对事务内读取到的内存做处理,避免写操作阻塞读操作。

MVCC实现方式:

  • 更新时并非直接更新数据库,而是将旧的数据标记为过时的,新的数据以新增方式写入,后续通过垃圾回收机制回收旧的数据。
  • mysql和innodb使用另外一种,数据库只保存最新的数据,但会通过undo动态重构旧版本数据。

InnoDB的MVCC实现机制:通过在数据后面添加两个隐藏列来实现,分别是创建时间和删除时间,此处的时间不是时间戳,是版本号。每开启一个新事务,版本号递增。

mysql最左匹配原则的原理

原理:索引在查询时,会从联合索引的最左边开始查起。

比如索引为A\B\C.当查询AB或BA时,索引可以生效,查询A\AB\ABC时索引生效

mysql的InnoDB和MyISAM

  • 共同点:这两个都是mysql常见的存储引擎。
  • 区别:
  1. 事务支持:Innodb支持事务,适合需要高可靠性的应用;但myisam不支持,不能保证数据的一致性和完整性
  2. 锁定机制:Innodb采用行级锁,适合高并发场景,能有效减少冲突;myisam使用表级锁,并发写入时性能较差
  3. 外键支持:Innodb支持外键,确保数据完整性,但myisam不支持外键
  4. 崩溃恢复:Innodb具备崩溃回复能力,但myisam需要手动恢复
  5. 性能:Innodb在写操作时表现更好,适合频繁写入的场景;myisam适合读多写少的业务场景
  6. 存储结构:Innodb的数据与索引存储在同一个文件中(.ibd),支持表空间管理;myisam的数据和索引分开存储
  7. 全文索引:Innodb再5.6之后支持全文索引,但myisam一直都支持
  8. 压缩:Innodb支持表压缩,压缩后节省存储空间,但myisam压缩后为只读模式
  9. 适用场景:Innodb适合需要事务、高并发、数据完整性要求较高的系统,如电商系统;myisam适合读多写少、不需要事务的系统,如日志系统、数据仓库等
  10. ACID兼容:Innodb支持ACID;myisam不支持
  • mysql有innodb和myisam两种类型,其中innodb使用聚簇索引,myisam使用非聚簇索引:
  • 聚簇索引是对磁盘数据进行重新组织排列的算法,特点是存储数据的顺序和索引顺序一致,一般主键会默认创建聚簇索引,一个表只有一个聚簇索引。
  • innodb分主索引和次索引,主索引节点和数据放在一起,次索引放在主键的位置。
  • myisam的主次索引指向的都是该数据在磁盘的位置。
  • innodb的二级索引存放的是key+主键。通过二级索引去查询时会先查询主键,再通过主键来查找数据块。
  • myisam的主索引和二级索引一样,存放都是行+列的组合,存放的是数据地址。

聚簇索引:

  1. 索引项直接对数据库,可以直达
  2. 主键缺省可以用它
  3. 索引项排序和数据行完全一致。
  4. 一个表只有一个聚簇索引(数据一旦存储,顺序只能有一种)。

非聚簇索引:

  1. 不能直达,可能要多次链式访问多页表之后,才能到达
  2. 一个表可以有多个非聚簇索引
  • 聚簇索引优点:数据条目少的时候,无需回行(主键直接挂数据块)
  • 缺点:不规则的插入数据可能会导致频繁的分裂

数据库死锁

  1. 概念:多个线程资源并发请求因互相争夺资源引起的互相等待的现象
  2. 本质原因:
    • 系统资源有限
    • 进程推进顺序不合理
  3. 产生死锁的四个必要条件:
    • 互斥:某种资源只允许一个进程访问,访问期间其他进程不得访问,直到该进程访问结束。
    • 占有且等待:一个进程本身占有资源(一种或多种),而且还有资源未得到满足,等待其他进程释放资源。
    • 不可抢占:别人已经占有了某项资源,不能因为自己也需要这项资源,就去把资源抢占过来
    • 循环等待:存在一个进程链,每个进程都占有下一个进程所需要的的至少一种资源。
      满足以上四个条件才会形成死锁,破坏其中任意一个,都可以解决死锁问题。
  4. 解决死锁的方案:
    • 互斥是所有非共享资源所必须的,不仅不能破坏,还应该加以保证。因此只能从另外三个条件着手
    • 破坏占有且等待:
      • 方法一:在进程执行初期,一次性申请够所有需要的资源。如此做会造成资源浪费,会导致饥饿现象,
      • 方法二:允许进程获取期初运行所需的资源,运行过程中逐步放开分配掉的已经使用完毕的资源,然后去获取新的资源,可解决方法一问题。
    • 破坏不可抢占条件:当一个进程请求资源没有获得满足时,应释放掉本身已经占有的资源,如果会导致之前做的工作可能白费,会延长进程周期,加大资源消耗。
    • 破坏循环等待:通过定义资源的线性顺序来预防,为每个资源编号,进程只能申请大于当前编号的资源。会降低资源利用率。
  5. 预防死锁:
    • 若进程启动会导致死锁,则不启动该进程
    • 若进程申请追加资源会导致死锁,则拒绝该申请

      预防死锁通常采用银行家算法:可用资源向量、最大需求矩阵、分配矩阵、需求矩阵。通过一系列算法确保分配的资源不会导致死锁后才会进行资源分配。

  6. 解除死锁:
    • 抢占资源:从一个或多个进程中抢占足够数量的资源,提供给死锁进程
    • 终止进程:
      • 简单粗暴,终止所有死锁进程。可能会导致执行了很久的程序重新执行
      • 逐个终止进程,直到死锁状态解除,可按照以下顺序来终止:进程优先级、进程已运行时间和还需要运行的时间、进程已占用系统资源、进程运行完成还需要占用的资源、终止进程的数目、进程是交互还是批处理。

mysql索引失效的情况:

  1. 使用like,左边有百分号的情况下索引失效,右边有百分号时,索引有效
  2. 出现not,<>,!=号时,索引失效
  3. 存在隐藏的字段类型转换时索引失效,比如查询条件是varchar类型,如果查询时不加引号,可能会自动转换成int类型,导致索引失效
  4. or语句前后没有同时使用索引时,索引失效
  5. 违反最左匹配原则时,索引失效
  6. 对索引字段使用计算,有函数操作时,索引失效
  7. 如果全表扫描的速度比索引快,则索引失效

为什么不能用select *

  1. 性能问题:不必要的数据传输,会导致索引利用不足,增加io开销
  2. 可维护性差:代码的可读性降低,表结构变更容易出问题
  3. 有数据泄漏风险
  4. 造成资源浪费
  5. 扩展性差
  6. orm框架的兼容问题,表结构发生变化,可能导致映射错误

为什么不能使用insert a (select b)

  1. 列未必完全匹配,容易出错
  2. 可能会出现主键冲突
  3. 如果有并发,会出现数据一致性的问题
  4. 非空约束也可能导致插入失败
  5. 在跨数据库时,会出现兼容性问题
  6. 如果查询的是一张大表,可能会导致锁表

mysql主从库设计方案

  • 主从复制的基本原理:
  1. 主库(master):负责写操作,并将这些操作记录到二进制日志(Binary log)中。
  2. 从库(slave):从主库读取二进制文件,并在从库重放这些操作,从而实现数据同步

注意事项:

  • 主从库的配置应尽可能一致,避免出现性能瓶颈;
  • 主从库之间的网络延迟应尽可能低且稳定。
  • 定期检查数据差异

优势:

  1. 高可用性和扩展性:读操作可以配置多个从库,将压力分散到不同的从库,减轻主库压力。写操作仍然在主库进行。
  2. 主库故障时,可以将从库切换到主库,自动切换工具(MHA,Orchestrator)

一致性问题: 至少确保一个从库接收到主库的更新,再返回给客户端

监控和维护:

  1. 监控从库的复制延迟,确保数据同步的及时性,使用grafana监控数据性能
  2. 定期备份数据库数据,及清理binlog文件

java基础相关

JAVA本身有值传递吗

JAVA所有传递都是值传递,本身不存在引用传递

单例模式和工厂模式

参考:单例模式

  • 单例模式:只能有一个实例、必须创建自己的唯一实例,必须对外所有接口提供自身创建的实例
    • 懒汉模式,线程非安全,不支持多线程
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class Singleton {
      private static Singleton instance;
      private Singleton (){}

      public static Singleton getInstance() {
      if (instance == null) {
      instance = new Singleton();
      }
      return instance;
      }
      }
    • 懒汉模式,线程安全,支持多线程,但效率差
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class Singleton {
      private static Singleton instance;
      private Singleton (){}
      public static synchronized Singleton getInstance() {
      if (instance == null) {
      instance = new Singleton();
      }
      return instance;
      }
      }
    • 饿汉模式,线程安全,常用方式,但容易产生垃圾对象
      1
      2
      3
      4
      5
      6
      7
      public class Singleton {
      private static Singleton instance = new Singleton();
      private Singleton (){}
      public static Singleton getInstance() {
      return instance;
      }
      }
    • 枚举
      1
      2
      3
      4
      5
      public enum Singleton {
      INSTANCE;
      public void whateverMethod() {
      }
      }

JVM相关

java文件从开始到执行成功,中间经历了那些步骤?

  1. 生成.java文件
  2. 编译成.class文件
  3. 执行jvm.cfg
  4. 通过jvm.cfg文件找到jvm.dll
  5. 通过jvm.dll找到JNI
  6. 通过JNI找到对应的main函数执行

JVM的加载机制

  1. 通过class文件执行类加载子系统
  2. 类加载子系统加载内存空间:方法区、java堆、java栈、本地方法栈
  3. 通过垃圾回收器进行内存释放
  4. pc寄存器挂载于内存空间中
  5. 本地方法栈执行本地方法接口,挂载本地方法库
  6. 最终通过执行引擎来内存空间中的堆栈

双亲委派模型

双亲委派模型是jvm类加载机制的核心原则,即当一个类加载进来时,会先将请求委派给父类加载器,如果父类不能加载再由当前类来加载,如果所有的类加载器都加载不了,则抛出ClassNotFoundException异常。

垃圾回收机制

在jvm运行过程中当对象不再被引用时候,会系统会自动进行垃圾回收,其中垃圾回收有以下几种算法

  • (1)引用计数法
  • (2)可达性分析算法
  • (3)标记清除算法
  • (4)标记复制算法
  • (5)标记整理算法

validate关键字的作用

用于给常量加锁,保证其原子性,让其走主内存去获取数据。但不太稳定,当加锁的基本类型变成对象时,将会不生效,所以没有把握的情况下最好用synchronized关键字

CAS原理

简单翻译为:比较并交换,由三部分组成,内存位置,原值,新值,执行时先记录原值,然后去执行逻辑,若逻辑执行完成更新这个值时发现这个值没有发生变化,则cas操作成功。若发生变化,则失败重新操作。
导致问题:aba问题

ArrayList和LinkedList的区别

(1)ArrayList是数组结构而LinkedList是链表结构。
(2)在进行get、set时,ArrayList可以直接从index的位置去读取,而LinkedList需要去for循环查找,所以性能较差
(3)在进行增删时,ArrayList主要耗时为system.arrayCopy操作,而LinkedList是循环,所以性能快慢不好说。
(4)LinkedList线程不安全,单线程里面建议使用ArrayList,多线程建议使用copyOnWriteArrayList
(5)copyOnWriteArrayList在读时不加锁,但写时候加锁,写的操作是复制底层数组,修改新数组,替换旧数组,使用的volatile关键字加锁

string类为什么是final的

参考:String类为什么是final的
为了安全,如果做成可变的,在开发过程中,如果字符串可以随意变化,后面的赋值影响到了前面的逻辑,最终使逻辑变得更加复杂难懂。如果确实需要可变,可以用stringbuilder。不可变可保证线程安全。

过滤器、监听器、拦截器、触发器

过滤器:

  1. 在请求进入容器之后、获取servelt之前生效,返回时也是在servelt请求之后生效。生命周期由servelt容器管理。
  2. 实现基于回调函数
  3. 常用的场景式过滤敏感信息、设置字符编码、进行url级别的权限控制。
  4. 可通过webFilter注解实现

拦截器:

  1. 请求在进入controller前后、可以获取ioc容器的各个bean,生命周期由ioc容器管理
  2. 依赖的是反射机制
  3. 用于面向切面编程,常用有登录权限控制、事务管理、日志管理等。
  4. 通过实现HandlerInterceptor来自定义拦截器

触发器:

触发器常用于定时任务,比如一些需要定点执行的操作可用触发器完成

监听器:

用于对web容器中对象增删查改后相关的业务操作。

hashMap相关

  1. hashMap的原理:HashMap以数组+链表形式实现,数组是HashMap的主体,每个数组元素上都有一个链表结构。
  2. 负载因子:
    负载因子初始为0.75,可以手动调整,负载因子的大小决定hashMap容器的容量在多大时进行扩容,比如目前大小是16,那就是16*0.75 =
    12时进行扩容。负载因子增大可以提升空间利用率,但会导致红黑树会变得更加复杂,进而导致查询效率降低。而负载因子调小可以让hashmap更快速扩容,提升查询效率,但会导致资源浪费。0.75是总结出来的比较合适的一个数,如果有个性化需要,也可以进行调整。
  3. LinkedHashMap:是hashMap的一个子类,记录了插入顺序,如果需要输入顺序和输出顺序相同,则可以用它。
  4. treeMap:录入时会对键值进行排序,默认按升序
  5. HashSet:简化版的hashMap,存的是不重复的键的集合,不是key-value结构。线程非安全。

java中的多态有什么特性

概念:允许统一操作作用于不同对象产生的不同行为。
类型:

  1. 编译时多态,通过方法重载实现
  2. 运行时多态,通过方法重写和向上转型实现
  • 运行时多态(核心机制):

    • 实现方案:

      • 继承+方法重写:子类重写父类的方法
      • 向上转型:父类引用指向子类对象
      • 动态绑定:JVM在运行时根据对象实际决定调用哪个方法

      例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
       class Animal {
      void sound() {
      System.out.println("Animal makes sound");
      }
      }

      class Dog extends Animal {
      @Override
      void sound() {
      System.out.println("Dog barks");
      }
      }

      class Cat extends Animal {
      @Override
      void sound() {
      System.out.println("Cat meows");
      }
      }

      public class Test {
      public static void main(String[] args) {
      Animal animal1 = new Dog(); // 向上转型
      Animal animal2 = new Cat(); // 向上转型

      animal1.sound(); // 输出 "Dog barks"(动态绑定)
      animal2.sound(); // 输出 "Cat meows"
      }
      }

  • 编译时多态:同一个类有多个不同的同名方法,参数列表不同(类型、数量、顺序)

    • 绑定机制:编译时根据参数类型来确定调用哪个方法(静态绑定)

总结:

  • 多态性本质:同一操作在不同对象上会表现出不同的行为
  • 核心价值:提高代码的灵活性、可扩展性、可维护性
  • 实现关键:继承/接口+方法重写+向上转型+动态绑定。
  • 合理利用多态性,可以使代码简洁,解耦。适应复杂的需求变化

StringBuffer和StringBuilder的区别

StringBuffer线程安全,其append使用了synchronized关键字来修饰,StringBuilder线程不安全

重写和重载的区别

  1. 重载必须是在同一个类中,方法名相同,但后面具体的参数不同;重写是子类重写父类的方法
  2. 静态方法可以重载,但不能重写
  3. 重载是在编译时多态;重写是在运行时多态

多线程相关

线程池

常见线程池:

  1. Executors.newCacheThreadPool():
    • 可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务。该线程池无限大
  2. Executors.newFixedThreadPool(int n):
    • 创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。
    • 定长线程池的大小最好根据系统资源进行设置。
    • CPU 核数 = Runtime.getRuntime().availableProcessors()
    • 如果是计算密集型:线程数设置为CPU核数+1
    • 如果是IO密集型,线程数设置为2倍的CPU核数
  3. Executors.newScheduledThreadPool(int n):
    • 创建一个定长线程池,支持定时及周期性任务执行
  4. Executors.newSingleThreadExecutor():
    • 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  5. ThreadPoolExecutor:自定义线程池

BlockingQueue(缓冲队列)

  1. 缓冲队列BlockingQueue简介:
    • BlockingQueue是双缓冲队列。BlockingQueue内部使用两条队列,允许两个线程同时向队列一个存储,一个取出操作。在保证并发安全的同时,提高了队列的存取效率。
    • 常用的几种BlockingQueue:
      • ArrayBlockingQueue(int i):规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的。
        2)LinkedBlockingQueue()或者(inti):
        大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue有大小限制,不指定大小,其大小有Integer.MAX_VALUE来决定。其所含的对象是FIFO顺序排序的。
      • PriorityBlockingQueue()或者(int i):类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator决定。
      • SynchronizedQueue():特殊的BlockingQueue,对其的操作必须是放和取交替完成。

线程池的七大核心参数

  1. 核心线程数(corePoolSize):线程池的核心线程数也是最小线程数,这些线程不会被回收,即使没有线程执行,也只会处于空闲状态,如果线程池中的线程数小于核心线程数,则会在执行任务时创建。
  2. 最大线程数(maximumPoolSize):线程池所允许的最大线程数量,当核心线程数满了,且队列满了之后,会继续创建线程。
  3. keepAliveTime:超过核心线程数的临时线程的存活时间
  4. unit:存活时间的单位,s、ms等等
  5. 工作队列(workQueue):当核心线程数满了之后,会创建工作队列,缓冲队列是一个先进先出的模式底层实现设计java的AQS机制。
  6. 创建线程的工厂类:通常我们会自定义一个线程工厂的名称,以方便快速定位线程
  7. 线程拒绝策略,当线程池和队列满了之后,会执行线程的拒绝策略

线程池的四大拒绝策略

  1. 线程池默认拒绝策略:抛异常处理
  2. 直接丢弃不处理
  3. 丢弃最老的线程队列
  4. 等待调用execute进行处理,页面进行loding状态

线程池使用原则

  1. 线程工厂类参数一定要传,且一定要有意义,方便后期问题定位
  2. 尽量避免为局部变量创建线程池,引入线程池的目的是为了提高资源的复用率,为局部变量创建线程池很难达到这个效果,而且如果忘记调用shutdown,多次运行后会出现内存泄露的情况(OutOfMemoryError)
  3. 设计可监控的线程池
    1. 对拒绝策略监控,一旦进入拒绝策略,说明最大线程数都处理不过来了,这种情况必须进行处理
    2. 监控队列大小:如果队列有积压,说明服务可能会有问题,需要关注。
    3. 监控活跃线程数量,如果线程池设置过大,会造成资源的浪费,可以根据日常活跃线程数来调整线程池大小。
    4. 监控线程总数,创建线程时候+1,销毁时-1,这样来监控是否有资源泄露。

进程和线程的区别

  1. 线程是进程的基本单元,一个进程至少有一个线程
  2. 进程是系统正在运行的基本程序,每个进程是独立的。举个例子:手机里可以同时运行,微信,QQ,淘宝,京东,而这几个每个软件都可以有多线程执行。

Threadlocal

本地线程变量,多线程访问同一个共享变量时候会有并发问题,如果创建一个ThreadLocal变量来修饰的话,每个线程都会有这个变量的一个副本,实际操作时操作的是本地内存中的变量,从而规避线程安全问题。

AQS机制(抽象队列同步器)

AQS是java并发包的核心框架,通过FIFO等待队列和状态管理机制,实现锁的等待和唤醒。

  • 核心思想:
    1. 资源共享模式:
      • 独占模式:同一时刻,只有一个线程可以访问资源
      • 共享模式:统一时刻,有多个线程可以访问资源
    2. 状态管理:
      • AQS是使用一个volatile的int类型变量state来表示同步状态,通过getState(),setState(),compareAndSetState()方法来操作状态
    3. 等待队列:
      • AQS维护一个FIFO(先进先出)的双向队列,用户存储等待获取资源的线程;每个节点代表一个等待线程,包含引用、等待状态等信息
    4. 模板方法设计模式:
      • AQS是一个抽象类,定义了获取和释放资源的方法(如acquire(),release())。
      • 具体的同步器需要实现tryAcquire()和tryRelease()方法,定义如何获取和释放资源
  • 优点:
    1. 比较灵活,可以自由的实现各种同步工具。
    2. 使用cas操作和volatile变量,避免锁的开销
    3. 可以根据AQS实现自定义的同步器
  • 缺点:
    1. 实现较为复杂,需要深入理解其内部逻辑
    2. 调试比较困难,尤其是涉及并发的场景

总结:AQS是java并发包的核心框架,通过状态管理、等待队列和模板方法设计模式,提供灵活、高性能的同步机制。

在线程池中,execute()和submit()有什么区别?

这是线程池中提交任务的两种方法

  • 区别:
    1. execute()没有返回值,直接返回void类型,但submit()会返回Future类型,可以通过Future.get()来获取异常信息
    2. execute()适用于不需要返回结果的简单业务场景,submit()适用于需要获取结果或处理异常的任务。

分布式问题

如何设计一个秒杀系统

  1. 秒杀系统的常见问题:
    1. 频繁访问数据库,导致服务挂掉
    2. 缓存穿透、缓存击穿和缓存雪崩
    3. 链接暴露,被提前秒杀
    4. 流量过大
    5. 超卖
  2. 解决方案:
    1. 服务单一职责,新建秒杀数据库,就算真的挂掉也不影响其他服务
    2. 缓存问题方案见redis
    3. 链接被别人以F12的方式提前搞走,进行提前秒杀,通过加密让前端获取动态链接,保证链接不会提前暴露
    4. 流量过大可以采用redis集群,哨兵机制,已经mq消峰
    5. 数据加载到redis中,通过redis事务的原子性来控制超卖
    6. 其他手段:秒杀前前端按钮置灰,不允许频繁点击,连续点击多次就提示其等等再点。
    7. 解决超卖问题:悲观锁,乐观锁,队列形式:做订单队列。一个一个处理,达到阈值就显示卖完。用redis的原子自增操作:讲操作改为先申请后确认,再有在redis处理完后再进行异步写库。有可能导致数据一致和少卖的情况(申请后不确认付款,也会占用名额)。
    8. 京东库存系统关于超卖的处理:首先设置预占库存,若可用库存不足,则不再对外销售,其次用乐观锁,在写库时查版本号,最后有安全库存的设置。保证有少量超卖也无所谓。

哨兵机制

  • 在redis集群中会有主服务器和从服务器,若其中一个服务器出现问题,则需要进行故障转移,如何判断,转移,就需要用到哨兵机制
  • 哨兵机制主要执行以下三个任务:
  1. 监控:定期向主从服务器发送请求,看是否能正常返回,不可以就认为可能出现问题
  2. 提醒:当哨兵监控到服务器问题后,会向管理员发送故障提醒
  3. )自动故障转移:当主服务器出现问题,哨兵会自动选举新的主服务器,当客户端请求过来时,返回新的请求地址。

如果服务器崩溃了,在不影响客户使用的前提下,应该怎么处理

  1. 高可用架构,尽可能减少对客户的影响
  2. 负载均衡:将流量发送到不同的服务器
  3. 集群部署:确保多台服务器同时运行
  4. 自动故障转移:使用主从复制或分布式一致性协议,实现自动故障转移
  5. 监控告警:监控CPU、内存、磁盘、网络等,设置阈值,达到就告警
  6. 定期对服务器进行健康检查
  7. 崩溃后快速恢复:自动重启、故障隔离、数据恢复
  8. 容灾恢复:备份策略、容灾演练、多区域部署
  9. 客户端容错处理:重试机制、缓存数据、优雅降级(友好的错误提示)
  10. 日志与故障分析:日志收集、故障分析、改进措施

算法相关

雪花算法:

雪花算法是Twitter公司发明的一种算法,主要目的是解决在分布式环境下,ID怎样生成的问题

  1. 分布式ID生成规则硬性要求:全局唯一:不能出现重复的ID号,单调递增
  2. 分布式ID生成可用性要求:
    • 高可用:发布一个获取分布式ID的请求,服务器就要保证99.999%的情况下给创建一个全局唯一的分布式ID。
    • 低延迟:发布一个获取分布式ID的请求,要快,急速。
    • 高QPS:假如并发一口气10万个创建分布式ID请求同时杀过来,服务器要顶得住并且成功创建10万个分布式ID

经测试snowflake每秒能生成26万个自增可排序的ID。snowflake生成的ID结果是一个64bit大小的整数,为一个Long型(转换成字符串后长度最多19)。分布式系统内不会产生ID碰撞(datacenter和workerId作区分)并且效率高。不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也非常高,可以根据自身业务分配bit位,非常灵活。


Java项目遇到的问题
https://zhyyao.me/2022/10/01/technology/java/java_project/
作者
zhyyao
发布于
2022年10月1日
许可协议