☀面向对象编程有哪些特性
面向对象编程主要有三点特性,分别是封装、继承和多态
- 封装是指将属性和行为绑定在一起,并通过使用private修饰符对外隐藏内部实现细节,
- 继承是指允许子类继承父类的属性和方法,实现代码复用,同时支持拓展和重写父类行为
- 多态是指同一方法根据对象的实际类型表现出不同的行为,具体表现为编译时多态和运行时多态,编译时多态一般有方法重载,运行时多态通过动态绑定实现,父类引用指向子类对象,实际执行的方法取决于具体的对象类型
☀面向对象和面向过程区别
- 面向对象以对象为核心,通过抽象现实世界的实体作为类,将属性和方法捆绑到一起,面向过程以函数为核心,将程序分解为一系列步骤,按序执行
- 面向对象通过封装、继承、多态三大特性实现高级抽象,面向过程通过函数分解任务,数据通过参数传递
- 面向对象适用于企业级应用的复杂系统,可以降低模块间的耦合性,提高拓展性和可维护性。面向过程适用于简单任务或者高性能场景
面向对象的设计原则
- 单一职责,每个类都应专注于单一功能,避免职责过多
- 开闭原则,模块应该对功能拓展开放,而对修改关闭
- 里氏替换原则,任何父类都可以被其子类所替换
- 依赖倒置,模块不应该依赖于具体实现,而是应该依赖于抽象(针对接口编程)
- 接口隔离原则,接口应该尽可能精简,模块不应该依赖它不需要的接口
☀接口、普通类和抽象类有什么区别和共同点
- 接口可以被多实现,抽象类和普通类只能被单继承
- 接口只能有抽象方法(1.8之后可以有默认方法和静态方法),抽像类和普通类可以有静态方法和普通方法
- 接口只能有常量,默认public static final修饰,而普通类和抽象类可以有实例变量、静态变量
- 接口不能有构造器,不能实例化,抽象类可以有构造器,但不能用来创建对象实例,普通类可以有构造器可以创建对象实例
静态变量和静态方法,非静态内部类和静态内部类
- 静态变量和静态方法都属于类,通过static修饰,静态方法可以通过Class.变量或Class.方法调用,所有类实例共享一份静态变量
- 静态内部类只可以访问外部类的静态成员,非静态内部类可以访问外部类的所有成员
- 静态内部类可以直接通过外部类名实例化,非静态内部类依赖于对象实例,需要通过外部对象实例创建
☀深拷贝和浅拷贝区别了解吗
- 浅拷贝会复制基本数据类型的值,引用类型则拷贝内存地址,导致新旧引用类型共享同一引用数据,修改任意对象的引用类型字段会直接影响另一对象
- 深拷贝会复制基本数据类型的值,并且会递归复制对象内部所有的引用类型数据,新旧引用类型均指向不同的引用数据,彼此修改互不影响
☀如何实现浅拷贝和深拷贝
- 浅拷贝可以通过实现Cloneable接口和clone方法、手动复制
- 深拷贝可以通过手动递归复制、通过将对象序列化为字节流再反序列化为对象
☀int和Integer的区别
- int是基本数据类型,直接存储数值,占用4字节,Integer是引用类型,属于int的包装类,存储对象引用,它是一个对象,还需要额外的内存开销(如对象头和字段信息等)
- int是基本数据类型可以声明和使用,而Integer必须实例化之后才能使用,并且提供了额外的功能,比如支持泛型、类方法、缓存、自动装箱拆箱、序列化等机制
- int默认值为0,Integer初始值为null
为什么有了int还需要Integer
- Java的泛型和集合等特性要求需要使用对象,而不能使用基本数据类型,更符合面向对象编程
- int的默认值是0,无法判别是赋值为0还是没有赋值,Integer的默认值是null,可以明确表示数据缺失状态
- 并且提供了额外的功能,比如支持泛型、类方法、缓存优化、自动装箱拆箱、序列化等机制
☀什么是自动拆箱/装箱
- 自动拆箱,指将包装类型自动转化为基本数据类型,底层使用Integer.intValue()
- 自动装箱,指将基本数据类型自动转化为包装类型,底层使用Integer.valueOf()。如果是在[-128,127]缓存范围内,则直接使用缓存对象,否则创建新实例
☀重载和重写的区别
- 重载的作用是使用相同的方法名不同的参数列表实现不同的功能,重写的作用是子类继承父类,重新定义父类方法
- 重载和重写都需要相同的方法名
- 重载必须要求不同的参数列表,重写必须要相同的参数列表
- 重载的返回类型可以不同,重写的返回类型必须是父类或者是父类的子类
- 重载的返回修饰符可以不同,重写返回修饰符范围必须比父类大
- 重载是编译时多态,即编译时期决定调用哪个方法,重写是运行时多态,运行时动态绑定,根据对象实际类型决定调用哪个方法
☀什么是泛型,有什么作用
- 泛型是一种参数化类型的机制,允许在定义类、接口或方式使用类型参数
- 泛型在编译阶段会强制检查类型约束,增强了代码的安全性,避免因类型转换错误导致的ClassCastException等运行时异常问题,
- 泛型减少了显式地类型转换的需求,提高了代码的可读性和维护性
- 同时泛型还可以通过通配符等方式定义泛型类、泛型方法或泛型接口,实现适配多种数据类型的逻辑代码
- Java通过类型擦除机制实现泛型,即泛型会在编译期擦除具体的类型信息,并替换为原生类型,确保与旧版本非泛型代码兼容
☀什么是反射,有哪些应用场景
- 反射是java提供的一种在运行时动态访问类的类名、方法、字段、构造器的一种机制,并且还可以通过设置访问权限来访问具体对象的私有成员
- 其核心原理是JVM在类加载时生成对应的Class对象,该对象存储了类的完整结构信息
- 应用场景主要有Spring通过反射实现依赖注入和Bean实例化,AOP、Spring声明式事务通过反射机制动态生成代理类,结合反射在运行时动态读取注解信息并执行相应逻辑
什么是注解,注解的作用域
- 注解是java提供的一种为代码添加元数据的语法结构,用于描述类、方法、字段等元素的附加信息,注解本身不直接影响程序逻辑,但可以通过编译器、运行时反射机制等方式进行处理
- Target指定作用域,可以选择标记在类ElementType.TYPE、方法ElementType.METHOD、属性EelementType.FIELD上
- Retention指定生命周期,注解在编译期RetentionPolicy.SOURCE、类加载期RetentionPolicy.CLASS、运行期时RetentionPolicy.RUNTIME生效
创建对象有哪些方式
- 通过new关键字调用类的构造器实例化对象
- 通过反射调用类的newInstance创建对象实例,通过反射获取类的构造器Constructor.newInstance创建对象
- 类实现Cloneable方法,并重写clone方法
- 通过将序列化的字节流通过反序列化ObjectInputStream.readObject重建对象
如何实现序列化
- 通过实现Serializable接口, 并声明serialVersionUID显式定义序列化版本标识符,避免类结构变更导致反序列化失败,如果有变量不想序列化,可以通过transient关键字修饰该变量
- 再通过ObjectOutputStream.writeObject和ObjectInputStream.readObject实现对象的序列化和反序列化
- 还可以通过Json序列化(jackson,fastjson)和ProtoBuff序列化
☀介绍一下Java的异常体系
- Java的异常体系基于Throwable类作为基类,分为两个核心子类Error和Exception
- Error表示严重的系统错误,程序无法处理且无需捕获,例如内存溢出OutOfMemoryError和栈溢出StackOverflowError
- Exception表示程序运行时可以捕获和处理的问题,主要可以分为运行时异常RuntimeException和编译时异常CheckedException
- 常见的运行时异常有空指针异常NullPointerException、数组越界ArrayIndexOutOfBoundsException、类型转化错误ClassCastException
- 编译时异常要求开发者必须通过try-catch捕获或在方法声明中用throws显式抛出,或者编译不通过,常见的有IOException等
☀异常处理方式,介绍下关键词
- Java通过try-catch-finally捕获异常,使用try包裹可能抛出异常的代码块,使用catch捕获具体的异常并处理,使用finally执行最终过程例如关闭资源
- 使用throw关键字可以主动抛出异常
- 使用throws关键字在方法上声明潜在异常,将异常处理传递给上层调用者
- 可以使用try-with-resources语法自动管理资源
什么时候不会执行finally
- 在进入try、catch或者finally块时触发了jvm强制退出,如System.exit(0)
- 异常发生在try-catch-finally块之外并抛出,则try-catch-finally块就不会执行
- 系统发生故障,如断电、操作系统崩溃、JVM崩溃
☀介绍一下final关键字
- final修饰基础数据类型,代表不可变的常量
- final修改引用数据类型,代表引用地址不可变,但对象内部内容可以变
- final修饰类,代表类不可以被继承
- final修饰方法,代表方法不能被子类重写
☀==和equals的区别
- 基本数据数据类型使用比较的是值是否相等,引用数据类型使用比较的是引用内存地址是否相同
- equals应用于引用数据类型,默认行为是比较引用地址是否相同,但在一些类中equals被重写用于比较内容是否相同
为什么重写equals也需要重写hashcode
- 在Java中,如果两个对象使用equals判定为相同,它们的hashcode的值也必须相同,如果改写了equals,不重写hashcode,可能会导致相等的对象拥有不同的哈希值,会导致使用哈希特性的一些集合如hashMap在使用哈希值时无法正确去重或检索对象
☀请说说String、StringBuffer、StringBuilder三者区别
- String是不可变的,其底层通过final修饰字符数据,修改操作如拼接、替换都会生成新对象,内存开销大;StringBuilder和StringBuffer底层使用非final修饰的动态字符数据,支持在原对象上修改,避免频繁创建对象开销
- String的不可变性天然支持线程安全,StringBuilder是非线程安全的,StringBuffer是线程安全的,所有的方法都使用synchronized关键字加锁,使用同步机制会带来性能消耗,因此StringBuilder在单线程场景下性能更优,所以在多线程场景下使用StringBuffer,单线程处理使用StringBuilder
BIO、NIO、AIO区别
- BIO是一种同步阻塞I/O模型,线程发起I/O操作后会一直阻塞等待完成,采用一个请求对应一个线程的机制,适合低并发场景
- NIO是一种同步非阻塞I/O模型,通过多路复用器selector实现单线程管理多个通道channel,基于缓冲区buffer和通道进行读写,通过轮询的方式检查就绪事件,避免了线程阻塞,适用于高并发场景
- AIO是一种异步非阻塞I/O模型,由操作系统完成I/O操作后通过回调机制通知应用线程,基于事件驱动,无需主动轮询,适用于连接数多且长时操作场景
Java8有哪些新特性
- Lambda表达式,简化匿名表达式的写法,实现原理是编译时类中会生成一个静态方法,静态方法的代码就是Lambda表达式中的代码,然后会在类中生成一个匿名内部类实现接口,并重写接口的抽象方法,方法内部会去调用上一步的静态方法,从而实现Lambda代码执行
- Stream流,提供声明式数据流处理,支持链式操作、并行计算、复杂集合处理如过滤器、映射、聚合,例如filter、map、collect
- Optional类,通过封装可能为空的对象,减少空指针异常,Optional.ofNullable().orElse
- 接口类额外的默认方法和静态方法
- 全新的日期和时间Api LocalDate、LocalTime、LocalDateTime
- Completable类支持异步编程
Stream流有哪些操作
- Stream流操作可以分为中间操作和终端操作
- 中间操作,filter,map,flatMap, sorted,distinct, limit,skip,peek
- 终端操作,collect,count, reduce, findAny,findFirst,allMatch, anyMatch, noneMatch, toArray,forEach, min,max,
Future和CompletableFuture的区别
- Future是ExecutorService执行后的一个返回结果,可以通过.get()阻塞直接结果返回,只适用于简单的异步任务,无法直接组合多个任务结果
- Completable在处理复杂异步任务比Future提供了更加丰富的功能,比如异步回调方法thenApply、thenAccept,组合任务操作thenCombine,all of,any of等,还提供了额外的异常处理方式exceptionally、handle
☀常见的集合有哪些
- 在Java中常用的集合分为单列集合Collection和双列集合Map
- List,ArrayList 动态数据,LinkedList 双向链表
- Set,HashSet 哈希表,TreeSet 红黑树
- Queue,ArrayDeque 双向队列,PriorityQueue 堆
- Map,HashMap 数组+链表+红黑树,TreeMap 红黑树,LinkedHashMap 哈希表+双向链表
为什么不使用数组,要使用集合
- 集合支持自动扩容机制,会自动扩大容量,数组的长度通常是固定的,处理动态数据不方便
- 集合框架提供了多样化的数据结构如HashSet、TreeSet、HashMap等,实现去重、排序、键值对映射,比数组的有限操作功能更加丰富
☀ArrayList和LinkedList的区别
- 存储方面,动态数组在内存中连续存储,链表每个节点包含前驱和后继指针,内存空间不连续
- 访问效率方面,动态数组提供了快速随机访问机制,链表的随机访问需要遍历整个链表
- 修改效率方面,动态数组修改需要移动后续元素,时间复杂度是O(N),链表修改头尾效率,时间复杂度是O(1),修改中间位置时间复杂度则都是On
- 扩容方面,动态数组当容量不足时,会额外扩容1.5倍,链表每个节点需要额外存储2个指针,内存开销更大,频繁增删会引发内存碎片
- 数组更适用于高频随机访问,链表适用于头尾频繁操作的插入删除场景
ArrayList的线程不安全问题
- ArrayList底层的数组操作缺乏同步机制,在多线程修改时会导致数据不一致或者运行异常
- 当多个线程同时写入时,由于size++不是原子操作,可能会覆盖同一索引位置的值,因此会导致数据丢失或出现null值
- 当多个线程同时写入时,多个线程都先判断了没有发生越界的情况,然后其中一个线程放入元素,此时集合满了,另外一个线程又放入元素,这时候就会出现越界异常
- 可以通过Collections.synchronizedList包裹list,或改用并发安全集合CopyOnWriteArrayList或Vector
☀知道哪些线程安全的集合
- List,CopyOnWriteList,Vector
- Set,ConcurrentHashSet
- Queue,ArrayBlockingQueue,LinkedBlockingQueue
- Map,ConcurrentHashMap、HashTable
- 或者使用Collections.synchronizedList、Collections.synchronizedMap、Collections.synchronizedSet
☀HashMap的原理是什么
- hashMap底层是构建了一个哈希表的数组结构,通过哈希函数和高低位异或进行扰动处理,计算键的哈希值,再通过hash&(n-1)映射到数组中的某个哈希桶中
- 如果位置不为空,则直接存入,如果发生了哈希冲突,就通过链表的方式串联起来,如果链表的长度超过了8,并且数组容量大于64,就需要转化为红黑树提升查找效率,如果红黑树内部的节点数量小于等于6,就还需要重新退化回链表
- 如果数组元素的容量超过了数组容量*负载因子时,就会触发数组扩容机制,就还需要将数组容量扩容为原先的两倍,数组当中的元素需要重哈希到新位置中,元素的新位置为哈希值&(newCap-1),如果元素的哈希值&oldCap,如果高位是1,就会被放入新位置(原位置+旧容量)中,否则位置不变,保证了元素哈希分布均匀性
☀HashMap为什么采用红黑树不采用B+树和平衡二叉树
- 平衡二叉树AVL通过严格的高度平衡即平衡因子不大于1,实现时间复杂度Ologn的查找,在涉及修改时会导致频繁调整节点,平均旋转次数会高于红黑树;而采用红黑树这种相对平衡要求较低的自平衡二叉树,更适合在写场景多的情况下使用
- B+树是一种多路查找树,它是一颗矮胖树,通过减少树高来减少I/O次数,但节点存储了大量数据指针,更适用于面向磁盘存储优化;而红黑树节点只需要存储键值、父子指针、颜色等,内存结构更加紧凑,更适合在内存场景下使用
哈希冲突的解决方法有哪些
- 链地址法,通过将哈希冲突的元素使用链表串联起来,链表长度过长可以转化为红黑树
- 开放地址法,通过线性探测法或者二次探测法,寻找空着的桶
- 再哈希法,再使用不同的哈希函数重新计算得到新的位置
☀HashMap的put过程,如何扩容的
- 首先根据键计算哈希值,过程中会将高低位的哈希值进行异或进行扰动处理
- 根据哈希值(hash & n - 1)找到具体的桶下标
- 如果桶是空的,直接插入新节点,如果桶不是空的,就需要遍历链表的元素,如果哈希值相等并且值相同就覆盖,否则就插入到链尾
- 如果数组大于等于64,并且链表的长度大于8,就需要将链表转化为红黑树优化查询效率
- 如果哈希表内的元素数量超过了数组容量*负载因子的大小,就还需要将数组容量扩容为原先的两倍,数组当中的元素需要重哈希到新位置中,元素的新位置为哈希值&(newCap-1),如果元素的哈希值&oldCap,如果高位是1,就会被放入新位置(原位置+旧容量)中,否则位置不变,保证了元素哈希分布均匀性
HashMap为什么采用2倍扩容
- 位运算优化索引计算,当容量保持为2的幂次时,各位桶位置的计算可以通过位运算替代取模运算,计算效率更高
- 降低扩容时元素的迁移成本,新位置一种是保持原索引位置,或迁移至原位置+oldCap,差异在于hash&oldCap高位是否为1即可判断是否需要偏移,避免了元素的全量哈希计算
- 通过保持2的幂次时,结合计算hash时的高低位异或扰动机制,可以有效地减少哈希冲突,提升哈希分布的均匀性
☀JDK 1.7和1.8中HashMap的区别
- JDK1.7之前HashMap的数据结构使用数组+链表,1.8之后使用数组+链表+红黑树,提升查询效率为Ologn
- JDK1.7之前扩容时头插法,JDK1.8使用尾插法,改进并发时插入导致的死循环问题
- 哈希值计算的变化,JDK1.8将高位哈希值高低位进行异或进行扰动处理,减少哈希冲突概率,使得哈希分布更加均匀
- 扩容时元素重哈希机制发生变化,JDK1.7所有元素需要全量计算哈希值并迁移,1.8元素通过高位掩码(hash&oldCap)判断高位是1还是0判断是否移动到新位置,新位置=原先位置+oldCap
☀HashMap的线程不安全问题
- 1.7的hashMap,当多个元素并发插入时,可能会因为头插法导致链表反转形成环形结构,最终造成死循环的问题。例如线程A先插入A,同时A触发了扩容,需要重新进行元素移动,紧接着线程B插入B,此时由于是头插法,此时内部链表是B->A的,而由于A的视角头结点仍然是A,但实际上是B,此时进行元素的重新分配会使用链表反转就可能会形成A->B->A的情况
- 当多个线程同时执行put时,可能会导致元素覆盖问题,两个元素值同时放入一个空桶时,后插入的元素会覆盖前一个元素,导致数据丢失
- 修改操作的中间状态对其他线程不可见,例如size++等自增操作不是原子性的,多线程并发修改可能导致size数量小于实际值
讲讲HashSet的底层原理
- HashSet内部基于HashMap实现的,具体元素值存储在key中,value默认常量Object放入
- HashSet元素值的唯一性是通过HashMap的键值的唯一性保证的
并发和并行的区别
- 并发指多个任务在一段时间内交替执行,通过时间片轮转和上下文切换的方式实现
- 并行指多个任务在一段时间内同时执行,需要依赖多核CPU在物理上并行处理
同步和异步的区别
- 同步指多个任务需要按照一定的顺序规则执行,前一个任务未完成,后续任务会处于阻塞等待状态
- 异步指允许任务并发执行,发起异步调用无需等待结果即可继续执行后续操作,异步执行的结果可以通过回调、通知等方式
☀说一下Java内存模型JMM
- Java内存模型定义了多线程环境下共享变量的访问规则,JMM可以分为主内存和工作内存,所有共享变量存储于主内存,线程操作变量时需将主内存数据拷贝到工作内存副本,修改后再同步回主内存
- JMM的核心特性主要有三点,第一个是原子性,通过synchronized关键词或者锁机制保证操作的不可分割性;第二个是可见性,通过volatile关键字强制线程从主内存读取最新值,并通过内存屏障禁止指令重排序,同时synchronized锁释放机制也会强制刷新工作内存到主内存;第三个是有序性,通过happens-before规则约束指令执行顺序,保证程序运行的有序性
☀说一下volatile关键字的作用,底层如何实现
- volatile关键字修饰的变量可以保证变量的可见性,当变量修改时,会将新值刷新到主内存,并强制其他线程从主内存重新加载该变量的最新值
- volatile关键字通过插入内存屏障禁止指令重排序,写操作后会插入写写屏障(防止将volatile写重排序到普通写之前),写读屏障(防止将volatile写重排序到volatile读之后),读操作后会插入读读屏障(防止将volatile读重排序到普通读之后),读写屏障(防止将volatile读重排序到普通写之后)
介绍一下happens-before和as if serial原则
- happens-before原则,是JMM用于确保多线程操作中的可见性和有序性的同步规则,核心规则有,单线程内代码按顺序执行,锁释放先于后续的锁获取,volatile变量写操作先于volatile变量的读操作,传递性规则(A先于B,B先于C,则A先于C)
- as if serial原则针对单线程程序的执行优化,允许对指令重排序并且不破坏数据依赖性,但必须保证重排序后的执行结果与原先执行结果一致,如果C依赖A,C依赖B,那么C就一定不会排序到AB之前
☀线程的创建方式
- 通过继承Thread类,重写run方法,受到单继承限制
- 通过实现Runnable接口的run方法,在传递给Thread进行启动,避免了单继承的限制
- 通过实现Callable类,允许任务有返回值并抛出异常,配合FutureTask使用
☀说说线程的生命周期
- NEW,创建,线程对象被创建仅分配了内存资源但未启动,未调用start方法
- RUNNABLE,可运行,调用start方法进入该状态,线程等待CPU时间片或正在运行,对应就绪或运行
- BLOCKED,阻塞,线程尝试获取同步锁失败而阻塞等待,争抢到锁后恢复到RUNNABLE
- WAITING,等待,线程调用wait,join或LockSupport.park进入此状态
- TIMED_WAITING,限时等待,线程调用sleep,wait(time),join(time)等,超时恢复到RUNNABLE
- TERMINATED,终止态,线程执行完run方法或抛出异常进入此状态,生命周期结束
sleep和wait的区别
- 锁的释放,使用sleep命令不会释放锁,线程在休眠时期仍然会占有锁,使用wait命令会主动释放当前线程持有的对象锁
- 唤醒机制,sleep只需要等待休眠时候就会恢复,wait命令需要搭配notify或notifyAll使用
- 调用方式,wait命令必须在同步代码块或方法中使用,否则会抛出IllegalMonitorStateException异常,使用sleep命令可以在任何地方调用
- 线程状态,使用sleep使线程状态进入TIMED_WAITING状态,使用wait命令使线程进入WAITING或TIMED_WAITING状态
blocked和waiting状态的区别
- 触发机制,blocked是因线程为竞争到锁资源而阻塞,Waiting因线程主动调用wait、join、LockSupport.park方法后进入等待
- 锁的释放,blocked是因为线程未竞争到锁资源而导致阻塞会持续尝试获取锁,WAITING的线程会主动释放持有的锁,允许其他线程获取锁
- 唤醒机制,blocked状态在锁可用时由系统自动调度恢复为RUNNABLE,waiting状态需要外部notify或者notifyall显式唤醒,否则会一直等待下去
线程之间的通信方式
- 共享变量与同步机制,使用synchronized关键字或Lock接口保证对共享变量的同步访问
- 等待、通知机制,使用wait、notify、notifyall方式,线程可在共享对象上等待或被唤醒
- 条件变量,使用newCondition创建条件变量,使用await和signal提供等待或通知控制
- 阻塞队列,实现生产者-消费者模型,通过put和take实现自动阻塞或唤醒线程
- 同步工具类,使用Countdownlatch、cyclicBarrier、semaphore等工具协调线程间顺序
如何停止一个线程
- 循环检查标志位,声明一个volatile标志的布尔变量作为标志位,线程运行时循环检查,当外部将状态修改则退出
- 中断机制,调用线程的interrupt方法发送中断请求,interrupt不会立刻终止线程而只是设置中断标志位,线程需要手动检查中断状态isInterrupted,或触发可中断操作如sleep、wait、join响应中断,收到中断请求后会抛出InterruptedException
- 使用线程池提交任务,通过Future.cancel停止线程,依赖于中断机制
什么是线程上下文切换
- 线程上下文切换指CPU从一个线程切换到另一个线程,操作系统会保存当前线程的上下文(寄存器、程序计数器、栈信息等)到TCB,然后从内存中加载目标线程的上下文,恢复线程执行
- 触发上下文切换的场景包括线程时间片耗尽、线程调用wait、sleep方法主动让出CPU、触发系统中断如I/O操作后线程被挂起
线程上下文切换的影响,如何减少
- 线程上下文切换的影响,主要有保存和恢复上下文需要消耗额外的CPU执行时间,频繁的上下文切换会导致系统资源的竞争加剧,切换后新线程的局部性原理失效,导致CPU缓存命中率下降,频繁访问主内存
- 如何减少线程上下文切换的影响,合理控制线程数量,避免创建过多线程,使用CAS等无锁机制减少线程间的锁竞争,使用轻量级的协程(由jvm管理的虚拟线程,上下文切换开销少),使用异步编程模型Completable减少线程因等待资源而阻塞的情况
JUC包下常用并发工具类
- 并发集合类,ConcurrentHashMap,CopyOnWriteArrayList,ArrayBlockingQueue
- 线程同步类,CountDownLatch,semaphore,CyclicBarrier
- 原子操作类,AtomicInteger,AtomicReference, AtomicStampedReference
你是如何使用原子操作类的
- 常用方法如incrementAndGet实现自增,addAndGet实现累加,compareAndSet实现条件更新,这些方法底层都基于volatile和CAS机制实现,保证操作的原子性和可见性,避免了锁的开销
☀怎么保证多线程安全
- 保证共享变量的可见性,需要使用volatile关键字修饰变量,保证变量的可见性和防止指令重排序
- 使用同步机制,使用synchronized关键字修饰代码块或方法,或者使用显式加锁解锁的ReentrantLock实现更灵活的锁控制,确保同一时刻仅有一个线程访问共享变量
- 使用线程局部变量,比如使用ThreadLocal为每个线程创建独立的局部变量,保证线程间的资源隔离
- 使用线程安全的数据结构,如ConcurrentHashMap、CopyOnWriteArrayList、ArrayBlockingQueue等并发容器
- 使用原子类,使用AtomicInteger、AtomicReference等原子类保证操作的原子性
☀Java中有哪些锁,在哪些场景下使用
- 乐观锁和悲观锁,乐观锁通过CAS检测数据是否被其他线程修改,无需显示加锁,适用于读多写少的低竞争环境下使用;悲观锁,通过独占资源实现同步,适用于写多读少的高竞争情况下使用,比如synchronized锁和ReentrantLock
- 公平锁和非公平锁,公平锁按照线程顺序分配锁保证公平性但性能低,非公平锁允许插队获取锁,性能更高
- 可重入锁,如synchronized锁和ReentrantLock,运行同一线程多次获取同一锁
- 读写锁,满足读读共享,读写互斥,写写互斥,允许多线程并发读取资源,适用读多写少的情景下使用,比如ReentrantReadWriteLock
- 自旋锁,线程通过循环检查锁是否可用,避免线程挂起,适用于锁占用时间极短的场景
什么是线程池
- 线程池是一种管理和复用线程的并发编程机制,核心思想是通过预先创建一组线程并维护其生命周期。线程池的主要作用是,可以通过复用线程减少频繁创建和销毁的开销;任务到达时无需等待线程创建即可立即执行;控制最大线程数防止系统过载;通过任务队列和拒绝策略统一调用管理任务;
☀线程池的七大参数
- 核心线程数,线程池中常驻的线程数量,即使空闲也不会被回收
- 最大线程数,允许线程池创建的最大线程数,当任务队列已满并且核心线程繁忙,会创建非核心线程直至最大线程数
- 任务队列,存储待执行任务的阻塞队列,缓冲超出核心线程处理能力的任务
- 线程空闲存活时间,非核心线程的空闲存活时间,超过时间后会被回收
- 时间单位,定义线程存活时间的粒度
- 线程工厂,用于创建新线程的工厂类
- 拒绝策略,当线程池的线程和任务队列均达到上限时对新任务执行的策略
☀线程池工作流程
- 任务提交时当线程数小于核心线程数,优先创建核心线程处理任务
- 当线程数等于核心线程数,任务会存入工作队列
- 如果工作队列满了,并且线程数小于最大线程数,线程池会创建非核心线程处理新提交的任务,这类线程超过空闲存活时间后会被回收
- 如果工作队列和线程数均达到上限,就会触发拒绝策略
☀Executors创建的线程池有哪些种类
- FixedThreadPool,核心线程数和最大线程数相同为n,并且采用无界的LinkedBlockingQueue,可能会导致内存溢出
- SingledThreadPool,核心线程数和最大线程数相同为1,并且采用无界的LinkedBlockingQueue,适用于单线程操作
- CachedThreadPool,核心线程数为0,最大线程数为Integer.MAX_VALUE,工作队列采用SynchronousQueue直接移交任务,适用于短期高并发任务,高负载时可能创建过多线程导致耗尽资源
- ScheduledThreadPool,核心线程数为n,最大线程数为Integer.MAX_VALUE,工作队列采用DelayedWorkQueue,可以延迟执行或者周期性执行任务
☀为什么禁止使用Executors创建线程池,使用线程池应该注意哪些问题
- 如果使用无界队列,任务堆积时可能会导致内存溢出
- 如果最大线程数设置过大,极端情况下会导致线程数量激增,耗尽CPU或内存资源
- Executors默认的队列容量、最大线程数、以及拒绝策略等,无法根据业务需求进行调整,具有局限性
- 因此使用ThreadPoolExecutor手动创建线程池,结合具体业务场景配置参数、队列及拒绝策略是保障系统稳定性和性能的一种最佳实践
- 使用线程池的注意事项,核心线程数的选择(I/O密集型*2、CPU密集型+1)、队列选择(优先选择有界,防止内存溢出)、拒绝策略的选择(根据对任务是否可丢弃的业务场景设置)、异常的捕获(try-catch或者重写afterExecute)、线程池的关闭(shutdown等待任务完成后完毕)
如何处理线程池异常
- 通过try-catch包裹业务逻辑捕获异常
- 使用ExecutorService过程中使用submit后,使用Future.get获取数据后捕获异常
- 自定义ThreadFactory为线程池每个线程设置UncatchExceptionHandler异常处理器
- 自定义ThreadPoolExecutor并重写afterExecute方法(该方法在任务执行后触发,无论是否抛出异常,类似于handle)
- 在使用CompletableFuture过程中使用exceptionally和handle处理
线程池如何关闭
- 使用shutdown,会将线程的状态设置为shutdown,停止接受新任务,会中断所有空闲线程,正在执行任务的线程不会被停止,并等待所有已提交的任务执行完成后(包括队列中等待的任务),然后停止线程池
- shutdownnow,会将线程池的状态设置为stop,停止接受新任务,会强行调用Thread.interrupt所有工作线程,但正在执行的任务是否能中断取决于任务是否响应中断,如果任务没有try catch interruptException就还是会继续执行,然后停止线程池
线程池常用阻塞队列
- ArrayBlockingQueue,基于数组实现的有界队列,采用FIFO,需要控制内存使用的场景
- LinkedBlockingQueue,基于链表实现的无界队列,可能会导致任务堆积造成OOM
- SynchronousQueue,一种不存储元素直接移交的队列,使得线程池优先创建线程,适用于短期高并发场景,可能会导致线程创建过多导致资源耗尽
- PriorityBlockingQueue,一种支持优先级排序的无界队列,适用于需要处理高优先级任务的情景
- DelayedWorkQueue,基于堆结构的延迟队列,任务按照延迟时间排序,任务只达到指定延迟后才会取出执行
☀线程池有哪些拒绝策略
- AbortPolicy,默认策略,当任务无法处理时抛出RejectExecutionException异常
- CallerRunsPolicy,由投递任务的线程直接执行被拒绝的任务,可能会导致主线程提交任务速度下降,适用于任务不能丢失且接受延迟的场景
- DiscardPolicy,丢弃任务策略,不会抛出异常,适用于允许任务丢失的场景
- DiscardOldestPolicy,抛弃队列中最老未处理的任务策略
☀CurrentHashMap原理
- 1.8之前数据结构 使用segment+数组+链表,1.8之后使用数组+链表+红黑树
- 在线程安全方面,1.8之前采用segment分段锁,由于segment默认只有16个,对每个段独立上锁因此最大并发数为16,1.8之后如果得到下标的哈希桶里面为空,则使用CAS方式放入,如果桶里面有元素对哈希桶使用synchronized上锁,实现更细粒度的并发控制,最大并发数就是桶的数量,吞吐量更高
CopyOnWriteArrayList原理
- CopyOnWriteArrayList就是基于写时复制机制实现的线程安全列表,核心原理是读写分离和数据副本隔离
- 修改操作时,通过reentrantlock独占锁保证线程安全,每次修改都会复制原数组生成新副本,然后在这个新副本上进行修改操作,再通过volatile替换原数组引用保证可见性,然后解锁
- 读操作时,会直接访问当前数据引用,不会上锁,因此允许并发读,如果读操作始终基于某一时刻数组的快照,可能会读取到旧数据
介绍一下BlockingQueue
- BlockingQueue是一个接口,提供了线程安全的队列操作,并支持在队列空和满时阻塞线程的能力,例如put往满队列时插入元素时,会阻塞直至队列有空间,当从空队列中take时,会阻塞等待新元素到达
- ArrayBlockingQueue,基于数组实现的有界队列
- LinkedBlockingQueue,基于链表实现的无界队列
- SynchronousQueue,无缓冲队列,每个插入操作必须等待对应移除操作
- PriorityBlockingQueue,一种支持优先级排序的无界队列
☀什么是线程死锁
- 多个线程由于互相竞争共享资源而导致互相等待,导致所有线程无法继续推进的现象
☀如何预防和避免线程死锁
- 互斥,资源独占性,资源在同一时间只能被一个线程持有
- 持有并等待,线程持有资源不释放,同时请求新资源
- 不可剥夺,资源只能由持有者释放
- 循环等待,线程间形成资源请求的环形链
- 避免死锁需要破坏死锁必要条件的之一即可,例如打破持有并等待条件,就需要让线程一次性获取所有所需资源;消除循环等待,需要更改线程的资源分配顺序,需要统一线程之间的资源分配顺序,比如一个是先A再B,一个先B再A的情况,我们都修改成先A再B
介绍一下银行家算法
- 第一步资源请求检查,需要验证进程的请求资源数量是否小于等于需要的资源数量,并且小于等于可获得的资源数量
- 第二步安全性检查,通过资源请求检查后,系统会试探性地为进程分配资源,并通过寻找安全序列,即一个进程的执行顺序,每个进程都能获得所需资源并释放,来判断系统是否处于安全状态,如果存在安全序列则分配资源,否则拒绝分配请求并回滚
☀如何检测死锁
- 通过top、ps -aux命令、或者jps查看具体的进程PID
- 使用jstack -l 命令查看生成的线程快照,包含线程ID内具体线程栈信息,会明确标注是否发生死锁,以及相互等待锁的线程和阻塞的代码位置
- 使用arthas工具的dashboard或者thread -n查看前几个线程栈信息
☀synchronized底层原理
- synchronzied底层实现原理主要通过对象头中的Mark Word和monitor监视器机制,并结合JVM的锁升级平衡性能与线程安全
- 其中每个Java对象在内存中均包括对象头,其中对象头的Mark Word存储了锁状态、分代年龄、哈希码等信息,当使用synchronized时,JVM会通过修改Mark Word的锁标识位或修改指针等方式来实现锁机制
- 监视器机制主要通过字节码指令monitorenter和monitorexit实现代码块的同步,线程进入同步代码块后,JVM会尝试获取对象的监视器monitor;monitor监视器的内容主要包括owner持有锁的线程,entrylist竞争锁的阻塞线程队列,waitset调用wait后等待状态的线程队列
☀synchronized锁升级
- synchronized锁升级是JVM为了优化多线程竞争场景下的性能设计的一种动态调整策略
- 偏向锁,当第一个线程首次获取锁时,JVM会将对象头的markword记录下当前线程ID,并设置偏向锁标志位,此时无需CAS即可快速获取锁,后续同一线程进入,仅需对比线程ID即可复用锁,避免锁开销
- 轻量级锁,当第二个线程尝试竞争锁时,偏向锁会被撤销,升级为轻量级锁;此时线程会通过CAS自旋尝试将对象头的Mark Word替换为指向线程栈帧中的Lock Record锁记录的指针,并将Lock Record中的owner指针指向对象的Mark Word;如果自旋次数超过阈值10次,或者有第三个竞争线程,就升级为重量级锁
- 重量级锁,此时JVM通过操作系统的互斥量Mutex管理线程的阻塞与唤醒,JVM会将对象头中的Mark Word替换为指向监视器monitor的指针,没有获取到锁的线程进入阻塞队列等待操作系统调度
☀synchronized和ReentrantLock区别
- synchronized可以使用关键字的形式修饰方法或者代码块,无需手动管理锁的释放,ReentrantLock使用显式的lock和unlock方法手动控制锁的生命周期
- synchronized底层通过对象头Monitor Word和Monitor监视器机制实现锁机制,ReentrantLock则是继承了AQS抽象类来实现
- synchronized仅支持非公平锁,而ReentrantLock内部实现了非公平锁、公平锁、读写锁等多种锁,并且额外的功能比如提供了Condition条件变量等待和唤醒线程的机制、超时放弃避免死锁等
- 在使用场景上,synchronized更适用于简单的同步需求,对于需要更细粒度、高竞争的场景下,更适合使用ReentrantLock
☀synchronized如何使用,修饰不同类型方法的区别
- synchronized修饰实例方法,锁对象是当前方法的调用者实例,此时同一对象实例的多个线程访问会互斥
- synchronized修饰静态方法,锁对象是当前类的Class对象,此时所有线程调用该方法都会互斥
- synchronized修饰代码块,可以自定义锁对象,来缩小锁粒度提高并发性能
如何理解可重入锁
- 可重入是Java多线程确保线程安全的重要机制,可重入锁允许同一个线程重复获取同一把锁而不会导致死锁;具体的讲,当一个线程获取锁后,其后续代码需要获取相同的锁无需竞争,而是通过计数器机制记录锁的持有次数,每次加锁时计数器自增,解决时递减,直至计数器为零才完全释放锁
介绍一下AQS的原理
- AQS底层维护了一个volatile变量修饰的state表示资源状态,比如锁的重入次数或许可证的数量,配合CAS实现无锁化的原子状态变更
- 内部还有一个CLH双向队列,专门用于管理竞争资源的线程,当线程获取资源失败时,会被封装为节点加入到队列尾部自旋等待前一个节点唤醒,如果超过自旋次数就使用LockSupport.park阻塞;释放资源时,通过unpakSuccessor唤醒队列中的后续节点
- 子类只需要实现tryAcquire和tryRelease方法来定义资源的获取和释放,就可以实现线程的同步和调度
ReentrantLock实现原理,如何实现公平锁的?
- ReentrantLock实现原理基于AQS抽象类,并且重写了tryAcquire和tryRelease方法,核心是通过维护状态变量state记录锁的持有次数实现可重入和一个CLH双向队列用于管理竞争资源的线程;
- ReentrantLock默认创建非公平锁,加锁时会先尝试CAS操作直接抢占锁,如果成功直接获取锁;如果失败通过acquire方法,在进入CLH队列之前还会尝试跟队列头节点的后继节点进行锁的争抢,如果成功则获取锁;如果失败,则加入到CLH队列尾部自旋尝试等待前驱节点释放锁,如果在有限次自旋中前驱节点没有释放锁,则使用LockSupport.park进行阻塞并等待前驱节点唤醒
- 对于公平锁来说,公平锁的lock方法直接调用acquire,没有非公平锁的CAS的插队步骤,并在tryAcquire方法内会额外判断一下CLH队列是否已经有其他线程节点正在等待获取锁,如果存在,则直接加入到队尾自旋等到前驱节点释放锁,如果在有限次自旋中前驱节点没有释放锁,就使用LockSupport.park进行阻塞并等待前驱节点唤醒,保证了公平锁的FIFO顺序
☀什么是乐观锁,跟悲观锁的区别?
- 乐观锁和悲观锁是两种处理并发冲突的机制,核心区别在于对数据冲突的预期和处理方式
- 乐观锁假设并发冲突概率较低,仅在数据提交时通过版本号或者CAS机制检测冲突,如果版本号匹配或者CAS操作成功就更新数据,否则通过重试或回滚解决冲突;乐观锁通过减少锁竞争,吞吐量更高,但重试可能增加开销,更适用于读多写少的,并发冲突概率低的并发场景
- 悲观锁则假设并发冲突必然发生,通过加锁的方式在操作前独占资源,确保同一时刻只有一个线程可以修改数据;悲观锁因为线程阻塞会导致性能下降,更适合写多读少,并发冲突概率高的并发场景
☀CAS算法有哪些问题
- aba问题,当变量值从A改成B在改成A,此时CAS无法感知中间状态的变化,可能导致逻辑错误;具体解决方案是可以加上版本号Stamp或时间戳标记变量的每次修改
- 循环时间长开销大,自旋CAS在高并发场景下可能因为竞争激烈导致频繁重试,浪费CPU资源;解决方案是降级为锁机制
- CAS无法保证多个共享变量的原子性操作;解决方案是将多个变量封装为一个对象例如通过AtomicReference类,或者使用锁机制管理
☀ThreadLocal的作用和原理,以及会产生什么问题
- ThreadLocal的作用为每个线程提供独立的变量副本,实现线程间的数据隔离,避免多线程下的数据竞争问题
- ThreadLocal的核心原理是每个线程内部维护的ThreadLocalMap数据结构,当调用set命令时,每个线程会将ThreadLocal实例作为Key,具体的值作为value存入ThreadLocalMap中。当get时,则从当前线程的ThreadLocalMap中根据ThreadLocal实例作为Key取出对应的值
- ThreadLocal可能会引发内存泄露问题;因为ThreadLocalMap的Entry对Key采用弱引用的,对Value是强引用的,如果外部未显式调用remove,当ThreadLocal实例被回收时,Entry的Key会变为null,但值仍然存在强引用不会被回收;如果在线程池场景下,线程长期复用会累计这些无法访问的值,最终引发OOM;
- 解决方法,一个ThreadLocal内部的get和set使用前会遍历ThreadLocalMap中所有key为null的entry进行清除;一个是使用完毕后立即使用显式的remove方法进行清空
☀类加载过程
- 类加载过程是JVM将类的字节码文件加载到内存并初始化的过程
- 加载,通过类加载器获取类文件的二进制字节流,转化为方法区的运行时数据结构,并生成.class对象作为访问入口
- 链接,主要分为三个子阶段验证、准备和解析,验证字节码符合JVM规范不会危害安全(包括文件格式、元数据、字节码、符号引用验证),准备为静态变量分配内存并设置默认初始值,若是final直接赋值,解析则是将常量池的符号引用转化为直接引用(类、方法名改为内存地址或偏移量)
- 初始化,执行由编译器生成的构造器,完成静态变量的赋值和静态代码块的逻辑
类初始化时机
- 创建实例,通过new实例化对象
- 访问类的静态成员,访问静态方法和静态变量
- 通过反射调用初始化类
- 子类继承父类,初始化子类时,触发父类初始化
malloc和new的区别
- malloc是手动分配内存空间大小;new是自动计算所需内存大小
- malloc仅分配原始内存,需要手动初始化和清理资源;new是分配内存后自动调用函数初始化对象
☀对象创建的过程了解吗
- 对象创建主要包括五个步骤
- 类加载检查,JVM首先会检查目标类是否加载、解析和初始化
- 内存分配,JVM根据对象大小在堆内存中分配内存,主要的方式有指针碰撞(内存规整情况,通过移动分界指针完成分配)和空闲列表(适用于内存碎片化情况,通过维护空闲内存块列表查找可用空间)
- 初始化零值,分配的内存空间的变量会被初始为零值或null
- 设置对象头,对象头主要包含两部分信息,运行时元数据Mark Word(包括哈希码,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID等信息),类型指针(指向方法区中的类信息,确定对象类型)
- 执行构造方法,最后调用构造方法完成对象初始化,显式属性赋值,实例代码块,构造函数逻辑
☀对象内存结构
- 对象头,包括运行时数据markword(哈希码,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID等信息),类型指针,如果是数组还有有一个长度字段
- 实例数据,存储对象声明的成员变量内容
- 对齐填充,JVM要求对象起始地址必须是8字节的整数倍,如果不足需要通过填充空白字节对齐
☀对象创建方式
- 通过new方式,直接调用构造方法创建对象
- 使用反射机制,使用newInstance或者获取构造器在调用newInstanec动态创建对象
- 通过反序列化的方式,实现Serializable接口,通过ObjectInputStream从字节流中反序列化重建对象
- clone方法,通过实现Cloneable接口,调用clone复制现有对象
☀什么是双亲委派模型
- 双亲委派机制是一种Java类加载器中的层级委托机制,其核心思想是当一个类加载器加载某个类时,会优先将请求委托给父加载器处理,依次向上传递至最顶层的启动类加载器;只有当父类加载器无法加载时,子类加载器才会尝试加载
- 双亲委派模型自顶向下可以分为启动类加载器(负责加载JRE核心类库jre/lib)、扩展类加载器(加载jre/lib/ext)、应用程序类加载器、用户自定义类加载器
☀双亲委派模型的优势
- 双亲委派的优势主要体现在安全性、避免重复加载以及保证类的一致性
- 由于层级委托机制,会优先委派父类加载器加载,这有效防止了核心类库被用户自定义的同名类篡改,保障JVM运行环境的安全性
- 其次,父类加载器已加载的类会被缓存,子类加载器无需重复加载
- 最后,通过类加载器的层级委托机制,确保了类名相同的类在不同层级加载器中具有全局唯一性,例如String无论哪个类加载器加载,最终都会由顶层的启动类加载器完成
破坏双亲委派模型的场景
- 用户自定义类加载器覆盖机制,开发者通过继承ClassLoader并重写loadClass方法,绕开默认的父类委派流程,自定义控制类加载的逻辑
- 多应用隔离和热部署,Tomcat作为Web容器需要部署多个Web应用,每个应用可能依赖不同版本的相同类库,通过自定义的WebappClassLoader,打破了双亲委派模型,它会首先尝试Tomcat让每个Web应用程序优先从自身的WEB-INF目录下加载类,找不到时才委派父类加载器;这种机器实现了应用间的类隔离,避免类冲突,并支持无需启动容器即可重新加载单个应用的热部署方式
- SPI机制,核心类由启动类加载器加载,但其需要动态加载厂商提供的SPI实现类,此时需要通过线程上下文类加载器,启动类加载器会主动委派给应用程序加载器完成加载,打破了传统的自上而下的委派顺序
☀JVM内存区域
- JVM内存区域是Java虚拟机管理程序运行内存的核心结构,主要分为线程共享区域和线程私有区域
- 对于线程私有区域,虚拟机栈,存储方法调用的栈帧(包括局部变量表、操作数栈、动态链接、方法出口等),方法执行时入栈,结束时出栈;本地方法栈,为JVM使用到的Native方法服务,本地方法执行时同样会创建栈帧;程序计数器,记录当前线程执行字节码的行号
- 对于线程共享区域,堆,用于存储所有对象实例及数组,堆可以进一步划分为新生代和老年代;方法区,存放类的元数据、运行时常量池、静态变量等
- 直接内存,不属于JVM运行时数据区的一部分,绕开了JVM堆内存的管理机制,通过NIO引入,直接内存通过操作系统本地内存分配,减少了数据在JVM堆与操作系统内存之间的拷贝,用于提高I/O性能
JDK 1.6、1.7、1.8的内存区域区别
- JDK1.6方法区通过永久代实现,永久代位于JVM运行时数据区中,存储类的元数据、运行时常量池、静态变量和字符串常量池
- JDK1.7方法区仍通过永久代实现,将字符串常量池迁移至堆中,减少永久代的内存压力
- JDK1.8移除了永久代,方法区由元空间替代,元空间使用本地内存,运行时常量池和类的元数据转移到元空间,元空间可以动态扩展大小从而避免了永久代的OOM问题,提升了内存管理的灵活性
介绍一下JVM堆和栈,有什么区别
- 堆是线程共享的内存区域,用于存储程序运行时创建的对象实例和数组,其生命周期由垃圾回收器管理,通过分代设计优化GC效率
- 栈是线程私有的,用于存储方法调用的上下文信息,包括局部变量、操作数栈、动态链接和方法出口等,栈帧随方法调用创建,结束后出栈销毁,无需GC参与
- 在出现异常情况下,堆溢出表现为OutOfMemory,通常是对象过多或内存泄露;栈溢出表现为StackOverFlow,通常因为递归过深
☀JVM堆结构
- JVM堆通常采用分带设计来优化垃圾回收效率,可以分为新生代和老年代,默认占比1:2
- 新生代,包括一个Eden区和两个Survivor区,默认占比8:1:1,新创建的对象优先分配到Eden区,当Eden区满时触发minor gc,使用标记复制算法,存活对象复制到Survivor区其余对象清空,经过多次GC后未被回收的对象晋升到老年代
- 老年代,用于存储长期存活的大对象,当老年代空间不足时会触发Full GC,采用标记清除或标记整理算法
逃逸分析
- 逃逸分析是JVM一种优化技术,来判断对象作用域是否可能逃逸出当前方法或线程,从而决定是否分配到内存中
- 主要可以分为未逃逸(对象仅在方法内使用,未被外部方法引用,也未被其他线程引用)、方法逃逸(对象被外部方法引用)、线程逃逸(对象被其他线程访问)
- 如果对象未逃逸,则JVM可能将其分配在栈内存而非堆内存中,避免垃圾回收开销;如果对象仅被单线程访问,编译器会移除不必要的同步锁,减少性能开销;
对象什么时候会进入老年代
- 当对象大小超过设定阈值时,大对象会绕开新生代直接在老年代分配
- 当年轻代的对象经历的GC次数超过设定的阈值时,会晋升到老年代
- 如果幸存者区某一年龄段的总对象总大小超过了该区容量的一半,那么年龄大于该断的对象会直接晋升到老年代,避免幸存者区空间不足
方法区里有哪些东西
- 类的元数据,包括类的结构信息、类的访问修饰符、类名、成员变量和成员方法、字节码等信息
- 运行时常量池,存储编译器生成的字面量如字符串和数字常量,符号引用如类、接口和方法的全名引用
- 静态变量,存储被static修饰的变量,非final修饰的静态变量存储在方法区,final修饰的静态变量存入运行时常量池
☀内存泄漏和内存溢出的理解,常见原因有哪些
- 内存泄漏指程序申请内容后未能正确释放内存资源,导致无用对象持续占用内存空间,最终可用内存逐渐减少的现象;常见原因有静态集合类的滥用,没有及时关闭数据库、IO等链接资源,ThreadLocal的引用没有显式清理,长生命周期对象持有短生命周期对象引用(单例模式内部持有临时对象引用,导致后者无法释放)等
- 内存溢出指程序申请内存时,系统无法提供足够的空间,导致程序崩溃的现象;造成原因有创建过多对象导致堆内存溢出、递归调用过深导致栈内存溢出、静态变量过多导致元空间内存溢出、NIO操作分配过多内存导致直接内存溢出
☀什么是垃圾回收,如何触发
- 垃圾回收是Java自动管理内存的核心机制,通过垃圾检测并回收程序中不再被引用的对象以释放内存空间,避免内存泄露和内存溢出,触发垃圾回收主要分为自动触发和手动触发两类
- 自动触发包括,分代收集机制,当Eden区内存分配时触发minor gc,采用复制算法快速回收短生命周期对象,当老年代空间不足时触发full gc,通常使用标记清除或标记整理算法
- 手动触发包括调用System.gc,可以建议JVM执行垃圾回收,但实际触发时间由回收器决定,并不会立刻执行
☀如何判断是否垃圾
- 引用计数法,每个对象设置一个计数器,记录被引用的次数,当计数器为零时被回收,但是存在两个对象相互引用导致循环引用不能被回收的问题
- 可达性分析,JVM从GC ROOTs根对象触发,遍历所有的引用链,标记所有可达对象,如果对象不可达则判定为可回收的垃圾;
GC Roots包括哪些引用
- 虚拟机栈引用,包括当前执行方法的局部变量、方法参数等
- 本地方法栈中的JNI引用,包括通过Java本地接口调用Native方法中引用的对象
- 方法区的静态引用,包括类的static静态变量
- 方法区的常量引用,包括运行时常量池中的字符串字面量和static final修饰的常量
☀对象引用类型有哪些
- 强引用,通过new关键字之间创建,强引用关联的对象,即使内存不足,GC也不会回收该对象
- 弱引用,弱引用关联的对象,在下一次GC都会被回收,无论内存是否充足
- 软引用,软引用关联的对象,在内存不足时会被GC回收
- 虚引用,虚引用无法获取对象实例,仅用于跟踪对象垃圾回收过程,在下一次GC时虚引用也会被回收
什么是STW
- STW是JVM中一种暂停所有用户线程以保证数据一致性和安全性的操作,例如JVM进行垃圾回收的过程中保证对象引用不被修改
- JVM会使用Safe Point机制来确保线程能够安全暂停,Safe Point是指线程在执行到某些特定位置(例如方法调用,循环跳转)可以安全地被暂停
☀垃圾收集器有哪些
- Serial / Serial Old,单线程工作的收集器,新生代使用标记复制算法,老年代使用标记整理算法,垃圾回收时需要触发STW,暂停其他所有工作线程直至垃圾收集结束
- Parallel New、Parallel Scavenge / Parallel Old,多线程并行收集器,新生代使用标记复制算法,老年代使用标记整理算法,Parallel Scavenge专注于高吞吐量(高效使用CPU时间)
- CMS,用于老年代的并行收集器,具有高并发、低停顿的特点,以最短GC回收停顿时间作为目标,通过并发标记和并发清除,缺点是会产生内存碎片
- G1,可以同时用于新生代和老年代的回收器,使用标记整理算法,是一种面向大内存、高吞吐场景的垃圾回收器,它将堆划分为多个小的区域,避免内存碎片,优点是可以指定期望的停顿时间
☀垃圾回收算法有哪些
- 标记-清除算法,首先标记所有可达对象,然后清除所有不可达的垃圾对象,优点是实现简单缺点是会产生内存碎片
- 复制算法,将内存分为两块,仅使用其中一块,当区域满时,将存活对象复制到另一块并清空原区域,优点是无内存碎片且效率高,缺点是内存利用率50%
- 标记-整理算法,在标记前阶段后,将所有存活对象向内存一端移动并整理,优点是避免内存碎片,缺点是会带来额外的性能开销
- 分代收集算法,根据对象生命周期优化垃圾回收过程,将堆分为新生代和老年代,新生代使用复制算法,老年代使用标记-清除或标记-整理算法
CMS垃圾回收过程
- 初始标记,触发STW,标记所有跟GC roots直接关联的对象
- 并发标记,工作线程和用户线程同时运行,工作线程并发标记从初始标记的对象出发,标记所有可达对象
- 重新标记,触发STW,修正并发标记期间因程序运行导致的引用变动(通过三色标记算法,标记访问到但未完成标记其引用的对象为灰色,然后标记灰色对象引用的对象为灰色,再将原先灰色对象标记为黑色,代表完全处理,最后剩余的白色对象就是不可达对象需要回收,所有黑色对象都是存活对象)
- 并发清除,工作线程和用户线程运行,清除所有垃圾对象,此阶段采用标记清除算法,会产生内存碎片
CMS和G1垃圾收集器的区别
- 收集范围方面,CMS的收集范围是仅对老年代进行回收需要搭配其他新生代的收集器使用;G1的收集范围是年轻代和老年代
- 在垃圾回收算法方法,CMS采用标记清除算法,容易会产生内存碎片,可能导致后续触发Full GC以压缩内存;G1采用标记整理算法,通过将堆分为多个region,动态管理分配给新生代和老年代的region数量,避免内存碎片
- 垃圾回收过程方面,CMS的回收过程分为初始标记、并发标记、重新标记、并发清除,G1回收过程是初始标记、并发标记、最终标记、筛选回收,G1的最后一个过程需要STW
- 回收停顿时间方面,CMS以最小停顿时间为目标,通过并发标记和并发清除减少停顿时间;G1通过可预测的停顿时间模型,并通过设置期望停顿时间,通过维护的优先队列主动回收价值高的region
- 浮动垃圾方面,CMS在并发清除的过程用户线程仍在运行,这个过程中产生新的老年代垃圾对象称为浮动垃圾,这些垃圾无法被当前周期处理,只能留到下次GC清除;这个过程可能会导致老年代预留给这些浮动垃圾的空间不足,触发并发模式失败,CMS最终会退化为Serial Old收集器,引发长时间STW重新回收垃圾对象
什么是minor gc、major gc、full gc,触发场景
- minor gc,主要针对新生代(Eden区和2个survivor区)垃圾回收,当Eden区空间不足无法分配新对象时触发minor gc,采用复制算法将新生代的存活对象转移到另一个幸存者区或者晋升老年代,其余垃圾对象清除;特点是频繁触发,因为年轻代中的对象的生命周期较短,回收效率高,停顿时间短
- major gc,主要针对老年代的垃圾回收,当老年代空间不足时(长期存活对象积累过多,大对象分配失败)触发major gc,采用标记-清除或标记-整理算法回收垃圾对象;特点是触发频率较低,因为老年代的对象生命周期较长,回收停顿时间较长
- full gc,对整个堆内存包括年轻代、老年代以及元空间的全局回收,触发条件是显式调用system.gc,或者元空间不足,老年代空间无法容纳minor gc后晋升的对象
Web容器和Spring容器区别
- Web容器,用于运行Java Web应用程序的服务器环境,常见的有tomcat和jetty等,用于管理Servlet、Filter等Web组件
- Spring容器,专注于应用程序中的对象生命周期的管理和依赖注入,Spring父容器能够管理Service、DAO等业务层Bean,Spring子容器如Spring MVC管理Controller等Web组件
谈谈自己对于Spring的理解
- Spring是一个轻量级的框架,核心设计理念是通过ioc控制反转和aop面向切面编程实现组件间高内聚和低耦合
- Spring的ioc有通过控制反转实现管理对象的生命周期和依赖关系,Spring
- Spring的aop面向切面编程,实现对横切关注点的模块化,通过动态模式增强目标对象的逻辑
- Spring还提供了一套事务管理接口,包括声明式的和编程式的事务,无需关注具体的事务API
- Spring还提供了Spring MVC提供了了灵活的请求处理流程
☀谈谈自己对于Spring IoC的理解
- spring ioc的核心思想,是通过控制反转将对象的控制权交由IOC容器管理,而不需要用户每次手动去创建对象
- ioc可以通过DI依赖注入实现,通过xml配置、java注解、java配置,容器会注入相应的对象
- ioc的主要工作流程可以分为三个阶段,IOC容器初始化(解析xml和注解,获取所有bean定义信息生成BeanDefinition并注册到IOC容器的BeanDefinitionMap中),Bean实例化及依赖注入(Spring根据反射实例化Bean对象,并通过构造方法、setter方法或autowired完成对象注入),最后是Bean的使用
- ioc的好处是,组件之间通过接口和依赖注入解耦,开发无需手动创建对象交由Spring统一管理Bean生命周期简化对象管理
☀谈谈自己对于Spring AOP的理解
- spring aop的核心思想是面向切面编程,将与业务逻辑无关的通用功能从业务代码中解耦,通过动态代理技术实现代码增强
- 定义切面,使用aspect注解标注类,定义切点(pointcut)和通知(advice, 如before,after,around等注解)
- 解析切点,解析pointcut表达式,确定需要增强的方法连接点joint point
- 运行时织入,主要包括创建代理对象和方法调用拦截两方面。创建代理对象,如果目标类实现了接口则使用JDK动态代理通过Proxy.newProxyInstance创建代理对象,如果没有接口则使用CGLIB,通过Enhancer生成代理对象,会创建目标类的子类;方法调用拦截,如果是JDK动态代理,实现invocationHandler接口调用invoke,如果是CGLIB,实现methodInterceptor接口调用Intercept,执行完增强逻辑后,在调用目标方法
☀什么是动态代理
- Java的动态代理是一种在运行时动态创建代理对象的机制,用于不修改目标类的情况下对方法调用进行增强
- Java的动态代理主要分为两种,一种是基于接口的代理JDK动态代理,它可以在运行时动态生成一个实现了相同接口的代理类,核心是通过实现InvocationHandler接口并调用invoke方法
- 另一种是基于类的代理Cglib动态代理,他可以在运行时动态生成一个目标类的子类,核心是通过创建Enhancer代理对象,实现MethodInterceptor接口并调用invokeSuper方法
动态代理和静态代理的区别
- 动态代理是代码运行期间,通过JDK动态代理或CGLIB方式动态生成代理类,无需预先定义
- 静态代理需要手动编写代理类,并显式实现目标类的相同的接口或继承父类
Spring的aop什么情况下会失效
- 同类内部方法调用,由于调用发生在目标对象内部,而非代理对象上,AOP增强逻辑不会触发
- 方法非public修饰,aop默认只对public生效
- 目标类未被Spring管理,目标类没有使用component、service等注解声明为Spring的bean,未被spring容器加载
- 未启用AOP注解支持,没有在启动类上添加enableAspectJAutoProxy,aop功能无法激活(使用proxyTargetClass=true,可以强制使用cglib动态代理)
- 目标类不能被动态代理,例如使用jdk动态代理但是目标类没有实现接口,使用cglib动态代理,但是目标类被final修饰
☀Spring的事务什么情况下会失效
- 方法非public修饰,事务注解用于private或protectd方法(非public修饰,Spring事务基于AOP实现,默认只对public方法生效)
- 自调用问题,同一类的非事务方法内部调用事务方法时(未通过代理对象,事务不会生效)
- 异常处理不当,若方法内捕获异常未重新抛出或异常类型未被Spring默认回滚规则支持(默认是运行时异常,需要编译时异常需要显示声明rollbackfor),事务不会回滚
- 事务传播行为配置错误,内部方法设置了不支持事务或者禁止事务的传播行为会导致事务失效
- 目标类未被Spring管理,目标类没有使用component、service等注解声明为Spring的bean,未被spring容器加载,事务注解无效
Spring事务如何实现回滚
- Spring事务基于AOP和事务管理器的协同工作,核心原理是通过动态代理对事务方法进行拦截,由事务管理器统一控制事务的提交或回滚
- 当方法执行过程抛出异常符合回滚规则,则会自动调用事务管理器的rollback方法,回滚所有数据库操作
循环依赖是什么?Spring如何解决循环依赖问题
- 循环依赖是指两个或者多个bean直接或间接依赖形成一个闭环(例如对象A依赖对象B,同时对象B中又依赖对象A,形成了环),导致Spring容器无法正确初始化
- Spring通过三级缓存机制解决单例模式下setter和字段注入循环依赖问题(构造器注入不支持)
- 一级缓存存储完全初始化并且可用的bean,二级缓存存储已实例化但未完成属性注入的Bean,三级缓存存储Bean的工厂对象,用于生成原始对象或代理对象,确保在依赖注入时能动态代理对象
- 创建的流程,创建A时会先实例化并通过工厂对象存入三级缓存;填充A的依赖时发现需要B,触发B的创建;创建B时同样实例化存入三级缓存,填充B的依赖时从三级缓存获取A的工厂生成A早期对象,A移入二级缓存,B完成初始化后移入一级缓存;A再从一级缓存中获取B,最终A也完成初始化移入一级缓存
Spring三级缓存可以换成二级缓存吗
- 如果在没有AOP动态代理的情况下,二级缓存可以通过提前暴露实例化但未初始化的bean对象解决循环依赖问题,三级缓存并非必须
- 如果在有AOP动态代理的情况下,必须保留三级缓存,因为三级缓存通过存储bean工厂对象来延迟代理对象的生成;如果使用二级缓存解决循环依赖,意味着Bean在实例化之后就创建了代理对象,违背了AOP的生命周期设计原则,应该在Bean实例化并且初始化后(BeanPostProcessor)才生成代理
Spring常用注解
- 容器相关,component声明bean组件(派生注解语义化分层,controller, service,mapper,repository)
- 注入相关,autowired (by type类型), resource (by name名字,搭配qualifier指定具体实例)
- 配置相关,configuration标记配置类, bean声明bean,scope指定bean作用域(singleton或者prototype)
- web层相关的,restcontroller(controller+Responsebody,构建restful api), requestmapping映射HTTP请求到控制器方法(及简化注解postmapping,getmapping),responseBody JSON序列化写入HTTP响应体, requestBody反序列化JSON请求体, pathVariable绑定url参数, requestParam获取请求参数
- 切面相关,aspect定义切面 before方法前, after方法后, around 前与后, pointcut切点
- 事务相关的,transactional开启事务
- 拓展功能,enableasync开启异步,async使用异步方法调用,schedule定时任务
Spring中使用到的设计模式
- 工厂模式,通过BeanFactory和ApplicationContext实现bean对象的创建与管理
- 代理模式,spring aop基于动态代理实现面向切面编程,实现日志、事务等横切关注点的统一处理
- 单例模式,spring默认将bean作用域设为单例
- 适配器模式,spring mvc中HandlerAdapter将不同类型的控制器适配到统一的处理接口
- 模版方法模式,spring中的jdbcTemplate和hibernateTemplate等使用到了模版模式
☀Bean的生命周期
- 实例化,通过构造函数或者工厂方法创建bean实例
- 属性注入,Spring容器通过(setter、构造器注入、字段注入)依赖注入Bean属性
- 初始化阶段,aware接口回调(BeanNameAware、BeanFactoryAware、ApplicationContextAware)使bean能够获取容器信息;BeanPostProcessor前置处理,通过postProcessBeforeInitialization方法对bean进行增强或修改;执行初始化方法,包括postConstruct注解方法,实现initializingBean接口的afterPropertiesSet方法,然后是使用init-method声明的初始化方法;BeanPostProcessor后置处理,通过postProcessAfterInitialization完成最终处理(例如aop代理)
- 使用阶段,bean初始化之后可以被其他组件调用
- 销毁阶段,容器关闭时,如果bean实现了DisposableBean接口,Spring将调用它的destroy方法完成销毁,然后是声明的destroy-method方法
Bean是线程安全的吗
- 如果是单例模式,如果使用时Bean为无状态,则线程安全;如果Bean有状态需要对bean内部存在被并发修改的成员变量,则可能出现线程安全问题
- 如果是多例模式,则每次使用都会创建一个新的实例,是线程安全的
☀SpringMVC处理流程
- 用户发起的HTTP请求至前端控制器DispatchServlet
- DispatcherServlet收到请求调用HandlerMapping处理器映射器组件解析请求URL
- HandlerMapping根据URL匹配到对应的Handler处理器,并生成处理器执行链HandlerExecutionChain一并返回给DispatcherServlet
- DispatcherServlet调用处理器适配器HandlerAdapter执行某个执行器
- 执行处理器Handler,会对目标controller方法执行业务逻辑
- Handler执行完成,最终封装返回ModelAndView对象给HandlerAdapter
- HandlerAdapter返回ModelAndView对象给DispatcherServlet
- DispatcherServlet将ModelAndView传给ViewReslover视图解析器
- ViewReslover解析后返回具体View
- DispatchServlet对View进行渲染视图,将模型数据填充至视图
- DispatchServlet响应用户
Springboot自动装配原理
- 启动类注解触发,启动类上的SpringBootApplication注解,内部包含EnableAutoConfiguration注解,该注解通过import(AutoConfigurationImportSelector.class)引入自动装配的核心类
- 加载候选配置类,AutoConfigurationImportSelector类会扫描所有依赖的META-INF/spring.factories文件,读取其中预定义的配置类(org.springframework.boot.autoconfigure.EnableAutoConfiguration)
- 利用条件注解如ConditionalOnClass和ConditionalOnMissingBean过滤配置类
- 配置类通过Configuration注解和Bean注解生成Bean实例,注入Spring容器,完成自动化配置
Springboot启动原理
- Springboot应用通过SpringBootApplication注解标记的主类启动,该注解整合了三个核心注解Configuration、EnableAutoConfiguration、ComponentScan,主类的main方法调用了SpringApplication.run()方法,触发整个启动流程
- 创建SpringApplication实例,确定Springboot应用类型(Web应用还是Servlet应用),并通过ApplicationContextInitializer初始化应用的上下文类型
- 初始化环境和监听器,会解析命令行参数、配置文件(application.properties或者application.yml)及环境变量,构建ConfigurableEnvironment对象,并初始化ApplicationListener事件监听器
- 加载配置类并触发自动配置,使用EnableAutoConfiguration注解该注解通过import(AutoConfigurationImportSelector.class)引入自动装配的核心类加载META-INF/spring.factories配置文件,并根据条件注解过滤后进行自动装配
- 加载并注册Bean,SpringBoot通过refershContext方法,完成bean工厂初始化、bean后置处理器注册、并解析各种注解标注的组件注册到bean工厂中
- web环境中嵌入式容器启动,Springboot启动嵌入式Web容器(如Tomcat和Jetty),Springboot通过ServletWebServerApplicationContext启动
- Springboot启动完成后,会扫描并执行实现了ApplicationRunner和CommandLinerRunner接口的bean
- 最后,SpringBoot发布ApplicationReadyEvent事件,通知所有监听器应用已启动完成
介绍一下SpringTask
- SpringTask是Spring框架提供的一个轻量级的任务调度框架
- SpringTask的核心原理是通过EnableScheduling和Schedule注解结合使用,在启动类上标注EnableScheduling,在需要定时执行的方法上使用Schedule注解,并搭配cron表达式配置执行规则
- SpringTask默认是单线程执行任务的,但可以通过实现SchedulingConfigurer类配置线程池ThreadPoolTaskExecutor类,避免任务阻塞
介绍一下SpringCache
- SpringCache是Spring框架提供的缓存抽象层,核心思想是基于AOP动态代理技术,通过实现Cache和CacheManager接口,在方法前后自动处理缓存逻辑
- 通过声明式注解Cacheable、CachePut、CacheEvict等注解实现缓存数据的查询、更新和清除,还提供了条件缓存condition,动态键生成key等,无需手动编写底层缓存操作代码
介绍一下过滤器Filter和拦截器Interceptor
- 过滤器Filter是Servlet容器管理,作用于所有web请求包括静态资源,Interceptor拦截器是Spring框架提供的组件,仅作用于Spring MVC的Controller层
- 过滤器在请求进入Servlet前或响应返回客户端后进行处理,拦截器在请求进入Controller方法前后(通过实现HandlerInterceptor的preHandler、postHandler)进行处理,因此过滤器顺序会先于拦截器
- 过滤器是基于回调函数实现,依赖于Servlet容器;拦截器是通过Spring aop实现的,并且生命周期由Spring管理,可以支持依赖注入
JDBC连接数据库步骤
- 加载数据库驱动,通过Class.forName(“com.mysql.cj.jdbc.Driver”)反射机制显式注册驱动
- 建立数据库连接,调用DriverManager.getConnection(url, username, password)方法跟数据库建立连接
- 创建Statement对象,通过connection.createStatement生成基本Statement或者使用创建预编译的prepareStatement,支持参数化查询,使用?作为占位符,可以防止SQL注入
- 执行SQL并处理结果,查询语句使用executeQuery获取ResultSet结果集,更新语句通过executeUpdate返回影响行数
- 资源释放,按ResultSet、statement、connection顺序调用close方法关闭资源
Mybatis的工作原理
- 根据mybatis-config.xml和mapper.xml完成初始化,生成MappedStatement配置对象
- 创建会话工厂SqlSessionFactory
- 通过SqlSessionFactory创建数据库会话对象SqlSession,SqlSession可以定位具体的mapper
- SqlSession通过调用Executor执行器执行数据库操作,还包括处理参数ParameterHandler、处理数据库会话StatementHandler、处理结果集ReusltSetHandler
- 使用SqlSession提交事务
- 关闭SqlSession会话
Mybatis的一级缓存和二级缓存
- 一级缓存是基于SqlSession的本地缓存,存储在SqlSession的哈希表中。SqlSession关闭后缓存情况,主要用于减少重复查询
- 二级缓存是基于Mapper的全局缓存,存储在Mapper的命名空间中,多个SqlSession可以共享,需要手动开启,适用于频繁查询并且数据不常变动的场景
Mybatis和MybatisPlus的区别
- Mybatis作为基础的半自动化ORM框架,需要手动编写SQL和XML映射文件,需要自行处理动态SQL拼接,灵活性高效率低
- MybatisPlus在它的基础上做了增强,通过继承baseMapper来实现通用的CRUD方法、MybatisPlus引入Lambda表达式和链式调用并提供LambdaQueryWrapper条件构造器等,减少重复代码,效率高
- 并且MybatisPlus提供了额外的功能,如代码生成器,分页插件、更多的注解支持实体与数据库表映射
数据库三大范式
- 第一范式,任何字段都必须是原子型的,不可分割为更小的数据单元
- 第二范式,在第一范式的基础上,任何非主键字段完全依赖于主键
- 第三范式,在第二范式基础上,要求非主键字段之间无传递依赖
char、varchar、text有什么区别
- char是固定长度的字符串,无论实际长度如何,始终占用固定空间
- varchar是可变长度字符串,但需要额外的1-2字节存储实际数据长度,(varchar(n),存储n个字符而非n个字节)
- text也是可变长度字符串一般用于存储超过65536长度的字符串,最大可以存储4GB
in和exists的区别
- in会先查询子查询,将结果集缓存到内存,然后主查询逐行去缓存表中筛选记录,适用于子查询结果集较少的场景
- exist则是会遍历主查询的每条记录,逐行检查子查询是否存在匹配数据,如果找到匹配记录会立即返回减少全表扫描,适用于主表记录少,子查询记录多的场景
- in对null值处理可能会导致逻辑问题,exists仅关注子查询是否存在不关注值,不受null值影响
查询SQL的执行顺序
- from
- on
- join
- where
- group by
- having
- select
- distinct
- order by
- limit
Mysql的存储引擎有哪些
- memory,数据使用内存存储,读写速度极快,数据库重启或宕机就会丢失数据,不支持事务、行级锁和外键,适用于临时表或缓存
- MyISAM,读取速度快,数据以非聚簇索引形式存储(.frm存储文件库表结构,.myd存储数据,myi,存储索引),不支持崩溃恢复能力,不支持事务、行级锁和外键,适用于读多写少
- Innodb,是MYSQL默认的存储引擎,具体ACID事务支持、行级锁、外键约束等特性,数据以聚簇索引形式存储(.frm存储文件库表结构,.ibd存储数据和索引),结合Buffer Pool优化读写性能,并具备崩溃恢复能力,适用于高并发的读写操作
☀索引的原理
- 索引是数据库中的一种高效的数据结构,能够将全表扫描的随机磁盘I/O转化为有序查询的局部磁盘I/O,实现查询加速
- 在MYSQL中默认使用B+树作为聚簇索引,叶子节点存储完整数据行,而非叶子节点存储索引值和指针,这种多路平衡树结构可以将千万级数据控制在三层以内,使得范围查询和顺序查询的磁盘I/O效率显著提升
☀索引的分类
- 数据结构,B+树索引,哈希索引,全文索引
- 物理存储方式,聚簇索引,非聚簇索引
- 按字段特性,前缀索引,唯一索引,主键索引,二级索引
- 按字段个数,普通索引,联合索引
☀聚簇索引和非聚簇索引的区别
- 聚簇索引和非聚簇索引核心区别在于数据存储方式上
- 聚簇索引的叶子节点存储具体的数据行
- 非聚簇索引将数据行和索引分开存储,叶子节点仅存储指向数据行的指针
☀B+树、B树、红黑树的特点及区别
- 数据存储位置,B+树数据行存储在叶子节点,非叶子节点只存储索引值;B树和B+树每个节点都存储数据行
- 叶子节点结构,B+树的叶子节点都处在同一层,并且通过双向链表连接,可以实现顺序遍历和范围查询,B树和红黑树不能做到顺序遍历和范围查询
- 树的高度,由于B+树的非叶子节点不存储数据行可以存储更多的索引值,使得树的高度更低,磁盘I/O次数少效率高,B树非叶子节点也存储数据,使得树的高度更高,磁盘I/O效率比B+树低,红黑树是一颗自平衡二叉搜索树,每个节点最多有2个子节点,树的高度较高,磁盘I/O效率低下
- 空间利用率,B+树和B树的单个节点存储了大量键值数据,更适用磁盘使用场景,红黑树每个节点只存储数据、颜色和指针,结构更加紧凑、内存占用小,更容易被CPU缓存预加载,适用于内存场景
- 平衡维护复杂度,B+树和B树需要通过节点分裂与合并维护平衡,维护成本高,红黑树通过颜色标记和有限次旋转实现,维护成本低
☀什么是联合索引,什么是最左匹配原则
- 联合索引是将多个字段组合为一个索引结构,索引中的数据按照字段的声明顺序依次进行排序
- 联合索引符合最左匹配原则,要求查询条件必须从索引的最左字段开始匹配,如果跳过左侧字段或者字段不连续会导致索引失效,导致全表扫描;例如(A,B,C)这个联合索引,就必须要由A,(A,B), (ABC)这样三个顺序才会触发,如果是B,BC这样就不行
什么是回表
- 回表是指通过二级索引查询数据时,由于索引中未包含所有的查询字段,数据库还需要根据索引中存储的主键值再次访问主键索引获取完整数据行的过程
什么是覆盖索引
- 覆盖索引是数据库优化中的一种机制,其核心在于索引本身包含了所有的查询字段,使得查询可以直接从索引中获取数据而无需回表,减少磁盘I/O效率
什么是索引下推
- 索引下推是数据库优化联合索引的一种机制,其核心在于将where条件中没有使用到索引的字段的过滤操作,从server层下推到存储引擎层执行,从而减少了回表次数,提升了磁盘I/O效率
什么是前缀索引
- 前缀索引是数据库优化针对字符串类型字段的一种索引策略,其核心在于仅对字段值的前N个字符建立索引,而非整个字符串,减少索引数据的存储量,同时提升查询效率,可以通过截取字段前缀计算区分度select count (distinct left (column, length)) / count(*),适用于长文本字段的场景中
☀索引失效的场景
- 使用or连接的多个条件中,有一个没有使用索引
- 使用模糊查询,通配符%在前面
- 索引列使用函数或者运算
- 隐式类型转换,例如字符串类型字段和数字进行比较
- 没有符合联合索引的最左匹配原则
- 使用不等于操作符,优化器可能因扫描大量数据而放弃索引
索引优化
- 避免索引失效场景,禁止在索引列上使用函数或运算,避免Like通配符%开头,注意隐式类型转换
- 使用覆盖索引避免回表减少I/O开销
- 使用前缀索引减少字符串索引的大小,提高查询效率
- 创建索引时注意字段的区分度,应选择区分度高的字段建立索引
- 创建联合索引时,字段区分度高的放在左边,同时查询列要遵循最左前缀原则
- 使用游标或者延迟关联优化深度分页
- 通过执行计划explain分析,关注type列是否走索引,避免ALL全表扫描
☀事务的四大特性,如何实现
- 原子性,保证事务全部要么成功要么全部失败回滚,使用undo log实现,事务执行过程中,所有修改会先记录到undo log中,若事务失败则可以通过undo log将事务恢复
- 一致性,事务执行前后数据库保持逻辑上的正确性,依赖原子性、隔离性、持久性来实现
- 隔离性,保证事务间的隔离性,事务执行期间互不影响,使用MVCC+锁机制实现,MVCC通过隐藏字段(事务ID、回滚指针)和undo log版本链记录数据版本,结合事务隔离级别控制数据的可见性,通过记录锁和间隙锁等锁机制方式防止幻读问题
- 持久性,事务提交后数据永久保存,使用redo log实现,事务提交前将修改写入redo log并刷盘,系统崩溃也能恢复数据
MySQL有哪些并发问题
- 脏读,事务A读取了未提交事务B的数据
- 不可重复读,事务A两次查询的数据结果不一样
- 幻读,事务A两次查询的数据集不一样
☀事务的隔离级别,解决了哪些并发问题
- 读未提交,允许事务读取其他事务未提交的修改,未解决任何问题
- 读已提交,保证事务只读取其他事务已提交的修改,解决脏读问题
- 可重复读,保证事务中两次查询的数据结果一致,解决脏读和不可重复读问题
- 串行化,强制事务串行执行,对记录加上读写锁,解决脏读和不可重复读和幻读问题
☀MYSQL如何实现隔离级别的
- MYSQL通过多版本并发控制MVCC和锁机制实现可重复读的事务隔离级别
- MySQL通过每行记录的隐藏字段事务ID(trx_id)和回滚指针(roll_pointer),当事务修改数据时,旧版本的数据通过回滚指针形成版本链,事务读取时,根据版本链选择可见的数据版本
- 每个事务启动时,根据隔离级别生成Read View(如果是读已提交级别,就每次查询都会生成一个新的Read View,如果是可重复读级别,事务启动时生成一个全局Read View,整个事务期间都基于该快照读取数据),记录当前活跃事务ID,所有活跃事务中的最小事务ID,最大事务ID,自身事务ID;如果数据的事务ID小于最小事务ID代表已经提交,或者事务ID不在活跃ID并且小于等于最大事务ID也代表由已提交事务生成,或者数据事务ID等于当前事务ID代表数据行可见
- 在可重复读隔离级别下,快照读基于Read View访问版本链中的可见数据,避免加锁,实现无阻塞读取;当前读会通过锁机制,使用记录锁和间隙锁锁定当前行和间隙,防止其他事务插入新数据,避免幻读;
☀MySQL有哪些锁
- 全局锁,flush tables with read lock锁定整个数据库表实例,所有表只读,适用于备份场景
- 表级锁,元数据锁(执行DDL时上锁,阻塞DML),表锁(包括表共享锁,表排他锁),意向锁(当执行插入、更新、删除时会先对表加上意向排他锁,当相加表排他锁时用于快速判断表里是否有记录被加锁)
- 行级锁,记录锁(单行),间隙锁(索引记录之间的间隙),临键锁(左开右闭),插入意向锁(一种插入时使用特殊的间隙锁,与间隙锁互斥,插入意向锁之间相容如果不是插入同一位置)
MYSQL如何解决死锁
- 定位死锁,使用show engine innodb status命令查看死锁日志,分析具体代码问题,修改资源访问顺序,避免死锁
- 优化索引,通过合理设计索引减少全表扫描的情况,缩短锁占有时间
- 开启死锁检测,检测到死锁后(innodb_deadlock_detect),主动回滚死锁链条中的某一个事务,让其他事务得以执行
- 锁超时机制,设置事务等待锁的时间(innodb_lock_wait_timeout),超时则自动回滚,避免无限等待
- 使用乐观锁机制,通过版本号和时间戳等方式实现无锁化更新,减少显式锁竞争
☀MySQL有哪些日志,如何实现的,有什么作用
- undo log,属于innodb的逻辑日志,回滚日志,记录了事务前的数据版本,用于事务的回滚,保证事务的原子性和隔离性
- redo log,属于innodb的物理日志,基于WAL(write ahead logging)机制,事务提交前将数据页的修改循环写入redo log,确保崩溃恢复时重放未刷盘的脏页数据,保证事务的持久性
- binlog,作为server层的逻辑日志,以二进制格式记录所有数据变更(DML和DDL),采用追加写入的方式,用于数据的备份和主从复制
- error log,错误日志,用于记录mysql启动、运行及异常日志
- slow query log,慢查询日志,用于记录执行时间超过阈值的SQL,用于性能分析和优化
- relay log,中继日志,从库复制主库binlog事件时暂存的日志,格式与binlog一致,用于主从数据回放
MySQL如何保障数据不丢失
- WAL与Redo log机制,所有DML操作会在内存的buffer pool修改数据页的同时,将这些变更以redo log的形式顺序写入磁盘,redo log记录了具体数据页的变更,确保崩溃恢复时重放未刷盘的脏页数据。redo log日志刷盘策略默认每次事务提交时强制redo log刷盘,避免日志丢失
- 异步脏页刷盘策略,Buffer pool的脏页不会立刻刷盘,而是通过后台线程周期性刷盘,平衡性能和数据丢失风险,此外redo log的CheckPoint机制确保刷盘进度可以追踪,避免全量恢复
- 事务与持久性保障,通过两阶段提交保证事务原子性,配合undo log实现事务回滚,redo log保障已提交事务的持久性,bin log日志保障记录数据变更,搭配刷盘策略实现主从复制和数据备份
什么是两阶段提交,为什么
- MYSQL的两阶段提交是为了解决redo log和bin log两者的原子性问题,确保两者状态一致,两阶段提交主要可以分为准备和提交两个阶段,内部会开启XA事务
- 准备阶段,innodb将事务修改写入redo log,并标记为prepare状态,然后刷盘
- 提交阶段,server层将事务操作写入bin log并刷盘,然后调用存储引擎的事务提交接口,修改redo log标记为commit状态,完成最终提交
- 如果系统崩溃后,可以找到prepare阶段的redo log,去bin log中查找是否包含XA事务的ID,如果包含说明两个日志均刷盘,可以提交事务,如果不包含则意味着bin log没有刷盘,需要回滚事务
执行一条查询SQL请求的过程
- 客户端跟MYSQL建立连接,获取权限,管理连接,客户端发送SQL查询语句到MYSQL服务器
- MYSQL查询是否缓存,如果命中缓存直接返回
- 解析SQL,通过解析器对SQL查询语句进行词法分析、语法分析,确保数据库、表、字段都是存在的
- 优化器确定SQL语句的执行计划,包括使用哪些索引,以及决定表之间的连接顺序
- 执行器调用存储引擎的API进行数据读写
- 存储引擎返回结果集给客户端
执行一条更新SQL请求的过程
- 客户端发送SQL请求到MYSQL服务器,经过连接器、解析器、优化器、执行器
- 执行器调用存储引擎,开启事务,更新具体记录前,记录undo log
- 然后将记录更新写入buffer pool并标记为脏页,再将记录写入redo log
- 更新语句执行完成后,server层开始记录对应的bin log
- 两阶段事务提交,准备阶段将redo log设置为prepare状态,然后将其刷盘,提交阶段将操作写入binlog并刷盘,然后调用存储引擎的事务提交接口将修改redo log状态修改为commit
☀慢查询如何定位,如何优化
- 定位和分析慢SQL,启用慢查询日志,定位慢查询的SQL位置,使用explain分析SQL索引是否生效,避免ALL全表扫描
- 建立和优化索引,对高频查询条件、连接条件、排序和分组条件中的字段建立索引,删除不必要的索引减少维护开销,使用前缀索引减少索引空间开销等
- 避免索引失效场景,避免如索引列使用函数、隐式类型转换,或左模糊匹配
- 查询优化,精简查询字段避免使用select *;使用覆盖索引避免回表操作,提升磁盘I/O效率;拆分连表join改为单表查询,将计算逻辑在业务层面中处理;深分页问题则使用延迟关联、书签方式解决
- 优化数据库表,如果单表数据量超过千万级别,需要采用分库分表
- 使用缓存技术,引入缓存层如Redis,使用旁路缓存策略存储热点数据和频繁查询结果
MySQL执行计划,有哪些参数
- type,索引类型包括(system系统表,const主键或唯一索引,eq_ref唯一索引扫描, ref非唯一索引扫描, range索引范围扫描, index全索引扫描, all全表扫描)
- possible_keys,可能采用的索引
- key,实际采用的索引
- key_len,索引长度
- rows,预估扫描的行数
- filtered,过滤后数据百分比
- extra,using filesort 使用外部排序,using index 使用率覆盖索引,using where 使用了where过滤
MySQL主从复制过程
- MYSQL主从复制的过程基于二进制日志binlog实现数据同步,涉及主服务器和从服务器的协同工作
- 主服务器在处理所有修改操作时,会将操作记录写入二进制日志binlog中
- 主服务器会创建一个binlog dump线程负责读取binlog日志内容并发送给从服务器
- 从服务器会创建一个I/O线程将binlog写入中继日志relay log
- 从服务器的SQL线程会读取读取relay log中的日志,并逐条执行
MySQL主从延迟如何解决
- 强制走主库方案,对于大事务及实时性要求高的操作,直接在主库执行,避免从库延迟
- 启用并行复制提高从库SQL线程的并发处理能力
- 优化网络和硬件,提升主从库的带宽,在同一局域网部署减少网络延迟,确保从库硬件性能与主库匹配
MySQL分库分表有哪些类型
- MYSQL分库分表类型主要分为垂直拆分和水平拆分两大类,垂直拆分又可以分为垂直分库和垂直分表,水平拆分可以分为水平分库和水平分表
- 垂直分库,按照业务模块或功能将不同的表划分到不同的数据库中
- 垂直分表,将单表按列拆分,按照字段相关性再划分到多张表中,减少单表体积量,提升I/O效率
- 水平分库,按照分片规则将单库数据分散到多个库中,解决单库并发与存储瓶颈
- 水平分表,将单表数据按行拆分到多个表中,降低单表数据量,优化查询性能与写入效率
☀Redis为什么快
- Redis基于内存存储,采用key-value形式存储数据,可以快速存取数据,避免了传统数据库的磁盘I/O瓶颈
- Redis采用单线程模型,Redis主线程采用单线程执行命令,避免了多线程上下文切换和锁竞争开销
- Redis采用I/O多路复用技术,使用epoll等技术单个线程即可管理数万个socket连接,并以事件驱动模式实现非阻塞处理保证高吞吐量
- Redis提供了多个高效的数据结构,比如String、List、Set、Sorted Set,针对不同场景可以使用不同的数据类型支持快速数据操作
☀Redis有哪些应用
- 缓存,存储高频访问数据作为数据库前置缓存层,缓解数据库压力
- 分布式锁,使用setnx命令实现多节点的互斥锁,配合过期时间避免死锁
- 排行榜,使用有序集合zset的排序特性构建实时排行榜
- 会话管理,利用hash结构存储用户等会话信息
- 统计计数,使用自增等原子命令实现点赞数、访问数统计,使用hyper loglog实现基数统计
- 限流,通过滑动窗口算法或者令牌桶算法
- 消息队列,使用List结构实现阻塞任务队列,或者通过发布订阅模式
ZSET为什么使用跳表而不使用B+树
- 跳表通过多层有序链表结构实现快速查找,其单个节点平均只存储1.33个指针(值、后续指针、跨度),使用内存更加紧凑,更符合CPU的缓存局部性原理,适合高频内存操作,B+树专为磁盘设计,单个节点存储大量索引指针,加载节点会占据大量内存,不适用于内存模式
- 跳表的数据结构实现难度比B+树低,B+树需要处理复杂的节点分裂、合并与平衡操作,降低了开发和维护成本,符合redis的设计原则
- 跳表的插入和修改仅需要修改相邻节点的指针,通过随机层高实现节点平衡(创建节点时,每层以0.25的概率增加,低于0.25停止,高度level决定了该节点会插入到[0,level]级链表),B+树插入可能触发节点分裂及合并,虽然时间复杂度都是Ologn,但实际跳表写入速度会比B+树效率更高
介绍下Redis中的字符串类型String
- String底层采用SDS简单动态字符串实现的
- O1获取字符串长度,额外使用len记录了当前字符串的长度,不需要通过遍历去计算长度
- 具备自动扩容,根据alloc(分配给字符数组的空间长度)-len检查空间是否满足,如果空间不足,SDS会自动扩容
- SDS使用二进制存储数据,而不是使用ascii码存储
介绍下Redis中的压缩列表ziplist
- Redis中的压缩列表ziplist是一种为了节省内存而设计的紧凑型顺序数据结构,采用连续内存块存储多个元素
- 压缩列表表头有三个字段,zlbytes整个压缩列表占用,zltail尾偏移,zllen节点数量
- 当中存储压缩列表节点entry,包括prevlen前一个节点长度,encoding类型和长度,data实际数据
- zlend用于表级压缩列表末端
- ziplist的优势在于内存利用率高,适合存储小规模数据,但存在插入连锁更新问题,引发后续节点prevlen字段级联调整,影响性能
介绍下Redis中的快速列表quicklist
- redis的list早期使用ziplist和linkedlist,少时用ziplist,多时用linkedlist,为了减少链表前后指针占用字节数过大导致内存浪费的问题,Redis使用quicklist进行了优化
- quicklist的核心思想是结合压缩列表和双向链表的优势,每个quicklistNode节点存储一个固定大小的压缩列表quicklist,在通过双向链表串联多个qucicklistNode节点
介绍下Redis中的字典,扩容重哈希过程
- 字典底层是一种基于哈希表实现的键值对数据结构,其核心结构包含2个哈希表ht0和ht1
- 字典的扩容过程采用渐进性重哈希,当哈希表ht0空间不够,给哈希表ht1分配空间用于扩容,在重哈希过程中,每次哈希表的修改操作,会将ht0对应的键值对重哈希到ht1中,直至ht0全部迁移完成,释放ht0, 并将ht1设为新的ht0(如果过程中键值ht0没找到,就找ht1)
Redis如何实现I/O多路复用的
- Redis的I/O多路复用使用Reactor模式和epoll实现,通过epoll_create创建epoll对象
- 当一个客户端与服务端连接时,redis会将每个socket对应的文件描述符FD,使用epoll_ctl注册到监听队列(红黑树)中
- 当客户端调用accpet连接、read读、write写等操作命令时,会将命令封装成事件绑定到对应的FD上,epoll监听到文件描述符FD有事件,就会将其丢入到epoll的就绪队列
- 而Redis的主事件循环会调用epoll_wait函数(读取就绪队列数据)阻塞等待事件的到来,如果某个FD数据到达,就会触发对应的事件处理器进行处理,读事件读取客户端的发送数据会解析命令并执行,写事件则将结果返回客户端
Redis哪些地方使用多线程
- 在Redis6.0之后,Redis采用多线程处理客户端连接的读写请求,将网络I/O的读写任务分配给多个工作线程执行,提高高并发下的吞吐量
- 数据持久化操作,包括RDB快照生成,AOF日志重写,都是通过后台线程异步执行,防止主线程阻塞
- 耗时任务的执行,例如调用unlink进行大健的删除
☀Redis如何保障数据不丢失
- Redis为了实现数据不丢失,提供了多种持久化机制,包括RDB、AOF和混合持久化
- RDB持久化,是一种Redis中的一种快照持久化方式,它会定期生成二进制的内存快照文件,保存了某一刻的全量数据;优点是文件体积小、恢复速度快,缺点是可能会丢失一段时间内的数据
- AOF持久化,向文件末尾追加写入每个写操作命令,并定时重写进行压缩;优点是提供了更好的数据安全性,缺点是文件体积大,重放操作命令导致恢复速度慢
- 混合持久化,结合了RDB和AOF,这种方式首先以RDB格式保存当前数据的快照,后续追加写操作命令,兼备数据恢复数据和数据安全性
AOF持久化执行流程
- 每次执行写操作时,会将所有修改操作追加到AOF缓冲区
- 根据配置的刷盘策略(always每次都fsync、everysec每秒、no操作系统调度),将缓存区的内容写入到AOF文件
- 随着AOF文件增大,会触发AOF重写bgwriteof机制,主线程会fork生成子进程,基于内存数据快照生成新的AOF临时文件,仅保留最终数据状态的命令,重写期间的写入命令同时追加到AOF缓冲区和重写缓冲区,保证数据一致性
- 子进程完成后,用新的AOF文件替换旧文件
- 重启时优先加载AOF文件,逐条回放命令恢复内存数据
RDB持久化执行流程
- 根据配置的save规则,定期触发RDB持久化
- 当触发RDB持久化时,主线程调用会fork生成子进程负责数据快照的生成
- 子进程会遍历内存中所有的键值对,将其序列化为二进制格式写入到临时RDB文件中
- 子进程完成后,用新的RDB文件替换掉旧的RDB文件,完成持久化
☀Redis使用的过期删除策略
- 过期删除策略主要有定时删除、惰性删除、定期删除,Redis主要使用惰性删除和定期删除
- 定时删除,如果某个键一旦过期,就立即触发删除,缺点是占用CPU时间删除与任务无关的过期键上,对系统吞吐量造成影响
- 惰性删除,当Redis访问某个键时,检查键是否过期,如果过期立即删除并返回控制,否则正常返回数据
- 定期删除,Redis定期从设置了过期时间的键集合中随机抽取一部分键(默认15个)进行检查,并删除其中已过期的键
☀Redis内存淘汰策略
- noeviction,当redis达到最大内存限制时,不进行任何淘汰操作,而是直接返回错误
- volatile-ttl,优先淘汰即将过期的键值
- volatile-random,随机淘汰设置了过期时间的任意键值
- volatile-lru,淘汰设置了过期时间的键中最近最少使用的
- volatile-lfu,淘汰设置了过期时间的键中最少使用的
- allkeys-random,随机淘汰所有键任意一个键
- allkeys-lru,淘汰所有键中最近最少使用的
- allkeys-lfu,淘汰所有键中最少使用的
怎么判断Redis某个节点是否正常工作
- 使用ping命令,如果redis节点正常工作,会返回pong,否则出现问题
- 使用info命令,返回redis节点的详细运行信息例如内存使用率、连接数等,判断节点是否处于正常状态
- 使用cluster info命令,查看redis集群状态,查看cluster_state是否为ok,如果是fail说明有节点不可用
- 使用telnet,测试redis节点端口是否可达
- 采用监控系统,配置Prometheus、grafana等监控工具,实时监控redis性能指标如内存使用率、qps、延迟等
☀Redis常见的数据类型
- String字符串
- hash哈希表
- list列表
- set集合
- zset有序集合
- hyperloglog基数统计
- bigmap位图
- geo地理空间
- stream消息队列
☀缓存穿透、缓存击穿、缓存雪崩,如何解决
- 缓存穿透,请求访问缓存和数据库都不存在的数据,导致请求直接穿透到数据库层;解决方案,缓存空值、布隆过滤器(预先存储有效对象,快速过滤不存在对象)
- 缓存击穿,热点数据过期失效时,大量并发请求直接打到数据库;解决方案,分布式锁(保证只有一个线程构建缓存,防止同一时间访问数据库),热点数据不过期(通过异步更新缓存)
- 缓存雪崩,大量缓存同时过期或缓存集群故障引发,导致瞬时大量请求访问数据库;解决方案,过期时机随机化(在固定过期时间加上随机额外时间),构建集群(保障服务可用性),热点数据不过期(通过异步更新缓存),多级缓存架构(使得分布式缓存不可用时,本地缓存生效),限流和降级(实现请求限流或者服务不可用时返回兜底数据保护数据库)
布隆过滤器原理、优缺点、应用场景
- 布隆过滤器原理是一种基于概率的数据结构,通过位数组和多个哈希函数实现快速存在性判断
- 添加元素时,使用k个哈希函数计算得到k个位索引设置为1
- 查询元素时,使用k个哈希函数计算对应的k个位索引是否都为1,如果有一位为0,则代表不存在,如果全为1,则代表可能存在,有误判性
- 优点是可以仅存储哈希标记而非原始数据,内存消耗低;查询和插入时间复杂度为O(k);不存储具体数据,安全性强;缺点是存在误判率,可以通过增大m和k降低概率,不支持删除,只能用于判断存在性
- 主要的应用场景有缓存穿透防护、海量数据判重等
什么是热key问题,如何解决
- 热key指Redis中某个key在短时间内被高频访问,导致单个节点负载激增问题,使用hotkeys命令或者通过监控框架京东hotkeys或者Prometheus探测
- 热点分散,可以通过访问键时添加随机后缀的方式,将热key复制到多个redis节点,通过分片查询的方式分担单个节点的读压力
- 读写分离,增加redis从节点处理读请求,分担单节点读压力
什么是大key问题,如何解决
- 大key指单个键值对应的value占用过大的内存空间,导致主线程阻塞、网络带宽占用过高、数据倾斜等问题,可以通过redis-cli bigkeys或者memory usage获取内存占用
- 数据拆分,将大key按业务逻辑拆分成多个小key,在业务层在进行合并
- 使用压缩算法,压缩大key数据来减少空间存储和网络传输消耗
- 对大key进行清理,使用unlink异步删除大key,避免阻塞
Redis实现分布式锁的原理
- 方法一使用set key value nx ex 上锁,通过将nx和expire作合并防止出现分步操作不是原子性的问题;并且设置过期时间防止客户端宕机导致死锁问题;同时并生成全局唯一标识作为锁的value值,在释放锁时通过lua脚本校验标识,确保只有锁持有者才能执行删除操作(保证校验和删除是原子操作),防止其他客户端误删;setnx不具备锁续期的功能,可能因为某些业务处理时间超过锁过期时间的情景
- 方法二使用redisson,redisson实现了锁的可重入性,通过计数器记录同一线程的重入次数,避免其他线程误删问题;redisson的看门狗机制会设置后台线程定期检测并延长锁的过期时间,实现锁的自动续期;redisson支持redlock算法,在集群模式下需要半数以上节点加锁成功才算成功,保障可靠性
Redisson看门狗机制
- Redisson看门狗机制是一种用于分布式锁自动续期的设计,解决因为业务执行过长导致锁过期释放的问题
- 当客户端获取到锁后,如果没有显式指定锁的过期时间,redisson会启动一个后台线程每隔锁超时时间的1/3(默认30s,所以默认10s)检测(确保仅当前持有锁的线程可以续期)并延长锁的过期时间至看门狗时间(默认30s)
- 如果客户端崩溃或者线程终止,看门狗则停止续期,锁最终因超时自动释放,避免死锁
☀如何保证缓存数据库的数据一致性
- 旁路缓存策略,读操作时如果缓存未命中,则从数据库读取数据并回填缓存,写操作先更新数据库然后删除缓存;在并发查询可能存在读取旧数据回写缓存问题,可通过延迟双删策略缓解;但是可能存在缓存删除操作失败问题
- 引入消息队列,将缓存删除操作交给消费者处理,如果缓存删除操作失败,消息队列的重试机制可以重新进行尝试删除缓存,但是会造成业务代码入侵
- 结合数据库订阅+消息队列,通过canal监听数据库binlog日志,当数据库数据修改后将对应的binlog数据推送到消息队列,由消费者异步删除缓存,并且通过消息队列的重试机制保证操作成功,可以降低业务代码入侵
- 兜底策略,设置缓存过期时间保证数据最终一致性
介绍下Redis主从机制,如何同步数据的
- Redis主从机制是一种基于主节点和从节点的数据复制模型,实现数据备份、读写分离和高可用性。主节点负责写操作,并将数据变更同步到从节点,从节点支持读操作,通过复制主节点数据保持一致性
- 数据同步过程可以分为全量同步和增量同步
- 全量复制,当从节点首次连接主节点或主从关系重建时,主节点会生成当前数据的RDB快照,并将其发送给从节点,从节点接收并加载RDB文件后,完成初始数据同步
- 增量同步,此后主节点将新的写命令记录到复制缓冲区(环形缓冲区,可能被覆盖),并通过异步方式持续发送给从节点,从节点接收并执行命令,维持数据一致性。如果网络中断后恢复,从节点会基于偏移量从缓冲区中续传增量数据(如果要读取的数据已经不在环形缓冲区内,将采用全量同步方式)
Redis哨兵机制,选主机制
- Redis哨兵机制是Redis实现高可用的方案,主要用于监控主从集群中的主节点状态,并在主节点故障时自动触发故障转移
- 每个哨兵实例定期向所有主节点发送心跳检测,检测节点健康状态,若主节点超时未响应,则标记为主观下线;此时哨兵会询问其他哨兵,当超过半数(quorum)的哨兵确认主节点故障后,则标记为客观下线,触发故障转移
- 哨兵集群会通过raft选举算法,选举出leader,从故障的主节点中从节点选举新节点的过程
- 哨兵leader先过滤网络不稳定和已下线的从节点,选择从节点优先级最高的节点,如果优先级相同则选择复制偏移量最大的节点,如果还是一样就选择运行ID最小的节点
- 补充,raft算法是一种分布式一致性算法,每个哨兵节点会向其他所有哨兵发送投票请求,如果某个节点获得超过半数的投票,则选举为leader负责后续的故障转移操作,否则选举失败重新发起投票
- 补充,脑裂问题是指分布式集群中由于网络分区或主从节点无法通信,导致多个主节点同时存在的现象,导致从节点推举为为新的主节点,原先写入的主节点数据丢失,最终引发数据不一致的问题;解决方案,可以通过同步延迟(主从复制时间)限制主库写入条件,避免脑裂期间数据不一致
Redis集群模式,如何划分切片的
- Redis集群模式采用多主多从的结构,每个主节点复杂处理数据,从节点作为数据备份,支持读写分离、自动故障转移保障高可用,并通过数据分片的方式保障高性能,根据实际需求增加和减少节点保障高拓展
- 在切片机制中,redis通过哈希槽实现数据分片,集群将键空间划分为16384个哈希槽,每个主节点分配一部分哈希槽;数据写入时,使用CRC16算法计算键得到16位哈希值,再对16384取模确定所属哈希槽,最终路由到对应节点
消息队列模型
- 点对点模型,基于队列实现,生产者将消息发送到队列,消费者通过竞争机制从队列拉取消息,一个消息只能被一个消费者消费,消费成功后消息从队列删除。
- 发布订阅模型,基于主题topic实现,生产者将消息发布到主题,所有订阅该主题的消费者均可以接收到消息副本,一条消息可以被多个消费者消费(例如kafka的多个消费者组可以订阅同一个主题,一条消息被多个消费者组消费,一条消息只会被同一个消费者组的任一消费者消费)
☀消息队列使用场景
- 系统解耦,通过消息队列实现不同服务间的松耦合,生产者只需要将消息发送到队列,无需关注消费者具体实现
- 异步处理,将耗时操作从主流程中剥离出来,发送到消息队列异步处理,提升响应速度
- 削峰填谷,应对突发高并发场景,消息队列可以通过缓存的方式平台流量,按照消费者的处理能力消费消息,避免瞬时流量压垮系统
☀消息队列架构设计
- 注册中心,负责服务发现和路由管理,生产者、消费者和broker启动时向其注册元数据,实现松耦合和动态扩缩容
- 生产者,负责生成和发送消息到broker,根据业务逻辑封装消息,并通过消息投递到指定的topic或队列
- 消费者,消费者订阅特定topic或队列,从broker拉取消息,处理完成后向broker发送确认以确保消息不丢失,支持集群消费和广播模式
- broker,作为消息代理,承担消息的接收、存储、持久化和路由分发功能,通过主从复制或分区副本实现高可用,并管理消息的重试、死信队列等机制
☀三种常用消息队列的区别
- Rabbitmq基于AMQP协议,具有复杂的路由功能,支持多种交换机类型,Kafka采用分区副本机制实现数据备份,Rocketmq采用主从机制实现,支持半消息机制的分布式事务
- 性能与吞吐量,Kafka的吞吐量百万级低延迟,rocketmq十万级低延迟,rabbitmq吞吐量万级但延迟最低
- 消息持久化与可靠性,三者都支持消息持久化,rabbitmq数据存储在队列,kafka的数据存储在分区,rocketmq的数据存储在commitlog
- 延时消息支持,rabbitmq可以借助插件或死信队列实现延迟消息,rocketmq原生支持延迟消息,kafka不直接支持
☀消息队列如何处理消息重复
- 消费者端的业务逻辑处理应该实现幂等性,确保同一消息多次处理的效果一致
- 可以通过消息全局唯一标识判重,使用数据库唯一索引进行约束防止数据重复插入
- 业务状态机判断,消费时校验状态,跳过已经消费完成的消息
- 悲观锁机制,利用分布式锁机制对消息ID上锁,确保同一时间只有一个消费者线程处理
- 乐观锁机制,使用版本号+乐观锁机制,防止重复操作
☀消息队列如何保证消息不丢失
- 发送者端,生产者通过同步发送模式并等待Broker的ACK确认,确保消息成功刷盘
- broker端,broker需要开启同步刷盘和多副本机制,确保消息写入磁盘,且消息
- 消费者端,消费者使用确认机制,只有业务逻辑处理成功后提交确认,如果处理失败触发重试,多次重试失败后发送到死信队列供人工排查
☀消息队列如何保证消息有序
- 消息的有序性是指消息的消费顺序与消息发送顺序保持一致
- 队列路由策略,保证某业务逻辑分组下的消息顺序,需要通过消息的唯一标识计算哈希值,并路由到同一个队列中
- 消费有序性,同一队列仅由一个消费者进行处理保证顺序消费****
☀消息队列如何处理消息积压
- 增加消费者实例,如果topic的队列数量大于消费者数量,可以对消费者进行扩容,单个队列对应单个消费者,提高消费能力
- 消息转储分流,临时扩容TOPIC和新消费者,并将积压的消息转发到新topic中进行消费
- 限流与降级,对生产者限流,限制新消息产生速度;在特殊情况下,对系统进行降级,关闭非核心业务
Rocketmq和Kafka的确认机制有什么不同
- 生产者确认机制,Kafka的消息确认机制有三种模式,分别是ACK=0,异步发送,ACK=1,同步发送但是只等待leader的确认,ACK=all,生产会在消息发送后等待所有分区的确认;Rocketmq的消息发送方式有三种,同步发送(包括同步刷盘和主从复制完成),异步发送并等待回调,单向发送
- 消费者确认机制,Kafka通过偏移量提交实现确认,开发者调用commitSync或者commitAsync手动提交offset或者选择处理完成后自动提交offset;Rocketmq支持显示调用acknowledge实现手动调用或者处理完成后自动确认
Rocketmq和Kafka的broker有什么区别
- kafka采用分区独立存储,每个topic的partition分区对应独立的日志文件(segment分段存储),多分区随机写会导致性能劣化
- rocketmq采用commitlog统一存储+consumequeue消费队列,所有消息顺序写入commitlog文件,消息队列中的消息存储了commitlog中的物理偏移量和长度,通过顺序写入避免了kafka的分区随机写问题
- rocketmq采用broker主从架构,主节点将消息同步至从节点,依赖namserver协调主备切换;Kafka则采用分区副本机制,由zookeeper或者Kraft协议管理副本同步状态,通过选举leader保障可用性
- 补充,rocketmq还额外存储了indexFile索引文件,提供了一种可以通过key或时间区间来查询消息方法
Rocketmq如何实现延时消息
- rocketmq的延时消息的实现是通过将任务发送到一个延时TOPIC的队列,由ScheduleMessageService通过轮询的方式判断哪些消息时间到期,到期后将延时TOPIC队列中的消息发送到原始topic队列中
Rocketmq如何实现分布式事务
- rocketmq基于两阶段提交思想和半消息实现分布式事务
- 生产者向Broker发送一条半消息,broker持久化后返回确认,此时消费者不可见
- 生产者收到半消息确认后,执行本地事务
- 如果本地事务成功,生产者发送commit,broker将半消息转移到目标topic,消费者可见并消费
- 如果本地事务失败,生产者发送rollback,broker删除半消息。
- 如果因为网络原因或者宕机未收到确认,触发消息回查,向生产者查询事务状态,如果超时则默认回滚事务,删除半消息
Rocketmq如何实现消息过滤
- 使用tag过滤,在发送消息时设置单一TAG
- 使用SQL过滤,消费者使用SQL表达式过滤
Rocketmq刷盘机制
- 同步刷盘,在生产者发送消息到broker时,将消息刷到commitlog日志后才返回给生产者确认
- 异步刷盘,在生产者发送消息到broker后写入PageCache直接返回确认,由后台线程批量刷盘
Rocketmq为什么不使用Zookeeper作为注册中心
- Zookeeper遵循CP的性质保证强一致性,在选举期间集群会进入不可用状态;而注册中心的核心职责是服务发现,最大限度保障可用性AP,rocketmq的nameserver即使部分节点宕机,其余节点仍可提供服务,避免了服务不可用
- zookeeper的ZAB协议需要同步事件日志和数据快照,写入性能受限于多数节点确认;Nameserver仅维护broker的元数据(地址、topic路由),采用内存存储且无复杂事务机制,通过心跳机制更新数据,同时可以通过水平扩容灵活应对高并发场景,更加轻量化
- 消息发送应该弱依赖于注册中心,生产者和消费者首次获取broker地址后会在本地缓存路由信息,后续通信直接与broker加护,即使注册中心不可用,仍可以基于缓存支持短时消息收发
Kafka是采用推还是拉模式
- 生产者到broker采用推模式,消费者到broker采用拉模式
- 生产者采用push模式将消息主动推送到broker,由broker承担消息持久化和副本管理责任,生产者无需本地保存数据,降低了生产者消息可靠性要求
- 消费者通过拉模式的好处是,消费者可以根据自身处理能力调整拉取频率,避免因broker推送速度太快导致消费者过载;
Kafka为什么比Rocketmq快
- Kafka通过sendfile函数实现零拷贝(sendfile会直接将磁盘加载到内核缓冲区,再将内核缓冲区的数据传输到网卡,仅需2次用户态内核态切换,2次数据拷贝),而rocketmq使用的mmap+write(mmap磁盘加载到内核缓冲区,再调用write后,内核缓冲区传输到socket缓存区,socket缓冲区传输到网卡,4次用户态内核态切换,3次数据拷贝)
- 批量处理与压缩机制,Kafka的生产者段将多个小消息合并成批量消息再发送,减少网络I/O次数,同时支持批量压缩降低网络传输开销
- 磁盘顺序写入优化,Kafka和Rocketmq都采用顺序写入磁盘的方式,避免了随机写的开销
☀线程,进程,协程的区别
- 进程是操作系统分配资源的基本单位,进程直接相互隔离,每个进程拥有独立的虚拟内存空间(代码段、数据段、堆、栈)和系统资源(文件句柄),进程切换需要保存整个进程的上下文,切换开销大
- 线程是CPU调度的基本单位,是进程内的执行单元,同一进程内的线程共享内存和资源,每个线程有自己独立的栈和程序计数器,线程切换需要保存堆栈、寄存器和程序计数等,切换开销较小
- 协程是用户态的轻量级线程,由程序显式调度,共享线程资源,切换仅需保存栈和寄存器,无需进行内核态用户态切换,切换开销最小
进程分配资源的资源有哪些
- 文件句柄
- 虚拟内存
- 信号量
☀进程调度算法
- 先来先服务,按照队列顺序执行线程
- 短作业优先,优先执行运行时间最短的进程
- 时间片轮转,每个进程分配一个固定时间片
- 优先级调度,根据进程优先级执行,支持抢占式(高抢低)和非抢占式
- 高响应比优先,响应比=(等待时间+运行时间)/运行时间,选择最高响应比进程执行
- 多级反馈队列,将进程分配到多个队列,高优先级队列时间片短,低优先级时间片长,如果时间片内未执行完,则降级到下一级队列
☀进程间通信
- 共享内存,进程间通过共享内存实现数据共享
- 通道,分为匿名管道和命令管道,匿名管道适用于父子进程通信,命令管道适用于任意两个进程通信
- 消息队列,通过消息队列实现异步通信
- 信号,异步通信机制,通过信号来通知
- 信号量,PV操作实现进程间同步
- socket,不同主机之间的进程通信
☀线程比进程高效的原因
- 资源分配与共享机制,线程作为进程的执行单元,共享进程资源,避免了跨进程资源访问的开销,线程间通信效率更高;而进程需要独立的地址空间和资源管理,进程间资源访问需要借助管道、消息队列、共享内存等机制,因此线程比进程更高效
- 创建和切换开销,线程的创建和切换只需要栈信息、寄存器、程序计数器等信息,而进程的创建和切换需要完整的地址空间、资源分配,线程开销比进程更小
- 线程的并行执行效率更高,多个线程可以在多个cpu核心上并行执行,提高程序的执行效率,由于线程间共享进程资源,通信和数据交换更加高效;进程间由于资源隔离,无法像线程那样充分利用多核资源
☀进程的五种状态
- 创建态,进程正在初始化,操作系统为进程分配进程控制块PCB(PID、进程状态、调度信息、程序计数器、内存地址、资源清单、寄存器信息)
- 运行态,获取到时间片,进程占用cpu执行指令
- 就绪态,进程等待操作系统调度分配
- 阻塞态,进程因等待外部事件(I/O、锁释放)主动让出CPU,进入阻塞队列
- 终止态,进程完成执行或者异常退出
☀用户态和内核态是如何切换的
- 用户态和内核态的切换主要有三种方式
- 应用程序通过系统调用,CPU从用户态切到内核态,内核会找到中断向量表找到中断处理程序并执行,完成后返回用户态
- 程序运行发生异常错误(缺页异常、除零错误),cpu自动切换到内核态处理异常
- 外部设备事件(磁盘I/O、网络数据达到)触发中断信号,CPU暂停当前用户态任务,保存现场,并切换到内核态,找到中断向量表并执行中断处理程序
☀用户态和内核态,为什么要划分用户态和内核态
- 用户态和内核态是操作系统中的两种运行模式,主要作用是隔离用户进程和系统资源,保证系统的安全性、稳定隔离性
- 安全性,用户态是普通程序运行状态,只能访问受限的系统资源,无法直接操作硬件;内核态是操作系统内核运行的状态,拥有最高权限,可以直接访问CPU、内存以及外设
- 稳定隔离性,用户态程序出现问题时,不会影响内核运行,避免程序故障导致系统崩溃
什么是虚拟内存和物理内存
- 物理内存是计算机硬件中实际存在的内存
- 虚拟内存是由操作系统通过分页或者分段的方式为进程提供连续的虚拟地址空间,并将这些页或者段映射到物理空间,实际数据可能存储在物理内存或者磁盘交换区中
怎么将虚拟地址转化为物理地址的
- 在操作系统中虚拟地址到物理地址的转化是通过内存管理单元MMU通过多级页表机制实现的
- 虚拟地址拆分为虚拟页号和页内偏移量,首先通过访问快表TLB查询是否包含对应的页表项,如果命中直接获取物理页号,如果没有命中需要访问页表
- 多级页表记录了页号之间的多级关系,可以由一级页号从一级页表查找到二级页表地址,再用二级页号从二级页表中找到物理页帧地址,最后再将物理页帧地址和页偏移拼接得到最终的物理地址
什么是分段,什么是分页
- 分段是按照程序的逻辑地址空间划分为长度可变的段,每个段代表程序中的一个逻辑单元。虚拟地址由段号和段内偏移量组成,通过段表查询段的基地址转化为具体物理地址;分段的优势在于符合程序逻辑结构,便于数据隔离,但可能造成外部碎片
- 分页是将虚拟和物理空间均划分为固定大小的页,虚拟地址由页号和业内偏移量组成,通过页表映射到具体物理页帧;分页的优势在于消除外部碎片,提高内存利用率,缺点是存在内部碎片
什么是缺页中断
- 缺页中断是当程序访问的虚拟地址空间中的某个页面未被加载到物理内存中,由内存管理单元MMU触发的中断
- 如果分配页面时,内存不足触发页面置换算法,将旧页面置换到磁盘中
- 从磁盘将缺失的页面加载到内存,并更新页表映射
- 重新执行触发缺页的指令,完成内存访问
页面置换算法有哪些
- OPT,最佳置换算法,替换未来最长时间不会被访问的页面
- FIFO,先来先服务,优先淘汰最先进入内存的页面
- LRU,淘汰最近最少使用的页面
- LFU,淘汰最少使用次数的页面
- CLOCK,时钟页面置换算法,将页面保存在环形链表中,如果遇见访问位是0就淘汰,是1就置为0,直到找到一个访问位为0的页面
程序的内存布局
- 从高地址到低地址排列,可以分为7个部分
- 内核空间,为系统调用和中断处理保留,用户程序无法直接访问
- 栈段,函数调用时存储的局部变量和参数存储,向低地址增长
- 内存映射段,用于文件映射(动态库加载)和共享内存
- 堆段,为程序malloc或new动态分配的内存,向高地址增长
- BSS段,存放未初始化的全局变量和静态变量
- 数据段,存放初始化的全局变量和静态变量
- 代码段,存在可执行指令
select、poll、epoll
- select、poll和epoll都是I/O多路复用的核心机制
- select和poll均采用轮询方式遍历所有文件描述符FD,时间复杂度为ON,select通过长度为1024的位图存储文件描述符,而poll改用动态数组,突破FD个数限制;epoll,通过红黑树管理文件描述符FD,通过epoll_ctl加入红黑树,epoll基于事件驱动,当socket事件发生时,通过注册的回调函数将就绪事件放到就绪队列,程序通过调用epoll_wait函数返回事件发生的文件描述符个数,避免了轮询的开销
什么是零拷贝技术
- 零拷贝技术是一种通过减少数据在用户空间和内核空间直接的冗余复制操作来提升系统性能的优化机制,核心原理是绕开传统I/O流程中调用read和wrtie多次数据拷贝的开销
- 而零拷贝技术通过操作系统提供的mmap和sendfile系统调用,结合DMA机制将数据直接从内存缓冲区传输到目标设备,避免了用户空间的中间复制操作
- mmap,数据从磁盘写入内核缓存区,调用write,内核缓存区写入socket缓冲区,socket缓冲区写入外设,4次用户态内核态切换,3次拷贝
- sendfile,数据从磁盘写入内核缓存区,内核缓存区直接写入外设,2次用户态内核态切换,2次拷贝
☀OSI七层模型和TCP/IP模型
- OSI,应用层(负责给应用程序提供统一接口),表示层(负责数据转换、压缩和加密),会话层(负责管理实体间的会话),传输层(实现端到端的数据传输),网络层(负责数据的路由、转发、分片),数据链路层(负责数据帧和差错检测),物理层(物理网络中传输数据帧)
- TCP/IP模型,应用层,传输层,网际层,网络接口层
每一层对应了哪些网络协议
- 应用层,HTTP、DNS、FTP、TELNET、SMTP
- 传输层,TCP、UDP
- 网络层,ICMP,IP
☀从输入 URL 到页面展示到底发生了什么
- 浏览器解析URL获取协议、域名和路径,通过DNS将域名转化为得到服务器IP地址(如果本地域名服务器缓存无记录,则递归查询DNS服务器获取ip)
- 浏览器通过TCP/IP协议与服务器三次握手后建立可靠连接,如果使用HTTPS,则需要通过TLS/SSL协议加密
- 浏览器构建HTTP请求报文(包括请求行、请求头和请求体),通过TCP连接发送至服务器
- 服务器处理完请求后,构建HTTP响应报文(状态码、响应头、响应体给)返回给浏览器
- 浏览器接收到响应报文后,开始解析响应体中的HTML内容,渲染页面
- 当浏览器与服务器四次挥手后,断开TCP连接
☀DNS 解析的过程是什么样的
- 先请求本地域名服务器,如果有缓存域名和IP地址的关系,直接返回
- 请求根域名服务器,找到.com的地址
- 请求顶级域名服务器,找到xxx.com的地址
- 请求权威域名服务器,找到最终的ip地址返回给本地域名服务器
HTTP报文结构
- HTTP报文可以分为请求报文和响应报文
- 请求报文由请求行、请求头、空行、请求体构成;请求行包括请求方法、请求资源路径和协议(GET /index.html HTTP/1.1);请求头,包括请求的附加信息,比如host域名、user-agent客户端信息、content-type数据格式等;空行,用于分割首部和主体,由CRLF回车换行符表示;请求体,包含在请求中需要携带的数据
- 响应报文由响应行、响应头、空行、响应体构成;响应行包括协议、状态码和状态信息(HTTP/1.1 200 OK);响应头包括响应的附加信息如content-type数据格式、content-length数据长度等;空行,用于分割首部和主体,由CRLF回车换行符表示;响应体,包含响应的数据,服务器返回的资源内容如HTML或JSON
HTTP请求类型有哪些
- GET,用于请求获取指定资源
- POST,向服务器提交资源,通常用于提交表单数据或资源创建
- PUT,用于向服务器更新指定资源,通常用于更新已存在资源
- DELETE,用于请求服务器删除指定资源
- HEAD,类似于GET请求,只用于获取报头,返回的响应中没有具体的内容
☀GET和POST的区别
- GET通常是幂等的读取操作,用于请求获取指定资源,不会改变服务器状态;POST通常用于资源创建,可能会触发服务器状态改变
- GET的请求参数附加在在url中,POST将请求数据放在请求体中
- GET受浏览器URL长度限制,POST适合提交大量数据
☀HTTP 状态码有哪些
- 1xx代表请求被接收,100需要客户端继续发送请求,101网络协议转换
- 2xx代表请求被成功处理,200请求成功,201服务器资源创建成功,204处理成功但无返回内容
- 3xx代表重定向,301永久重定向,302临时重定向,304资源未修改,使用缓存
- 4xx代表客户端请求错误,400请求参数或格式不正确,401需权限认证,403服务器拒绝请求,404请求资源不存在,405请求方法不支持
- 5xx代表服务端处理请求错误,500服务端内部错误,502网关代理接受无效响应,503服务器过载或维护,504网关代理未能即时收到请求
☀HTTP和HTTPS的区别
- HTTP协议是超文本传输协议,通过明文传输数据,不提供任何加密措施,通信内容容易被窃听或篡改;而HTTPS在HTTP的基础上引入SSL/TLS协议实现加密传输,采用非对称加密协商会话密钥和会话密钥对称加密数据,保证传输的安全性
- HTTPS要求服务器部署由CA机构签发数字证书,用于验证服务器身份和绑定公钥;HTTP无需证书,无法保证服务端的合法性
- HTTP的默认端口是80,HTTPS的默认端口是443
HTTPS如何建立连接
- 客户端向服务端发送请求
- 服务端接收到请求后,返回自己的数字证书,包含了公钥、颁发机构、数字签名等信息(数字签名是将证书内容HASH值使用私钥加密后得到的)
- 客户端收到服务端的数字证书后,验证证书的合法性(通过公钥解密数字签名得到的HASH是否与数字证书内容通过HASH算法后得到值相等),如果合法,生成一个随机码,公钥加密后得到会话密钥发送给服务端
- 服务端收到会话密钥,并使用私钥解密,得到具体的随机码
- 客户端和服务端通过随机码进行对称加密传输数据
HTTP1.0、1.1、2.0、3.0的区别
- HTTP1.0基于短连接,每次HTTP请求需单独创建TCP连接,请求完即断开连接,性能开销大
- HTTP1.1开始支持长连接(keep alive),默认情况下不会立即关闭连接,允许在一个连接上发送多个请求和响应,减轻连接开销,但是响应需要按顺序返回,因此存在队头阻塞问题
- HTTP2.0开始,使用二进制格式替代文本进行传输;使用头部压缩,减少多个请求中的冗余数据传输;支持服务器主动推送资源;采取多路复用,一个TCP连接上同时进行多个HTTP请求和响应,解决队头阻塞问题
- HTTP3.0基于QUIC协议,基于UDP实现,彻底规避由于TCP丢包导致的请求阻塞问题,并且继承TLS1.3协议实现快速握手
HTTP和WebSocket
- HTTP和Websocket都是基于TCP的应用层协议,核心区别在于通信模式
- HTTP采用单向的请求-响应模型,由客户端发起请求后服务器返回响应,每次交互需要重新建立TCP连接,导致较大的连接开销;而WebSocket通过一次HTTP握手后即升级为全双工协议,建立持久长连接,支持服务器主动推送数据,减少重新建立连接的开销
- HTTP是无状态的,每个请求和响应都是独立的,服务器不会保存客户端的上下文,需要通过cookie、session、token等方式维持会话状态;Websocket连接建立后,双方可保持通信状态,后续交互无需传输冗余信息
HTTP和RPC
- HTTP超文本传输协议是一种通用的应用层协议,通常用于客户端与服务端的通信,基于请求-响应模型;RPC远程接口调用是一种更抽象的协议,目的是调用远程服务能够像调用本地方法一样
- HTTP默认采用HTTP/1.1协议,RPC可以基于TCP、HTTP/2.0协议或其他协议实现
- HTTP1.1使用文本格式传输,RPC使用自定义的序列化方法(protobuf)进行高效传输,传输开销和带宽占用更小
- HTTP更适合用于跨平台调用的场景,RPC更适合用于追求高性能、低延迟的分布式系统内部通信
cookie、session、token的区别
- cookie由服务器生成并存储在客户端,通过HTTP头部传递,用于保存会话标识或简单状态信息;session数据存储在服务器端,适合存储敏感信息,通过session ID与客户端关联,该ID通常通过cookie传递;token,无状态机制,由服务端根据用户信息及签名生成令牌token后返回给客户端存储
- cookie存储在客户端,容易受到CSRF/XSS攻击;session存储在服务器断,可以保护会话数据,但sessionID泄漏可能导致会话劫持;token通过加密签名确保数据安全性,但可能发生token泄漏
- session存在服务器资源占用和分布式环境下的拓展性问题,token无需在服务器存储会话状态,更适合无状态架构和跨域场景
什么是jwt,有哪些字段,优缺点?
- JWT是一种无状态的身份验证和授权机制,由服务端生成,在客户端存储,通过加密算法保护
- JWT由头部head、载荷payload、签名signature三部分构成;头部声明令牌类型以及签名算法;载荷包含用户信息、签发者、过期时间等时间;签名则是通过加密算法对前面两部分加密哈希生成
- 优点是JWT令牌通过在令牌中包含必要的身份验证和会话信息,使得服务器无需存储会话信息;当需要认证时,客户端在后续的请求中携带令牌,服务器对令牌中的身份和用户信息进行解析和验证;这种机制减少了服务器空间开销,并且解决了分布式集群情况下的身份验证和会话管理问题
- 缺点是可能发生JWT劫持,需要使用刷新令牌(每个令牌有一定有效期,过期后需要重新获取令牌,即使泄漏被恶意使用也会很快失效)、黑名单拦截(维护一个令牌的黑名单,如果令牌在黑名单中,则直接拦截)
☀TCP和UDP的区别
- TCP是面向连接的协议,需要通过三次握手建立可靠连接,四次挥手断开连接;UDP是无连接的,直接发送数据包;
- TCP是可靠传输,通过序列号、确认机制、重传机制等方式保证数据传输的可靠性;UDP是不可靠传输,数据包可能丢失、重复和乱序接收
- TCP通过流量控制、拥塞控制,确保网络传输中速度不会拥塞和丢包;UDP不提供这两种机制,不会影响发送的传输速率
- TCP的头部是20-60字节,UDP的头部字节是8字节,TCP头部开销比UDP更大
- TCP是面向字节流传输的,UDP是面向报文传输的
TCP报文头部
- 源端口号,16位,用于标识发送端的应用程序端口号
- 目标端口号,16位,用于标识接受端的应用程序端口号
- 序列号,32位,发送的字节流中的第一个字节的顺序号
- 确认号,32位,发送确认的序列号,即TCP希望收到的下一个序列号
- 首部长度,4位,表示TCP头部长度
- 保留位,6位,为将来使用预留
- 控制位,6位,包含SYN、FIN、ACK、URG、PSH、RST等
- 窗口大小,16位,用于流量控制,表示接收端还能接受数据的字节数(基于接受缓冲区大小)
- 校验和,16位,用于检测数据在传输中是否发生变化
- 紧急指针,16位,指出报文端中的有紧急数据的位置
UDP报文头部
- 源端口号,16位
- 目标端口号,16位
- 总长度,16位
- 校验和,16位
☀TCP如何实现可靠传输
- 连接管理,通过三次握手建立连接,数据传输完成后四次挥手有序断开连接
- 序列号与确认机制,每个数据段分配唯一序列号,接收方通过ACK确认已接收数据的最高连续序列号
- 超时重传机制,发送端未即时收到ACK时触发超时重传,向接收端重新发送数据包
- 流量控制,通过滑动窗口动态调整发送速率,接收方根据缓冲区容量通知窗口大小
- 拥塞控制,结合慢启动、拥塞避免、快重传和快恢复算法,实现动态拥塞窗口控制
- 校验机制,使用校验和验证数据是否错误,如果错误则丢弃数据包要求重传
TCP流量控制
- TCP流量控制是一种动态调整发送速率防止接收方缓冲区溢出的机制
- 接收方通过TCP报文首部的窗口大小字段告知发送方当前可用缓冲区大小,发送方根据该大小限制发送的数据量,确保不超过接收窗口范围
☀TCP拥塞控制
- 慢启动,初始拥塞窗口由1MSS(最大报文段长度)开始,每接收到一个ACK后增加1,第二次发送2个MSS,接收到2个ACK后拥塞窗口增加到4,然后8,16…
- 拥塞避免,当达到慢启动阈值时使用拥塞避免算法,拥塞窗口改为线性增长,每个RTT增加1MSS,当检测到超时或丢包后,将慢启动阈值改为拥塞窗口的一半,拥塞窗口改为1,重新执行慢启动
- 快重传,当接受到3个重复ACK时,不等待超时直接重传丢失报文段
- 快恢复,当触发快重传时,慢启动阈值修改为原先的一半,将拥塞窗口大小调整为慢启动阈值+3(接收到三个快重传的ACK),之后执行拥塞避免算法线性增加拥塞窗口
☀TCP三次握手
- 第一次握手,是客户端发送SYN报文请求建立连接(SYN=1,seq=x),进入SYN_SENT状态
- 第二次握手,服务端收到SYN报文后,响应SYN-ACK报文(SYN=1,ACK=1, seq = y,ack=x+1 ),服务端进入SYN_RCVD状态
- 第三次握手,客户端收到SYN-ACK报文后,回复ACK报文(ACK=1, seq=x+1, ack=y+1),双方进入ESTABLISTHED状态
TCP四次挥手
- 第一次挥手,客户端发送FIN报文请求断开连接(FIN=1,seq=u),进入FIN_WAIT1状态
- 第二次挥手,服务端收到FIN报文后,响应ACK报文(ACK=1, seq = v, ack = u+1),服务端进入CLOSE_WAIT状态,客户端进入FIN_WAIT2状态
- 第三次挥手,服务端发送完数据后,向客户端发送FIN报文后,进入LAST_ACK状态(FIN=1, seq = w, ack = u+1)
- 第四次挥手,客户端收到FIN报文,回复ACK报文后(ACK=1, seq = u+1, ack=w+1),进入TIME_WAIT状态,等待2MSL后进入CLOSED状态
为什么不是两次握手
- 两次握手无法避免旧连接报文干扰,若使用两次握手,当网络延时中滞留的旧连接请求到达服务端时,服务端会直接建立连接并发送数据;但此时客户端可能已经关闭旧连接,导致服务端持续等待,造成连接资源浪费;
- 两次握手无法避免半开连接,如果第二次握手的SYN-ACK报文丢失,服务端也会认为连接已建立并分配资源,此时客户端因未收到确认报文而处于等待状态,服务端将长期维持无效连接,造成资源浪费
- 三次握手通过客户端在第三次ACK中携带序列号确认,能够识别并拒绝旧连接报文,也可以明确双方资源就绪状态,避免半开连接导致的资源浪费
为什么不是三次挥手
- 三次挥手无法保证服务端连接正确断开,第三次挥手后,客户端直接断开连接,如果发送的ACK报文丢失,服务端就算超时重传FIN报文,客户端也无法接收,因此会导致服务端一直处于LAST-ACK连接状态
- 三次挥手无法保证服务端的后续数据传输,由于只有三次挥手,因此服务端FIN报文需要在第二次挥手时就发送出去,服务端可能有仍未发送完的数据,导致数据丢失
- 四次挥手通过2次FIN和ACK的发送和响应,配合客户端的TIME_WAIT状态,确保能够正确关闭两个方向的数据传输
☀TIME_WAIT为什么要等2MSL
- 确保连接断开,客户端发送的最后一个ACK报文可能因为网络问题丢失,如服务端未收到确认,会重传FIN报文。等待2MSL(报文最大存活时间)可以为服务端重传FIN报文预留出足够时间,从而保证双方正常关闭连接,否则可能导致服务端未收到ACK而持续重试,导致资源无法释放
- 消除旧连接报文干扰,等待2MSL可确保本地连接产生的所有报文在网络中消亡,避免新连接收到因网络延时滞留的旧报文(FIN报文或数据包),从而引发数据混乱和错误关闭
☀TCP的粘包和拆包,如何解决
- 粘包指发送方连续发送的多个数据包因为接收方缓冲区未及时读取被合并为一个包,导致接收方无法区分数据边界
- 拆包指单个数据因超过最大报文长度被分割为多个包传输,或接收缓冲区不足以容纳完整数据而拆分
- 解决方案,每个数据包固定长度,不足部分做填充,接收方按固定长度解析;在数据包间添加分隔符作为边界;在消息头部定义长度字段,接收方先读取长度在截取对应数据
什么是CSRF攻击
- CSRF是跨域请求伪造,利用用户已认证身份的攻击手段,攻击者通过诱导用户访问恶意页面或点击伪造请求,利用浏览的cookie机制自动携带用户凭证,从而以用户名义执行非授权操作
什么是XSS攻击
- XSS是跨站脚本攻击,攻击者通过在web页面中插入恶意脚本代码,然后诱使用户访问该页面;从而使得恶意脚本在用户浏览器中执行,从而盗取用户信息、控制用户行为等
什么是DNS劫持
- DNS劫持的原理是攻击者在用户查询DNS服务器时篡改解析过程,将用户请求的域名重定向到虚拟或恶意IP地址,导致用户无法访问目标网站或误入仿冒网站
什么是DDOS攻击
- DDOS攻击是使用操控分布在多个位置的计算机设备发起大规模请求,以耗尽目标系统的网络带宽或系统资源,从而使其无法正常提供服务的恶意行为
☀什么是CAP理论
- CAP理论是分布式系统中的核心原则,包括一致性、可用性、分区容错性
- consistent,一致性,要求所有节点在任意时刻数据同步,任何读都能读取最新写入结果
- available,可用性,强调系统提供的服务必须一直处于可用的状态,每次请求都能返回效应
- partition,分区容错性,系统在遇到任何网络分区故障的时候,仍然能够对外提供服务
什么是BASE理论
- BASE理论是分布式系统中的一种核心思想,是基于CAP理论逐步演化而来
- Basically Available,基本可用,指系统出现部分故障时,仍能保证核心功能可用,而非完全不可用
- Soft State,软状态,允许存在系统中的数据暂时的不一致状态,这种状态不会影响系统的整体运行
- Event consistent,最终一致性,系统在软状态的基础上,系统通过异步同步机制,确保经过一定时间后所有数据副本最终达成一致
☀有哪些方式实现分布式锁
- 基于Redis的分布式锁,通过setnx结合ex实现锁互斥和超时释放,或者Redission框架实现锁重入、锁续期
- 基于zookeeper的分布式锁,通过创建临时有序节点对资源上锁,完成操作后删除节点,后续序号被唤醒(在指定父节点目录下创建临时顺序子节点,上锁的客户端依次按照顺序1,2,3依次创建节点,客户端获取父节点下的所有子节点并按序号排序,如果自身序号最小则获取锁,否则监听前一个节点的删除事件)
- 基于MySQL的分布式锁,可以通过数据库的行锁的方式进行上锁
什么是zookeeper,核心原理是什么
- zookeeper是一个分布式协调服务框架,其核心目标是为分布式系统提供一致性、高可用性和可靠性的数据管理和协调能力
- zookeeper通过ZAB一致性协议实现数据的强一致性,通过消息广播和崩溃恢复实现ZAB
- 消息广播,leader负责接收客户端请求,将操作封装为提案并按顺序广播给所有follwer,当多数节点成功收到提案,并写入本地日志后,leader会通知所有follower提交,保证一致性
- 崩溃恢复,当leader故障后,zookeeper会进入崩溃恢复状态,在这个过程中集群推举出新的领导者,并且推举过程中不会接收新的写请求
了解过哪些分布式事务
- TCC,try, commit, cancel, try代表尝试执行代码,如果成功则执行commit代码,如果失败则执行cancel代码,具有业务侵占性
- XA,两阶段提交,分为准备和提交两阶段,准备阶段执行分支事务操作但不提交,如果所有分支事务操作都成功,则触发全局提交,否则回滚所有分支事务
- AT,通过记录事务提交前的修改快照,如果事务提交成功就删除快照,如果事务回滚就根据快照恢复
- SAGA,可以分为正向补偿和逆向补偿,整个流程按照ABCD进行下去,每执行一个流程就获得正常补偿,如果在C流程失败,则需要逆向执行C,B,A补偿,常见的有每个服务逐层异常抛出进行回滚事务
分布式一致性算法Raft
- Raft是一种专为分布式系统设计的一致性算法
- 领导选举,集群中的节点分为leader,follwer,candidate三者角色;leader会定期向所有follwer发送心跳消息;当follower未收到leader的心跳信号时,会转变为candidate发起选举,通过多数投票机制选出leader
- 日志复制,leader负责接收客户端请求,将操作封装为日志并按顺序广播给所有follwer,当多数节点成功复制日志后,leader会通知所有follower提交,保证一致性
了解过哪些限流算法
- 固定窗口算法,在指定周期内的累加访问次数,如果访问次数超过阈值,触发限流([0,3]300,[4,6]300,有可能在[2,5]达到600,存在窗口边界问题)
- 滑动窗口算法,滑动窗口在固定窗口的基础上进行了更加精细的分片,将一个窗口划分为多个小窗口,当当前时间大于当前窗口的最大时间点,将窗口平移一个小窗口,整个窗口的所有请求相加不能大于阈值(假设窗口大小是3,每个小窗口跨度是1,就会计算[0,3],[1,4]),解决了窗口边界问题
- 漏桶算法,以恒定的速度处理请求,请求先进入桶中,桶满则拒绝新请求,不能解决流量突发问题,系统空闲时也不能突破速率限制
- 令牌桶算法,系统以固定效率生成令牌存入桶中,请求需获取令牌才能被处理,需要额外的空间存储令牌
6237

被折叠的 条评论
为什么被折叠?



