目录
- Java基础
- JDK/JRE/JVM三者的关系
- JDK常用的包
- == 和 equals 的区别是什么?
- Java 中的几种基本数据类型了解么?
- 存储金额用什么数据类型?
- 内部类和静态内部类的区别?
- 静态变量和实例变量的区别?
- Java中变量和常量有什么区别?
- 面向对象有哪些特性?
- 封装的目的
- 什么是自动拆装箱?
- 基本类型和包装类型的区别?
- 装箱的好处有哪些?
- final 关键字中有什么作用?
- 接口和抽象类有什么区别?
- String, StringBuffer 和 StringBuilder区别
- String 类的常用方法有哪些?
- Object的常用方法有哪些
- continue、break 和 return 的区别是什么?
- 为什么重写 equals() 时必须重写 hashCode() 方法?
- Java创建对象有几种方式?
- throw 和 throws 有什么区别?
- &和&&的区别
- final、finally、finalize有什么区别?
- 重载和重写有什么区别?
- 什么是可变长参数?
- try-catch-finally 如何使用?
- finally 中的代码一定会执行吗?
- 常见的Exception有哪些?
- Error和Exception的区别?
- 什么是反射?
- 反射常见的应用场景
- Java 中 IO 流分为几种?
- I/O 流为什么要分为字节流和字符流?
- Files类的常用方法都有哪些?
- 什么是AIO、BIO和NIO?
- 什么是序列化和反序列化?
- 序列化和反序列化常见应用场景
- 如何在Java中实现序列化与反序列化
- 说说static 和 transient 在序列化中的区别
- java常见的引用类型
- java中深拷贝和浅拷贝
- Comparable和Comparator区别?
- 什么是SPI机制?
- 一个模板类应遵循什么标准
- SPU和SKU
- 你知道雪花算法吗?
- 你了解的设计模式有哪些?
- 享元设计模式
- Integer缓存机制
- BeanFactory和FactoryBean的区别
- Java8的新特性有哪些?
- Stream流常用方法?
- 什么是数据脱敏?
- 如何实现数据脱敏
- java应用程序的服务器有哪些
- 集合
- 并发
- 并发和并行的区别
- 线程和进程的区别?
- 线程有哪些状态?
- 创建线程的方式有哪些?有什么特点?
- Runnable 和 Callable 的区别?
- 如何启动一个新线程、调用 start 和 run 方法的区别?
- Java中常见的锁
- Synchronized锁的升级过程
- 什么是用户态和内核态?
- 什么是CAS?
- 线程相关的基本方法?
- 同步和异步的区别
- 如何实现线程同步?
- 什么是线程死锁?死锁如何产生?
- 如何避免线程死锁?
- 如何检测死锁?
- wait和sleep有哪些区别?
- JUC包提供了哪些原子类?
- JUC包常用的辅助类
- Lock和synchronized的区别
- Lock 接口实现类有哪些?有什么作用?
- synchronized的作用有哪些?
- volatile关键字有什么用?
- 什么是ThreadLocal?它的原理是什么?
- volatile和synchronized的区别是什么?
- 为什么要用线程池?
- JDK内置线程池有哪几种?
- 线程池常见参数有哪些?如何解释?
- 为什么不推荐使用内置线程池?
- 线程池的拒绝策略有哪些?
- 线程池的工作原理
- 线程池的状态有哪些?
- 怎么自定义线程池?
- JVM
- 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启动原理?
- 了解Spring Boot中的日志组件吗?
- 自定义stater
- 为什么不用Task用XXL-JOB
- 什么是本地缓存?
- 在哪用过本地缓存和分布式缓存?
- 本地缓存的方案有哪些?
- 说一下 PO、VO、DAO、BO、DTO、POJO 的区别和使用场景?
- MySql
- Redis
- Elasticsearch
- MongoDB
- MyBatis
- RabbitMQ
- Spring Cloud
- 分布式与微服务区别?
- 单体应用和微服务的区别?
- Spring Boot与Spring Cloud有什么区别?
- 什么是CAP原则?
- Spring Cloud Alibaba 组件有哪些?
- Nacos心跳机制
- Nacos配置中心动态刷新原理
- Nacos和ZooKeeper的区别?
- 目前主流的负载方案有哪些?
- Nginx作为服务端负载均衡器,常见的负载均衡策略有哪些?
- Nginx配置文件有哪些内容?
- Spring Ribbon是什么?
- Ribbon负载均衡策略有哪些?
- Ribbon第一次调用为什么会很慢?
- Feign 和 OpenFeign 的区别?
- QPS和TPS的区别?
- 流量控制规则 (FlowRule)
- 熔断降级规则 (DegradeRule)
- Sentinel 限流规则怎么进行持久化?
- 流量网关与服务网关的区别?
- 限流、降级和熔断有什么区别?
- Gateway的三大属性
- Gateway的三大案例组件
- 全局链路跟踪的流程
- 什么是MDC?
- 为什么要用服务网关
- Sentinel 和 Gateway 结合时,配置流量控制的模式有哪些?
- 不同服务之间如何进行通信
- 在微服务中如何监控服务
- 慢接口怎么追踪的?
- 怎么进行接口调优?
- Openfeign如何使用
- Openfeign自定义拦截器
- Dubbo是什么?
- RPC 是什么?
- Dubbo和Spring Cloud的区别?
- 常见分布式事务解决方案
- XA协议中2PC和3PC的区别
- 什么是TCC?
- Seata 三大角色
- Seata 的两阶段提交执行流程
- Seata涉及到了哪几张表?
- 项目的几种发布方式和特点
- Linux常用命令有哪些?
- Docker常用的命令有哪些?
- docker网络模型有哪些?
- k8s.yml文件有哪些内容?
- dockerfile文件中有哪些命令?
- 认证鉴权
- 计算机基础
- 前端拓展
- 场景题
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
。
存储金额用什么数据类型?
使用的 BigDecimal
类型
不仅可以精确表示和计算任意精度的数值,解决了浮点数的精度问题。
而且提供了丰富的数学运算方法,支持四舍五入、比较等操作
内部类和静态内部类的区别?
- 生命周期
- 内部类的实例与外部类的实例绑定在一起
- 而静态内部类的实例与外部类的实例无关。
- 访问外部类成员
- 内部类可以访问外部类的所有成员,包括私有成员;
- 静态内部类只能访问外部类的静态成员。
- 创建方式
- 内部类的创建需要外部类的实例,例如
outer.new InnerClass()
; - 静态内部类的创建不需要外部类的实例,例如
new OuterClass.StaticNestedClass()
。
- 内部类的创建需要外部类的实例,例如
静态变量和实例变量的区别?
- 定义方式:
- 静态变量:使用
static
关键字修饰。 - 实例变量:不使用
static
关键字修饰。
- 静态变量:使用
- 生命周期:
- 静态变量:类加载时初始化,程序终止时销毁。
- 实例变量:对象创建时初始化,对象销毁时释放。
- 内存分配:
- 静态变量:存储在方法区,所有对象共享。
- 实例变量:存储在堆内存,每个对象有自己的副本。
- 访问方式:
- 静态变量:可以通过类名或对象实例访问。
- 实例变量:只能通过对象实例访问。
- 作用域:
- 静态变量:属于类,所有对象共享。
- 实例变量:属于对象,每个对象独立。
Java中变量和常量有什么区别?
-
声明:常量使用
final
关键字声明。 -
值的可变性:变量的值可以在程序运行过程中多次改变。常量的值一旦赋值后,就不能再改变。
-
内存分配:变量在内存中分配空间,每次赋值时会更新内存中的值。常量在编译时确定值,并在内存中只分配一次空间,运行时不可变。
-
命名规范:变量通常使用驼峰命名法,首字母小写。常量通常使用全大写字母,单词之间用下划线分隔。
面向对象有哪些特性?
面向对象三大特性:封装,继承,多态:
- 封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。 良好的封装能够减少耦合。
- 不同类型的对象,相互之间经常有一定数量的共同点。同时,每一个对象还定义了额外的特性使得他们与众不同。
- 多态,顾名思义,表示一个对象具有多种的状态,具体表现为 父类的引用指向子类的实例。。
封装的目的
-
数据隐藏(Data Hiding)
-
增强代码的可维护性
-
提高代码的复用性
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的包装类包装起来;
- 拆箱:将包装类型转换为基本数据类型;
基本类型和包装类型的区别?
在 Java 中,基本类型和包装类型有以下几个主要区别。
- 基本类型直接存储值,占用栈内存,而包装类型是对象,存储在堆内存中,栈中存储的是对象的引用。
- 基本类型的性能较高,因为它们是值类型,直接存储在栈中,访问速度快;而包装类型的性能较低,因为它们是对象,涉及对象的创建和垃圾回收,访问速度较慢。
- 基本类型功能较为简单,只能存储和操作值,而包装类型提供了更多的方法和功能,如 toString、parseInt、valueOf 等,支持对象操作和方法调用。
- 基本类型有默认值,例如 int 的默认值是 0,boolean 的默认值是 false;而包装类型的默认值是 null,因为它们是对象。
- Java 提供了自动装箱和拆箱功能,可以自动将基本类型转换为包装类型,反之亦然。
装箱的好处有哪些?
-
简化代码:装箱使得代码更加简洁和易读,减少了显式类型转换的需要。
例如,
Integer value = 10;
比Integer value = new Integer(10);
更简洁。 -
提高开发效率:装箱减少了手动创建包装类型对象的繁琐操作,提高了开发效率。
例如,在使用泛型集合时,
list.add(10);
比list.add(new Integer(10));
更方便。 -
增强泛型支持:装箱使得基本类型可以方便地用于泛型集合和方法,因为泛型不支持基本类型。
例如,
List<Integer> list = new ArrayList<>(); list.add(10);
。 -
兼容旧代码:装箱使得新代码可以更容易地与旧代码兼容,特别是在处理旧的 API 时。
例如,可以使用
setNumber(10);
调用一个需要Integer
参数的方法。 -
自动处理 null 值:包装类型可以为
null
,而基本类型不能。装箱使得在需要处理null
值的场景中更加方便。例如,
Integer value = null;
可以直接赋值为null
。
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()
:唤醒在此对象上等待的所有线程
continue、break 和 return 的区别是什么?
continue
:指跳出当前的这一次循环,继续下一次循环。break
:指跳出整个循环体,继续执行循环下面的语句。return
:用于跳出所在方法,结束该方法的运行。
为什么重写 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)。
什么是可变长参数?
可变长参数(Varargs)是一种允许方法接受不定数量参数的功能。
在 Java 中,通过在参数类型后面加上省略号 ...
来声明可变长参数。使用可变长参数可以简化方法调用,使得方法可以接受零个、一个或多个参数。在方法内部,可变长参数会被当作数组处理。
可变长参数必须是方法参数列表中的最后一个参数,如果有其他参数,它们必须在可变长参数之前声明。可变长参数增加了方法的灵活性,使得方法调用更加方便
try-catch-finally 如何使用?
try
块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。catch
块:用于处理 try 捕获到的异常。finally
块:无论是否捕获或处理异常,finally 块里的语句总会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
finally 中的代码一定会执行吗?
不一定,以下几种情况 finally
中的代码不会被执行:
- 执行
finally
之前虚拟机被终止运行 - 程序所在的线程死亡。
- 关闭 CPU
常见的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位(两个字节),以字符为单位输入输出数据
I/O 流为什么要分为字节流和字符流?
Java 中的 I/O 流被分为字节流和字符流,主要是为了满足不同类型的输入输出需求。
-
字节流:用于处理原始二进制数据,以字节为单位进行读写操作,适用于图像、音频、视频文件等二进制数据的处理,以及网络通信。
-
字符流:用于处理文本数据,以字符为单位进行读写操作,支持字符编码转换,适用于文本文件、XML 文件等文本数据的处理。
字节流不涉及字符编码,性能较高,而字符流支持字符编码转换,提供了更高级的文本处理功能。
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
等文本格式 - 反序列化:将在序列化过程中所生成的数据 转换为原始数据结构或者对象的过程
序列化和反序列化常见应用场景
- 对象在进行 网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象 存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象 存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
- 将对象 存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
如何在Java中实现序列化与反序列化
- 实现
Serializable
接口 - 实现
Externalizable
接口 - 使用
JSON
序列化库 - 使用
XML
序列化库 - 使用二进制序列化库
说说static 和 transient 在序列化中的区别
static
关键字:
- 定义:static 关键字用来修饰类的变量或方法,这意味着它们是属于类本身的,而不是某个对象的实例。
- 序列化过程:在序列化过程中,被
static
修饰的变量不会被序列化。因此,序列化一个对象时,不会包含其类变量的值,因为类变量的值在JVM中是全局的,与特定的序列化实例无关。 - 反序列化过程:在反序列化时,
static
变量的值依然是保持类级别的当前状态,而不是从序列化的数据中获取。因为静态变量是共享的,因此它们与对象序列化或反序列化没有直接关联。
transient
关键字:
- 定义:用于修饰类的成员变量,指明该变量在序列化时不应被序列化。
- 序列化过程:当一个对象被 序列化时,
transient
修饰的变量则会被忽略。 - 反序列化过程:在对象 反序列化时,
transient
变量会被初始化为它们的默认值。例如,数值类型会初始化为 0,对象类型会初始化为 null。序列化前设置的值将无法恢复。
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可以有 多个字段排序
- 接口所在包不同:
什么是SPI机制?
- Java SPI
SPI 全称 Service Provider Interface,是 Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。
SPI的作用就是为这些被扩展的API 寻找服务实现。本质是通过基于接口的 编程+策略模式+配置文件 实现动态加载。可以实现 解耦 (接口和实现分离),提高框架的 可拓展性(第三方可以自己实现,达到插拔式的效果)。
- Spring SPI
Spring SPI对 Java SPI 进行了封装增强。我们只需要在 META-INF/spring.factories
中配置接口/类/注解名,即可通过服务发现机制,在运行时进行自动加载。
一个模板类应遵循什么标准
- 类,方法,属性等做到望名知意,遵循restFulApi风格。
- 写注释
- 封装和继承
- 满足单一职责
- 满足开闭原则,就是扩展开放,修改关闭。
SPU和SKU
- 区别:简单来说,SPU 更加关注于商品本身的信息,而 SKU 则侧重于商品的具体规格和库存管理。
- 联系:一个 SPU 可能包含多个 SKU。例如,对于同一款式的衣服(SPU),因为有不同的颜色和尺码,每个颜色和尺码的组合都会被视为一个独立的 SKU。
你知道雪花算法吗?
雪花算法是生成 分布式全局唯一id 的一个算法 ,他会得到一个64位长度的long类型的数据,其中这64位的数据,有四个部分组成:
- 第一部分:第一个bit位是一个符号位,因为id不会为负数,所以它一般是0 (最高位是符号位,始终为0,用于标识ID是正数)
- 第二部分:41个bit位来表示毫秒单位的时间戳 (精确到毫秒级)
- 第三部分:10个bit位来表示工作机器的id
- 第四部分:用12个bit位来表示递增的序列号 (用于标识同一毫秒内生成的不同ID,支持每个节点每毫秒产生4096个唯一的ID)
你了解的设计模式有哪些?
- 原型模式(protoType):原型模式用到的是深拷贝
- 单例模式(Singleton):
- 饿汉式:在类加载时就完成初始化,线程安全。
- 懒汉式:在第一次使用时才初始化,需要额外的同步机制来保证线程安全。
- DCL:双重检查锁 ,当第一次并发进来多个线程,第一重锁判断对象是否为空,第二重锁就是为了只创建一次对象,后续再使用的时候第一重锁就拦截下来了,无需和锁打交道了。
- Synchronize
- Volatile(1.保证多线程可见性 2.不能保证原子性)
- 懒汉式的好处:只有在需要时才创建实例,避免资源浪费,使用DCL双重检查锁,保证线程安全。
- 适配器模式:适配器模式是一种结构型设计模式,它允许不兼容的接口一起工作。
HttpRequestHandlerAdapter
实现了HandlerAdapter
接口,并适配了HttpRequestHandler
接口。SimpleControllerHandlerAdapter
它实现了HandlerAdapter
接口,并适配了Controller
接口。
- 观察者模式:它定义了对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。
- 策略模式:它允许你在运行时选择算法族中的一个算法。策略模式将算法封装成独立的对象,使得它们可以互换。这样,客户端可以在运行时根据需要选择不同的算法。
- 责任链模式:它允许你将请求沿着处理者链进行传递,直到有一个处理者能够处理该请求为止。这种模式使得多个对象都有机会处理请求,从而减少了请求的发送者和接收者之间的耦合。
- 构建者模式:构建者模式将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。
享元设计模式
享元设计模式(Flyweight Design Pattern)是一种用于性能优化的设计模式,
享元设计模式的核心思想是将对象的内在状态(Intrinsic State)和外在状态(Extrinsic State)分开:
- 内在状态(Intrinsic State):对象的共享部分,存储在享元对象内部,不随环境变化。
- 外在状态(Extrinsic State):对象的非共享部分,由客户端代码管理,随环境变化。
享元设计模式使用场景:
- 字符串池(String Pool)
- Integer数据缓存区 -128 127
- 数据库连接池
Integer缓存机制
Integer
类在 Java 中提供了一个内部缓存机制(享元模式),用于提高常用整数值的性能。
缓存机制减少了对象的创建和垃圾回收的开销,提高了性能,同时节省了内存。
-
缓存范围:Integer 类内部维护了一个缓存数组,用于存储从 -128 到 127 的常用整数对象。
这个范围可以通过设置系统属性
java.lang.Integer.IntegerCache.high
来调整。 -
缓存使用:当使用
Integer.valueOf(int i)
方法创建 Integer 对象时,如果 i 在缓存范围内,会直接返回缓存中的对象,而不是创建新的对象。
BeanFactory和FactoryBean的区别
BeanFactory
:是一个基本的IOC容器接口,负责管理和创建Bean,支持延迟初始化和依赖注入FactoryBean
:是一个特殊的接口,允许用户自定义Bean的创建逻辑,实现该接口的类可以返回不同类型的对象,并控制创建Bean的生命周期。
Java8的新特性有哪些?
- Lambda 表达式:Lambda允许把函数作为一个方法的参数
- Stream API :新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中
- 默认方法:默认方法就是一个在接口里面有了一个实现的方法。
- Optional 类 :Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。
- Date Time API :加强对日期与时间的处理。
Stream流常用方法?
foreach
:遍历集合中的元素filter
:过滤,把不符合条件的元素过滤掉map
:映射,把集合中的元素映射到新的集合中count
:统计集合中元素个数skip
:跳过集合中前n个元素limit
:保留集合中前n个元素distinct
:去重sort
:排序,默认升序,可以结合Comparable
和Comparator
定义排序规则allMatch
:集合中的元素是否全部匹配anyMatch
:是否至少匹配一个元素noneMatch
:是否不匹配指定的条件min
:获取集合中的最小值max
:获取集合中的最大值reduce
:把集合中的元素聚合成一个元素,实现对集合求和、求乘积和求最值操作
什么是数据脱敏?
数据脱敏是一种 保护敏感数据 的技术,通过修改数据的显示或存储形式,使其在不影响业务功能的前提下,无法被未授权人员识别或利用。
数据脱敏的目的:保护个人隐私、遵守法律法规(如 GDPR、HIPAA 等)、防止数据泄露
如何实现数据脱敏
- 首先创建一个序列化器,其中使用 Hutool工具 的
StrUtil.hide
方法对敏感信息进行脱敏处理,例如隐藏手机号和邮箱地址的部分字符。 - 通过
@Desensitization
注解指定哪些字段需要进行脱敏。 - 当
Jackson
需要序列化一个带有@Desensitization
注解的字段时,它会使用序列化器对该字段进行序列化。 - 序列化器会根据注解中的规则和参数来决定如何脱敏该字段的内容。
- 脱敏后的结果将被写入 JSON 数据中,从而保护了敏感信息不被泄露。
java应用程序的服务器有哪些
-
Apache Tomcat
- 描述:轻量级、开源的 Servlet 容器,支持 Servlet 和 JSP 技术,适用于中小型 Web 应用。
- 特点:启动速度快,配置简单,社区支持广泛。
-
Jetty
- 描述:轻量级、嵌入式的 Web 服务器,支持 Servlet 规范,适用于微服务和测试环境。
- 特点:启动时间短,内存占用低,易于嵌入到其他 Java 应用中。
-
JBoss
- 描述:全功能的 Java EE 应用服务器,支持完整的 Java EE 规范,适用于企业级应用。
- 特点:功能丰富,扩展性强,支持集群和负载均衡
集合
常见的集合有哪些?
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自旋操作来尝试获取锁,如果成功则进入临界区域,否则升级为 重量级锁。
在
synchronized
锁的升级过程中,偏向锁和轻量级锁都处于用户态,它们通过自旋等机制避免了内核态切换,提高了性能。而当竞争激烈时,锁会升级为重量级锁,此时锁处于内核态,会导致线程的阻塞和唤醒,需要进行内核态切换
什么是用户态和内核态?
根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:
- 用户态(User Mode) : 用户态运行的进程可以直接读取用户程序的数据,拥有较低的权限。当应用程序需要执行某些需要特殊权限的操作,例如读写磁盘、网络通信等,就需要向操作系统发起系统调用请求,进入内核态。
- 内核态(Kernel Mode):内核态运行的进程几乎可以访问计算机的任何资源包括系统的内存空间、设备、驱动程序等,不受限制,拥有非常高的权限。当操作系统接收到进程的系统调用请求时,就会从用户态切换到内核态,执行相应的系统调用,并将结果返回给进程,最后再从内核态切换回用户态。
内核态相比用户态拥有更高的特权级别,因此能够执行更底层、更敏感的操作。不过,由于进入内核态需要付出较高的开销(需要进行一系列的上下文切换和权限检查),应该尽量减少进入内核态的次数,以提高系统的性能和稳定性
什么是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 :唤醒等待的 所有线程。
同步和异步的区别
- 同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
- 异步:调用在发出之后,不用等待返回结果,该调用直接返回
如何实现线程同步?
线程同步是为了确保多个线程在访问共享资源时不会发生冲突,保证数据的一致性和完整性。
synchronized
关键字ReentrantLock
锁volatile
关键字join
方法:调用 join 方法的线程会阻塞,直到目标线程结束。wait
和notify
方法:wait 方法使当前线程进入等待状态,释放锁;notify 方法唤醒一个等待的线程。- 原子类:原子类使用硬件级别的原子操作(如 CAS 操作)来实现线程安全。
- 辅助类:使用JUC包中提供的
CountDownLatch
和CyclicBarrier
实现线程同步
什么是线程死锁?死锁如何产生?
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
-
死锁产生条件:
-
互斥:一个资源一次只能被一个线程持有。
-
请求与保持:一个进程因请求资源而阻塞时,不释放获得的资源。
-
不剥夺:进程已获得的资源,在未使用之前,不能强行剥夺。
-
循环等待:进程之间循环等待着资源
-
当以上条件同时满足时,就可能会出现死锁的情况。
如何避免线程死锁?
要避免线程死锁,可以采取以下几种方法:
-
尽量避免使用多个锁,尽量使用一个锁或者使用更加高级的锁,例如读写锁或者
ReentrantLock
。 -
减少锁的粒度, 确保同步代码块的执行时间尽可能短,这样可以减少线程等待时间,从而避免死锁的产生。
-
使用尝试锁,通过
ReentrantLock.tryLock()
方法可以尝试获取锁,如果在规定时间内获取不到锁,则放弃锁。 -
避免嵌套锁,如果需要使用多个锁,确保它们的获取顺序是一致的
如何检测死锁?
使用jmap
、jstack
等命令查看 JVM 线程栈和堆内存的情况。
如果有死锁,jstack
的输出中通常会有 Found one Java-level deadlock:
的字样,后面会跟着死锁相关的线程信息。
另外,实际项目中还可以搭配使用top
、df
、free
等命令查看操作系统的基本情况,出现死锁可能会导致 CPU、内存等资源消耗过高。
采用 VisualVM
、JConsole
等工具进行排查
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
会
为什么要用线程池?
使用线程池主要有以下几个原因。
-
线程池可以减少线程创建和销毁的开销,提高系统的性能和响应速度。
-
线程池可以控制资源使用,防止因大量线程同时运行而导致系统资源耗尽,提高了系统的稳定性和可靠性。
-
线程池中的线程可以被复用,执行多个任务,提高了线程的利用率,减少了资源浪费。
-
线程池提供了任务队列,可以管理待执行的任务,支持任务的排队和优先级管理,方便了任务的管理和监控
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
线程池的状态有哪些?
- RUNNING(运行状态):这是线程池的初始状态。线程池接受新的任务提交,并且会处理队列中的等待任务。线程池中的线程可以执行任务
- SHUTDOWN(关闭状态):当调用线程池的
shutdown()
方法时,线程池会进入关闭状态,此时线程池不再接受新的任务,但会 执行 任务队列中的任务。 - STOP(停止状态):当调用线程池的
shutdownNow()
方法时,线程池会进入停止状态,此时线程池不再接受新的任务,并且会 中断 正在执行的任务。 - TIDYING(整理状态):当线程池中的所有任务都已终止,并且工作线程的数量为0时,线程池进入此状态,通常用来执行一些清理操作。
- TERMINATED(终止状态):这是线程池的最终状态。
怎么自定义线程池?
- JDK 方式:使用
ThreadPoolExecutor
类,提供了详细的配置选项,如核心线程数、最大线程数、空闲线程存活时间、工作队列、线程工厂和拒绝策略。 - Hutool 方式:使用
Hutool
库中的ExecutorBuilder
工具类,简化了线程池的创建过程,但功能相对较少
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 应用诊断工具
Arthas常用命令有哪些?
dashboard
:显示当前 JVM 的 实时数据面板,包括 CPU、内存、线程、GC 等信息。thread
:查看线程信息,包括线程堆栈、线程状态等trace
:跟踪方法的调用路径和耗时,帮助分析性能瓶颈heap
:用于查看 JVM 堆内存的使用情况,统计对象数量和大小,查找特定对象,帮助诊断内存泄漏问题
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被称为轻量级框架,因为它可以以非侵入性的方式运行。
- 松耦合:通过控制反转(IOC)和依赖注入(DI)实现松耦合。
- 封装公共逻辑:支持面向切面编程(AOP),并将其与业务逻辑分离,从而提高模块化程度和代码的重用性。
- 支持声明式事务:Spring框架简化了事务管理,允许开发者通过简单的配置来管理事务,而无需编写大量的事务管理代码。
- 易于集成:方便集成各种优秀框架,例如Hibernate、MyBatis等ORM框架。
- 强大的测试支持:Spring提供了丰富的单元测试和集成测试支持,包括模拟对象的支持,使得测试变得更加简单高效。
什么是Spring AOP?
AOP(Aspect-Oriented Programming),即面向切面编程,用人话说就是把公共的逻辑抽出来,让开发者可以更专注于业务逻辑开发,可以减少系统的重复代码和降低模块之间的耦合度。
切面就是那些与业务无关,但所有业务模块都会调用的公共逻辑。
AOP有哪些实现方式?
AOP有两种实现方式:静态代理和动态代理。
-
静态代理:
代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,也称编译时增强。AspectJ使用的是静态代理。
缺点:代理对象需要与目标对象实现一样的接口,并且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护。
-
动态代理:
代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。
JDK动态代理和CGLIB动态代理的区别?
Spring AOP中的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。
- JDK 动态代理 依赖于 反射机制来创建代理,适用于实现接口的情况。
- CGLib 动态代理 通过字节码生成技术创建子类来实现代理,适用于没有实现接口的类。
如何实现动态代理?
JDK动态代理实现步骤:
- 定义一个接口及其实现类。
- 定义一个
InvocationHandler
实现类,重写invoke
方法。 - 使用
Proxy.newProxyInstance
方法生成代理对象。
CGLIB动态代理实现步骤:
- 添加CGLIB依赖(例如使用Maven)。
- 使用
Enhancer
类来生成代理对象。 - 实现
MethodInterceptor
接口,重写intercept
方法。
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启动原理?
运行流程:
-
创建 ApplicationContext:
SpringApplication
会根据应用的类型(如 web 应用、非 web 应用)创建相应的ApplicationContext
。 -
加载配置:
SpringApplication
会加载应用的配置文件(如 application.properties 或 application.yml),并解析配置。 -
自动配置:@EnableAutoConfiguration 注解启用自动配置功能,Spring Boot 会根据类路径中的依赖自动配置相应的组件。
-
组件扫描:@ComponentScan 注解启用组件扫描功能,Spring Boot 会扫描指定包及其子包下的组件(如 @Component、@Service、@Repository、@Controller 等)。
-
初始化监听器和事件:
SpringApplication
会初始化各种监听器和事件处理器,以便在应用启动的不同阶段执行相应的操作。 -
启动嵌入式容器:如果是 web 应用,SpringApplication 会启动嵌入式容器(如 Tomcat、Jetty、Undertow)。
-
启动完成:应用启动完成后,会触发
ApplicationReadyEvent
事件,通知所有监听器应用已经启动完成
了解Spring Boot中的日志组件吗?
在Spring Boot中,日志组件的设计遵循了门面模式的概念。
在日志处理方面,Spring Boot使用SLF4J作为门面。
SLF4J是一个抽象层,它为Java平台上的多种日志框架提供了一个统一的接口。
使用时只需要调用api即可,不需要关注是哪个组件进行实现的
Spring Boot默认使用的 logback
日志组件。
自定义stater
- 创建一个新的 Maven 项目:这个项目将包含你的自定义 Starter 的代码。
- 添加所需的依赖项:在
pom.xml
中添加依赖项,包括 Spring Boot 和任何其他你需要的库。 - 编写自动配置类:在你的 Starter 中,创建一个自动配置类,这个类将包含配置的逻辑。
- 创建配置文件:在
META-INF
目录下创建spring.factories
文件,指定你的自动配置类。 - 发布你的 Starter:将你的自定义 Starter 发布到 Maven 仓库或本地使用。
为什么不用Task用XXL-JOB
- Task:默认线程池是
newScheduledThreadPool
- 描述: Spring提供的任务调度工具,简单易用。
- 特点: 适合简单的定时任务,功能相对有限。
- XXL-JOB:
- 描述: 分布式任务调度平台,功能强大。
- 特点: 支持分布式调度、可视化管理、故障转移等高级特性。
- 适用场景: 复杂的分布式任务调度场景。
什么是本地缓存?
本地缓存(Local Cache) 是一种将数据存储在应用程序本地内存中的技术,目的是减少对后端数据源(如数据库、远程服务)的访问次数,从而提高应用程序的性能和响应速度。本地缓存通常用于存储频繁访问且不经常变化的数据。
- 本地缓存的优势
- 提高性能:减少对后端数据源的访问次数,降低网络延迟,提高响应速度。
- 减轻后端压力:减少后端数据源的负载,提高系统的整体性能。
- 提高可用性:即使后端数据源不可用,缓存中的数据仍然可以提供服务。
- 本地缓存的局限性
- 数据一致性:缓存中的数据可能与后端数据源中的数据不一致,需要有效的缓存更新机制。
- 内存限制:本地缓存占用内存资源,需要合理管理缓存大小。
- 单点故障:本地缓存依赖于单个节点,如果节点故障,缓存中的数据会丢失
在哪用过本地缓存和分布式缓存?
-
本地缓存:适用于需要频繁访问且数据量较小的场景,如用户会话信息、配置信息等。
例如,在一个单体应用中,使用
ConcurrentHashMap
来缓存用户会话信息,以减少对数据库的访问频率,提高应用性能。 -
分布式缓存:适用于需要高并发访问且数据量较大的场景,如电商网站的商品信息、社交平台的用户信息等。
例如,在一个分布式系统中,使用
Redis
作为分布式缓存,存储商品信息和用户信息,以提高系统的可用性和伸缩性。
本地缓存的方案有哪些?
Spring Cache
:Spring 框架提供的缓存抽象层,通过注解简化缓存管理,支持多种缓存实现。Map
和HashMap
:使用 Java 标准库中的 Map 或 HashMap 实现简单的本地缓存,适用于小型应用。ConcurrentHashMap
:Java 标准库提供的线程安全的 Map 实现,适用于多线程环境下的简单缓存需求。Caffeine
:基于 Guava Cache 优化的高性能缓存库,提供更高的性能和更低的内存占用。Ehcache
:广泛使用的开源 Java 缓存框架,支持内存和磁盘存储,提供丰富的缓存管理和分布式缓存功能。
说一下 PO、VO、DAO、BO、DTO、POJO 的区别和使用场景?
-
PO(Persistent Object,持久化对象):PO 是与数据库表结构一一对应的对象,通常用于 ORM 框架中。
它包含数据库表的所有字段,用于数据库操作,如增删改查。例如,在 MyBatis 中,PO 用于映射 SQL 查询结果。
-
VO(View Object,视图对象):VO 是用于前端展示的数据对象,通常用于封装页面需要显示的数据。
它包含页面需要的所有信息,可以是多个 PO 或其他对象的组合。例如,在 MVC 架构中,控制器将多个 PO 组合成一个 VO,传递给视图层。
-
DAO(Data Access Object,数据访问对象):DAO 是一个设计模式,用于封装对数据库的访问逻辑,提供数据操作的方法。
它包含对数据库的 CRUD 操作方法,隔离了业务逻辑和数据访问逻辑。例如,
UserDAO
提供getUserById
、saveUser
等方法。 -
BO(Business Object,业务对象):BO 是用于封装业务逻辑的对象,包含业务规则和业务方法。
它通常会调用 DAO 进行数据操作,用于业务逻辑层,处理复杂的业务规则和逻辑。例如,
UserService
可能包含registerUser
、validateUser
等业务方法。 -
DTO(Data Transfer Object,数据传输对象):DTO 是用于在不同层或不同系统之间传输数据的对象,通常用于远程调用或服务间通信。
-
POJO(Plain Old Java Object,普通 Java 对象):POJO 是一个普通的 Java 对象,没有任何特殊限制或继承特定类的要求。
它简单、轻量,通常包含属性和对应的
getter
、setter
方法,可以作为其他对象的基础。例如,PO、VO、DTO 等都可以是 POJO。
MySql
聚集索引和非聚集索引的区别
- 聚集索引:包含主键和非主键数据以及索引
- 非聚集索引:只包含主键和索引,当没有实现索引覆盖时会进行回表,走聚集索引
数据库三范式
-
第一范式
1NF 原子性,列或者字段不能再分,要求属性具有原子性,不可再分解; -
第二范式
2NF 唯一标识。即每个表只描述一种实体,每个记录都有唯一标识,不存在部分依赖关系。 主要是解决行的冗余。- 每个表必须有一个主键
- 非主键字段要完全依赖于主键
-
第三范式
3NF 直接性,非主键字段不依赖于其它非主键字段, 主要是解决列的冗余.
MyISAM 存储引擎 与 InnoDB 引擎区别
-
事务支持:MyISAM 不支持事务处理,而 InnoDB 支持事务处理,可以通过使用事务来确保数据的完整性和一致性。
-
锁定机制(锁的粒度):MyISAM 表级锁在执行 SELECT 操作时会对 表 进行读锁定,而执行 INSERT、UPDATE 或 DELETE 操作时会对 表 进行写锁定,因此在写操作执行时,读操作会被阻塞。而 InnoDB 支持行级锁,不会对整个表进行锁定,可以减少锁定冲突和死锁的发生。
-
外键支持:MyISAM 不支持外键约束,而 InnoDB 支持外键约束,可以通过外键约 束来保证数据的引用完整性
-
并发性能:在并发性能方面,InnoDB 要优于 MyISAM。由于 InnoDB 支持行级锁定 和 事务处理,因此在高并发情况下,InnoDB 的并发性能更高。
因此,在设计数据库时,需要考虑具体情况选择适合的 存储引擎。
- 如果需要支持事务处理 和 外键约束,以及具有更好的并发性能,则应选择 InnoDB。
- 如果只是进行简单的读写操作,并且需要更快的查询速度,则可以选择 MyISAM。
in和exit的区别
EXISTS
: 找到第一条满足条件的记录即停止。IN
: 需要遍历整张表
union和union all区别
UNION
: 去除重复,保证唯一性。UNION ALL
: 不去重,包含所有行,速度快。
CHAR 和 VARCHAR 的区别是什么?
CHAR 和 VARCHAR 是最常用到的字符串类型,两者的主要区别在于:
CHAR
是定长字符串,VARCHAR
是变长字符串。CHAR
在存储时会在右边填充空格以达到指定的长度,检索时会去掉空格;VARCHAR
在存储时需要使用 1 或 2 个额外字节记录字符串的长度,检索时不需要处理。
MySql常见的日志都有什么用?
-
二进制日志(binlog) :主要记录的是更改数据库数据的 SQL 语句。
-
慢查询日志(slow query log) :执行时间超过 long_query_time秒钟的查询,解决 SQL 慢查询问题的时候会用到。
-
事务日志(redo log 和 undo log) :redo log 是重做日志,undo log 是回滚日志。
Binlog日志三种格式
MySQL 提供了三种 Binlog 格式,每种格式记录的内容略有不同:
STATEMENT
:语句模式,记录 SQL 语句的文本。- 优点:占用空间小。
- 缺点:某些情况下可能无法正确恢复数据,因为 SQL 语句的执行结果可能因环境不同而不同。
ROW
:行模式,记录每一行数据的变化。- 优点:能够精确记录每一行数据的变化,适用于主从复制和数据恢复。
- 缺点:占用空间较大。
MIXED
:混合模式,结合了STATEMENT
和ROW
的特点,根据情况自动选择记录方式。- 优点:兼顾了 STATEMENT 和 ROW 的优点。
- 缺点:配置和管理相对复杂
explain的type有哪些值?
所有值的顺序从最优到最差排序为:
const
>eq_ref
>ref
>range
>index
>ALL
常见的几种类型具体含义如下:
const
:表中最多只有一行匹配的记录,一次查询就可以找到,常用于使用主键或唯一索引的所有字段作为查询条件。eq_ref
:当连表查询时,前一张表的行在当前这张表中只有一行与之对应。是除了 system 与 const 之外最好的 join 方式,常用于使用主键或唯一索引的所有字段作为连表条件。ref
:使用普通索引作为查询条件,查询结果可能找到多个符合条件的行。range
:对索引列进行范围查询,执行计划中的 key 列表示哪个索引被使用了。index
:查询遍历了整棵索引树,与 ALL 类似,只不过扫描的是索引,而索引一般在内存中,速度更快。ALL
:全表扫描
count(*)、count(1)、count(字段) 的区别
*
统计行数不关心null
数据count(字段)
过滤null
值,比count(*)
差count(*)
做了优化,它会找最合适的索引遍历,效率高于count(1)
索引有什么用?
索引存储在 内存 中,为服务器存储引擎为了快速找到记录的一种数据结构。
索引的主要作用是 加快数据查找速度,提高数据库的性能。 空间换时间
索引的优缺点
优点:加快查询效率
-
唯一性索引:保证数据库表中每一行数据的唯一性
-
加速查询:减少数据扫描的行数,从而提高查询速度。
-
加速排序和分组:索引可以帮助优化 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
会把经常使用的索引放到内存中,加快检索速度
什么是索引下推?
通过使用非主键索引进行查询的时候,在满足一定的条件下,存储引擎层会在回表查询之前对数据进行过滤,可以减少存储引擎回表查询的次数
数据库锁有哪些?
-
行锁和表锁
主要是针对锁粒度划分的,一般分为:行锁、表锁、库锁-
行锁:访问数据库的时候,锁定整个行数据, 防止并发错误。
-
表锁:访问数据库的时候,锁定整个表数据,防止并发错误。
行锁 和 表锁 的区别:
- 行锁:
开销大,加锁慢,会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高 - 表锁:
开销小,加锁快,不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
-
-
悲观锁和乐观锁
- 悲观锁:每次去拿数据的时候都认为会进行修改,所有每次在拿数据的时候都会上锁.
- 乐观锁:每次去拿数据的时候都认为不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间有没有更新这个数据,使用版本号机制,如果版本号不支持舍弃这次操作,并且设置 重试机制,乐观锁适用于多读的应用类型 ,这样可以提高吞吐量 。
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
-
字段长度固定的表查询会更快
-
把数据库的大表按时间或一些标志分成小表
-
相关文章:数据库事务相关面试题
Sharding-JDBC分库分表
Sharding-JDBC
最早是当当网内部使用的一款分库分表框架,后来交给Apache管理了。
Sharding-jdbc是ShardingSphere的其中一个模块,定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
-
什么是分库分表?
阿里巴巴的《Java 开发手册》中数据库部分的建表规约:单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
-
分库分表的实现方式
- 垂直分库:按照业务将表进行分类,分布到不同的数据库上面。比如说电商系统的数据库,可以根据业务分为会员中心数据库,商品数据库,订单数据库,库存数据库等。每个数据库负责一部分业务模块,从而实现模块化管理和负载均衡。
- 垂直分表:按照字段访问频次,将一张表的字段拆分到多张表当中。比如说会员表,内部包含了会员的id,name,密码,积分,生日,会员等级等,我们可以把积分,生日,会员等级等查询频次比较高的字段新建一个会员信息表存放这些字段。一对一的关系。
- 水平分库:将表水平切分后 按某个字段的某种规则分到不同的数据库,使得每个库具有相同的表,表中的数据不相同,水平分库一般是伴随水平分表。
- 水平分表:整张表在数据结构不变的情况下,将大表按一定规则拆分成多个小表
分库分表的优缺点
- 优点:提高性能、提升可扩展性、提高可用性、优化资源利用。
- 缺点:复杂性增加、数据一致性问题、查询复杂度增加、运维成本增加、初始投入大。
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数据持久化机制
持久化就是把内存的数据写到磁盘中,防止服务宕机导致内存数据丢失。
RDB方式
RDB
是 Redis 默认的持久化方案。RDB
持久化时会将内存中的数据写入到磁盘中,也就是快照(Snapshot),数据恢复是将快照文件直接读到内存中。
RDB的优缺点:
- 缺点 :
- 快照时间有间隔,不能实时备份,丢失数据可能会比较多
- 开启子进程备份数据,在数据集比较庞大时,fork()可能会非常耗时,造成服务器在一定时间内停止处理客户端。
- 优点 :
- 恢复数据比较快
- 备份的文件就是原始内存数据的大小,不会额外增加数据占用。
AOF方式
AOF(append only file)持久化:以独立日志的方式记录每次写命令,Redis重启时会重新执行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?canal优缺点是什么?
-
Canal 的工作原理
Canal
通过监听 MySQL 的Binlog
(二进制日志)来捕获数据变更事件,并将这些事件转换为 JSON 格式的消息,然后发送到目标系统。 -
Canal 的优点
- 轻量级
- 高可用性
- 易于集成
- 支持多种数据源
-
Canal的缺点
- 只支持mysql
- 配置复杂
- 增加维护成本
- 有一定延迟
什么是分布式锁,Redisson有什么用?
分布式锁 确保在同一时间只有一个节点能获得对共享资源的独占访问权限,从而解决并发访问问题。
可通过redis
的setnx
命令实现分布式锁
Redisson,又称看门狗,通过对分布式锁进行独特的续命机制,既避免了锁的时间过长造成的效率低下,又避免了锁的时间不够,业务未执行完时资源被其他线程抢占的问题。
原理:当锁的剩余时间到某个阈值(剩余2/3)时,业务仍未执行完,就会对锁进行续命,延长锁的时间,直至业务执行完成。
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的配置文件和xml映射文件,生成配置对象
- 构建会话工厂,并传入配置对象
- 由会话工厂创建sqlsession,并且传入配置对象中的连接信息、sql语句、参数类型和结果集类型
- 通过executor执行器,执行sql语句,返回指定类型的结果集
详细流程可参考:MyBatis执行流程
Mybatis和JDBC的区别
- 使用方式:
- JDBC:直接操作数据库,需要 手动管理 连接、预编译、执行和结果集。
- MyBatis:无需再去手动处理连接,结果集,Mybatis框架会自动进行这些操作 ,通过 XML 配置文件或注解映射 SQL 语句和 Java 对象,简化开发。
- SQL 管理:
- JDBC:SQL 语句直接写在 Java 代码中,不易维护。
- MyBatis:SQL 语句写在 XML 配置文件中,便于管理和维护。
- 结果集处理:
- JDBC:需要手动遍历 ResultSet 对象,将数据转换为 Java 对象。
- MyBatis:自动将结果集映射到 Java 对象,简化结果集处理。
- 性能:
- JDBC:完全控制数据库连接和 SQL 执行,适合对性能有极高要求的应用。
- MyBatis:内置连接池管理和预编译优化,提高性能。
- 学习曲线:
- JDBC:学习曲线较平缓,适合初学者。
- 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注入有哪些方式?
- 使用
PreparedStatement
类:通过使用参数化查询,可以确保用户输入的数据不会被解析为 SQL 语句的一部分,从而避免 SQL 注入攻击。 - 使用 ORM 框架:如 MyBatis,在编写SQL语句时可使用
#{}
指定参数,使这些参数不会重新编译,只作为普通字符串,从而避免 SQL 注入攻击。
#{}和${}的区别是什么?
#{ }
被解析成预编译语句,预编译之后可以直接执行,不需要重新编译${ }
仅仅为一个字符串替换,每次执行sql之前需要进行编译,存在 sql 注入问题。
Mysql存储Json数据
-
添加依赖
在pom.xml
中添加 MyBatis 和Jackson
的依赖。 -
创建实体类
-
创建自定义
JsonTypeHandler
继承
BaseTypeHandler
类,并实现setNonNullParameter
和getNullableResult
方法。这些方法分别用于将 Java 对象转换为 JSON 字符串并存储到数据库中,以及从数据库中读取 JSON 字符串并转换为 Java 对象。
-
注册自定义
TypeHandler
在
mybatis-config.xml
中注册自定义的TypeHandler
,指定javaType
为java.util.Map
,jdbcType
为VARCHAR
,并提供自定义TypeHandler
的全限定类名。 -
创建 Mapper 接口和 XML 文件
-
使用 Mapper 接口进行数据的插入和查询操作
MyBatis 会自动使用自定义的 TypeHandler 来处理 JSON 字段的转换。
Mybatis百万数据插入
在使用 MyBatis 进行大量数据插入时,传统的单条插入方式效率较低,容易导致性能问题。为了高效地插入百万级别的数据,可以采用以下几种方法:
- 多线程插入
- 原理:使用
CountDownLatch
和 多线程技术,将数据分成多个批次,每个线程处理一个批次,充分利用多核 CPU 的并行处理能力。
- 原理:使用
- 分批插入
- 原理:通过
ExecutorType.BATCH
将SqlSesion设置成批处理模式,将数据分成多个小批次,逐批插入,避免一次性插入大量数据导致内存溢出或数据库连接超时。
- 原理:通过
- 批量插入
- 原理:使用 MyBatis 的
foreach
标签一次性插入多条数据,减少数据库连接和事务的开销。
- 原理:使用 MyBatis 的
执行速度:多线程插入 > 分批插入 > 批量插入
RabbitMQ
为什么用MQ? 异步 、削峰、解耦
MQ(Message Queue,消息队列)是一种在分布式系统中进行消息传递的技术,它主要用于实现服务间的 解耦、异步处理、削峰填谷等功能。下面我将分别解释这几个概念,并说明它们是如何通过MQ实现的。
- 异步处理
异步处理是指一个操作不需要等待另一个操作完成就可以继续执行的过程。
- 在传统的同步模式下,客户端发送请求后必须等待服务器响应才能继续执行后续操作;
- 而在异步模式下,客户端发送请求后无需等待,可以立即返回并执行其他任务,服务器处理完请求后再通知客户端结果。
MQ如何实现异步处理:
- 当客户端向服务端发送请求时,该请求不是直接被服务端处理,而是先存入MQ中。
- 服务端从MQ中获取请求消息后进行处理,客户端无需等待服务端处理完成即可继续执行其他任务。
- 这种方式提高了系统的响应速度和吞吐量,使得系统能够处理更多的并发请求。
- 解耦
解耦是指降低系统各组件之间的依赖性,使得每个组件都可以独立地开发、部署和扩展。这有助于提高系统的灵活性和可维护性。
MQ如何实现解耦:
- 发送方将消息发送到MQ中,而接收方从MQ中读取消息。这样发送方和接收方之间就不存在直接的调用关系。
- 即使接收方暂时不可用,发送方也可以继续发送消息,因为消息会被暂存在MQ中,直到接收方恢复可用。
- 发送方和接收方可以独立扩展,而不影响彼此的工作。
- 削峰填谷
削峰填谷是指通过缓存、异步处理等手段来平衡系统的负载,避免高峰期系统过载或低谷期资源浪费的情况。
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
分布式与微服务区别?
- 概念层面:
- 微服务是设计层面的东西,一般考虑如何将系统从逻辑上进行拆分,也就是垂直拆分;
- 分布式是部署层面的东西,即强调物理层面的组成,即系统的各子系统部署在不同计算机上。
- 粒度划分:
- 微服务倾向于更细粒度的服务划分,每个服务只专注于一个特定的业务功能,并力求做到“做一件事并做好”
- 分布式系统中的服务划分粒度可大可小,可以包含多个紧密相关的业务功能
- 目标:
- 微服务架构的核心理念是围绕业务能力组织服务,强调服务之间的松耦合和高内聚
- 分布式系统的设计目标是为了提高系统的可靠性、可用性、可扩展性和稳定性
一句话概括:分布式:分散部署;微服务:分散能力。
单体应用和微服务的区别?
- 架构结构
- 单体应用:所有的业务逻辑、数据访问和用户界面都集成在一个应用中,功能模块耦合在一起。
- 微服务:将一个大型的应用拆分成多个小型的、独立的服务,实现了解耦,每个服务专注于一个特定的业务领域
- 开发和测试
- 单体应用:开发者需要了解整个应用的代码库,开发周期较长,代码变更可能影响多个模块。且测试难度高。
- 微服务:开发者可以专注于特定的服务,开发周期较短,代码变更影响范围较小,测试单个服务相对简单。
- 部署和扩展
- 单体应用:整个应用作为一个整体进行部署,部署过程较为复杂,难以扩展。
- 微服务:每个服务可以独立部署,部署过程较为简单,可以针对特定服务进行细粒度的扩展,提高资源利用率和性能。
- 故障隔离
- 单体应用:一个模块的故障可能会影响整个应用的稳定性,故障隔离能力较差。
- 微服务:每个服务独立运行,一个服务的故障不会直接影响其他服务,故障隔离能力较强。
- 技术栈
- 单体应用:通常使用统一的技术栈,难以引入新的技术和框架。
- 微服务:每个服务可以使用最适合的技术栈,灵活性较高。
Spring Boot与Spring Cloud有什么区别?
- Spring Boot:简化开发,提供自动配置、起步依赖、嵌入式服务器、生产准备等功能,适用于快速开发单个独立的应用程序。
- Spring Cloud:提供微服务治理功能,如服务发现、配置管理、断路器、API 网关、消息总线、分布式链路追踪等,适用于构建复杂的微服务架构。
什么是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 的心跳机制是确保服务提供者和服务消费者的健康状态的重要手段。通过心跳机制,Nacos 可以实时监测服务实例的存活状态,并及时更新服务列表,确保服务发现的准确性和可靠性。
-
心跳频率:服务实例 默认每隔5秒钟向Nacos发送一次心跳包。心跳包中包含服务实例的信息,如名称、IP地址、端口号等。
-
健康检查:Nacos接收到心跳包后,会检查服务实例是否仍在注册列表中。如果服务实例在列表中,那么Nacos认为它是健康的。
-
超时机制:
如果服务实例连续三次(默认情况下大约
15秒
)未能发送心跳包,Nacos会将该服务实例标记为不健康。
如果服务实例连续六次(默认情况下大约30秒
)未能发送心跳包,Nacos将移除该服务实例。
Nacos配置中心动态刷新原理
Nacos 使用长轮询机制来实现实时配置更新。具体过程如下:
- 客户端发起请求:客户端通过后台线程发起一个 HTTP 请求 到 Nacos 服务端,用于 监听配置变化。
- 服务端挂起连接:Nacos 服务端接收到请求后,会挂起(hold)这个 HTTP 连接一段时间(例如 30 秒),在此期间服务端监控配置文件的变化。
- 两种情况
- 无变化情况:若在这段时间内没有检测到配置文件有任何变更,服务端将释放连接并向客户端返回一个指示,表明配置没有更新。
- 配置变更情况:如果在挂起期间检测到配置文件发生变化,服务端会立即释放连接并将最新的配置推送给客户端。
- 循环轮询:无论哪种情况,客户端在接收完响应后,会在短暂延迟(如 10 毫秒)之后重新发起一个新的 HTTP 请求,从而形成循环轮询机制以持续监听配置更新。
通过 Spring Cloud 原生注解@RefreshScope 实现配置自动更新
Nacos和ZooKeeper的区别?
-
Nacos
提供服务管理界面,方便管理和监控服务 -
Nacos
阿里巴巴开源,搭配阿里巴巴其他组件发挥更好效果。 -
配置管理
- ZooKeeper:需要自行实现配置的发布、订阅和更新逻辑,使用复杂。
- Nacos:提供开箱即用的配置管理功能,使用简单,支持 自动 更新和推送。
-
服务发现
- ZooKeeper:需要 自行实现 服务注册、服务发现和健康检查逻辑,使用复杂。
- Nacos:提供开箱即用的服务注册、服务发现和健康检查功能,使用简单,内置健康检查机制
目前主流的负载方案有哪些?
目前主流的负载方案分为以下两种:
- 服务端负载均衡
在消费者和服务提供方中间使用独立的代理方式进行负载,有硬件的(比如 F5),也有软件的(比如 Nginx,OpenResty) - 客户端负载均衡器
客户端根据自己的请求情况做负载均衡,Ribbon 就属于客户端自己做负载均衡。
Nginx作为服务端负载均衡器,常见的负载均衡策略有哪些?
- Round Robin Rule :轮询策略,依次选择每个服务实例。
- Least Connections:最少连接数,最少连接数算法将请求分配给当前活动连接数最少的后端服务器。这有助于避免某些服务器过载,而其他服务器则处于空闲状态的情况。
- IP Hash:IP哈希,IP哈希算法根据客户端IP地址的哈希值将请求分配给后端服务器。这样,同一个客户端的请求总是被分配到同一台后端服务器,从而实现会话粘性。
- URL Hash:URL哈希,URL哈希算法根据请求URL的哈希值将请求分配给后端服务器。这样,相同URL的请求总是被分配到同一台后端服务器。
- Weighted Round Robin:加权轮询,加权轮询算法在轮询的基础上,为每台后端服务器分配一个权重。数字越大,权重越高,分配到的请求越多。适用于后端服务器性能差异较大的情况。
Nginx配置文件有哪些内容?
-
server
块:listen
:指定监听的端口和 IP 地址。server_name
:指定服务器的域名或 IP 地址。root
:指定网站根目录。location
:定义 URL 匹配规则和处理方式,设置跨域规则access_log
:指定访问日志文件的位置
-
upstream
块:定义后端服务器组,用于负载均衡。 -
全局块:
user
:指定运行 Nginx 的用户和用户组。worker_processes
:指定 Nginx 的工作进程数,通常设置为 CPU 核心数。error_log
:指定错误日志文件的位置和级别
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开发的一个分支
QPS和TPS的区别?
-
衡量的对象不同:
QPS
关注的是请求的数量,不区分请求的具体内容。TPS
关注的是完整业务操作的数量,通常涉及到数据的一致性处理。
-
应用场景不同:
QPS
通常用于衡量接口服务的处理能力。TPS
通常用于衡量后端服务或数据库的处理能力。
-
计算方法不同:
QPS
计算的是单位时间内处理的请求总数。TPS
计算的是单位时间内完成的事务总数。
流量控制规则 (FlowRule)
熔断降级规则 (DegradeRule)
Sentinel 限流规则怎么进行持久化?
-
为什么要持久化:可以通过
DashBoard
在可视化界面中设置限流规则,但是在 应用重启后,Sentinel 规则就消失了,生产环境需要将配置的规则进行持久化。 -
怎么进行持久化:将限流、熔断配置规则持久化进
Nacos
保存,只要刷新被监控的应用某个 REST 地址,Sentinel 控制台的流控规则就能看到,持久化后无需重新配置就能看到。只要 Nacos 里面的配置不删除,针对该应用的Sentinel上的流控规则持续有效
流量网关与服务网关的区别?
流量网关和服务网关在系统整体架构中所处的位置如上图所示,
-
流量网关:(如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全局链路跟踪
全局链路跟踪的流程
-
前端生成 Trace ID:
- 前端使用 Axios 的请求拦截器生成
traceId
。 - 将带有
traceId
的请求发送给流量网关(Nginx)。
- 前端使用 Axios 的请求拦截器生成
-
服务网关处理 Trace ID:
- Nginx 再将请求转发给服务网关(Gateway)。
- Gateway 检查请求中的
traceId
是否为空。 - 如果
traceId
为空,生成一个新的traceId
。 - 并将
traceId
放到请求头中,继续向下执行请求。
-
服务器端处理 Trace ID:
- 自定义一个 traceIdFilter 拦截器,检查请求中是否有
traceId
。 - 如果没有
traceId
,生成一个新的traceId
。 - 将
traceId
放入 MDC。 - 通过 ELK 堆栈进行日志采集,使用 Kibana 进行可视化展示。
- 自定义一个 traceIdFilter 拦截器,检查请求中是否有
什么是MDC?
MDC(Mapped Diagnostic Context) 是一个在日志框架中使用的上下文信息存储机制,主要用于在多线程环境中存储和传递与当前线程相关的诊断信息。
MDC 通常与日志框架(如 Log4j、Logback、SLF4J 等)一起使用,以便在日志记录中包含更多的上下文信息,从而更容易地进行日志分析和问题排查
分布式系统中,MDC 可以用于传递跟踪信息(如 traceId),帮助进行全链路跟踪和问题定位。
为什么要用服务网关
- 简化客户端的工作
- 降低函数间的耦合度
- 解放开发人员把精力专注于业务逻辑的实现
Sentinel 和 Gateway 结合时,配置流量控制的模式有哪些?
- 路由模式:根据服务名为每个路由单独配置流量控制和熔断规则,适用于不同的服务或路径,限流力度比较大。
- 分组模式:根据服务名和分组来配置流量控制规则,适用于需要对不同服务或路径进行分组管理的场景。
- 全局模式:在网关层面统一配置流量控制和熔断规则,适用于所有路由。
- 动态规则模式:通过 Sentinel 控制台动态配置和管理流量控制规则,适用于需要实时调整流量控制策略的场景。
不同服务之间如何进行通信
可以使用OpenFeign来实现不同服务之间的通信。
OpenFeign是一个声明式的Web客户端,它可以简化编写Web服务客户端的代码。它是Spring Cloud的一部分,主要用于简化与RESTful服务的集成。
在微服务中如何监控服务
可使用Skywalking实现服务监控,Skywalking是一个分布式系统的应用程序性能监控工具 (Application Performance Managment),提供了完善的链路追踪能力。
- Skywalking主要可以监控接口、服务、物理实例的一些状态,特别是在压测的时候可以看到众多服务中哪些服务和接口比较慢
- Skywalking还可以设置告警规则,如果项目出现bug可对开发人员进行通知,以便在第一时间进行修复
- SkyWalking 提供了服务调用关系的拓扑图,帮助你直观地了解各个服务之间的依赖关系和调用路径。
慢接口怎么追踪的?
- 日志记录:使用日志框架(如
Log4j
、SLF4J
)在接口的关键位置记录详细的日志,包括请求参数、响应时间、SQL 执行时间等。 - 分布式链路追踪:使用分布式链路追踪工具(如
SkyWalking
)来追踪请求的完整调用链路,监控应用的调用链路和性能指标。
怎么进行接口调优?
-
慢 SQL 调优:优化数据库查询,减少查询时间和资源消耗。
例如,为经常查询的字段添加索引,优化 SQL 语句,使用分页查询,使用缓存等。
-
代码优化:优化业务逻辑,减少不必要的计算和资源消耗。
例如,合并多次查询,使用懒加载,异步处理任务,使用缓存等。
-
使用中间件:使用中间件(如缓存、消息队列、负载均衡)来提高系统的性能和可扩展性。
例如,使用
Redis
缓存数据,使用RabbitMQ
异步处理任务,使用 Nginx 进行负载均衡。 -
发多个副本:通过水平扩展,增加应用的副本数量,提高系统的并发处理能力和可用性。
例如,使用
Docker
容器化应用,使用Kubernetes
进行容器编排,自动管理应用的副本数量和负载均衡
Openfeign如何使用
- 引入依赖
- 编写FeignClient接口
- 在启动类中开启对OpenFeign注解的支持
- 注入FeignClient
- 发起调用,像调用本地方法一样调用远程服务
Openfeign自定义拦截器
可以通过在OpenFeign中自定义拦截器的方式,来实现服务远程调用过程中的日志输出、认证授权等应用。
自定义拦截器步骤:
- 继承
RequestInterceptor
类 - 实现apply方法,编写相应逻辑
- 将拦截器类放入Ioc容器中
Dubbo是什么?
Dubbo 是一个高性能的 Java RPC 框架,主要用于构建分布式服务架构,提供服务发现、负载均衡、服务治理等功能
RPC 是什么?
RPC(Remote Procedure Call) 即远程过程调用,是一种通信协议。
通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。并且!我们不需要了解底层网络编程的具体细节。
Dubbo和Spring Cloud的区别?
-
Spring Cloud
基于微服务架构,Dubbo
基于 RPC 框架 -
Spring Cloud
主要使用HTTP
协议,Dubbo
使用RPC
协议,RPC使用的是TCP协议,性能高 -
Spring Cloud
常用Nacos
作为服务发现与注册中心,Dubbo
默认使用Zookeeper
作为服务发现与注册中心 -
Dubbo
主要支持 Java,跨语言支持相对较弱 -
Spring Cloud
基于Spring Boot
,配置简便,使用注解和配置文件即可快速搭建微服务架构,而Dubbo
配置相对复杂
常见分布式事务解决方案
-
XA两段提交(低效率)
-
TCC三段提交
-
Seata(alibaba) (推荐)
XA协议中2PC和3PC的区别
XA协议 包括两阶段提交(2PC)和三阶段提交(3PC)两种实现
-
2PC(二阶段提交协议)
- 第一阶段:预提交阶段
由事务协调者询问通知各个事务参与者,是否准备好了执行事务 - 第二阶段:提交阶段
协调者收到各个参与者的准备消息后,根据反馈情况通知各个参与者commit
提交或者rollback
回滚
- 第一阶段:预提交阶段
-
3PC(三阶段提交协议)
- 第一阶段:准备阶段
协调者向参与者发送canCommit
请求,判断是否具备执行条件,参与者如果可以提交就返回Yes
响应,否则返回No
响应 - 第二阶段:预提交阶段
协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit
操作 - 第三阶段:提交阶段
协调接收到所有参与者发送的ACK响应,根据反馈情况通知各个参与者commit
提交或者rollback
回滚
- 第一阶段:准备阶段
-
区别:与2PC相比,3PC降低了阻塞时长,加入了超时机制,并且在等待超时后,协调者或参与者会中断事务,避免了协调者单点问题,阶段三中协调者出现问题时,参与者会继续提交事务,但是数据不一致问题仍然存在。
什么是TCC?
TCC 模式是一种分布式事务解决方案,全称为 Try-Confirm-Cancel
。它通过业务代码实现 Try
、Confirm
和 Cancel
三个操作,确保分布式事务的一致性
-
Try
操作尝试执行业务操作,检查资源是否可用,但不实际消耗资源。例如,在订单服务中,Try 操作可以检查库存是否足够,但不实际扣减库存。
-
Confirm
操作确认执行业务操作,实际 消耗资源。例如,在订单服务中,Confirm 操作实际扣减库存。
-
Cancel
操作取消执行业务操作,补偿机制,释放资源。例如,在订单服务中,Cancel 操作恢复库存
Seata 三大角色
-
事务协调器(TC,Transaction Coordinator)
TC 是全局事务的管理者,负责协调所有的分支事务,确保全局事务的一致性。
-
事务管理器(TM,Transaction Manager)
TM 是事务的发起者和管理者,负责全局事务的开始、提交和回滚。
-
资源管理器(RM,Resource Manager)
RM 负责管理具体的数据库资源,执行分支事务,并与 TC 交互。
Seata 的两阶段提交执行流程
第一阶段:准备阶段
-
全局事务开始:
- TM 向 TC 注册全局事务,获取全局事务 ID(XID)并插入
global_table
表中。
- TM 向 TC 注册全局事务,获取全局事务 ID(XID)并插入
-
分支事务注册:
- RM 向 TC 注册分支事务,将 XID 和分支事务 ID(Branch ID)关联起来。
- TC 插入
branch_table
表,记录分支事务的信息,包括分支事务 ID、全局事务 ID 和分支事务的状态。
-
分支事务执行:
- RM 执行具体的数据库操作,并将操作结果暂存。
- RM 会将分支事务的状态(例如,成功或失败)上报给 TC。
- TC 记录每个分支事务的准备状态并更新
branch_table
表。
第二阶段:提交或回滚阶段
-
全局事务提交或回滚:
- TM 会根据执行情况决定全局事务的提交或回滚。
- 如果决定提交,TM 会向 TC 发送全局提交请求。
- 如果决定回滚,TM 会向 TC 发送全局回滚请求。
-
分支事务提交或回滚:
- TC 接收到全局提交或回滚请求后,会根据全局事务 ID 查找所有相关的分支事务。
- TC 依次向每个 RM 发送分支提交或回滚请求。
- RM 执行分支事务的提交或回滚操作,并将结果上报给 TC。
- TC 更新
branch_table
表中相应分支事务的状态。
-
全局事务结束:
- TC 收到所有分支事务的提交或回滚 结果 后,更新
global_table
表中全局事务的状态。 - 如果所有分支事务都成功提交,全局事务标记为成功。
- 如果有任何一个分支事务回滚,全局事务标记为失败。
- TC 收到所有分支事务的提交或回滚 结果 后,更新
Seata涉及到了哪几张表?
global_table
:保存全局事务的信息,包括全局事务 ID、状态、开始时间等。branch_table
:保存分支事务的信息,包括分支事务 ID、全局事务 ID、资源 ID、状态等。lock_table
:记录被锁定的行的具体信息,用于防止并发冲突。undo_log
:保存事务的 UNDO 日志,用于在事务回滚时恢复数据。
项目的几种发布方式和特点
- jar包发布
jar包发布过于依赖于环境 - docker发布
docker发布虽然解决了环境依赖问题,但是不好管理 - k8s发布
k8s发布可以进行流量监控,针对流量规模对容器进行自动伸缩,可以热部署,可结合cicd自动发布,容器的自动化编排,还可以在yaml配置文件里设置调优参数
Linux常用命令有哪些?
Docker常用的命令有哪些?
命令 | 作用 |
---|---|
images | 列出所有镜像 |
search | 查找镜像 |
pull | 拉取镜像 |
rmi | 删除镜像 |
build | 构建镜像 |
create | 创建一个新容器但不启动它 |
run | 创建并运行容器 |
start | 启动容器 |
stop | 停止容器 |
restart | 重启容器 |
ps | 列出正在运行容器 |
exec | 以交互模式进入容 |
logs | 查看容器日志 |
port | 查看容器端口映射 |
top | 显示容器内进程 |
cp | 拷贝文件 |
rm | 删除容器 |
docker网络模型有哪些?
Bridge
模式:默认网络模式,容器通过docker0
虚拟网桥连接,每个容器有独立的 IP 地址,适用于需要网络隔离的场景。Host
模式:容器与宿主机共享网络命名空间,使用宿主机的网络接口,适用于需要高性能网络通信的场景。Container
模式:容器与已存在的容器 共享 网络命名空间,适用于多容器协同工作的场景。None
模式:容器没有网络接口,仅能访问本地环回接口,适用于不需要网络通信的场景。- 自定义网络:用户自定义的网络,包括桥接网络、覆盖网络、Macvlan 网络和 IPvlan 网络,适用于复杂网络拓扑和跨主机通信的场景。
k8s.yml文件有哪些内容?
- k8s的版本号
- 应用的部署模式
- 应用的名字
- 要运行应用实例的数量
- 环境变量
- 引入
skywalking
监控服务状态 - 配置日志文件
- cpu的限制
- 数据卷的挂载
dockerfile文件中有哪些命令?
命令 | 例子 | 作用 |
---|---|---|
FROM | FROM ubuntu:latest | 指定构建镜像的基础镜像 |
EXPOSE | EXPOSE 8080 | 指定容器提供服务的端口 |
WORKDIR | WORKDIR /app | 设置工作目录 |
ENV | ENV APP_HOME=/usr/src/app | 设置环境变量,之后的指令可以引用这些变量 |
ADD | ADD https://example.com/path/to/file /dest/ | 从构建上下文添加一个文件/URL,并自动处理解压 |
COPY | COPY ./src / | 从构建上下文添加一个文件/目录到镜像内 |
VOLUME | VOLUME /data | 创建一个挂载点 |
RUN | RUN apt-get update && apt-get install -y curl | 执行命令来生成新的镜像层 |
CMD | CMD ["echo", "hello world"] | 指定容器启动时默认执行的命令,可以通过 docker run 时的参数覆盖 |
ENTRYPOINT | ENTRYPOINT ["bash", "-c", "python app.py"] | 定义容器启动时要执行的命令,命令不会被覆盖 |
认证鉴权
Session和Cookie的关系
Session和Cookie都可以用来实现跟踪用户状态,而二者是关系的:Session的实现依赖于Cookie。
- Session的底层原理:
- 当client(浏览器)第1次发起请求并获取session后,服务端在服务器内部创建一个Session对象,并将该session的id以(
JSESSIONID=id
值)的cookie写回浏览器 - 当client(浏览器)二次请求时,会自动携带Cookie(也就是
JSESSIONID
),服务端根据cookie记录的id值获取相应的Session
- 当client(浏览器)第1次发起请求并获取session后,服务端在服务器内部创建一个Session对象,并将该session的id以(
Cookie与Session的区别
- 存储位置不同
cookie:是针对每个网站的信息,保存在客户端.
session:是针对每个用户的,Session中主要保存用户的登录信息,保存在服务器端. - 存储数据大小不同
cookie:一个 cookie存储的数据不超过4K。
session:session存储在服务器上可以任意存储数据, 无大小限制. - 生命周期不同
cookie:cookie可以主动设置生命周期。还可以通过浏览器工具清除.
session:session的生命周期是间隔的,从创建时开始计时如在30min内没有访问session,那么session生命周期就被销毁。 - 数据类型不同
cookie:value只能是字符串类型。
session:value是object类型。 - 安全性不同
cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗.
考虑到安全应当使用session。
如何实现Session共享
session共享的原因:维护用户状态、提升用户体验、负载均衡、资源共享。
-
一种解决方案是 session 数据持久化.,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败, 同时它增加了数据库的压力.
-
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
JWT和Token的区别
- 普通
Token
:任意字符串,结构不固定,安全性依赖实现,用途广泛。 JWT
:Token的一种形式,固定结构(Header.Payload.Signature
),通过签名保证安全性,适合无状态的分布式系统,对移动端和单点登录友好,避免CSRF攻击。
JWT 由哪些部分组成?
WT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 是一种规范化之后的 JSON 结构的 Token。
实际的 JWT 大概就像下面这样。
它是一个很长的字符串,中间用点(.)分隔成三个部分。
JWT 的三个部分依次如下。
- Header(头部) 明文
- Payload(负载) 明文
- Signature(签名)
如何防止 JWT 被篡改?
有了签名之后,即使 JWT 被泄露或者截获,黑客也没办法同时篡改 Signature、Header、Payload。
这是为什么呢?因为服务端拿到 JWT 之后,会解析出其中包含的 Header、Payload 以及 Signature 。服务端会根据 Header、Payload、密钥再次生成一个 Signature。拿新生成的 Signature 和 JWT 中的 Signature 作对比,如果一样就说明 Header 和 Payload 没有被修改。
不过,如果服务端的秘钥也被泄露的话,黑客就可以同时篡改 Signature、Header、Payload 了。黑客直接修改了 Header 和 Payload 之后,再重新生成一个 Signature 就可以了。
密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。
JWT的特点
- 无状态:不需要存储再服务器上了,但是缺点就是不可控
- 有效避免了 CSRF 攻击:利用 Cookie进行攻击,但是JWT肯本就不放在Cookie里面
- 适合移动端应用
- 单点登录友好
什么是CSRF攻击?
CSRF(Cross-Site Request Forgery,跨站请求伪造) 是一种常见的安全攻击方式,攻击者利用用户的已验证身份,诱使用户在不知情的情况下执行非预期的操作。
这种攻击通常发生在用户已经登录某个网站的情况下,攻击者通过诱导用户访问恶意网站或点击恶意链接,利用用户的身份向目标网站发送请求。
为什么要用双Token无感刷新,它解决了什么问题?
为了保证安全性,后端设置的Token不可能长期有效,过了一段时间Token就会失效。而发送网络请求的过程又是需要携带Token的,一旦Token失效,用户就要重新登陆,这样用户可能需要频繁登录,体验不好。
为了解决这个问题,采取双Token(Access_Token,Refresh_Token)无感刷新,用户完全体会不到Token的变化,但实际上,Token已经刷新了。
两个Token的作用:
-
鉴权Token(Access_Token):用于鉴定用户身份,即每次发送网络请求都需要携带这个Access_Token
-
刷新Token(Refresh_Token):用于刷新Access_Token,即调用A接口需要携带Refresh_Token, 用它换得最新的Access_Token
什么是单点登录(SSO)?
SSO
英文全称 Single Sign On
,单点登录。SSO 是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
单点登录实现原理:
- 在A系统点击登录,重定向到登录页单独的域名中进行登录
- 登录成功后,携带Token重定向到A系统,A系统将Token存到Cookie或LocalStorage中
- 进入B系统时,发现Cookie中有该公司的Token,就会直接使用该Token
- B系统在发送请求时就会携带该Token
rbac相关的表
RBAC(Role-Based Access Control,基于角色的访问控制)是一种常用的访问控制机制,用于管理系统中的用户权限。
RBAC通过将用户分配给角色,并将权限授予角色,从而控制用户对资源的访问。
基于RBAC实现的权限管理通常需要涉及以下几张表:
spring security原理
对于servlet三大组件,spring security位于Filter过滤链中,SpringSecurity 本质就是一个过滤器链
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链
Spring Security 作用机制:
- 注册标准Filter:首先,会自动注入一个DelegatingFilterProxy到 Servlet 的FilterChain中。
- 请求转发到 Spring Security:当请求到来时,DelegatingFilterProxy就会自动在 Spring容器 中搜索名称为springSecurityFilterChain的Filter实体,其实际类型为FilterChainProxy。DelegatingFilterProxy最终会将请求转发给到FilterChainProxy。
- 找到匹配请求处理的SecurityFilterChain:FilterChainProxy内部维护了一系列SecurityFilterChains,他会依据请求内容找到对应处理该请求的SecurityFilterChain。
- 请求处理:找到能处理请求的第一个SecurityFilterChain后,就会遍历该SecurityFilterChain内部维护的一系列Filters(默认15个),依次让这些 Security Filter 处理该请求,完成认证、授权等功能。
计算机基础
URI 和 URL 的区别是什么?
- URI(Uniform Resource Identifier) 是统一资源标志符,可以唯一标识一个资源。
- URL(Uniform Resource Locator) 是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 定位 这个资源。
URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。
URL 是一种具体的 URI,它不仅唯一标识资源,而且还提供了定位该资源的信息
OSI七层网络协议
TCP/UDP的区别
- 是否面向连接:
UDP
在传送数据之前不需要建立连接。TCP
提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。
- 是否是可靠传输:
UDP
只管发送,不保证对方是否收到该消息。TCP
提供可靠的传输服务,发送消息前先建立连接,保证消息按序到达。
- 是否有状态:
UDP
是无状态服务,发出去之后就不管了。TCP
传输是有状态的,会记录消息发送成功、消息已被接受等状态。
- 传输效率:因为没有额外的操作,所以 UDP 比 TCP 快得多
- 传输形式:
TCP
是面向字节流的,UDP
是面向报文的。 - 是否提供广播或多播服务:
TCP
只支持点对点通信,UDP
支持一对一、一对多、多对一、多对多
常见的网络协议
- IP - 网际协议
- HTTP - 超文本传输协议
- HTTPS - 安全的 HTTP
- SSH-安全壳协议
- FTP - 文件传输协议
- SMTP - 简单邮件传输协议
为什么要使用DNS?
首先,DNS 使得域名更容易记忆,提高了用户体验。
其次,DNS 提供了灵活性和可维护性,可以轻松更改服务器的 IP 地址而不需要用户知道。
此外,DNS 支持负载均衡和故障恢复,提高了系统的可用性和性能。DNS 还可以与 CDN 结合使用,优化全球范围内的内容分发。
Http是怎么工作的
- 建立TCP连接
- 客户端发送请求到web服务端
- 服务端处理请求
- 服务端响应请求
- 客户端处理响应,展示页面
- 断开TCP连接
HTTP 和 HTTPS 有什么区别?
HTTP 和 HTTPS 对比
-
端口号:HTTP 默认是
80
,HTTPS 默认是443
。 -
URL 前缀:HTTP 的 URL 前缀是
http://
,HTTPS 的 URL 前缀是https://
。 -
安全性和资源消耗:
- HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。
- HTTPS 所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。
所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
-
搜索引擎优化:因为 HTTPS 能够提供更高的安全性和用户隐私保护。使用 HTTPS 协议的网站在搜索结果中可能会被优先显示。
简述对称加密和非对称加密的主要区别?
- 对称加密:使用同一个密钥进行加密和解密,速度快,适用于大量数据;
- 非对称加密:使用公钥和私钥进行加密和解密,安全性高,适用于身份验证和数字签名
什么是跨域问题?
跨域问题通常指的是在浏览器中由于同源策略的限制而产生的问题。
什么是同源策略?
同源策略(Same-origin policy)是浏览器的一项重要安全特性,用于控制不同来源的网页之间的交互。这项政策的主要目的是为了保护用户的隐私和数据安全,防止恶意网站执行某些恶意操作。
但在实际应用中,有些情况下需要跨域访问。
跨域问题的原因分析?
一般来讲,源由协议、域名和端口号组成。如果两个 URL 的协议、域名和端口号中任何一个不同,就被认为是跨域。通常产生跨域问题有以下几种原因:
- 协议不同:如 https和http;
- 端口不同
- 域名不同
解决跨域问题
- 使用
@CrossOrigin
注解 - 使用Nginx来实现跨域
- Node代理
- 使用全局跨域配置
- 使用CorsFilter跨域
为什么建立连接要三次握手?
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。
- 第一次握手:申请建立连接
Client
什么都不能确认;Server
确认了对方发送正常,自己接收正常
- 第二次握手:确认建立连接
Client
确认了:自己发送、接收正常,对方发送、接收正常;Server
确认了:对方发送正常,自己接收正常
- 第三次握手:收到确认,建立成功
Client
确认了:自己发送、接收正常,对方发送、接收正常;Server
确认了:自己发送、接收正常,对方发送、接收正常
为什么断开连接要四次挥手?
TCP 是全双工通信,可以双向传输数据,完全关闭需要双方确认。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。
举个例子:A 和 B 打电话,通话即将结束后。
第一次挥手:A 说“我没啥要说的了”(申请断开)
第二次挥手:B 回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话(传输数据)
第三次挥手:于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”(传完数据,确认断开)
第四次挥手:A 回答“知道了”,这样通话才算结束(知道对方断开了,自己也断开)
前端拓展
Vue是什么?
Vue是一个渐进式 JavaScript 框架,适合从小型项目到大型复杂应用。
- 核心特点:易用性、响应式系统、组件化、轻量级、丰富的生态系统。
- 主要概念:模板语法、指令、计算属性、组件、生命周期钩子,双向绑定,数据驱动视图。
- 常用工具:Vue Router、Vite
Vue的优势有哪些?
- 学习曲线平缓:Vue 的文档清晰,API 简洁,易于上手,适合初学者快速掌握。
- 轻量级:Vue 核心库体积小,加载速度快,不会显著增加项目的初始加载时间。
- 双向数据绑定:Vue 提供了强大的双向数据绑定机制,使得数据模型和视图之间的同步变得简单。
- 组件化:Vue 支持组件化开发,可以将应用程序分解为可重用的组件,提高代码的可维护性和复用性。
- 虚拟 DOM:Vue 使用虚拟 DOM 技术,优化了 DOM 操作,提高了渲染性能。
- 生态系统丰富:Vue 拥有丰富的生态系统,包括路由管理(
Vue Router
)、状态管理(Pinal
)、构建工具(Vite
)等。
v-if 和 v-show的区别?
v-show
隐藏则是为该元素添加css--display:none
,dom元素依旧还在。
v-if
显示隐藏是将dom元素整个添加或删除
如何选择:
v-if
与v-show
都能控制dom元素在页面的显示v-if
相比v-show
开销更大(直接操作dom节点增加与删除)- 如果需要非常频繁地切换,则使用
v-show
较好 - 如果在运行时条件很少改变,则使用
v-if
较好
watch与compute的区别?
watch
用于监听数据的变化,监听的数据每次变化时都会触发,可在方法中对新值和旧值做一些操作。
computed
是计算属性,可以对几个参数设置一个表达式,调用computed会得到表达式计算之后的结果。
区别:
watch
监听的参数变化时才会触发。computed
中的任一个参数有变化,都会重新计算一下表达式的值。computed
比watch
触发更频繁,更消耗资源。
使用过哪些前端组件?
- Element Plus:一个基于 Vue 3 的桌面端组件库,提供丰富的 UI 组件,帮助快速构建美观的用户界面。
- Pinia:Vue 3 的状态管理库,提供简洁的 API 和类型支持,用于管理应用的全局状态。
- Vite:由 Vue 作者开发的下一代前端构建工具,基于 ES 模块(ESM)提供快速的冷启动和热更新,大幅提升开发体验。
- Axios:一个基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js,支持拦截请求和响应,处理错误等。
- ECharts:一个由百度开源的图表库,提供丰富的图表类型和高度可定制的配置选项,用于数据可视化。
- Vue Router:Vue.js 的官方路由管理器,用于构建单页面应用(SPA),支持嵌套路由、动态路由匹配等功能。
Vue父子组件如何进行交互?
Vue 3.4之后可使用defineModel进行父子组件的交互
- 父组件中,在子组件标签上使用v-model绑定变量
<Son v-model:name="name">
- 子组件中使用defineModel定义变量
//定义一个defineModel import {defineModel} from vue const a =defineModel("img") const b =defineModel() //绑定的变量名字默认为defineModel
Vue的生命周期
- 创建阶段
beforeCreate
:实例刚被创建,数据观测和事件配置尚未初始化。created
:实例已完成数据观测、属性和方法的运算,初始化事件,但尚未挂载到 DOM。
- 挂载阶段
beforeMount
:实例已编译模板,但尚未挂载到 DOM。mounted
:实例已挂载到 DOM,可以访问到真实的 DOM 元素。
- 更新阶段
beforeUpdate
:数据更新时调用,发生在虚拟 DOM 打补丁之前。updated
:数据更新后,虚拟 DOM 重新渲染并打补丁到真实 DOM 之后调用。
- 销毁阶段
beforeUnmount
(Vue 3.x):实例即将被销毁,可以在此执行清理操作。unmounted
(Vue 3.x):实例已被销毁,所有绑定和事件监听器都被移除
v-for指令中的key属性有什么用?
key
属性给每个列表项提供唯一标识,帮助 Vue 高效更新和重用 DOM 元素,提高性能和稳定性,提高渲染效率。
重定向和转发的区别?
- 重定向:客户端 两次请求,URL 变化,适用于 页面跳转 和 跨域请求。
- 转发:客户端 一次请求,URL 不变,适用于服务器内部处理
Pinia的存储位置有哪些?
Pinia 默认将状态存储在内存中,适用于大多数临时状态管理的场景。除此之外,Pinia 的状态还可以持久化到 LocalStorage
或 SessionStorage
-
LocalStorage
:将状态存储在浏览器的 LocalStorage 中,数据在浏览器关闭后仍然保留,只能存储字符串数据,存储容量有限(通常为 5MB 左右) -
SessionStorage
:将状态存储在浏览器的 SessionStorage 中,数据在浏览器会话结束(关闭浏览器标签或窗口)后会丢失,存储容量有限(通常为 5MB 左右)。
场景题
了解过低代码吗?对低代码的看法
低代码平台通过 可视化界面 和 拖拽式组件,帮助开发者快速构建企业应用,减少手工编码的工作量。宜搭是由阿里巴巴集团自主研发的一款低代码平台
优点:
- 快速开发
- 降低开发门槛
- 易于维护
- 支持多种数据源和第三方服务的集成
- 降低开发成本
缺点:
- 定制化能力有限
- 不适用并发场景
- 有一定学习成本
若依框架了解过吗?
若依是一个基于 Spring Boot
和Spring Cloud
的快速开发平台,可以帮助开发者快速构建微服务项目,减少重复性工作,提高开发效率,还提供权限管理、数据字典、代码生成器等功能
-
优点
- 快速开发:
- 代码生成器:若依提供了强大的代码生成器,可以根据数据库表自动生成CRUD代码,大大减少了开发工作量。
- 模板丰富:提供了丰富的前端和后端模板,包括登录、注册、权限管理等,开发者可以快速搭建基础功能。
- 架构先进:
- 微服务架构:支持Spring Cloud微服务架构,可以轻松实现服务拆分和分布式部署。
- 模块化设计:采用模块化设计,各模块之间松耦合,易于扩展和维护。
- 开源免费
- 前端友好
- 前后端分离:采用了前后端分离的设计,前端使用Vue.js框架,后端使用Spring Boot,适合现代Web应用开发
- 快速开发:
-
缺点
- 学习成本:若依涉及的技术栈较多,包括Spring Boot、Spring Cloud、Vue.js等,对于初学者来说有一定的学习曲线。
- 高并发:在高并发场景下,若依系统可能会面临资源消耗大的问题,需要进行性能优化和调优。
下单请求三重防重
- 前端:用户点击下单后显示loading层,或者按钮置灰,防止重复发送请求
- 后端
- redis分布式锁:
setNx
防止用户用同一个账户在不同设备下提交多次,但是锁的时间,也就是TTL
不好把握 - Redisson锁(看门狗):针对执行时间比较长的业务,通过看门狗机制对分布式锁进行续命,直到业务执行结束。
- redis分布式锁:
如何设计猜你喜欢功能?
在页面的一个埋点的接口,用户每次点击某个商品的时候就会把商品id存入redis中并用zset存储。
通过(score
)进行排名,用户每点击就+1,我们通过前10的商品id去Redis或者商品表拿它的标签,通过标签从es进行搜索,再把搜索到的商品推送给用户。
如何设计一个秒杀系统?
秒杀活动请求以公网为划分点,可以分为:前端部分、后端部分。
整体思路是尽量将流量挡在前面,让尽量少的流量留到后端部分。因为越往后端,我们的处理逻辑就越重,其处理能力也越弱。
-
前端优化
对于前端部分来说,常见的优化手段有:【页面静态化 + CDN】、【请求频率限制】。CDN: 内容分发网络,它由非常多台 分布在世界各地 的缓存服务器组成。每次用户请求特定域名的时候,会转发到对应 CDN 的 DNS 解析服务器,随后会返回一台 离用户地理位置最近的一台 CDN 服务器
- 页面静态化 + CDN
可以将所有可以静态化的内容全部静态化,然后将其配置在 CDN 服务器上。这样既提高了用户打开页面的时间,又减少了后端服务器的压力 - 请求频率限制
求频率限制,指的是根据业务的特点,在前端做一些流量拦截,减少后端服务器的压力。常见的拦截方式有:- 设定一个请求概率,只允许 30% 的概率向后端发送接口请求。
- 设定一个请求频率,例如 10 秒钟只能请求 1 次,随后按钮置灰。
- 页面静态化 + CDN
-
后端优化
后端的优化有如下几种方式:- 增加缓存层 + 预热数据
- MQ 异步处理
- 限流、熔断、降级
- 业务端优化
谈一下对高并发的理解,平时怎么处理高并发问题?
高并发是指系统在同一时间段内能够处理 大量 并发请求的能力。
在互联网应用中,随着用户数量的增长,系统的访问量也会急剧增加,这就需要系统能够支持高并发,以保证服务的稳定性和响应速度。
下面是一些关于高并发的处理方法:
- 分库分表
- Spring Cloud Sentinel [哨兵]
- K8S 部署应用 [可以动态扩展副本]
- 用nginx作负载均衡
- Redis作缓存层
- mysql数据库用集群 (读写分离)
- Lua脚本减少对数据库的请求
- MQ异步
- 前端静态页面
- CDN
什么是 CDN ?
CDN 就是内容分发网络
本质就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担
怎么防止大量的空商品请求导致黑名单变大
- 使用
cannal
监听数据库insert
操作 xxl-job
每天同步最大商品id- 商品id如果大于最大商品id则视为非法请求,不做响应。
GitFlow有哪些分支?
- 主分支(
Master
):存储 生产 就绪的代码,通常是最新的稳定版本。 - 开发分支(
Develop
):包含最新的 开发 成果,是所有新功能的集成点。 - 特性分支(
Feature
Branch):基于 Develop 分支创建,用于开发新功能或修复。 - 发布分支(
Release
Branch):从 Develop 分支创建,用于准备新版本的发布。 - 修复分支(
Hotfix
Branch):从 Master 分支创建,用于紧急修复生产环境中的问题。
uni-app调微信接口怎么实现登录
- 准备工作:注册微信开放平台账号,创建应用,获取
AppID
和AppSecret
,并配置微信登录。 - Uni-app 集成:安装微信 SDK,调用
uni.login
获取code
,将code
发送到后端。 - 后端处理:使用
code
调用微信接口获取返回一个JSON串,其中 包含openid
,将JSON串转成对象并返回给前端。 - 用户信息:前端调用
uni.getUserInfo
获取用户详细信息。