Java项目遇到的问题
实际项目中,Java项目遇到的问题,包含MQ、SpringBoot、Redis、MySQL、Kafka 等…
MQ 相关
MQ 大量未订阅消息处理
- 产生原因:消费端宕机、消费端消费能力不足、生产端流量过大。
- 解决方案:
- 加机器,如果是线上事故,尽可能多的申请服务器,尽量短时间将 MQ 消费掉。
- 在 MQ 配置中增加最大消费数量和每次从消息队列中读取的消费数量。
- 上线专门记录消息的队列,将其持久化到数据库中,后续慢慢处理。
若因为积压时间太久,导致消息丢失如何处理
- 先找到丢失的消息:如果有备份,直接从备份里恢复;如果没有,则尝试从生产者的生产记录里找到,让生产者重新发送。
- 分析问题出现原因:
- 首先看消息是不是积压时间过长丢失的,如果是,延长这个时间。
- 然后看是不是队列空间打满了,如果是,扩大空间。
- 最后增强消费能力。
- 后续优化:
- 采用高可用架构,配置集群,保证一台服务器宕掉的时候,其他依然可用。
- 进行数据复制,防止单点丢失数据。
若积压太多,导致 MQ 满了如何处理
- 直接写程序将新收到的 MQ 丢弃,在流量低峰期再找回来。
如果 MQ 和 MySQL 同时挂了,怎么处理
- MQ 持久化,写数据库、写磁盘、写日志。恢复后从里面读取,重新发送。
RabbitMQ 的原理
- 生成者发送消息之后并不会直接到消费者,而是会把消息发送到交换机(Exchange),在交换机中将其放在合适的队列(Queue),然后才是消费者进行消费。
消息丢失如何处理
- 消息丢失分三种情况:生产者消息丢失、队列消息丢失、消费者消息丢失。
- 生产者消息丢失:用 Confirm 模式,将生产者发送消息时,生成一个唯一 ID,在消费队列接收后返回一个 ACK(包含唯一
ID),如果接收失败,就返回 NACK 重新发送即可。 - 队列消息丢失:同样的方式,将消息做持久化,持久化完成后再返回 ACK。
- 消费者消息丢失:一般是采用了自动确认功能,改为手动确认,在消费者消费完成后再返回成功。而不是接收后自动返回成功。
- 生产者消息丢失:用 Confirm 模式,将生产者发送消息时,生成一个唯一 ID,在消费队列接收后返回一个 ACK(包含唯一
Redis 相关
缓存穿透、缓存击穿和缓存雪崩
- 缓存穿透:大量不存在的查询读缓存读不到,直接去读取数据库。造成服务崩溃的情况。
- 解决方案:
- 在缓存中增加 value 为空的缓存,但这样会加大内存消耗,可以设置一个较短的过期时间。如果在缓存时间内在数据库中增加了这个
key 对象,会造成缓存和实际不一致,可以加库后发 MQ 更新缓存。 - 如果实时性要求不高,可以采用布隆过滤器,它可以减少使用缓存空间。
- 在缓存中增加 value 为空的缓存,但这样会加大内存消耗,可以设置一个较短的过期时间。如果在缓存时间内在数据库中增加了这个
- 解决方案:
- 缓存击穿:若一个热点 key 的缓存失效了,此时新建缓存比较复杂,同时多个请求来新建该缓存,导致服务崩溃。
- 解决方案:
- 加锁排队,同一个 key 同时只有一个在新建。风险:如果新建时间较长,会有死锁风险,会导致吞吐量降低,但可以较好的保证数据一致性。
- 永不过期的缓存,缓存本身无物理过期时间,添加缓存逻辑过期时间,若超过则更新缓存,更新期间还读以前的缓存。更新后读新缓存。不会有死锁风险,但会有数据不一致的情况出现。
- 解决方案:
- 缓存雪崩:若一个缓存一段时间内失效,此时有大量请求进来,所有请求落在服务器上,会引起缓存雪崩,服务宕机。
- 解决方案:
- Redis 二级缓存,设置过期时间不同。
- 分布式部署多个机房,提高缓存可用性。
- 数据预热,提前加载好缓存数据。
- 加锁排队,一个 key 同时只能有一个线程访问,其他排队等待。
- 解决方案:
Redis 的数据类型
- String(字符串)、List(列表)、Set(集合)、Hash(哈希)、Zset(有序集合)。
Redis 的数据结构
- SDS:简单动态字符串,对应数据类型中的 String。
- Redis 是用 C 语言实现的,但在 C 语言里面的 char 类型字符串存在缺陷,所以 Redis 封装了一个新的简单动态字符串。
- C 语言中 char 的缺陷:
- 字符数组结束位置会插入
\0
,所以出现\0
时 C 语言就会认为字符串已经结束了。导致字符串不能全量读取。这种限制导致
C 语言的字符串只能读取文本,不能读取音频、视频等二进制数据。 - CHAR 类型字符串不会计算自身缓冲区大小,有缓冲区溢出的风险。
- 字符数组结束位置会插入
- SDS 数据结构:len(字符串长度)、alloc(分配的空间长度)、flags(SDS 类型)、buf[](字节数组)。
- 优势:
- C 语言中获取字符串长度需要遍历,时间复杂度为 O(N),而简单动态字符串可以直接获取,时间复杂度为 O(1)。
- 二进制安全,不需要
\0
来控制结尾,同时还支持了文本、视频、音频等的存储。 - 不会发生缓冲区溢出,有 alloc-len 来获取剩余空间大小,若缓冲区大小不够用,Redis 会自动扩张 SDS 空间(小于 1M
的翻倍扩容,大于 1M 的按 1M 扩容)。 - 节省内存空间,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 实现的锁可能存在以下问题:
- 单点故障:如果 Redis 实例宕机,锁就会失效。
- 时钟漂移:如果 Redis 实例的时钟不同步,可能导致锁的过期时间计算错误。
- 核心思想:在多个独立的 Redis 实例上同时获取锁,只有大多数实例(N/2+1)成功获取锁,才算真正获取到锁。
- 步骤:
- 获取当前时间:记录获取锁的开始时间。
- 依次向多个 Redis 实例请求锁;(1. 使用相同的 key 和随机值作为锁的值,2. 设置锁的过期时间)。
- 计算获取锁的时间:如果获取锁的时间小于锁的过期时间,并且大多数实例(N/2+1)成功获取锁,则认为锁获取成功。
- 释放锁:向所有 Redis 实例发送删除锁的请求。
- 优点:避免单点故障,部分实例宕机依然可用,且基于 Redis 实现,易于集成。
- 缺点:需要在多个 Redis 实例上获取锁,增加了网络通信和计算开销。如果 Redis
实例的时钟不同步,可能会导致锁的过期时间计算错误。而且可靠性存在争议,不能完全解决分布式锁的问题。 - 使用场景:分布式任务调度:确保同一时间只有一个节点执行任务;资源竞争控制:防止多节点同时操作共享资源。
- 替代方案:Zookeeper,Etcd 分布式锁,数据库分布式锁。
Etcd 分布式锁
- 特性:
- 租约:Etcd 里面的时间绑定机制,为键值对设置一个有效期,租约过期时,之前设置的键值对自动删除。
- 事务:支持事务操作,保证原子性。
- Watch 机制:监听某个键的变化,用来实现锁的等待和通知。
- 实现方案:
- 创建租约,设置过期时间。
- 客户端尝试通过事务写入一个键值对(锁的唯一标识),失败表示锁已经被其他人持有。
- 监听锁释放:如果获取失败,可以通过 Watch 机制,监听锁的释放。
- 释放锁:客户端删除键值对,或者租约过期来释放锁。
Redis 的过期策略和淘汰机制
- 过期策略:
- 惰性删除:访问时检查,并删除过期键。
- 定期删除:定期随机检查,并删除过期键。
- 淘汰机制:
- 不淘汰,默认策略。当有新的写入进来发现内存不足,直接报错。适用于不允许数据丢失的场景。
- 从设置了过期时间的键中,删除最近最少使用的。
- 从设置了过期时间的键中,删除最不经常使用的。
- 从设置了过期时间的键中,随机淘汰。
- 从设置了过期时间的键中,淘汰剩余时间最短的键。
- 从所有键中,淘汰最近最少使用的。
- 从所有键中,淘汰最不经常使用的。
- 从所有键中,随机淘汰。
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 的启动原理
- 开始启动。
- 构造一个 Spring 应用,进行模块的初始化,包括配置 source、配置是否为 Web
环境、创建初始化构造器(获取工厂对象,生成工厂实例)、创建应用监听器(获取工厂对象,生成工厂实例)、配置应用的主方法所在类。 - 启动该应用:启动监听器模块、启动配置环境模块、启动应用上下文(创建上下文对象、基本属性配置、更新应用上下文:准备环境所需
Bean 工厂和通过工厂产生环境所需的 Bean)。
Spring Boot 的启动类,使用 @SpringBootApplication
注解。
Spring Boot 比较重要的注解
- @SpringBootConfiguration:继承
@Configuration
,标识当前是注解类。- 按照原来 XML 配置文件的形式,Spring Boot 大多采用配置类来解决配置问题:
- XML 中进行配置,指向类路径。
- 通过
@Bean
可注入。任何一个标注了@Bean
的方法,其返回值将作为 Bean 定义注册到 Spring 的 IOC 容器。方法名将默认成该
Bean 定义的 ID。
- 按照原来 XML 配置文件的形式,Spring Boot 大多采用配置类来解决配置问题:
- @EnableAutoConfiguration:开启 Spring Boot 注解功能。
- @ComponentScan:扫描路径设置。
- 对应 XML 配置中的元素。
- 自动扫描并加载符合条件的组件。
- 将 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 框架内定义的三级缓存来实现的。 - 三级缓存:
- singletonObjects:第一级缓存,里面放置的是实例化好的单例对象。
- earlySingletonObjects:第二级缓存,里面存放的是提前曝光的单例对象。
- singletonFactories:第三级缓存,里面存放的是要被实例化的对象的对象工厂。
- 所以当一个 Bean 调用构造函数进行实例化后,即使属性还未填充,就可以通过三级缓存向外暴露依赖的引用值(所以循环依赖问题的解决也是基于
Java 的引用传递),这也说明了另外一点,基于构造函数的注入,如果有循环依赖,Spring 是不能够解决的。还要说明一点,Spring 默认的
Bean Scope 是单例的,而三级缓存中都包含 singleton,可见是对于单例 Bean 之间的循环依赖的解决,Spring 是通过三级缓存来实现的。
Spring 的 @Import
注解的作用
- 将没有使用
@Component
注解的普通 class 加入到 Spring 容器,由 Spring 管理。 - 导入一个 Configuration 类(比如你想组合多个 Java Config 类到一个 Java Config 类,或者你引入的第三方 jar 包中的 Java
Config 类没在你 Spring Boot 程序的子包下,即没有被扫描进 Spring 容器)。 - 通过实现了
ImportSelector
接口的类,导入多个 class 到 Spring 容器(Spring Boot 的自动装配@EnableAutoConfiguration
)。 - 通过实现
ImportBeanDefinitionRegistrar
接口的方式(MyBatis 整合 Spring:MapperScannerRegistrar.java
和@MapperScan
注解。 - 使用@Import搭配@Configuration导入到Spring容器。
同一个类中调用 @Transaction注解的方法会有事务效果吗?
没有,可以Autowired注入自己,然后再调用注入的类中的方法,即自己依赖自己,循环依赖
springboot启动过程中如何加载META-INF/spring.factories
1 |
|
IOC和AOP的原理和应用
- IOC本质就是依赖注入,在开发过程中,A如果需要引用的B,那就需要new B()
;但这样耦合度较高,如果B做了调整,那A也需要相应调整。在spring引入ioc之后,可以直接通过工厂模式加反射机制,将其托管给spring容器(常见的dto的构造器原理)。这样就降低了代码的耦合程度。
AOP本质是面向切面编程,除了spring中的应用之外,更多算是一种思想,将代码以切面的形式区分为核心切面和横切面。开发人员在开发过程中只需要考虑自己的核心业务逻辑,横切逻辑无需考虑在内。在spring中的应用就是将公用的逻辑提取出来,在代码执行时加载,如权限控制,日志管理,事务COMMIT等。这个思想在开发过程中也可以继续沿用。
SpringMVC的大致实现过程
- 用户发起request请求,如果有多个servlet,则通过servletmapping来指定具体的servlet。
- 通过servlet指定的url去需要指定的controller。
- 通过controller去调用modelandview,返回指定视图view。
- 将返回model组装返回给response。
Spring事务的传播级别
- spring的默认级别,判断上下文是否存在事务,若存在则加入当前事务,若不存在,则新建事务。
- 判断上下文是否存在事务,若不存在,则以非事务方式运行
- 判断上下文是否存在事务,若存在,则抛出异常
- 判断上下文是否存在事务,若不存在,则抛出异常
- 每次执行都会创建新事务,同事挂起上下文中的事务,执行完成后再恢复上下文中的事务,子事务的执行不影响父事务的执行和回滚
- 判断上下文中是否存在事务,若存在,则挂起,直到当前事务执行完成再恢复(降低事务大小,将非核心逻辑包裹执行)。
- 嵌套事务,若上下文中存在事务则嵌套执行,若没有,则新建事务。
Spring如何自定义注解
- @Target(ElementType.TYPE)注解,是说这个注解可以作用于类,接口或枚举类型;
- @Component 表示这个注解是spring组件注解,会被spring bean加载
- @Documented 表示这个注解会被放在javadoc中
- @Retention(RetentionPolicy.RUNTIME) 表示注解运行时保留,可以通过反射读取
- @interface 接口属性
除了eurka的其他注册中心?
- 市面主流的注册中心有很多,根据自身需要去选用即可
- eureka特点是易于集成到Spring colud生态,高可用,支持多节点复制。缺点是一致性较弱,功能也比较简单。
- zookeeper拥有强一致性,但配置比较复杂,同时写的性能也比较差
- consul支持服务发现和多数据中心,但配置也比较复杂,强一致性也会影响性能。
- Nacos,没有eurkea和consul来的成熟,
- etcd,拥有高可用,强一致性,但配置较为复杂,且功能也比较单一,主要用于键值存储
Spring Boot 和Spring Cloud的区别
- Spring Boot是快速开发和运行Spring的工具,适合单体应用和微服务里面的单个服务。
- Spring Cloud是构建分布式系统和微服务的工具集合,适合复杂的分布式场景。
Mysql相关
mybatis方言处理
- 定义mapper.xml文件,在里面加入方言支持(只需要列出必要字段)
- 因为mapper XML 文件不支持继承,(一个接口中的方法在XML文件中必须有实现-否则启动报错),所以采用 Mapper.java 接口类继承的方式
数据库事务特性(ACID原则):
- 原子性:一个事务多个操作,要么同时成功提交,要么同时失败回滚。
- 一致性:一个事务执行前后,必须从一个一致状态变成另一个一致状态,比如A和B一共有400元,两者无论互相转账几次,都应该是400元。
- 隔离性:一个事务内部操作和使用数据,对其他同时进行的事务,都是隔离的
- 持久性:数据库修改后应该是持久修改。后面就算数据库出现问题,数据恢复后,数据也不应该发生变化
数据库可能出现的问题:
- 脏读:事务A读取到了事务B尚未提交的数据
- 不可重复读:事务A第一次读取数据之后,事务B提交了数据,事务A第二次读取时候发现数据和第一次读取不一致。
- 幻读:事务A修改了表里面的所有数据但没有提交,这时事务B对这个表新增了一行,然后提交了,事务A提交之后发现有一行没有修改。这种现象叫幻读。若要解决,需要在修改前进行锁表
数据库隔离级别:
- 读未提交,
- 读已提交
- 可重复读
- 串行化
mysql的mvcc的并发控制的问题
概念:MVCC又叫多版本并发控制,提供访问数据库时,对事务内读取到的内存做处理,避免写操作阻塞读操作。
MVCC实现方式:
- 更新时并非直接更新数据库,而是将旧的数据标记为过时的,新的数据以新增方式写入,后续通过垃圾回收机制回收旧的数据。
- mysql和innodb使用另外一种,数据库只保存最新的数据,但会通过undo动态重构旧版本数据。
InnoDB的MVCC实现机制:通过在数据后面添加两个隐藏列来实现,分别是创建时间和删除时间,此处的时间不是时间戳,是版本号。每开启一个新事务,版本号递增。
mysql最左匹配原则的原理
原理:索引在查询时,会从联合索引的最左边开始查起。
比如索引为A\B\C.当查询AB或BA时,索引可以生效,查询A\AB\ABC时索引生效
mysql的InnoDB和MyISAM
- 共同点:这两个都是mysql常见的存储引擎。
- 区别:
- 事务支持:Innodb支持事务,适合需要高可靠性的应用;但myisam不支持,不能保证数据的一致性和完整性
- 锁定机制:Innodb采用行级锁,适合高并发场景,能有效减少冲突;myisam使用表级锁,并发写入时性能较差
- 外键支持:Innodb支持外键,确保数据完整性,但myisam不支持外键
- 崩溃恢复:Innodb具备崩溃回复能力,但myisam需要手动恢复
- 性能:Innodb在写操作时表现更好,适合频繁写入的场景;myisam适合读多写少的业务场景
- 存储结构:Innodb的数据与索引存储在同一个文件中(.ibd),支持表空间管理;myisam的数据和索引分开存储
- 全文索引:Innodb再5.6之后支持全文索引,但myisam一直都支持
- 压缩:Innodb支持表压缩,压缩后节省存储空间,但myisam压缩后为只读模式
- 适用场景:Innodb适合需要事务、高并发、数据完整性要求较高的系统,如电商系统;myisam适合读多写少、不需要事务的系统,如日志系统、数据仓库等
- ACID兼容:Innodb支持ACID;myisam不支持
- mysql有innodb和myisam两种类型,其中innodb使用聚簇索引,myisam使用非聚簇索引:
- 聚簇索引是对磁盘数据进行重新组织排列的算法,特点是存储数据的顺序和索引顺序一致,一般主键会默认创建聚簇索引,一个表只有一个聚簇索引。
- innodb分主索引和次索引,主索引节点和数据放在一起,次索引放在主键的位置。
- myisam的主次索引指向的都是该数据在磁盘的位置。
- innodb的二级索引存放的是key+主键。通过二级索引去查询时会先查询主键,再通过主键来查找数据块。
- myisam的主索引和二级索引一样,存放都是行+列的组合,存放的是数据地址。
聚簇索引:
- 索引项直接对数据库,可以直达
- 主键缺省可以用它
- 索引项排序和数据行完全一致。
- 一个表只有一个聚簇索引(数据一旦存储,顺序只能有一种)。
非聚簇索引:
- 不能直达,可能要多次链式访问多页表之后,才能到达
- 一个表可以有多个非聚簇索引
- 聚簇索引优点:数据条目少的时候,无需回行(主键直接挂数据块)
- 缺点:不规则的插入数据可能会导致频繁的分裂
数据库死锁
- 概念:多个线程资源并发请求因互相争夺资源引起的互相等待的现象
- 本质原因:
- 系统资源有限
- 进程推进顺序不合理
- 产生死锁的四个必要条件:
- 互斥:某种资源只允许一个进程访问,访问期间其他进程不得访问,直到该进程访问结束。
- 占有且等待:一个进程本身占有资源(一种或多种),而且还有资源未得到满足,等待其他进程释放资源。
- 不可抢占:别人已经占有了某项资源,不能因为自己也需要这项资源,就去把资源抢占过来
- 循环等待:存在一个进程链,每个进程都占有下一个进程所需要的的至少一种资源。
满足以上四个条件才会形成死锁,破坏其中任意一个,都可以解决死锁问题。
- 解决死锁的方案:
- 互斥是所有非共享资源所必须的,不仅不能破坏,还应该加以保证。因此只能从另外三个条件着手
- 破坏占有且等待:
- 方法一:在进程执行初期,一次性申请够所有需要的资源。如此做会造成资源浪费,会导致饥饿现象,
- 方法二:允许进程获取期初运行所需的资源,运行过程中逐步放开分配掉的已经使用完毕的资源,然后去获取新的资源,可解决方法一问题。
- 破坏不可抢占条件:当一个进程请求资源没有获得满足时,应释放掉本身已经占有的资源,如果会导致之前做的工作可能白费,会延长进程周期,加大资源消耗。
- 破坏循环等待:通过定义资源的线性顺序来预防,为每个资源编号,进程只能申请大于当前编号的资源。会降低资源利用率。
- 预防死锁:
- 若进程启动会导致死锁,则不启动该进程
- 若进程申请追加资源会导致死锁,则拒绝该申请
预防死锁通常采用银行家算法:可用资源向量、最大需求矩阵、分配矩阵、需求矩阵。通过一系列算法确保分配的资源不会导致死锁后才会进行资源分配。
- 解除死锁:
- 抢占资源:从一个或多个进程中抢占足够数量的资源,提供给死锁进程
- 终止进程:
- 简单粗暴,终止所有死锁进程。可能会导致执行了很久的程序重新执行
- 逐个终止进程,直到死锁状态解除,可按照以下顺序来终止:进程优先级、进程已运行时间和还需要运行的时间、进程已占用系统资源、进程运行完成还需要占用的资源、终止进程的数目、进程是交互还是批处理。
mysql索引失效的情况:
- 使用like,左边有百分号的情况下索引失效,右边有百分号时,索引有效
- 出现not,<>,!=号时,索引失效
- 存在隐藏的字段类型转换时索引失效,比如查询条件是varchar类型,如果查询时不加引号,可能会自动转换成int类型,导致索引失效
- or语句前后没有同时使用索引时,索引失效
- 违反最左匹配原则时,索引失效
- 对索引字段使用计算,有函数操作时,索引失效
- 如果全表扫描的速度比索引快,则索引失效
为什么不能用select *
- 性能问题:不必要的数据传输,会导致索引利用不足,增加io开销
- 可维护性差:代码的可读性降低,表结构变更容易出问题
- 有数据泄漏风险
- 造成资源浪费
- 扩展性差
- orm框架的兼容问题,表结构发生变化,可能导致映射错误
为什么不能使用insert a (select b)
- 列未必完全匹配,容易出错
- 可能会出现主键冲突
- 如果有并发,会出现数据一致性的问题
- 非空约束也可能导致插入失败
- 在跨数据库时,会出现兼容性问题
- 如果查询的是一张大表,可能会导致锁表
mysql主从库设计方案
- 主从复制的基本原理:
- 主库(master):负责写操作,并将这些操作记录到二进制日志(Binary log)中。
- 从库(slave):从主库读取二进制文件,并在从库重放这些操作,从而实现数据同步
注意事项:
- 主从库的配置应尽可能一致,避免出现性能瓶颈;
- 主从库之间的网络延迟应尽可能低且稳定。
- 定期检查数据差异
优势:
- 高可用性和扩展性:读操作可以配置多个从库,将压力分散到不同的从库,减轻主库压力。写操作仍然在主库进行。
- 主库故障时,可以将从库切换到主库,自动切换工具(MHA,Orchestrator)
一致性问题: 至少确保一个从库接收到主库的更新,再返回给客户端
监控和维护:
- 监控从库的复制延迟,确保数据同步的及时性,使用grafana监控数据性能
- 定期备份数据库数据,及清理binlog文件
java基础相关
JAVA本身有值传递吗
JAVA所有传递都是值传递,本身不存在引用传递
单例模式和工厂模式
参考:单例模式
- 单例模式:只能有一个实例、必须创建自己的唯一实例,必须对外所有接口提供自身创建的实例
- 懒汉模式,线程非安全,不支持多线程
1
2
3
4
5
6
7
8
9
10
11public 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
10public 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
7public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
} - 枚举
1
2
3
4
5public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
- 懒汉模式,线程非安全,不支持多线程
JVM相关
java文件从开始到执行成功,中间经历了那些步骤?
- 生成.java文件
- 编译成.class文件
- 执行jvm.cfg
- 通过jvm.cfg文件找到jvm.dll
- 通过jvm.dll找到JNI
- 通过JNI找到对应的main函数执行
JVM的加载机制
- 通过class文件执行类加载子系统
- 类加载子系统加载内存空间:方法区、java堆、java栈、本地方法栈
- 通过垃圾回收器进行内存释放
- pc寄存器挂载于内存空间中
- 本地方法栈执行本地方法接口,挂载本地方法库
- 最终通过执行引擎来内存空间中的堆栈
双亲委派模型
双亲委派模型是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。不可变可保证线程安全。
过滤器、监听器、拦截器、触发器
过滤器:
- 在请求进入容器之后、获取servelt之前生效,返回时也是在servelt请求之后生效。生命周期由servelt容器管理。
- 实现基于回调函数
- 常用的场景式过滤敏感信息、设置字符编码、进行url级别的权限控制。
- 可通过webFilter注解实现
拦截器:
- 请求在进入controller前后、可以获取ioc容器的各个bean,生命周期由ioc容器管理
- 依赖的是反射机制
- 用于面向切面编程,常用有登录权限控制、事务管理、日志管理等。
- 通过实现HandlerInterceptor来自定义拦截器
触发器:
触发器常用于定时任务,比如一些需要定点执行的操作可用触发器完成
监听器:
用于对web容器中对象增删查改后相关的业务操作。
hashMap相关
- hashMap的原理:HashMap以数组+链表形式实现,数组是HashMap的主体,每个数组元素上都有一个链表结构。
- 负载因子:
负载因子初始为0.75,可以手动调整,负载因子的大小决定hashMap容器的容量在多大时进行扩容,比如目前大小是16,那就是16*0.75 =
12时进行扩容。负载因子增大可以提升空间利用率,但会导致红黑树会变得更加复杂,进而导致查询效率降低。而负载因子调小可以让hashmap更快速扩容,提升查询效率,但会导致资源浪费。0.75是总结出来的比较合适的一个数,如果有个性化需要,也可以进行调整。 - LinkedHashMap:是hashMap的一个子类,记录了插入顺序,如果需要输入顺序和输出顺序相同,则可以用它。
- treeMap:录入时会对键值进行排序,默认按升序
- HashSet:简化版的hashMap,存的是不重复的键的集合,不是key-value结构。线程非安全。
java中的多态有什么特性
概念:允许统一操作作用于不同对象产生的不同行为。
类型:
- 编译时多态,通过方法重载实现
- 运行时多态,通过方法重写和向上转型实现
运行时多态(核心机制):
实现方案:
- 继承+方法重写:子类重写父类的方法
- 向上转型:父类引用指向子类对象
- 动态绑定: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
29class 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线程不安全
重写和重载的区别
- 重载必须是在同一个类中,方法名相同,但后面具体的参数不同;重写是子类重写父类的方法
- 静态方法可以重载,但不能重写
- 重载是在编译时多态;重写是在运行时多态
多线程相关
线程池
常见线程池:
- Executors.newCacheThreadPool():
- 可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务。该线程池无限大
- Executors.newFixedThreadPool(int n):
- 创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。
- 定长线程池的大小最好根据系统资源进行设置。
- CPU 核数 = Runtime.getRuntime().availableProcessors()
- 如果是计算密集型:线程数设置为CPU核数+1
- 如果是IO密集型,线程数设置为2倍的CPU核数
- Executors.newScheduledThreadPool(int n):
- 创建一个定长线程池,支持定时及周期性任务执行
- Executors.newSingleThreadExecutor():
- 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
- ThreadPoolExecutor:自定义线程池
BlockingQueue(缓冲队列)
- 缓冲队列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,对其的操作必须是放和取交替完成。
- ArrayBlockingQueue(int i):规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的。
线程池的七大核心参数
- 核心线程数(corePoolSize):线程池的核心线程数也是最小线程数,这些线程不会被回收,即使没有线程执行,也只会处于空闲状态,如果线程池中的线程数小于核心线程数,则会在执行任务时创建。
- 最大线程数(maximumPoolSize):线程池所允许的最大线程数量,当核心线程数满了,且队列满了之后,会继续创建线程。
- keepAliveTime:超过核心线程数的临时线程的存活时间
- unit:存活时间的单位,s、ms等等
- 工作队列(workQueue):当核心线程数满了之后,会创建工作队列,缓冲队列是一个先进先出的模式底层实现设计java的AQS机制。
- 创建线程的工厂类:通常我们会自定义一个线程工厂的名称,以方便快速定位线程
- 线程拒绝策略,当线程池和队列满了之后,会执行线程的拒绝策略
线程池的四大拒绝策略
- 线程池默认拒绝策略:抛异常处理
- 直接丢弃不处理
- 丢弃最老的线程队列
- 等待调用execute进行处理,页面进行loding状态
线程池使用原则
- 线程工厂类参数一定要传,且一定要有意义,方便后期问题定位
- 尽量避免为局部变量创建线程池,引入线程池的目的是为了提高资源的复用率,为局部变量创建线程池很难达到这个效果,而且如果忘记调用shutdown,多次运行后会出现内存泄露的情况(OutOfMemoryError)
- 设计可监控的线程池
- 对拒绝策略监控,一旦进入拒绝策略,说明最大线程数都处理不过来了,这种情况必须进行处理
- 监控队列大小:如果队列有积压,说明服务可能会有问题,需要关注。
- 监控活跃线程数量,如果线程池设置过大,会造成资源的浪费,可以根据日常活跃线程数来调整线程池大小。
- 监控线程总数,创建线程时候+1,销毁时-1,这样来监控是否有资源泄露。
进程和线程的区别
- 线程是进程的基本单元,一个进程至少有一个线程
- 进程是系统正在运行的基本程序,每个进程是独立的。举个例子:手机里可以同时运行,微信,QQ,淘宝,京东,而这几个每个软件都可以有多线程执行。
Threadlocal
本地线程变量,多线程访问同一个共享变量时候会有并发问题,如果创建一个ThreadLocal变量来修饰的话,每个线程都会有这个变量的一个副本,实际操作时操作的是本地内存中的变量,从而规避线程安全问题。
AQS机制(抽象队列同步器)
AQS是java并发包的核心框架,通过FIFO等待队列和状态管理机制,实现锁的等待和唤醒。
- 核心思想:
- 资源共享模式:
- 独占模式:同一时刻,只有一个线程可以访问资源
- 共享模式:统一时刻,有多个线程可以访问资源
- 状态管理:
- AQS是使用一个volatile的int类型变量state来表示同步状态,通过getState(),setState(),compareAndSetState()方法来操作状态
- 等待队列:
- AQS维护一个FIFO(先进先出)的双向队列,用户存储等待获取资源的线程;每个节点代表一个等待线程,包含引用、等待状态等信息
- 模板方法设计模式:
- AQS是一个抽象类,定义了获取和释放资源的方法(如acquire(),release())。
- 具体的同步器需要实现tryAcquire()和tryRelease()方法,定义如何获取和释放资源
- 资源共享模式:
- 优点:
- 比较灵活,可以自由的实现各种同步工具。
- 使用cas操作和volatile变量,避免锁的开销
- 可以根据AQS实现自定义的同步器
- 缺点:
- 实现较为复杂,需要深入理解其内部逻辑
- 调试比较困难,尤其是涉及并发的场景
总结:AQS是java并发包的核心框架,通过状态管理、等待队列和模板方法设计模式,提供灵活、高性能的同步机制。
在线程池中,execute()和submit()有什么区别?
这是线程池中提交任务的两种方法
- 区别:
- execute()没有返回值,直接返回void类型,但submit()会返回Future类型,可以通过Future.get()来获取异常信息
- execute()适用于不需要返回结果的简单业务场景,submit()适用于需要获取结果或处理异常的任务。
分布式问题
如何设计一个秒杀系统
- 秒杀系统的常见问题:
- 频繁访问数据库,导致服务挂掉
- 缓存穿透、缓存击穿和缓存雪崩
- 链接暴露,被提前秒杀
- 流量过大
- 超卖
- 解决方案:
- 服务单一职责,新建秒杀数据库,就算真的挂掉也不影响其他服务
- 缓存问题方案见redis
- 链接被别人以F12的方式提前搞走,进行提前秒杀,通过加密让前端获取动态链接,保证链接不会提前暴露
- 流量过大可以采用redis集群,哨兵机制,已经mq消峰
- 数据加载到redis中,通过redis事务的原子性来控制超卖
- 其他手段:秒杀前前端按钮置灰,不允许频繁点击,连续点击多次就提示其等等再点。
- 解决超卖问题:悲观锁,乐观锁,队列形式:做订单队列。一个一个处理,达到阈值就显示卖完。用redis的原子自增操作:讲操作改为先申请后确认,再有在redis处理完后再进行异步写库。有可能导致数据一致和少卖的情况(申请后不确认付款,也会占用名额)。
- 京东库存系统关于超卖的处理:首先设置预占库存,若可用库存不足,则不再对外销售,其次用乐观锁,在写库时查版本号,最后有安全库存的设置。保证有少量超卖也无所谓。
哨兵机制
- 在redis集群中会有主服务器和从服务器,若其中一个服务器出现问题,则需要进行故障转移,如何判断,转移,就需要用到哨兵机制
- 哨兵机制主要执行以下三个任务:
- 监控:定期向主从服务器发送请求,看是否能正常返回,不可以就认为可能出现问题
- 提醒:当哨兵监控到服务器问题后,会向管理员发送故障提醒
- )自动故障转移:当主服务器出现问题,哨兵会自动选举新的主服务器,当客户端请求过来时,返回新的请求地址。
如果服务器崩溃了,在不影响客户使用的前提下,应该怎么处理
- 高可用架构,尽可能减少对客户的影响
- 负载均衡:将流量发送到不同的服务器
- 集群部署:确保多台服务器同时运行
- 自动故障转移:使用主从复制或分布式一致性协议,实现自动故障转移
- 监控告警:监控CPU、内存、磁盘、网络等,设置阈值,达到就告警
- 定期对服务器进行健康检查
- 崩溃后快速恢复:自动重启、故障隔离、数据恢复
- 容灾恢复:备份策略、容灾演练、多区域部署
- 客户端容错处理:重试机制、缓存数据、优雅降级(友好的错误提示)
- 日志与故障分析:日志收集、故障分析、改进措施
算法相关
雪花算法:
雪花算法是Twitter公司发明的一种算法,主要目的是解决在分布式环境下,ID怎样生成的问题
- 分布式ID生成规则硬性要求:全局唯一:不能出现重复的ID号,单调递增
- 分布式ID生成可用性要求:
- 高可用:发布一个获取分布式ID的请求,服务器就要保证99.999%的情况下给创建一个全局唯一的分布式ID。
- 低延迟:发布一个获取分布式ID的请求,要快,急速。
- 高QPS:假如并发一口气10万个创建分布式ID请求同时杀过来,服务器要顶得住并且成功创建10万个分布式ID
经测试snowflake每秒能生成26万个自增可排序的ID。snowflake生成的ID结果是一个64bit大小的整数,为一个Long型(转换成字符串后长度最多19)。分布式系统内不会产生ID碰撞(datacenter和workerId作区分)并且效率高。不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也非常高,可以根据自身业务分配bit位,非常灵活。