目录
- Java基础
- JDK/JRE/JVM三者的关系
- JDK常用的包
- == 和 equals 的区别是什么?
- Java 中的几种基本数据类型了解么?
- 什么是自动拆装箱?
- final 关键字中有什么作用?
- 接口和抽象类有什么区别?
- String, StringBuffer 和 StringBuilder区别
- String 类的常用方法有哪些?
- Object的常用方法有哪些
- 为什么重写 equals() 时必须重写 hashCode() 方法?
- Java创建对象有几种方式?
- throw 和 throws 有什么区别?
- &和&&的区别
- final、finally、finalize有什么区别?
- 重载和重写有什么区别?
- 常见的Exception有哪些?
- Error和Exception的区别?
- 什么是反射?
- 反射常见的应用场景
- Java 中 IO 流分为几种?
- Files类的常用方法都有哪些?
- 什么是AIO、BIO和NIO?
- 什么是序列化和反序列化?
- java常见的引用类型
- java中深拷贝和浅拷贝
- Comparable和Comparator区别?
- 序列化和反序列化常见应用场景
- 什么是SPI机制?
- 集合
- 常见的集合有哪些?
- Collection和Collections有什么区别?
- ArrayList 和 Array(数组)的区别?
- ArrayList 和 LinkedList 的区别是什么?
- Arraylist 和 Vector 的区别
- HashMap和Hashtable的区别
- 哪些集合类是线程安全的?哪些不安全?
- HashMap原理
- 解决hash冲突有哪些方法?
- Set是怎么去重的?为什么要重写equals?
- HashSet、LinkedHashSet 和 TreeSet 的区别?
- ConcurrentHashMap原理
- ConcurrentHashMap新特点?
- 并发
- 并发和并行的区别
- 线程和进程的区别?
- 线程有哪些状态?
- 创建线程的方式有哪些?有什么特点?
- Runnable 和 Callable 的区别?
- 如何启动一个新线程、调用 start 和 run 方法的区别?
- Java中常见的锁
- 公平锁与非公平锁
- 共享式与独占式锁
- 悲观锁与乐观锁
- Synchronized锁的升级过程
- 什么是CAS?
- 线程相关的基本方法?
- 什么是线程死锁?死锁如何产生?
- 如何避免线程死锁?
- wait和sleep有哪些区别?
- JUC包提供了哪些原子类?
- 基本类型原子类
- 数组类型原子类
- 引用类型原子类
- JUC包常用的辅助类
- Semaphore(信号量)
- CountDownLatch (倒计时器)
- CyclicBarrier(循环栅栏)
- Lock和synchronized的区别
- Lock 接口实现类有哪些?有什么作用?
- synchronized的作用有哪些?
- volatile关键字有什么用?
- 什么是ThreadLocal?它的原理是什么?
- volatile和synchronized的区别是什么?
- ConcurrentHashMap原理
- JDK内置线程池有哪几种?
- FixedThreadPool
- SingleThreadExecutor
- CachedThreadPool
- ScheduledThreadPool
- 线程池常见参数有哪些?如何解释?
- 为什么不推荐使用内置线程池?
- 线程池的拒绝策略有哪些?
- 线程池的工作原理
- JVM
- Java对象内存布局
- markWord 数据结构
- JVM 内存结构(JDK1.8)
- 堆内存结构(JDK1.8)
- GC垃圾回收
- 如何发现垃圾
- 如何回收垃圾
- JVM调优参数
- 为什么元空间初始和最大的大小要设置一样?
- 什么是STW?有什么影响?
- 垃圾回收器
- JVM监控工具
- JVM故障诊断工具
- JAVA类加载器有哪些?
- 类加载器的工作原理
- 双亲委派机制图解
- 为什么需要双亲委派?
- 内存泄漏和内存溢出的区别是什么?
- Spring
- Spring的优点
- 什么是Spring AOP?
- AOP有哪些实现方式?
- JDK动态代理和CGLIB动态代理的区别?
- Spring AOP相关术语
- Spring通知有哪些类型?
- 什么是Spring IOC?
- Spring中Bean的作用域有哪些?
- Spring中的Bean什么时候被实例化?
- Spring中Bean的生命周期
- 依赖注入的方式
- @Autowired和@Resource有什么区别?
- @Component和@Bean的区别
- Bean 是线程安全的吗?
- 什么是事务?
- spring 事务的实现方式
- Spring 事务隔离级别
- Spring 事务传播属性
- Spring 事务在什么情况下会失效?
- Spring怎么解决循环依赖的问题?
- 什么是MVC?
- Spring MVC工作原理
- Spring Boot的优势
- Spring Boot自动装配原理
- 了解Spring Boot中的日志组件吗?
- MySql
- 聚集索引和非聚集索引的区别
- 数据库三范式
- MyISAM 存储引擎 与 InnoDB 引擎区别
- 索引
- 索引的优缺点
- 索引的分类
- 索引结构
- B树与B+树的区别
- 索引失效的几种情况
- 数据库锁
- MySql 优化
- SQL慢查询优化
- 索引优化
- 表优化
- Redis
- 为什么要用Redis?
- Redis到底是多线程还是单线程?
- Redis数据持久化机制
- RDB方式
- AOF方式
- Redis是单线程,但为什么快?
- Redis 过期删除策略
- Redis 内存淘汰策略
- 在百万keys的Redis里面,如何模糊查找某几个key?
- Redis 数据类型的使用场景
- Redis主从同步机制
- Redis集群模式有哪些?
- Redis缓存穿透,缓存击穿,缓存雪崩
- 布隆过滤器
- 数据库和缓存的一致性
- Elasticsearch
- Elasticsearch 是什么?
- Elasticsearch 使用场景?
- 倒排索引是什么?
- 倒排索引创建流程
- 倒排索引检索流程
- MongoDB
- MongoDB 是什么?
- Mysql和MongDB对比
- MyBatis
- MyBatis四种拦截器
- JDBC执行流程
- MyBatis执行流程
- Mybatis的一级二级缓存
- mybatis一对多,多对多
- 什么是ORM?ORM框架有哪些?
- MyBatis与Hibernate的区别
- 常用动态sql?
- MyBatis接口的底层原理
- PageHelper分页的原理是什么?
- MyBatis中接口绑定有几种实现方式?
- #{}和${}的区别是什么?
- MyBatis 中如何执行批处理?
- RabbitMQ
- 为什么用MQ? 异步 、削峰、解耦
- 1. 异步处理
- 2. 解耦
- 3. 削峰填谷
- Exchange类型
- 什么是死信队列?
- 如何保证消息的可靠性?
- RabbitMQ中如何解决消息堆积问题?
- RabbitMQ中如何保证消息有序性?
- 如何防止消息重复消费?(如何保证消息幂等性)
- 多线程异步和MQ的区别
- Spring Cloud
- 分布式与微服务区别?
- 什么是CAP原则?
- Spring Cloud Alibaba 组件有哪些?
- Nacos配置中心动态刷新原理
- 目前主流的负载方案有哪些?
- Nginx作为服务端负载均衡器,常见的负载均衡策略有哪些?
- Spring Ribbon相关
- Spring Ribbon是什么?
- Ribbon负载均衡策略有哪些?
- Ribbon第一次调用为什么会很慢?
- Feign 和 OpenFeign 的区别?
- 流量网关与服务网关的区别?
- 限流、降级和熔断有什么区别?
- Gateway的三大属性
- Gateway的三大案例组件
- 为什么要用服务网关
- 不同服务之间如何进行通信
- 在微服务中如何监控服务
- Openfeign如何使用
- Openfeign自定义拦截器
- Seata中2PC和3PC的区别
- 项目的几种发布方式和特点
- MongoDB和mysql区别
- 什么是分布式锁,Redisson有什么用?
- 下单请求三重防重
Java基础
JDK/JRE/JVM三者的关系
-
Jdk
【Java Development ToolKit】java开发 工具箱,提供了 Java 的开发环境和运行环境。
-
Jre
【Java Runtime Enviromental】为 Java 的运行提供了所需 环境。
-
JVM
【Java Virtual Machine】就是我们耳熟能详的 Java 虚拟机。Java 能够跨平台运行的核心在于 JVM 。
JRE = JVM + Java 核心类库
JDK = JRE + Java工具 + 编译器 + 调试器
JDK常用的包
java.lang
:这个是系统的 基础类,比如String、Math、Integer、System和Thread,提供常用功能。java.io
: 这里面是所有 输入输出 有关的类,比如文件操作等。java.net
: 这里面是与 网络 有关的类,比如URL,URLConnection等。java.util
: 这个是系统 辅助类,特别是集合类Collection,List,Map等。java.sql
: 这个是 数据库 操作的类,Connection,Statememt,ResultSet等
== 和 equals 的区别是什么?
==
运算符- 基本类型:比较值
- 引用类型:比较地址
equals()
- 基本类型:比较值
- 引用类型:默认使用的
==
比较的地址,不过一般进行重写,如Integer和String重写后比较的是值
Java 中的几种基本数据类型了解么?
Java 中有 8 种基本数据类型,分别为:
- 6 种数字类型:
- 4 种整数型:
byte
、short
、int
、long
- 2 种浮点型:
float
、double
- 4 种整数型:
- 1 种字符类型:
char
- 1 种布尔型:
boolean
。
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的包装类包装起来;
- 拆箱:将包装类型转换为基本数据类型;
final 关键字中有什么作用?
-
修饰引用
- 如果引用为基本数据类型,则该引用为常量,该值无法修改;
- 如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
- 如果引用是类的成员变量,则必须当场赋值,否则编译会报错。
-
修饰方法:无法被子类重写,但仍然可以被继承。
-
修饰类:无法被继承
接口和抽象类有什么区别?
- 相同点:
- 都不能直接实例化
- 都可以有抽象方法
- 都是体现多态
- 不同点:
- 抽象类的子类使用
extends
来继承;接口必须使用implements
来实现接口。 - 抽象类可以有 构造函数;接口不能有。
- 类可以实现很多个接口;但是只能继承一个抽象类。单继承多实现
- 接口中的方法默认使用
public
修饰;抽象类中的方法可以是任意访问修饰符。
- 抽象类的子类使用
String, StringBuffer 和 StringBuilder区别
- 可变性:
String
不可变StringBuffer
和StringBuilder
可变
- 线程安全:
String
不可变,因此是线程安全的StringBuilder
不是线程安全的StringBuffer
是线程安全的,内部使用synchronized
进行同步
String 类的常用方法有哪些?
indexOf()
:返回指定字符的索引。charAt()
:返回指定索引处的字符。replace()
:字符串替换。trim()
:去除字符串两端空白。split()
:分割字符串,返回一个分割后的字符串数组。getBytes()
:返回字符串的 byte 类型数组。length()
:返回字符串长度。toLowerCase()
:将字符串转成小写字母。toUpperCase()
:将字符串转成大写字符。substring()
:截取字符串。equals()
:字符串比较
Object的常用方法有哪些
toString()
:默认输出对象地址。equals()
:默认比较两个引用变量是否指向同一个对象(内存地址)。hashCode()
:将与对象相关的信息映射成一个哈希值,默认的实现hashCode值是根据内存地址换算出来。clone()
:复制对象的副本,使用的是浅拷贝。getClass()
:返回此 Object 的运行时类,常用于java反射机制。wait()
:使当前线程释放对象锁,进入等待状态。notify()
:唤醒在此对象上等待的单个线程,选择是任意性的。notifyAll()
:唤醒在此对象上等待的所有线程
为什么重写 equals() 时必须重写 hashCode() 方法?
java规范中约定 相等对象 的hashCode值必须相等。
equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等,所以hashCode和equals都要重写。
Java创建对象有几种方式?
- 用
new
语句创建对象。 - 使用反射,使用
Class.newInstance()
创建对象。 - 调用对象的
clone()
方法。 - 使用反序列化
throw 和 throws 有什么区别?
throw
用于 手动抛出java.lang.Throwable
类的一个例化对象throws
的作用是作为 方法声明和签名的一部分,方法被抛出相应的异常以便调用者能处理。
&和&&的区别
&运算符有两种用法:(1)按位与;(2)短路与。
&
:按位与,通过&两边的值计算结果。&&
:短路运算,如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
final、finally、finalize有什么区别?
final、finally和finalize是Java中的三个不同的概念。
final
:用于声明变量、方法或类,使之不可变、不可重写或不可继承。finally
:是异常处理的一部分,用于确保代码块(通常用于资源清理)总是执行。finalize
:是Object类的一个方法,用于在对象被垃圾回收前执行清理操作,但通常不推荐使用
重载和重写有什么区别?
-
重载
重载发生在同一个类内部,目的是提供多种同名方法以适应不同的情况。
- 方法名必须 相同。
- 参数列表必须 不同(参数的数量、类型或顺序不同)。
- 返回类型无要求。
-
重写
在子类中重新定义父类中的方法称为方法重写。
- 方法名必须 相同。
- 参数列表必须 相同。
- 返回类型必须 相同。
- 访问修饰符不能比父类的方法更严格(例如,父类方法为public,则子类重写的方法也应该是public)。
常见的Exception有哪些?
常见的 RuntimeException
:
ClassCastException
// 类型转换异常IndexOutOfBoundsException
// 数组越界异常NullPointerException
// 空指针ArrayStoreException
// 数组存储异常NumberFormatException
// 数字格式化异常ArithmeticException
// 数学运算异常
常见的 CheckedException
:
NoSuchFieldException
// 反射异常,没有对应的字段ClassNotFoundException
// 类没有找到异常IOException
// IO异常SQLException
// SQL异常
Error和Exception的区别?
-
Error
:JVM 无法解决的严重问题,如栈溢出StackOverflowError
、内存溢出OOM
等。程序无法处理的错误。 -
Exception
:其它因编程错误或偶然的外在因素导致的一般性问题。可以在代码中进行处理。如:空指针异常、数组下标越界等。
什么是反射?
反射机制指的是程序在运行时能够获取自身的信息。在java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有属性和方法。
Java的反射可以:
- 在运行时判断任意一个 对象所属的类。
- 在运行时判断任意一个类 所具有的成员变量和方法。
- 在运行时任意调用一个对象的 方法
- 在运行时 构造任意一个类的对象
反射常见的应用场景
- 动态代理
- JDBC的
class.forName
- BeanUtils中属性值的拷贝
- RPC框架
- ORM框架
- Spring的IOC/DI
Java 中 IO 流分为几种?
- 按功能来分:输入流(input)、输出流(output)。
- 按类型来分:字节流和字符流。
- 字节流和字符流的区别是:
- 字节流按8位(一个字节),以字节为单位输入输出数据
- 字符流按 16位(两个字节),以字符为单位输入输出数据
Files类的常用方法都有哪些?
exists()
:检测文件路径是否存在。createFile()
:创建文件。createDirectory()
:创建文件夹。delete()
:删除一个文件或目录。copy()
:复制文件。move()
:移动文件。size()
:查看文件个数。read()
:读取文件。write()
:写入文件。
什么是AIO、BIO和NIO?
-
BIO (Blocking I/O)
:同步阻塞I/O, 线程发起IO请求后,一直阻塞,直到缓冲区数据就绪后,再进入下一步操作。 -
NIO (Non-Blocking I/O)
:同步非阻塞IO,线程发起IO请求后,不需要阻塞,立即返回。用户线程不原地等待IO缓冲区,可以先做一些其他操作,只需要定时轮询检查IO缓冲区数据是否就绪即可。 -
AIO ( Asynchronous I/O)
:异步非阻塞I/O模型。线程发起IO请求后,不需要阻塞,立即返回,也不需要定时轮询检查结果,异步IO操作之后会回调通知调用方
什么是序列化和反序列化?
- 序列化:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是
JSON
,XML
等文本格式 - 反序列化:将在序列化过程中所生成的数据 转换为原始数据结构或者对象的过程
java常见的引用类型
java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用
-
强引用:普通的变量引用
- 第一种:直接new
User user = new User();
- 第二种:通过反射
Class<?> studentClass = Class.forName("com.beiyou.model.student"); Object o = studentClass.newInstance();
- 第一种:直接new
-
软引用:
- 当所剩内存空间不够我们新的对象存储的时候,直接干掉软引用。
- 当所剩内存空间够我们新对象的存储的时候,不会删除我们的软引用对象。
SoftReference<User> user = new SoftReference<User>(new User());
-
弱引用:
将对象用WeakReference
弱引用类型的对象包裹,只要GC执行了,他就会被回收掉.public static WeakReference<User> user = new WeakReference<User>(new User());
-
虚引用:虚引用也称为幽灵引用或者幻影引用
java中深拷贝和浅拷贝
Java中,对象的复制分为深拷贝(Deep Copy)和浅拷贝(Shallow Copy)两种方式,主要区别在于对对象内部引用类型成员变量的处理不同。
-
浅拷贝(Shallow Copy)
浅拷贝是指在创建新对象时,对于对象中的基本数据类型 的成员变量会 复制其值,而对于引用类型成员变量则只复制其引用实现浅拷贝的方式:
Java中实现浅拷贝的一种常见方式是通过 Object类的clone()
方法。需要注意的是,clone()方法是受保护的,所以通常 需要在类中重写此方法并声明为public
,同时还需要实现Cloneable
接口。 -
深拷贝(Deep Copy)
深拷贝则是 完全复制对象及其所有引用类型成员变量指向的对象,即不仅复制引用本身,还复制引用指向的对象。Java中实现深拷贝有几种常见方式:
- 序列化
- 构造函数
- 复制对象的属性
- 使用第三方库
Comparable和Comparator区别?
-
相同点:
Comparable
和Comparator
都是用于比较排序 -
不同点:
- 接口所在包不同:
java.lang.Comparable
、java.util.Comparator
- 比较逻辑不同:
Comparable
是内部比较器,Comparator
是外部比较器 - 排序方法不同:Comparable重写方法
compareTo(T o)
,Comparator重写方法compare(T o1, T o2)
- 排序规则数量限制不同:Comparable 唯一字段排序,Comparator可以有 多个字段排序
- 接口所在包不同:
序列化和反序列化常见应用场景
- 对象在进行 网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象 存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象 存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
- 将对象 存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
什么是SPI机制?
- Java SPI
SPI 全称 Service Provider Interface,是 Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。
SPI的作用就是为这些被扩展的API 寻找服务实现。本质是通过基于接口的 编程+策略模式+配置文件 实现动态加载。可以实现 解耦 (接口和实现分离),提高框架的 可拓展性(第三方可以自己实现,达到插拔式的效果)。
- Spring SPI
Spring SPI对 Java SPI 进行了封装增强。我们只需要在 META-INF/spring.factories
中配置接口/类/注解名,即可通过服务发现机制,在运行时进行自动加载。
集合
常见的集合有哪些?
Java集合类主要由两个接口Collection
和Map
派生出来的,Collection
有三个子接口:List
、Set
、Queue
。
Java集合框架图如下:
Collection和Collections有什么区别?
Collection
是一个 集合接口:它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。是list,set等的父接口。Collections
是一个 包装类:它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
ArrayList 和 Array(数组)的区别?
ArrayList 内部基于动态数组实现,比 Array(静态数组) 使用起来更加灵活:
Array
数组长度固定,ArrayList
可动态扩容。Array
可以直接存储基本类型数据,也可以存储对象。ArrayList
中只能存储对象。ArrayList
允许你使用泛型来确保类型安全,Array 则不可以。ArrayList
支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法,比如 add()、remove()等。Array
创建时必须指定大小,ArrayList
创建时不需要指定大小
ArrayList 和 LinkedList 的区别是什么?
- 数据结构:
ArrayList
是 动态数组 的数据结构实现,而LinkedList
是 双向链表的数据结构实现。 - 随机访问:
ArrayList
比LinkedList
随机访问效率高,因为LinkedList
是线性的数据存储方式,所以需要移动指针从前往后依次查找。 - 增删效率:在非首尾的增加和删除操作,
LinkedList
要比ArrayList
效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
综合来说,在需要 频繁读取 集合中的元素时,更推荐使用 ArrayList
,而在 插入和删除操作较多时,更推荐使用 LinkedList
。
Arraylist 和 Vector 的区别
ArrayList
在内存不够时扩容为原来的1.5倍,Vector
是扩容为原来的2倍。- Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为操作Vector效率比较低
HashMap和Hashtable的区别
- 存储:
HashMap
允许 key和 value 为null
,而Hashtable
不允许。 - 线程安全:
Hashtable
是线程安全的,而HashMap
是非线程安全的。 - 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用
HashMap
替代,如果需要多线程使用则用ConcurrentHashMap
替代。
哪些集合类是线程安全的?哪些不安全?
线性安全的集合类:
Vector
:比ArrayList多了同步机制。Hashtable
。ConcurrentHashMap
:是一种高效并且线程安全的集合。Stack
:栈,也是线程安全的,继承于Vector。
线性不安全的集合类:
- Hashmap
- Arraylist
- LinkedList
- HashSet
- TreeSet
- TreeMap
HashMap原理
HashMap 使用动态数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的
链表长度大于8(TREEIFY_THRESHOLD)时,会把链表转换为红黑树
红黑树节点个数小于6(UNTREEIFY_THRESHOLD)时才转化为链表,防止频繁的转化
解决hash冲突有哪些方法?
- 链表法
- 开放地址法
- 再hash法
- 公共溢出区
Set是怎么去重的?为什么要重写equals?
HashSet底层使用的是HashMap,HashSet中的元素实际上由HashMap的key
来保存,而HashMap的value则存储了一个静态的Object对象
当判断两个对象是否相等时:
1:获取对象的hashcode,找到对应的桶位,如果没有数据,直接存,如果有数据
2:需要调用equals方法,判断是否是同一个对象,是:丢弃 不是:链表
HashSet、LinkedHashSet 和 TreeSet 的区别?
HashSet
是 Set 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值;
LinkedHashSet
是 HashSet 的子类,能够按照添加的顺序遍历;
TreeSet
底层使用红黑树,能够按照添加的顺序遍历,排序的方式可以自定义
ConcurrentHashMap原理
-
Java7 中
ConcurrentHashMap
使用的分段锁每一个
Segment
上同时只有一个线程可以操作,每一个Segment
都是一个类似HashMap
数组的结构,它可以扩容,它的冲突会转化为链表。但是Segment
的个数一但初始化就不能改变。
-
Java8 中的
ConcurrentHashMap
使用的Synchronized
锁加 CAS 的机制。结构由 Java7 中的
Segment
数组 +HashEntry
数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个HashEntry
的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表
ConcurrentHashMap新特点?
Java8 中的 ConcurrentHashMap
新增了一些特点:
-
分段锁机制:
将数据分成多个 segment 来实现锁的粒度更细,从而减小锁的竞争范围,提高并发性能
-
CAS 算法:
对数据进行更新时,采用
CAS
(Compare And swap)算法来保证更新的原子性,避免了锁的颗粒度过大而带来的性能问题。 -
扩容机制:
扩容时,只需要对需要扩容的 Segment 进行扩容,而不是对整个 Map 进行扩容,这样可以减少扩容对并发性能的影响。
总之,ConcurrentHashMap 在 JDK8 中有了很大的优化和改进,减小了锁的粒度,提高了并发性能和可伸缩性,并且也是线程安全的,是AGSHI高并发环境下的一个非常优秀的容器。
并发
并发和并行的区别
-
并发:两个及两个以上的作业看起来像是同时进行的,实际上它们是在交替执行。
-
并行:并行则强调真正的同时性,两个及两个以上的作业在物理上同时执行。
线程和进程的区别?
-
进程是程序运行和资源分配的基本单位,一个进程可以包含多个线程,而且最少拥有一个线程。
-
线程是是cpu调度和分配的基本单位
线程有哪些状态?
NEW: 初始状态,线程被创建出来但没有被调用 start() 。
RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
BLOCKED:阻塞状态,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕
创建线程的方式有哪些?有什么特点?
-
继承
Thread
类并重写run
方法创建线程:实现简单但不可以继承其他类。 -
实现
Runnable
接口并重写run
方法:避免了单继承局限性,编程更加灵活,实现解耦。 -
实现
Callable
接口并重写call
方法:可以获取线程执行结果的返回值,并且可以抛出异常。 -
使用线程池创建(使用
java.util.concurrent.Executor
接口)
Runnable 和 Callable 的区别?
-
Runnable
接口run
方法无返回值,异常且无法捕获处理; -
Callable
接口call
方法有返回值,支持泛型, 可以获取异常信息
如何启动一个新线程、调用 start 和 run 方法的区别?
run
方法只是thread
的一个普通方法,线程对象调用run
方法不开启线程- 调用
start
方法可以启动线程,使得线程进入就绪状态,并让 jvm 调用run
方法在开启的线程中执行
Java中常见的锁
分类标准 | 分类 |
---|---|
根据线程是否需要对资源加锁 | 悲观锁/乐观锁 |
根据多个线程是否能获取同一把锁 | 共享锁/独享(独占、排他)锁 |
根据锁是否能够重复获取 | 可重入锁/不可重入锁 |
根据锁的公平性进行区分 | 公平锁/非公平锁 |
当多个线程并发访问资源时,当使用synchronized时 | 锁升级( 偏向锁Q /轻量级锁/重量级锁) |
根据资源被锁定后,线程是否阻塞 | 自旋锁/适应性自旋锁 |
公平锁与非公平锁
按照线程访问顺序获取对象锁。
synchronized
是非公平锁,Lock
默认是非公平锁,可以设置为公平锁,公平锁会影响性能
共享式与独占式锁
共享式与独占式的最主要区别在于:同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。
例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。
悲观锁与乐观锁
-
悲观锁,每次访问资源都会加锁,执行完同步代码释放锁,
synchronized
和ReentrantLock
属于悲观锁。 -
乐观锁,不会锁定资源,所有的线程都能访问并修改同一个资源,如果没有冲突就修改成功并退出,否则就会继续循环尝试。
乐观锁最常见的实现就是
CAS
。 -
适用场景:
- 悲观锁适合写操作多的场景。
- 乐观锁适合读操作多的场景,不加锁可以提升读操作的性能
Synchronized锁的升级过程
-
一开始是无锁状态
-
当一个线程首次获得对象锁时,JVM会设置为 偏向锁。
-
当第二个线程尝试获取偏向锁失败时,偏向锁会升级为 轻量级锁
-
此时,JVM会使用CAS自旋操作来尝试获取锁,如果成功则进入临界区域,否则升级为 重量级锁。
什么是CAS?
CAS全称Compare And Swap
,比较与交换,是乐观锁的主要实现方式。
CAS在不使用锁的情况下实现多线程之间的变量同步。ReentrantLock
内部的AQS和原子类内部都使用了CAS
线程相关的基本方法?
线程相关的基本方法有 wait
,notify
,notifyAll
,sleep
,join
,yield
等
-
线程等待(wait)
调用wait
方法的线程进入WAITING
状态,只有等待另外线程的通知或被中
断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。 -
线程睡眠(sleep)
sleep 导致当前线程休眠,进入TIMED-WATING
状态,与 wait 方法不同的是sleep
不会释放当前占有的锁, -
线程让步(yield)
yield 会使当前线程 让出 CPU 执行时间片,与其他线程一起重新竞争CPU 时间片。 -
线程中断(interrupt)
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。线程在合适的时候中断. -
Join 等待其他线程终止
join() 方法,等待其他线程终止,当前线程再由阻塞状态变为就绪状态Runable
. -
线程唤醒
- notify:唤醒在等待的 单个线程,被唤醒的线程会继续与其他线程进行竞争
- notifyAll :唤醒等待的 所有线程。
什么是线程死锁?死锁如何产生?
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
-
死锁产生条件:
-
互斥:一个资源一次只能被一个线程持有。
-
请求与保持:一个进程因请求资源而阻塞时,不释放获得的资源。
-
不剥夺:进程已获得的资源,在未使用之前,不能强行剥夺。
-
循环等待:进程之间循环等待着资源
-
当以上条件同时满足时,就可能会出现死锁的情况。
如何避免线程死锁?
要避免线程死锁,可以采取以下几种方法:
-
尽量避免使用多个锁,尽量使用一个锁或者使用更加高级的锁,例如读写锁或者
ReentrantLock
。 -
减少锁的粒度, 确保同步代码块的执行时间尽可能短,这样可以减少线程等待时间,从而避免死锁的产生。
-
使用尝试锁,通过
ReentrantLock.tryLock()
方法可以尝试获取锁,如果在规定时间内获取不到锁,则放弃锁。 -
避免嵌套锁,如果需要使用多个锁,确保它们的获取顺序是一致的
wait和sleep有哪些区别?
相同点:
- 它们都可以使当前线程暂停运行,把机会交给其他线程
- 任何线程在调用wait()和sleep()之后,在等待期间被中断都会抛出
InterruptedException
不同点:
- wait()是Object超类中的方法;而sleep()是线程Thread类中的方法
- 对锁的持有不同,wait()会释放锁,而sleep()并不释放锁
- 唤醒方法不完全相同,wait()依靠
notify
或者notifyAll
、中断、达到指定时间来唤醒;而sleep()到达指定时间被唤醒 - 调用wait()需要先获取对象的锁,而Thread.sleep()不用
JUC包提供了哪些原子类?
基本类型原子类
使用原子的方式更新基本类型
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean :布尔型原子类
数组类型原子类
使用原子的方式更新数组里的某个元素
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray :引用类型数组原子类
引用类型原子类
- AtomicReference:引用类型原子类
- AtomicStampedReference:带有版本号的引用类型原子类。
- AtomicMarkableReference :原子更新带有标记的引用类型。
JUC包常用的辅助类
Semaphore(信号量)
synchronized
和 ReentrantLock
都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量
Semaphore 有两种模式:
- 公平模式: 调用 acquire() 方法的顺序就是获取许可证的顺序,遵循 FIFO(先进先出);
- 非公平模式: 默认,抢占式的
CountDownLatch (倒计时器)
CountDownLatch
用于某个线程等待其他线程执行完任务再执行,CountDownLatch
是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch
使用完毕后,它不能再次被使用
CyclicBarrier(循环栅栏)
CyclicBarrier
用于一组线程互相等待到某个状态,然后这组线程再同时执行,CyclicBarrier
的计数器可以使用reset()方法重置,可用于处理更为复杂的业务场景
Lock和synchronized的区别
Lock
是接口,synchronized
是关键字synchronized
是非公平锁,Lock
接口支持公平锁和非公平锁- 当线程离开
synchronized
块或者方法时,锁会自动释放,使用Lock
接口时,必须显式地调用lock()
方法获取锁,并且在完成任务后显式地调用unlock()
方法释放锁 synchronized
锁不可被中断等待,除非锁被释放,Lock
接口可以通过调用lock.tryLock()
方法尝试获取锁,如果失败则可以选择放弃等待
Lock 接口实现类有哪些?有什么作用?
- ReentrantLock:支持重入(即可以由持有锁的同一个线程多次获取)。可以指定公平策略。如果不指定,默认是非公平的。
- ReentrantReadWriteLock:这个类实现了读写锁,允许多个读取者同时访问,但是一次只能有一个写入者。读取锁通常用于读多写少的情况,以提高并发性能。
synchronized的作用有哪些?
-
原子性:确保线程互斥的访问同步代码;
-
可见性:保证共享变量的修改能够及时可见;
-
有序性:有效解决重排序问题。
volatile关键字有什么用?
volatile
是轻量级的同步机制,volatile
保证变量对所有线程的可见性,不保证原子性。- 禁止进行指令重排序。
什么是ThreadLocal?它的原理是什么?
ThreadLocal
是一个线程本地变量,它可以为每一个线程都创建一个私有的变量,每个线程只能获取到自己的变量,从而避免了线程安全问题。
ThreadLocal
的核心是 ThreadLocalMap
类,它是一个线程级别的哈希表,用于存储每个线程的变量。
ThreadLocalMap
以 ThreadLocal
对象作为 key,并且key是软引用
以变量值作为 value,value中的对象是强引用,每个线程都可以通过 ThreadLocalMap
获取自己的变量。
volatile和synchronized的区别是什么?
volatile
只能使用在变量上;而synchronized
可以在类,变量,方法和代码块上。volatile
至保证可见性;synchronized
保证原子性与可见性。volatile
禁用指令重排序;synchronized
不会。volatile
不会造成阻塞;synchronized
会
ConcurrentHashMap原理
-
Java7 中
ConcurrentHashMap
使用的分段锁每一个
Segment
上同时只有一个线程可以操作,每一个Segment
都是一个类似HashMap
数组的结构,它可以扩容,它的冲突会转化为链表。但是Segment
的个数一但初始化就不能改变。
-
Java8 中的
ConcurrentHashMap
使用的Synchronized
锁加 CAS 的机制。结构由 Java7 中的
Segment
数组 +HashEntry
数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个HashEntry
的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表
JDK内置线程池有哪几种?
FixedThreadPool
- 创建一个固定大小的线程池。
- 线程池中的线程数量是固定的,当一个任务完成后,这个线程会被用来执行另一个任务。
- 如果提交的任务数量超过了线程的数量,那么超出的任务会被放在队列中等待。
SingleThreadExecutor
- 创建一个单线程化的线程池。
- 只有一个工作线程,确保所有任务按照指定顺序(FIFO,先进先出)执行。
- 通常用于需要保证顺序执行的任务,例如更新GUI等。
CachedThreadPool
- 创建一个可缓存的线程池。
- 这种线程池能够自动调整线程数量,如果一段时间内没有新的任务提交,多余的空闲线程就会被终止。
- 适合处理大量短时间的任务。
ScheduledThreadPool
- 创建一个支持定时及周期性任务执行的线程池。
- 支持在给定延迟后运行命令或者定期执行。
- 可以用它来安排在将来某个时刻或定期执行的任务。
线程池常见参数有哪些?如何解释?
ThreadPoolExecutor 3 个最重要的参数:
corePoolSize
: 核心线程数。这是线程池中始终维持的最小线程数。即使线程空闲,核心线程也不会被销毁。只有当线程池被关闭时,才会终止核心线程。maximumPoolSize
: 最大线程数。线程池允许创建的最大线程数。当任务队列满且等待的任务数量超过了队列容量时,线程池会创建新的线程来处理额外的任务,直到达到最大线程数。workQueue
:任务队列。当提交的任务数量超过 corePoolSize 时,新提交的任务会被放置在这个队列中等待执行。
ThreadPoolExecutor其他常见参数 :
keepAliveTime
:空闲线程的存活时间。unit
: keepAliveTime 参数的时间单位。threadFactory
:线程工厂。用于创建新线程,可以用来设置线程名称、优先级等。handler
:拒绝策略。
为什么不推荐使用内置线程池?
Executors 返回线程池对象的弊端如下:
FixedThreadPool
和SingleThreadExecutor
:使用的是有界阻塞队列是 LinkedBlockingQueue ,其任务队列的最大长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM(内存溢出)。CachedThreadPool
:使用的是同步队列 SynchronousQueue, 允许创建的线程数量为 Integer.MAX_VALUE ,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM(内存溢出)。ScheduledThreadPool
和SingleThreadScheduledExecutor
:使用的无界的延迟阻塞队列 DelayedWorkQueue ,任务队列的最大长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM(内存溢出)。
线程池的拒绝策略有哪些?
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolExecutor
定义一些策略:
AbortPolicy
:抛出 RejectedExecutionException异常来拒绝新任务的处理。CallerRunsPolicy
:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。DiscardPolicy
:不处理新任务,直接丢弃掉。DiscardOldestPolicy
:此策略将丢弃最早的未处理的任务请求。
线程池的工作原理
-
有任务提交过来,先分配给核心线程执行
-
核心线程满了之后,将后续任务提交到工作队列中
-
工作队列也存放满了,就看最大线程数有没有满,没有就继续增加线程
-
最大线程数也满了,就会执行拒绝策略,默认是
AbortPolicy
JVM
Java对象内存布局
markWord 数据结构
JVM 内存结构(JDK1.8)
-
程序计数器:
线程私有,记录代码执行的位置. -
Java虚拟机栈:
线程私有,每个线程都有一个自己的Java虚拟机栈 ,默认大小是1M -
本地方法栈:
线程私有,每个线程都有一个自己的本地方法栈,Java虚拟机栈加载的是普通方法,本地方法加载的是native
修饰的方法.native
:表示这个方法不是java原生的,是由C或C++实现的 -
堆:
线程共享,用于存放对象,new的对象都存储在这个区域 -
元空间:
线程共享,存储class信息,类的信息,方法的定义,静态变量,常量池 等
- 堆和元空间是线程共享的,在Java虚拟机中只有一个堆、一个元空间,并在JVM启动的时候就创建,JVM停止才销毁。
- 栈、本地方法栈、程序计数器是每个线程私有的,随着线程的创建而创建,随着线程的结束而死亡。
每个存储位置会产生的异常:
堆内存结构(JDK1.8)
- 年轻代:Eden+S0+S1, S0和S1大小相等, 新创建的对象都在年轻代
- 老年代:经过年轻代 多次垃圾回收存活下来的对象存在年老代中.
GC垃圾回收
JVM的垃圾回收动作可以大致分为两大步:
- 如何发现垃圾
- 如何回收垃圾
线程私有的不存在垃圾回收,只有线程共享的才会存在垃圾回收,所以堆中存在垃圾回收.
如何发现垃圾
常见的用于「发现垃圾」的算法有两种,引用计数算法 和 根搜索算法。
-
引用计数算法
堆中的对象每被引用一次,则计数器加1,每减少一个引用就减1,当对象的引用计数器为0时可以被当作垃圾收集。
- 优点:快。
- 缺点:无法检测出循环引用。如两个对象互相引用时,他们的引用计数永远不可能为0。
-
根搜索算法(也叫根可达性分析)
根搜索算法是把所有的引用关系 看作一张图,从根节点(GCRoot)开始遍历,找出被根节点引用的节点,对于没有被根节点指向的节点,即可以当作垃圾。
- Java中可作为GCRoot的对象有:
- java虚拟机栈中引用的对象
- 本地方法栈引用的对象
- 元空间中静态属性引用的对象
- 元空间中常量引用的对象
- Java中可作为GCRoot的对象有:
如何回收垃圾
Java中用于「回收垃圾」的常见算法有4种:
-
标记-清除算法(markandsweep)
首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。
缺点: 标记清除之后会产生大量的不连续的内存碎片
-
标记-整理算法
首先标记出所有需要回收的对象,让所有存活的对象移动到另一个位置,在移动过程中清理掉可回收的对象,这个过程叫做整理。
优点:内存被整理后不会产生大量不连续内存碎片
缺点:耗时耗力 -
复制算法(copying)
将空间分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。
缺点:可使用的内存只有原来一半。在某一个时刻点,总有一个 S 是空的,可能是S0 也可能是S1.。
-
分代收集算法(generation)
当前主流JVM都采用分代收集(GenerationalCollection)算法,这种算法会根据对象存活周期的不同将内存划分为年轻代、年老代,不同生命周期的对象可以采取不同 的回收算法,以便提高回收效率。-
年轻代(YoungGeneration)
- 所有新生成的对象首先都是放在年轻代的。
- 新生代内存按照
8:1:1
的比例分为一个eden区和两个Survivor(s0,s1)区。大部分对象在Eden区中生成。
回收流程:
回收时先将eden区存活的对象复制到一个s0区,然后清空eden区,当这个s区,也存放满了时,则将eden区和s0区存活对象复制到另一个s1区,然后清空eden和这个s0区,此时s0区是空的,然后将s0区和s1区交换,即保持s1区为空,如此往复.-
特殊情况:当一个大对象不足于存放到eden区时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次
FullGC
,也就是新生代、老年代都进行回收。 -
新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高
-
年老代(OldGeneration)
-
在年轻代中经历了 N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。默认是15次,且最大15次。
-
内存比新生代也大很多(大概是2倍),当老年代内存满时触发FullGC,
FullGC发生频率比较低,老年代对象存活时间比较长,存活率比较高。
-
-
元空间-持久代(PermanentGeneration)
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。
-
JVM调优参数
这里只给出一些常见的性能调优的参数及其代表的含义。
-
-Xms8g
: 设置JVM中堆初始堆大小为8g。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。 -
-Xmx8g
: 设置JVM中堆最大可用内存为8g。 -
-Xmn4g
: 设置年轻代大小为4G。 -
-XX:NewRatio=2 设置年轻代(包括Eden和两个Survivor区)与年老代的比值
-
-XX:SurvivorRatio=8 ,所以默认值 Eden:S0:S1=8:1:1。
-
-Xss1m:设置每个线程的栈大小
-
-XX:MaxMetaspaceSize=128m
: 设置元空间最大为为128m , -
-XX:MetaspaceSize=128m
用于设置元空间的初始大小, 默认值约21M -
-XX:MaxTenuringThreshold=15:设置垃圾最大年龄。最大不超过15。
为什么元空间初始和最大的大小要设置一样?
因为元空间是使用的直接内存,当内存不够时会重新申请空间,进行一次FullGC
,并且会产生一个比较长的STW
(时停,暂停其他操作),会非常影响性能。
并且每次扩容不是一部到位的,它会一点一点的申请空间,这样会产生多次 FullGC
和 STW
。
所以初始直接给最大内存可以避免这种情况。
什么是STW?有什么影响?
STW,是Stop-The-World
的缩写,Stop-The-World是指系统在执行特定操作时,必须暂停所有的应用程序线程。
比如在Java中,当需要进行垃圾回收的时候,垃圾回收器需要停止应用程序的所有线程,以便可以安全地识别和回收不再使用的对象。这个过程我们就会称之为是Stop The World
了。
垃圾回收器
-
ParNew
+CMS
回收算法:ParNew【年轻代】【复制算法】,CMS【年老代】【标记清除】
-
Parallel Scavenge
+Parallel Old
回收算法:Parallel Scavenge【年轻代】【复制算法】,Parallel Old【年老代】【标记整理】
-
G1
:JDK9 默认的收集器回收算法:年轻代【复制算法】,年老代【标记-整理】
- 优点:
- 可以处理年轻代和年老代,
- 对内存进行了分区,缩小STW的规模,提高了垃圾回收的性能
- 优点:
JVM监控工具
-
JConsole:可以用于监视 JVM 的性能和资源利用情况
-
VisualVM:强大的JVM图形化监控工具,比jconsole强大完善
-
Arthas:阿里巴巴开发并开源的 Java 应用诊断工具
JVM故障诊断工具
jps
:虚拟机进程状况jinfo
:java配置信息工具jhat
:虚拟机堆转储快照分析工具jstat
:虚拟机统计信息监控工具jmap
:java内存映像工具jstack
:java堆栈跟踪工具
JAVA类加载器有哪些?
-
启动类加载器(Bootstrap Class Loader):也称为根类加载器,它负责加载Java虚拟机的核心类库,如java.lang.Object, java.lang.String等。
-
扩展类加载器(Extension Class Loader):它是用来加载Java扩展类库的类加载器。
扩展类库包括javax和java.util等包 -
应用程序类加载器(App Class Loader):也称为系统类加载器,它负责加载应用程序的类。
-
自定义类加载器(custom Class Loader):开发人员可以根据需要实现的类加载器。
类加载器的工作原理
类加载器的工作可以简化为三个步骤:
- 加载(Loading):根据类的全类名,定位并读取类文件的字节码。
- 链接(Linking):将类的字节码转换为可以在虚拟机中运行的格式。链接过程包括三个阶段:
- 验证(Verification):验证字节码的正确性和安全性,确保它符合Java虚拟机的规范。
- 准备(Preparation):为类的静态变量分配内存,并设置默认的初始值。
- 解析(Resolution):将类的符号引用(比如方法和字段的引用)解析为直接引用(内存地址)。
- 初始化(Initialization):执行类的初始化代码,包括静态变量的赋值和静态块的执行。
双亲委派机制图解
类加载器 采用了 双亲委派模型(Parent Delegation Model)来加载类。
当一个类加载器需要加载类时,它会 首先委派给其 父类加载器 加载。
如果父类加载器无法加载,才由该类加载器自己去加载。
这种层级关系使得类加载器能够 实现类的共享和隔离,提高了代码的 安全性 和 可靠性
为什么需要双亲委派?
- 通过双亲委派机制,可以 避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
- 通过双亲委派机制,可以 保证类安全性。因为BootstrapClassLoader在加载的时候,只会加载JAVA_HOME中的jar核心类库,如java.lang.String,那么这个类是不会被加载。
内存泄漏和内存溢出的区别是什么?
-
内存泄漏指的是程序中分配的内存在不再需要时没有被正确释放或回收的情况。这会导致程序持续占用内存,随着时间的推移,可用内存逐渐减少,最终可能导致程序性能下降或崩溃。
-
内存溢出指的是程序试图分配超过其可用内存空间的情况。这通常会直接导致Java程序崩溃。
Spring
Spring的优点
- 通过控制反转和依赖注入实现松耦合。
- 支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
- 支持声明式事务。
- 方便集成各种优秀框架。
- 方便程序的测试。
什么是Spring AOP?
AOP(Aspect-Oriented Programming),即面向切面编程,用人话说就是把公共的逻辑抽出来,让开发者可以更专注于业务逻辑开发,可以减少系统的重复代码和降低模块之间的耦合度。
切面就是那些与业务无关,但所有业务模块都会调用的公共逻辑。
AOP有哪些实现方式?
AOP有两种实现方式:静态代理和动态代理。
-
静态代理:
代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,也称编译时增强。AspectJ使用的是静态代理。
缺点:代理对象需要与目标对象实现一样的接口,并且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护。
-
动态代理:
代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。
JDK动态代理和CGLIB动态代理的区别?
Spring AOP中的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。
- JDK 动态代理 依赖于 反射机制来创建代理,适用于实现接口的情况。
- CGLib 动态代理 通过字节码生成技术创建子类来实现代理,适用于没有实现接口的类。
Spring AOP相关术语
术语 | 含义 |
---|---|
目标(Target) | 被通知的对象 |
代理(Proxy) | 向目标对象应用通知之后创建的代理对象 |
连接点(JoinPoint) | 目标对象的所属类中,定义的所有方法均为连接点 |
切入点(Pointcut) | 被切面拦截 / 增强的连接点 (切入点一定是连接点,连接点不一定是切入点) |
通知(Advice) | 增强的逻辑 / 代码,也即拦截到目标对象的连接点之后要做的事情 |
切面(Aspect) | 切入点(Pointcut)+通知(Advice) |
Weaving(织入) | 将通知应用到目标对象,进而生成代理对象的过程动作 |
Spring通知有哪些类型?
在AOP术语中,切面的工作被称为通知。通知实际上是程序运行时要通过Spring AOP框架来触发的代码段。
Spring切面可以应用5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能;
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知(After-returning ):在目标方法成功执行之后调用通知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的逻辑
通知的执行顺序:
什么是Spring IOC?
IOC:控制反转,由Spring容器管理bean的整个生命周期。
通过反射实现对其他对象的控制,包括初始化、创建、销毁等,解放手动创建对象的过程,同时降低类之间的耦合度
- Spring IOC的实现机制:工厂模式+反射机制
Spring中Bean的作用域有哪些?
Bean的作用域:
singleton
:单例,Spring中的bean默认都是单例的。prototype
:原型,每次请求都会创建一个新的bean实例。- request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
- session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。
- application:全局session作用域。
Spring中的Bean什么时候被实例化?
- 单例作用域(Singleton):在 Spring 容器 启动 时,会立即实例化单例作用域的 Bean,将它们存储在容器的 Bean 工厂中,以便随时获取。
- 原型作用域(Prototype):在 请求 获取原型作用域的 Bean 时,Spring 容器才会实例化该 Bean,并返回给请求方。
- 其他作用域:如 Web 作用域和 Session 作用域等,它们的实例化时间依赖于具体的使用场景。
Spring中Bean的生命周期
Bean生命周期可以粗略的划分为五大步:
- 第一步:实例化Bean
- 第二步:Bean属性赋值
- 第三步:初始化Bean
- 第四步:使用Bean
- 第五步:销毁Bean
依赖注入的方式
在 Spring 中实现依赖注入的常见方式有以下 3 种:
- 属性注入(Field Injection)
- @Autowire实现属性注入
- @Resurce实现属性注入
- Set方法注入(Setter Injection)
- 构造方法注入(Constructor Injection)
@Autowired和@Resource有什么区别?
-
Autowired
是Spring提供的;Resource
是J2EE提供的 -
Resource
默认使用name装配,未指定name时,会按类型装配 -
AutoWired
按类型装配,如果要使用名称装配可以用@Qualifier结合使用
@Component和@Bean的区别
-
@Component 注解用在类上,表明一个类会作为组件类,并告知Spring要为这个类创建bean,每个类对应一个 Bean。
-
@Bean 注解用在方法上,表示这个方法会返回一个 Bean。
-
@Bean 注解更加灵活,相比 @Component 注解自定义性更强
Bean 是线程安全的吗?
Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。
-
prototype
作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。 -
singleton
作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。- 有状态Bean(包含可变的成员变量的对象),存在线程安全问题。
- 无状态Bean(没有定义可变的成员变量,比如dao和service),不能保存数据,是线程安全的。
什么是事务?
事务是一个操作序列,要么全部执行成功,要么全部执行失败。事务有四个重要特性,称为 ACID
特性:
- Atomicity(原子性):事务中的所有操作要么全部完成,要么全部不完成。
- Consistency(一致性):事务完成后,数据要处于一致的状态。
- Isolation(隔离性):一个事务的执行不能被其他事务干扰。
- Durability(持久性):事务完成后,数据应该永久保存
补充:
undo_log
表保证事务 原子性(A) 和 一致性(C )redo_log
表保证事务 持久性(D)- 隔离级别 保证事务 隔离性(I)
spring 事务的实现方式
Spring事务机制主要包括声明式事务和编程式事务。
- **编程式事务:通过编程的方式管理事务,手动去开启、提交、回滚事务、这种方式带来了很大的灵活性,但很难维护。
- 声明式事务:将事务管理代码从业务方法中分离出来,通过aop进行封装。Spring声明式事务使得我们无需要去处理获得连接、关闭连接、事务提交和回滚等这些操作。使用 @Transactional 注解开启声明式事务。
Spring 事务隔离级别
-
读未提交(read Uncommited)
在该隔离级别,所有的事务都可以读取到别的事务中未提交的数据,会产生脏读问题,在项目中基本不怎么用, 安全性太差;脏读:所谓的脏读,其实就是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。
也就是说,当前事务读到的数据是别的事务想要修改成为的但是没有修改成功的数据。 -
读已提交(read commited)
处于READ COMMITTED
级别的事务可以看到其他事务对数据的修改。也就是说,在事务处理期间,如果其他事务修改了相应的数据,那么同一个事务的多个 SELECT 语句可能返回不同的结果。在一个事务内,能看到别的事务提交的数据。出现 不可重复读。不可重复读:事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变 了,然后事务A再次读取的时候,发现 数据不匹配,就是所谓的不可重复读了。
-
可重复读(Repeatable read)
这是 MySQL 的默认隔离级别,它确保了一个事务中多个实例在并发读取数据的时候会读取到一样的数据;不过理论上,这会导致另一个棘手的问题:幻读 。幻读:事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,导致事务A再次搜索发现有N+M条数据了,就产生了幻读,简单来说就是突然多了几行数据。
为了解决幻读问题,MySQL引入了两种不同的MVCC实现方式:基于快照的MVCC 和 基于原始行的MVCC。
- 基于快照的MVCC:该方式会为每个事务创建一个快照,事务开始时记录数据库的当前版本号,当事务再次访问该行数据时,会检查当前版本号是否与快照版本号一致,如果不一致则会进行回滚或重新读取数据。
- 基于原始行的MVCC:该方式会为每行数据创建一个版本链表,每次更新操作都会创建一个新的版本号,并将旧版本号链接到新版本号上。当事务需要读取数据时,会检查当前版本号是否在版本链表中,如果在则读取最新版本的数据,避免幻读问题。
-
可串行化
有效避免“脏读”、“不可重复读”、“幻读”,不过效率特别低。
不可重复读和幻读比较:
- 不可重复读 针对的是
update
或delete
,是由于数据发生改变导致的- 幻读 针对的
insert
,是由于行数发生改变导致的。
Spring 事务传播属性
记忆方法:
-
两个
REQUIRED
:一定有事务- 带NEW:总是自己建自己的事务。
- 不带NEW:有就加入,没有才建。
-
两个
SUPPORTS
- 带NOT:直接不用。
- 不带NOT:有就用,没有就拉到。
-
MANDATORY
:强制的意思,必须用,语气强烈,没有就异常。 -
NEVER
:从不,就不用,语气强烈,有就异常。 -
NESTED
:嵌套的意思,有,建嵌套事务。没有,新建普通事务。
Spring 事务在什么情况下会失效?
-
非
public
修饰的方法 -
自调用(Self-Invocation)
自调用指的是一个类的方法在调用同一个类的另一个方法,事务管理会失效。
-
数据库不支持事务
MySQL中,MyISAM引擎不支持事物,InnoDB 支持事物 -
异常类型不匹配
@Transactional 注解默认只管理运行时异常(如RuntimeException及其子类)和错误(如Error)。 -
传播属性设置不当导致不走事务
@Transactional 默认的事务传播机制是:REQUIRED
,若指定成了NOT_SUPPORTED
、NEVER
事务传播机制,则事物不生效 -
捕获异常未抛出
-
Bean没有纳入Spring IOC容器管理
-
事务方法内启动新线程进行异步操作
Spring怎么解决循环依赖的问题?
-
对于构造器注入的循环依赖,Spring处理不了,会直接抛出
BeanCurrentlylnCreationException
异常。 -
对于属性注入的循环依赖(单例模式下),是通过三级缓存处理来循环依赖的。
-
对于非单例对象的循环依赖,无法处理。
什么是MVC?
MVC是指Model-View-Controller
,是一种软件设计模式,它将应用程序分为三个部分:模型、视图和控制器
MVC模式的核心思想是将应用程序的表示(视图)和处理(控制器)分离开来,从而使得应用程序更加灵活、易于维护和扩展。这种模式可以提高代码的可读性和可维护性,同时也可以促进代码的复用和分工,使得多人协作开发变得更加容易
Spring MVC工作原理
Spring MVC 原理如下图所示:
- 流程说明(重要):
- 客户端(浏览器)发送请求, DispatcherServlet拦截请求。
- DispatcherServlet 根据请求信息调用 HandlerMapping 。 HandlerMapping 根据 URL 去匹配查找能处理的 Handler(也就是我们平常说的 Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。
- DispatcherServlet 调用 HandlerAdapter适配器执行 Handler 。
- Handler 完成对用户请求的处理后,会 返回一个 ModelAndView 对象给DispatcherServlet,ModelAndView 顾名思义,包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View。
- ViewResolver 会根据逻辑 View 查找实际的 View。
- DispaterServlet 把返回的 Model 传给 View(视图渲染)。
- 把 View 返回给请求者(浏览器)
Spring Boot的优势
- 约定大于配置:大家默认的一些约定可直接使用,无需配置
- 开箱即用:无需配置,直接可使用
- 内置tomcat
Spring Boot自动装配原理
Spring Boot自动装配如下图所示:
Springboot项目的启动类需要由 @SpringBootApplication 注解修饰,该注解复合了如下三个注解。
-
@SpringBootConfiguration。表明Springboot启动类是一个配置类;
-
@ComponentScan。会将指定路径下的被特定注解修饰的类加载为Spring中的Bean,这些特定注解为@Component,@Controller,@Service,@Repository和@Configuration注解;
-
@EnableAutoConfiguration。用于开启Springboot的自动装配,该注解复合了如下两个核心注解。
- @AutoConfigurationPackage。用于将启动类所在的包里面的所有组件注册到spring容器。
- @Import(AutoConfigurationImportSelector.class)。通过
AutoConfigurationImportSelector
类加载配置文件中配置的bean。
-
自动装配流程说明(重要):
- @Import 将 AutoConfigurationImportSelector 注入到spring容器中
- AutoConfigurationImportSelector 通过 SpringFactoriesLoader 从类路径下去读取
META-INF/spring.factories
文件信息 - 此文件中有一个key为
org.springframework.boot.autoconfigure.EnableAutoConfiguration
,定义了一组需要自动配置的bean
了解Spring Boot中的日志组件吗?
在Spring Boot中,日志组件的设计遵循了门面模式的概念。
在日志处理方面,Spring Boot使用SLF4J作为门面。
SLF4J是一个抽象层,它为Java平台上的多种日志框架提供了一个统一的接口。
使用时只需要调用api即可,不需要关注是哪个组件进行实现的
Spring Boot默认使用的 logback
日志组件。
MySql
聚集索引和非聚集索引的区别
- 聚集索引:包含主键和非主键数据以及索引
- 非聚集索引:只包含主键和索引,当没有实现索引覆盖时会进行回表,走聚集索引
数据库三范式
-
第一范式
1NF 原子性,列或者字段不能再分,要求属性具有原子性,不可再分解; -
第二范式
2NF 唯一标识。即每个表只描述一种实体,每个记录都有唯一标识,不存在部分依赖关系。 主要是解决行的冗余。- 每个表必须有一个主键
- 非主键字段要完全依赖于主键
-
第三范式
3NF 直接性,非主键字段不依赖于其它非主键字段, 主要是解决列的冗余.
MyISAM 存储引擎 与 InnoDB 引擎区别
-
事务支持:MyISAM 不支持事务处理,而 InnoDB 支持事务处理,可以通过使用事务来确保数据的完整性和一致性。
-
锁定机制(锁的粒度):MyISAM 表级锁在执行 SELECT 操作时会对 表 进行读锁定,而执行 INSERT、UPDATE 或 DELETE 操作时会对 表 进行写锁定,因此在写操作执行时,读操作会被阻塞。而 InnoDB 支持行级锁,不会对整个表进行锁定,可以减少锁定冲突和死锁的发生。
-
外键支持:MyISAM 不支持外键约束,而 InnoDB 支持外键约束,可以通过外键约 束来保证数据的引用完整性
-
并发性能:在并发性能方面,InnoDB 要优于 MyISAM。由于 InnoDB 支持行级锁定 和 事务处理,因此在高并发情况下,InnoDB 的并发性能更高。
因此,在设计数据库时,需要考虑具体情况选择适合的 存储引擎。
- 如果需要支持事务处理 和 外键约束,以及具有更好的并发性能,则应选择 InnoDB。
- 如果只是进行简单的读写操作,并且需要更快的查询速度,则可以选择 MyISAM。
索引
索引存储在 内存 中,为服务器存储引擎为了快速找到记录的一种数据结构。
索引的主要作用是 加快数据查找速度,提高数据库的性能。 空间换时间
索引的优缺点
优点:加快查询效率
-
唯一性索引:保证数据库表中每一行数据的唯一性
-
加速查询:减少数据扫描的行数,从而提高查询速度。
-
加速排序和分组:索引可以帮助优化 ORDER BY 和 GROUP BY 操作。
缺点:
- 占用空间:索引会增加磁盘空间的使用。
- 影响写性能:插入、删除、更新操作需要维护索引,可能会导致性能下降。
索引的分类
-
普通索引:加速数据的检索,允许重复值。
-
唯一索引:确保列中的所有值唯一,加速查询。但允许有空值。
-
主键索引:特殊的唯一索引,用于唯一标识数据表中的某一条记录,不允许有空值。
-
联合索引(又叫复合索引):由多个列组成的索引,提升多列查询的效率。遵循靠左原则。
最左前缀匹配原则:即在使用复合索引时,查询条件必须包含索引中最左边的列才能触发索引的使用。如果查询条件没有包含最左边的列,则索引无法生效。
使用场景:
- 登录时,手机号+密码
- 商品表中,类型+状态
- 订单表中,订单号+用户Id
-
全文索引:用于快速全文搜索。不过一般使用ElasticSearch做全文索引(搜索引擎库)。
索引结构
Mysql 目前提供了以下 4 种索引:
- B+Tree 索引: 最常见的索引类型, 大部分索引都支持 B+树索引.
- Hash 索引: 只有 Memory 引擎支持, 使用场景简单.
- R-Tree 索引(空间索引): 空间索引是 MyISAM 引擎的一个特殊索引类型, 主要地理空间数据
- S-Full-text(全文索引): 主要用于全文索引, InnoDB 从 Mysql5.6 版本开始支持全文索引.
B树与B+树的区别
- 存储数据的位置:
- B树: 数据既存储在所有节点中(叶子节点和非叶子节点都有数据)
- B+树: 所有的数据记录都存储在叶子节点中,非叶子节点仅包含索引信息。叶子节点包含了完整的数据和索引键。
- 叶子节点之间的链接:
- B树: 叶子节点之间没有链接。
- B+树: 叶子节点之间通过指针相互链接,形成一个 链表 或 循环链表,这使得范围查询和遍历变得高效。
补充:动画演示数据结构
索引失效的几种情况
-
范围条件查询
当查询到的记录大于总记录数30%时,就不再使用索引,直接会扫描全表 -
索引列上操作(使用函数、计算等)导致索引失效
-
字符串不加引号, 造成索引失效
-
OR 关键字连接:OR关键字两边的字段必须都要有索引,任一个字段没索引就会进行全表扫描
-
使用
!=
导致索引失效 -
like以通配符开头
('%abc...')
导致索引失效 -
排序列包含不同索引的列
数据库锁
-
行锁和表锁
主要是针对锁粒度划分的,一般分为:行锁、表锁、库锁-
行锁:访问数据库的时候,锁定整个行数据, 防止并发错误。
-
表锁:访问数据库的时候,锁定整个表数据,防止并发错误。
行锁 和 表锁 的区别:
- 行锁:
开销大,加锁慢,会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高 - 表锁:
开销小,加锁快,不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
-
-
悲观锁和乐观锁
- 悲观锁:每次去拿数据的时候都认为会进行修改,所有每次在拿数据的时候都会上锁.
- 乐观锁:每次去拿数据的时候都认为不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间有没有更新这个数据,使用版本号机制,如果版本号不支持舍弃这次操作,并且设置 重试机制,乐观锁适用于多读的应用类型 ,这样可以提高吞吐量 。
MySql 优化
SQL慢查询优化
- 定位执行效率慢的 sql 语句
- EXPLAIN:可以帮助分析查询计划,查看索引是否被使用。
- 慢查询日志:开启 MySQL 的慢查询日志,分析执行时间较长的查询,优化相关索引。
- 拓展:
- 使用
xxl-job
查询慢日志 - 通过
elk
监控自己项目的日志 - 使用
skyWalking
查看哪个接口比较慢
- 使用
-
Sql 语句优化
-
多表连接的字段上需要建立索引,这样可以极大提高表连接的效率.
-
避免在索引列上使用计算
-
避免在索引列上使用
IS NULL
和IS NOT NULL
-
对查询进行优化,应尽量避免全表扫描,首先应考虑在
where
及order by
涉及的列上 建立索引。 -
应尽量避免在 where 子句中对字段进行表达式操作和
null
值判断,这将导致引擎放弃使用索引而进行全表扫描 -
排序时,尽量同时用升序或同时用降序.
-
分组时默认会进行排序,额外的排序会降低效率. 如果不需要排序可以禁止, 使用
order by null
禁用默认排序. -
尽量避免子查询, 可以将子查询优化为 join 多表连接查询.
-
避免使用
SELECT *
-
列表数据不要拿全表,要使用LIMIT来分页,每页数量也不要太大,当只要一行数据时使用
LIMIT 1
-
索引优化
-
对查询频繁, 且数据量比较大的表, 建立索引.
-
遵循最左前缀匹配原则,合理设计复合索引。
复合索引命名规则: index _ 表名 _ 列名 1 _ 列名 2 _ 列名 3
-
使用覆盖索引,减少回表查询。
-
定期检查并删除冗余或重复的索引。
-
利用唯一索引提高查询效率, 区分度越高, 使用索引的效率越高.
-
使用短索引, 提高索引访问时的 I/O 效率, 从而提升 Mysql 查询效率.
表优化
-
保留冗余字段:避免表之间的连接过于频繁
-
增加派生列。派生列是由表中的其它多个列的计算所得,增加派生列可以减少统计运算 这也就是反第三范式。
-
分割表:垂直拆分和水平拆分。
- 水平切分:将记录散列到不同的表中,各表的结构完全相同,每次从分表中查询, 提高效率。
- 垂直切分:将表中大字段单独拆分到另外一张表, 形成一对一的关系。
-
字段设计
-
表的字段尽可能用
NOT NULL
-
字段长度固定的表查询会更快
-
把数据库的大表按时间或一些标志分成小表
-
相关文章:数据库事务相关面试题
Redis
为什么要用Redis?
- 基于内存操作,内存读写速度快
- k-v模型,value值支持多种数据类型,包括String、Hash、List、Set、ZSet等
- 支持持久化,Redis支持
RDB
和AOF
两种持久化机制,持久化功能可以有效地避免数据丢失问题 - 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
- 支持高并发,使用IO模型(epoll), 天生支持高并发.
- 工作线程单线程,即避免频繁的上下文切换,又避免了线程安全问题,Redis6.0之后IO线程引入了多线程
- 具有本地方法,计算向数据移动
Redis到底是多线程还是单线程?
对于Redis到底是多线程还是单线程,分为两个阶段:
- Redis 6.0 之前:
Redis是 单线程 的,IO操作和计算操作串行执行 - Redis 6.0之后 :
Redis 将IO操作交给IO线程处理,并且使用线程池,使 IO线程实现多线程 并行执行IO操作,而计算操作由工作线程处理,工作线程仍保持单线程。
Redis数据持久化机制
持久化就是把内存的数据写到磁盘中,防止服务宕机导致内存数据丢失。
Redis支持两种方式的持久化,一种是 RDB
的方式,一种是 AOF
的方式。前者会根据指定的规则定时将内存中的数据存储在硬盘上,而后者在每次执行完命令后将命令记录下来。一般将两者结合使用。
RDB方式
RDB
是 Redis 默认的持久化方案。RDB
持久化时会将内存中的数据写入到磁盘中,也就是快照(Snapshot),数据恢复是将快照文件直接读到内存中。
RDB的优缺点:
- 缺点 :
- 快照时间有间隔,不能实时备份,丢失数据可能会比较多
- 开启子进程备份数据,在数据集比较庞大时,fork()可能会非常耗时,造成服务器在一定时间内停止处理客户端。
- 优点 :
- 恢复数据比较快
- 备份的文件就是原始内存数据的大小,不会额外增加数据占用。
AOF方式
AOF(append only file)持久化:以独立日志的方式记录每次写命令,Redis重启时会重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,AOF 是Redis持久化的主流方式。
AOF的优缺点:
- 优点 :
- 数据安全性高,不易丢数据
- AOF文件有序保存了所有写操作,可读性强
- 缺点 :
- AOF方式生成文件体积变大
- 数据恢复速度比RDB慢
Redis是单线程,但为什么快?
- Redis 基于内存,内存的访问速度比磁盘快很多
- 单线程操作,避免了频繁的上下文切换
- 合理高效的数据结构
- 采用了非阻塞I/O多路复用机制 epool
Redis 过期删除策略
- 惰性删除 :放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
- 定期删除 :每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定
Redis 内存淘汰策略
Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
Redis 提供 8 种数据淘汰策略:
LRU 全称Least recently used, 淘汰的是最近最久未使用的数据项。
LFU 全称Least-frequently used,淘汰的是最近访问频率最低的数据项,4.0及以上版本可用。
范围 | 淘汰策略名称 | 策略含义 | 人话 |
---|---|---|---|
默认策略 | noeviction | 不淘汰数据;写不进去返回错误 | 不删除任意数据,这时如果内存不够时,会直接返回错误 |
只针对设置 过期的keys | volatile-lru | 根据 LRU 算法挑选数据淘汰 | 从设置了过期时间的数据集中,选择最近最久未使用的数据释放 |
volatile-lfu | 根据 LFU 算法挑选数据淘汰 | 淘汰掉设置了过期时间的key过去被访问次数最少的数据 | |
volatile-random | 随机挑选数据淘汰 | 从设置了过期时间的数据集中,随机 | |
volatile-ttl | 挑选越早过期的数据进行删除 | 从设置了过期时间的数据集中,选择马上就要过期的数据进行释放操作 所有keys | |
所有keys | allkeys-lru | 根据 LRU 算法挑选数据淘汰 | 从数据集中(包括设置过期时间以及未设置过期时间的数据集中) 选择最近最久未使用的数据释放 |
allkeys-random | 随机挑选数据淘汰 | 随机选择一个数据进行释放 | |
allkeys-lfu | LFU 算法挑选数据淘汰 | 淘汰掉过去被访问次数最少的一条数据 |
在百万keys的Redis里面,如何模糊查找某几个key?
在 Redis 中进行模糊查找 keys
通常使用 KEYS
命令或者 SCAN
命令配合模式匹配。
但是需要注意的是,KEYS
命令在大数据量的情况下可能会导致性能问题,因为它会阻塞服务器并消耗大量资源。
因此,在生产环境中,推荐使用 SCAN
命令来实现类似的功能
SCAN
命令并不能保证每次返回相同数量的 keys
,它只是尽量接近 COUNT
参数指定的数量。
Redis 数据类型的使用场景
数据类型 | 使用场景 |
---|---|
String | Session会话 |
业务缓存 | |
分布式锁 | |
计数器 | |
限流 | |
全局唯一Id | |
Hash | 电商购物车 |
Bitmap | 用户签到 |
List | 消息队列 |
ZSet | 排行榜 |
Redis主从同步机制
步骤如下:(全量-增量)
- 从服务器向主服务器发送同步命令
sync
; - 主服务器接收到 同步命令 后,会执行
bgsave
命令,在后台生成一个rdb
文件,并使用一个缓冲区记录从现在开始执行的所有写命令; - 当主服务器执行完
bgsave
命令后,主服务器会将bgsave
命令生成的rdb
文件发送给从服务器; - 从服务器接收到这个
rdb
文件,然后加载到内存 ;之后主服务器会把刚刚在缓存区的命令同步过来,从服务器就会执行这些命令(两边就一致了,全量)。 - 以上处理完之后,之后主数据库每执行一个写命令,都会将写命令发送给从数据库(增量)
Redis集群模式有哪些?
Redis提供了多种集群模式以适应不同场景下的 高可用性 和 水平扩展需求 。以下是Redis集群模式:
- 主从复制(Master-Slave)模式:
在此模式下,有一个主节点负责处理写入请求,而从节点则复制主节点的数据并提供读取服务。- 优点:实现简单,能实现数据冗余,通过 读写分离 提高系统性能。
- 缺点:需要手动进行故障转移,无法自动处理主节点故障;不支持自动的数据分区(sharding),难以做到水平扩展。
- 哨兵(Sentinel)模式:
Sentinel是Redis提供的一个 高可用性 解决方案,它能监控主从节点状态,并在主节点出现故障时自动完成故障转移。- 优点:解决了主从模式下手动故障转移的问题,提供了 自动化监控和故障恢复机制。
- 缺点:虽然比主从模式增加了自动化,但仍 不支持自动的数据分区,且随着节点数量增加,管理和配置的复杂性也会增大。
- Redis Cluster模式:
Redis Cluster是官方正式支持的 分布式 解决方案,它采用了数据分片(sharding)技术,将数据分散在多个节点上。- 优点:真正实现了 分布式存储,每个节点都可以处理读写请求,具备 良好的水平扩展能力;内置了数据自动分割、故障检测与转移功能。
- 缺点:相比其他模式更复杂,需要更多的网络资源和配置管理;客户端需要支持集群特性;跨slot的数据操作可能涉及多个节点,有一定复杂度。
Redis缓存穿透,缓存击穿,缓存雪崩
- 缓存穿透: 缓存和数据库中都不存在要请求的数据 (解决方法:黑名单、布隆过滤器)
- 缓存击穿:一个或多个热点的key失效了,缓存中没有但数据库中有的数据,这时大量的并发请求直接到达数据库 (解决方法:提前预热)
- 缓存雪崩:大量key同时失效,查询数据量巨大,引起数据库压力过大甚至down机 (解决方法:避免大量的key同一时间失效,错峰)
布隆过滤器
布隆过滤器(Bloom Filter)是一种概率型数据结构,它主要用于检测一个集合中是否包含某个元素,特别适用于大数据量的情况下进行快速查找。
布隆过滤器的主要特点包括空间效率高和查询速度快,但也存在一定的误报
结论:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
数据库和缓存的一致性
当涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。
常见更新策略:
- 先删缓存,再更新数据库
- 先更新数据库,再删除缓存
- 普通双删: 删缓存->更新数据库->再删除缓存
- 延迟双删: 删缓存->更新数据库->延迟3-5秒再删除缓存
不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。
要真正解决一致性问题可使用
canal
canal
:以数据库从表的方式来监听数据库数据的变化
Elasticsearch
Elasticsearch 是什么?
ElasticSearch 是一个开源的 分布式、RESTful 搜索和分析引擎,可以用来解决使用数据库进行模糊搜索时存在的性能问题,适用于所有类型的数据,包括文本、数字、地理空间、结构化和非结构化数据
Elasticsearch 使用场景?
- 电商网站检索
- ELK 日志采集
倒排索引是什么?
倒排索引 也被称作反向索引(inverted index),是用于提高数据检索速度的一种数据结构,空间消耗比较大。
倒排索引首先将检索文档进行分词得到多个词语/词条,然后将词语和文档 ID 建立关联,从而提高检索效率。
倒排索引创建流程
- 建立文档列表,每个文档都有一个唯一的文档 ID 与之对应。
- 通过分词器对文档进行分词,生成类似于 <词语,文档ID> 的一组组数据。
- 将词语作为索引关键字,记录下词语和文档的对应关系,也就是哪些文档中包含了该词语
倒排索引检索流程
- 根据分词查找对应文档 ID
- 根据文档 ID 找到文档
MongoDB
MongoDB 是什么?
MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂的数据类型,是一款非常流行的 文档类型数据库
Mysql和MongDB对比
MySQL | MongDB | |
---|---|---|
数据库 | 一个数据库包含多个表 | 一个数据库包含多个集合 |
集合 | 一个表包含多行数据 | 一个集合包含多个文档 |
文档 | 一行数据由多个列组成。 | 一个文档是一个键值对的集合,类似于 JSON 对象。 |
键 | 列名定义了表中的字段 | 文档中的键对应于字段名 |
值 | 每一列都有特定的数据类型,如 VARCHAR、INT 等 | 文档中的值可以是各种类型的数据,包括字符串、数字、数组、嵌套文档等 |
MyBatis
MyBatis四种拦截器
-
Executor(执行器拦截器):
- 作用:拦截MyBatis执行器方法的执行
- 使用场景: update、query、commit、rollback 等。可以用来实现缓存、事务、分页等功能。
-
StatementHandler(语句拦截器):
- 作用:拦截SQL语句的执行
- 使用场景:prepare、parameterize、batch、update、query 等。可以用来修改 SQL 语句、添加参数、记录日志等功能。
-
ParameterHandler(参数拦截器):
- 作用:拦截SQL语句的参数设置
- 使用场景:setParameters 等。可以用来动态添加最后更新人、最后更新时间等。
-
ResultHandler(结果集拦截器):
- 作用:拦截从SQL语句返回的结果集的处理
- 使用场景:handleResultSets、handleOutputParameters 等。可以用来转换或过滤结果集等功能。
拦截器执行位置图示:
JDBC执行流程
JDBC执行流程大致可总结为以下六步:
- 第一步:注册驱动
- 第二步:获取连接
- 第三步:获取数据库操作对象
- 第四步:执行SQL语句
- 第五步:处理查询结果集
- 第六步:释放资源
详细流程可参考:JDBC执行流程
MyBatis执行流程
详细流程可参考:MyBatis执行流程
Mybatis的一级二级缓存
-
一级缓存默认开启,二级缓存默认关闭
-
为什么不用二级缓存?
因为开启二级缓会在内存中开辟一块空间,比如我们有多个应用的话,每个应用都会创建二级缓存会造成内存的大量占用。
-
生命周期:
-
一级缓存:同一个
SqlSession
,当提交或关闭时清空缓存 -
二级缓存:同一个
SqlSessionFactory
对象创建的多个SqlSession
共享其缓存
-
mybatis一对多,多对多
在MyBatis中,处理一对多和多对多关系通常使用association
和collection
标签。
- 一对多:使用
association
标签,指定javaType
属性指向实体类。 - 多对多:同样使用
collection
标签,但需要指定中间关联表
什么是ORM?ORM框架有哪些?
ORM, Object-Relationl Mapping,对象关系映射。它的作用是在关系型数据库 和 对象 之间作一个映射。
常用ORM框架:
-
Hibernate
-
MyBatis
MyBatis与Hibernate的区别
- MyBatis:
- ORM思想: 半自动ORM,需要手动编写SQL语句。
- 特点: 灵活性高,性能好,适合复杂的SQL查询。
- 适用场景: 对SQL有较高要求的场景。
- Hibernate:
- ORM思想: 全自动ORM,通过注解或XML配置映射关系。
- 特点: 自动化程度高,开发效率高,适合简单的CRUD操作。
- 适用场景: 快速开发,对SQL要求不高的场景。
常用动态sql?
-
if
:通过判断条件,动态地生成 SQL 语句。 -
choose
:类似于 Java 中的 switch 语句,根据条件选择生成哪一条 SQL 语句。 -
where
:用于在 SQL 语句的 WHERE 子句中动态拼接条件。 -
set
:用于在 SQL 语句的 SET 子句中动态拼接更新字段和值。 -
foreach
:用于遍历集合或数组,动态生成 SQL 语句。
MyBatis接口的底层原理
- 先定义一些接口(称为 Mapper/Dao),并且通过 XML 文件或者注解的方式定义对应的 SQL
- 当我们注入这个Dao的实例时,注入的其实就是mybatis提供的代理实现类,它使用jdk动态代理通过我们的接口动态创建了代理类,并通过Aop进行了增强。
- 当调用 Dao中的方法时,动态代理会根据方法签名找到对应的 SQL 语句,并通过SqlSession 负责执行对应SQL。
- 执行完 SQL 后,根据 Mapper 文件中定义的映射规则,将结果集转换为 Java 对象返回给调用者。
PageHelper分页的原理是什么?
-
当我们在代码中使用
PageHelper.startPage(int pageNum, int pageSize)
设置分页参数之后,其实PageHelper会把他们存储到ThreadLocal
中。 -
PageHelper会在执行器的
query
方法执行之前,会从ThreadLocal中再获取分页参数信息,页码和页大小,然后执行分页算法,计算需要返回的数据块的起始位置和大小。 -
最后,PageHelper会通过修改SQL语句的方式,在SQL后面动态拼接上limit语句,限定查询的数据范围,从而实现物理分页的效果
MyBatis中接口绑定有几种实现方式?
- 通过注解绑定,在接口的方法上面加上 @Select@Update等注解里面包含Sql语句来绑定(SQL语句比较简单的时候,推荐注解绑定)
- 通过xml里面写SQL来绑定, 指定xml映射文件里面的namespace必须为接口的全路径名(SQL语句比较复杂的时候,推荐xml绑定
#{}和${}的区别是什么?
#{ }
被解析成预编译语句,预编译之后可以直接执行,不需要重新编译${ }
仅仅为一个字符串替换,每次执行sql之前需要进行编译,存在 sql 注入问题。
MyBatis 中如何执行批处理?
使用 BatchExecutor
完成批处理
RabbitMQ
为什么用MQ? 异步 、削峰、解耦
MQ(Message Queue,消息队列)是一种在分布式系统中进行消息传递的技术,它主要用于实现服务间的 解耦、异步处理、削峰填谷等功能。下面我将分别解释这几个概念,并说明它们是如何通过MQ实现的。
1. 异步处理
异步处理是指一个操作不需要等待另一个操作完成就可以继续执行的过程。
- 在传统的同步模式下,客户端发送请求后必须等待服务器响应才能继续执行后续操作;
- 而在异步模式下,客户端发送请求后无需等待,可以立即返回并执行其他任务,服务器处理完请求后再通知客户端结果。
MQ如何实现异步处理:
- 当客户端向服务端发送请求时,该请求不是直接被服务端处理,而是先存入MQ中。
- 服务端从MQ中获取请求消息后进行处理,客户端无需等待服务端处理完成即可继续执行其他任务。
- 这种方式提高了系统的响应速度和吞吐量,使得系统能够处理更多的并发请求。
2. 解耦
解耦是指降低系统各组件之间的依赖性,使得每个组件都可以独立地开发、部署和扩展。这有助于提高系统的灵活性和可维护性。
MQ如何实现解耦:
- 发送方将消息发送到MQ中,而接收方从MQ中读取消息。这样发送方和接收方之间就不存在直接的调用关系。
- 即使接收方暂时不可用,发送方也可以继续发送消息,因为消息会被暂存在MQ中,直到接收方恢复可用。
- 发送方和接收方可以独立扩展,而不影响彼此的工作。
3. 削峰填谷
削峰填谷是指通过缓存、异步处理等手段来平衡系统的负载,避免高峰期系统过载或低谷期资源浪费的情况。
MQ如何实现削峰填谷:
- 在高流量期间,系统产生的大量消息会被存入MQ中,而不是直接由后端服务处理。这可以防止后端服务因短时间内处理大量请求而过载。
- 当流量高峰过去后,后端服务可以从MQ中慢慢消费这些消息,从而平衡处理过程,确保服务质量。
- 通过这种方式,系统能够在高峰期吸收额外的请求,在低谷期释放资源,从而达到资源的有效利用。
综上所述,MQ作为一种重要的中间件技术,对于提升系统的性能、稳定性和可扩展性具有重要作用。
Exchange类型
常用的交换机有以下三种,因为消费者是从队列获取信息的,队列是绑定交换机的,所以对应的消息推送/接收模式也会有以下几种:
- Direct Exchange
直连型交换机,根据RoutingKey(路由键)路由到不同的队列 - Fanout Exchange
扇型(广播)交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。 - Topic Exchange
主题交换机,这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。
简单地介绍下规则:
*
(星号) 用来表示一个或多个字符#
(井号) 用来表示任意数量的字符
什么是死信队列?
-
死信(Dead Letter):是指在消息队列系统中那些无法被正常消费的消息。
-
死信队列(Dead Letter Queue, DLQ):当消息无法被正常处理时,也就是死信,可以将这些死信发送到一个专门的队列中,以便于后续检查和处理
以下是一些可能导致消息成为死信的情况:
- 消息被拒绝访问:消费者显式地拒绝了消息(使用 channel.BasicNack 或 channel.BasicReject 方法),并且设置了 requeue 参数为 false,这意味着消息不应该被重新放回原队列。
- 消费者发生异常,超过重试次数 。 (其实spring框架调用的就是 basicNack)
- 消息过期:如果消息设置了生存时间(Time To Live, TTL),并且超过了这个时间限制,消息就会变为死信。
- 队列达到最大长度:如果消息队列达到了最大长度限制,新的消息将无法加入队列,这时这些新消息也会变成死信。
如何保证消息的可靠性?
消息丢失场景:
- 消息到 MQ 的过程中搞丢
- MQ 自己搞丢
- MQ 到消费过程中搞丢。
生产端消息可靠性保证:
-
消息持久化:
当生产者发布消息时,可以选择将其标记为持久化到磁盘上。 -
确认(Confirm)机制:
开启confirm回调模式后,RabbitMQ会在消息成功写入到磁盘并至少被一个交换器接受后,向生产者发送一个确认(acknowledgement)。若消息丢失或无法投递给任何队列,RabbitMQ将会发送一个否定确认(nack). 生产者可以根据这些确认信号判断消息是否成功送达并采取相应的重试策略。
消费端消息可靠性保证:
-
消息确认(Acknowledgements):
- 手动应答:代码冗余多,容易出现死循环。
- 自动应答:开启重试功能,发生错误时重新发送,可配置死信队列,重试一定次数后放入死信队列。
-
死信队列(Dead Letter Queue):
RabbitMQ中如何解决消息堆积问题?
解决消息堆积有三种种思路:
- 扩大队列容积,提高堆积上限,采用惰性队列
- 在声明队列的时候可以设置属性x-queue-mode为lazy,即为惰性队列
- 基于磁盘存储,消息上限高
- 性能比较稳定,但基于磁盘存储,受限于磁盘I0,时效性会降低
- 增加更多消费者,提高消费速度(不能保证有序性)
- 在消费者内开启线程池加快消息处理速度(不能保证有序性)
RabbitMQ中如何保证消息有序性?
单个队列与单一消费者:
- 单个队列:将所有需要保持有序的消息发送到同一个队列中。RabbitMQ的队列是先进先出(FIFO)的数据结构,消息在被发送到队列之后,会按照发送的顺序被排列在队列中。
- 单一消费者:确保该队列只被一个消费者(单线程)处理。这样,消费者会按照队列中的顺序接收到消息,并依次处理,从而保证了消息的顺序性。多消费者时会对消息进行均发,因此无法保证顺序。
如何防止消息重复消费?(如何保证消息幂等性)
幂等性指的是一个操作无论执行多少次,其结果都是相同的。
在分布式系统和消息队列中,幂等性特别重要,因为它可以确保即使在消息重复发送或处理过程中出现故障的情况下,系统状态的一致性和数据的完整性。
生产端保证消息的幂等性:
- 状态检查:
在消息发送前,先查询数据库,确认此消息是否已被处理过(一般通过单据状态)。如果是,则直接忽略;否则,继续处理,并在处理完成后更新消息状态为已处理。
消费端保证消息的幂等性:
- 唯一标识:每个消息都携带一个业务ID(BizId),如订单号、交易流水号等,以便在消费端能够识别重复的消息。
多线程异步和MQ的区别
- CPU消耗。多线程异步可能存在CPU竞争,而MQ不会消耗本机的CPU。
- MQ 方式实现异步是完全解耦的,适合于大型互联网项目。
- 削峰或者消息堆积能力。当业务系统处于高并发,MQ可以将消息堆积在Broker实例中,而多线程会创建大量线程,甚至触发拒绝策略。
- 使用MQ引入了中间件,增加了项目复杂度和运维难度。
总的来说,规模比较小的项目可以使用多线程实现异步,大项目建议使用MQ实现异步
Spring Cloud
分布式与微服务区别?
- 概念层面:
- 微服务是设计层面的东西,一般考虑如何将系统从逻辑上进行拆分,也就是垂直拆分;
- 分布式是部署层面的东西,即强调物理层面的组成,即系统的各子系统部署在不同计算机上。
- 粒度划分:
- 微服务倾向于更细粒度的服务划分,每个服务只专注于一个特定的业务功能,并力求做到“做一件事并做好”
- 分布式系统中的服务划分粒度可大可小,可以包含多个紧密相关的业务功能
- 目标:
- 微服务架构的核心理念是围绕业务能力组织服务,强调服务之间的松耦合和高内聚
- 分布式系统的设计目标是为了提高系统的可靠性、可用性、可扩展性和稳定性
一句话概括:分布式:分散部署;微服务:分散能力。
什么是CAP原则?
- 一致性(Consistency):所有节点在同一时刻看到相同的数据。
- 可用性(Availability):每个请求不管成功或者失败都应该在合理的时间内得到响应。
- 分区容错性(Partition tolerance):分区容错性是说当网络故障导致分布式系统的一部分节点无法与其他节点通信(有节点挂掉时),系统仍应该能够运作。
在一个分布式系统中,不可能同时实现一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)。因此,设计分布式系统时必须在这三个属性之间做出选择,只能同时满足其中的两个。
常见的有CA、CP、AP三种系统,而我们通常使用AP保证可用性和分区容错性,不追求实时一致性,只保证最终一致性即可。
Spring Cloud Alibaba 组件有哪些?
Spring Cloud Alibaba 是阿里开源的一套微服务开发组件,致力于提供微服务开发的一站式解决方案,核心组件有下面这些:
- Sentinel:以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
- Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。服务。
- Seata:分布式事务解决方案。
除了上面这些之外,使用 Spring Cloud Alibaba 的时候一般会搭配下面这些 Spring Cloud 组件一起使用:
- OpenFeign:轻量级 RESTful 的 HTTP 服务客户端
- Gateway:用于网关服务,实现请求的转发和路由。
- Ribbon:用于客户端负载均衡,将请求分发给不同的微服务实例。
Nacos配置中心动态刷新原理
Nacos 使用长轮询机制来实现实时配置更新。具体过程如下:
- 客户端发起请求:客户端通过后台线程发起一个 HTTP 请求 到 Nacos 服务端,用于 监听配置变化。
- 服务端挂起连接:Nacos 服务端接收到请求后,会挂起(hold)这个 HTTP 连接一段时间(例如 30 秒),在此期间服务端监控配置文件的变化。
- 两种情况
- 无变化情况:若在这段时间内没有检测到配置文件有任何变更,服务端将释放连接并向客户端返回一个指示,表明配置没有更新。
- 配置变更情况:如果在挂起期间检测到配置文件发生变化,服务端会立即释放连接并将最新的配置推送给客户端。
- 循环轮询:无论哪种情况,客户端在接收完响应后,会在短暂延迟(如 10 毫秒)之后重新发起一个新的 HTTP 请求,从而形成循环轮询机制以持续监听配置更新。
这种方式使得客户端能够及时获取到最新的配置信息,同时减少了不必要的频繁请求。
目前主流的负载方案有哪些?
目前主流的负载方案分为以下两种:
- 服务端负载均衡
在消费者和服务提供方中间使用独立的代理方式进行负载,有硬件的(比如 F5),也有软件的(比如 Nginx,OpenResty) - 客户端负载均衡器
客户端根据自己的请求情况做负载均衡,Ribbon 就属于客户端自己做负载均衡。
Nginx作为服务端负载均衡器,常见的负载均衡策略有哪些?
- Round Robin Rule :轮询策略,依次选择每个服务实例。
- Least Connections:最少连接数,最少连接数算法将请求分配给当前活动连接数最少的后端服务器。这有助于避免某些服务器过载,而其他服务器则处于空闲状态的情况。
- IP Hash:IP哈希,IP哈希算法根据客户端IP地址的哈希值将请求分配给后端服务器。这样,同一个客户端的请求总是被分配到同一台后端服务器,从而实现会话粘性。
- URL Hash:URL哈希,URL哈希算法根据请求URL的哈希值将请求分配给后端服务器。这样,相同URL的请求总是被分配到同一台后端服务器。
- Weighted Round Robin:加权轮询,加权轮询算法在轮询的基础上,为每台后端服务器分配一个权重。数字越大,权重越高,分配到的请求越多。适用于后端服务器性能差异较大的情况。
Spring Ribbon相关
最少连接数算法将请求分配给当前活动连接数最少的后端服务器。这有助于避免某些服务器过载,而其他服务器则处于空闲状态的情况。
Spring Ribbon是什么?
Spring Ribbon 是一个客户端负载均衡工具,它可以在调用微服务时动态地从多个实例中选择一个来进行访问。
Ribbon主要作为Netflix OSS组件集成在Spring Cloud中使用,帮助服务消费者在不同的服务实例之间进行负载均衡,通过这种方式增加系统的可用性和容错性
Ribbon负载均衡策略有哪些?
- Round Robin Rule :轮询策略,依次选择每个服务实例。
- Random Rule :随机策略,随机选择一个服务实例。
- Weighted Response Time Rule :根据响应时间来分配权重,响应时间短的服务实例权重越大。
Ribbon第一次调用为什么会很慢?
Ribbon默认是采用懒加载,Ribbon 客户端在第一次使用时需要初始化一些内部状态,例如从服务注册中心拉取服务实例列表、构建连接池,TCP 三次握手创建连接等,因此第一次调用会很慢。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时。
Feign 和 OpenFeign 的区别?
-
- Feign是由Netflix开发的一个声明式的HTTP客户端库
-
- OpenFeign是Spring Cloud官方基于Feign开发的一个分支
- 提供了对Spring MVC的支持
- 提供了对Spring的自动配置支持,使得集成更加简便
- 整合了更多的扩展 (请求重试策略、超时控制、请求拦截器)
- OpenFeign是Spring Cloud官方基于Feign开发的一个分支
流量网关与服务网关的区别?
流量网关和服务网关在系统整体架构中所处的位置如上图所示,
-
流量网关:(如Nignx,OpenResty,Kong)是指提供全局性的、与后端业务应用无关的策略,例如 HTTPS证书认证、Web防火墙、全局流量监控,黑白名单等。
-
服务网关:(如Spring Cloud Gateway)是指与业务紧耦合的、提供单个业务域级别的策略,如服务治理、token认证,负载均衡等。也就是说,流量网关负责南北向流量调度及安全防护,微服务网关负责东西向流量调度及服务治理。
限流、降级和熔断有什么区别?
在微服务架构中,降级(Degradation)和熔断(Circuit Breaker)都是用来提升系统稳定性和可用性的技术手段
- 限流:限制单位时间内通过的请求量或对某个资源的访问频率,防止系统过载。
- 熔断(Circuit Breaker):当某个依赖服务出现故障或响应时间过长时,暂时停止对该服务的请求,避免进一步加重故障服务的负担,并保护调用方免受性能影响。
- 降级(Degradation):当系统资源紧张或服务性能下降时,通过主动降低服务质量来保证核心功能的可用性。
Gateway的三大属性
-
路由(route):由ID、目标URI、断言集合和过滤器集合组成。如果聚合断言结果为真,则转发到该路由
-
断言(Predicate): 路由的筛选条件,允许开发人员匹配 HTTP 请求中的任何内容,比如请求头、请求参数或请求路径
-
过滤器(filter):可以在返回请求之前或之后修改请求和响应的内容
- 生命周期
- PRE:前置过滤器,在请求被路由目标服务之前调用
- POST:后置过滤器,在路由到微服务以后执行
- 作用范围
- 局部过滤器: GatewayFilter,应用到单个路由或者一个分组的路由上(需要在配置文件中配置)
- 全局过滤器: GlobalFilter,应用到所有的路由上(无需配置,全局生效)
- 生命周期
Gateway的三大案例组件
LogTime
:下游接口耗时TokenGlobalFilter
:全局Token认证TraceGlobalFilter
:traceId全局链路跟踪
为什么要用服务网关
- 简化客户端的工作
- 降低函数间的耦合度
- 解放开发人员把精力专注于业务逻辑的实现
不同服务之间如何进行通信
可以使用OpenFeign来实现不同服务之间的通信。
OpenFeign是一个声明式的Web客户端,它可以简化编写Web服务客户端的代码。它是Spring Cloud的一部分,主要用于简化与RESTful服务的集成。
在微服务中如何监控服务
可使用Skywalking实现服务监控,Skywalking是一个分布式系统的应用程序性能监控工具 (Application Performance Managment),提供了完善的链路追踪能力。
- skywalking主要可以监控接口、服务、物理实例的一些状态,特别是在压测的时候可以看到众多服务中哪些服务和接口比较慢
- skywalking还可以设置告警规则,如果项目出现bug可对开发人员进行通知,以便在第一时间进行修复
Openfeign如何使用
- 引入依赖
- 编写FeignClient接口
- 在启动类中开启对OpenFeign注解的支持
- 注入FeignClient
- 发起调用,像调用本地方法一样调用远程服务
Openfeign自定义拦截器
可以通过在OpenFeign中自定义拦截器的方式,来实现服务远程调用过程中的日志输出、认证授权等应用。
自定义拦截器步骤:
- 继承
RequestInterceptor
类 - 实现apply方法,编写相应逻辑
- 将拦截器类放入Ioc容器中
Seata中2PC和3PC的区别
XA协议包括两阶段提交(2PC)和三阶段提交(3PC)两种实现
-
2PC(二阶段提交协议)
- 第一阶段:预提交阶段
由事务协调者询问通知各个事务参与者,是否准备好了执行事务 - 第二阶段:提交阶段
协调者收到各个参与者的准备消息后,根据反馈情况通知各个参与者commit
提交或者rollback
回滚
- 第一阶段:预提交阶段
-
3PC(三阶段提交协议)
- 第一阶段:准备阶段
协调者向参与者发送canCommit
请求,判断是否具备执行条件,参与者如果可以提交就返回Yes
响应,否则返回No
响应 - 第二阶段:预提交阶段
协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit
操作 - 第三阶段:提交阶段
协调接收到所有参与者发送的ACK响应,根据反馈情况通知各个参与者commit
提交或者rollback
回滚
- 第一阶段:准备阶段
-
区别:与2PC相比,3PC降低了阻塞时长,加入了超时机制,并且在等待超时后,协调者或参与者会中断事务,避免了协调者单点问题,阶段三中协调者出现问题时,参与者会继续提交事务,但是数据不一致问题仍然存在。
项目的几种发布方式和特点
- jar包发布
jar包发布过于依赖于环境 - docker发布
docker发布虽然解决了环境依赖问题,但是不好管理 - k8s发布
k8s发布可以进行流量监控,针对流量规模对容器进行自动伸缩,可以热部署,可结合cicd自动发布,容器的自动化编排,还可以在yaml配置文件里设置调优参数
MongoDB和mysql区别
- MongoDB是文档型非关系型数据库,mysql是关系数据库。
- MongoDB比较灵活,相比mysql不用建表就可以直接进行存储,针对变化比较频繁的场景,MongoDB比较适合。
- 使用场景不同,MongoDB是首页,包括我们的一些活动页,快速开发,快速迭代,MongoDB后台。
什么是分布式锁,Redisson有什么用?
分布式锁 确保在同一时间只有一个节点能获得对共享资源的独占访问权限,从而解决并发访问问题。
Redisson锁,简称看门狗,通过对分布式锁进行独特的续命机制,既避免了锁的时间过长造成的效率低下,又避免了锁的时间不够,业务未执行完时资源被其他线程抢占的问题。
原理:当锁的剩余时间到某个阈值时,业务仍未执行完,就会对锁进行续命,延长锁的时间,直至业务执行完成。
下单请求三重防重
- 前端:用户点击下单后显示loading层,或者按钮置灰,防止重复发送请求
- 后端
- redis分布式锁:处理完该用户一个下单请求后会设置一个分布式锁(比如实际场景中一般5秒内不会下单两次)
- Redisson锁(看门狗):如果业务执行时间超过五秒,就会通过看门狗机制对锁进行续命,直到业务执行结束。