# 面试 **Repository Path**: huzhengyan/interview ## Basic Information - **Project Name**: 面试 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-08-11 - **Last Updated**: 2025-08-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 前言 [推荐网站](https://juejin.cn/) 面试题回答: > 切记不要问什么答什么,原则上尽可能多的阐述. > > 1. 面试问的问题 你不清楚 > > > 回答差边内容 > > 2. ### **延伸技术深度** > > 在基础回答后,主动延伸相关技术(如问`HashMap`时提到`ConcurrentHashMap`) > > 3. **不确定的问题**:坦诚但积极补救 > > 4. ### **强调实践经验** > > 结合项目经历回答理论问题 > > 5. ### **准备高频问题** 强调过的一定要记 > > 6. ### **反问面试官** > > 面试尾声时,通过高质量问题展示你的思考深度: > > - “这个项目的技术栈中,Java 部分主要面临哪些挑战?” > - “团队在代码质量和性能优化方面有哪些最佳实践?” > > 7. ### **模拟面试训练** > > - 对着镜子或朋友模拟回答,注意语速和肢体语言。 > - 用手机录制回答,复盘时优化逻辑和表达。 > > 8. **保持积极**:即使遇到难题,也要展现思考过程和学习能力。 * 是什么? * 实现原理 * 应用场景 * 相关联的知识 ,把知识串联起来 # Java基础 ## 基础题 ### Java的数据类型有哪些?(低) * 基本数据类型 8大基本类型 byte short int long float double boolean char * 引用数据类型 > 1. 类 > > 2. 接口 > > 3. 数组 > > ~~~java > int [] arr=new int[10]; > ~~~ 扩展: 基本类型与引用类型的区别 1. 初始值不一样 2. 存储不一样 * 栈空间 作用域结束后 空间立即释放 * 堆空间 存储对象,实例存储在栈空间,指定对象的首 地址 ### 堆和栈的区别 * Java的内存分为两类,一类是栈内存,一类是堆内存。 * 栈中存储的是当前线程的方法调用、基本数据类型和对象的引用,栈是有序的。 * 堆中存储的是对象的值,堆是无序的。 ### ==和equals方法的区别(中) **==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址) **equals()** : 它的作用也是判断两个对象的内容是否相等。但它一般有两种使用情况: - 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。使用的是Object类中默认的实现(默认使用==比较 ) - 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 **扩展:**(关联知识) 重写equlals方法的同时重写hashcode方法 > ### 为什么在重写 equals 方法的时候需要重写 hashCode 方法? > > * 重写equals方法 是为了 比较两个内容相同的对象 比较时相等 > > * 重写hashCode是因为在使用散列数据结构时,比如哈希表,我们希望相等的对象具有相等的哈希码。 > > > 在Java中,哈希表使用哈希码来确定存储对象的位置。如果两个相等的对象具有不同的哈希码,那么它们将被存储在哈希表的不同位置,导致无法正确查找这些对象。 ### 什么是重载?什么是重写?两者什么区别?(了解) > 重载: 方法名相同,参数类型或个数不同 > > 重写: 子类重写父类的方法 (子类中定义与父类相同的方法、参数 ) 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。 * 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分 * 重写:发生在父子类中 1. 方法名、参数列表必须相同,返回值小于等于父类 2. 抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则) 3. 如果父类方法访问修饰符为private则子类中就不是重写。 ### 什么是序列化?什么是返序列化?(高) * 将对象以二进制形式存储到存储介质 (硬盘、文件、网络 ) > 条件 : 对象所属的类必须实现序列化接口 Serializable * 从文件或网络中读取二进制转换为对象 扩展: * 序列化 ObjectOutputStream (写) * 反序列化 ObjectInputStream (读) > * 序列化是把内存Java对象保存到存储介质中 > * 反序列化就是把存储介质中的数据转化为Java对象。 > * 需要进行序列化的对象的类必须实现Serializable接口 > * Java通过ObjectInputStream和ObjectOutputStream实现序列化和反序列化 应用场景:(重点前2个) * 缓存系统(如 Redis 存储 Java 对象)(如JDK序列化器) * **消息中间件**(如 Kafka、RabbitMQ)传输对象时,需先序列化字节流再通过网络发送 * mybatis 二级缓存 (可以不说 因为用的少) > ### **一、数据持久化存储** > > - 场景说明:将内存中的对象状态保存到 > > 文件、数据库或其他存储介质中,以便程序重启后恢复数据。 > > - 例如:缓存系统(如 Redis 存储 Java 对象)、日志记录(保存对象操作历史)、游戏进度存档等。 > > - **核心价值**:实现 **跨会话的数据持久化**,避免内存数据随程序关闭丢失。 > > ### **二、分布式系统间通信** > > - 场景说明:在分布式架构中(如微服务、RPC 框架),对象需通过网络在 > > 不同服务器或进程间传输 > > - 例如: > - 使用 Java RMI(远程方法调用)时,参数和返回值需序列化后通过网络传输。 > - 分布式框架(如 Dubbo、Spring Cloud)通过序列化实现跨节点的对象传递。 > > - **核心价值**:突破单机限制,实现 **跨进程 / 跨机器的对象交互**。 > > ### **三、缓存与中间件数据传递** > > - 场景说明: > - **缓存框架**(如 Ehcache、Guava Cache)将对象序列化后存入磁盘或分布式缓存(如 Redis),减少内存占用。 > - **消息中间件**(如 Kafka、RabbitMQ)传输对象时,需先序列化字节流再通过网络发送。 > - **核心价值**:提升数据传输效率,适配不同存储介质的格式要求。 > > ### **四、对象克隆(深拷贝)** > > - 场景说明 通过序列化和反序列化实现 > > 复杂对象的深拷贝 > > (复制对象及其引用的所有嵌套对象)。 > > - 对比浅拷贝(仅复制对象引用),序列化可确保克隆对象与原对象在内存中完全独立。 > > - **核心价值**:安全高效地创建对象副本,避免引用共享导致的数据污染。 > > ### **五、框架与中间件的底层支持** > > - 场景说明 许多 Java 框架和工具依赖序列化机制实现核心功能: > - **Hibernate**:将实体对象序列化为二进制数据存入数据库。 > - **Java 自带的序列化机制**:用于实现 `RMI`、`JMX`(Java 管理扩展)等标准库功能。 > - **分布式计算框架**(如 Hadoop MapReduce):在任务节点间传递自定义对象。 > - **核心价值**:作为底层技术基石,支撑上层应用的功能实现。 > > ### **六、网络传输协议适配** > > - **场景说明**:当需要将 Java 对象通过 **非 Java 系统或异构网络** 传输时(如与 C++、Python 服务通信),可通过序列化将对象转为通用格式(如 JSON、XML),或直接传输字节流后在对端反序列化。 > - **核心价值**:打破语言和平台壁垒,实现跨技术栈的数据交互。 > > ### **注意事项** > > - **实现 `Serializable` 接口**:需标记类为可序列化,否则无法进行序列化操作。 > - **版本控制**:通过 `serialVersionUID` 显式声明序列化版本,避免类结构变更导致反序列化失败。 > - **性能优化**:大对象序列化可能影响性能,可结合压缩技术(如 Gzip)或选择更高效的序列化框架(如 Protostuff、Kryo)替代 Java 原生序列化。 ### &和&&的区别(中) * & 逻辑与 > &运算符有两种用法: > > * 按位与 二进制计算 相同位置都为1结果为1,否则为0 > * 逻辑与 结果是boolean类型 判断两边的表达式的值 * && 短路与 &&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。 注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。 应用场景: ~~~java //假如 有一个引用类型参数 list //这里能不能将list.size()>0 放前面? 如果放前面且list=null 会报空指针异常 //相反,将null!=list放前面,如果list=null表达式null!=list为false,注定结果为false,则右边的表达式 //list.size()>0不再执行 不会报错 if(null!=list&&list.size()>0){ } ~~~ ### 抽象类和接口的对比(中) - 抽象类是对类的对象(把多个类相同的属性或方法提取出来(纵向抽取),放在一个类中,通过继承达到复用)。接口是抽象方法和常量的集合。 > 横向抽取(多个不相干的类,如事务):AOP 切面编程 - 从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。 #### **相同点** - 接口和抽象类都不能实例化 - 都位于继承的顶端,用于被其他类实现或继承 - 都包含抽象方法(抽象类可以没有抽象方法,但有抽象方法的类一定是抽象类),其子类都必须覆写这些抽象方法 #### **不同点** | 参数 | 抽象类 | 接口 | | ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | 声明 | 抽象类使用abstract关键字声明 | 接口使用interface关键字声明 | | 实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 | | 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | | 访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是public static。并且不允许定义为 private 或者 protected | | 多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 | | 字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final 的 | **备注**:Java8接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。 > ~~~java > public interface f{ > default void test{ > //默认实现 > } > } > ~~~ > > 现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。 接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则: - 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。(实现接口只代表拥有该功能,如接口有开的功能 ,门可以开,电脑可以开 ,门和电脑没有关系) - 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。抽象类与子类是is a的关系 。(如抽象图形 具体类 三角形) **扩展:你开发过程中都用了什么?(重点)** > 接口: > > 1. **解耦业务逻辑与具体实现** 业务层 持久层 > 2. **定义通用规范或协议** 如Servlet JPA > > 抽象类: > > 1. 在业务层中,抽象类 `BaseService` 封装公共的数据库操作方法(如 `save()`、`delete()`),子类(如 `UserService`、`OrderService`)只需实现特定业务逻辑 > > 如:BaseController(控制层 提供常用实现) BaseServiceImpl(业务层默认实现) ### Java反射机制(高) > - 反射的应用场景(如框架开发、注解解析) > - `Class` 类的作用及获取方式 #### 什么是反射机制?(掌握) ​ JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;**这种动态获取类的信息以及动态调用对象的方法的功能称为java语言的反射机制**。 #### **反射的核心作用**(掌握) ##### **动态获取类信息** - 在运行时知道一个类的完整结构(如包含哪些方法、字段、构造器),无需在编译时提前确定。 - 例如:即使只拿到一个对象,也能反向 “看透” 它属于哪个类,以及这个类的所有细节。 ##### **动态创建对象** - 不通过 `new` 关键字,而是根据类名(字符串形式)直接创建对象。 - 场景:框架(如 Spring)中根据配置文件动态生成 Bean 对象。 ##### **动态调用方法和操作字段** - 运行时调用对象的方法(即使方法是私有的),或修改对象的字段值(无视访问修饰符)。 - 例如:通过反射强制修改一个被 `private` 修饰的字段值。 ##### **实现通用代码逻辑** - 编写与具体类无关的通用逻辑(如序列化、依赖注入),增强代码灵活性。 #### **反射的典型应用场景**(掌握) ##### Spring 框架 - **Bean 实例化**:Spring 的核心功能之一是 IoC(控制反转)和 DI(依赖注入),它借助反射来创建和管理 Bean 对象。在 Spring 的配置文件或者注解里,会定义 Bean 的信息。Spring 容器启动时,会依据这些信息,利用反射调用类的构造方法来创建 Bean 实例。比如,配置文件里定义了一个`UserService`类,Spring 就会通过反射实例化这个类。 - **AOP(面向切面编程)**:AOP 是 Spring 框架的另一个关键特性,它可以在不修改原有代码的情况下,为程序添加额外的功能,像日志记录、事务管理等。Spring AOP 利用动态代理来实现,而动态代理的实现离不开反射机制。当代理对象的方法被调用时,Spring 会通过反射调用目标对象的方法,并且在方法调用前后执行相应的增强逻辑。 - **注解处理**:Spring 大量使用注解来配置和管理 Bean,例如`@Component`、`@Service`、`@Autowired`等。Spring 容器会通过反射扫描类上的注解,依据注解的信息进行相应的处理,像创建 Bean、注入依赖等。 ##### Hibernate 框架 - **对象 - 关系映射(ORM)**:Hibernate 是一个流行的 Java ORM 框架,它可以把 Java 对象映射到数据库表。Hibernate 在运行时利用反射来获取实体类的属性和方法信息,进而生成 SQL 语句,实现对象的持久化操作。比如,当保存一个实体对象时,Hibernate 会通过反射获取对象的属性值,然后生成对应的`INSERT`语句。 - **动态加载实体类**:Hibernate 支持动态加载实体类,它可以在运行时通过反射加载实体类,并且根据实体类的映射信息来操作数据库。这样就能在不修改代码的情况下,动态地添加或修改实体类。 ##### MyBatis 框架 - **Mapper 接口代理**:MyBatis 提供了 Mapper 接口的代理机制,让开发者可以通过接口来操作数据库。MyBatis 在运行时会利用反射生成 Mapper 接口的代理对象,当调用 Mapper 接口的方法时,代理对象会根据方法名和参数,动态地生成 SQL 语句并执行。 - **结果集映射**:MyBatis 在处理查询结果时,会通过反射将查询结果映射到 Java 对象上。它会根据实体类的属性名和查询结果的列名进行匹配,然后通过反射调用实体类的 setter 方法来设置属性值。 ##### JUnit 框架 - **测试方法执行**:JUnit 是 Java 中常用的单元测试框架,它通过反射来发现和执行测试方法。JUnit 会扫描测试类中的所有方法,根据方法上的`@Test`注解来识别哪些方法是测试方法,然后通过反射调用这些测试方法。 #### **反射的优缺点**(了解) | **优点** | **缺点** | | ----------------------------- | --------------------------------------------------- | | 1. 动态性强,突破编译时限制 | 1. 性能开销:反射操作比直接调用慢(尤其多次调用时) | | 2. 实现通用逻辑,减少重复代码 | 2. 破坏封装性:可访问私有成员,违反面向对象设计原则 | | 3. 框架和工具的底层基石 | 3. 代码可读性差:过度使用会使逻辑晦涩难懂 | #### **反射的核心概念**(了解) 1. **Class 对象** - 每个类在内存中都有一个对应的 `Class` 对象,包含类的元数据(如方法列表、字段列表)。 - 例如:`String.class` 就是字符串类的 `Class` 对象。 2. **Constructor(构造器)** - 反射中可通过 `Constructor` 对象调用类的构造方法,创建实例(包括私有构造器)。 3. **Method(方法)** - 通过 `Method` 对象可在运行时调用类的方法,支持传递参数和获取返回值。 4. **Field(字段)** - 通过 `Field` 对象可读取或修改类的字段值,甚至突破 `private` 修饰符的限制。 #### **注意事项**(了解) 1. 权限问题 - 操作私有成员时,需调用 `setAccessible(true)` 绕过访问控制检查(可能引发安全问题)。 2. 性能优化 - 对频繁使用的反射操作,可缓存 `Method`、`Field` 等对象,避免重复获取。 3. 安全性考量 - 反射可能被恶意代码利用(如创建危险类实例),需在敏感场景中谨慎使用。 #### Java获取反射的三种方法(了解) 1.通过new对象实现反射机制 2.通过路径实现反射机制 3.通过类名实现反射机制 ```java public class Student { private int id; String name; protected boolean sex; public float score; } public class Get { //获取反射机制三种方式 public static void main(String[] args) throws ClassNotFoundException { //方式一(通过建立对象) Student stu = new Student(); Class classobj1 = stu.getClass(); System.out.println(classobj1.getName()); //方式二(所在通过路径-相对路径) Class classobj2 = Class.forName("fanshe.Student"); System.out.println(classobj2.getName()); //方式三(通过类名) Class classobj3 = Student.class; System.out.println(classobj3.getName()); } } ``` ## 异常处理 ### java 常见Exception(中) 运行时异常和非运行时异常 #### 运行时异常 都是RuntimeException类及其子类异常: * IndexOutOfBoundsException 索引越界异常(常见于集合list 数组 indexOf查找) * ArithmeticException:数学计算异常 (不常见) * **NullPointerException:空指针异常** * ArrayOutOfBoundsException:数组索引越界异常 * **ClassNotFoundException:类文件未找到异常** (常见于需要依赖的类库jar包 找不到 ) * **ClassCastException:造型异常(类型转换异常)** 这些异常是不检查异常(Unchecked Exception),程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的。 扩展: 自定义异常(验证异常、业务异常....) #### 非运行时异常 也称为编译期异常。 是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如: * **IOException、文件读写异常** * **FileNotFoundException:文件未找到异常** * EOFException:读写文件尾异常 * MalformedURLException:URL格式错误异常 * SocketException:Socket异常 * **SQLException:SQL数据库异常** > 扩展了解: Error 和Exception的区别 ## **IO 与 NIO** ### io流分几种(低) #### 一、按流向分类 1. **输入流(InputStream/Reader)** 1. 输入流 * 数据源(如文件、网络等)读取数据。 - 常见的输入流有FileInputStream、BufferedInputStream、ObjectInputStream、FileReader、BufferedReader等。 2. 输出流(OutputStream/Writer) - 用于向目标(如文件、网络等)写入数据。 - 常见的输出流有FileOutputStream、BufferedOutputStream、ObjectOutputStream、FileWriter、BufferedWriter等。 #### 二、按数据类型分类 1. 字节流(Byte Stream) - 以字节为单位进行读写操作的流。 - 常见的字节流有InputStream和OutputStream的子类,如FileInputStream、FileOutputStream等。 - 字节流通常用于处理非文本数据,如图片、音频、视频等媒体文件。 2. 字符流(Character Stream) - 以字符为单位进行读写操作的流。 - 常见的字符流有Reader和Writer的子类,如FileReader、FileWriter等。 - 字符流通常用于处理文本数据,如文本文件、程序代码等。 #### 三、按功能分类 1. 缓冲流(Buffered Stream) - 提供缓冲功能,能够减少IO操作的次数,提高IO操作的性能。 - 常见的缓冲流有BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter等。 2. 转换流(Conversion Stream) - 用于字符流和字节流之间的转换。 - 常见的转换流有InputStreamReader和OutputStreamWriter等。 3. 文件流(File Stream) - 专门用于对文件进行读写操作的流。 - 常见的文件流有FileInputStream、FileOutputStream、FileReader、FileWriter等。 4. 内存流(Memory Stream) - 将数据保存在内存中进行读写操作的流。 - 常见的内存流有ByteArrayInputStream、ByteArrayOutputStream、CharArrayReader、CharArrayWriter等。 5. 对象流(Object Stream) - 用于序列化和反序列化对象的流。 - 常见的对象流有ObjectInputStream和ObjectOutputStream等。 #### 四、其他分类方式 除了上述分类方式外,IO流还可以根据其他标准进行分类,如根据是否阻塞(同步IO、异步IO)、是否面向缓冲区(NIO、AIO)等。这些分类方式在实际应用中也有重要的参考价值。 综上所述,IO流的分类方式多种多样,具体使用哪种类型的流取决于实际应用场景的需求。在选择合适的IO流时,需要综合考虑数据的类型、流向、性能要求以及应用场景等因素。 ### BIO,NIO,AIO 有什么区别?(中) - 简答 - BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。 - NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。 - AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。 - 详细回答 - **BIO (Blocking I/O):** 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 - **NIO (New I/O):** NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 - **AIO (Asynchronous I/O):** AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 **适用场景分析:** * BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解,如之前在Apache中使用。 * NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持,如在 Nginx,Netty中使用。 * AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持,在成长中,Netty曾经使用过,后来放弃。 ## 集合框架(重点 高频) > 偏底层 结果 原理 ### List,Set,Map的区别?(中) > 其它问法:你开发过程中用到的容器有哪些? > > 建议在阐述集合特点时顺便说一下底层结构,如List的实现类ArrayList,它的底层是Object类型的数组。 - `List`是一个有序的集合,里面可以存储重复的元素。 - `Set`是一个不能存储相同元素的集合。 无序 不重复 - `Map`是一个通过键值对的方式存储元素的,键不能重复。 - Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。 - Collection集合主要有List和Set两大接口 - List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。kəˈlekʃ(ə)n*/* - Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 - Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。 Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap ### HashMap的底层原理(高频) > [hashmap头插法和尾插法区别](https://zhuanlan.zhihu.com/p/449337470) > > [HashMap扩容机制,头插法和尾插法图解](https://blog.csdn.net/qq_50801874/article/details/142341462) > > [红黑树](https://juejin.cn/post/6844904069723586568?searchId=2025060514033243197F323802AEDFE15A) 1. 什么是hashmap ? 它的特点是什么 > ### 基本特性 > > - **键值对存储**:`HashMap`以键值对形式存储数据,每个键都是唯一的,一个键对应一个值。 > - **无序性**:存储的键值对是无序的,不会按照插入顺序或其他特定顺序排列。 > - **允许 null 值和 null 键**:可以有一个 null 键,也能有多个 null 值。 > - **非线程安全**:(这个特点可以在介绍完hashmap后引出线程安全的实现)在多线程环境下,若多个线程同时访问`HashMap`,并且至少有一个线程对其结构进行修改,那么它必须在外部进行同步。 2. 底层结构 > 1. 加载因子 0.75 16*0.75=12 当数组中元素的数量大于12时,扩容为原来的1倍 16-->32 > > 2. 结构: 数组+链表 +红黑树 在第一次使用时初始化(第一次调用put方法存值时) > > 3. threshold 阈值 = capacity * load factor > > 4. ### 树化的条件 > > 1. **链表长度达到阈值**:链表的长度达到 8 时,会触发树化的检查。 > 2. **桶数组的容量达到一定值**:`HashMap` 的桶数组(即 `table` 数组)长度达到 64。 > > 5. ### 反树化的条件 > > 当红黑树中的节点数量减少到 6 个时,红黑树会转换为链表 > > 6. #### HashMap的重要参数 > > HashMap 的实例有两个参数影响其性能:初始容量和加载因子。 > > 初始容量容量默认为 16 > > 加载因子: 0.75 > > 扩容的倍数是:2 3. 实现原理 > map.put(k,v)实现原理: > > 1. 调用K的hashCode()方法得出hash值。 > 2. 通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。 > > map.get(k)实现原理: > > 1. 先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。 > 2. 通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。 4. 应用场景(可以不说) > 1. mybatis结果集映射 > 2. 在控制层接收参数 5. 知识串联 > 通过前文引入串联的知识 。 > > HashMap是线程非安全的,如果在多线程环境下,以前用HashTable,现在我们用的最多的是ConcurrentHashMap,接着介绍该类。 > ### HashMap ,Hashtable两者之间的区别(了解) > 主要考察线程是否安全,先说两者的区别,因为hashtable效率低,然后引出 ConcurrentHashMap > > hashtable继承字典类Dictionary,它的子类Properties我们用的比较 多。 HashMap 和 Hashtable 两者都是Map接口之下的两个实现类,Map对象是键值对的,所以两类也是键值对的.(键值对即 key = "value",通过一个键值,获取一个值) HashMap的默认初始化容量为16,而Hashtable的初始化容量是11, Hashtable主要继承的是Dictionary类,HashMap主要继承的是Abstractmap()类,例图如下 ![image-20250604170632530](assets/image-20250604170632530.png) ![img](assets/JAY6WBAAFA.png) 1. Hashtable 是线程安全的,使用synchronized同步代码块保证安全行,效率低 2. HashMap 非线程安全的,效率高,如果要在多线程场景下使用,请用 ConcurrentHashMap 3. key 健值: HashMap 健值 可与为 null, HashTable 不允许为null ### 说说你对ConcurrentHashMap的了解?(高频) [参考](https://blog.csdn.net/D812359/article/details/125094698) 在 Java 里,CAS(Compare-And-Swap)属于一种无锁算法,它能让线程在不使用锁的情况下进行原子操作。该算法的核心原理是:**先比较某个内存位置的当前值与预期值,若二者相等,就将该内存位置的值更新为新值;若不相等,则不做更新**。 在数据结构上, JDK1.8 中的 `ConcurrentHashMap` 选择了与 HashMap 相同的`数组+链表+红黑树`结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用 **`CAS + synchronized`** 实现更加细粒度的锁。 ### ThreadLocal(线程局部变量)(高频) > 1. 介绍概念 > 2. 实现原理 > 3. 应用场景 > 4. 内存泄露 ThreadLocal(线程局部变量)是Java中的一个类,它提供了一种线程局部变量的机制,即每个使用该变量的线程都会有一个该变量的独立副本,该副本对其他线程不可见,确保了线程间的数据隔离。以下是关于ThreadLocal的详细解释: #### 一、基本概念 - **定义**:ThreadLocal为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以访问自己内部的副本变量,而不会影响到其他线程的副本。 - 用途 1. **线程上下文数据存储**:在多线程应用程序中安全地存储和访问与线程相关的数据。 2. **线程上下文传递**:在异步编程场景中,将数据从一个线程传递到另一个线程。 3. **线程局部状态维护**:每个线程维护自己的局部状态信息,避免线程间的干扰。 4. **性能统计和追踪**:存储每个线程的计时器或统计器,用于性能分析和瓶颈定位。 5. **临时资源分配**:在需要临时分配资源的场景中,避免资源的竞争和冲突。 6. **日志记录和调试**:存储当前线程的标识或调试信息,帮助排查多线程环境下的问题。 7. **测试场景模拟**:在测试场景中模拟特定的线程环境。 #### 二、实现原理 - **ThreadLocalMap**:ThreadLocal使用内部类ThreadLocalMap来管理每个线程的数据。每个ThreadLocal对象在ThreadLocalMap中都有一个唯一的键值对,键为ThreadLocal对象自身,值为线程特定的数据。 - 存储和访问 - 当调用ThreadLocal的`set`方法时,会先获取当前线程的ThreadLocalMap,然后将ThreadLocal对象作为键,要保存的值作为值,存储到ThreadLocalMap中。 - 当线程需要获取ThreadLocal对象的值时,调用ThreadLocal的`get`方法,通过当前线程的ThreadLocalMap获取对应的值。 - **内存泄漏问题**:ThreadLocal在保存时会将自己作为弱引用(WeakReference)的键存储在ThreadLocalMap中。如果ThreadLocal对象没有外部强引用,那么在垃圾回收时可能会被回收,但其对应的value仍然存在于ThreadLocalMap中,导致内存泄漏。因此,在使用完ThreadLocal后,应该调用`remove`方法清除数据 #### 使用场景 1. 需要保存线程的上下文信息,例如 用户信息,通过token解析出来的用户id等 ; 2. 需要对线程的局部变量进行隔离,避免线程安全问题; LocalDate LocalDateTime 3. 需要在跨类跨方法使用同一个变量,同时又不希望使用全局变量的情况; 4. 需要避免传递参数的繁琐,例如在 Spring 框架中使用的事务管理。 需要注意的是,由于 ThreadLocal 存储数据的副本是存储在每个线程的 ThreadLocalMap 中的,因此需要及时清理 ThreadLocal 中存储的数据,以避免内存泄漏问题。通常可以在使用完毕后通过调用 ThreadLocal 的 remove() 方法来清理数据,或者使用 Java 8 引入的新特性 ### Java四大引用 Java中的四大引用包括强引用、软引用、弱引用和虚引用,它们之间的主要区别在于对垃圾回收器的抵抗力不同,以及各自的用途和生命周期特点。 1. **强引用(Strong Reference)** - **定义**:强引用是最常见的引用类型,我们平时使用的普通对象引用就是强引用。 - **特点**:只要对象具有强引用,垃圾回收器就永远不会回收它。即使内存不足,也会导致程序抛出OutOfMemoryError异常。 - **用途**:用于普通的对象引用。 - **生命周期**:强引用的对象一直存活直到显式地被回收(例如,将引用置为null)。 2. **软引用(Soft Reference)** - **定义**:软引用是一种比强引用稍微弱一些的引用类型。 - **特点**:只有在系统将要发生内存溢出之前,软引用指向的对象才会被垃圾回收器回收。如果内存足够,垃圾回收器就不会回收它。 - **用途**:通常用于实现内存敏感的缓存。 - **生命周期**:软引用的对象在内存不足时可以被回收。 - **实现**:需要使用`java.lang.ref.SoftReference`类来实现。 3. **弱引用(Weak Reference)** - **定义**:弱引用是一种比软引用更弱的引用类型。 - **特点**:弱引用关联的对象只能生存到下一次垃圾收集发生为止,无论当前内存是否足够,只要垃圾收集器开始工作,都会回收掉只被弱引用关联的对象。 - **用途**:主要用于实现规范的映射关系,可以自动清理不再使用的对象。 - **实现**:需要使用`java.lang.ref.WeakReference`类来实现。 4. **虚引用(Phantom Reference)** - **定义**:虚引用是四种引用类型中最弱的一种。 - **特点**:虚引用与垃圾回收过程没有直接关系,主要用于跟踪对象被垃圾回收器回收的活动。虚引用必须和引用队列(`ReferenceQueue`)一起使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收之前,把这个虚引用加入到与之关联的引用队列中。 - **用途**:主要用于跟踪对象的回收情况。 - **实现**:需要使用`java.lang.ref.PhantomReference`类来实现。 - **访问对象**:虚引用无法直接访问对象,其`get()`方法总是返回null。 综上所述,Java中的四大引用类型各自具有不同的特点和用途,它们为Java程序的内存管理和垃圾回收提供了灵活性和高效性。在实际开发中,可以根据具体需求选择合适的引用类型来优化程序的性能和内存使用效率。 ### 哈希码(了解) 哈希码(Hash Code)是计算机科学中用于快速数据查找的一种编码方式。它通过某种算法将输入(通常是一个字符串或一段数据)转换成一个固定长度的输出值,这个输出值就是哈希码。**哈希码的主要目的是提高数据检索的效率,通过哈希码,可以快速定位到数据在存储结构中的位置,从而加快查找速度**。 #### 哈希码的特点 1. **固定长度**:无论输入数据的长度如何,哈希码的长度都是固定的。 2. **唯一性**:理想情况下,不同的输入数据应该产生不同的哈希码,但在实际应用中,由于哈希函数的设计限制和哈希值的长度限制,可能会出现不同的输入数据产生相同的哈希码的情况,这称为哈希冲突(Hash Collision)。 3. **不可逆性**:从哈希码一般无法直接还原出原始数据,这是哈希码的一个重要安全特性。 #### 哈希码的应用 1. **数据检索**:在数据库和索引结构中,哈希码用于快速定位数据。 2. **安全**:在密码学和安全领域,哈希码用于生成摘要(如MD5、SHA-1、SHA-256等),用于验证数据的完整性和真实性。 3. **集合实现**:在Java等编程语言中,哈希码是实现HashMap、HashSet等集合类的关键。这些集合通过哈希码来确定元素的存储位置,从而快速地进行插入、删除和查找操作。 #### 哈希函数的设计 设计一个好的哈希函数非常重要,因为它直接影响到哈希表的性能和安全性。一个好的哈希函数应该具有以下特点: - **均匀性**:哈希值应该尽可能均匀地分布在哈希表的空间中,以减少哈希冲突。 - **雪崩效应**:输入数据的微小变化应该导致哈希值的显著变化,这有助于减少哈希冲突并提高安全性。 - **计算效率**:哈希函数的计算应该尽可能高效,以支持快速的数据检索。 #### 注意事项 - 哈希冲突是不可避免的,因此设计哈希表时需要考虑如何处理冲突,常见的冲突解决方法有开放寻址法和链地址法。 - 在使用哈希码进行安全性相关的操作时(如密码存储),应避免使用简单的哈希函数(如MD5、SHA-1),因为这些函数存在已知的弱点,容易受到攻击。应使用更安全的哈希函数(如SHA-256、SHA-3等)。 ### hashCode 与 equals (重要) > 相关面试题:==和equals方法的区别? > > 关注点: equals方法比较的是两个对象的内容,默认是Object类里的实现,比较的是对象首地址,一般我们要重写equals方法,而重写equals方法是要求同时重写hashCode方法,引出 该话题 #### hashCode()介绍 - hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。 - 散列表(哈希表)存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) #### 为什么要有 hashCode > 我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: ​ 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。 #### hashCode()与equals()的相关规定 - 如果两个对象相等,则hashcode一定也是相同的 - 两个对象相等,对两个对象分别调用equals方法都返回true - 两个对象有相同的hashcode值,它们也不一定是相等的。如3C和2b两个字符串哈希码一样但内容不同。 > 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖 > hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) #### 为什么在重写 equals 方法的时候需要重写 hashCode 方法? * 重写equals方法 是为了 比较两个内容相同的对象 比较时相等 * 重写hashCode是因为在使用散列数据结构时,比如哈希表,我们希望相等的对象具有相等的哈希码。 > 在Java中,哈希表使用哈希码来确定存储对象的位置。如果两个相等的对象具有不同的哈希码,那么它们将被存储在哈希表的不同位置,导致无法正确查找这些对象。 ### Hsah碰撞是什么?如何解决?(重点掌握解决冲突的方法) #### Hash如何存数据 hash表的本质其实就是数组,hash表中通常存放的是键值对Entry。 ![在这里插入图片描述](assets/31a4a978cbd84819cca09ce2f217b6e7.png) 这里的id是个key,哈希表就是根据key值来通过哈希函数计算得到一个值,这个值就是下标值,用来确定这个Entry要存放在哈希表中哪个位置。 #### Hash碰撞 hash碰撞指的是,两个不同的值(比如张三、李四的id)经过hash计算后,得到的hash值相同,后来的李四要放到原来的张三的位置,但是数组的位置已经被张三占了,导致冲突。 #### 解决方法 ##### 开放寻址法 开放寻址法指的是,当前数组位置1被占用了,就放到下一个位置2上去,如果2也被占用了,就继续往下找,直到找到空位置。 ![在这里插入图片描述](assets/5e85c3bc314baf948ca9ddb8874f3ffb.png) > 开放定址法通过探测哈希表中的空闲位置来解决哈希碰撞。当发生碰撞时,哈希表会寻找下一个空闲的槽位来存储元素。常见的探测策略有: > > - **线性探测**:如果当前位置发生碰撞,检查下一个位置,直到找到空位。 > > - **二次探测**:采用平方的方式逐步探测空闲位置。 > > ~~~ > 同一位置被占,不再一个一个找,而是按 “跳跃式” 查找: > 第 1 次尝试:5 + 1² = 6 > 第 2 次尝试:5 + 2² = 9 > 第 3 次尝试:5 + 3² = 14 > 以此类推.. > ~~~ > > - **双重哈希**:使用第二个哈希函数来计算探测步长。 ##### 拉链法 链地址法是通过为每个哈希表的槽位创建一个链表来存储所有发生碰撞的元素。当发生碰撞时,新的元素会被加入到该位置的链表中。 **解决碰撞的过程** - 每个槽位是一个链表。 - 当发生碰撞时,将元素添加到该槽位的链表中。 - 查找操作时,首先计算哈希值,若发生碰撞,遍历链表进行查找。 拉链法采用的是链表的方式,这个时候位置4就不单单存放的是Entry了,此时的Entry还要额外保存一个next指针,指向数组外的另一个位置,将李四安排在这里,张三那个Entry中的next指针就指向李四的这个位置,也就是保存的这个位置的内存地址。 如果还有冲突,就把又冲突的那个Entry放到一个新位置上,然后李四的Entry指向它,这样就形成一个链表。 ![在这里插入图片描述](assets/cae1a4305f5901b50f848d6c3d166806.png) ##### 再哈希法 当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。缺点:计算时间增加。 ##### 建立公共溢出区 将哈希表分为基本表和溢出表。凡是与基本表发生冲突的元素,一律填入溢出表。查找时,先在基本表中查找,若找不到再到溢出表中查找。这种方法实现相对简单,但可能增加查找的时间开销。 ### 集合框架底层数据结构 #### Collection ##### List - Arraylist: Object数组(常用) > 1. 声明时 默认是空数组 > 2. 第一次添加元素时,扩容为10的数组 > 3. 第二次扩容: 原始数组的长度+原始数组长度的一半(如果是小数取小于当前数的最大整数) 10 --- 15---22---33 > 4. 特点: 查询快、增删改慢(遍历查找元素) - LinkedList: 双向循环链表 > 每个节点存当前元素,同时存前一个节点和下一个节点 > > ![image-20240723151023414](assets/image-20240723151023414.png) > > 特点: 读取慢(沿着链表遍历),增删改快 * Vector: Object数组 > 1. 初始容量默认是10 > > 2. 如果没有指定增长量,每次扩容是之前数组长度的一倍 > > > 如默认初始为10: > > > > * 第一次扩容 20 > > * 第二次扩容 40 > > * ..... > > 3. 如果指定增长量,每次扩容是在原有数组基础上直接增加指定的量(capacityIncrement) ##### Set - HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素 - LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。 - TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。) #### Map ##### HashMap ##### 底层结构 * JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突) * JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间 > ![image-20240723170107854](assets/image-20240723170107854.png) > > HashMap由数组(键值对Node组成的数组主干)+ 链表(元素太多时为解决哈希冲突数组的一个元素上多个Node组成的链表)+ 红黑树(当链表的元素个数达到8链表存储改为红黑树存储)进行数据的存储。 > > - **链表转红黑树**:当某个哈希桶中的链表长度大于等于 `TREEIFY_THRESHOLD`(默认为 8)时,并且整个哈希表的容量大于等于 `MIN_TREEIFY_CAPACITY`(默认为 64)时,链表会转换成红黑树。这是为了避免在哈希表容量较小且链表长度增长较快时频繁地进行树化和去树化操作。 > - **红黑树转链表**:当红黑树中的节点数减少到小于 `UNTREEIFY_THRESHOLD`(默认为 6)时,如果哈希表的容量仍然大于等于 `MIN_TREEIFY_CAPACITY`,则红黑树会被转换回链表。 > > HashMap采用table数组存储Key-Value的,每一个键值对组成了一个Node节点(JDK1.7为Entry实体,因为jdk1.8加入了红黑树,所以改为Node)。Node节点实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Node节点,以此来解决Hash冲突的问题。 ##### 原理 HashMap jdk1.7 的底层是基于数组+链表实现的.通过添加键的 **hashcode**与**数组的长度-1** **相与** 来得到这个元素在数组中的位置,如果这个位置没有数据,那么就把这个数据当做第一个节点。如果这个位置有了链表,那么在JDK1.7的时候使用的是头插法,在JDK1.8的时候使用尾插法。 元素以键值对的形式存放,并且允许null键和null值,**因为key值唯一(不能重复)**,因此,null键只有一个。另外,hashmap不保证元素存储的顺序,是一种无序的,和放入的顺序并不相同(此类不保证映射的顺序,特别是它不保证该顺序恒久不变)。HashMap是线程不安全的。 ##### HashMap的重要参数 HashMap 的实例有两个参数影响其性能:初始容量和加载因子。 * 初始容量容量默认为 16 * 加载因子: 0.75 * 阈值 threshold = hashmap的数组长度\* 加载因子 默认是16*0.75=12 数组容量达到阈值 扩容 * 树化阈值 8 链表上的元素大于等于8时 且数组长度达到64, 转化为红黑树 * 扩容的倍数是:2 ##### HashMap中的put()和get()的实现原理 ###### map.put(k,v)实现原理 1. 首先计算K的hash值 2. 通过哈希表函数/哈希算法,将hash值转换成数组的下标(key的hash值与数组长度减一 按位与 ) 3. 下标位置上如果没有任何元素,将k,v封装到Node对象当中(节点), 就把Node添加到这个位置上 4. 如果说下标对应的位置上有值(说明哈希冲突),就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。 ###### map.get(k)实现原理 1. 先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。 2. 通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。 ##### LinkedHashMap * LinkedHashMap 继承自 HashMap,所以它的底层仍然是由**数组+链表**或**红黑树**组成。 * LinkedHashMap 在hashmap的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 > ~~~java > static class Entry extends HashMap.Node { > Entry before, after; > Entry(int hash, K key, V value, Node next) { > super(hash, key, value, next); > } > } > ~~~ ##### HashTable 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 ##### TreeMap 红黑树(自平衡的排序二叉树) ##### ConcurrentHashMap > [参考](https://blog.csdn.net/qq_29051413/article/details/107869427) > > hashmap中存hashmap 二维hashmap (参照二维数组) > > 一维:Segment > > 二维:hashmap (数组+链表) ##### jdk7结构(了解) ![image-20240723175547739](assets/image-20240723175547739.png) ##### jdk8结构 ![image-20240723175949925](assets/image-20240723175949925.png) 在JDK 8中,`ConcurrentHashMap`的实现已经发生了重大变化,它不再使用JDK 7及之前版本中的`Segment`分段锁机制。在JDK 7及以前,`ConcurrentHashMap`通过引入`Segment`数组来将数据分成多个段(Segment),每个段都是一个可重入的锁(ReentrantLock),从而实现了对数据的分段加锁,以提高并发访问的效率。这种设计在一定程度上减少了锁的粒度,但仍然存在锁的争用问题。 然而,从JDK 8开始,`ConcurrentHashMap`采用了全新的实现方式,基于Node数组+链表+红黑树的结构,并通过CAS(Compare-And-Swap)操作来实现无锁算法,以及使用`synchronized`关键字来保证线程安全。这种设计大大减少了锁的粒度,几乎达到了每个桶(Bucket)只对应一个锁的效果,从而进一步提高了并发性能。 具体来说,JDK 8中的`ConcurrentHashMap`主要使用了以下技术和结构: 1. **Node数组**:存储键值对的数组,是数据的主要存储结构。 2. **链表**:当某个桶中的元素过多时(默认是8个),会将这些元素存储在链表中,以链表的形式来扩展存储能力。 3. **红黑树**:如果链表中的元素继续增加,并且数组长度超过了`MIN_TREEIFY_CAPACITY`(默认是64),或链表上节点达到8,那么链表会转换成红黑树,以优化查找效率。 4. **CAS操作**:在更新Node数组中的元素时,会尝试使用CAS操作来实现无锁更新。 5. **synchronized锁**:在扩容、添加元素到链表或红黑树等需要保证线程安全的情况下,会使用`synchronized`关键字来保证操作的原子性。 这种新的实现方式使得`ConcurrentHashMap`在JDK 8及以后的版本中,不仅保持了原有的高并发性能,还在某些场景下(如大量数据插入和查找)表现出了更优的性能。同时,由于不再使用`Segment`分段锁,也使得`ConcurrentHashMap`的API和内部实现变得更加简洁和易于理解 ### Java stream操作流常用的方式(了解) #### 常用的Stream方法 在Java中,Stream操作流是Java 8新引入的一个功能,它提供了很多强大的操作,方便我们进行集合的处理和操作。常用的Stream操作方式有: * 过滤:使用filter()方法可以过滤掉集合中不符合条件的元素。 * 映射:使用map()方法可以对集合中的每一个元素进行映射处理。 * 排序:使用sorted()方法可以对集合中的元素进行排序。 * 去重:使用distinct()方法去掉集合中的重复的元素。 * 统计:使用count()方法可以对集合中的元素进行统计。 * 聚合:使用reduce()方法可以对集合中的元素进行聚合计算。 * 遍历:使用forEach()方法可以遍历集合中的每一个元素。 * 匹配:使用anyMatch()、allMatch()、noneMatch()方法可以对集合中的元素进行匹配判断。 * 分组:使用qroupingBy()方法可以按照某一个属性进行分组。 * 转换:使用collect()方法可以将集合中的元素转换为另一个集合。 * 平均:使用average()方法可以用于计算一组元素的平均值。 #### Stream流中的方法 Stream提供了大量的方法进行聚集操作,这些方法既可以是“中间的”,也可以是“末端的”。 * 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的map()方法就是中间方法。中间方法的返回值是另外一个流。 * 末端方法:末端方法是对流的最终操作。当对某个Stream执行末端方法后,该流将会被“消耗”且不再可用。上面程序中的sum()、count()、average()等方法都是末端方法。 除此之外,关于流的方法还有如下两个**特征**: * 有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。 * 短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。 #### Stream常用的中间方法 * filter(Predicate predicate):过滤Stream中所有不符合predicate的元素。 * mapToXxx(ToXxxFunction mapper):使用ToXxxFunction对流中的元素执行一对一的转换,该方法返回的新流中包含了ToXxxFunction转换生成的所有元素。 * peek(Consumer action):依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。该方法主要用于调试。 * distinct():该方法用于排序流中所有重复的元素(判断元素重复的标准是使用equals()比较返回true)。这是一个有状态的方法。 * sorted():该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。 * limit(long maxSize):该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。 #### Stream常用的末端方法 * forEach(Consumer action):遍历流中所有元素,对每个元素执行action。 * toArray():将流中所有元素转换为一个数组。 * reduce():该方法有三个重载的版本,都用于通过某种操作来合并流中的元素。 * min():返回流中所有元素的最小值。 * max():返回流中所有元素的最大值。 * count():返回流中所有元素的数量。 * anyMatch(Predicate predicate):判断流中是否至少包含一个元素符合Predicate条件。 * noneMatch(Predicate predicate):判断流中是否所有元素都不符合Predicate条件。 * findFirst():返回流中的第一个元素。 * findAny():返回流中的任意一个元素。 除此之外,Java 8允许使用流式API来操作集合,Collection接口提供了一个stream()默认方法,该方法可返回该集合对应的流,接下来即可通过流式API来操作集合元素。由于Stream可以对集合元素进行整体的聚集操作,因此Stream极大地丰富了集合的功能。 Java 8新增了Stream、IntStream、LongStream、DoubleStream等流式API,这些API代表多个支持串行和并行聚集操作的元素。上面4个接口中,Stream是一个通用的流接口,而IntStream、LongStream、DoubleStream则代表元素类型为int、long、double的流。 Java 8还为上面每个流式API提供了对应的Builder,例如Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builder,开发者可以通过这些Builder来创建对应的流。 独立使用Stream的步骤如下: 1. 使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。 2. 重复调用Builder的add()方法向该流中添加多个元素。 3. 调用Builder的build()方法获取对应的Stream。 4. 用Stream的聚集方法。 在上面4个步骤中,第4步可以根据具体需求来调用不同的方法,Stream提供了大量的聚集方法供用户调用,具体可参考Stream或XxxStream的API文档。对于大部分聚集方法而言,每个Stream只能执行一次。 ### JDK8新特性(了解) > [参考](https://javaguide.cn/java/new-features/java8-common-new-features.html) #### 一.**Lambda表达式** Lambda表达式是JDK1.8引入的一种新语法,也是推动java8版本发展的最重要的一个更新。Lambda表达式可以理解为是一种匿名函数,**简化了匿名内部类的使用,并提高代码的可读性和性能**。**Lambda表达式的基本语法: 接口/父类 引用名=(参数)->表达式**。 Lambda表达式的特性: - **简洁**:将冗长的代码变的简单易读。 - **灵活**:传递参数和返回值。 - **高效**:提高代码性能,减少创建过多的中间对象。 ```java //匿名 Add add1 = new Add(){ public void add(int a, int b) { System.out.println(a+b); }; }; add1.add(1,2); //lambda Add add2 = (a,b) -> System.out.println(a+b); add2.add(1,2); ``` #### 二、接口增强(允许接口拥有方法体) 新的Interface接口中可以用default和static修饰。 **default的重要特性**:它允许在接口中提供方法的默认实现,这样,即使为接口新增了方法,已有的实现类也不必强制实现这些方法,而是可以选择性地覆盖它们。比如:某个接口实现类较多,又需要添加方法就可以使用default ```java interface MyInterface { default void myMethod() { System.out.println("This is a default method."); } } ``` ** static方法作用:**提供与接口相关的工具方法或辅助方法。它通常用于定义一些通用的功能,这些功能不需要依赖于具体的实现类,而是直接与接口本身相关联。 ```java interface MyInterface { //静态方法 可通过接口名直接调用 static void staticMethod() { } } ``` #### 三、函数式接口 函数式接口是只有一个抽象方法的接口,通常用于支持lambda表达式和函数式编程风格。函数式接口通常使用`@FunctionalInterface`注解来标记判断其是否只包含一个抽象方法,主要是为了编译器检查和代码清晰度。 1. Java 8中常见的函数式接口如下: 1、**自定义式函数式接口** ```java @FunctionalInterface public interface Add { int add(int a, int b); } public static void main(String[] args) { Add add = (int a, int b) -> a + b; System.out.println(add.add(3, 4)); // 输出: 7 } ``` 2、**Predicate**:输入一个参数T,返回一个布尔值,可用于执行一些判断逻辑。 ```java Predicate isEmpty = s -> s.isEmpty(); System.out.println(isEmpty.test("")); // 输出: true ``` 3、**Function:**输入一个参数T,返回一个结果R,可用于执行一些转换逻辑。 ```java Function square = (Integer x) -> x * x; System.out.println(square.apply(5)); // 输出: 25 ``` 4、**Consumer:**消费型接口,输入一个参数T,表示消费掉这个参数,可用于执行一些操作但没有返回值。 5、**Supplier:**供给型接口,返回一个数据T,可用于执行一些数据生成操作。 #### 四、方法引用 方法引用可以看作是 Lambda 表达式的一种简化形式,当 Lambda 表达式只是简单地调用一个已有的方法时,就可以使用方法引用来代替它。 方法引用也可以理解为式Lambda表达式的一种语法糖(语法糖就是可以用更少的代码完成相同的功能),是Lambda表达式的简化形式,可以提高代码的可读性和简洁性。语法:**类名::方法名** 1. 对象的实例方法引用 :**object::methodName** 1. 类的静态方法引用:**ClassName::staticMethodName** 1. 特定类型的实例方法引用:**TypeName::methodName** 1. 构造器引用:**ClassName::new** ```java List names = Arrays.asList("Alice", "Bob", "Charlie"); // 使用 lambda 表达式将每个字符串转换为大写 List upperCaseNames = names.stream() .map((String name) -> name.toUpperCase()) .collect(Collectors.toList()); //方法引用 引用String类型toUpperCase方法 List upperCaseNames = names.stream() .map(String::toUpperCase) .collect(Collectors.toList()); ``` #### 五、Stream API Stream API是新引入的一个抽象层,它使得集合或数组操作更加简洁和高效。 ##### Stream 特点 1. **不是数据结构**:Stream 不存储数据,它只是对集合、数组进行各种操作。 1. **延迟计算**:许多流操作(如过滤、映射)都是惰性执行的,这意味着它们不会立即执行,而是等到实际需要结果时才进行计算。 1. **内部迭代**:与传统的外部迭代不同,Stream 使用内部迭代,这使得开发者只需要指定想要做什么(如过滤元素),而不需要关心如何遍历每个元素。 ##### Stream常见操作 **中间操作**:中间操作返回一个新的流,并且可以链式调用。 **终端操作**:终端操作触发流的执行,并产生一个结果或副作用。一旦执行了终端操作,该流就被消耗,不能再使用。 ##### java.util.stream常用方法 ```java //filter(Predicate predicate):过滤 list.stream().filter(s -> s.startsWith("a")); //map(Function mapper):元素转换 list.stream().map(String::toUpperCase) //sorted 排序 list.stream().sorted(); //distinct():去除重复元素。 list.stream().distinct(); //limit(maxSize):限制 Stream 中元素的最大数量。 list.stream().limit(); //boolean allMatch(Predicate predicate) 此流的所有元素是否与提供的predicate匹配。 list.stream().allMatch(); //skip(long n) 跳过前面n个元素 list.stream().skip(); //使用Collector对流进行归纳 list.stream().collect(Collector.toList()) //count统计Stream元素数量 list.stream().count(). //forEach遍历Stream所有元素 list.stream().ForEach(); //max(Comparator comparator)查找最大值 list.stream().max(); //返回一个包含此流的元素的数组 toArray(); list.stream().toArray(); //findFirst() 和 findAny():查找第一个元素或任意元素(在并行流中) list.stream().findFirst(); list.stream().parallelStream().findAny(); ``` #### 六、Optional类 Optional是一个工具类,可以是一个为null的容器对象,它的主要作用就是为了避免null值异常,防止NullpointException。 **Optional常用方法** ```java // 创建一个可能为null的字符串 String s1 = null; String s2="hello"; // 使用Optional.ofNullable()来创建Optional对象 Optional optionalString = Optional.ofNullable(s1); Optional optionalString2 = Optional.ofNullable(s2); // 使用ifPresent()方法处理Optional中的值 optionalString.ifPresent(s -> System.out.println("Value: " + s)); optionalString2.ifPresent(s -> System.out.println("Value: " + s)); ``` ```java Person person = new Person(); //正常 if (ObjectUtil.isNotNull(person)){ System.out.println(person.getName()); } //Optional处理空值 Optional.ofNullable(person).map(o->o.getName()).ifPresent(o-> System.out.println(o)); ``` #### 七、新的日期和时间API Java 8引入了全新的日期和时间API, 通过引入 `java.time` 包来提供一套更为强大、线程安全且易于使用的日期和时间 API。 对原有的`java.util.Date`和`java.util.Calendar`进行了重新设计和扩展。新的日期和时间API设计上比较简洁,同时也提供了许多方便的日期处理方法和函数。 ##### 常见的时间和日期类型 - **LocalDate**:表示年月日。 - **LocalTime**:表示时分秒。 - **LocalDateTime**:表示日期和时间。 - **Duration**:表示两个时间之间的持续时间。 - **Period**:表示两个日期之间的时间间隔。 ##### Java 8中常见的日期和时间方法 - plusDays:增加指定的天数。 - minusDays:减少指定的天数。 - withYear:设置年份。 ##### Java8实现日期计算和格式化 ```java //获取当前时间 LocalDateTime time=LocalDateTime.now(); System.out.println(time); //减少指定天数 LocalDateTime yesterday = time.minusDays(1); System.out.println(yesterday); //通过DateTimeFormatter设置需转换的格式 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String formattedTime = yesterday.format(formatter); System.out.println(formattedTime); ``` ### java中深拷贝和浅拷贝(熟悉) 应用场景:java中深拷贝与浅拷贝,是用来描述对象或者对象数组 中,引用数据类型的一个复制场景。 浅拷贝:就是只复制某一个对象的引用地址,而不复制这个对象本身,这种复制方式意味着两个引用指针,指向被复制对象的同一块内存地址。 ![img](assets/VEZOWBAACQ.png) 深拷贝:两个引用对象分别指向两个不同的对象,且两个引用对象的引用分别指向两个不同的对象。就是说完全创建一个一模一样的新对象,新对象和老对象之前不共享任何内存,也就意味着对深对象的修改,不会影响老对象的一个值。 ![img](assets/VEZOWBACGA.png) 实现方式:在java里面无论是深拷贝还是浅拷贝,我们都需要去通过实现Cloneable这样一个接口,并且实现clone()方法,然后我们可以在clone()方法里面去实现浅拷贝与深拷贝的业务逻辑,不同的是深拷贝需要对其内的引用类型的变量,再进行一次 clone()。 如何选择拷贝方式:如果对象有引用属性,那就要基于具体的需求来选择浅拷贝还是深拷贝。意思是如果对象引用任何时候都不会被改变,那么没必要使用深拷贝,只需要使用浅拷贝就行了。如果对象引用经常改变,那么就要使用深拷贝。 ### 简述一下你了解的设计模式。(自选6种以上 高频面试) [大话设计模式(带目录完整版)](assets/大话设计模式(带目录完整版).pdf) 所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解其设计思路。 在 GoF 的《Design Patterns: Elements of Reusable Object-OrientedSoftware》中给出了三类(创建型[对类的实例化过程的抽象化]、结构型[描述如何将类或对象结合在一起形成更大的结构]、行为型[对在不同的对象之间划分责任和算法的抽象化])共 23 种设计模式,包括 * Abstract Factory(抽象工厂模式) * **Builder(建造者模式)** * **Factory Method(工厂方法模式)** * Prototype(原始模型模式) * **Singleton(单例模式)** > 实现条件 : > > 1. 构造方法私有化 (避免在外部创建对象) > > 2. 提供一个静态方法 返回本类的实例 > > > 实例创建的方式 : > > > > 1. 调用的时候创建 (懒汉式) > > 2. 类加载的时候创建 (静态代码块) * Facade(门面模式) * **Adapter(适配器模式)** > io InputStreamReader OutputStreamWriter * Bridge(桥梁模式) * Composite(合成模式) * **Decorator(装饰模式)** > io流中 BufferedInputStream BufferedOutputStream * Flyweight(享元模式) * **Proxy(代理模式)** > 动态代理 Proxy Aop 的原型就是动态代理 * Command(命令模式) * Interpreter(解释器模式) * Visitor(访问者模式) * Iterator(迭代子模式) * Mediator(调停者模式) * Memento(备忘录模式) * Observer(观察者模式) > 了解 * State(状态 模式 ) * Strategy(策略 模式 ) * Template Method(模板方法模式) * Chain Of Responsibility(责任链模式)。 面试被问到关于设计模式的知识时,可以拣最常用的作答,例如: (1)工厂模式:工厂类可以根据条件生成不同的子类实例,这些子类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。 (2)代理模式:给一个对象提供一个代理对象,并由代理对象控制原对象的引用。实际开发中,按照使用目的的不同,代理可以分为:远程代理、虚拟代理、保护代理、Cache 代理、防火墙代理、同步化代理、智能引用代理。 (3)适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作。 (4)模板方法模式:提供一个抽象类,将部分逻辑以具体方法或构造器的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。除此之外,还可以讲讲上面提到的门面模式、桥梁模式、单例模式、装潢模式(Collections 工具类和 I/O 系统中都使用装潢模式)等,反正基本原则就是拣自己最熟悉的、用得最多的作答,以免言多必失。 #### 用 Java 写一个单例类。 (1)饿汉式单例 ```java public class Singleton { private Singleton(){ } private static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; } } ``` (2)懒汉式单例 ```java public class Singleton { private static Singleton instance = null; private Singleton() { } public static synchronized Singleton getInstance(){ if (instance == null) instance = new Singleton(); return instance; } } ``` 实现一个单例有两点注意事项: ①将构造器私有,不允许外界通过构造器创建对象; ②通过公开的静态方法向外界返回类的唯一实例 ### 框架中 用到的设计模式有哪些?(熟悉) #### spring Spring 框架是 Java 开发中非常流行的框架,它广泛运用了多种设计模式,以下是一些主要的设计模式及其在 Spring 中的应用: ##### 单例模式 Spring 的 Bean 默认是单例模式。单例模式确保一个类仅有一个实例,并提供一个全局访问点。在 Spring 里,单例模式可减少内存开销与系统资源消耗。例如,Spring 容器中管理的数据源 Bean 通常为单例,保证整个应用使用同一数据源实例,避免资源浪费。 ##### 工厂模式 Spring 的 BeanFactory 是工厂模式的典型体现。工厂模式定义一个创建对象的接口,让子类决定实例化哪个类。BeanFactory 负责创建和管理 Bean 对象,开发者通过配置文件或注解告知 Spring 如何创建 Bean,将对象的创建和使用分离,提高代码的可维护性和可扩展性。 ##### 代理模式 Spring AOP(面向切面编程)借助代理模式实现。代理模式为其他对象提供代理以控制对该对象的访问。Spring AOP 能在不修改原有代码的情况下,对目标对象的方法进行增强,如添加日志记录、事务管理等功能。Spring AOP 有 JDK 动态代理和 CGLIB 代理两种实现方式。 ##### 观察者模式 Spring 中的事件驱动机制运用了观察者模式。观察者模式定义一种一对多的依赖关系,让多个观察者对象监听一个主题对象,当主题对象状态改变时,通知所有观察者更新。在 Spring 里,`ApplicationEvent`和`ApplicationListener`分别对应主题对象和观察者对象,开发者可自定义事件和监听器,实现系统的解耦和扩展性。 ##### 模板方法模式 Spring 的 JdbcTemplate、RedisTemplate 等模板类使用了模板方法模式。模板方法模式定义一个操作中的算法骨架,将一些步骤延迟到子类中实现。模板类封装了通用的操作流程,如数据库连接、事务管理等,开发者只需实现特定的业务逻辑,减少代码重复。 ##### 策略模式 Spring 的事务管理使用了策略模式。策略模式定义一系列算法,将每个算法封装起来,使它们可以相互替换。Spring 提供了多种事务传播行为和隔离级别,开发者可根据需求选择不同的事务策略。 #### Spring MVC ##### 策略模式(Strategy Pattern) - **说明**:Spring MVC 中的`HandlerMapping`接口及其实现类体现了策略模式。`HandlerMapping`负责将请求映射到具体的处理器(`Handler`),不同的`HandlerMapping`实现类代表了不同的映射策略。例如,`RequestMappingHandlerMapping`根据请求的 URL 路径和方法等信息来匹配处理器,而`SimpleUrlHandlerMapping`则根据简单的 URL 匹配规则来映射处理器。 - **作用**:通过使用策略模式,Spring MVC 可以根据不同的应用场景和需求选择合适的映射策略,使得请求映射的过程更加灵活和可配置。 ##### 适配器模式(Adapter Pattern) - **说明**:Spring MVC 中的`HandlerAdapter`接口及其实现类是适配器模式的应用。`HandlerAdapter`的作用是将不同类型的处理器适配成统一的接口,以便`DispatcherServlet`能够统一调用处理器的方法。不同的处理器可能有不同的方法签名和参数类型,`HandlerAdapter`将这些差异封装起来,使得`DispatcherServlet`不需要了解具体处理器的细节,只需要通过`HandlerAdapter`来调用处理器。 - **作用**:适配器模式使得 Spring MVC 能够支持多种类型的处理器,提高了系统的可扩展性和兼容性,同时也遵循了开闭原则,即当需要添加新类型的处理器时,只需要增加相应的`HandlerAdapter`实现类,而不需要修改`DispatcherServlet`等核心代码。 ##### 命令模式(Command Pattern) - **说明**:Spring MVC 中的`Handler`可以看作是命令对象。每个`Handler`都封装了一个具体的请求处理逻辑,就像命令模式中的命令对象封装了一个具体的操作一样。`DispatcherServlet`将请求发送给`Handler`,就相当于调用命令对象的执行方法,`Handler`执行相应的业务逻辑并返回结果。 - **作用**:命令模式将请求的发送者和接收者解耦,使得系统的结构更加清晰。在 Spring MVC 中,通过将请求处理逻辑封装在`Handler`中,使得不同的请求可以对应不同的`Handler`,方便了业务逻辑的组织和管理,同时也提高了代码的可测试性。 #### MyBatis ##### 工厂模式 - **体现**:`SqlSessionFactory` 是工厂模式的典型应用。`SqlSessionFactory` 负责创建 `SqlSession` 对象,`SqlSession` 是 MyBatis 中与数据库交互的核心对象,类似于一个会话。通常会通过 `SqlSessionFactoryBuilder` 读取 MyBatis 的配置文件来构建 `SqlSessionFactory` 实例。 - **作用**:将 `SqlSession` 对象的创建过程封装起来,让开发者只需关注如何使用 `SqlSession` 进行数据库操作,而不用关心其具体的创建细节,提高了代码的可维护性和可扩展性。 ##### 代理模式 - **体现**:MyBatis 的 Mapper 接口使用了代理模式。当你定义一个 Mapper 接口后,MyBatis 会在运行时为这个接口生成一个代理对象。开发者可以直接调用这个代理对象的方法,而无需编写实现类,MyBatis 会根据 Mapper 接口方法上的注解或 XML 配置文件来执行相应的 SQL 语句。 - **作用**:简化了数据访问层的开发,减少了样板代码。开发者只需要定义接口和方法,MyBatis 会自动处理 SQL 语句的执行和结果映射。 ##### 模板方法模式 - **体现**:MyBatis 的 `BaseExecutor` 类及其子类使用了模板方法模式。`BaseExecutor` 定义了数据库操作的基本流程和骨架,例如查询、更新等操作的通用步骤,而将一些具体的实现细节(如缓存处理、事务管理等)交给子类去实现。 - **作用**:将通用的操作流程封装在父类中,不同的子类可以根据需要实现特定的步骤,提高了代码的复用性,同时也方便了扩展和维护。 ##### 建造者模式 - **体现**:`SqlSessionFactoryBuilder` 可以看作是建造者模式的应用。它通过一系列的配置参数(如 XML 配置文件、Java 代码配置等)逐步构建出 `SqlSessionFactory` 对象。 - **作用**:将 `SqlSessionFactory` 对象的创建过程进行了封装和抽象,使得创建过程更加灵活和可定制,开发者可以根据不同的需求选择不同的配置方式来构建 `SqlSessionFactory`。 ##### 适配器模式 - **体现**:MyBatis 中的 `ResultSetHandler` 接口及其实现类可以看作是适配器模式的应用。不同的数据库返回的结果集格式可能不同,`ResultSetHandler` 的作用就是将数据库返回的结果集适配成 Java 对象。 - **作用**:将不同格式的结果集统一转换为开发者需要的 Java 对象,使得开发者可以以统一的方式处理不同数据库的查询结果,提高了代码的兼容性和可移植性。 ### 常见的Java算法(了解) | | | | ------------ | ------------------------------------------------------------ | | 简称 | 简介 | | 二分查找法 | 在二分查找方法中,将集合重复地分成两半,并根据关键字是小于还是大于集合的中间元素来在集合的左半部分或右半部分中搜索关键元素。二分查找要求用于查找的内容逻辑上来说是需要有序的,查找的数量只能是一个不能是多个。 | | 冒泡排序算法 | 它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。 | | 插入排序算法 | 通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应的位置并插入。 | | 快速排序算法 | 对冒泡算法的一种改进。是指通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序。整个排序过程可以递归进行,以此达到整个数据变成有序序列。 | | 希尔排序算法 | 希尔排序是插入排序的一种又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法 | | 归并排序算法 | 归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 | | 桶排序算法 | 桶排序也叫箱排序,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到 O(n log n) 下限的影响。 | | 基数排序算法 | 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。 | | 剪枝算法 | 在搜索算法中优化中,剪枝,就是通过某种判断,避免一些不必要的遍历过程,形象的说,就是剪去了搜索树中的某些“枝条”,故称剪枝。应用剪枝优化的核心问题是设计剪枝判断方法,即确定哪些枝条应当舍弃,哪些枝条应当保留的方法。 | | 回溯算法 | 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。 | | 最短路径算法 | 从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径叫做最短路径。解决最短路的问题有以下算法,Dijkstra 算法,Bellman-Ford 算法,Floyd 算法和 SPFA算法等。 | ## **多线程与并发**(重点 高频) ### 创建线程的四种方法(高频) #### 四种方法 1. 继承 Thread类 2. 实现 Runable 接口 3. 实现 Callable 接口(通过泛型确定返回值的类型) 有返回值 4. 使用线程池来创建 [参考 建议阅读](https://mp.weixin.qq.com/s?__biz=MzIwNjg4MzY4NA==&mid=2247507394&idx=2&sn=39cb7f3e0b55820d4c7b60c04a41274e&chksm=971843c9a06fcadfa26860b1c29c267d809d0ecf1d911b1165cfb35a71d615badbf6d6f51fcc&scene=21#wechat_redirect) > **线程池的分类(高薪常问)** > > ![img](assets/QMZ6WBAAXM.png) > > * newCachedThreadPool:创建一个可进行缓存重复利用的线程池 > > > * 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。 > > > > - 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。 > > * newFixedThreadPool:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,线程池中的线程处于一定的量,可以很好的控制线程的并发量 > > * newSingleThreadExecutor : 创建一个单线程的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行。线程池中最多执行一个线程,之后提交的线程将会排在队列中以此执行 > > * newSingleThreadScheduledExecutor:创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期执行 > > * newScheduledThreadPool:创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行 > > * newWorkStealingPool:创建一个带并行级别的线程池,并行级别决定了同一时刻最多有多少个线程在执行,如不传并行级别参数,将默认为当前系统的 CPU 个数 #### Runnable 和 Callable 的区别?(必会) 主要区别: * Runnable 接口 run 方法无返回值,异常且无法捕获处理; ~~~java //Runnable异常demo try { Thread myThread = new Thread(() -> { System.out.println("子线程名称:" + Thread.currentThread().getName()); throw new RuntimeException("我不想干活了"); }); myThread.start(); } catch (Exception ex) { //这里捕捉不到异常的。 System.out.println("Runnable 无法捕捉异常,所以这句话根本不会打印出来"); } ~~~ * Callable 接口 call 方法有返回值,支持泛型, 可以获取异常信息 ~~~java //Callable异常demo try { FutureTask futureTask = new FutureTask(() -> { System.out.println("子线程名称:" + Thread.currentThread().getName()); if (1 == 1) { throw new RuntimeException("我也不想干活了"); } return "我执行完了"; }); Thread thread = new Thread(futureTask); thread.start(); String ret = futureTask.get(); System.out.println(ret); } catch (Exception ex) { //这里是能捕捉到异常的。 System.out.println("Callable 能捕捉异常,所以这句话能打印出来"); } ~~~ #### 为什么需要线程池(了解) 在实际使用中,线程是很占用系统资源的,如果对线程管理不完善的话很容易导致系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有如下好处: * 使用线程池可以重复利用已有的线程继续执行任务,避免线程在创建销毁时造成的消耗 * 由于没有线程创建和销毁时的消耗,可以提高系统响应速度 * 通过线程池可以对线程进行合理的管理,根据系统的承受能力调整可运行线程数量的大小等 #### 线程池的核心参数(高薪常问) 1. 核心线程数(corePoolSize) - 线程池中始终存在的线程数量,即使它们处于空闲状态。 - 当线程数小于核心线程数时,新任务提交到线程池会创建新的线程来处理任务,直到线程数达到核心线程数。 2. 最大线程数(maximumPoolSize) - 线程池中允许的最大线程数量。 - 当工作队列满了之后,线程池会尝试创建新的线程来处理任务,但线程数不会超过最大线程数。 3. 工作队列(workQueue) - 用于保存等待执行的任务的阻塞队列。 - 常见的阻塞队列有ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue等。 > 线程池中的任务队列类型主要有以下几种: > > - 有界队列 > - **特点**:有固定的容量限制,当队列已满时,再向队列中添加任务会导致添加操作阻塞或者失败。 > - **使用场景**:适用于需要严格控制任务数量,防止资源过度消耗的场景。例如,在系统资源有限的情况下,为了避免任务堆积过多导致内存溢出等问题,可以使用有界队列。常见的有界队列实现类有`ArrayBlockingQueue`。 > - 无界队列 > - **特点**:理论上可以容纳无限数量的任务,不会因为队列满而阻塞任务的添加。 > - **使用场景**:当任务产生的速度相对较慢,或者系统资源足够充足,能够处理大量任务时,可以使用无界队列。它可以保证任务不会因为队列满而被拒绝,但需要注意可能会导致内存占用过高的问题。例如`LinkedBlockingQueue`就是一种无界队列。 > - 优先队列 > - **特点**:队列中的任务具有优先级,会按照优先级的高低来执行任务。通常使用优先级队列来确保重要的任务能够优先得到处理。 > - **使用场景**:在一些实时性要求较高或者任务重要性有差异的系统中经常使用。例如,在一个视频会议系统中,音频和视频数据的处理任务可能具有较高的优先级,而一些后台的日志记录等任务优先级较低,就可以使用优先队列来保证关键任务的及时处理。Java 中的`PriorityBlockingQueue`就是一种优先队列的实现。 - 当线程池中的线程数达到核心线程数后,新任务会被添加到工作队列中等待执行。 4. 线程保持活动时间(keepAliveTime) - 当线程数量超过核心线程数时,这是超过核心线程数的线程在终止前等待新任务的最长时间。 - 如果超过这个时间还没有新的任务分配给这些线程,那么这些线程将被终止。 5. 时间单位(unit) - keepAliveTime的时间单位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS等。 6. 线程工厂(threadFactory) - 用于创建新线程的工厂。 - 通过自定义线程工厂,可以为线程设置一些属性,如线程名、是否为守护线程等。 7. 拒绝处理策略(handler) - 当线程池和工作队列都满了之后,新提交的任务无法被处理时所使用的策略。 - 常见的拒绝处理策略有AbortPolicy(抛出RejectedExecutionException异常)、CallerRunsPolicy(由提交任务的线程来运行任务)、DiscardPolicy(丢弃任务)和DiscardOldestPolicy(丢弃队列中最旧的未处理任务)。 > **拒绝策略** > > * ThreadPoolExecutor.AbortPolicy(系统默认): 丢弃任务并抛出RejectedExecutionException 异常,让你感知到任务被拒绝了,我们可以根据业务逻辑选择重试或者放弃提交等策略 > * ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。 > * ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程),通常是存活时间最长的任务,它也存在一定的数据丢失风险 > * ThreadPoolExecutor.CallerRunsPolicy:既不抛弃任务也不抛出异常,而是将某些任务回退到调用者,让调用者去执行它。 这些参数共同决定了线程池的行为和性能。在实际应用中,需要根据任务的特点和系统的需求来合理地配置这些参数,以达到最佳的并发处理效果。 #### 线程池的原理(高薪常问) ![img](assets/QMZ6WBAAXI.png) **基本原理:** 1、任务提交到线程池以后,如果工作线程数小于coreSize,直接交给核心线程执行任务 2、核心线程数都满了,任务放到队列中 3、队列满了,创建核心线程数以外的线程执行任务 4、达到最大线程数,且队列仍然是满的状态,使用拒绝策略 #### 线程池的关闭(了解) 关闭线程池,可以通过 shutdown 和 shutdownNow 两个方法 原理:遍历线程池中的所有线程,然后依次中断 1、shutdownNow 首先将线程池的状态设置为 STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表; 2、shutdown 只是将线程池的状态设置为 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程,继续执行正在执行的任务,直到任务结束. ### 启动一个线程用run还是start 启动一个线程是调用start()方法,使线程就绪状态,以后可以被调度为运行状态,一个线程必须关联一些具体的执行代码,run()方法是该线程所关联的执行代码。 ### 线程具有五中基本状态 ![20191022100239169](assets/20191022100239169.png) * 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread(); * 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行; * 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中; * 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态 * 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期 ### 线程的常用方法的 * join() :t.join()方法只会使主线程进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。 * yield() :t.yield()让当前线程从“运行状态”进入到“就绪状态”,和让其它线程获一起回到起跑线取 cpu 执行权, * wait 和 sleep区别 | name | 释放锁 | 唤醒条件 | 作用对象 | 方法属性 | 使用范围 | | ----- | ------ | --------------------------------------- | --------------- | -------- | -------------------------------- | | wait | 是 | 其它线程调用对象的notify()或notifyAll() | Object 类的方法 | 实例方法 | 同步方法或同步代码块,否则报异常 | | sleep | 否 | 超时或interrupt() | Thread 类的方法 | 静态方法 | 无限制 | ### 为什么 wait() 定义在 object 里,而不是 Thread类里 Java的锁是对象级别的锁,而不是线程级别,所以 wait 是 Object 的方法。sleep 是线程级别的,所以存在于 Thread 类中 ### 为什么 wait() 要放在同步代码块 防止在 cpu 切换线程的时候,其它线程先执行了 notify() ,后执行 wait(),线程永远无法被唤醒, 所以要放在同步代码块 ### sleep()方法和wait()方法的异同点 为什么 wait() 要放在同步代码块中? 同步方法就是在方法前加关键字 synchronized,然后被同步的方法一次只能有一个线程进sleep()方法和wait()方法的异同点 ###### 相同之处: - sleep()方法和wait()方法都是使当前线程进入休眠状态。 - 两者都可以响应interrupt中断,也就是说在线程调用方法进入休眠状态后,如果收到中断信号,都可以进行响应并中断,且都可以抛出InterruptException异常。 ###### 不同之处: - 所属类不同: sleep()方法属于Thread类的静态方法,而wait()方法属于Object类。 为什么 wait() 定义在 object 里,而不是 Thread类’//‘里? Java的锁是对象级别的锁,而不是线程级别,所以 wait() 是 Object 的方法。sleep()是线程级别的,所以存在于 Thread 类中。 - 唤醒方式不同: sleep()方法必须传递一个超时时间的参数,过了这个时间后线程就会自动唤醒,进入就绪状态; ​ 而wait()方法可以不传递参数,此时线程会进入永久休眠,直到另一个线程调用notify()方法或者notifyAll()方法之后,休眠的线程才会被唤醒。也就是说sleep()方法可以 主动唤醒,而不传参的wait()方法只能被动唤醒。 - 语法使用不同: 入,其他线程等待;防止在 cpu 切换线程的时候,其它线程先执行了notify(),后执行 wait(),线程永远无法被唤醒, 所以要将wait()放在同步代码块。 - 释放锁资源不同: 在线程调用wait()方法时,线程会主动释放锁并进入等待状态; ​ 而线程在调用sleep()方法时,线程不会释放锁。也就是说sleep()方法是抱着锁睡的。 - 线程进入状态不同: 线程调用sleep()方法会进入TIMED_WAITING有时限等待状态;而调用无参数的wait()方法,线程会进入WAITING无时限等待状态。 ### 线程锁(高频) #### 多线程同步有哪几种方法? > 介绍Synchronized和Lock时,说明只适用于单体结构项目,在分布中,这种加 锁无效,引出在分布式中使用分布式锁: > > * Redis > * redission Synchronized关键字,Lock锁实现,分布式锁等。 #### Synchronized 用过吗,其原理是什么 Synchronized 是 Java 中用于实现同步的关键字,它可以用于方法和代码块。Synchronized 的原理是,它会使用对象的内置锁(也称为监视器锁)来实现同步,每个对象都有一把内置锁,当一个线程访问一个同步代码块时,它会尝试获取这个锁,如果锁被其他线程持有,则该线程将被阻塞,直到锁被释放。 Synchronized 有以下几个特点: * 互斥性:Synchronized 保证同一时刻只有一个线程可以获取锁,并且只有该线程可以执行同步代码块中的代码。 * 可重入性:同一个线程可以多次获取同步锁而不会被阻塞,这样可以避免死锁的发生。 * 独占性: 如果一个线程获得了对象的锁,则其他线程必须等待该线程释放锁之后才能获取锁。 * 缺点:非公平锁 ,当锁被释放后,任何一个线程都有机会竞争得到锁,这样做的目的是提高效率,但缺点是可能产生线程饥饿现象。 除了使用 Synchronized 关键字之外,Java 还提供了一些其他的同步机制,例如 ReentrantLock(rɪˈentrənt)、Semaphore、CountDownLatch 等。在选择同步机制时,需要根据具体的应用场景和需求来选择合适的同步方式。 #### synchronized 与 Lock 的区别 | 类别 | synchronized | lock | | -------- | -------------------------- | ------------------------------------------ | | 存在层次 | Java关键字,jvm层面 | 是一个类 | | 锁的释放 | JVM 将确保锁会获得自动释放 | lock 必须在 finally 块中释放 | | 锁状态 | 无法判断 | 可以判断 | | 锁类型 | 可重入,不可中断、非公平 | 可重入、可判断、可公平(两者皆可默认非公平) | | 性能 | 少量同步 | 大量同步 | * synchronized是关键字,是JVM层面的,而Lock是一个接口,是JDK提供的API。 * 当一个线程获取了synchronized锁,其他线程便只能一直等待直至占有锁的线程释放锁。当发生以下情况之一线程才会释放锁: a.占有锁的线程执行完了该代码,然后释放对锁的占有。 b.占有锁线程执行发生异常,此时JVM会让线程自动释放锁。 c.占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。 但是如果占有锁的线程由于要等待IO或者因为其他原因(比如调用sleep方法)而使线程阻塞了,但是又没有释放锁,那么线程就只能一直等待,那么这时我们可能需要一种可以不让线程无期限的等待下去的方法,比如只等待一定的时间(tryLock(long time, TimeUnit unit)或者能被人为中断lockInterrup0tibly(),这种情况我们需要Lock。 * 当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象,但是如果采用synchronized进行同步的话,就会导致当多个线程都只是进行读操作时也只有获取锁的线程才能进行读操作,其他线程只能等待锁释放后才能读,Lock则可以实现当多个线程都只是进行读操作时,线程之间不会发生冲突,例如:ReentrantReadWriteLock()。 > 1. 读 和写 冲突 > 2. 写和写 冲突 > 3. 读读 不冲突 * 可以通过Lock得知线程有没有成功获取到锁 (例如:ReentrantLock) ,但这个是synchronized无法办到的。 * 锁属性上的区别:synchronized是不可中断锁和非公平锁,ReentrantLock可以进行中断操作并别可以控制是否是公平锁。 * synchronized能锁住方法和代码块,而Lock只能锁住代码块。 * synchronized无法判断锁的状态,而Lock可以知道线程有没有拿到锁。 * 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时,此时Lock的性能要远远优于synchronized。 [参考](https://blog.csdn.net/xyy1028/article/details/107333451) #### ReentrantLock的底层原理 `ReentrantLock`是 Java 里用于实现线程同步的一个类,它出自`java.util.concurrent.locks`包,提供了比`synchronized`关键字更丰富的锁操作。其底层原理主要基于 AQS(Abstract Queued Synchronizer)框架,下面为你详细介绍: ##### 1. AQS 框架概述 AQS 是一个用于构建锁和同步器的框架,很多同步类的实现都依赖于它,像`ReentrantLock`、`CountDownLatch`等。AQS 内部维护了一个`volatile int state`变量,用于表示同步状态,还维护了一个 FIFO(先进先出)的线程等待队列。 ##### 2. `ReentrantLock`的结构 `ReentrantLock`内部有一个静态抽象类`Sync`,它继承自`AbstractQueuedSynchronizer`,并且有两个具体的实现类:`NonfairSync`(非公平锁)和`FairSync`(公平锁)。 ##### 3. 非公平锁(`NonfairSync`)的加锁原理 - **尝试直接获取锁**:当线程调用`lock()`方法时,`ReentrantLock`会先尝试直接将`state`状态从 0 变为 1,如果成功,就表示获取到了锁,当前线程成为锁的持有者。 - **进入 AQS 队列**:若尝试失败,线程会进入 AQS 的等待队列。 - **重入情况**:如果当前线程已经是锁的持有者,再次获取锁时,`state`的值会加 1,这体现了锁的可重入性。 ##### 4. 公平锁(`FairSync`)的加锁原理 - **检查队列**:公平锁在获取锁时,会先检查 AQS 队列中是否有其他线程在等待,如果有,当前线程会进入队列尾部等待。 - **尝试获取锁**:若队列中没有其他线程等待,才会尝试将`state`状态从 0 变为 1。 ##### 5. 解锁原理 - **释放锁**:当线程调用`unlock()`方法时,会将`state`的值减 1。 - **完全释放**:若`state`的值减为 0,就表示锁被完全释放,此时会唤醒 AQS 队列中等待的线程。 ### 多线程如何获取异步执行结果 在 Java 中,有多种方式可以在多线程环境下获取异步执行的结果,下面分别介绍使用`Future`接口和`CompletableFuture`类来实现的方法。 ##### 使用`Future`接口 `Future`接口代表异步计算的结果。你可以使用`ExecutorService`提交任务并获取`Future`对象,之后通过该对象获取任务的执行结果。 ```java import java.util.concurrent.*; public class FutureExample { public static void main(String[] args) { // 创建一个线程池 ExecutorService executor = Executors.newSingleThreadExecutor(); // 提交一个任务并获取 Future 对象 Future future = executor.submit(() -> { // 模拟耗时操作 Thread.sleep(2000); return 42; }); try { // 检查任务是否完成 if (!future.isDone()) { System.out.println("任务还未完成,等待中..."); } // 获取任务的结果 Integer result = future.get(); System.out.println("任务结果: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } finally { // 关闭线程池 executor.shutdown(); } } } ``` 上述代码创建了一个单线程的线程池,提交了一个任务到线程池并获得`Future`对象。`future.get()`方法会阻塞当前线程,直到任务完成并返回结果。 ##### 使用`CompletableFuture`类 `CompletableFuture`是 Java 8 引入的一个强大的异步编程工具,它可以更方便地处理异步任务和结果。 ```java import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class CompletableFutureExample { public static void main(String[] args) { // 创建一个异步任务 CompletableFuture future = CompletableFuture.supplyAsync(() -> { try { // 模拟耗时操作 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } return 42; }); try { // 获取任务结果 Integer result = future.get(); System.out.println("任务结果: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } } ``` 此代码通过`CompletableFuture.supplyAsync`方法创建了一个异步任务,该任务会在后台线程中执行。`future.get()`方法会阻塞当前线程,直至任务完成并返回结果。 这两种方式都能实现多线程异步执行并获取结果,`Future`接口适合简单的异步任务,而`CompletableFuture`类则更适合复杂的异步操作和组合任务。 ### 线程锁的类型(中) #### 悲观锁(Pessimistic Locking) 悲观锁假设最坏的情况,即认为冲突一定会发生,因此在操作数据前就进行加锁处理,确保数据加锁期间不会被其他线程修改。Java中常见的悲观锁实现方式包括: 1. **synchronized关键字**:Java的内置锁,可用于方法或代码块上,确保同一时刻只有一个线程能执行该段代码。 2. **ReentrantLock**:`java.util.concurrent.locks`包下的锁,提供了比synchronized更灵活的锁操作,如尝试非阻塞地获取锁、可中断地获取锁以及超时获取锁等。 #### 乐观锁(Optimistic Locking) 乐观锁假设最好的情况,即认为冲突不会经常发生,因此在数据提交更新时,才会真正对数据加锁。乐观锁通常是通过版本号(version)或时间戳(timestamp)来实现的,在更新数据时,会检查版本号或时间戳是否发生了变化,从而判断是否发生了并发修改。 #### 公平锁(Fair Lock) **定义**: 公平锁是一种按照请求顺序授予锁的机制。即先请求锁的线程会先获得锁,后请求锁的线程会后获得锁。这种锁通过维护一个队列来管理等待的线程,确保每个线程都能公平地获取到锁。 **特点**: 1. **避免饥饿**:所有线程都有机会获得锁,不会出现某些线程长期得不到锁的情况。 2. **可预测性**:锁的获取是按顺序进行的,具有较好的可预测性。 3. **性能开销**:由于需要维护一个队列,公平锁在管理上有一定的性能开销。 4. **上下文切换增加**:由于公平锁可能需要频繁地切换线程,导致上下文切换的次数增加,影响性能。 **实现**: 在Java中,`ReentrantLock`类可以通过构造函数指定为公平锁(传入`true`作为参数),而在Go语言中,标准库不直接提供公平锁的实现,但可以通过自定义方式(如使用条件变量`sync.Cond`)来实现。 #### 非公平锁(Unfair Lock) **定义**: 非公平锁是一种不按照请求顺序授予锁的机制。即任何线程都有可能在任何时候获得锁,而不考虑请求顺序。这种锁通常会优先考虑当前已经持有锁的线程,以提高系统的吞吐量。 **特点**: 1. **高性能**:由于没有队列管理的开销,非公平锁通常性能较高,特别是在高并发场景下。 2. **减少上下文切换**:非公平锁可以减少线程之间的上下文切换,提升效率。 3. **可能导致饥饿**:某些线程可能长时间得不到锁,导致线程饥饿。 4. **不可预测性**:锁的获取是随机的,具有较低的可预测性。 **实现**: 在Java中,`ReentrantLock`类默认实现的是非公平锁(如果不指定构造函数的参数),而在Go语言中,`sync.Mutex`默认实现的是非公平锁。 #### 公平锁与非公平锁总结 | | 公平锁 | 非公平锁 | | -------------- | ------------------------------- | ------------------------- | | **定义** | 按照请求顺序授予锁 | 不按照请求顺序授予锁 | | **避免饥饿** | 是 | 否(可能导致饥饿) | | **可预测性** | 高 | 低 | | **性能开销** | 较高(维护队列) | 较低(无队列管理) | | **上下文切换** | 较多 | 较少 | | **Java实现** | `ReentrantLock(true)` | `ReentrantLock()`(默认) | | **Go实现** | 需要自定义(如使用`sync.Cond`) | `sync.Mutex`(默认) | 公平锁和非公平锁各有优缺点,选择哪种锁机制取决于具体的应用场景和需求。如果需要确保所有线程都能公平地访问共享资源,避免饥饿现象,那么公平锁是更好的选择。然而,如果对性能有较高要求,且可以容忍一定程度的不可预测性和可能的饥饿现象,那么非公平锁可能更适合。 #### 可重入锁 可重入锁(Reentrant Lock),也称为递归锁,是一种支持同一个线程多次获取同一个锁的机制。即同一个线程可以重复获取一个锁n次,同样的,在释放时也需要释放n次。这种锁在Java中非常重要,因为它允许一个线程在持有锁的情况下再次获取该锁,而不会导致死锁。 ##### 一、概念 - **可重入性**:指一个线程在持有某个锁的情况下,可以多次重新获取该锁。这对于递归函数或方法中的同步块特别有用,因为它们可能会再次调用自己或同步的其他方法。 - **synchronized与ReentrantLock**:Java中的`synchronized`关键字和`ReentrantLock`类都是可重入锁的实现。它们允许同一个线程在持有锁的情况下多次进入同步块或方法,而不会导致死锁。 ##### 二、特点 1. **线程安全**:可重入锁保证了同一时刻只有一个线程可以执行同步代码块,从而避免了多线程环境下的数据竞争和状态不一致问题。 2. **灵活性**:与`synchronized`相比,`ReentrantLock`提供了更多的灵活性,如尝试非阻塞地获取锁(`tryLock()`)、可中断地获取锁(`lockInterruptibly()`)、以及超时获取锁(`tryLock(long timeout, TimeUnit unit)`)等。 3. **可重入性**:同一个线程可以多次获取同一个锁,这对于递归调用或复杂的同步逻辑非常有用。 ##### 三、实现原理 - **synchronized**:每个对象都有一个内部锁(也称为监视器锁)。当一个线程进入同步代码块时,它会自动获取该对象的锁。如果线程再次进入该对象的其他同步代码块,它会再次获取锁,并且JVM会跟踪锁的获取次数(通过锁计数器)。当线程退出所有同步代码块时,锁计数器会递减,直到为0时释放锁。 - **ReentrantLock**:`ReentrantLock`通过内部的一个状态(state)和一个所有者(owner)线程来管理锁的可重入性。当线程首次获取锁时,它会将锁的所有者设置为自己,并将状态设置为1。如果线程再次获取锁,状态会递增。当线程释放锁时,状态会递减,并且如果状态为0,则锁会被释放并允许其他线程获取。 ##### 四、应用场景 - **递归调用**:在递归函数中,可能会多次调用自己或同步的其他方法,此时可重入锁非常有用。 - **复杂同步逻辑**:在复杂的同步逻辑中,可能需要多次获取和释放锁,可重入锁允许这种操作而不会导致死锁。 - **性能优化**:在某些情况下,使用`ReentrantLock`可能比`synchronized`提供更好的性能,因为它提供了更多的灵活性和控制。 ##### 五、注意事项 - 在使用可重入锁时,必须确保在`finally`块中释放锁,以避免死锁。 - 尽量避免在持有锁的情况下执行耗时的操作,以免影响系统的并发性能。 - 在选择`synchronized`和`ReentrantLock`时,应根据具体场景和需求进行选择。如果不需要`ReentrantLock`提供的额外功能,建议使用`synchronized`,因为它更简单且性能也相当。 #### 读写锁 读写锁(Read-Write Lock)是一种并发控制机制,用于在读多写少的场景下提供更好的性能。它允许多个线程同时读取共享数据,但在写操作时需要互斥地独占访问,从而在保证数据一致性的同时提高系统的并发性。以下是关于读写锁的详细介绍: ##### 一、基本概念 读写锁由读锁和写锁两部分组成: - **读锁**:允许多个线程同时获取,因为读操作本身是线程安全的。当读锁被多个线程持有时,其他线程仍然可以获取读锁进行读取操作,但不能获取写锁进行写入操作。 - **写锁**:是互斥锁,不允许多个线程同时获取。当写锁被一个线程持有时,其他线程无法获取读锁或写锁进行任何操作,直到写锁被释放。 ##### 二、特点 读写锁的主要特点可以归纳为以下几点: 1. **读读不互斥**:多个线程可以同时获取读锁进行读取操作。 2. **读写互斥**:读锁和写锁是互斥的,即当有一个线程持有写锁时,其他线程无法获取读锁或写锁。 3. **写写互斥**:多个线程不能同时获取写锁进行写入操作。 4. **公平性选择**:读写锁支持非公平性(默认)和公平性的锁获取方式。非公平锁的吞吐量较高,而公平锁则确保所有线程按照请求锁的顺序来获取锁。 5. **重入性**:支持重入,即同一个线程可以多次获取读锁或写锁,避免死锁。 6. **锁降级**:遵循获取写锁、获取读锁再释放写锁的顺序,写锁可以降级为读锁,提升并发性能。但锁升级(在持有读锁的情况下获取写锁)是不被支持的。 ##### 三、实现方式 在Java中,读写锁通过`ReentrantReadWriteLock`类实现。该类提供了读锁(`ReadLock`)和写锁(`WriteLock`)的获取和释放方法。使用`ReentrantReadWriteLock`时,可以通过调用`readLock()`和`writeLock()`方法来分别获取读锁和写锁。 ##### 四、使用场景 读写锁适用于读多写少的场景,如缓存系统、数据库访问、文件系统操作、消息队列等。在这些场景中,多个线程可能需要同时读取共享数据,但写入操作相对较少。使用读写锁可以允许多个线程同时进行读取操作,提高并发性能,同时确保在写入操作时数据的一致性和完整性。 ##### 五、注意事项 - 在使用读写锁时,需要注意读写操作的互斥性,避免在持有写锁的情况下进行读操作,或在持有读锁的情况下进行写操作。 - 在释放锁时,应确保在`finally`块中释放锁,以避免死锁的发生。 - 根据具体场景选择合适的锁获取方式(公平锁或非公平锁),以优化系统性能。 总的来说,读写锁是一种高效的并发控制机制,适用于读多写少的场景。通过合理地使用读写锁,可以在保证数据一致性的同时提高系统的并发性能。 #### 互斥锁 互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性 #### 自旋锁(Spinlock) 自旋锁(Spinlock)是一种用于多线程编程中的同步机制,它采用循环等待的方式尝试获取锁,直到锁被释放为止。以下是关于自旋锁的详细解释: ##### 定义 当一个线程尝试去获取某一把锁的时候,如果这个锁已经被另外一个线程占有了,那么此线程就无法获取这把锁,该线程会等待,间隔一段时间后再次尝试获取。这种采用循环加锁,等待锁释放的机制就称为自旋锁。 ##### 工作原理 自旋锁的工作原理可以简单概括为:线程在尝试获取锁时,如果发现锁已被其他线程占用,则会进入一个循环中,不断检查锁的状态,直到锁被释放为止。在这个过程中,线程会保持运行状态,并不断地进行空转(即执行无意义的循环),而不是被阻塞或挂起。 ##### 特点 1. **非阻塞**:自旋锁在获取锁之前不会使线程进入阻塞状态,从而避免了线程切换和上下文切换的开销。 2. **忙等待**:由于线程在获取锁之前会不断循环检查锁的状态,这种机制被称为忙等待(Busy Waiting)。虽然这种方式可以减少线程切换的开销,但如果锁被长时间持有,则会导致CPU资源的浪费。 3. **适用场景**:自旋锁适用于锁持有时间非常短,且线程竞争不激烈的场景。在这种情况下,自旋锁的性能通常优于其他类型的锁。 ##### 优缺点 **优点**: - 减少了线程阻塞和唤醒的开销,提高了响应速度。 - 在锁持有时间较短且线程竞争不激烈的场景下,性能较好。 **缺点**: - 如果锁被长时间持有,则会导致CPU资源的浪费。 - 在多线程竞争激烈的情况下,自旋锁的性能会急剧下降。 ##### 应用场景 自旋锁通常用于以下场景: - 锁持有时间非常短,且线程竞争不激烈的场景。 - 需要快速响应的场景,如中断处理、底层驱动等。 ##### 注意事项 - 在使用自旋锁时,需要仔细评估锁的持有时间和线程竞争情况,以避免造成CPU资源的浪费。 - 在多线程竞争激烈的情况下,应考虑使用其他类型的锁,如互斥锁(Mutex)等。 ##### Java中的自旋锁 在Java中,虽然没有直接提供名为“自旋锁”的类,但可以通过原子类(如`AtomicReference`)和循环检查的方式来实现自旋锁的功能。此外,Java虚拟机(JVM)在实现某些锁(如偏向锁、轻量级锁)时,也会采用类似自旋锁的机制来优化性能。 ##### 总结 自旋锁是一种通过循环等待来尝试获取锁的同步机制,它适用于锁持有时间非常短且线程竞争不激烈的场景。然而,在使用自旋锁时需要注意其可能导致的CPU资源浪费问题,并根据实际情况选择合适的锁机制。 ### 什么是线程死锁?死锁如何产生?如何避免线程死锁?(中) 例如,假设线程 A 持有资源 X,并等待资源 Y,而线程 B 持有资源 Y,并等待资源 X,这时候就会出现死锁。 #### 死锁 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。 当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。 当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。 #### 死锁的产生的一些特定条件 Java 中,死锁产生的一些特定条件通常包括以下四个方面: 1. 互斥条件:一个资源一次只能被一个线程持有,如果其他线程想要获取该资源,就必须等待该线程释放该资源。 2. 保持条件:一个线程请求资源时,如果已经持有了其他资源,就可以保持对这些资源的控制,直到满足所有资源的要求才释放。 3. 不剥夺条件:已经分配的资源不能被其他线程剥夺,只能由持有资源的线程释放。 4. 环路等待条件:多个线程形成一种循环等待的关系,每个线程都在等待其他线程所持有的资源,从而导致死锁的产生。 当以上条件同时满足时,就可能会出现死锁的情况。为了避免死锁,需要在设计时遵循一定的规范和原则,例如尽量避免嵌套锁,确保同步代码块执行时间尽可能短等,同时也可以使用专门的工具进行死锁检测和分析,帮助我们找到死锁的根本原因并进行相应的优化和调整。 #### 如何避免 要避免线程死锁,可以采取以下几种方法: 1. 尽量避免使用多个锁,尽量使用一个锁或者使用更加高级的锁,例如读写锁或者 ReentrantLock。 2. 确保同步代码块的执行时间尽可能短,这样可以减少线程等待时间,从而避免死锁的产生。 3. 使用尝试锁,通过 ReentrantLock.tryLock() 方法可以尝试获取锁,如果在规定时间内获取不到锁,则放弃锁。 4. 避免嵌套锁,如果需要使用多个锁,请确保它们的获取顺序是一致的,这样可以避免死锁。 ### volatile关键字的作用(中) > [参考](https://zhuanlan.zhihu.com/p/633426082) - 保证变量的可见性 volatile关键字的作用就是保证共享变量的**可见性**。什么是可见性呢,就是一个线程读变量,总是能读到它在内存中的最新的值,也就是说不同的线程看到的一个变量的值是相同的。CPU都是有缓存的,volatile能让行缓存无效,因此能读到内存中最新的值。 - 保证赋值操作的原子性 原子性就是不能被线程调度打断的操作,是线程安全的操作,对于原子性操作,即使在多线程环境下,也不用担心线程安全问题或者数据不一致的问题。有些变量的赋值本身就是原子性的,比如对boolean,对int的赋值,但是像对于long或者double则不一定,如果是32位的处理器,对于64位的变量的操作可能会被分解成为二个步骤:高32位和低32位,由此可能会发生线程切换,从而导致线程不安全。如果变量声明为volatile,那么虚拟机会保证赋值是原子的,是不可被打断的。 - 禁止指令重排 正常情况下,虚拟机会对指令进行重排,当然是在不影响程序结果的正确性的前提下。volatile能够在一定程度上禁止虚拟机进行指令重排。还有就是对于volatile变量的写操作,保证是在读操作之前完成,假设线程A来读变量,刚好线程B正在写变量,那么虚拟机会保证写在读之前完成。 比如: ~~~java private volatile boolean flag; public void setFlag(boolean flag) { this.flag = flag; } public void getFlag() { return flag; } ~~~ 假设线程A来调用setFlag(true),线程B同时来调用getFlag,对于一般的变量,是无法保证B能读到A设置的值的,因为它们执行的顺序是未知的。但是像上面,加上volatile修饰以后,虚拟机会保证,线程A的写操作在线程B的读操作之前完成,换句话,B能读到最新的值。当然了,用锁机制也能达到同样的效果,比如在方法前面都加上synchronized关键字,但是性能会远不如使用volatile。 > ## **volatile的内存原理** > > 知道了volatile有什么用,怎么用以后,可以了解的更深一点,以加深理解。但要搞懂,就必须先要搞懂它的背景以及背景的背景: > > ### **并发的基本概念** > > - 原子性 > 一个或者多个操作(赋值也好,运算也好)不能被线程调度打断,要么一次性执行完,要么就不执行。 > - 可见性 > 现代处理器是多核心的,或者多CPU的,但是主存(通常意义上的操作系统内存,或者物理内存)却是在CPU之间共享的。多核心处理的优势在于,从机器级别支持多线程并发,而且为了弥补主存与CPU核心之间的速度差异,便有了CPU核心缓存,因此,每个CPU核心(或者说每个线程)是有独立的内存的。这样就带来了可见性的问题,同一个变量c,A线程操作的是c在A线程的缓存中的值,B操作的是c在B的缓存中值,也就是说最新的变量的值对于其他线程是不可见的,这就有了可见性的问题。 > - 有序性 > 对于单线程来说,程序的执行顺序就是按照代码的书写顺序,从上到下,从左到右(分号分隔写在同一行时)。但是多线程情况就不一定了,线程调度器随时可能打断某一程,执行其他线程。这就导致了,程序并不是按照预期的顺序执行的,导致结果跟预期不一致。 **注意**:这里的顺序,并不是严格的指令执行的顺序,而且从结果正确性的角度来看的,比如: > > ```text > int a = 10; > int b = a + 1; > ``` > > 这段代码的有序性的意思是:当执行到第二条语句,只要a的值是10就可以了,至于a = 10它究竟是否是在下面语句前执行,并不关心。但是,除了a = 10语句外,没有其他的方式能让a变成10,所以,肯定是执行了语句了才能把a变成10。说起来比较绕,这个例子也过于简单。但是可以这么简单的理解为:单线程情况下,程序是按书写的顺序来执行的,更准确的说法是程序员预期的顺序来执行的。但多线程会打破这种有序性。 ## JVM (重点 高频) ### JVM内存模型(重要) [参考](https://doocs.github.io/jvm/01-jvm-memory-structure.html) ![jvm-memory-structure](assets/jvm-memory-structure.jpg) JDK8中,内存模型分为5部分: 线程私有的: * 程序计数器 * 本地方法栈 * java虚拟机栈 线程共享的: * 堆 * 元数据区(在 jdk7中叫方法区、永久区,jdk8后由元数据区替代) #### 程序计数器 ##### 定义 程序计数器是一块较小的内存空间,是当前线程正在执行的那条字节码指令的地址。若当前线程正在执行的是一个本地方法,那么此时程序计数器为`Undefined` > 线程私有的(每个线程都有一个自己的程序计数器),是一个指针.代码运行,执行命令.而每个命令都是有行号的,会使用程序计数器来记录命令执行到多少行了.记录代码执行的位置. ##### 作用 - 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。 - 在多线程情况下,程序计数器记录的是当前线程执行的位置,从而当线程切换回来时,就知道上次线程执行到哪了。 ##### 特点 - 是一块较小的内存空间。 - 线程私有,每条线程都有自己的程序计数器。 - 生命周期:随着线程的创建而创建,随着线程的结束而销毁。 - 是唯一一个不会出现 `OutOfMemoryError` 的内存区域。 Java 虚拟机栈(Java 栈) Java 虚拟机栈是描述 Java 方法运行过程的内存模型。 Java 虚拟机栈会为每一个即将运行的 Java 方法创建一块叫做“栈帧”的区域,用于存放该方法运行过程中的一些信息,如: - 局部变量表 - 操作数栈 - 动态链接 - 方法出口信息 - ...... ![jvm-stack](assets/jvm-stack.jpg) #### 本地方法栈(C 栈) 本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。它与 Java 虚拟机栈实现的功能类似,只不过本地方法栈是描述本地方法运行过程的内存模型。 #### 堆 是用来存放对象的内存空间,`几乎`所有的对象都存储在堆中。 jvm-memory ##### 堆的特点 - 线程共享,整个 Java 虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java 虚拟机栈、本地方法栈都是一个线程对应一个。 - 在虚拟机启动时创建。 - 是垃圾回收的主要场所。 - 堆可分为新生代(Eden 区:`From Survior`,`To Survivor`)、老年代。 - Java 虚拟机规范规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。 - 关于 Survivor s0,s1 区: 复制之后有交换,谁空谁是 to。 ##### 新生代与老年代 - **老年代比新生代生命周期长**。 - 新生代与老年代空间默认比例 `1:2`:JVM 调参数,`XX:NewRatio=2`,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3。 - HotSpot 中,Eden 空间和另外两个 Survivor 空间缺省所占的比例是:`8:1:1`。 - **几乎所有的 Java 对象都是在 Eden 区被 new 出来的,Eden 放不了的大对象,就直接进入老年代了**。 > Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。 > > 当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。 > > SurvivorFrom 区:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。 > > SurvivorTo 区:保留了一次 MinorGC 过程中的幸存者。 > > Eden 和 S0,S1 区的比例为 8 : 1 : 1 > > 幸存者 S0,S1 区:复制之后发生交换,谁是空的,谁就是 SurvivorTo 区 > > JVM 每次只会使用 eden 和其中一块 survivor 来为对象服务,所以无论什么时候,都会有一块 survivor 是空的,因此新生代实际可用空间只有 90% > > 当 JVM 无法为新建对象分配内存空间的时候 (Eden 满了),Minor GC 被触发。因此新生代空间占用率越高,Minor GC 越频繁。 ##### 对象分配过程 - new 的对象先放在 Eden 区,大小有限制 - 如果创建新对象时,Eden 空间填满了,就会触发 Minor GC,将 Eden 不再被其他对象引用的对象进行销毁,再加载新的对象放到 Eden 区,特别注意的是 Survivor 区满了是不会触发 Minor GC 的,而是 Eden 空间填满了,Minor GC 才顺便清理 Survivor 区 - 将 Eden 中剩余的对象移到 Survivor0 区 - 再次触发垃圾回收,此时上次 Survivor 下来的,放在 Survivor0 区的,如果没有回收,就会放到 Survivor1 区 - 再次经历垃圾回收,又会将幸存者重新放回 Survivor0 区,依次类推 - 默认是 15 次的循环,超过 15 次,则会将幸存者区幸存下来的转去老年区 jvm 参数设置次数 : -XX:MaxTenuringThreshold=N 进行设置 - 频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间搜集 ##### Full GC /Major GC 触发条件 - 显示调用`System.gc()`,老年代的空间不够,方法区的空间不够等都会触发 Full GC,同时对新生代和老年代回收,FUll GC 的 STW 的时间最长,应该要避免 - 在出现 Major GC 之前,会先触发 Minor GC,如果老年代的空间还是不够就会触发 Major GC,STW 的时间长于 Minor GC ###### Minor GC(新生代 GC) - **回收区域**:只清理新生代(Eden 区、Survivor 区)。 - **触发条件**:当 Eden 区空间不足,无法为新对象分配内存时就会触发。 - 特点 - 发生频率较高,不过速度很快。 - 采用复制算法,把存活的对象移到 Survivor 区或者老年代。 - 发生 Minor GC 时,会出现 “Stop The World”(STW)现象,但暂停时间很短。 ###### Major GC(老年代 GC) - **回收区域**:只针对老年代进行回收。 - **触发条件**:老年代空间不足时会触发,比如大对象直接进入老年代,或者 Minor GC 后对象晋升到老年代导致空间不够。 - 特点 - 速度比 Minor GC 慢,因为老年代对象存活时间长,回收工作更复杂。 - 不同垃圾回收器(如 CMS、G1)的实现方式有所不同。 - 同样会导致 STW,而且暂停时间比 Minor GC 长。 ###### Full GC(全局 GC) - **回收区域**:会清理整个堆内存,包括新生代、老年代和永久代(元空间)。 - 触发条件 - 老年代空间不足。 - 永久代(元空间)空间不足。 - 调用`System.gc()`方法(不过 JVM 可能会忽略这个调用)。 - CMS GC 出现 Concurrent Mode Failure。 - 堆碎片过多,导致无法分配连续内存块。 - 特点 - 是最彻底的垃圾回收方式,但也是最慢的。 - 会导致较长时间的 STW,可能会严重影响应用程序的性能。 ###### **三者的区别** | **对比项** | **Minor GC** | **Major GC** | **Full GC** | | ---------------- | --------------- | -------------- | ------------------------------------------------------ | | **回收区域** | 只回收新生代 | 只回收老年代 | 回收整个堆(包括新生代、老年代、永久代 / 元空间) | | **触发条件** | Eden 区空间不足 | 老年代空间不足 | 多种条件(如老年代 / 元空间不足、调用`System.gc()`等) | | **STW 暂停时间** | 短 | 较长 | 最长 | | **频率** | 高 | 低 | 更低 | | **速度** | 快 | 较慢 | 最慢 | ###### **GC 优化建议** - **减少 Full GC**:避免创建过大的对象,合理设置老年代和新生代的大小。 - 选择合适的垃圾回收器 - 对于吞吐量优先的应用,可以选择 Parallel GC。 - 对于响应时间敏感的应用,CMS 或 G1 是比较好的选择。 - **监控 GC 情况**:使用工具(如`jstat`、`jvisualvm`、G1 日志)来监控 GC 频率和暂停时间,以便进行优化。 理解这三种 GC 的区别,有助于你更好地调优 JVM 参数,提升应用程序的性能。 #### **元空间(元数据区、Metaspace)** 元空间是 JDK1.8 及之后,HotSpot 虚拟机对方法区的新实现。 元空间不在虚拟机中,而是直接用物理(本地)内存实现,不再受 JVM 内存大小参数限制,JVM 不会再出现方法区的内存溢出问题,但如果物理内存被占满了,元空间也会报 OOM 元空间和方法区不同的地方在于编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分: * 类元信息(Class)类元信息在类编译期间放入元空间,里面放置了类的基本信息:版本、字段、方法、接口以及常量池表 * 常量池表:主要存放了类编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中 * 运行时常量池(Runtime Constant Pool)运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些; 运行时常量池具备动态性,可以添加数据,比较多的使用就是 String 类的 intern() 方法 > | **区域** | **所属版本** | **内存位置** | **核心作用** | **常见问题** | > | --------------------- | ---------------------- | --------------------------- | -------------------------------- | ------------------------------- | > | 方法区 | JVM 规范(所有版本) | 逻辑概念(由具体 JVM 实现) | 存储类元数据、常量、静态变量等 | 无直接 OOM,依赖实现方式 | > | 永久区(Perm Gen) | JDK 1.7 及之前 | Java 堆(独立区域) | 早期方法区的实现,存储类元数据等 | `PermGen space` 内存溢出 | > | 元数据区(MetaSpace) | JDK 1.8 及之后 | 本地内存(Native Memory) | 方法区的新实现,存储类元数据 | 需注意本地内存不足 | > | 直接内存 | 所有版本(需手动使用) | 本地内存 | 优化 I/O 性能,直接操作非堆内存 | `Direct buffer memory` 内存溢出 | ### GC(Garbage Collection,垃圾回收) GC(Garbage Collection,垃圾回收)是自动管理程序内存的机制,它帮助程序员自动释放那些不再被程序所使用的内存对象,从而避免内存泄漏等问题。以下是对GC垃圾回收的详细解释: #### 一、GC的基本概念 GC是Java等编程语言中的一项重要特性,通过GC,程序员不需要手动分配和释放内存,大大简化了内存管理的复杂性。在GC中,垃圾指的是那些不再被程序所使用的内存对象,这些对象不再被访问,也不再对程序的正确性产生任何影响。GC的任务是找到这些垃圾对象,并释放它们所占用的内存空间,以便程序能够继续使用这些内存空间。 #### 二、垃圾对象的判断算法 在GC中,判断一个对象是否为垃圾对象主要有两种算法: 1. **引用计数算法**:给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1。任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”。然而,这种算法存在内存空间浪费严重和循环引用的问题,因此Java并不采用此算法。 2. **可达性分析算法**:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即GC Roots到这个对象不可达),证明此对象是不可用的。在Java中,可作为GC Roots的对象包括虚拟机栈(栈帧中的本地变量表)中引用的对象、本地方法栈中JNI(Native方法)引用的对象等。 > GC Roots 是一组必须活跃的引用,包括以下几类: > > 1. **虚拟机栈(栈帧中的本地变量表)中引用的对象** > 例如,方法中声明的局部变量所引用的对象。 > 2. **方法区中类静态属性引用的对象** > 例如,类的静态成员变量所指向的对象。 > 3. **方法区中常量引用的对象** > 例如,字符串常量池(String Pool)中的字符串对象。 > 4. **本地方法栈(Native 方法)中引用的对象** > 例如,通过 Native 方法调用本地代码(如 C/C++)所引用的对象。 > 5. **JVM 内部引用(如类加载器、异常对象 `System.out`、`System.in` 等)** > 这些对象在 JVM 启动时就存在,不会被回收。 > 6. **被同步锁(`synchronized` 关键字)持有的对象** > 反映 Java 虚拟机内部的同步机制。 > 7. **JVM 编译器优化时的临时对象** > 某些 JIT 编译器优化阶段会保留的对象引用。 > > ### **GC Roots 的作用** > > - **判断对象存活的起点**:GC 机制通过从 GC Roots 出发的引用链遍历,标记所有可达对象(这些对象不会被回收),未被标记的对象则被判定为可回收对象。 > - **避免循环引用的漏判**:即使两个对象互相引用(循环引用),但只要它们都无法从 GC Roots 到达,仍会被判定为可回收。 > > ### **常见应用场景** > > 1. **垃圾回收算法的基础** > 主流的垃圾回收算法(如标记 - 清除、标记 - 复制、标记 - 整理)均基于 GC Roots 进行可达性分析。 > 2. **内存泄漏排查** > 若对象无法被回收但仍被 GC Roots 引用(如静态变量持有长生命周期对象),可能导致内存泄漏。通过分析 GC Roots 引用链,可定位泄漏源头。 > > ### **与引用类型的关系** > > GC Roots 本身是强引用(Strong Reference),被其直接或间接引用的对象不会被垃圾回收器回收,除非引用链被切断。若需要允许对象被回收,需断开与 GC Roots 的引用关系(如将变量置为 `null`)。 > > ### **总结** > > GC Roots 是 JVM 垃圾回收机制的核心概念,它通过定义一组 “活跃” 的引用起点,确保垃圾回收器能正确识别存活对象和可回收对象。理解 GC Roots 有助于深入分析 JVM 内存管理、优化内存使用并排查内存相关问题。 [参考](https://blog.csdn.net/qq_32099833/article/details/109253339) #### 三、垃圾回收算法 GC通过不同的算法来回收垃圾对象,常用的算法包括: 1. **标记-清除算法**:首先标记出所有需要回收的对象,然后在标记完成后统一回收所有被标记的对象。这种算法的缺点是效率问题和空间问题,即标记和清除的效率都不高,且标记清除后会产生大量不连续的内存碎片。 2. **复制算法**:将内存空间分为两部分,每次只使用其中的一部分。当这部分内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一部分上面,然后再把已经使用过的内存区域一次清理掉。这种算法的优点是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,但缺点是空间利用率低,且存活对象较多时复制开销大。 3. **标记-整理算法**:标记过程与标记-清除算法一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。这种算法解决了空间利用率低的问题,但仍然存在复制搬运元素开销大的问题。 4. **分代收集算法**:根据对象存活的不同生命周期将内存划分为不同的域,如老年代和新生代。老年代的特点是每次垃圾回收时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量垃圾需要被回收。因此,可以根据不同区域选择不同的算法,如新生代使用复制算法,老年代使用标记-整理算法。 #### 四、Java中的垃圾回收器(了解) Java虚拟机(JVM)提供了多种垃圾回收器,以适应不同的应用场景和性能需求。常见的垃圾回收器包括Serial GC、Parallel GC、CMS、G1 GC等。选择合适的垃圾回收器对于优化Java应用程序的性能至关重要。 #### 按执行机制分类 - 串行垃圾回收器(Serial GC): - 它是最基础的垃圾回收器,采用单线程进行垃圾回收操作。在回收期间,会暂停所有应用程序线程(Stop-The-World),直至回收完成。此回收器适用于单线程环境或者对内存需求较小的应用。 - 激活参数:`-XX:+UseSerialGC`。 - 并行垃圾回收器(Parallel GC): - 并行垃圾回收器利用多线程来加速垃圾回收过程。和串行垃圾回收器一样,在回收时也会暂停所有应用程序线程。不过,由于采用多线程,其回收效率更高,适合对吞吐量要求较高的应用。 - 激活参数:`-XX:+UseParallelGC`(新生代并行回收)和`-XX:+UseParallelOldGC`(老年代并行回收)。 - 并发标记清除垃圾回收器(CMS GC): - CMS GC 致力于减少垃圾回收时的停顿时间。它采用多线程并发的方式进行垃圾回收,尽可能地与应用程序线程同时运行。该回收器会经历初始标记、并发标记、重新标记和并发清除等阶段,其中初始标记和重新标记阶段会有短暂的停顿。 - 激活参数:`-XX:+UseConcMarkSweepGC`。 - G1 垃圾回收器(G1 GC): - G1 GC 是一种面向服务器端应用的垃圾回收器,旨在兼顾吞吐量和低延迟。它把堆内存划分为多个大小相等的区域(Region),并跟踪每个区域的垃圾堆积情况,优先回收垃圾最多的区域。G1 GC 在运行过程中也会有短暂的停顿,但能更好地控制停顿时间。 - 激活参数:`-XX:+UseG1GC`。 - Z 垃圾回收器(ZGC): - ZGC 是一种可扩展的低延迟垃圾回收器,旨在实现亚毫秒级的停顿时间。它采用了染色指针和读屏障等技术,能够在不暂停应用程序线程的情况下进行大部分垃圾回收操作。ZGC 适用于对响应时间要求极高的大型应用。 - 激活参数:`-XX:+UseZGC`。 - Shenandoah 垃圾回收器: - Shenandoah 是一种低延迟的垃圾回收器,其设计目标是在不牺牲吞吐量的前提下,实现几乎无停顿的垃圾回收。它通过与应用程序线程并发执行大部分垃圾回收工作,显著减少了停顿时间。 - 激活参数:`-XX:+UseShenandoahGC`。 #### 按回收区域分类 - 新生代垃圾回收器: - 负责回收新生代(Eden 区和 Survivor 区)的垃圾,常见的有 Serial、Parallel Scavenge 和 ParNew 等。 - 老年代垃圾回收器: - 主要回收老年代的垃圾,像 Serial Old、Parallel Old 和 CMS 等。 - 全堆垃圾回收器: - 能够对整个堆内存(包括新生代和老年代)进行垃圾回收,例如 G1、ZGC 和 Shenandoah 等。 #### 五、GC的代价 虽然GC为程序员带来了极大的便利,但它也引入了一定的代价。GC过程中需要消耗一定的系统资源,如CPU时间和内存空间。此外,GC还可能导致应用程序的停顿(Stop-The-World),特别是在进行全量垃圾回收时。因此,在设计Java应用程序时,需要权衡GC的便利性和其带来的代价。 综上所述,GC是Java等编程语言中自动管理内存的重要机制。通过选择合适的垃圾回收算法和垃圾回收器,可以优化Java应用程序的性能和稳定性。然而,也需要注意GC带来的代价,并在设计应用程序时进行合理的权衡。 ### JVM调优 JVM调优,即Java虚拟机(Java Virtual Machine)调优,是指对Java虚拟机的运行参数进行调整,以优化Java应用程序的性能。以下是对JVM调优的详细解释: #### 一、JVM调优的目标 JVM调优的主要目标包括: 1. **减少延迟**:提高应用程序的响应速度,减少用户等待时间。 2. **提高吞吐量**:增加应用程序在单位时间内处理的任务数量。 3. **减少内存占用**:优化内存使用,降低内存泄漏和内存溢出的风险。 #### 二、JVM调优的关键参数 1. **堆内存设置**: - `-Xms`:设置JVM启动时的初始堆大小。例如,`-Xms256m`表示设置初始堆大小为256MB。 - `-Xmx`:设置JVM可以使用的最大堆大小。例如,`-Xmx1024m`表示设置最大堆大小为1024MB。 2. **垃圾回收器选择**: - JVM提供了多种垃圾回收器,如Serial GC、Parallel GC、CMS、G1 GC等。选择合适的垃圾回收器以适应不同的应用场景。 - 例如,`-XX:+UseG1GC`表示选择G1垃圾回收器。 3. **垃圾回收触发条件和频率**: - 通过调整新生代和老年代的大小比例来影响垃圾回收的触发条件和频率。例如,`-XX:NewRatio=2`表示设置新生代和老年代的大小比例为1:2。 - 设置Eden区与Survivor区的大小比例。例如,`-XX:SurvivorRatio=8`表示设置Eden区与Survivor区的大小比例为8:1:1(Eden区与两个Survivor区的大小比例)。 4. **其他JVM参数**: - 栈大小:`-Xss`参数用于设置每个线程的栈大小。例如,`-Xss256k`表示设置每个线程的栈大小为256KB。 - 元空间大小:`-XX:MetaspaceSize`参数用于设置元空间的初始大小。例如,`-XX:MetaspaceSize=128m`表示设置元空间的初始大小为128MB。 - 直接内存大小:`-XX:MaxDirectMemorySize`参数用于设置直接内存的大小。 #### 三、JVM调优的工具和方法 1. **JVM监控工具**: - 使用JVM监控工具(如VisualVM、JConsole、MAT等)来监控JVM的运行状态,并分析内存泄漏、垃圾回收情况等。 - 这些工具提供了直观的界面和丰富的功能,有助于开发者深入了解JVM的内部工作机制。 > [JVisualVM的使用教程](https://blog.csdn.net/DevelopmentStack/article/details/117385852) > > [jconsole 使用](https://blog.csdn.net/xhmico/article/details/130720808) 2. **代码层面优化**: - 优化代码逻辑,减少不必要的对象创建,优化数据结构和算法等。 - 减少锁的竞争,使用无锁数据结构,优化线程池配置等。 3. **GC日志分析**: - 通过分析GC日志,可以了解垃圾回收的频率、时间、堆内存的使用情况等。 - 根据GC日志的分析结果,可以调整JVM的参数以优化垃圾回收的性能。 4. **JVM参数调整**: - 根据应用程序的需求和运行环境,调整JVM的启动参数以优化性能。 - 例如,启用分层编译(`-XX:+TieredCompilation`)可以提高编译效率;配置JVM日志(`-XX:+PrintGCDetails`)以便在出现问题时能够快速定位。 #### 四、调优步骤 ① 分析 GC 日志及 dump 文件,判断是否需要优化,确定问题根源所在 ② 确定 JVM 调优量化目标和 JVM调优参数(JVM 调优参数根据历史 JVM 参数来调整) ③ 依次调优内存、延迟、吞吐量等指标 ④ 对比观察调优前后的差异,同时需要不断的分析和调整参数,直到找到合适的 JVM 参数配置; ⑤ 找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。 #### 五、JVM调优的注意事项 1. **了解应用程序的需求**:在进行JVM调优之前,需要深入了解应用程序的需求和运行环境,以便选择合适的调优策略。 2. **逐步调整参数**:不要一次性调整过多的JVM参数,而是应该逐步调整并观察应用程序的性能变化。 3. **监控和评估**:在调优过程中,需要持续监控应用程序的性能指标(如吞吐量、延迟、内存占用等),并根据监控结果进行评估和调整。 4. **谨慎使用非标准参数**:JVM提供了一些非标准参数(如以`-XX`开头的参数),这些参数可能具有不稳定性或不确定性。因此,在使用这些参数时需要谨慎考虑其风险和收益。 综上所述,JVM调优是一个复杂而精细的过程,需要深入了解JVM的工作原理和参数配置,并根据应用程序的需求和运行环境进行选择和调整。通过合理的JVM调优,可以显著提高Java应用程序的性能和稳定性。 ### JVM调优参数 JVM调优是指通过调整Java虚拟机的启动参数来优化应用程序的性能。这些参数涉及内存管理、垃圾收集器选择、线程管理等多个方面。以下是一些常用的JVM调优参数及其作用: #### 堆内存设置 - `-Xms`:设置JVM启动时的初始堆大小。 - `-Xmx`:设置JVM最大堆大小。 - `-Xmn`:设置新生代大小。 - `-XX:NewSize=`:设置新生代初始大小。 - `-XX:MaxNewSize=`:设置新生代最大大小。 - `-XX:SurvivorRatio=`:设置新生代中Eden区与Survivor区的比例。 #### 垃圾收集器选择 - `-XX:+UseSerialGC`:使用串行垃圾收集器。 - `-XX:+UseParallelGC`:使用并行垃圾收集器。 - `-XX:+UseConcMarkSweepGC`:使用CMS垃圾收集器。 - `-XX:+UseG1GC`:使用G1垃圾收集器。 - `-XX:+UseZGC`:使用Z Garbage Collector。 #### 垃圾收集行为调整 - `-XX:+DisableExplicitGC`:禁用`System.gc()`的调用。 - `-XX:+HeapDumpOnOutOfMemoryError`:当发生内存溢出时生成堆转储。 #### JIT编译器设置 - `-XX:+TieredCompilation`:启用分层编译。 - `-XX:MaxInlineSize=`:设置内联方法的最大字节数。 #### 线程栈大小 - `-Xss`:设置每个线程的栈大小。 #### 直接内存设置 - `-XX:MaxDirectMemorySize=`:设置直接内存的最大大小。 #### 监控和诊断工具 - `-XX:+PrintGCDetails`:打印详细的GC日志。 - `-XX:+PrintGCDateStamps`:在GC日志中打印时间戳。 - `-XX:+PrintGCApplicationConcurrentTime`:打印应用程序的并发时间。 #### 其他高级设置 - `-XX:+UseBiasedLocking`:启用偏向锁,减少锁的竞争。 - `-XX:+UseStringDeduplication`:启用字符串去重。 #### 调优建议 在进行JVM调优时,应该根据应用程序的具体需求和运行环境来选择合适的参数。通常,需要进行多次测试和调整以达到最佳性能。使用JVM监控工具如VisualVM、JConsole等可以帮助分析和理解JVM的行为,从而做出更合理的调优决策[3](https://blog.csdn.net/xintai1999/article/details/140436452)[5](https://blog.csdn.net/qq_31532979/article/details/135927632)[6](https://developer.aliyun.com/article/1568031). 请注意,以上信息是基于最新的搜索结果,以确保时效性和准确性。在实际应用中,您可能需要根据具体情况调整这些参数。 ### JVM调优工具 JVM 调优是提升 Java 应用性能的关键环节,合理使用调优工具可以帮助开发者快速定位性能瓶颈、分析内存使用情况等。以下是常见的 JVM 调优工具及其特点和使用场景: #### **一、JDK 自带工具** JDK 内置了一系列实用工具,无需额外安装即可使用,适合基础的性能分析和监控。 ##### 1. **jps(JVM Process Status Tool)** - **功能**:查看当前运行的 Java 进程,获取进程 ID(PID),用于其他工具定位目标进程。 - 命令格式 ```bash jps [options] # 常用选项:-l(显示完整类名/ jar 路径)、-v(显示 JVM 启动参数) ``` - 示例 ```bash $ jps -l 12345 com.example.MyApp # 显示进程 ID 和主类名 ``` ##### 2. **jstat(JVM Statistics Monitoring Tool)** - **功能**:实时监控 JVM 各内存区域的使用情况、垃圾回收(GC)统计信息等。 - 命令格式 ```bash jstat [options] PID [interval] [count] # options 示例: # -gc:查看 GC 详细统计(Eden、Survivor、Old 区等的使用情况) # -gccapacity:查看各区内存容量 # -gcutil:查看 GC 耗时占比 # interval:采样间隔(毫秒),count:采样次数 ``` - 示例 ```bash $ jstat -gcutil 12345 1000 5 # 每 1 秒采样一次,共采样 5 次 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 92.13 28.37 98.70 98.26 123 12.34 5 4.56 16.90 ``` - **关键指标**:`YGC`(年轻代 GC 次数)、`YGCT`(年轻代 GC 耗时)、`FGC`(老年代 GC 次数)、`FGCT`(老年代 GC 耗时)、`GCT`(总 GC 耗时)。 ##### 3. **jinfo(Configuration Info for Java)** - **功能**:查看或修改 JVM 运行时参数(如内存配置、GC 策略等)。 - 命令格式 ```bash jinfo [options] PID # 常用选项: # -flags:查看所有 JVM 启动参数 # -flag :查看指定参数值(如 -flag UseG1GC) # -flag [+|-]:开启/关闭指定参数(动态修改,部分参数支持) ``` - 示例 ```bash $ jinfo -flags 12345 # 查看进程的 JVM 参数 -XX:+UseG1GC -Xms2048m -Xmx4096m ... ``` ##### 4. **jmap(Memory Map for Java)** - **功能**:生成 JVM 内存转储快照(Heap Dump),用于分析内存泄漏或对象分布。 - 命令格式 ```bash jmap [options] PID # 常用选项: # -dump:format=b,file=:生成堆转储文件(如 -dump:file=heapdump.hprof) # -histo:查看堆中对象的统计信息(类名、实例数、占用空间) ``` - 示例 ```bash $ jmap -dump:file=heapdump.hprof 12345 # 生成堆转储文件 $ jmap -histo 12345 | head -n 20 # 查看前 20 大对象 ``` ##### 5. **jhat(Java Heap Analysis Tool)** - **功能**:基于 `jmap` 生成的堆转储文件,启动 HTTP 服务器提供在线分析界面,查看对象引用关系等。 - 命令格式 ```bash jhat ``` - **注意**:该工具已逐渐被更强大的图形化工具替代(如 MAT),实际中较少使用。 ##### 6. **jstack(Stack Trace for Java)** - **功能**:生成线程快照(Thread Dump),用于分析线程状态(如阻塞、死锁、竞争锁等)。 - 命令格式 ```bash jstack [options] PID # 常用选项:-l(显示锁的详细信息,适用于分析死锁) ``` - 示例 ```bash $ jstack 12345 > thread_dump.log # 将线程快照保存到文件 ``` - **关键场景**:排查线程长时间阻塞、CPU 利用率过高等问题。 #### **二、图形化工具** 图形化工具通过可视化界面简化调优流程,适合快速定位问题和实时监控。 ##### 1. **JConsole(JDK 自带)** - **功能**:基于 JMX 的图形化监控工具,可监控内存、线程、类加载、CPU 使用率等,支持远程连接。 - 启动方式 ```bash jconsole # 启动后选择本地进程或远程连接 ``` - 特点 - 实时图表展示内存变化、GC 次数等。 - 线程标签页可查看线程状态(RUNNABLE、BLOCKED、WAITING 等),支持生成线程快照。 ##### 2. **VisualVM(JDK 自带,需单独安装)** - **功能**:比 JConsole 更强大的分析工具,支持性能监控、内存分析、CPU 分析、插件扩展(如安装 Visual GC 插件)。 - 启动方式 ```bash jvisualvm # JDK 8 及以后需单独下载安装 ``` - 核心功能 - **性能分析**:通过抽样或探针模式分析 CPU 耗时,定位热点方法。 - **内存分析**:生成堆转储文件,对比不同时刻的内存快照,查找内存泄漏。 - **插件扩展**:安装 `Visual GC` 插件可实时查看各内存区域的 GC 情况。 ##### 3. **MAT(Memory Analyzer Tool)** - **功能**:专业的堆转储分析工具,由 Eclipse 开发,用于深入分析内存泄漏和对象引用链。 - 特点 - 自动检测可能的内存泄漏点(Leak Suspects),生成报告。 - 支持对象查询、引用树分析、支配树(Dominator Tree)等高级功能。 - 使用流程 1. 使用 `jmap` 生成堆转储文件。 2. 用 MAT 打开文件,分析 `Histogram`、`Dominator Tree` 等视图。 ##### 4. **GCeasy** - **功能**:在线 GC 日志分析工具,将 GC 日志上传后自动生成可视化报告,分析 GC 性能瓶颈。 - 特点 - 无需安装,简单易用,适合快速分析 GC 日志。 - 报告包含 GC 频率、耗时、内存趋势、建议优化方向等。 #### **三、命令行工具(高级)** ##### 1. **top + jstack(组合使用)** - **场景**:当 Java 进程 CPU 使用率过高时,先用 `top` 定位消耗 CPU 的线程,再用 `jstack` 分析线程状态。 - 步骤 1. `top -p PID`:查看目标进程的 CPU 占用率。 2. 按 `shift + h` 显示该进程的所有线程,找到 CPU 占用高的线程 ID(如 `0xabc1`)。 3. 将线程 ID 转换为十六进制(如 `printf "%x\n" 线程ID`)。 4. `jstack PID | grep -A 20 十六进制ID`:查看该线程的堆栈信息,定位热点代码。 ##### 2. **arthas(阿尔萨斯)** - **功能**:开源的 Java 诊断工具,支持实时监控、热更新代码、查看方法调用链路等高级功能。 - 特点 - 无需重启应用,动态 attach 到运行中的进程。 - 常用命令: - `thread`:查看线程状态,定位阻塞线程。 - `sc`:查看类加载信息。 - `watch`:监控方法入参、返回值、异常等。 - `tt`:记录方法调用历史,用于问题回溯。 - 安装 ```bash curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar # 选择目标进程 ``` #### **四、生产环境调优建议** 1. **优先分析 GC 日志**:通过 `-Xlog:gc*=debug` 或 `-verbose:gc` 开启 GC 日志,结合 `GCeasy` 或 `gceasy.io` 分析。 2. **避免频繁生成堆转储**:生产环境生成堆转储可能影响性能,建议先通过轻量级工具(如 `jstat`、`arthas`)初步分析。 3. **结合应用日志**:调优时需关联应用业务日志,避免因代码逻辑问题(如缓存未释放)导致的性能问题被误判为 JVM 问题。 4. **逐步调整参数**:JVM 参数(如 `-Xms`、`-Xmx`、`-XX:G1HeapRegionSize`)调整需小步验证,避免因参数设置不当引发新问题。 #### **总结** - **入门工具**:JConsole、VisualVM 适合快速上手,用于基础监控和分析。 - **内存分析**:MAT 是堆转储分析的首选,结合 `jmap` 使用。 - **实时诊断**:arthas 功能强大,适合复杂问题的动态追踪。 - **GC 优化**:`jstat` 配合 GC 日志分析工具(如 GCeasy)是核心手段。 # MySQL数据库 ## 如何理解存储引擎(理解) MySQL 存储引擎是 MySQL 数据库管理系统中用于存储、检索和管理数据的组件,它就像是一个负责数据存储和访问的 “引擎”,不同的存储引擎具有不同的特点和功能,以适应各种不同的应用场景和需求。可以从以下几个方面来理解 MySQL 存储引擎: ### 存储引擎的作用 - **数据存储与组织**:负责将数据以特定的格式存储在磁盘上,决定了数据的存储结构和布局。例如,InnoDB 存储引擎采用聚簇索引结构,将数据和索引紧密结合存储,以提高查询性能。 - **数据访问与检索**:提供了不同的算法和机制来快速地检索和访问数据。例如,MyISAM 存储引擎使用表级锁,在查询时可以快速锁定整个表,适用于读操作频繁的场景;而 InnoDB 支持行级锁,能更细粒度地控制并发访问,适合读写并发较高的应用。 - **事务处理**:一些存储引擎支持事务,能够确保数据的一致性和完整性。如 InnoDB 存储引擎遵循 ACID(原子性、一致性、隔离性、持久性)原则,通过事务日志等机制来保证事务的正确执行和数据的安全。 - **数据完整性约束**:可以实现对数据的完整性检查和约束。例如,InnoDB 支持外键约束,能够确保表与表之间数据的一致性,而 MyISAM 则不支持外键。 ### 常见存储引擎的特点 - InnoDB - **事务支持**:支持事务,具备强大的事务处理能力,能保证数据的一致性和完整性,适用于对数据一致性要求高的场景,如银行转账、电商订单处理等。 - **行级锁**:采用行级锁,并发性能好,在高并发读写操作下能有效减少锁冲突,提高系统的并发处理能力。 - **聚簇索引**:数据和索引存储在一起,以主键为聚簇索引,能快速定位数据,对于基于主键的查询性能很高。 - MyISAM - **快速读取**:不支持事务和行级锁,表级锁的机制使得读取操作非常快,适用于以读为主的应用,如数据仓库、日志系统等。 - **空间效率高**:数据存储紧凑,占用空间小,对于一些对空间要求较高且读多写少的场景较为适用。 - **不支持外键**:不支持外键约束,在数据关联方面的功能相对较弱。 - Memory - **内存存储**:数据存储在内存中,读写速度极快,适用于对实时性要求极高的场景,如缓存、临时数据处理等。 - **临时数据处理**:通常用于存储临时表或中间结果集,当数据库服务重启后,Memory 表中的数据会丢失。 ### 选择合适的存储引擎 - **根据应用场景**:如果是电商系统、银行系统等对数据一致性和事务处理要求高的场景,优先选择 InnoDB;如果是数据仓库、日志分析等以读为主的场景,MyISAM 可能更合适;对于实时性要求极高的缓存场景,则可以考虑 Memory 存储引擎。 - **考虑数据特点**:如果数据量较大且更新频繁,InnoDB 的行级锁和事务支持更能保证数据的一致性和并发性能;如果数据量较小且以读为主,MyISAM 的简单高效可能更具优势。 - **结合性能需求**:对于查询性能要求高,尤其是基于主键查询频繁的应用,InnoDB 的聚簇索引结构能提供很好的性能;而对于一些需要快速读取大量数据的场景,MyISAM 的快速读取特性可能更符合需求。 总之,理解 MySQL 存储引擎就是要明白它们在数据存储、访问、事务处理等方面的不同特点和作用,根据具体的应用场景和数据需求来选择合适的存储引擎,以达到最佳的性能和数据管理效果。 ## MySQL的存储引擎 MySQL是一个开源的关系型数据库管理系统,它支持多种存储引擎,每种存储引擎都有其独特的特点和适用场景。以下是几种常用的MySQL存储引擎及其特点: #### InnoDB - **事务支持**:InnoDB是MySQL的默认存储引擎,支持ACID事务,提供了提交、回滚和崩溃恢复能力。 - **行级锁定**:相比MyISAM的表级锁定,InnoDB提供了更好的并发性能。 - **外键约束**:支持外键,有助于维护数据的参照完整性。 - **支持MVCC(多版本并发控制)**:提供了非锁定读,提高了多用户并发处理能力。 - **自适应哈希索引**:优化了某些查询的性能。 - **支持大容量数据存储**:表空间可以达到64TB。 - **支持全文索引**:从MySQL 5.6版本开始支持。 #### MyISAM - **非事务性**:不支持事务,因此没有ACID特性。 - **表级锁定**:在高并发环境下可能成为瓶颈。 - **空间和内存使用效率高**:适合读密集型应用。 - **不支持外键**:数据完整性需要在应用层处理。 - **不支持崩溃恢复**:数据库崩溃后需要手动恢复。 #### Memory - **基于内存存储**:数据在内存中,速度快但不稳定,数据库重启后数据丢失。 - **支持表级锁**:由于数据在内存中,锁的开销相对较小。 - **不支持事务和外键**。 #### Archive - **用于存储归档数据**:支持高并发的插入操作,但不支持更新或删除。 - **不支持索引**:查询性能依赖于顺序读取。 #### CSV - **将数据存储在逗号分隔的文本文件中**:易于与其他程序交换数据,但性能较差,不支持索引。 #### Blackhole - **接受但不存储数据**:通常用于复制配置中的黑洞表,用于丢弃接收到的数据。 #### Federated - **允许访问远程数据库中的表**:通过网络连接到另一台MySQL服务器。 ## MyISAM与InnoDB区别(高频) MyISAM与InnoDB是MySQL数据库中两种常见的存储引擎,它们在多个方面存在显著差异。以下是它们之间主要区别的详细归纳: ### 1. 存储结构与文件类型 - MyISAM - 使用非聚集索引,索引和数据文件是分离的。 - 在磁盘上存储为三个文件:.frm(表定义)、.MYD(数据文件)、.MYI(索引文件)。 - 支持静态表、动态表和压缩表三种存储格式。 - InnoDB - 使用聚集索引,索引和数据是关联在一起的,表数据文件本身就是按B+Tree组织的一个索引结构。 - 有两种存储方式:共享表空间存储和多表空间存储。 - 表结构和MyISAM一样,以表名开头,扩展名是.frm。如果使用共享表空间,所有表的数据文件和索引文件保存在一个表空间里;如果使用多表空间,每个表都有一个表空间文件(.ibd)。 ### 2. 事务与外键(重点) - MyISAM - 不支持事务处理,也不支持外键。 - InnoDB - 支持事务处理,是事务安全的存储引擎。 - 支持外键,可以确保数据的参照完整性。 ### 3. 锁机制(重点) - MyISAM - 只支持表级锁,无论是读操作还是写操作都会锁定整个表。 - 表级锁开销小,加锁快,但并发度低。 - InnoDB - 支持表级锁和行级锁,默认情况下使用行级锁。 - 行级锁开销大,但并发度高,可以有效减少锁冲突。 - InnoDB的行锁是通过索引实现的,如果没有命中索引,则会退化为表锁。 ### 4. 性能与功能 - MyISAM - 强调性能,特别是读操作的性能,因为表级锁在读取时不会阻塞其他读操作。 - 适用于读多写少的场景,如Web应用中的静态内容表。 - 支持全文索引(FULLTEXT),在全文搜索方面性能较好。 - InnoDB - 提供了更丰富的数据库功能,如事务、外键等。 - 适用于需要高并发写入和复杂查询的场景。 - 不直接支持全文索引,但可以通过插件(如Sphinx)实现。 ### 5. 计数与数据维护 - MyISAM - 保存了表的总行数,执行`SELECT COUNT(*) FROM table;`时直接返回该值,速度很快。 - InnoDB - 不保存表的总行数,执行`SELECT COUNT(*) FROM table;`时需要全表扫描,速度较慢。 - 在处理大量数据时,InnoDB的自动增长列和索引管理更为复杂。 ### 6. 备份与恢复 - MyISAM - 数据以文件形式存储,备份和恢复时可以单独针对某个表进行操作。 - InnoDB - 备份和恢复相对复杂,需要拷贝数据文件、备份binlog或使用mysqldump等工具。 - 在数据量较大时,备份和恢复操作可能较为耗时。 ### 7. 适用场景 - MyISAM - 适用于读操作远多于写操作的场景,如Web应用的静态内容表。 - 适用于不需要事务和外键支持的应用。 - InnoDB - 适用于需要事务处理、外键支持以及高并发写入的应用。 - 适用于对数据完整性和一致性要求较高的场景。 综上所述,MyISAM和InnoDB在存储结构、事务支持、锁机制、性能与功能等方面存在显著差异。在选择存储引擎时,应根据具体的应用场景和需求进行综合考虑。 ## 什么是索引?(必问 高频) > 从以下方面回答: > > 1. 索引的概念 > 2. 索引的类型 > 3. 索引适用的场景 > 4. 索引创建的原则 > 5. 索引失效 > > 使用索引查询一定能提高查询的性能吗?为什么 > > 使用索引查询不一定能提高查询性能。虽然索引在很多情况下可以显著提升查询速度,但在某些场景下,它可能对性能没有帮助,甚至会降低性能。具体分析如下: > > ### 索引能提高查询性能的原因 > > - **快速定位数据**:索引就像一本书的目录,通过它可以快速定位到所需数据在数据库中的位置,避免全表扫描。例如,在一个包含大量用户信息的表中,如果要查询特定姓名的用户记录,在姓名列上建立索引后,数据库可以直接根据索引找到对应的记录,而无需逐行扫描整个表,从而大大提高查询效率。 > - **减少 I/O 操作**:数据库在读取数据时,需要从磁盘将数据块加载到内存中。使用索引可以精准地获取所需数据所在的数据块,减少不必要的数据块读取,降低 I/O 操作次数,进而提高查询性能。 > > ### 索引不能提高查询性能的情况 > > - **索引列选择性低**:如果一个列的取值范围很小,例如性别列,只有 “男” 和 “女” 两种取值,那么建立索引可能不会带来明显的性能提升。因为即使使用索引,数据库也需要扫描大量包含相同值的记录,无法有效缩小查询范围。 > - **查询语句复杂**:当查询语句涉及到多个表的连接、复杂的条件过滤以及聚合操作等时,索引的作用可能会受到限制。因为数据库需要综合考虑各种因素来选择最优的查询执行计划,索引并不一定是决定性能的关键因素。在这种情况下,优化查询语句的结构、调整表的设计等可能比依赖索引更有效。 > - **索引维护成本高**:索引虽然可以加快查询速度,但会增加数据插入、更新和删除操作的成本。因为在对数据进行这些操作时,数据库需要同时维护索引的一致性,这可能导致额外的性能开销。如果表中的数据经常发生变化,而查询操作相对较少,那么过多的索引可能会降低整体性能。 > - **数据库系统选择不使用索引**:数据库的查询优化器会根据统计信息和查询语句的具体情况来决定是否使用索引。如果优化器认为全表扫描或其他查询策略更高效,它可能会选择不使用索引。例如,当表非常小,全表扫描的成本低于使用索引的成本时,优化器会选择全表扫描。 ### 什么是索引? **索引是数据库中一个排序的数据结构,以协助快速查询、更新数据库表中数据**。索引的实现通常使用B树及其变种B+树。 > 更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间的。 ### 索引的类型有哪些? 数据库中的索引类型包括但不限于以下几种: - **单列索引**:针对表中单个列的索引,适用于该列的等值查询、范围查询和排序操作。 - **多列索引**(复合索引):针对表中多个列的联合索引,适用于这些列的复合查询。 > 涉及到: 最左前缀匹配原则 (很重要 很重要 很重要) > > ### 最左前缀匹配原则 > > 最左前缀匹配原则是指当查询条件涉及到复合索引的多个列时,数据库查询优化器会优先使用索引中最左边的列进行匹配,并向右依次匹配剩余的列,直到遇到不满足查询条件的列为止。如果查询条件跳过了索引中的某一列或者只涉及到索引中非最左边的列,那么该索引可能无法被有效利用,或者只能部分利用。 > > 遵循最左前缀匹配原则至关重要,因为它直接影响到查询的性能。在创建复合索引时,应该将查询中使用频率最高的列放置在索引的最左边,以确保索引能够被最大化利用。 > > > 如索引列:(a,b,c,d),等同于创建了4个索引: (a) (ab) (abc) (abcd) - **唯一索引**:确保索引列的值在整个表中是唯一的,可以加速唯一值的查询。 > 用户名唯一 、订单单号 这类的数据如果经常查,建议创建唯一索 引 - **主键索引**:一种特殊的唯一索引,用于唯一标识表中的每一行,每个表只能有一个主键索引。 - **全文索引**:用于对文本类型的列进行全文搜索,能高效处理大文本字段的查找。 - **空间索引**:用于地理空间数据类型的列,例如Point、LineString和Polygon,可以加速对地理空间数据的搜索和分析 > #### 按数据结构分类 > > - **B+树索引**:MySQL中默认和最常用的索引类型,适用于范围查询和全表扫描。 > - **Hash索引**:适用于等值查询,不适用于范围查询 > - **全文索引**:专门用于处理文本数据的全文搜索 > - **R树索引**:用于多维空间数据的索引,如GIS应用 > > #### 按物理存储分类 > > - **聚集(簇)索引**:数据的物理顺序与索引的逻辑顺序一致,每个表只能有一个聚集索引 > - **非聚集(簇)索引**:数据的物理顺序与索引的逻辑顺序不一致,一个表可以有多个非聚集索引 > > #### 按字段特性分类 > > - **主键索引**:建立在主键上的索引,保证数据的唯一性和非空性[3](https://blog.csdn.net/weixin_43287459/article/details/140593851)[5](https://blog.csdn.net/Aaaaaaatwl/article/details/138958112)[8](https://blog.csdn.net/rabbit_qi/article/details/138226983)。 > - **唯一索引**:保证索引列中的每个值都是唯一的,允许有空值[3](https://blog.csdn.net/weixin_43287459/article/details/140593851)[5](https://blog.csdn.net/Aaaaaaatwl/article/details/138958112)[8](https://blog.csdn.net/rabbit_qi/article/details/138226983)。 > - **普通索引**:提高查询性能的索引,允许重复值和空值[3](https://blog.csdn.net/weixin_43287459/article/details/140593851)[5](https://blog.csdn.net/Aaaaaaatwl/article/details/138958112)[8](https://blog.csdn.net/rabbit_qi/article/details/138226983)。 > - **前缀索引**:对字符串的前几个字符创建索引,减少索引占用的存储空间 > > #### 按字段个数分类 > > - **单列索引**:对单个列创建的索引 > - **联合索引**:对多个列创建的索引,适用于多列一起使用的查询 ### 索引有哪些优缺点? #### 索引的优点 1. **提高检索速度**:索引可以显著加快数据检索的速度,尤其是在大型数据集中,通过减少磁盘I/O操作次数,降低系统资源消耗[1](https://blog.csdn.net/m0_60980259/article/details/139176455)[2](https://blog.csdn.net/qq_33240556/article/details/139646989)。 2. **加速排序和分组操作**:索引可以优化ORDER BY和GROUP BY子句的执行速度,因为索引本身是有序的 3. **提高连接性能**:索引能加快多表连接(JOIN)操作的执行效率[2](https://blog.csdn.net/qq_33240556/article/details/139646989)。 4. **保证数据完整性**:通过唯一性约束或主键约束,索引可以帮助保证数据的唯一性和完整性[1](https://blog.csdn.net/m0_60980259/article/details/139176455)。 5. **支持快速查找**:索引提供快速的查找功能,使得查询更加灵活和高效[6](https://blog.csdn.net/weixin_44772566/article/details/136989284)。 #### 索引的缺点 1. **占用存储空间**:索引会占用额外的存储空间,这可能会随着数据量的增加而显著增大[1](https://blog.csdn.net/m0_60980259/article/details/139176455)[2](https://blog.csdn.net/qq_33240556/article/details/139646989)。 2. **维护成本高**:索引需要定期维护,包括创建、更新和删除索引,这可能会增加数据库的负担和维护成本 3. **增加写操作的时间**:对表进行插入、更新和删除操作时,索引也需要进行相应的更新,这可能会增加写操作的时间[2](https://blog.csdn.net/qq_33240556/article/details/139646989)[6](https://blog.csdn.net/weixin_44772566/article/details/136989284)。 4. **不适用于所有查询**:并非所有的查询都适合使用索引,有些查询可能会因为索引而变得更慢[6](https://blog.csdn.net/weixin_44772566/article/details/136989284)。 5. **索引失效**:如果索引选择不当或者使用不当,可能会导致索引失效,从而影响查询性能[6](https://blog.csdn.net/weixin_44772566/article/details/136989284)。 ### 适用场景 #### 索引适用的场景 索引是数据库管理系统中用于加快数据检索速度的一种数据结构。以下是索引适用的一些典型场景: 1. **主键和外键字段**:主键字段通常会自动创建索引,因为它们用于唯一标识表中的每一行,外键字段用于建立表之间的关系,对这些字段建立索引可以提高连接操作的效率[1](https://blog.csdn.net/m0_59166601/article/details/136407781)[2](https://blog.csdn.net/m0_63208096/article/details/134982519)。 2. **离散度高的字段**:离散度指的是字段值的唯一性程度。具有高离散度的字段,如主键和具有唯一性约束的字段,非常适合作为索引[1](https://blog.csdn.net/m0_59166601/article/details/136407781)。 3. **存储空间小的字段**:占用存储空间较少的字段更适合作为索引,例如整数字段相对于字符串字段来说,占用的空间更少,因此更适合作为索引[1](https://blog.csdn.net/m0_59166601/article/details/136407781)。 4. **经常出现在查询条件中的字段**:经常出现在WHERE子句、ORDER BY或GROUP BY操作中的字段,应该考虑建立索引,因为这有助于提高查询和排序的效率[1](https://blog.csdn.net/m0_59166601/article/details/136407781)。 5. **经常用于排序的字段**:如果某个字段经常用于排序操作,应该考虑建立索引,因为这有助于提高排序的效率[1](https://blog.csdn.net/m0_59166601/article/details/136407781)。 6. **连接字段**:如果一个表经常与其他表进行连接操作,那么连接字段上建立索引可以显著提高连接查询的效率[1](https://blog.csdn.net/m0_59166601/article/details/136407781)。 #### 不适用索引的场景 索引虽然可以提高查询效率,但也有不适用的情况: 1. **频繁更新的字段**:如果一个表需要经常进行更新操作,那么在该表上建立索引可能会导致写入效率降低,因为每次数据更新后,索引也需要同步更新[1](https://blog.csdn.net/m0_59166601/article/details/136407781)。 2. **数据重复度高的字段**:对于数据重复度很高的字段,建立索引的效果不佳,因为这些字段的区分度不高,无法有效地缩小查询范围[1](https://blog.csdn.net/m0_59166601/article/details/136407781)。 3. **使用左模糊查询的字段**:如果对某个字段进行左模糊查询(即查询条件为'%xxx'),即使该字段上有索引,索引也无法发挥作用[1](https://blog.csdn.net/m0_59166601/article/details/136407781)。 4. **选择性低的字段**:某个字段的取值种类较少,导致该字段在过滤数据时的 “区分能力” 较差 5. **空间有限的数据库**:在存储空间有限的数据库中,过多的索引会占用宝贵的存储空间,这可能会影响数据库的整体性能[1](https://blog.csdn.net/m0_59166601/article/details/136407781)。 6. **小表查询优化不足**:对于数据量较小的表,全表扫描可能比使用索引更高效,因为索引的维护也需要成本[1](https://blog.csdn.net/m0_59166601/article/details/136407781)。 7. **索引失效的情况**:在某些查询条件下,数据库没有使用已经创建的索引来查找数据,而是进行了全表扫描,这通常会导致查询效率降低[1](https://blog.csdn.net/m0_59166601/article/details/136407781)。 ### 主键索引与唯一索引的区别 主键索引与唯一索引在数据库管理系统中扮演着重要的角色,但它们在多个方面存在显著的差异。以下是主键索引与唯一索引的主要区别: #### 1. 定义与本质 - **主键索引**:主键索引是一种特殊的约束,它要求表中的每一行都有一个唯一标识,即主键值。主键索引不仅保证了数据的唯一性,还决定了表中数据的物理存储顺序(在支持聚簇索引的数据库中,如InnoDB)。主键索引不允许有空值。 - **唯一索引**:唯一索引是一种索引类型,它确保索引列中的每个值都是唯一的,不允许重复。与主键索引不同,唯一索引列可以包含空值(但需要注意的是,如果列中已存在空值,则只能有一个空值,因为索引的唯一性约束)。 #### 2. 数量限制 - **主键索引**:一个表只能有一个主键索引。这是由主键索引的唯一性和非空性决定的。 - **唯一索引**:一个表可以有多个唯一索引。这些唯一索引可以应用于不同的列或列组合,只要它们各自保证唯一性即可。 #### 3. 空值处理 - **主键索引**:主键索引列不允许有空值。这是主键约束的一部分,确保每一行都有一个明确且唯一的标识。 - **唯一索引**:唯一索引列允许有空值,但空值在唯一性约束中被视为不同的值(即多个空值不会违反唯一性约束,但通常只允许一个空值存在,除非数据库配置或索引策略允许多个空值)。 #### 4. 外键引用 - **主键索引**:主键索引可以被其他表用作外键,建立表之间的关联关系。这是实现数据库参照完整性的重要机制之一。 - **唯一索引**:唯一索引不能直接被其他表用作外键。外键约束通常要求引用的是主键或具有唯一性的列,但直接引用唯一索引作为外键的情况较少见,因为主键索引提供了额外的完整性和性能优势。 #### 5. 索引类型与性能 - 在某些数据库系统中(如MySQL的InnoDB存储引擎),主键索引是聚簇索引,它决定了表中数据的物理存储顺序。这意味着通过主键索引查询数据可以非常高效,因为数据本身就是按照索引顺序存储的。 - 唯一索引可以是聚簇索引(如果它是表上的第一个索引且表支持聚簇索引)或非聚簇索引。非聚簇唯一索引在查询时可能需要额外的步骤来定位数据行,因为索引和数据在物理上是分开的。 #### 6. 创建方式 - **主键索引**:主键索引通常是在创建表时通过定义主键约束来创建的,也可以使用ALTER TABLE语句在表创建后添加主键约束。 - **唯一索引**:唯一索引可以通过CREATE UNIQUE INDEX语句在表创建后添加,也可以在创建表时通过定义唯一性约束来间接创建。 综上所述,主键索引与唯一索引在定义、数量限制、空值处理、外键引用、索引类型与性能以及创建方式等方面存在明显的区别。理解这些区别对于设计高效、健壮的数据库架构至关重要 ### 索引设计的基本原则 索引设计是数据库优化中非常重要的环节,它直接影响到数据库查询的性能。以下是一些基本的索引设计原则: 1. **根据查询需求设计索引**:索引应该根据实际的查询模式来设计,尤其是那些经常出现在`WHERE`子句、`ORDER BY`、`GROUP BY`中的字段 2. 选择区分度高的列:区分度高的列(即唯一值较多的列)更适合建索引,因为它们可以帮助更快地缩小查询范围 3. **使用唯一索引**:当列的值具有唯一性或高度区分度时,应考虑使用唯一索引,这样的索引不仅可以加速查询,还能保证数据的唯一性 4. **控制索引数量**:虽然索引可以提升查询速度,但过多的索引会占用更多存储空间,增加数据库维护成本,并可能降低写操作的性能。因此,索引的数量应当适度 5. **考虑索引覆盖**:尽量让索引包含查询中需要的所有列,这样数据库可以直接从索引中获取数据,而无需回表查询,这种情况下称为“**覆盖索引**”,能够显著提高查询性能 6. **联合索引优化**:在创建复合索引(联合索引)时,遵循最左前缀匹配原则。即在查询时,从索引的最左侧列开始进行匹配,这样可以最大化利用索引 7. **避免索引列含NULL值**:如果可能,为索引列定义`NOT NULL`约束,因为数据库优化器在处理NULL值时会有额外的开销 8. **前缀索引**:对于长字符串类型的字段,可以考虑建立前缀索引,只包含部分字符到索引树里去,以减少索引的大小和提高查询性能 9. **索引列的数据类型**:尽量使用数据类型较小的列作为索引,因为这样可以减少索引所占用的空间,提高查询性能 10. **索引维护**:定期检查并分析表的索引使用情况,删除不再有效或冗余的索引,以减少存储空间和维护开销 ### 索引哪些情况会失效(重要 必问) - 查询条件包含or,可能导致索引失效 - like通配符可能导致索引失效。如 like '%a' 会失效,而 like 'a%'不会失效 - 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效(**最左前缀匹配原则**) - 索引列使用了函数或表达式 当查询条件中对索引列使用了函数、类型转换或表达式时,数据库需要先计算表达式的值,然后再进行查询,这会导致索引失效 - 对索引列运算(如,+、-、*、/),索引失效。 - 索引字段上使用(!= 或者 < >)时,可能会导致索引失效。 - 使用了`NOT IN`或`NOT EXISTS`:这些操作符在某些情况下可能导致索引失效,尤其是处理不确定数量的结果集时 - 索引列上存在过多的 NULL 值 索引字段上使用is null, is not null,可能导致索引失效。 - 表中数据量较少 对于数据量较少的表,数据库可能会选择全表扫描而不是利用索引,因为全表扫描的开销相对较小。 - 隐式类型转换:查询条件中的列与索引列的类型不匹配,数据库进行隐式类型转换,可能会使索引失效 ### 使用索引查询一定能提高查询的性能吗?为什么 通常,通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。 - 索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O。 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况: - 基于一个范围的检索,一般查询返回结果集小于表中记录数的30% - 基于非唯一性索引的检索 ### 百万级别或以上的数据如何删除(场景) - 关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。 1. 所以我们想要删除百万数据的时候可以先删除索引(此时大概耗时三分多钟) 2. 然后删除其中无用数据(此过程需要不到两分钟)(要分批删除,不要一次性删除 ) 3. 删除完成后重新创建索引(此时数据较少了)创建索引也非常快,约十分钟左右。 4. 与之前的直接删除绝对是要快速很多,更别说万一删除中断,一切删除会回滚。那更是坑了。 ## 使用explain优化sql和索引? 对于复杂、效率低的sql语句,我们通常是使用explain sql 来分析sql语句,这个语句可以打印出,语句的执行。这样方便我们分析,进行优化 [参考](https://zhuanlan.zhihu.com/p/281517471) [MySQL官网](https://dev.mysql.com/doc/refman/8.4/en/explain-output.html) ![explain Sql执行计划](assets/explain%20Sql%E6%89%A7%E8%A1%8C%E8%AE%A1%E5%88%92.png) ### 优化建议 - **索引优化**:确保查询使用合适的索引,避免全表扫描(`type=ALL`)和文件排序(`Using filesort`)。 - **覆盖索引**:尽量使用覆盖索引(`Using index`),减少访问表数据的开销。 - **避免临时表**:优化 GROUP BY 和 DISTINCT 操作,避免创建临时表(`Using temporary`)。 - **分析 `rows` 值**:如果估计的扫描行数过多,可能需要优化查询条件或添加索引。 > **ken_len**: > > ### 作用 > > - **评估索引使用效率**:`key_len` 的值越大,说明索引使用得越充分。 > - **判断复合索引使用情况**:通过 `key_len` 可以知道复合索引中被实际使用的索引列。 > - **分析索引是否包含 NULL 值**:若索引列允许为 NULL,`key_len` 会增加额外的字节。 > > ### filtered定义与作用 > > - **含义**:`filtered` 表示在存储引擎从表中读取数据后,经过 `WHERE` 条件过滤后剩余数据的比例。 > > - **取值范围**:0% 到 100%。值越高,说明过滤后保留的数据越多;值越低,说明过滤掉的数据越多。 > > - **作用**:评估 `WHERE` 条件的过滤效率,辅助优化索引和查询条件。 > > - 计算公式 > > ~~~ > filtered = (满足条件的行数 / 扫描的行数) × 100% > ~~~ > > - #### 评估索引有效性 > > - **低 `filtered` 值(接近 0%)**:表示索引能有效过滤大量数据,查询效率较高。 > - **高 `filtered` 值(接近 100%)**:表示索引几乎没有过滤数据,可能需要优化索引或查询条件。 > > #### `Extra`(额外信息,重要) > > - 常见值及含义 > - `Using filesort`:MySQL 需要额外的排序操作,通常出现在 ORDER BY 或 GROUP BY 操作中,性能开销较大。 > - `Using temporary`:MySQL 需要创建临时表来存储结果,常见于 GROUP BY 和 DISTINCT 操作,应尽量避免。 > - **Using index**:使用了覆盖索引,直接从索引中获取数据,无需访问表数据,性能较好。 > - `Using where`:使用 WHERE 子句进行过滤。 > - `Using join buffer`:使用连接缓冲区,通常表示没有使用合适的索引。 > - `Impossible WHERE`:WHERE 子句条件永远为 FALSE,查询不会返回任何结果。 ## 数据库如何优化 数据库优化是一个复杂的过程,涉及到多个方面,以下是一些常见的优化方法: ### 索引优化 - **合理创建索引**:分析查询语句中用于条件过滤、连接操作和排序的列,并在这些列上创建索引。但索引并非越多越好,过多的索引会增加存储成本和数据更新的开销。 - **覆盖索引**:尽量使用覆盖索引,即索引包含了查询所需的所有列,这样查询时可以直接从索引中获取数据,而无需回表查询,提高查询效率。 ### 查询语句优化 - **避免全表扫描**:确保查询语句能够利用索引来过滤数据,避免使用没有索引支持的条件进行查询,导致全表扫描。 - **优化连接查询**:在连接多个表时,确保连接条件正确且使用了合适的索引,以减少连接操作的成本。 - **使用合适的聚合函数**:在进行聚合查询时,选择合适的聚合函数,并确保查询语句能够高效地计算聚合结果。 ### 数据类型优化 - **选择合适的数据类型**:根据数据的实际范围和用途,选择占用空间小、查询效率高的数据类型。例如,对于固定长度的字符串,使用 CHAR 类型比 VARCHAR 类型更节省空间和查询效率更高。 - **避免使用 TEXT 和 BLOB 类型**:如果可能,尽量避免在经常查询的列中使用 TEXT 和 BLOB 类型,因为这些类型的数据存储和查询效率相对较低。 ### 数据库设计优化 - **范式化设计**:遵循数据库设计的范式,以减少数据冗余,提高数据的一致性和完整性。但在某些情况下,为了提高查询性能,可以适当违反范式,进行反范式化设计。 - **合理分表**:根据数据的特点和查询需求,将大表拆分成多个小表,以提高查询效率和维护性。例如,可以按照数据的时间范围、业务模块等进行分表。 ### 服务器配置优化 - **调整内存配置**:根据服务器的物理内存大小和数据库的使用情况,合理配置数据库的内存参数,如缓冲池大小、排序缓冲区大小等,以提高数据库的性能。 - **优化磁盘 I/O**:使用高速的磁盘设备,如固态硬盘(SSD),可以显著提高数据库的读写性能。同时,可以通过调整磁盘缓存、优化磁盘分区等方式来进一步优化磁盘 I/O。 ### 定期维护数据库 - **清理无用数据**:定期删除不再使用的数据,以减少数据库的存储空间和查询数据量。 - **优化表结构**:定期对表进行分析和优化,以更新统计信息、整理碎片,提高数据库的性能。 - **备份数据库**:定期备份数据库,以防止数据丢失,并在必要时能够快速恢复数据库。 ## MySQL慢查询怎么解决? - **slow_query_log** 慢查询开启状态。 > ~~~sql > #查看 > show variables like 'slow_query_log' > #打开慢查询 > set @@global.slow_query_log='on' > ~~~ - **slow_query_log_file** 慢查询日志存放的位置(这个目录需要MySQL的运行帐号的可写权限,一般设置为MySQL的数据存放目录)。 ~~~sql #查看位置 show variables like 'slow_query_log_file'; ~~~ - **long_query_time** 查询超过多少秒才记录。 ~~~sql #查看慢查询时间 默认为10s show variables like 'long_query_time'; #更新为5s 更新当前会话的时间 set long_query_time=5 #更新全局 set global long_query_time=5 ~~~ ## 数据库锁(中) MySQL中的锁是用来管理对数据库中数据的并发访问,以确保数据的一致性和完整性。根据不同的分类标准,MySQL锁可以分为多种类型。以下是从不同角度对MySQL锁进行分类的详细介绍: ### 基于属性的分类 1. 共享锁(Share锁) - 也称为读锁,当一个事务为数据加上读锁后,其他事务只能对该数据加读锁,而不能加写锁,直到所有的读锁释放后,其他事务才能对数据进行加写锁。 - 主要用于支持并发的读取数据,读取数据时不支持修改,避免出现重复读的问题。 ~~~mysql #开启共享锁 行锁 begin; #开启事务 select * from c_goods where id=1 for share ; #或 select * from c_goods where id=1 LOCK IN SHARE MODE; ~~~ 2. 排他锁(X锁) - 也称为写锁,当一个事务为数据加上写锁时,其他事务将无法再为数据加任何锁,直到该锁释放后,其他事务才能对数据进行加锁。 - 主要用于保护数据的写操作,确保在数据修改时,不允许其他人同时修改或读取,从而避免脏数据和脏读的问题。 > **写锁(X锁)**:当事务对数据进行INSERT、UPDATE或DELETE操作时,InnoDB会自动对数据行加写锁(X锁)。在事务提交前,其他事务不能对这些数据行进行读取(需要S锁)或写入(需要X锁) ~~~mysql #显示加锁 begin; #开启事务 select * from c_goods where id=1 for update ; ~~~ ### 基于粒度分类 1. 行级锁 - 行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁(读) 和 排他锁(写)。 - 特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 > ~~~sql > #通过select语句加锁 > FOR {UPDATE | SHARE} | LOCK IN SHARE MODE > ~~~ > > * UPDATE 写锁 排它锁 > * SHARE 共享锁 读锁 2. 表级锁 - 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。 - 特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。 > ~~~sql > #表读锁 > lock table t1 read; > #表写锁 > lock table t1 write ; > > #释放锁 > unlock talbe ; # tables > ~~~ > > 3. 页级锁(了解) * 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。 * 特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般 ### 基于策略分类 1. 乐观锁 - 假设并发操作时不会发生冲突,只在提交事务时检查数据是否被其他事务修改过。 - 常用于读多写少的场景,通过版本号或时间戳等方式实现。 2. 悲观锁 - 假设并发操作时会发生冲突,因此在操作期间持有锁来避免冲突。 - 常用于写多读少的场景,通过`SELECT ... FOR UPDATE`等语句实现。 ### 其他特殊锁(了解) 1. 全局锁 - 对整个数据库实例加锁,限制除了超级用户外的所有查询和修改操作。 - 一般用于备份、恢复等操作。 2. 元数据锁(MDL) - 锁定数据库对象的元数据,如表结构,用于保证数据定义的一致性。 - 在对表进行CRUD操作时自动加上,事务提交后释放。 3. 意向锁 - 是一种表明事务将要请求什么类型锁的锁。 - 包括意向共享锁和意向排他锁,用于辅助系统在给定事务请求锁之前,判断是否会与其他事务的锁冲突。 综上所述,MySQL中的锁类型多样,每种锁都有其特定的应用场景和优势。在实际应用中,需要根据具体需求和数据库的特性来选择合适的锁策略,以保证数据的一致性和并发性能。 ## mysql 什么是死锁?怎么解决? ### 死锁的定义 在 MySQL 里,死锁指的是两个或多个事务在执行过程中,因争夺锁资源而造成的一种互相等待的局面。若没有外力的介入,这些事务都无法继续推进。 以下为一个简单的死锁示例: 假设有两个事务 `T1` 和 `T2`,以及两张表 `A` 和 `B`。 - 事务 `T1` 先对表 `A` 加锁,接着尝试对表 `B` 加锁。 - 事务 `T2` 先对表 `B` 加锁,然后尝试对表 `A` 加锁。 当事务 `T1` 持有表 `A` 的锁并请求表 `B` 的锁,而事务 `T2` 持有表 `B` 的锁并请求表 `A` 的锁时,就会形成死锁,因为两个事务都在等待对方释放锁,从而陷入无限等待的状态。 ### 解决死锁的方法 #### 1. 优化事务逻辑 - **减少事务持有锁的时间**:把不必要的操作移出事务,让事务尽快提交或回滚,以此减少锁的持有时间,降低死锁发生的概率。 - **按相同顺序访问资源**:在多个事务里,都按照相同的顺序来访问表或者行,避免循环等待的情况。 #### 2. 调整隔离级别 - **降低隔离级别**:不同的隔离级别对锁的使用和持有时间是不一样的。可以把隔离级别从较高的(如可串行化)调整为较低的(如读已提交),减少锁的竞争。不过要注意,降低隔离级别可能会引发其他并发问题,像脏读、不可重复读等。 #### 3. 设置锁超时时间 - **设置 `innodb_lock_wait_timeout` 参数**:该参数规定了事务等待锁的最长时间。当事务等待锁的时间超过这个设定值时,就会自动回滚,从而解除死锁。可以通过以下语句设置该参数: ```sql SET GLOBAL innodb_lock_wait_timeout = 60; ``` 上述语句把全局的锁等待超时时间设定为 60 秒。 #### 4. 死锁检测和回滚机制 - **InnoDB 存储引擎的死锁检测**:InnoDB 会自动检测死锁,一旦发现死锁,就会选择一个事务进行回滚,以解除死锁。被回滚的事务通常是开销较小的事务。可以通过以下语句查看死锁信息: ```sql SHOW ENGINE INNODB STATUS; ``` 该语句会显示 InnoDB 存储引擎的详细状态信息,其中包含最近一次死锁的详细情况,有助于分析死锁产生的原因。 #### 5. 数据库设计优化 - **索引优化**:合理地创建索引能够减少锁的范围,提高查询效率,进而降低死锁的发生概率。例如,在经常用于查询和更新的列上创建索引。 - **表结构优化**:避免在一张表中存储过多的数据,可根据业务需求进行表的拆分,减少锁的竞争。 ## 事务(必问 高频) > 从以下几方面说: > > 1. 事务的概念 > > 2. 事务的特性acid (可以结合日志文件说明 redo_log undo_log) > > 3. 事务的隔离级别 (4种) 隔离级别对应的问题 > > 4. 扩展: 在实际开发中如何使用事务? > > > 分为两种: > > > > 1. 单体结构 使用spring 管理事务 @Transactionl 原理用的aop 注意事务的5个属性 > > 2. 在分布式中使用分布式事务如seata 提到二段提交 三段提交 ### 定义 事务(Transaction)是数据库管理系统执行过程中的一个逻辑单位,它由一个或多个SQL语句组成,这些语句作为一个整体一起向系统提交,要么全部执行,要么全部不执行,即事务具有原子性(Atomicity)。事务的引入是为了解决数据库并发操作可能带来的数据不一致性和完整性问题。 ### 事务的特性(ACID) > 参考事务ACID核心日志机制 事务具有四个基本特性,通常简称为ACID: 1. **原子性(Atomicity)**:事务是数据库中的最小工作单元,事务中的所有操作要么全部完成,要么全部不做,事务在执行过程中发生错误会被回滚(Rollback)到事务开始前的状态,就像这个事务从未执行过一样。 2. **一致性(Consistency)**:事务必须使数据库从一个一致性状态变换到另一个一致性状态。一致性与原子性是密切相关的。 3. **隔离性(Isolation)**:数据库系统提供一定的隔离级别,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,或者说,事务处理过程中的数据变化只有在这个事务提交时才对其他事务可见。 4. **持久性(Durability)**:一旦事务被提交,它对数据库的修改就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。 ### 事务的控制(了角) 在MySQL中,你可以使用以下SQL语句来控制事务: - **START TRANSACTION** 或 **BEGIN**:显式地开启一个事务。如果不显式地开启事务,则每条单独的SQL语句都被视为一个事务执行。 - **COMMIT**:提交当前事务,使自事务开始以来对数据库所做的所有修改成为永久性的。 - **ROLLBACK**:回滚当前事务,撤销自事务开始以来所做的所有修改,使数据库回到事务开始前的状态。 - **SAVEPOINT**:在事务中创建一个保存点,一个事务中可以创建多个保存点。 - **RELEASE SAVEPOINT**:删除一个事务的保存点,当没有指定的保存点时,执行该操作会引发一个错误。 - **ROLLBACK TO SAVEPOINT**:把事务回滚到指定的保存点,而不回滚整个事务。 ### 隔离级别 为了解决并发事务可能带来的问题(如脏读、不可重复读、幻读),SQL标准定义了四个隔离级别: 1. **READ UNCOMMITTED**:最低级别,允许读取尚未提交的数据变更,可能会导致脏读、不可重复读或幻读。 2. **READ COMMITTED**:允许读取已经提交的数据,可以阻止脏读,但不可重复读和幻读仍有可能发生。 3. **REPEATABLE READ**:MySQL InnoDB的默认事务隔离级别。确保在同一个事务中多次读取同样记录的结果是一致的,但幻读仍有可能发生。 4. **SERIALIZABLE**:最高的隔离级别,通过强制事务串行执行,避免了脏读、不可重复读和幻读,但这也将大大降低数据库的并发性能。 ### 脏读、不可重复读、幻读 #### 脏读(Dirty Read) A事务读取B事务尚未提交的更改数据,并在这个数据的基础上进行操作,这个时候事务B回滚,那么A事务读到的数据是不被承认的,例如常见的取款事务和转账事务 ![è¿éåå¾çæè¿°](assets/SouthEast.png) #### 不可重复读(Unrepeatable Read) > 同一条数据,多次读取,结果不一致! 不可重复读是指A事务读取了B事务已经提交的更改数据。假如A在取款事务的过程中,B往该账户转账100,A两次读取的余额发生不一致。 ![img](assets/20191029213243618.png) ![img](assets/20191029213256402.png) 在同一事务中,T4时间和T7时间点读取的账户存款余额不一致。 #### 幻读(Phantom Read) > 多次读取数据的记录数不同 A事务读取B事务提交的新增数据,这时A事务将出现幻读的问题。幻读一般发生在计算机统计数据的事务中。假设银行系统在同一事务中两次统计存款账户的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100元,这时,两次统计的总金额将不一致。 ![img](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2JvYm96YWk4Ng==,size_16,color_FFFFFF,t_70.png) 幻读和不可重复读时两个容易混淆的概念,前者是指读到了其他已经提交事务的新增数据,而后者是指读到了已经提交事务的更改数据(更改或删除)。为了避免这两种情况,采取的对策是不同的:防止读到更数据,只需对操作的数据添加行级锁,阻止操作中的数据发生变化;而防止读到新增数据,则往往需要添加表级锁——将整张表锁定,防止新增数据。 #### 第一类丢失更新(了解) A事务撤销时,把已经提交的B事务的更新数据覆盖了。 ![img](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2JvYm96YWk4Ng==,size_16,color_FFFFFF,t_70-1706234575826-46.png) ![img](assets/20191029214813658.png) A事务在撤销时,“不小心”将B事务已经转入账户的金额给抹去了。 #### 第二类丢失更新(了解) A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失。 ![img](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2JvYm96YWk4Ng==,size_16,color_FFFFFF,t_70-1706234607623-51.png) 为了解决上述问题,数据库通过锁机制解决并发访问的问题。 * 根据锁定对象不同:分为行级锁和表级锁; * 根据并发事务锁定的关系上看:分为共享锁定和独占锁定 * 共享锁定会防止独占锁定但允许其他的共享锁定。 * 独占锁定既防止共享锁定也防止其他独占锁定。 为了更改数据,数据库必须在进行更改的行上施加行独占锁定,selsct for update语句都会隐式采用必要的行锁定 > 独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。获得排它锁的线程即能读数据又能修改数据。(读写锁) > > 共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据(读锁) 但是直接使用锁机制管理是很复杂的,基于锁机制,数据库给用户提供了不同的事务隔离级别,只要设置了事务隔离级别,数据库就会分析事务中的sql语句然后自动选择合适的锁。 ![image-20240726160027796](assets/image-20240726160027796.png) ## 事务 ACID 特性的核心日志机制 > 在介绍事务特性时可以做为扩展 在 MySQL 中,`undo_log`和`redo_log`是实现事务 ACID 特性的核心日志机制,它们分别用于**回滚操作**和**持久化变更**。以下是详细介绍: ### **1. undo_log(回滚日志)** #### **作用** - **实现原子性**:记录事务执行过程中**数据修改前的状态**,确保事务失败时可以回滚到初始状态。 - **实现 MVCC(多版本并发控制)**:为其他事务提供**一致性视图**,允许多个事务同时访问同一数据的不同版本。 #### **工作原理** - **记录变更前的数据**:当事务修改数据时,MySQL 会先将原始数据写入`undo_log`,再执行实际修改。 - **回滚机制**:若事务失败(如执行`ROLLBACK`或系统崩溃),通过`undo_log`中的记录恢复数据到修改前的状态。 - **MVCC 读视图**:当事务读取数据时,若数据已被其他事务修改但未提交,会通过`undo_log`获取修改前的版本。 #### **示例场景** ```sql START TRANSACTION; -- 假设原始数据:user_id=1, balance=1000 UPDATE users SET balance = balance - 100 WHERE user_id = 1; -- undo_log记录:user_id=1, balance=1000(修改前的值) COMMIT; -- 提交后,undo_log可能被清理(取决于隔离级别和GC策略) ``` ### **2. redo_log(重做日志)** #### **作用** - **实现持久性**:确保事务提交后,即使系统崩溃,数据变更也能通过`redo_log`恢复。 - **提升性能**:将随机 IO 转换为顺序 IO,减少磁盘寻道时间。 #### **工作原理** - **预写日志(WAL)**:事务修改数据时,先将变更写入`redo_log`(顺序写),再异步更新到数据页(随机写)。 - **崩溃恢复**:系统重启时,通过`redo_log`重新应用已提交但未写入磁盘的数据页变更。 - **循环写入**:`redo_log`是固定大小的文件组,写满后会循环覆盖最早的记录(需确保旧记录已刷新到磁盘)。 #### **示例流程** ```sql START TRANSACTION; UPDATE users SET balance = balance + 200 WHERE user_id = 2; -- 1. 记录redo_log:user_id=2, balance=1200(变更后的值) -- 2. 记录undo_log:user_id=2, balance=1000(变更前的值) -- 3. 执行实际数据页修改(可能延迟) COMMIT; -- 提交时只需确保redo_log写入磁盘(fsync) ``` ### **3. 关键区别对比** | **特性** | **undo_log** | **redo_log** | | ------------ | --------------------------------------- | ----------------------------------------- | | **记录内容** | 数据修改**前**的状态 | 数据修改**后**的状态 | | **作用** | 支持事务回滚、MVCC | 保障事务持久性、崩溃恢复 | | **存储位置** | 通常存储在共享表空间或独立表空间中 | 存储在独立的 redo 日志文件(ib_logfile*) | | **写入时机** | 事务执行过程中同步写入 | 事务执行过程中同步写入 | | **生命周期** | 事务提交后,旧版本可能被 GC(垃圾回收) | 循环使用,旧记录在数据页刷新后可覆盖 | ### **4. 常见配置参数** #### **undo_log 相关** - `innodb_undo_logs`:undo 日志文件数量(默认 128)。 - `innodb_max_undo_log_size`:单个 undo 日志文件的最大大小(默认 1GB)。 #### **redo_log 相关** - `innodb_log_file_size`:单个 redo 日志文件的大小。 - `innodb_log_files_in_group`:redo 日志文件组的数量(默认 2)。 - `innodb_flush_log_at_trx_commit`:控制 redo 日志刷新策略(0/1/2)。 ### **5. 性能与安全考量** - **undo_log**:过多的未提交事务会导致`undo_log`膨胀,可能影响性能,需合理设置`innodb_max_undo_log_size`。 - redo_log - 增大`innodb_log_file_size`可减少日志切换频率,提升写入性能。 - `innodb_flush_log_at_trx_commit=1`(默认值)确保完全持久化,但可能影响性能;设置为 2 可提升性能,但系统崩溃可能丢失部分事务。 ### **总结** - **undo_log**是**回滚的基础**,确保事务的原子性和 MVCC 的一致性视图。 - **redo_log**是**持久化的保障**,通过预写日志提升性能并确保崩溃恢复。 两者共同构成了 MySQL 事务系统的核心机制,缺一不可。 ## MySQL binlog redo_log undo_log(对比了解) MySQL 中的 `binlog`、`redo log` 和 `undo log` 是保证数据一致性、持久性和事务回滚的核心日志机制,它们的作用和工作方式各不相同: ### **1. Binlog(二进制日志)** #### **作用** - **复制(Replication)**:主从复制时,主库将 `binlog` 发送到从库执行,保证数据一致性。 - **恢复(Recovery)**:通过 `binlog` 进行 point-in-time 恢复(PITR)。 - **审计**:记录所有修改数据的 SQL 语句,用于审计和追踪。 #### **特点** - **逻辑日志**:记录 SQL 语句或行变更(取决于 `binlog_format`)。 - **事务提交时写入**:按顺序追加到文件,不影响当前事务执行。 - **循环使用**:通过 `expire_logs_days` 控制过期时间,自动删除旧日志。 #### **配置参数** ```ini # my.cnf 配置 log_bin = /var/log/mysql/mysql-bin.log # 启用 binlog 并指定路径 binlog_format = ROW # 推荐使用 ROW 格式(记录行变更) expire_logs_days = 7 # 日志保留 7 天 ``` ### **2. Redo Log(重做日志)** #### **作用** - **保证持久性(Durability)**:事务提交后,即使数据库崩溃,也能通过 `redo log` 恢复未写入磁盘的数据。 - **提高性能**:避免每次事务提交都写入磁盘,通过顺序写 `redo log` 替代随机写数据文件。 #### **特点** - **物理日志**:记录数据页的物理修改(如 “将某页某偏移量的值改为 X”)。 - **InnoDB 独有**:MyISAM 不支持 `redo log`。 - **循环写入**:固定大小的文件组(如 `ib_logfile0`、`ib_logfile1`),写满后循环覆盖。 #### **配置参数** ```ini # my.cnf 配置 innodb_log_file_size = 256M # 单个 redo log 文件大小 innodb_log_files_in_group = 2 # 日志文件数量 innodb_flush_log_at_trx_commit = 1 # 控制刷盘策略(1=最安全,0/2=性能优先) ``` ### **3. Undo Log(回滚日志)** #### **作用** - **事务回滚(Rollback)**:撤销未提交事务的修改,保证原子性。 - **MVCC(多版本并发控制)**:提供读一致性视图,使读操作无需加锁。 #### **特点** - **逻辑日志**:记录数据修改前的值(如 “将 X 从 1 改为 2” 的 undo 是 “将 X 从 2 改回 1”)。 - **持久化存储**:写入 `undo tablespace`,同时生成 `redo log` 保证持久化。 - **自动回收**:事务提交后,undo log 可能被 purge 线程删除(MVCC 不再需要时)。 #### **配置参数** ```ini # my.cnf 配置 innodb_undo_tablespaces = 2 # undo 表空间数量 innodb_undo_log_truncate = 1 # 启用 undo log 截断 innodb_max_undo_log_size = 1G # undo log 最大大小 ``` ### **三者对比** | 日志类型 | 作用 | 记录内容 | 写入时机 | 持久化方式 | | ------------ | -------------------- | ------------ | -------------------- | -------------------- | | **Binlog** | 复制、恢复、审计 | SQL 或行变更 | 事务提交后 | 追加写入 binlog 文件 | | **Redo Log** | 崩溃恢复、提高写性能 | 物理页修改 | 事务执行中(顺序写) | 循环写入固定文件组 | | **Undo Log** | 事务回滚、MVCC | 逻辑反向操作 | 事务执行中 | 写入 undo 表空间 | ### **工作流程示例** 1. **事务执行**: - 写入 `undo log`(记录旧值,支持回滚和 MVCC)。 - 修改内存中的数据页(脏页)。 - 写入 `redo log`(记录物理修改,保证崩溃恢复)。 2. **事务提交**: - 写入 `binlog`(记录逻辑变更)。 - 刷新 `redo log` 到磁盘(确保持久性)。 3. **崩溃恢复**: - 通过 `redo log` 恢复已提交但未写入磁盘的数据。 - 通过 `undo log` 回滚未提交的事务。 ### **常见问题** 1. **binlog 和 redo log 的同步**: - 通过 `sync_binlog` 和 `innodb_flush_log_at_trx_commit` 参数控制刷盘策略,平衡性能和安全性。 2. **undo log 膨胀**: - 长事务会占用大量 undo log,导致磁盘空间不足,需监控并优化事务时长。 3. **日志清理**: - binlog 通过 `PURGE BINARY LOGS` 或自动过期清理。 - redo log 自动循环覆盖。 - undo log 由 purge 线程自动回收。 理解这三种日志的机制,有助于优化 MySQL 性能、保证数据安全,以及处理故障恢复。 ## MySQL MVCC (自己认真看一下) > [MVCC多版本并发控制](https://www.jianshu.com/p/8845ddca3b23) > > [MVCC多版本并发控制-配图配文字讲解](https://blog.csdn.net/2301_77190282/article/details/145420452) ### 知道什么是当前读和快照读吗? 答:简单来说在高并发情况下当前读是获取最新的记录并且其他事务不能修改这个记录、快照读获取的有可能是老的数据。 当前读是加了锁的、加的是一种悲观锁。而快照读是没加锁的。 ### 请你讲下MVCC是什么? 答:全称Multi-Version Concurrency Control、就是一种多并发版本控制器、通俗点就是一种并发控制的方法,一般用于数据库中对数据库的并发访问。Mysql中的innoDB中就是使用这种方法来提高读写事务控制的、他大大提高了读写事务的并发性能,原因是MVCC是一种不采用锁来控制事物的方式,是一种非堵塞、同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。 小结一下 总之MVCC就是一些大牛、不满意读写使用锁去实现读写冲突问题、而提出的解决方案,一般在数据库中会使用MVCC加锁的方式解决读写、和写写冲突的问题。 ### 说一下MVCC的实现原理? 答:MVCC的实现原理是依靠记录中的3个隐含字段、undo log日志、Read View来实现的。 #### 隐含字段 有三种分别是 * 记录事务id的DB_TRX_ID、大小为6bt。 * 用来记录上一个版本数据记录的回滚指针、大小为7bt。 * 隐含的自增ID,作用是在没有设置主键的情况下自动以DB_ROW_ID产生一个簇拥索引、大小为6bt。 #### undo log日志 分为两种 * insert undo log:事务进行插入操作时产生、在事务回滚时需要,提交事务后可以被立即丢弃。 * update undo log:进行update、delete时产生的undo log、不仅在回滚事务时需要、在快照读时也需要。所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除 purge有点像jvm中的gc垃圾回收器 ![在这里插入图片描述](assets/80cd86555912848bf910fb6aa6688e92.png) 实际上对MVCC有帮助的是update undo log、undo log实际就是存在rollback中的旧记录链。 #### Read View(读视图) read view读视图就是在进行快照读时会产生一个read view视图、在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。说白了就是用来记录发生快照读那一刻所有的记录,当你下次就算有执行新的事务记录改变了read view没变读出来的数据依然是不变的。 而隔离级别中的RR(可重复读)、和RC(提交读)不同就是差在快照读时 前者创建一个快照和Read View并且下次快照读时使用的还是同一个Read View所以其他事务修改数据对他是不可见的、解决了不可重复读问题。 后者则是每次快照读时都会产生新的快照和Read View、所以就会产生不可重复读问题。 ## 数据库分页 ### 如何显示前 50 行? 在 MySQL 中, 使用以下代码查询显示前 50 行: ~~~sql SELECT * FROM TABLE LIMIT 50; ~~~ ### 如何实现分页? ~~~sql SELECT * FROM TABLE LIMIT 1,10 # 1 offset 起始位置 默认从0开始 10 row_count每页记录数 ~~~ ### 超大分页怎么处理? 在MySQL中处理大量数据的分页查询时,性能是一个重要的考虑因素。随着数据量的增加,分页查询可能会变得非常慢,特别是当请求的页码很高时。以下是一些优化MySQL分页查询性能的策略: 1. **使用索引**: 确保在用于排序和过滤的列上建立了适当的索引。例如,如果你通常按日期或ID排序,那么在这些列上创建索引将有助于提高查询性能。 2. **优化查询**: 避免在分页查询中使用`OFFSET`,因为它会导致MySQL扫描并丢弃大量的行,即使这些行最终不会被返回。相反,可以使用基于主键或唯一索引列的范围查询来实现分页。 3. **基于主键或索引列的分页**: 如果有一个自增的主键或唯一索引列(如`id`),可以记录上一次查询的最后一个`id`值,并在下一次查询时使用这个值作为起点。例如: ~~~sql SELECT * FROM your_table WHERE id > last_id ORDER BY id ASC LIMIT page_size; ~~~ 4. **记住上一页的最后一条记录**: 除了使用主键外,还可以记住上一页查询结果的最后一条记录的完整信息(如所有列的值),并在下一页查询时使用这些信息作为过滤条件。这通常涉及到更复杂的查询,但可以在没有主键或唯一索引列的情况下使用。 5. **使用覆盖索引**: 如果查询只涉及少数几列,并且这些列上都有索引,那么MySQL可以直接从索引中读取数据,而不需要回表查询。这可以显著提高查询性能。 针对大偏移量的情况,可以采用`覆盖索引 + 子查询`的方式来优化。示例如下: ~~~sql SELECT * FROM users WHERE id >= (SELECT id FROM users ORDER BY id LIMIT 100000, 1) LIMIT 10; ~~~ 6. **延迟关联**: 如果查询涉及多个表,并且分0的列进行的,那么可以先对这个表进行分页查询,然后再与其他表进行关联。这可以减少需要处理的数据量。 7. **优化服务器配置**: 确保MySQL服务器的配置是优化的,特别是与内存和缓存相关的配置。这可以帮助提高查询性能。 8. **考虑使用全文索引或搜索引擎**: 对于非常大的数据集和复杂的查询,可能需要考虑使用全文索引或专门的搜索引擎(如Elasticsearch)来提高查询性能。 9. **分析查询计划**: 使用`EXPLAIN`语句来分析查询计划,并根据结果调整查询和索引。 10. **避免在分页查询中使用复杂的计算或函数**: 这些操作会增加查询的复杂性,并可能导致性能下降。 请注意,没有一种通用的方法可以适用于所有情况。最佳实践是根据具体的应用场景和数据特点来选择和优化分页策略。如果数据量非常大,并且分页查询的性能仍然是一个问题,那么可能需要考虑更高级的技术,如数据分片、数据归档或使用专门的数据库系统来处理大数据。 ## 连接查询 在数据库查询中,理解不同类型的连接(JOIN)是非常重要的,尤其是内连接(INNER JOIN)、外连接(包括左外连接LEFT JOIN、右外连接RIGHT JOIN和全外连接FULL OUTER JOIN,尽管不是所有数据库系统都直接支持全外连接)、交叉连接(CROSS JOIN)以及它们与笛卡尔积(Cartesian Product)之间的关系。 ![img](assets/5d30954011d272310b20723931edb42f.jpg) ### **内连接(INNER JOIN)** 是一种数据库操作,用于从多个表中返回满足连接条件的记录。它只返回两个表中匹配的行,丢弃不满足条件的行。 #### 主要特点 1. **匹配条件**:内连接主要依据 ON 子句中设定的条件来匹配两个表的记录。 2. **结果集**:最终返回的结果集仅包含同时满足连接条件的记录。 3. **关键字**:在使用内连接时,`INNER JOIN` 关键字是可以省略的,直接用 `JOIN` 效果相同 ### 外连接(OUTER JOIN) 外连接分为左外连接(LEFT OUTER JOIN 或 LEFT JOIN)、右外连接(RIGHT OUTER JOIN 或 RIGHT JOIN)和全外连接(FULL OUTER JOIN)。这些连接类型返回至少在一个表中满足连接条件的行,并且还包括那些不满足连接条件的行,不过这些行的对应部分将包含NULL。 - **左外连接**(LEFT JOIN):返回左表(LEFT JOIN左侧的表)的所有行,即使右表中没有匹配。如果右表中没有匹配,则结果中右表的部分将包含NULL。 - **右外连接**(RIGHT JOIN):与左外连接相反,它返回右表(RIGHT JOIN右侧的表)的所有行,即使左表中没有匹配。 - **全外连接**(FULL OUTER JOIN):返回左表和右表中的所有行。当某行在另一个表中没有匹配时,则另一个表的部分将包含NULL。需要注意的是,并非所有数据库系统都直接支持全外连接,可能需要通过其他方式(如UNION)来实现。 > ### 核心特性 > > 1. **保留未匹配行** > 外连接的核心是保留至少一个表的全部数据,即使无匹配项。 > 2. **NULL 填充** > 未匹配的列用 `NULL` 填充,可通过 `IS NULL` 或 `IS NOT NULL` 过滤。 > 3. **多表连接** > 支持链式外连接,但需注意逻辑顺序 **交叉连接(CROSS JOIN)** 交叉连接返回第一个表中的每一行与第二个表中的每一行组合的结果。这通常会产生一个非常大的结果集,特别是当两个表都有很多行时。交叉连接的结果是一个笛卡尔积。 ### 笛卡尔积(Cartesian Product) 笛卡尔积是两个集合中所有元素的组合。在数据库查询的上下文中,当两个表进行交叉连接时,得到的结果集就是这两个表的笛卡尔积。每个来自第一个表的行都会与第二个表的每一行组合。 ![img](assets/0d574fc356251830dd0584914e53aa80.jpg) ### 总结 - **内连接**:只返回两个表中匹配的记录。 - **外连接**:返回至少一个表中所有记录,并包括那些在另一个表中没有匹配的记录(以NULL填充)。 - **交叉连接**(或笛卡尔积):返回两个表中所有可能的行组合。 了解这些不同类型的连接对于编写有效和高效的数据库查询至关重要。 ## 内外连接优化建议(了解) 内连接(INNER JOIN)和外连接(OUTER JOIN)的性能差异主要取决于数据量、索引使用和查询逻辑。以下是核心对比和优化建议: ### **1. 性能对比** | **场景** | **内连接(INNER JOIN)** | **外连接(OUTER JOIN)** | | ------------- | ---------------------------- | ------------------------------------------ | | **数据量** | 仅返回匹配行,结果集可能更小 | 需保留至少一个表的全部行,结果集可能更大 | | **匹配条件** | 只需处理匹配的行,计算量较小 | 需处理所有行(包括未匹配的行),计算量较大 | | **索引依赖** | 依赖索引快速定位匹配行 | 同样依赖索引,但需额外处理未匹配行 | | **NULL 处理** | 不涉及 NULL 填充 | 需为未匹配的行填充 NULL 值 | ### **2. 影响性能的关键因素** #### **(1)数据分布** - **内连接**:若两表关联字段存在大量匹配,性能较高。 - **外连接**:若存在大量未匹配行,外连接需额外处理 NULL 填充,可能变慢。 #### **(2)索引优化** - **内连接**:在关联字段上创建索引可显著加速匹配(如 `WHERE 表1.列 = 表2.列`)。 - **外连接**:索引同样重要,但对未匹配行的扫描无法避免(如左连接需扫描左表所有行)。 #### **(3)数据库实现** - **MySQL**:外连接(尤其是 FULL OUTER JOIN)可能需通过 `UNION` 实现,性能低于直接支持的数据库(如 PostgreSQL)。 - **大表连接**:外连接可能产生大量中间结果,导致内存溢出或磁盘临时文件增加。 ### **3. 优化建议** #### **(1)优先使用内连接** - **场景**:仅需匹配数据时,内连接通常更快(如 `SELECT * FROM 订单 JOIN 用户 ON 订单.用户ID = 用户.ID`)。 #### **(2)外连接优化** - **添加索引**:为关联字段添加索引(如 `CREATE INDEX idx_user_id ON 订单(用户ID)`)。 - **过滤数据**:在连接前通过 `WHERE` 子句减少数据量(如 `SELECT * FROM 订单 LEFT JOIN 用户 ON 订单.用户ID = 用户.ID WHERE 订单.日期 > '2023-01-01'`)。 - **避免全外连接**:MySQL 中用 `UNION` 替代 `FULL OUTER JOIN`,但需注意去重开销。 #### **(3)数据库特定优化** - **MySQL**:使用 `EXPLAIN` 分析查询执行计划,检查是否存在全表扫描。 - **PostgreSQL**:利用 `LEFT JOIN LATERAL` 优化嵌套子查询。 ### **4. 典型场景性能差异** | **场景** | **内连接性能** | **外连接性能** | **建议** | | -------------------- | -------------- | -------------- | -------------------------------------- | | 两表关联字段均有索引 | 高 | 中 | 优先内连接,外连接需评估 NULL 处理开销 | | 大表左连接小表 | 高 | 中 | 为大表关联字段添加索引 | | 需要返回未匹配的行 | 不适用 | 必须使用 | 优化索引,避免全表扫描 | | MySQL 全外连接 | 不适用 | 低(需 UNION) | 改用两次左连接 + UNION | ### **5. 性能测试示例** 假设有 100 万行的 `订单表` 和 10 万行的 `用户表`: ```sql -- 内连接(假设 90% 的订单有用户匹配) EXPLAIN SELECT * FROM 订单 INNER JOIN 用户 ON 订单.用户ID = 用户.ID; -- 左连接(需扫描全部 100 万行订单) EXPLAIN SELECT * FROM 订单 LEFT JOIN 用户 ON 订单.用户ID = 用户.ID; ``` 通过 `EXPLAIN` 查看是否使用索引(`type=ref` 或 `type=eq_ref` 表示高效)。 ### **总结** - **内连接**:在匹配率高、索引优化时性能最优。 - **外连接**:无法避免扫描全量数据,需通过索引和过滤减少数据量。 - **关键**:通过 `EXPLAIN` 分析执行计划,针对性优化索引和查询逻辑。 ## MySQL中常用 的函数有哪些?(中频) ### 一、数字函数 - **CEIL(x)/CEILING(x)**:返回大于或等于x的最小整数。 - **FLOOR(x)**:返回小于或等于x的最大整数。 - **ROUND(x)**:返回离x最近的整数;ROUND(x,y)对x进行四舍五入的操作,返回值保留小数点后面指定的y位。 ### 二、字符串函数 - **CONCAT(s1,s2,...)**:将s1,s2等多个字符串合并为一个字符串,分隔符默认是 逗号 - **CONCAT_WS(x,s1,s2,...)**:同CONCAT(s1,s2,...),但是每个字符串之间要加上x,x可以是分隔符。 - GROUP_CONCAT(expr) 将多条记录合并为一个 - **LENGTH(str)/CHAR_LENGTH(s)/CHARACTER_LENGTH(s)**:返回字符串s的字符数(不是字节长度)。 - **LOWER(s)/LCASE(s)**:将字符串s的所有字母变成小写字母。 - **UPPER(s)/UCASE(s)**:将字符串s的所有字母变成大写字母。 - **TRIM(s)**:去掉字符串s开始和结尾处的空格。 - **LTRIM(s)**:去掉字符串s开始处的空格。 - **RTRIM(s)**:去掉字符串s结尾处的空格。 - **SUBSTR(s, start, length)/SUBSTRING(s, start, length)**:从字符串s的start位置截取长度为length的子字符串。 - **REPLACE(s,from_str,to_str)**:把字符串s中的from_str内容替换为to_str。 - **POSITION(s1 IN s)/LOCATE(s1,s)**:从字符串s中获取s1的开始位置。 ### 三、日期函数 - **CURDATE()/CURRENT_DATE()**:返回当前日期。 - **CURRENT_TIME()/CURTIME()**:返回当前时间。 - **NOW()**:返回当前日期和时间。 - **DATE_ADD(date, INTERVAL expr type)**:计算起始日期date加上一个时间段后的日期。 - **DATEDIFF(d1,d2)**:计算日期d1和d2之间相隔的天数。 - **DATE_FORMAT(date,format)**:按表达式format的要求显示日期date。 ### 四、聚合函数 - **AVG(expression)**:返回某列的平均值。 - **COUNT(expression)**:返回某列/某组/整表的行数。 - **MAX(expression)**:返回某列的最大值。 - **MIN(expression)**:返回某列的最小值。 - **SUM(expression)**:返回指定字段的总和。 ### 五、流程控制函数 - **IF(test,v1,v2)**:如果表达式test成立,返回结果v1;否则,返回结果v2。 - ifnull(exp1,exp2) 如果表达式exp1不是null 返回exp1,否则返回exp2 ## datetime timestamp 区别(了解) > 重点描述: 存储范围 存储空间 ### 1. 存储范围(知道) - **DATETIME**:可以表示的日期时间范围从`1000-01-01 00:00:00`到`9999-12-31 23:59:59`。这个范围非常广泛,适用于需要存储历史久远数据的应用场景。 - **TIMESTAMP**:其标准范围是从`1970-01-01 00:00:01` UTC到`2038-01-19 03:14:07` UTC(即所谓的“2038年问题”)。然而,值得注意的是,MySQL 5.6.5及以后的版本支持一个扩展的TIMESTAMP范围,从`1970-01-01 00:00:00` UTC到`2106-02-07 06:28:15` UTC。尽管如此,TIMESTAMP的范围仍比DATETIME小。 ### 2. 存储方式 - **DATETIME**:存储日期和时间时,不进行时区转换。它直接存储日期和时间值,因此在不同的时区查询时,显示的值是固定的,不会自动转换为当前会话的时区。 - **TIMESTAMP**:在存储时,MySQL会自动将日期和时间从当前时区转换为UTC(协调世界时)进行存储。在查询时,又会将其转换回客户端的时区进行显示。这使得TIMESTAMP在跨时区的应用中更为方便。 ### 3. 默认值与NULL值 - **DATETIME**:列默认可以为NULL,即如果不指定值,则默认为NULL。 - **TIMESTAMP**:列默认为NOT NULL,如果尝试创建一个TIMESTAMP列并设置其默认值为NULL,MySQL会自动将其默认值设置为`0000-00-00 00:00:00`(但在严格模式下,这可能会导致错误)。此外,一个表中可以有多个TIMESTAMP列,但只有一个TIMESTAMP列可以设置为自动更新为当前时间戳(ON UPDATE CURRENT_TIMESTAMP)。 ### 4. 存储空间(知道) - **DATETIME**:通常需要8个字节的存储空间。 - **TIMESTAMP**:通常只需要4个字节的存储空间(尽管在某些情况下,如MySQL 5.6.4及以前版本,TIMESTAMP的存储空间可能因存储的分数秒精度而有所不同)。 ### 5. 使用场景 - **DATETIME**:适用于需要大范围日期时间值且不需要自动时区转换的场景。 - **TIMESTAMP**:适用于需要自动时区转换、记录数据创建或修改时间戳的场景,尤其是当需要节省存储空间时(因为TIMESTAMP占用的空间较小)。 ## sql 语句 执行顺序(中) SQL的执行顺序指的是SQL查询语句中各个子句的逻辑处理顺序,这个顺序与我们在编写SQL查询时书写的顺序并不完全相同。了解SQL的执行顺序对于优化查询、理解查询结果以及调试查询问题至关重要。SQL查询语句的执行顺序大致如下: 1. **FROM子句**:首先,SQL会执行FROM子句,以确定查询中涉及的所有表和视图。如果存在多表连接(如JOIN操作),则在这一步也会处理表之间的连接关系。FROM子句还负责数据的组装,即将来自不同数据源的数据合并成一个结果集。 2. **JOIN**:如果有多个表参与查询,则指定它们之间的连接类型和条件。 3. **ON**:在执行JOIN操作时,应用ON子句中的条件来筛选连接的记录。 4. **WHERE子句**:接下来,SQL会执行WHERE子句,对FROM子句产生的结果集进行筛选。WHERE子句中的条件用于过滤掉不满足条件的行,只保留满足条件的行进入下一步处理。 5. **GROUP BY子句**:如果查询中包含了GROUP BY子句,则SQL会在WHERE子句之后执行GROUP BY子句。GROUP BY子句将结果集中的行按照一个或多个列的值进行分组,每个分组包含具有相同列值的行。 6. **HAVING子句**:HAVING子句通常与GROUP BY子句一起使用,用于对分组后的结果进行过滤。与WHERE子句不同的是,HAVING子句可以对聚合函数的结果进行过滤。如果查询中没有GROUP BY子句,HAVING子句也可以单独使用,但此时它会将整个结果集视为一个大的分组。 7. **SELECT子句**:在WHERE、GROUP BY和HAVING子句处理完毕后,SQL会执行SELECT子句。SELECT子句指定了要从结果集中检索的列或表达式。如果查询中包含了聚合函数(如SUM、AVG、COUNT等),则这些函数会在这一步进行计算。 8. **DISTINCT关键字**(如果使用了):如果SELECT子句中包含了DISTINCT关键字,则SQL会在返回最终结果之前去除重复的行。 9. **ORDER BY子句**:最后,如果查询中包含了ORDER BY子句,则SQL会根据ORDER BY子句中的列或表达式对结果集进行排序。排序可以是升序(ASC)或降序(DESC),默认为升序。 10. **LIMIT/OFFSET子句**(或等效的):在某些数据库系统中,可能还允许使用LIMIT/OFFSET子句(或其等效物,如FETCH FIRST、OFFSET ROWS FETCH NEXT等)来限制返回的记录数或跳过某些记录。这些子句通常在查询的最后阶段执行。 ## MySQL B+Tree索引和Hash索引的区别?(高) - Hash索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位; - B+树索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问; 那为什么大家不都用Hash索引而还要使用B+树索引呢? MySQL 的 Hash 索引和 B+Tree 索引是两种不同的数据结构,它们在应用场景、性能特点等方面存在显著差异。以下是它们的主要区别: ### 1. **数据结构** - **Hash 索引**:使用哈希表实现,通过哈希函数将键值映射到哈希表中的位置。 - **B+Tree 索引**:使用 B + 树结构,所有数据都存储在叶子节点,非叶子节点仅用于索引导航。 ### 2. **适用场景** - **Hash 索引**:适用于精确匹配(如`WHERE col = value`),不支持范围查询(如`WHERE col > value`)。 - **B+Tree 索引**:支持精确匹配和范围查询,适用于各种查询条件。 ### 3. **查询性能** - **Hash 索引**:哈希冲突会降低性能,但平均时间复杂度为 O (1)。 - **B+Tree 索引**:时间复杂度为 O (log n),范围查询效率高。 ### 4. **排序支持** - **Hash 索引**:不支持排序,因为哈希表中的数据是无序的。 - **B+Tree 索引**:支持排序,因为 B + 树的叶子节点是有序的。 ### 5. **索引键值** - **Hash 索引**:仅支持等值比较,如`=`、`<=>`、`IN()`。 - **B+Tree 索引**:支持各种比较操作,如`>`、`<`、`BETWEEN`等。 ### 6. **存储空间** - **Hash 索引**:通常比 B+Tree 索引更紧凑,因为只需存储哈希值和指针。 - **B+Tree 索引**:需要存储键值和指针,空间占用较大。 ### 7. **应用场景** - **Hash 索引**:适用于内存表(Memory 引擎)和哈希分区。 - **B+Tree 索引**:适用于 InnoDB、MyISAM 等大多数存储引擎。 ### 8. **哈希冲突处理** - **Hash 索引**:通过链表或开放寻址法处理冲突。 - **B+Tree 索引**:不存在哈希冲突问题。 ### 总结 | 特性 | Hash 索引 | B+Tree 索引 | | -------- | ---------------- | ------------------ | | 数据结构 | 哈希表 | B + 树 | | 查询类型 | 等值查询 | 等值查询、范围查询 | | 排序支持 | 不支持 | 支持 | | 空间效率 | 高 | 中等 | | 适用场景 | 内存表、哈希分区 | 大多数场景 | **选择建议**: - 如果查询以等值匹配为主(如缓存系统),可考虑 Hash 索引。 - 如果需要支持范围查询或排序,应使用 B+Tree 索引。 ## MySQL 高并发环境解决方案? MySQL 高并发环境解决方案 分库 分表 分布式 增加二级缓存。。。。。 **需求分析**:互联网单位 每天大量数据读取,写入,并发性高。 - **现有解决方式**:水平分库分表,由单点分布到多点数据库中,从而降低单点数据库压力。 - **集群方案**:解决DB宕机带来的单点DB不能访问问题。 - **读写分离策略**:极大限度提高了应用中Read数据的速度和并发量。无法解决高写入压力。 ## 数据库三大范式是什么(高频) #### 三大范式 * 第一范式(1 NF):字段不可再拆分。 * 第二范式(2 NF):表中任意一个主键或任意一组联合主键,可以确定除该主键外的所有的非主键值。 * 第三范式(3 NF):在任一主键都可以确定所有非主键字段值的情况下,不能存在某非主键字段 A 可以获取 某非主键字段 B。 #### 理解 ##### 第一范式 - 1NF 遵循原子性。即,**表中字段的数据,不可以再拆分**。 先看一个不符合第一范式的表结构,如下: | 员工编码 | 姓名 | 年龄 | | -------- | ---------- | ---- | | 001 | 销售部小张 | 28 | | 002 | 运营部小黄 | 25 | | 003 | 技术部小高 | 22 | 在这一个表中的,姓名 字段下的数据是可以再进行拆分的,因此它不符合第一范式,那怎么样才符合第一范式呢?如下: | 员工编码 | 部门 | 姓名 | 年龄 | | -------- | ------ | ---- | ---- | | 001 | 销售部 | 小张 | 28 | | 002 | 运营部 | 小黄 | 25 | | 003 | 技术部 | 小高 | 22 | 那是否遵循第一范式就一定是好的呢?如下: | 员工编码 | 姓名 | 地址 | | -------- | ---- | ------------------ | | 001 | 小张 | 江西省南昌市东湖区 | | 002 | 小黄 | 广东省佛山市禅城区 | | 003 | 小高 | 湖北省武汉市新洲区 | 通过观察上述表结构,我们发现,地址是可以再进一步拆分的,比如: | 员工编码 | 姓名 | 省 | 市 | 区 | | -------- | ---- | ------ | ------ | ------ | | 001 | 小张 | 江西省 | 南昌市 | 东湖区 | | 002 | 小黄 | 广东省 | 佛山市 | 禅城区 | | 003 | 小高 | 湖北省 | 武汉市 | 新洲区 | 虽然拆分后,看上去更符合第一范式了,但是如果项目就只需要我们输出一个完整地址呢?那明显是表在没拆分的时候会更好用。 所以范式只是给了我们一个参考,我们更多的是要根据项目实际情况设计表结构。 ##### 第二范式 - 2NF 在满足第一范式的情况下,遵循唯一性,消除部分依赖。即,**表中任意一个主键或任意一组联合主键,可以确定除该主键外的所有的非主键值。** 再通俗点讲就是,**一个表只能描述一件事情**。 我们用一个经典案例进行解析。 | 学号 | 姓名 | 年龄 | 课程名称 | 成绩 | 学分 | | ---- | ---- | ---- | -------- | ---- | ---- | | 001 | 小张 | 28 | 语文 | 90 | 3 | | 001 | 小张 | 28 | 数学 | 90 | 2 | | 002 | 小黄 | 25 | 语文 | 90 | 3 | | 002 | 小黄 | 25 | 语文 | 90 | 3 | | 003 | 小高 | 22 | 数学 | 90 | 2 | 我们先分析一下表结构。 1. 假设学号是表中的唯一主键,那由学号就可以确定姓名和年龄了,但是却不能确定课程名称和成绩。 2. 假设课程名称是表中的唯一主键,那由课程名称就可以确定学分了,但是却不能确定姓名、年龄和成绩。 3. 虽然通过学号和课程名称的联合主键,可以确定除联合主键外的所有的非主键值,但是基于上述两个假设,也不符合第二范式的要求。 那我们应该**如何调整表结构**,让它能复合第二范式的要求呢? 我们可以**基于上述的三种主键的可能,拆分成 3 张表,保证一张表只描述一件事情**。 1. 学生表 - 学号做主键 | 学号 | 姓名 | 年龄 | | ---- | ---- | ---- | | 001 | 小张 | 28 | | 002 | 小黄 | 25 | | 003 | 小高 | 22 | 2. 课程表 - 课程名称做主键 | 课程名称 | 学分 | | -------- | ---- | | 语文 | 3 | | 数学 | 2 | 3. 成绩表 - 学号和课程名称做联合主键 | 学号 | 课程名称 | 成绩 | | ---- | -------- | ---- | | 001 | 语文 | 90 | | 001 | 数学 | 90 | | 002 | 语文 | 90 | | 002 | 语文 | 90 | | 003 | 数学 | 90 | 这时候我们可能会想,为什么我们就要遵循第二范式呢?**不遵循第二范式会造成什么样的后果呢**? 1. 造成整表的数据冗余。 如,学生表,可能我就只有2个学生,每个学生都有许多的信息,比如,年龄、性别、身高、住址......如果与课程信息放到同一张表中,可能每个学生有3门课程,那数据总条数就会变成6条了。但是通过拆分,学生表我们只需要存储 2 条学生信息,课程表只需要存储 3 条课程信息,成绩表就只需保留学号、课程名称和成绩字段。 2. 更新数据不方便。 假设,课程的学分发生了变更,那我们就需要把整表关于该课程的学分都要更新一次,但如果我们拆分出课程表,那我们就只需要把课程表中的课程信息更新就行。 3. 插入数据不方便或产生异常。 ① 假设主键是学号或课程名称,我们新增了某个课程,需要把数据插入到表中,这时,可能只有部分人有选修这门课程,那我们插入数据的时候还要规定给哪些人插入对应的课程信息,同时可能由于成绩还没有,我们需要对成绩置空,后续有成绩后还得重新更新一遍。 ② 假设主键是学号和课程名称的联合主键。同样也是新增了某课程,但是暂时没有人选修这门课,缺少了学号主键字段数据,会导致课程信息无法插入。 ##### 第三范式 - 3NF 在满足第二范式的情况下,消除传递依赖。即,**在任一主键都可以确定所有非主键字段值的情况下,不能存在某非主键字段 A 可以获取 某非主键字段 B**。 仍然用一个经典例子来解析 | 学号 | 姓名 | 班级 | 班主任 | | ---- | ---- | ------------- | ------ | | 001 | 小黄 | 一年级(1)班 | 高老师 | 这个表中,学号是主键,它可以唯一确定姓名、班级、班主任,符合了第二范式,但是在非主键字段中,我们也可以通过班级推导出该班级的班主任,所以它是不符合第三范式的。 那怎么设计表结构,才是符合第三范式的呢? 1. 学生表 | 学号 | 姓名 | 班级 | | ---- | ---- | ------------- | | 001 | 小黄 | 一年级(1)班 | 2. 班级表 | 班级 | 班主任 | | ------------- | ------ | | 一年级(1)班 | 高老师 | 通过把班级与班主任的映射关系另外做成一张映射表,我们就成功地消除了表中的传递依赖了。 ### mysql有关权限的表都有哪几个 MySQL服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库里,由mysql\_install\_db脚本初始化。这些权限表分别user,db,table\_priv,columns\_priv和host。下面分别介绍一下这些表的结构和内容: - user权限表:记录允许连接到服务器的用户帐号信息,里面的权限是全局级的。 - db权限表:记录各个帐号在各个数据库上的操作权限。 - table_priv权限表:记录数据表级的操作权限。 - columns_priv权限表:记录数据列级的操作权限。 - host权限表:配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受GRANT和REVOKE语句的影响。 ## mysql中 in 和 exists 区别(中) #### in关键字 确定给定的值是否与子查询或列表中的值相匹配。in在查询的时候,首先查询子查询的表,然后将内表和外表做一个笛卡尔积,然后按照条件进行筛选。所以相对内表比较小的时候,in的速度较快。 ~~~sql select * from A where id in (select id from B) ~~~ ​ 过程:in是先查询内表【select id from B】,再把内表结果与外表【select * from A where id in …】匹配,对外表使用索引,而内表多大都需要查询,不可避免,故外表大的使用in,可加快效率。 > 当A表的数据集大于B表的数据集时,用in优于exists。【in适合外部表数据大于子查询的表数据的业务场景】 #### exists关键字 指定一个子查询,检测行的存在。遍历循环外表,然后看外表中的记录有没有和内表的数据一样的。匹配上就将结果放入结果集中。 语法格式: ~~~sql select * from A where exists (select 1 from B where B.id = A.id) ~~~ 执行过程:exists是对外表【select * from A where exists …】做loop循环,每次loop循环再对内表(子查询)【select 1 from B where B.id = A.id】进行查询,因为对内表的查询使用的索引(内表效率高,故可用大表),而外表有多大都需要遍历,不可避免(所以尽量用小表),故内表大的使用exists,可加快效率。 #### in 与 exists 的区别 mysql中的in语句是把外表和内表作hash 连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。一直大家都认为exists比in语句的效率要高,这种说法其实是不准确的。这个是要区分环境的。 1. 如果查询的两个表大小相当,那么用in和exists差别不大。 2. 如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in。 3. not in 和not exists:如果查询语句使用了not in(索引失效),那么内外表都进行全表扫描,没有用到索引;而not extsts的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。 ## union union all 区别(了解) `UNION` 和 `UNION ALL` 都是 SQL 语句中用来合并两个或多个 SELECT 语句的结果集的操作符,但它们之间有一些关键的区别: 1. 去重 - `UNION`:在合并结果集时,会自动去除重复的行,只保留唯一的结果。 - `UNION ALL`:不会去除重复的行,所有的结果都会被包含在最终的结果集中,包括重复的行。 2. 性能 - `UNION`:因为需要去重,所以可能会有额外的性能开销。 - `UNION ALL`:通常比 `UNION` 更快,因为它不需要检查重复的行。 3. 使用场景 - 当你需要合并的结果集中不包含重复的行时,使用 `UNION`。 - 当你需要包含所有结果,包括重复的行时,使用 `UNION ALL`。 总结来说,`UNION` 和 `UNION ALL` 的主要区别在于是否去除重复的行,以及由此带来的性能差异。选择哪一个取决于你的具体需求。 ## drop、delete与truncate的区别 - 三者都表示删除,但是三者有一些差别: | 比较 | Delete | Truncate | Drop | | -------- | ---------------------------------------- | ------------------------------ | ---------------------------------------------------- | | 类型 | 属于DML | 属于DDL | 属于DDL | | 回滚 | 可回滚 | 不可回滚 | 不可回滚 | | 删除内容 | 表结构还在,删除表的全部或者一部分数据行 | 表结构还在,删除表中的所有数据 | 从数据库中删除表,所有的数据行,索引和权限也会被删除 | | 删除速度 | 删除速度慢,需要逐行删除 | 删除速度快 | 删除速度最快 | - 因此,在不再需要一张表的时候,用drop;在想删除部分数据行时候,用delete;在保留表而删除所有数据的时候用truncate。 # 书写高质量SQL的30条建议 ### 1、查询SQL尽量不要使用select *,而是select具体字段。 反例子: ```sql select * from employee; ``` 正例子: ```sql select id,name from employee; ``` 理由: - 只取需要的字段,节省资源、减少网络开销。 - select * 进行查询时,很可能就不会使用到覆盖索引了,就会造成回表查询。 > 回表查询是指在数据库系统中,当一个查询请求涉及多个索引时,可能需要在数据表的实际物理页面上进行额外的查找以获取完整的数据记录 ### 2、如果知道查询结果只有一条或者只要最大/最小一条记录,建议用limit 1 假设现在有employee员工表,要找出一个名字叫jay的人. ```sql CREATE TABLE `employee` ( `id` int(11) NOT NULL, `name` varchar(255) DEFAULT NULL, `age` int(11) DEFAULT NULL, `date` datetime DEFAULT NULL, `sex` int(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` 反例: ```sql select id,name from employee where name='jay' ``` 正例 ```sql select id,name from employee where name='jay' limit 1; ``` 理由: - 加上limit 1后,只要找到了对应的一条记录,就不会继续向下扫描了,效率将会大大提高。 - 当然,如果name是唯一索引的话,是不必要加上limit 1了,因为limit的存在主要就是为了防止全表扫描,从而提高性能,如果一个语句本身可以预知不用全表扫描,有没有limit ,性能的差别并不大。 ### 3、应尽量避免在where子句中使用or来连接条件 新建一个user表,它有一个普通索引userId,表结构如下: ```sql CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `userId` int(11) NOT NULL, `age` int(11) NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`), KEY `idx_userId` (`userId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` 假设现在需要查询userid为1或者年龄为18岁的用户,很容易有以下sql 反例: ```sql select * from user where userid=1 or age =18 ``` 正例: ```sql //使用union all select * from user where userid=1 union all select * from user where age = 18 //或者分开两条sql写: select * from user where userid=1 select * from user where age = 18 ``` 理由: - 使用or可能会使索引失效,从而全表扫描。 > 对于or+没有索引的age这种情况,假设它走了userId的索引,但是走到age查询条件时,它还得全表扫描,也就是需要三步过程: 全表扫描+索引扫描+合并 如果它一开始就走全表扫描,直接一遍扫描就完事。 mysql是有优化器的,处于效率与成本考虑,遇到or条件,索引可能失效,看起来也合情合理。 ### 4、优化limit分页 我们日常做分页需求时,一般会用 limit 实现,但是当偏移量特别大的时候,查询效率就变得低下。 反例: ```sql select id,name,age from employee limit 10000,10 ``` 正例: ```sql #方案一 :返回上次查询的最大记录(偏移量) select id,name from employee where id>10000 limit 10. #方案二:order by + 索引 select id,name from employee order by id limit 10000,10 #方案三:在业务允许的情况下限制页数: #方案四: 使用覆盖索引+子查询 ``` 理由: - 当偏移量最大的时候,查询效率就会越低,因为Mysql并非是跳过偏移量直接去取后面的数据,而是先查询出偏移量+要取的条数的 ,然后再把前面偏移量这一段的数据抛弃掉再返回的。 - 如果使用优化方案一,返回上次最大查询记录(偏移量),这样可以跳过偏移量,效率提升不少。 - 方案二使用order by+索引,也是可以提高查询效率的。 - 方案三的话,建议跟业务讨论,有没有必要查这么后的分页啦。因为绝大多数用户都不会往后翻太多页。 ### 5、优化你的like语句 日常开发中,如果用到模糊关键字查询,很容易想到like,但是like很可能让你的索引失效。 反例: ```sql select userId,name from user where userId like '%123'; ``` 正例: ```sql select userId,name from user where userId like '123%'; ``` 理由: - 把%放前面,并不走索引,如下: ![img](assets/170f7f2739040e5btplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) - 把% 放关键字后面,还是会走索引的。如下: ![img](assets/170f7f224aa3dfedtplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) ### 6、使用where条件限定要查询的数据,避免返回多余的行 假设业务场景是这样:查询某个用户是否是会员。曾经看过老的实现代码是这样。。。 反例: ```sql List userIds = sqlMap.queryList("select userId from user where isVip=1"); boolean isVip = userIds.contains(userId); ``` 正例: ```sql Long userId = sqlMap.queryObject("select userId from user where userId='userId' and isVip='1' ") boolean isVip = userId!=null; ``` 理由: - 需要什么数据,就去查什么数据,避免返回不必要的数据,节省开销。 ### 7、尽量避免在索引列上使用mysql的内置函数 业务需求:查询最近七天内登陆过的用户(假设loginTime加了索引) 反例: ```sql select userId,loginTime from loginuser where Date_ADD(loginTime,Interval -7 DAY) >=now(); ``` 正例: ```sql explain select userId,loginTime from loginuser where loginTime >= Date_ADD(NOW(),INTERVAL - 7 DAY); ``` 理由: - 索引列上使用mysql的内置函数,索引失效 ![img](assets/170fd5f19265afa9tplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) - 如果索引列不加内置函数,索引还是会走的。 ![img](assets/170f875955e8b7c0tplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) ### 8、应尽量避免在 where 子句中对字段进行表达式操作,这将导致系统放弃使用索引而进行全表扫 反例: ```sql select * from user where age-1 =10; ``` 正例: ```sql select * from user where age =11; ``` 理由: - 虽然age加了索引,但是因为对它进行运算,索引直接迷路了。。。 ![img](assets/170f85b4a47dc153tplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) ### 9、Inner join 、left join、right join,优先使用Inner join,如果是left join,左边表结果尽量小 > - Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集 > - left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。 > - right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。 都满足SQL需求的前提下,推荐优先使用Inner join(内连接),如果要使用left join,左边表数据结果尽量小,如果有条件的尽量放到左边处理。 反例: ```sql select * from tab1 t1 left join tab2 t2 on t1.size = t2.size where t1.id>2; ``` 正例: ```sql select * from (select * from tab1 where id >2) t1 left join tab2 t2 on t1.size = t2.size; ``` 理由: - 如果inner join是等值连接,或许返回的行数比较少,所以性能相对会好一点。 - 同理,使用了左连接,左边表数据结果尽量小,条件尽量放到左边处理,意味着返回的行数可能比较少。 ### 10、应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。 反例: ```sql select age,name from user where age <>18; ``` 正例: ```sql //可以考虑分开两条sql写 select age,name from user where age <18; select age,name from user where age >18; ``` 理由: - 使用!=和<>很可能会让索引失效 ![img](assets/170f8d5a32598527tplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) ### 11、使用联合索引时,注意索引列的顺序,一般遵循最左匹配原则。 表结构:(有一个联合索引idx_userid_age,userId在前,age在后) ```sql CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `userId` int(11) NOT NULL, `age` int(11) DEFAULT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`), KEY `idx_userid_age` (`userId`,`age`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; ``` 反例: ```sql select * from user where age = 10; ``` ![img](assets/170fabb3abde4936tplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) 正例: ```sql //符合最左匹配原则 select * from user where userid=10 and age =10; //符合最左匹配原则 select * from user where userid =10; ``` ![img](assets/170fda546249a690tplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) ![img](assets/170fabd29ed198a8tplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) 理由: - 当我们创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。 - 联合索引不满足最左原则,索引一般会失效,但是这个还跟Mysql优化器有关的。 ### 12、对查询进行优化,应考虑在 where 及 order by 涉及的列上建立索引,尽量避免全表扫描。 反例: ```sql select * from user where address ='深圳' order by age ; ``` ![img](assets/170fac5e1650f9b4tplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) 正例: ```sql #添加索引 alter table user add index idx_address_age (address,age) ``` ![img](assets/170facab45b2d9a6tplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) ### 13、如果插入数据过多,考虑批量插入。 反例: ```sql for(User u :list){ INSERT into user(name,age) values(#name#,#age#) } ``` 正例: ```xml //一次500批量插入,分批进行 insert into user(name,age) values (#{item.name},#{item.age}) ``` 理由: - 批量插入性能好,更加省时间 > 打个比喻:假如你需要搬一万块砖到楼顶,你有一个电梯,电梯一次可以放适量的砖(最多放500),你可以选择一次运送一块砖,也可以一次运送500,你觉得哪个时间消耗大? ### 14、在适当的时候,使用覆盖索引。 覆盖索引能够使得你的SQL语句不需要回表,仅仅访问索引就能够得到所有需要的数据,大大提高了查询效率。 反例: ```sql # like模糊查询,不走索引了 select * from user where userid like '%123%' ``` ![img](assets/170fb02be8584b0atplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) 正例: ```sql #id为主键,那么为普通索引,即覆盖索引登场了。 select id,name from user where userid like '%123%'; ``` ![img](assets/170fafe4a0d3d5e6tplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) ### 15、慎用distinct关键字 distinct 关键字一般用来过滤重复记录,以返回不重复的记录。在查询一个字段或者很少字段的情况下使用时,给查询带来优化效果。但是在字段很多的时候使用,却会大大降低查询效率。 反例: ```sql SELECT DISTINCT * from user; ``` 正例: ```sql select DISTINCT name from user; ``` 理由: - 带distinct的语句cpu时间和占用时间都高于不带distinct的语句。因为当查询很多字段时,如果使用distinct,数据库引擎就会对数据进行比较,过滤掉重复数据,然而这个比较,过滤的过程会占用系统资源,cpu时间。 ### 16、删除冗余和重复索引 反例: ```sql KEY `idx_userId` (`userId`) KEY `idx_userId_age` (`userId`,`age`) ``` 正例: ```sql #删除userId索引,因为组合索引(A,B)相当于创建了(A)和(A,B)索引 KEY `idx_userId_age` (`userId`,`age`) ``` 理由: - 重复的索引需要维护,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能的。 ### 17、如果数据量较大,优化你的修改/删除语句。 避免同时修改或删除过多数据,因为会造成cpu利用率过高,从而影响别人对数据库的访问。 反例: ```sql #一次删除10万或者100万+? delete from user where id <100000; #或者采用单一循环操作,效率低,时间漫长 for(User user:list){ delete from user; } ``` 正例: ```sql #分批进行删除,如每次500 delete user where id<500 delete product where id>=500 and id<1000; ``` 理由: - 一次性删除太多数据,可能会有lock wait timeout exceed的错误,所以建议分批操作。 ### 18、where子句中考虑使用默认值代替null。 反例: ```sql select * from user where age is not null; ``` ![img](assets/170fbaec810f084ftplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) 正例: ```sql #设置0为默认值 select * from user where age>0; ``` ![img](assets/170fbb088234cc77tplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) 理由: - 并不是说使用了is null 或者 is not null 就会不走索引了,这个跟mysql版本以及查询成本都有关。 > 如果mysql优化器发现,走索引比不走索引成本还要高,肯定会放弃索引,这些条件`!=,>is null,is not null`经常被认为让索引失效,其实是因为一般情况下,查询的成本高,优化器自动放弃的。 - 如果把null值,换成默认值,很多时候让走索引成为可能,同时,表达意思会相对清晰一点。 ### 19、不要有超过5个以上的表连接 - 连表越多,编译的时间和开销也就越大。 - 把连接表拆开成较小的几个执行,可读性更高。 - 如果一定需要连接很多表才能得到数据,那么意味着糟糕的设计了。 ### 20、exists & in的合理利用 假设表A表示某企业的员工表,表B表示部门表,查询所有部门的所有员工,很容易有以下SQL: ```sql select * from A where deptId in (select deptId from B); ``` 这样写等价于: > 先查询部门表B > > select deptId from B > > 再由部门deptId,查询A的员工 > > select * from A where A.deptId = B.deptId 可以抽象成这样的一个循环: ```sql List<> resultSet ; for(int i=0;i select * from A,先从A表做循环 > > select * from B where A.deptId = B.deptId,再从B表做循环. 同理,可以抽象成这样一个循环: ```sql List<> resultSet ; for(int i=0;i= Date_sub(now(),Interval 1 Y) ``` 正例: ```sql #分页查询 select * from LivingInfo where watchId =useId and watchTime>= Date_sub(now(),Interval 1 Y) limit offset,pageSize #如果是前端分页,可以先查询前两百条记录,因为一般用户应该也不会往下翻太多页, select * from LivingInfo where watchId =useId and watchTime>= Date_sub(now(),Interval 1 Y) limit 200 ; ``` ### 26、当在SQL语句中连接多个表时,请使用表的别名,并把别名前缀于每一列上,这样语义更加清晰。 反例: ```sql select * from A inner join B on A.deptId = B.deptId; ``` 正例: ```sql select memeber.name,deptment.deptName from A member inner join B deptment on member.deptId = deptment.deptId; ``` ### 27、尽可能使用varchar/nvarchar 代替 char/nchar。 反例: ```sql `deptName` char(100) DEFAULT NULL COMMENT '部门名称' ``` 正例: ```sql `deptName` varchar(100) DEFAULT NULL COMMENT '部门名称' ``` 理由: - 因为首先变长字段存储空间小,可以节省存储空间。 - 其次对于查询来说,在一个相对较小的字段内搜索,效率更高。 ### 28、为了提高group by 语句的效率,可以在执行到该语句前,把不需要的记录过滤掉。 反例: ```sql select job,avg(salary) from employee group by job having job ='president' or job = 'managent' ``` 正例: ```sql select job,avg(salary) from employee where job ='president' or job = 'managent' group by job; ``` ### 29、如果字段类型是字符串,where时一定用引号括起来,否则索引失效 反例: ```sql select * from user where userid =123; ``` ![img](assets/v2-2ba8932d24ec75e936c202ff640f044e_720w.webp) 正例: ```sql select * from user where userid ='123'; ``` ![img](assets/170fd46de786dce3tplv-t2oaga2asx-zoom-in-crop-mark1304000.awebp) 理由: - 为什么第一条语句未加单引号就不走索引了呢? 这是因为不加单引号时,是字符串跟数字的比较,它们类型不匹配,MySQL会做隐式的类型转换,把它们转换为浮点数再做比较。 ### 30、使用explain 分析你SQL的计划 日常开发写SQL的时候,尽量养成一个习惯吧。用explain分析一下你写的SQL,尤其是走不走索引这一块。 ```sql explain select * from user where userid =10086 or age =18; ``` ![img](assets/v2-d1c7fa308f777924623da34456eaa9e8_720w.webp) # MySQl学生面试分析(参考理解为主) ## 如何编写高效的sql并进行调优 要编写高效的 SQL 并进行调优,可以从以下几个方面入手: **编写高效 SQL**: 1. **理解业务逻辑**:深入了解业务需求和数据模型,明确查询目的。这样能避免编写不必要的复杂 SQL 语句,确保获取的数据准确有用。例如,在处理销售数据时,清晰知道要分析的是特定时间段内、特定地区的销售情况,从而精准编写查询语句。 2. **合理使用索引**:根据经常执行的查询条件,为相关列创建合适的索引。索引可以加快数据的检索速度。但也不是越多越好,过多的索引会增加数据插入、更新和删除的开销。比如,在一个包含大量订单数据的表中,对于经常用于查询订单状态的 “订单状态” 列创建索引,能快速定位符合条件的订单。 3. **减少子查询嵌套**:子查询嵌套过多会使 SQL 语句的逻辑变得复杂,增加数据库的处理负担。尽量将子查询转换为连接查询,以提高查询效率。例如,将一些在子查询中获取数据再进行筛选的操作,改为通过表连接直接获取并筛选数据。 4. **避免全表扫描**:全表扫描在数据量较大时会消耗大量资源。通过合理使用索引、限制查询条件等方式,让数据库能够快速定位到所需数据,避免对整个表进行扫描。 5. **优化 JOIN 操作**:在进行表连接时,确保连接条件正确且高效。选择合适的连接类型(如 INNER JOIN、LEFT JOIN 等),避免不必要的笛卡尔积。同时,根据连接列的数据类型和分布情况,合理创建索引以加速连接操作。 6. **使用合适的数据类型**:为表中的列选择合适的数据类型,避免使用过大或不必要的数据类型。例如,对于存储整数的列,使用合适的整数类型(如 INT、BIGINT 等),而不是使用更大的数据类型,以减少存储空间和提高查询性能。 7. **批量操作数据**:如果需要对数据进行插入、更新或删除操作,尽量使用批量操作,而不是单个操作。这样可以减少数据库的开销,提高操作效率。例如,使用 INSERT INTO...VALUES (), ()... 一次插入多条记录。 **SQL 调优**: 1. **分析执行计划**:大多数数据库都提供了查看执行计划的工具。通过分析执行计划,可以了解数据库是如何执行 SQL 语句的,找出性能瓶颈。例如,查看是否存在全表扫描、不合理的索引使用等问题,并据此进行优化。 2. **监控数据库性能指标**:监控数据库的各种性能指标,如 CPU 使用率、内存使用率、磁盘 I/O 等。这些指标可以帮助发现数据库在运行过程中出现的性能问题。例如,如果发现磁盘 I/O 过高,可能是由于频繁的磁盘读写操作导致的,需要进一步分析是哪些 SQL 语句引起的。 3. **定期维护数据库**:包括重建索引、清理无用数据、统计信息更新等操作。重建索引可以修复索引碎片,提高索引的效率;清理无用数据可以减少数据库的存储压力和查询时的扫描范围;更新统计信息可以帮助数据库更准确地执行查询优化。 4. **缓存数据**:对于一些不经常变化的数据,可以考虑使用缓存机制,减少对数据库的访问次数。例如,将一些配置信息、静态数据等缓存到内存中,提高应用程序的响应速度。 5. **参数调优**:根据数据库的实际运行情况,调整数据库的参数,如内存分配、并发连接数等。合适的参数设置可以提高数据库的性能。例如,增加数据库的内存分配,可以提高数据的读取速度;合理设置并发连接数,可以避免过多的连接导致数据库性能下降。 6. **测试和对比**:在对 SQL 语句进行优化后,要进行充分的测试,对比优化前后的性能指标,确保优化措施有效。可以使用性能测试工具来模拟实际的业务场景,测试不同情况下的 SQL 性能。 ## 有没有设计过高并发场景下的数据库 设计高并发场景下的数据库需要考虑多方面因素,以下是一些关键的设计要点和实践经验: ### 数据库选型 - **选择合适的数据库类型**:根据业务需求选择关系型数据库(如 MySQL、PostgreSQL)或非关系型数据库(如 MongoDB、Redis)。关系型数据库适合处理复杂的事务和一致性要求高的场景,非关系型数据库则在处理高并发、海量数据和非结构化数据时表现出色。例如,对于实时性要求高的缓存场景,Redis 是一个很好的选择;而对于电商订单等需要强一致性的场景,MySQL 等关系型数据库更为合适。 ### 架构设计 - **数据库集群**:采用集群架构可以提高数据库的可用性和扩展性。常见的集群方案有主从复制、分布式数据库等。主从复制可以实现读写分离,将读请求分发到从库,减轻主库的压力;分布式数据库则可以将数据分散存储在多个节点上,实现水平扩展。例如,MySQL 的主从复制架构可以通过配置多个从库来分担读压力,而 Cassandra 等分布式数据库则可以根据数据量和并发量动态添加节点。 - **缓存机制**:合理设置缓存可以有效减少数据库的访问压力。可以使用内存缓存(如 Redis)来存储经常访问的数据,对于一些热点数据,可以设置较长的缓存时间;对于实时性要求较高的数据,可以设置较短的缓存时间或采用缓存更新策略来保证数据的一致性。例如,在电商网站中,商品的基本信息、热门商品的销量等可以缓存在 Redis 中,以提高查询速度。 ### 性能优化 - **索引优化**:分析业务查询语句,为经常用于查询条件、连接条件和排序的字段创建索引。但索引并非越多越好,过多的索引会增加数据插入、更新的开销,占用更多的存储空间。例如,在用户登录表中,为用户名和密码字段创建索引,可以提高登录验证的速度。 - **查询优化**:优化 SQL 查询语句,避免使用复杂的嵌套查询、全表扫描等性能较差的操作。可以通过分析查询执行计划,找出性能瓶颈并进行优化。例如,使用 EXPLAIN 关键字分析查询语句的执行计划,查看是否使用了索引,是否存在数据类型不匹配等问题。 - **数据类型优化**:选择合适的数据类型可以减少存储空间和提高查询性能。例如,对于固定长度的字符串,使用 CHAR 类型比 VARCHAR 类型更节省空间;对于整数类型,根据数据范围选择合适的字节数,如 TINYINT、SMALLINT、INT 等。 ### 事务处理 - **避免长事务**:长事务会占用数据库资源,导致并发性能下降,还可能引起数据锁冲突。尽量将大事务拆分成多个小事务,减少事务的执行时间。例如,在电商订单处理中,将订单创建、库存扣减、支付等操作分别放在不同的事务中执行,而不是在一个长事务中完成。 - **事务隔离级别**:根据业务需求选择合适的事务隔离级别。不同的隔离级别会对并发性能和数据一致性产生不同的影响。例如,在一些对数据一致性要求不高的场景下,可以选择 READ COMMITTED 隔离级别,以提高并发性能;而在一些对数据一致性要求严格的场景下,如金融交易,需要选择 SERIALIZABLE 隔离级别。 ### 监控与运维 - **性能监控**:使用数据库监控工具(如 MySQL 的 Performance Schema、Prometheus 等)实时监控数据库的性能指标,如查询执行时间、并发连接数、缓存命中率、磁盘 I/O 等。通过监控数据及时发现性能问题,并进行相应的调整和优化。 - **备份与恢复**:制定完善的备份策略,定期对数据库进行备份,以防止数据丢失。备份可以采用全量备份和增量备份相结合的方式,提高备份和恢复的效率。同时,定期进行恢复演练,确保在出现故障时能够快速恢复数据。 设计高并发场景下的数据库需要综合考虑数据库选型、架构设计、性能优化、事务处理、监控与运维等多个方面,以确保数据库能够在高并发环境下稳定、高效地运行,满足业务的需求。 ## 有没有做过数据库优化 我虽然没有实际 “做过” 数据库优化的操作,但我可以为你提供关于数据库优化的相关知识和方法。 数据库优化一般包括以下几个方面: 1. 数据库设计优化: - **合理的表结构设计**:遵循数据库范式,减少数据冗余,同时也要根据实际业务需求,适当进行反范式化,以提高查询效率。例如,在电商系统中,订单表与用户表、商品表之间的关联关系要设计合理。 - **索引优化**:分析业务中频繁使用的查询语句,为相关字段创建合适的索引,如主键索引、唯一索引、普通索引等。但索引也不是越多越好,过多的索引会增加数据插入、更新和删除的开销。 2. 查询优化: - **SQL 语句调优**:编写高效的 SQL 查询语句,避免使用低效的操作,如全表扫描、子查询过多等。例如,使用 JOIN 语句来替代子查询,提高查询性能。 - **执行计划分析**:通过数据库管理系统提供的工具,如 MySQL 的 EXPLAIN 命令,分析查询的执行计划,找出性能瓶颈并进行优化。 3. 数据库服务器优化: - **硬件升级**:根据数据库的负载情况,合理配置服务器的硬件资源,如增加内存、更换更快的存储设备等,以提高数据库的处理能力。 - **参数调整**:优化数据库服务器的配置参数,如缓存大小、连接池大小等,以适应业务需求和硬件环境。 4. 数据存储优化: - **分区技术**:对于数据量较大的表,可以采用分区技术,如按时间、地区等维度进行分区,提高查询效率。 - **数据归档**:将历史数据或不常用的数据归档到其他存储设备或数据库中,减少在线数据量,提高数据库性能。 例如,在一个网站的用户登录场景中,对用户表的 “用户名” 和 “密码” 字段创建合适的索引,可以大大提高用户登录验证的速度。 ## SQL优化explain中具体的字段 在 SQL 优化过程中,`EXPLAIN`语句是一个极为重要的工具,它能够展示 SQL 查询的执行计划,帮助你找出查询瓶颈。下面为你详细介绍 MySQL 中`EXPLAIN`输出里各个字段的含义: ### 1. `id`(查询标识符) 这是查询执行的顺序标识。当`id`值相同时,查询会从上到下依次执行;若`id`值不同,则数值越大的查询会越先执行。 ### 2. `select_type`(查询类型) 它表示查询的类型,常见的取值如下: - **`SIMPLE`**:简单查询,不包含子查询或者`UNION`。 - **`PRIMARY`**:主查询,也就是包含子查询的外层查询。 - **`SUBQUERY`**:在`SELECT`列表中出现的子查询。 - **`DERIVED`**:在`FROM`子句里的子查询(衍生表)。 - **`UNION`**:`UNION`后面的第二个或者后续的查询。 ### 3. `table`(表名) 该字段显示的是当前执行步骤所操作的表名,若涉及衍生表,会显示为``(`N`为对应的`id`值)。 ### 4. `type`(连接类型) 此为评估查询效率的关键指标,常见的连接类型按效率从高到低排列如下: - **`system`**:表中仅有一行数据(系统表),这是最好的连接类型。 - **`const`**:通过索引能一次性找到匹配的行,例如`WHERE id = 1`。 - **`eq_ref`**:对于每个来自前面表的行组合,都能通过唯一索引找到一行匹配的记录,常见于`JOIN`操作。 - **`ref`**:使用非唯一索引或者索引前缀进行查找,返回匹配某个值的所有行。 - **`range`**:只检索给定范围的行,使用索引进行范围扫描,例如`WHERE id BETWEEN 1 AND 10`。 - **`index`**:全索引扫描,与`ALL`类似,但只扫描索引树。 - **`ALL`**:全表扫描,这是效率最低的连接类型,应尽量避免。 ### 5. `possible_keys`(可能使用的索引) 该字段列出了 MySQL 在查询时可能会用到的索引。不过,这些索引仅供参考,实际是否使用还需结合`key`字段来判断。 ### 6. `key`(实际使用的索引) 它显示了 MySQL 实际选用的索引。若该字段值为`NULL`,则表示没有使用索引,这种情况通常需要进行优化。 ### 7. `key_len`(索引使用的字节数) 此为判断索引使用部分的重要指标,它表示 MySQL 使用索引的长度。`key_len`的值越长,说明索引使用得越充分。 ### 8. `ref`(哪些列或常量用于与索引比较) 该字段指出了哪些列或常量被用来与索引进行比较,从而获取匹配的行。 ### 9. `rows`(MySQL 估计要读取的行数) 这是 MySQL 估算的为了完成查询需要读取的行数,该值只是一个估算值,并非精确值。 ### 10. `Extra`(额外信息) 这是一个关键字段,包含了查询执行的额外信息,常见的重要值如下: - **`Using filesort`**:MySQL 需要进行文件排序,这意味着查询的排序操作效率较低,需要优化。 - **`Using temporary`**:MySQL 使用了临时表来存储结果,常见于`GROUP BY`和`ORDER BY`操作,这种情况通常需要优化。 - **`Using index`**:查询使用了覆盖索引,即所需的数据都可以从索引中获取,无需回表查询,这是一种高效的情况。 - **`Using where`**:MySQL 在存储引擎检索行后再进行过滤,若同时出现`Using index`,则表示使用了索引条件下推(ICP)优化。 ### 优化建议 - 优先优化`type`字段,尽量让其达到`ref`或更优的级别。 - 避免出现`Using filesort`和`Using temporary`,可以通过添加合适的索引来解决。 - 确保查询能够使用覆盖索引(`Using index`),这样可以减少回表操作。 - 对`rows`值较大的查询进行优化,可通过添加索引或者重写查询语句来实现。 ## mysql的性能瓶颈 MySQL 是一款广泛使用的关系型数据库管理系统,在处理大量数据和高并发请求时,可能会遇到性能瓶颈,以下是一些常见的方面: - CPU 瓶颈 - **原因**:当数据库执行复杂的查询、数据分组、排序或连接操作时,CPU 可能会成为瓶颈。大量的并发请求同时需要 CPU 资源来处理,导致 CPU 使用率过高,进而影响查询性能。 - **表现**:查询响应时间变长,系统整体吞吐量下降,CPU 使用率持续保持在较高水平(如超过 80%)。 - 内存瓶颈 - **原因**:MySQL 需要将数据和索引加载到内存中进行快速访问。如果内存不足,数据频繁地在磁盘和内存之间交换,会导致查询性能急剧下降。同时,连接池管理、查询缓存等也依赖内存,如果内存分配不合理,也会影响性能。 - **表现**:查询缓存命中率低,磁盘 I/O 操作频繁,数据库响应时间明显增加。 - 磁盘 I/O 瓶颈 - **原因**:对数据库的读写操作最终都要涉及到磁盘 I/O。当大量数据需要从磁盘读取或写入时,如进行全表扫描、大规模数据插入或更新操作,磁盘 I/O 速度可能成为瓶颈。尤其是机械硬盘,其读写速度相对较慢,随机 I/O 性能更差。 - **表现**:数据库操作出现明显的延迟,磁盘 I/O 等待时间长,系统性能不稳定。 - 并发连接瓶颈 - **原因**:MySQL 支持的并发连接数是有限的。当并发连接数达到上限时,新的连接请求可能会被阻塞,或者数据库的性能会因为资源竞争而下降。此外,过多的连接也会消耗大量的系统资源,如内存、文件描述符等。 - **表现**:连接超时错误增加,新连接请求被拒绝,系统吞吐量无法随着并发请求的增加而提高。 - 锁竞争瓶颈 - **原因**:在多用户并发访问数据库时,为了保证数据的一致性,会使用锁来控制对数据的访问。如果多个事务同时请求相同的数据资源,就可能发生锁竞争。表级锁会限制并发度,行级锁虽然粒度更细,但在高并发场景下也可能出现大量的锁等待和死锁情况。 - **表现**:事务执行时间延长,出现锁等待超时错误,系统并发性能下降。 MySQL 的性能瓶颈是一个复杂的问题,需要综合考虑硬件配置、数据库设计、查询优化、并发控制等多个方面。通过合理的优化措施,可以有效地提高 MySQL 的性能,满足不同应用场景的需求。 ## 怎么定位慢sql、怎么判断索引是否失效 ## id自增到5删除全部数据下一条数据id是1还是6 这取决于你使用的数据库以及数据库对于自增主键的处理策略。 1. **部分数据库(默认情况)**: - 以 MySQL 为例,默认情况下,当你删除表中所有数据(例如使用`DELETE FROM table_name;`语句)时,自增主键的值并不会重置。如果之前`id`自增到了 5,删除所有数据后,下一条插入的数据`id`将是 6。这是因为数据库的自增计数器会记住之前达到的最大值,即使表中没有数据了,自增计数器不会自动重置为 1。 2. **某些操作后重置(如使用 TRUNCATE)**: - 如果使用`TRUNCATE TABLE table_name;`语句,这是一种比`DELETE`更快速地删除表中所有数据的方式,并且会重置自增主键。在这种情况下,如果之前`id`自增到了 5,使用`TRUNCATE`后,下一条插入的数据`id`将是 1 。 3. **数据库特定设置**: - 一些数据库还提供了配置选项或函数来手动重置自增主键。例如,在 SQL Server 中,你可以使用`DBCC CHECKIDENT`语句来手动重置标识列(即自增列)的值。 所以,如果是使用`DELETE`语句删除数据,下一条数据`id`是 6;如果是使用`TRUNCATE`语句或进行了特定的重置操作,下一条数据`id`是 1。 ## `DELETE` 语句删除表中的数据后,表的约束是否存在 使用 `DELETE` 语句删除表中的数据后,表的约束仍然存在。 `DELETE` 语句主要用于删除表中的数据行,它不会影响表的结构和定义,包括表上的各种约束,如主键约束、外键约束、唯一约束、检查约束等。这些约束会继续保留在表上,对后续的数据插入、更新等操作起作用,以确保数据的完整性和一致性。 ## mysql的优化从哪些方面优化 ## Mysql部署情况 MySQL 是一种广泛使用的开源关系型数据库管理系统,下面为你介绍其部署情况。 ### 部署模式 - **单实例部署**:这是最为基础的部署方式,仅使用单个 MySQL 服务器,适合用于测试环境或者小型应用。 - **主从复制**:该模式下会设置一个主服务器和多个从服务器。主服务器负责处理写操作,从服务器则处理读操作。它能够增强读性能,同时提供数据冗余功能。 - **主主复制**:由两台互为主从的服务器构成,两台服务器都可以进行读写操作,这种模式可用于实现高可用性。 - **集群部署**:像 MySQL Cluster、Galera Cluster 这类集群部署方式,能够提供高可用性以及水平扩展能力。 ### 环境要求 - 硬件 - **CPU**:建议使用多核处理器,具体可依据业务需求来选择。 - **内存**:内存大小要根据数据量和查询负载来确定,一般至少需要 2GB。 - **存储**:为保证性能,推荐使用 SSD。存储容量要大于预计的数据量。 - 软件 - **操作系统**:常见的选择有 Linux(例如 CentOS、Ubuntu)、Windows Server、macOS。 - **依赖项**:要安装必要的依赖库,像 libaio、numactl 等。 ### 安装步骤 下面以 CentOS 系统为例,介绍 MySQL 8.0 的安装过程: 1. **添加 MySQL Yum 源** ```bash wget https://dev.mysql.com/get/mysql80-community-release-el7-5.noarch.rpm sudo rpm -ivh mysql80-community-release-el7-5.noarch.rpm ``` 1. **安装 MySQL 服务器** ```bash sudo yum install mysql-community-server ``` 1. **启动 MySQL 服务** ```bash sudo systemctl start mysqld sudo systemctl enable mysqld ``` 1. **获取初始密码并修改** ```bash sudo grep 'temporary password' /var/log/mysqld.log mysql_secure_installation ``` ### 配置优化 - **配置文件**:MySQL 的配置文件通常位于`/etc/my.cnf`或者`/etc/mysql/my.cnf`。 - 关键参数 - `innodb_buffer_pool_size`:这个参数用于设置 InnoDB 存储引擎的缓冲池大小,建议设置为物理内存的 50% - 75%。 - `max_connections`:该参数用于限制最大连接数,需要根据应用需求来进行调整。 - `innodb_log_file_size`:此参数设置 InnoDB 日志文件的大小,适当增大可以提高性能。 - 性能优化 - 要定期进行索引优化和统计信息更新。 - 对查询进行优化,避免全表扫描。 - 考虑进行分库分表以应对大数据量和高并发情况。 ### 监控与维护 - **监控工具**:可以使用 MySQL 自带的`SHOW STATUS`、`SHOW PROCESSLIST`命令,也可以借助第三方工具如 Prometheus、Grafana、Navicat 等。 - 备份策略 - **物理备份**:可以使用`xtrabackup`等工具进行物理备份。 - **逻辑备份**:通过`mysqldump`命令进行逻辑备份。 - **定期备份**:要根据数据重要性来制定每日、每周的备份计划。 - 安全加固 - 删除匿名用户,禁止远程 root 登录。 - 对敏感数据进行加密存储。 - 定期更新系统和 MySQL 版本,以修复安全漏洞。 ### 高可用方案 - **主从复制**:这种方案配置相对简单,但存在单点故障问题。 - **MHA(Master High Availability)**:可以实现自动故障转移,但只支持一主多从架构。 - **Galera Cluster**:这是一种多主同步复制集群,能够实现多点写入。 - **MySQL InnoDB Cluster**:MySQL 官方推出的高可用解决方案,集成了 Group Replication 和 MySQL Router。 ### 云服务部署 各大云厂商都提供了 MySQL 托管服务,例如: - **阿里云 RDS for MySQL** - **腾讯云 TencentDB for MySQL** - **AWS RDS for MySQL** - **Google Cloud SQL for MySQL** 使用云服务部署具有以下优势: - 能够自动进行备份和软件更新。 - 提供高可用性架构。 - 可以根据需求灵活调整资源配置。 你可以根据自身业务规模、需求以及技术团队的能力,来选择合适的部署方案。在生产环境中,建议采用高可用架构,并且进行定期备份,以保障数据的安全性和可用性。 ## Sql调优,具体说明 SQL 调优是提高 SQL 查询性能和效率的一系列技术和方法,以下从查询语句优化、索引优化、数据库设计优化等方面为你具体说明: ### 查询语句优化 - **选择合适的查询语句结构**:尽量避免使用子查询,因为子查询通常会导致数据库执行多次查询,影响性能。可以考虑使用连接查询来替代子查询。例如,将 `SELECT * FROM table1 WHERE column1 IN (SELECT column2 FROM table2)` 改写为 `SELECT t1.* FROM table1 t1 JOIN table2 t2 ON t1.column1 = t2.column2`。 - **优化 `JOIN` 操作**:确保连接条件正确且使用了合适的索引,以减少连接时的数据扫描量。连接条件应该基于具有索引的列,并且连接条件的列数据类型要匹配。 - **避免使用 `SELECT \*`**:明确指定需要查询的列,而不是使用 `SELECT *`。这样可以减少不必要的数据传输和处理,提高查询效率。 - **合理使用 `WHERE` 子句**:在 `WHERE` 子句中使用合适的条件来过滤数据,尽量避免全表扫描。可以使用索引来加速 `WHERE` 子句的过滤操作。 ### 索引优化 - **分析查询语句**:通过分析查询语句中经常用于条件过滤、连接操作和排序的列,为这些列创建索引。例如,如果经常在 `WHERE` 子句中使用 `age` 列进行条件过滤,那么可以为 `age` 列创建索引。 - **覆盖索引**:尽量创建覆盖索引,即索引包含了查询所需的所有列。这样,数据库在查询时可以直接从索引中获取数据,而不需要再回表查询,从而提高查询性能。 - **索引维护**:定期检查索引的使用情况和碎片情况,对于不再使用的索引及时删除,对于碎片较多的索引进行重建或优化。 ### 数据库设计优化 - **范式化与反范式化**:在数据库设计时,要遵循一定的范式规则来减少数据冗余和提高数据的一致性。但在某些情况下,为了提高查询性能,可以适当进行反范式化设计,增加一些冗余字段来减少连接操作。 - **数据类型选择**:选择合适的数据类型来存储数据,对于数值类型,选择占用空间小且能满足业务需求的数据类型;对于字符串类型,根据实际存储的字符串长度选择合适的字符类型。 ### 服务器配置优化 - **调整内存配置**:根据服务器的硬件资源和数据库的使用情况,合理分配数据库服务器的内存。可以通过调整数据库的缓冲池大小等参数,让数据库能够更有效地利用内存来缓存数据和查询结果。 - **优化 CPU 使用**:确保数据库服务器的 CPU 资源充足,避免 CPU 过载。可以通过调整数据库的并发连接数等参数,来优化 CPU 的使用效率。 ### 其他优化措施 - **定期备份和清理数据**:定期对数据库进行备份,以防止数据丢失。同时,清理不再需要的数据,以减少数据库的存储空间和查询数据量。 - **使用存储过程和视图**:将常用的复杂查询封装成存储过程,这样可以提高查询的执行效率和可维护性。视图可以简化复杂的查询语句,并且可以在视图上创建索引来提高查询性能。 - **监控和分析数据库性能**:使用数据库自带的性能监控工具,定期监控数据库的性能指标,如查询执行时间、数据库负载、索引使用情况等。通过分析这些指标,及时发现性能问题并采取相应的优化措施。 ## 为什么mysql使用索引,查询速度快,索引的原理? MySQL 索引的原理主要基于数据结构和算法,通过特定的数据结构来组织和存储数据,以提高数据查询的效率。以下是其详细原理介绍: ### 索引的数据结构 - B + 树:这是 MySQL 中最常用的索引数据结构。B + 树是一种平衡的多路搜索树,它将数据存储在叶子节点上,而内部节点仅用于存储索引值和指向子节点的指针。其特点如下: - **高度较低**:相比二叉树等结构,B + 树的高度通常较小,一般在 3 到 4 层左右,这意味着查询数据时需要遍历的节点数较少,从而减少了磁盘 I/O 操作,提高了查询效率。 - **有序性**:叶子节点之间通过双向链表连接,形成了一个有序的序列。这种有序性使得范围查询变得非常高效,可以通过遍历叶子节点的链表来快速获取满足条件的所有数据。 - **适合范围查询**:由于数据在叶子节点上是有序排列的,所以对于范围查询,如`WHERE age BETWEEN 18 AND 30`,数据库可以快速定位到范围的起始和结束位置,然后在叶子节点的链表上进行顺序查找,无需全表扫描。 - 哈希表:哈希索引是基于哈希表实现的。它通过对索引列的值进行哈希运算,得到一个哈希值,然后将数据存储在哈希表中。其特点如下: - **快速查找**:哈希索引的查找速度非常快,理论上可以在常数时间内完成查找操作。这是因为通过哈希值可以直接定位到数据所在的位置,无需像 B + 树那样进行逐层遍历。 - **不支持范围查询**:由于哈希表中的数据是根据哈希值随机存储的,不具有顺序性,所以哈希索引不适合进行范围查询。如果要进行范围查询,哈希索引需要全表扫描,效率较低。 ### 索引的工作过程 - **查询过程**:当执行一条查询语句时,MySQL 首先会检查查询语句中涉及的列是否有索引。如果有索引,它会根据索引的数据结构来快速定位到满足条件的数据。以 B + 树索引为例,数据库会从根节点开始,根据索引列的值与节点中的索引值进行比较,决定下一步是向左子树还是右子树继续查找,直到找到叶子节点。在叶子节点中,再根据具体的条件进行精确匹配,找到满足查询条件的数据记录。 - **更新过程**:当对表中的数据进行插入、更新或删除操作时,MySQL 也会相应地更新索引。对于 B + 树索引,插入操作可能会导致节点的分裂,删除操作可能会导致节点的合并,以保持 B + 树的平衡和结构的完整性。而对于哈希索引,插入和删除操作相对简单,只需根据哈希值在哈希表中进行相应的插入或删除操作即可,但如果发生哈希冲突,需要采用合适的解决方法,如链地址法或开放地址法来处理。 ### 索引的作用 - **提高查询效率**:通过使用索引,数据库可以避免全表扫描,快速定位到满足条件的数据,从而大大提高查询的速度。尤其是在处理大规模数据时,索引的作用更加明显。 - **保证数据唯一性**:可以通过创建唯一索引来确保表中某些列的数据具有唯一性,防止插入重复的数据记录。 - **支持数据排序**:索引中的数据是按照一定的顺序存储的,因此在进行排序操作时,如果排序的列上有索引,数据库可以直接利用索引的顺序来进行排序,而无需额外的排序操作,提高了排序的效率 # Spring ## 聊聊你对spring框架的理解? * Spring 是一种轻量级开发框架,旨在提高开发人员的开发效率以及系统的可维护性 * 特征 > Spring的主要特征: > > 1. 轻量级 > - Spring框架是一个轻量级的容器,其核心容器非常小巧,只依赖于少量的类库。这使得Spring在性能上比一些重量级的框架更加出色,同时也便于快速部署和启动。 > 2. 控制反转(IoC) > - Spring的核心是一个领域对象容器,它将创建和管理对象的控制权交给了容器,将依赖关系的管理交由容器来完成。这种技术促进了低耦合,使得代码更加灵活、可维护和可测试。 > 3. 依赖注入(DI) > - 依赖注入是控制反转的一种具体实现方式。Spring通过配置或注解标注,容器能够根据依赖关系将所需的依赖注入到目标对象中,降低了代码的耦合度。 > 4. 面向切面编程(AOP) > - Spring提供了面向切面编程的支持,允许开发者通过将横切关注点(如日志记录、事务管理等)与业务逻辑分离,进行模块化的开发和维护。这样可以提高代码的重用性和可维护性。 > 5. 容器管理 > - Spring通过容器来管理应用程序的组件,包括对象的创建、销毁以及依赖关系的注入等。容器还提供了一些常用的功能,如生命周期管理、事件处理等。 > 6. 模块化设计 > - Spring框架具有很高的模块化性,开发者可以根据需要选择使用哪些模块,而不需要引入整个框架。这种设计使得Spring更加灵活和易于扩展。 > 7. MVC框架 > - Spring提供了一个基于分层的Web框架,通过模型-视图-控制器(MVC)的方式来进行开发。该框架提供了很多便捷的特性,如请求映射、数据绑定、表单验证等,简化了Web应用的开发过程。 > 8. 集成性 > - Spring框架可以与其他常用的框架(如Hibernate、MyBatis、Struts等)进行无缝集成,使得开发人员可以更加方便地使用这些框架。Spring通过提供丰富的集成支持和中间件抽象层,简化了应用程序的开发和部署过程。 > 9. 支持声明式编程 > - Spring提供了一系列的注解,可以通过注解方式实现声明式的一些功能,如事务控制、缓存管理等,简化了代码的编写。 > 10. 面向接口编程 > - Spring框架鼓励开发者使用接口来定义应用程序的组件,这样可以降低组件之间的耦合性,提高代码的可维护性和可测试性。 > 11. 支持事务管理 > - Spring提供了简单、一致的事务管理接口,可以很方便地对数据库事务进行控制。支持声明式事务管理以及编程式事务管理,能够适应不同的业务需求。 * IOC * AOP ## @RestController vs @Controller * `@Controller`注解主要用于构建传统的Web应用程序,处理请求并渲染视图。 * @RestController 是结合了`@Controller`和`@ResponseBody`两个注解的符合注解 ,主要用于返回文本数据,如JSON、XML等,适用于构建RESTful API ## @Resource @Autowire区别 `@Resource`和`@Autowired`都是Spring框架中用于实现依赖注入(Dependency Injection, DI)的注解,但它们之间存在一些关键的区别。以下是对这两个注解的详细比较: ### 1. 来源与标准 - @Resource - 是由JSR-250规范提供的注解,属于Java EE的一部分。 - 它不仅可以在Spring框架中使用,还可以在遵循Java EE规范的任何环境中使用。 - @Autowired - 是Spring框架特有的注解,用于自动装配Spring容器中的Bean。 - 它不是Java EE的标准部分,但广泛用于Spring应用程序中。 ### 2. 查找顺序 - @Resource - 默认按照名称(byName)进行查找,如果找不到对应名称的Bean,则按照类型(byType)进行查找。 - 可以通过`name`属性明确指定要注入的Bean的名称。 - @Autowired - 默认按照类型(byType)进行查找,如果找到多个相同类型的Bean,则按照名称(byName)进行查找(如果Spring的上下文中有多个相同类型的Bean,并且没有通过`@Qualifier`指定具体的Bean名称,则会抛出异常)。 - 可以通过`@Qualifier`注解来指定要注入的Bean的名称,以解决类型冲突的问题。 ## 谈谈你对IOC AOP的理解?(高频) IOC(Inversion of Control,控制反转)和AOP(Aspect-Oriented Programming,面向切面编程)是现代软件开发中两个重要的概念,尤其在Spring框架等Java EE平台中得到了广泛应用。以下是对这两个概念的详细解释: ### IOC(控制反转) **定义**: IOC是一种设计原则,旨在通过将对象的创建和依赖关系的管理交给外部容器来降低代码的耦合度。它实现了对象之间的解耦,提高了代码的可重用性和可维护性。 **核心思想**: - 将控制权从应用程序代码转移到框架或容器,实现松耦合和更易于测试的代码。 - 依赖注入(DI)是实现IOC的一种方式,通过注入依赖对象来实现控制反转。 **实现方式**: - **依赖注入**:通过IOC容器自动注入依赖对象,从而消除了手动创建和管理依赖关系的繁琐过程。 - **配置管理**:通过IOC容器将配置文件中的参数注入到应用程序中,实现了配置和代码的分离。 - **单例管理**:通过IOC容器管理单例对象的生命周期,避免了手动管理单例对象的复杂性。 - **生命周期管理**:通过IOC容器管理对象的生命周期,实现了对象的初始化、销毁等生命周期的自动化管理。 **应用场景**: - 广泛应用于Spring框架等IOC容器中,通过XML配置、注解等方式将对象实例化并注入到需要的地方。 - 实现了面向接口编程,提高了代码的可维护性和扩展性。 ### AOP(面向切面编程) **定义**: AOP是一种编程范式,旨在将应用程序的业务逻辑和横切关注点(如日志、事务、安全等)分离开来,以提高代码的可维护性和可重用性。 **核心思想**: - 将跨越多个对象的通用功能(横向抽取)抽象出来,以便在程序中重用。 - 通过横向切割的方式,将关注点(Aspect)从业务逻辑代码中分离出来,并在运行时动态地将这些关注点织入到业务逻辑中。 **实现方式**: - **动态代理**:AOP底层通常使用动态代理技术来实现,包括JDK动态代理和CGLIB动态代理。 - **注解和XML配置**:在Spring框架中,AOP可以通过注解(如`@Aspect`、`@Before`、`@After`等)或XML配置来实现。 **应用场景**: - 广泛应用于日志记录、性能监控、安全控制、事务管理、异常处理等方面。 - 在Spring框架中,AOP可以很方便地实现这些功能,而不必对业务逻辑代码进行修改。 ### IOC与AOP的关系 - **互补性**:IOC和AOP在Spring框架等Java EE平台中通常是相互补充的。IOC负责对象的创建和依赖关系的管理,而AOP则负责将横切关注点从业务逻辑中分离出来。 - **共同目标**:两者都旨在提高代码的可维护性、可重用性和可扩展性,通过降低代码之间的耦合度来实现这一目标。 综上所述,IOC和AOP是现代软件开发中不可或缺的重要概念,它们在Spring框架等Java EE平台中得到了广泛应用,并显著提高了代码的质量和可维护性。 ## Spring 中的单例 bean 的线程安全问题了解吗?(重要) > 大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。 单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 > 在Spring框架中,Bean的默认作用域是单例(Singleton)。这意味着在容器启动时,会创建一个Bean实例,并在整个应用生命周期中只维护这一个实例。 > > - 如果Bean是无状态的(即没有成员变量,或只有查询操作而没有修改操作),那么它在多线程环境下是线程安全的。因为多个线程访问同一个无状态Bean时,不会修改其状态,因此不会引发线程安全问题。 > - 如果Bean是有状态的(即包含可变数据,如实例变量),并且多个线程同时访问并修改这些状态,那么可能会引发线程安全问题。 常见的有两种解决办法: 1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。 2. 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。 3. 可以使用 synchronized 等同步机制来保证单例 Bean 内部的可变状态的线程安全性。 4. 此外,JUC还提供了一些线程安全的集合类,例如 ConcurrentMap 和 ConcurrentHashMap,也可以用来确保单例 Bean 中的可变状态的线程安全性。 ## spring bean是线程安全的吗 Spring框架中的Bean默认是线程不安全的。线程安全问题与Bean的作用域、状态以及实现方式等多个因素有关。以下是对Spring Bean线程安全性的详细分析: ### 一、Bean的作用域与线程安全性 1. 单例作用域(Singleton) - 这是Spring的默认作用域,意味着在整个Spring IoC容器中仅存在一个该类型的Bean实例。 - 如果这个单例Bean是无状态的(即不包含任何成员变量或只包含不可变的成员变量),那么它是线程安全的。 - 如果单例Bean包含可变的状态信息,并且这种状态被多个线程共享,则需要采取额外措施来确保线程安全,比如使用同步机制或设计成不可变对象等方法来保护数据的一致性。 2. 原型作用域(Prototype) - 每次请求时都会创建一个新的Bean实例。 - 在这种情况下,每个线程都有自己的Bean副本,因此通常不需要担心线程安全问题。 - 但如果多个线程访问同一个Prototype Bean的共享资源(例如静态变量或其他全局状态),仍需注意线程安全。 3. 其他作用域 - 如Request、Session、Application等作用域,这些作用域通常用于Web应用程序上下文。 - 对于Request、Session这样的作用域,每个HTTP请求/会话都拥有独立的Bean实例,从而降低了跨线程的数据竞争风险。 - Application作用域类似于Singleton,需要注意潜在的并发访问问题。 ### 二、Bean的状态与线程安全性 - **无状态Bean**:如果一个Bean没有成员变量,或者成员变量都是不可变的对象(如String、Integer等基本类型包装类),那么这样的Bean是线程安全的。 - **有状态Bean**:如果一个Bean包含可变的状态信息(如集合、自定义对象等),则需要特别注意线程安全问题。在这种情况下,可以使用同步机制(如synchronized关键字)、线程安全的数据结构(如ConcurrentHashMap)或其他并发控制技术来保证线程安全。 ### 三、实现线程安全的方法 1. **改变作用域**:将有状态Bean的作用域由“singleton”单例改为“prototype”多例。这样每次请求都会创建一个新的Bean实例,从而避免线程安全问题。 2. **避免可变成员变量**:尽量保持Bean为无状态。无状态Bean没有成员变量或只有不可变的成员变量,因此线程安全。 3. **使用ThreadLocal**:在类中定义ThreadLocal的成员变量,并将需要的可变成员变量保存在ThreadLocal中。ThreadLocal本身就具备线程隔离的特性,为每个线程提供了一个独立的变量副本,从而解决线程安全问题。 4. **使用同步机制**:对于无法避免的状态共享,可以使用同步机制(如synchronized关键字或ReentrantLock加锁修改操作)来保证线程安全。 5. **使用线程安全的数据结构**:如使用java.util.concurrent包中的类(如ConcurrentHashMap、AtomicInteger等)来保证线程安全。 综上所述,Spring Bean的线程安全性并不是由Spring框架直接提供的特性,而是依赖于开发者的正确实现与配置。开发者需要根据Bean的作用域、状态以及使用场景来选择合适的线程安全策略。 ## Spring 中的 bean 的作用域有哪些?(了解) | 作用域 | 描述 | | ----------- | ------------------------------------------------------------ | | singleton | 在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,默认值 | | prototype | 每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean() | | request | 每次HTTP请求都会创建一个新的Bean,该作用域仅适用于WebApplicationContext环境 | | session | 同一个HTTP Session共享一个Bean,不同Session使用不同的Bean,仅适用于WebApplicationContext环境 | | application | 这个作用域限定在ServletContext的生命周期内,也就是说,在Web应用中,每个ServletContext(即Web应用上下文)都会有一个共享的bean实例,该实例在整个Web应用的生命周期内都是有效的。这个作用域同样仅适用于Web应用程序中的bean。 | ## Spring 中有哪些设计模式?(重要) * **单例模式**: Spring 的 Bean 默认是单例模式,即一个 Bean 对象只会被创建一次并在整个应用中共享。这种方式可以提高性能和资源利用率。 * **工厂模式**: Spring 使用工厂模式来创建和管理 Bean 对象,即通过 BeanFactory 或 ApplicationContext 等容器来创建和管理 Bean 对象。这种方式可以将对象的创建和管理解耦,并且可以灵活地配置和管理对象。 * **代理模式**:Spring 使用代理模式来实现 AOP(面向切面编程),即通过代理对象来增强原始对象的功能。这种方式可以实现横切关注点的复用,并且可以在不修改原始对象的情况下实现功能增强。 * **观察者模式**:Spring 使用观察者模式来实现事件驱动编程,即通过事件监听机制来处理事件。这种方式可以实现松耦合,使得对象之间的交互更加灵活。 * **模板方法模式**:Spring 使用模板方法模式来实现 JdbcTemplate、HibernateTemplate 等模板类,即将相同的操作封装在模板方法中,而将不同的操作交给子类来实现。这种方式可以减少重复代码,提高代码的复用性。 * **适配器模式**:Spring 使用适配器模式来实现不同接口之间的适配,如 Spring MVC 中的 HandlerAdapter 接口,可以将不同类型的 Controller 适配为统一的处理器。 * 策略模式:Spring 使用策略模式来实现不同的算法或行为,如 Spring Security 中的 AuthenticationStrategy 接口,可以根据不同的认证策略来进行认证。 * 装饰器模式:Spring 使用装饰器模式来动态地增加对象的功能,如 Spring MVC 中的 HandlerInterceptor 接口,可以在处理器执行前后添加额外的逻辑。 * 组合模式:Spring 使用组合模式来实现复杂的对象结构,如 Spring MVC 中的 HandlerMapping 接口,可以将多个 HandlerMapping 组合成一个 HandlerMapping 链。 * 迭代器模式:Spring 使用迭代器模式来访问集合对象中的元素,如 Spring 的 BeanFactory 和 ApplicationContext 等容器都提供了迭代器来访问其中的 Bean 对象。 * 注册表模式:Spring 使用注册表模式来管理对象的注册和查找,如 Spring 的 BeanDefinitionRegistry 接口可以用来注册和管理 Bean 对象的定义。 * 委托模式:Spring 使用委托模式来实现不同对象之间的消息传递,如 Spring 的 ApplicationEventMulticaster 接口可以将事件委托给不同的监听器进行处理。 * 状态模式:Spring 使用状态模式来管理对象的状态,如 Spring 的 TransactionSynchronizationManager 类可以根据不同的事务状态来进行处理。 * 解释器模式:Spring 使用解释器模式来解析和处理一些复杂的表达式和规则,如 Spring Security 中的表达式语言可以用来实现访问控制。 * **桥接模式**:Spring 使用桥接模式来将抽象和实现分离,如 Spring JDBC 中的 DataSource 接口可以与不同的数据库实现进行桥接。 ## @Component 和 @Bean 的区别是什么? 1. 作用对象不同: * @Component 注解作用于类 * @Bean注解作用于方法。 2. @Component通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。 @Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了Spring这是某个类的示例,当我需要用它的时候还给我。 3. @Bean 注解比 Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册bean。比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。 ## Spring框架中哪些地方使用了反射? 在 Spring 框架中,反射机制被广泛用于以下几个方面: 1. 依赖注入:Spring 使用反射机制获取对象并进行属性注入,从而实现依赖注入。 2. AOP:Spring AOP 使用 JDK 动态代理或者 CGLIB 字节码增强技术来实现 AOP 的切面逻辑,这其中就包含了对被代理对象方法的反射调用。 3. MVC 框架:Spring MVC 框架使用反射来调用相应的控制器方法,从而实现请求的处理。 4. 数据库访问框架:Spring 的 JDBC 框架使用反射机制来实现对数据库的访问。 5. 容器管理:Spring 容器也使用了反射机制来管理对象的实例化和依赖注入。 需要注意的是,虽然反射机制为开发者提供了极大的便利性,但是过度使用反射也可能导致性能问题,在使用时需要进行适量控制。 ## Spring AOP 优点 1. 重用代码:AOP 允许在多个类或方法中共享代码。例如,可以使用 AOP 来访问日志记录或安全性检查的基本代码。 2. 简化代码:AOP 的主要目的是将横切关注点与业务逻辑分离。这意味着业务代码将更容易理解和维护。 3. 封装切面逻辑:AOP 允许开发人员将切面逻辑封装到一个模块中,并将其应用到整个应用程序中。这使得切面逻辑更容易维护和测试。 4. 提高性能:AOP 的另一个好处是可以提高应用程序的性能。例如,可以使用 AOP 来缓存数据库查询或缓存 Web 服务调用的结果。 5. 促进松耦合:AOP 使得组件之间的依赖关系更加松散。这使得代码更具可重用性,便于进行单元测试和集成测试。 ## Spring是如何解决循环依赖的(常问的 要理解) Spring框架通过一系列复杂的机制来解决循环依赖问题,特别是针对单例(singleton)Bean的循环依赖。Spring主要依赖其三级缓存机制来处理这种情况。以下是Spring解决循环依赖的详细过程: ### 一、循环依赖的定义 循环依赖指的是两个或多个Bean之间相互依赖,形成一个闭环。例如,Bean A依赖于Bean B,而Bean B又依赖于Bean A,这就构成了一个循环依赖。 ### 二、Spring解决循环依赖的机制 Spring通过三级缓存机制来解决单例Bean的循环依赖问题。这三级缓存分别是: 1. 一级缓存(singletonObjects) - **作用**:用于存储已经创建完成的单例 Bean 对象,是一个`ConcurrentHashMap`。当 Bean 创建完成后,会将其放入一级缓存中,后续获取 Bean 时直接从这里获取。 - **特点**:当Spring容器创建一个Bean时,会首先检查这个缓存中是否已经存在该Bean的实例,如果存在则直接返回,避免重复创建。 2. 二级缓存(earlySingletonObjects) - **作用**:用于存储早期暴露的 Bean 对象引用,也是一个`ConcurrentHashMap`。在 Bean 的创建过程中,将其提前暴露到二级缓存,这样在其他 Bean 依赖它时,可以先获取到一个早期的引用,解决循环依赖。 - **特点**:当Spring容器检测到循环依赖时,会将部分初始化完成的Bean放入这个缓存中,以便其他Bean能够引用。 3. 三级缓存(singletonFactories) - **作用**:是一个`ConcurrentHashMap`,用于存储 Bean 的工厂方法,通过它可以生成 Bean 对象。主要用于处理代理对象等特殊情况,在需要时通过工厂方法创建 Bean 的代理对象。 - **特点**:当Spring容器需要创建一个Bean时,如果一级和二级缓存中都不存在该Bean的实例,那么会从三级缓存中获取对应的Bean工厂,通过它来创建Bean的实例,并放入二级缓存中。 ### 三、解决循环依赖的具体过程 1. **创建 BeanA**:Spring 容器开始创建 BeanA,首先在一级缓存中查找是否存在 BeanA 的实例,如果不存在,则开始创建 BeanA。 2. **实例化 BeanA**:通过构造函数实例化 BeanA,但此时 BeanA 还未完成属性注入,处于 “半成品” 状态。 3. **暴露 BeanA 的早期引用**:将 BeanA 的早期引用放入二级缓存中,这样其他 Bean 就可以通过二级缓存获取到 BeanA 的引用,即使它还没有完全初始化。 4. **注入 BeanA 依赖的 BeanB**:在创建 BeanA 的过程中,发现它依赖于 BeanB,于是 Spring 容器开始创建 BeanB。 5. **创建 BeanB**:同样,在一级缓存中查找 BeanB 的实例,不存在则开始创建。在实例化 BeanB 后,发现 BeanB 又依赖于 BeanA。 6. **获取 BeanA 的早期引用**:由于 BeanA 已经将其早期引用放入了二级缓存,所以 BeanB 可以从二级缓存中获取到 BeanA 的引用,而不是再次尝试创建 BeanA,从而避免了循环创建的问题。 7. **完成 BeanB 的创建**:将 BeanA 注入到 BeanB 中,完成 BeanB 的属性注入和初始化,然后将 BeanB 放入一级缓存。 8. **完成 BeanA 的创建**:BeanA 获取到已经创建好的 BeanB,完成自身的属性注入和初始化,最终将 BeanA 放入一级缓存。 通过三级缓存的配合,Spring 能够巧妙地解决循环依赖问题,确保 Bean 之间的依赖关系能够正确处理,保证容器中 Bean 的正常创建和使用。需要注意的是,Spring 解决循环依赖是针对单例 Bean 而言的,对于原型 Bean,Spring 无法解决循环依赖,因为原型 Bean 每次都会创建新的实例,无法通过缓存来共享。 ### 四、总结 通过三级缓存机制,Spring能够解决单例Bean之间的循环依赖问题。需要注意的是,构造器循环依赖是无法通过Spring的三级缓存机制解决的,因为构造器循环依赖会导致Spring无法实例化任何一个Bean。在这种情况下,通常需要通过重构代码或使用其他设计模式来避免循环依赖。 ## Spring Bean 实例化和初始化过程(重要) > [参考](https://juejin.cn/post/7496876252630876195) Spring 框架作为 Java 开发中最流行的企业级框架之一,其核心容器(Spring IoC 容器)通过管理 Bean 的生命周期提供了强大的依赖注入和控制反转功能。Bean 的实例化和初始化是 Spring 容器管理的核心过程,理解这一过程对深入掌握 Spring 框架至关重要。 ### 一、Spring Bean 生命周期概述 Spring Bean 的生命周期可以分为以下几个主要阶段: 1. **Bean 定义加载**:Spring 容器读取配置文件(XML、注解或 Java 配置)中的 Bean 定义,生成 `BeanDefinition` 对象。 2. **Bean 实例化**:根据 `BeanDefinition`,Spring 通过反射机制创建 Bean 实例。 3. **属性填充**:为 Bean 注入依赖(依赖注入)。 4. **初始化**:执行初始化逻辑,包括调用 `BeanPostProcessor`、初始化方法等。 5. **使用阶段**:Bean 已经准备好,供应用程序使用。 6. **销毁阶段**:容器关闭时,销毁 Bean,调用销毁方法。 ### 二、Bean 实例化流程 #### 2.1 BeanDefinition 的作用 在 Spring 容器启动时,配置文件或注解会被解析为 `BeanDefinition` 对象,存储在 `BeanDefinitionRegistry` 中。`BeanDefinition` 包含了 Bean 的元信息,例如: - 类名(`beanClass`) - 作用域(`scope`,如 singleton、prototype) - 工厂方法(`factoryMethodName`) - 依赖关系(`dependsOn`) - 是否延迟初始化(`lazyInit`) 这些信息为后续的实例化提供了基础。 #### 2.2 实例化的触发 Spring 容器在启动时会根据配置决定是否立即实例化单例 Bean(`preInstantiateSingletons` 方法)。对于非单例 Bean,只有在首次请求时才会触发实例化。实例化的核心入口是 `AbstractBeanFactory` 的 `doGetBean` 方法。 #### 2.3 实例化过程 实例化是指通过 `BeanDefinition` 创建 Bean 对象的阶段,主要步骤如下: 1. **解析 Bean 类**: - Spring 通过 `BeanDefinition` 获取目标类的 `Class` 对象。 - 如果是工厂方法创建的 Bean,则调用指定的工厂方法。 2. **选择构造方法**: - Spring 使用 `ConstructorResolver` 解析构造方法。 - 如果有多个构造方法,Spring 会根据依赖注入的需要(例如 `@Autowired` 注解)选择合适的构造方法。 - 如果没有显式构造方法,Spring 使用默认的无参构造方法。 3. **创建实例**: - Spring 通过反射调用 `Constructor.newInstance()` 创建 Bean 实例。 - 如果 Bean 是通过 `FactoryBean` 创建的,则调用 `FactoryBean.getObject()` 方法获取实例。 - 此时,Bean 是一个“裸”对象,仅完成了内存分配,还未进行属性填充或初始化。 4. **处理循环依赖**: - 对于单例 Bean,Spring 在实例化后会将其放入一个“早期对象”缓存(`earlySingletonObjects`),以解决循环依赖问题。 - 如果检测到循环依赖,Spring 会抛出 `BeanCurrentlyInCreationException`,除非启用了 `@Lazy` 或其他机制。 #### 2.4 实例化的关键类 - `AbstractAutowireCapableBeanFactory`:负责 Bean 的实例化逻辑。 - `ConstructorResolver`:解析构造方法并决定使用哪个构造方法。 - `InstantiationStrategy`:定义实例化策略,默认使用 `CglibSubclassingInstantiationStrategy`(支持 CGLIB 动态代理)。 ### 三、Bean 初始化流程 实例化完成后,Spring 会对 Bean 进行初始化,包括属性填充、依赖注入和初始化方法的调用。初始化的核心入口是 `AbstractAutowireCapableBeanFactory` 的 `initializeBean` 方法。 #### 3.1 属性填充(Populate Bean) 属性填充是指为 Bean 注入依赖的过程,主要步骤如下: 1. **解析依赖**: - Spring 根据 `BeanDefinition` 中的 `propertyValues` 或注解(例如 `@Autowired`)确定需要注入的依赖。 - 如果使用了 `@Autowired`,Spring 会通过 `AutowiredAnnotationBeanPostProcessor` 解析注解。 2. **依赖注入**: - Spring 支持构造器注入、Setter 注入和字段注入。 - 对于构造器注入,依赖在实例化阶段已经注入。 - 对于 Setter 注入,Spring 调用 Setter 方法注入依赖。 - 对于字段注入,Spring 直接通过反射设置字段值。 3. **处理 `@Resource` 和 `@Inject`**: - 如果使用了 JSR-250 的 `@Resource` 或 JSR-330 的 `@Inject`,Spring 会通过相应的 `BeanPostProcessor` 完成注入。 4. **循环依赖检查**: - 在属性填充阶段,Spring 会再次检查循环依赖。如果依赖的 Bean 尚未创建完成,Spring 会尝试从早期对象缓存中获取。 #### 3.2 调用 Aware 接口 在属性填充后,Spring 会检查 Bean 是否实现了某些 `Aware` 接口,并调用相应的方法。例如: - `BeanNameAware`:调用 `setBeanName`,传入 Bean 的名称。 - `BeanFactoryAware`:调用 `setBeanFactory`,传入当前的 `BeanFactory`。 - `ApplicationContextAware`:调用 `setApplicationContext`,传入 `ApplicationContext`。 这些接口允许 Bean 获取容器中的上下文信息。 #### 3.3 执行 BeanPostProcessor 的前置处理 Spring 提供了 `BeanPostProcessor` 接口,允许开发者在初始化前后对 Bean 进行自定义处理。在初始化之前,Spring 会调用所有注册的 `BeanPostProcessor` 的 `postProcessBeforeInitialization` 方法。 例如,`ApplicationContextAwareProcessor` 会在这一阶段处理 `ApplicationContextAware` 接口的逻辑。 #### 3.4 执行初始化方法 Spring 支持多种方式指定初始化方法: 1. **@PostConstruct 注解**: - 如果 Bean 的方法上标注了 `@PostConstruct`,Spring 会通过 `CommonAnnotationBeanPostProcessor` 调用该方法。 2. **实现 InitializingBean 接口**: - 如果 Bean 实现了 `InitializingBean` 接口,Spring 会调用其 `afterPropertiesSet` 方法。 3. **自定义 init-method**: - 在 XML 配置或 `@Bean` 注解中可以指定 `init-method`,Spring 会调用该方法。 这三种方式的执行顺序为:`@PostConstruct` → `afterPropertiesSet` → `init-method`。 #### 3.5 执行 BeanPostProcessor 的后置处理 在初始化方法执行完成后,Spring 会调用所有 `BeanPostProcessor` 的 `postProcessAfterInitialization` 方法。这一阶段常用于代理对象的创建,例如: - `AnnotationAwareAspectJAutoProxyCreator`:为 Bean 创建 AOP 代理。 - `AsyncAnnotationBeanPostProcessor`:处理 `@Async` 注解。 #### 3.6 注册销毁回调 如果 Bean 实现了 `DisposableBean` 接口或指定了 `destroy-method`,Spring 会注册销毁回调,在容器关闭时调用。 ### 四、循环依赖的处理 循环依赖是 Spring Bean 管理中的常见问题。Spring 通过以下机制解决单例 Bean 的循环依赖: 1. **三级缓存**: - **singletonObjects**:存储完全初始化的 Bean。 - **earlySingletonObjects**:存储实例化但未初始化的 Bean。 - **singletonFactories**:存储 Bean 的工厂对象,用于创建早期引用。 2. **提前暴露对象**: - 在实例化后,Spring 会将 Bean 放入 `singletonFactories`,允许其他 Bean 在初始化阶段引用它。 3. **代理对象的处理**: - 如果 Bean 被 AOP 代理,Spring 会在 `BeanPostProcessor` 阶段替换为代理对象。 需要注意的是,Spring 无法解决构造器注入的循环依赖,因为构造器注入在实例化阶段就必须完成。 ### 五、常见问题与优化 #### 5.1 性能问题 - **过多的 BeanPostProcessor**:每个 Bean 都会经过所有 `BeanPostProcessor`,过多的处理器会影响性能。 - **复杂的依赖关系**:过多的依赖注入可能导致启动时间过长。 优化建议: - 减少不必要的 `BeanPostProcessor`。 - 使用延迟初始化(`@Lazy`)减少启动时的 Bean 加载。 #### 5.2 循环依赖问题 - **问题**:构造器注入或多例(prototype)Bean 的循环依赖无法自动解决。 - 解决方法: - 使用 `@Lazy` 注解延迟依赖注入。 - 调整代码结构,避免循环依赖。 - 使用 Setter 注入代替构造器注入。 #### 5.3 BeanPostProcessor 的误用 - **问题**:自定义 `BeanPostProcessor` 可能导致意外的行为,例如返回 null 或错误的代理对象。 - 解决方法: - 确保 `BeanPostProcessor` 的逻辑健壮。 - 在 `postProcessAfterInitialization` 中正确处理代理对象。 ## Spring注解(掌握) ![Spring注解](assets/Spring%E6%B3%A8%E8%A7%A3.png) # SpringMVC ## 什么是Spring MVC(了解) Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把模型-视图-控制器分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合 ## Spring MVC的优点 (了解) * 可以支持各种视图技术,而不仅仅局限于JSP; * 与Spring框架集成(如IoC容器、AOP等); * 清晰的角色分配: 1. 前端控制器(dispatcherServlet) 2. 请求到处理器映射(handlerMapping) 3. 处理器适配器(HandlerAdapter) 4. 视图解析器(ViewResolver) * 支持各种请求资源的映射策略。 ## Spring MVC工作流程 (必问 高频) ![image-20240131163957339](assets/image-20240131163957339.png) - 第一步:发起请求到前端控制器(DispatcherServlet) - 第二步:前端控制器请求处理器映射器HandlerMapping查找 Handler (可以根据xml配置、注解进行查找) - 第三步:处理器映射器HandlerMapping向前端控制器返回Handler,HandlerMapping会把请求映射为一个**执行链(**HandlerExecutionChain**)**对象(包含一个Handler处理器(页面控制器)对象,多个HandlerInterceptor拦截器对象),通过这种策略模式,很容易添加新的映射策略 - 第四步:前端控制器调用处理器适配器去执行Handler - 第五步:处理器适配器HandlerAdapter将会根据适配的结果去执行Handler - 第六步:Handler执行完成给适配器返回ModelAndView - 第七步:处理器适配器向前端控制器返回ModelAndView (ModelAndView是springmvc框架的一个底层对象,包括 Model和view) - 第八步:前端控制器请求视图解析器去进行视图解析 (根据逻辑视图名解析成真正的视图(jsp)),通过这种策略很容易更换其他视图技术,只需要更改视图解析器即可 - 第九步:视图解析器向前端控制器返回View - 第十步:前端控制器进行视图渲染 (视图渲染将模型数据(在ModelAndView对象中)填充到request域) - 第十一步:前端控制器向用户响应结果 ## Spring MVC 的核心组件(了解) ### DispatcherServlet 作用:接收请求,响应结果,相当于转发器,中央处理器。有了DispatcherServlet减少了其它组件之间的耦合度。 ### 处理器映射器HandlerMapping 作用:根据请求的url查找Handler**,既**负责完成客户请求到 Controller 映射。SpringMVC提供了不同的映射器实现实现不同的映射方式,例如:配置文件方式、实现接口方式、注解方式等。 ### 处理器适配器HandlerAdapter 作用:按照特定规则(HandlerAdapter要求的规则)去执行Handler。 ### 处理器Handler Handler是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下,Handler对具体的用户请求进行处理。 由于Handler设计到具体的用户业务请求,所以一般情况需要程序员根据业务需求开发Handler。 ### 视图解析器ViewResolver 作用:进行视图解析,根据逻辑视图名解析成真正的视图(view) ViewResolver负责将处理结果生成View视图,ViewResolver首先根据逻辑视图名解析成物理视图名,即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面的展示给用户。SpringMVC框架提供了很多View视图类型,包括:JSTLView、freemarkerView、pdfView等等. ### 视图View 注意:View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf…),一般情况下需要通过页面标签或者页面模板技术将模型数据通过页面展示给用户,需要由程序员根据业务需求开发具体的页面。 > **总结:** 需要我们开发的工作只有处理器 Handler 的编写以及视图比如JSP页面的编写。 ## @RestController 和 @Controller 有什么区别?(中频率) ### 返回结果不同 * @Controller 返回逻辑视图 * @RestController 返回的是xml或json格式数据 ### 组合不同 @RestController 是@Controller+@ResponseBody两个注解的复合 注解。 `@RestController` 注解,在 `@Controller` 基础上,增加了 `@ResponseBody` 注解,更加适合目前前后端分离的架构下,提供 Restful API ,返回 JSON 数据格式。 ## @RequestMapping 和 @GetMapping 注解有什么不同? * `@RequestMapping`:可注解在类和方法上;`@GetMapping` 仅可注册在方法上 * `@RequestMapping`:可进行 GET、POST、PUT、DELETE 等请求方法;`@GetMapping` 是 `@RequestMapping` 的 GET 请求方法的特例。 ## SpringMVC的常用注解(掌握) Spring MVC注解 ## 后端验证注解(掌握) ![验证](assets/%E9%AA%8C%E8%AF%81.png) ## Spring 中的拦截器是什么?它的作用是什么?(掌握) > 记住: > > 1. 拦截器的定义 实现HandlerInterceptor接口 > 2. 拦截器注册 实现WebMvcConfiger ,重写addInterceptor方法 在Spring框架中,拦截器(Interceptors)是一种基于AOP(面向切面编程)原则设计的组件,用来拦截应用程序的操作过程,例如HTTP请求的处理。它们主要用在Spring MVC框架中,但也可以在任何使用了Spring的地方应用拦截器的概念。 Springmvc的拦截器实现HandlerInterceptor接口后,会有三个抽象方法需要实现,分别为方法前执行preHandle,方法后postHandle,页面渲染后afterCompletion。 > 这使得拦截器成为处理横切关注点的理想选择,例如日志记录、性能统计、安全性和事务处理等。 **Spring MVC拦截器的作用** 在Spring MVC中,拦截器主要用于处理来自浏览器的HTTP请求。它们与Servlet过滤器相似,但提供了更细粒度的控制,因为它们可以访问请求的上下文、执行的控制器和渲染的视图等信息。Spring MVC拦截器可以: * 前置处理:在Controller方法(也就是请求的处理器)执行之前,可以进行一些前置处理。比如,可以验证用户身份、记录请求日志、处理跨站请求伪造(CSRF)等。 * 后置处理:在Controller方法执行之后,但在视图渲染之前,进行处理。这可以用来添加额外的模型数据到视图中,或者处理特定的业务逻辑。 * 完成处理:在整个请求结束之后,也就是视图渲染完成后进行。这可以用于清理资源,或者记录整个请求处理的完成时间等。 ## Spring拦截器的执行顺序(了解) Springmvc的拦截器实现HandlerInterceptor接口后,会有三个抽象方法需要实现,分别为方法前执行preHandle,方法后postHandle,页面渲染后afterCompletion。 1. 当俩个拦截器都实现放行操作时,顺序为preHandle 1,preHandle 2,postHandle 2,postHandle 1,afterCompletion 2,afterCompletion 1 2. 当第一个拦截器preHandle返回false,也就是对其进行拦截时,第二个拦截器是完全不执行的,第一个拦截器只执行preHandle部分。 3. 当第一个拦截器preHandle返回true,第二个拦截器preHandle返回false,顺序为preHandle 1,preHandle 2 ,afterCompletion 1 总结: * preHandle 按拦截器定义顺序调用 * postHandler 按拦截器定义逆序调用 * afterCompletion 按拦截器定义逆序调用 * postHandler 在拦截器链内所有拦截器返成功调用 * afterCompletion 只有preHandle返回true才调用 ## CORS(中) CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出[`XMLHttpRequest`](https://www.ruanyifeng.com/blog/2012/09/xmlhttprequest_level_2.html)请求,从而克服了AJAX只能[同源](https://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html)使用的限制。 > 同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域;参考下图 > > ![image-20220811145043190](assets/image-20220811145043190.png) > > ### 简介 CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。 整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。 ### 两种请求 浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。 #### 简单请求 只要同时满足以下两大条件,就属于简单请求。 ![image-20220811145316119](assets/image-20220811145316119.png) 这是为了兼容表单(form),因为历史上表单一直可以发出跨域请求。AJAX 的跨域设计就是,只要表单可以发,AJAX 就可以直接发。 凡是不同时满足上面两个条件,就属于非简单请求。 浏览器对这两种请求的处理,是不一样的。 #### 非简单请求 非简单请求是那种对服务器有特殊要求的请求,比如请求方法是`PUT`或`DELETE`,或者`Content-Type`字段的类型是`application/json`。 非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的`XMLHttpRequest`请求,否则就报错。 ### 服务器响应头设置 服务器收到"预检"请求以后,检查了`Origin`、`Access-Control-Request-Method`和`Access-Control-Request-Headers`字段以后,确认允许跨源请求,就可以做出回应。 ##### **Access-Control-Allow-Origin** 该字段是必须的。它的值要么是请求时`Origin`字段的值,要么是一个`*`,表示接受任意域名的请求。 ##### **Access-Control-Allow-Credentials** 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为`true`,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为`true`,如果服务器不要浏览器发送Cookie,删除该字段即可。 ##### **Access-Control-Expose-Headers** 该字段可选。CORS请求时,`XMLHttpRequest`对象的`getResponseHeader()`方法只能拿到6个基本字段:`Cache-Control`、`Content-Language`、`Content-Type`、`Expires`、`Last-Modified`、`Pragma`。如果想拿到其他字段,就必须在`Access-Control-Expose-Headers`里面指定。上面的例子指定,`getResponseHeader('FooBar')`可以返回`FooBar`字段的值。 ##### **Access-Control-Allow-Methods** 该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。 ##### **Access-Control-Max-Age** 该字段可选,用来指定本次预检请求的有效期,单位为秒。如有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。 ### MVC跨越设置 #### 方法一(了解) 使用mvc标签配置 ~~~xml ~~~ 说明: * path /** 拦截所有请求 * allowed-methods 允许请求的方法 * 代表所有 * allowed-headers 允许的请求头 * allowed-origins 允许访问的域名 * 代表所有域名都可以访问 * max-age 重新预检验跨域的缓存时间 #### 方法二(了解) 使用@CrossOrigin注解,要求srpingmvc 4.2以后版本 ~~~java @CrossOrigin public class BaseController {} ~~~ #### 方法三(可以) 创建过滤器设置 ~~~java /* 允许跨域的主机地址 */ response.setHeader("Access-Control-Allow-Origin", "*"); /* 允许跨域的请求方法GET, POST, HEAD 等 */ response.setHeader("Access-Control-Allow-Methods", "*"); /* 重新预检验跨域的缓存时间 (s) */ response.setHeader("Access-Control-Max-Age", "3600"); /* 允许跨域的请求头 */ response.setHeader("Access-Control-Allow-Headers", "*"); /* 是否携带cookie */ response.setHeader("Access-Control-Allow-Credentials", "true"); ~~~ #### 方法四 (可以 ) 如果跨域与登录拦截器同时存在,因为拦截器先执行,仍然会出现跨域问题,可以通过配置CorsFilter解决,如下: ~~~java @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); // 设置允许跨域请求的域名 config.addAllowedOrigin("*"); // 是否允许证书 config.setAllowCredentials(false); // 设置允许的方法 config.addAllowedMethod("*"); // 允许任何头 config.addAllowedHeader("*"); UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); configSource.registerCorsConfiguration("/**", config); return new CorsFilter(configSource); } ~~~ #### 方法五(推荐) 在使用全注解的情况下,实现WebMvcConfiger配置跨域处理。 > 注意:有时项目环境中有过多的拦截器或过滤 器,造成下面的拦截没有第一时间执行,跨域设置失败,可以考虑通过Filter来实现。 ~~~java @Configuration @ComponentScan(basePackages = {"com.by"}) @EnableWebMvc public class SpringMvcConfig implements WebMvcConfigurer { /** * 跨域处理 * @param registry */ @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**")//拦截所有的请求 .allowedHeaders("*")//允许 的请求头 .allowedOrigins("*")//允许的域名 .allowedMethods("GET","POST","PUT","DELETE","OPTIONS")//允许的方法 .maxAge(3600);//预检请求有效时间 } } ~~~ ## Restful风格(了解) ### 什么是Restful风格 > Restful是一种api设计风格,对同一资源路径,如/user ,不同的动词代表不同的作用。 ​ REST(英文:Representational State Transfer,简称REST),可译为"表现层状态转化”。 ​ RESTful(REST 风格)是一种当前比较流行的互联网软件架构模式, 它是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它充分并正确地利用 HTTP 协议的特性,为我们规定了一套统一的资源获取方式,以实现不同终端之间(客户端与服务端)的数据访问与交互。 ​ 基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。 ### Restful的特点 1.URI:每一个URI(统一资源定位符)指向一个特定的资源,通过URI来访问资源,独一无二 2.客户端和服务器之间,传递这种资源的某种表现 3.客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。 RESTful 架构风格是围绕资源展开的,资源操作都是统一接口的: * GET(SELECT):从服务器取出资源(一项或多项)。 * POST(CREATE):在服务器新建一个资源。 * PUT(UPDATE):在服务器更新资源(客户端提供完整资源数据)。 * PATCH(UPDATE):在服务器更新资源(客户端提供需要修改的资源数据)。 * DELETE(DELETE):从服务器删除资源。 > 使用RESTful操作资源 > 【GET】 /users # 查询用户信息列表 > > 【GET】 /users/1001 # 查看某个用户信息 > > 【POST】 /users # 新建用户信息 > > 【PUT】 /users/1001 # 更新用户信息(全部字段) > > 【PATCH】 /users/1001 # 更新用户信息(部分字段) > > 【DELETE】 /users/1001 # 删除用户信息 ### 总结 ​ RESTful风格要求每个资源都使用 URI (Universal Resource Identifier) 得到一个唯一的地址。所有资源都共享统一的接口,以便在客户端和服务器之间传输状态。使用的是标准的 HTTP 方法,比如 GET、PUT、POST 和 DELETE。 总之就是REST是一种写法上规范,获取数据或者资源就用GET,更新数据就用PUT,删除数据就用DELETE,然后规定方法必须要传入哪些参数,每个资源都有一个地址。 ## Spring MVC 拦截器与过滤器区别(了解) Spring MVC 拦截器(Interceptor)和过滤器(Filter)都是 Java Web 应用中用于处理请求的组件,但它们在设计层面、应用场景和功能上有明显区别。以下是两者的主要差异: ### **1. 所属规范与层次不同** | **维度** | **过滤器(Filter)** | **拦截器(Interceptor)** | | ------------ | ------------------------------------------- | -------------------------- | | **规范归属** | Java EE(Servlet)规范,不属于 Spring 框架 | Spring MVC 框架的组件 | | **应用层次** | 直接作用于 Servlet 容器(如 Tomcat) | 作用于 Spring MVC 框架内部 | | **执行时机** | 在请求进入 Servlet 之前和响应返回浏览器之前 | 在请求进入 Controller 前后 | ### **2. 实现机制与功能差异** | **功能点** | **过滤器(Filter)** | **拦截器(Interceptor)** | | ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | **接口** | 实现 `javax.servlet.Filter` 接口 | 实现 `org.springframework.web.servlet.HandlerInterceptor` 接口 | | **生命周期** | 由 Servlet 容器管理(单例) | 由 Spring 容器管理(支持依赖注入) | | **可访问对象** | 仅能访问 `ServletRequest`、`ServletResponse` 和 `FilterConfig` | 可访问 Spring 容器中的 Bean、请求参数、Model 等 | | **执行顺序控制** | 依赖 `web.xml` 中的 `` 顺序 | 依赖 `addInterceptors()` 方法中的注册顺序 | ### **3. 执行流程对比** **过滤器的执行流程**: 1. 请求进入 Tomcat → 匹配 Filter → 执行 `doFilter()` → 调用 `chain.doFilter()` 放行 → 请求到达 DispatcherServlet。 2. 响应返回时,按相反顺序再次执行 Filter。 **拦截器的执行流程**: 1. 请求到达 DispatcherServlet → 匹配 HandlerMapping → 执行拦截器的 `preHandle()` → 进入 Controller 方法。 2. Controller 处理完成后,执行 `postHandle()` → 视图渲染后,执行 `afterCompletion()`。 ### **4. 应用场景差异** | **场景** | **过滤器(Filter)** | **拦截器(Interceptor)** | | -------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | **通用功能** | - 字符编码转换(如 `CharacterEncodingFilter`) - 敏感词过滤 - 日志记录 - 跨域处理(CORS) | - 权限验证(如登录状态检查) - 性能监控(如接口耗时统计) - 动态修改 ModelAndView | | **依赖注入** | 不支持直接使用 Spring Bean | 可直接注入 Service 或 Repository | | **细粒度控制** | 无法区分不同 Controller 或方法 | 可针对特定路径或方法进行拦截(如 `@RequestMapping` 匹配) | ### **5. 配置方式对比** **过滤器配置**(以 Java 配置为例): ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Bean public FilterRegistrationBean characterEncodingFilter() { FilterRegistrationBean registration = new FilterRegistrationBean<>(); registration.setFilter(new CharacterEncodingFilter("UTF-8", true)); registration.addUrlPatterns("/*"); // 拦截所有请求 return registration; } } ``` **拦截器配置**: ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/api/**") // 拦截路径 .excludePathPatterns("/api/login"); // 排除路径 } } ``` ### **总结** - **过滤器**:更底层、更通用,适合处理与 Servlet 相关的全局功能(如编码、安全头、日志)。 - **拦截器**:更贴近业务层,依赖 Spring 容器,适合处理与 Controller 或请求参数相关的逻辑(如权限验证、性能监控)。 实际项目中,两者通常结合使用,例如: - 用过滤器处理字符编码、跨域、敏感词过滤。 - 用拦截器处理登录验证、接口限流、AOP 增强。 # MyBatis > 了解jpa mybatis-plus 二者区别 ## 谈一下你对mybatis的认识?(了解) * 介绍 mybaits > MyBatis 是一款优秀的持久层框架,它对 JDBC 操作数据库的过程进行了封装,使得开发者只需关注 SQL 本身,而无需处理如注册驱动、创建连接、关闭资源等繁杂的过程。 > > ### 主要特点 > > 1. **SQL 与代码分离**:MyBatis 采用 XML 或注解的方式将 SQL 语句与 Java 代码分离,这样便于对 SQL 进行统一管理和优化。 > 2. **灵活的 SQL 编写**:它支持编写动态 SQL,借助 OGNL 表达式能够实现如 `if`、`choose`、`where` 等动态元素,极大地提升了 SQL 编写的灵活性。 > 3. **强大的映射能力**:可以将结果集灵活地映射为 Java 对象,不仅支持基本类型的映射,还支持关联对象和集合的映射。 > 4. **轻量级框架**:MyBatis 框架本身不依赖过多外部库,使用起来较为轻便,学习成本也相对较低。 * 介绍mybatis的优缺点 适合场景 > ### 优缺点 > > **优点**: > > - 能够完全掌控 SQL,便于进行性能优化。 > - 可以与传统项目无缝集成,尤其适合对 SQL 优化有较高要求的场景。 > - 支持多种数据库,具备良好的数据库移植性。 > > **缺点**: > > - 需要手动编写 SQL,在复杂查询场景下开发效率可能会受到一定影响。 > - 对复杂的对象关系映射支持不够友好,可能需要编写大量的映射配置。 > > ### 适用场景 > > - 适用于对 SQL 性能要求较高,需要精细控制 SQL 执行的项目。 > - 适合传统的 CRUD 应用以及数据访问层相对稳定的项目。 > - 可用于与其他框架(如 Spring、Spring Boot)集成,构建企业级应用。 ## #{}和${}的区别是什么?(高频) 1. `#{}` 是预编译处理,`${}`是字符串替换。 2. Mybatis在执行 SQL 时,`#{}` 会被替换成 `?` 占位符,这样可以有效防止 SQL 注入攻击。MyBatis 会依据参数类型自动进行类型转换。 3. Mybatis在 SQL 预编译之前,`${}` 就会被直接替换为参数值,这种方式存在 SQL 注入风险。参数值不会经过类型转换,所以需要手动处理引号等问题。主要用于动态表名、列名或者 ORDER BY 子句等场景。 ## mybatis分页实现的方式 有哪些(了解) > 主要三类: > > 1. 数据库本身分页 (扩展: 大数据分页 ) > 2. myBatis内置分页RowBound > 3. 分页插件 MyBatis实现分页的方式主要有以下几种: 1. 物理分页 - **原生SQL分页**:直接在SQL语句中使用数据库提供的分页语法,如MySQL的`LIMIT`、Oracle的`ROWNUM`或`ROW_NUMBER()`、SQL Server的`OFFSET-FETCH`等。这种方式依赖于数据库的分页功能,性能较好,但SQL语句的编写需要针对具体的数据库进行调整。 - **MyBatis插件(Interceptor)**:通过实现MyBatis的Interceptor接口,可以在SQL执行前后进行拦截处理,自动地在查询语句后添加分页参数。这种方式的好处是可以在不修改原有SQL语句的情况下实现分页,提高了代码的复用性和可维护性。PageHelper是MyBatis分页插件的一个流行选择,它支持多种数据库,并且配置和使用起来相对简单。 2. 内存分页RowBounds(不推荐): - 这种方式是在查询出所有结果后,在Java内存中通过程序逻辑进行分页处理。这种方法虽然实现简单,但在处理大量数据时效率极低,因为它需要将所有数据加载到内存中,消耗大量内存资源,并且分页操作(如跳页)时性能更差。因此,在实际开发中,几乎不会使用内存分页。 3. 第三方分页插件或库 - 除了PageHelper之外,还有其他一些第三方分页插件或库,如MyBatis-Plus的分页功能。MyBatis-Plus是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生。它内置了分页插件,可以非常方便地实现分页功能,并且支持多种数据库。 4. 自定义分页查询 - 在一些特殊场景下,可能需要根据业务逻辑实现自定义的分页查询。这通常涉及到在Mapper接口中定义分页查询的方法,并在对应的XML文件中编写相应的SQL语句。在SQL语句中,可以使用数据库的分页语法或者结合业务逻辑进行分页处理。这种方式需要开发者对数据库和MyBatis有较深的理解。 在实际开发中,推荐使用物理分页的方式,特别是通过MyBatis插件(如PageHelper)来实现分页功能,因为它既高效又方便。如果项目已经集成了MyBatis-Plus,那么直接使用MyBatis-Plus的分页功能也是一个不错的选择。 ## 分页插件的原理(掌握) > mybatis中我们通常使用分页插件PageHelper来实现分页 **MyBatis分页插件的原理主要基于MyBatis的插件机制,通过拦截SQL查询操作,在查询语句中动态添加分页参数,从而实现分页查询功能。**具体来说,分页插件的原理可以归纳为以下几点: ### 1. 插件实现 分页插件通常需要实现MyBatis的`Interceptor`接口,并重写`intercept`方法。在`intercept`方法中,插件可以拦截到MyBatis执行SQL查询的关键点,即StatementHandler的`prepare`方法。 ### 2. SQL拦截与重写 当StatementHandler的`prepare`方法被触发时,分页插件会拦截到这个查询操作,并获取到当前的目标对象(它包含了要执行的SQL语句信息)。随后,插件会根据分页参数(如当前页码、每页显示的记录数等)来重写SQL语句,通常是在SQL语句的末尾添加`LIMIT`和`OFFSET`子句(或其他数据库特定的分页语法),以实现分页功能。 ~~~java @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) public class PageMe implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); //获取参数 Map map = (Map) boundSql.getParameterObject(); int page = Integer.parseInt(map.get("page").toString()); int pageSize = Integer.parseInt(map.get("pageSize").toString()); int offset = (page - 1) * pageSize; //修改sql语句 String tmp = sql + " limit " + offset + "," + pageSize; Class clazz = BoundSql.class; Field field = clazz.getDeclaredField("sql"); field.setAccessible(true); field.set(boundSql, tmp); System.out.println(boundSql.getSql()); return invocation.proceed(); } } ~~~ ### 3. 分页参数传递 分页参数可以通过多种方式传递给分页插件,例如通过Mapper接口方法的参数、全局配置、注解等。在分页插件内部,这些参数会被用来计算`LIMIT`和`OFFSET`的值,并嵌入到SQL语句中。 ### 4. 执行分页查询 修改后的SQL语句会由MyBatis执行,数据库根据这个带有分页参数的SQL语句返回对应页的数据。分页插件还会处理查询结果,确保只返回当前页的数据给调用者。 ### 5. 配置与使用 分页插件的使用通常需要在MyBatis的配置文件中进行配置,指定要使用的分页插件类,并设置相应的参数。然后,在Mapper接口中定义分页查询的方法,并在对应的XML文件中编写原始的SQL语句(无需包含分页参数)。最后,在业务代码中调用Mapper接口的分页查询方法,并传入分页参数即可实现分页查询。 ~~~xml ~~~ ### 6. 注意事项 - 分页插件的实现可能会依赖于具体的数据库方言,因为不同的数据库有不同的分页语法。 - 在使用分页插件时,需要注意SQL语句的编写规范,特别是不要在SQL语句的末尾添加分号(`;`),因为分页插件会在SQL语句后添加分页参数,分号会导致SQL语句执行出错。 - 分页插件可能会提供多种配置选项,如是否计算总记录数、是否进行内存分页等,需要根据实际需求进行选择。 综上所述,MyBatis分页插件通过拦截SQL查询操作、重写SQL语句、传递分页参数、执行分页查询等步骤,实现了对数据库查询结果的分页处理。这种方式既提高了查询效率,又降低了开发难度,是MyBatis框架中非常实用的一个功能。 ## mybatis 插件应用场景(重要) MyBatis 插件是一种强大的机制,可在 SQL 执行过程的关键节点插入自定义逻辑。以下是常见的应用场景及示例: ### **1. SQL 性能监控与分析** 在 SQL 执行前后记录时间,统计慢查询,生成性能报表。 **示例**:统计每条 SQL 的执行时间 ```java @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class PerformanceInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { long startTime = System.currentTimeMillis(); try { return invocation.proceed(); } finally { long endTime = System.currentTimeMillis(); System.out.printf("SQL执行耗时: %dms%n", (endTime - startTime)); } } } ``` ### **2. 数据权限过滤** 自动为 SQL 添加数据权限条件,实现行级数据隔离。 **示例**:根据当前用户角色自动过滤数据 ```java @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) public class DataPermissionInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); String originalSql = boundSql.getSql(); // 获取当前用户角色,添加过滤条件 String userRole = SecurityUtils.getCurrentUserRole(); if ("普通用户".equals(userRole)) { String modifiedSql = originalSql + " WHERE user_id = " + SecurityUtils.getCurrentUserId(); // 通过反射修改SQL Field sqlField = boundSql.getClass().getDeclaredField("sql"); sqlField.setAccessible(true); sqlField.set(boundSql, modifiedSql); } return invocation.proceed(); } } ``` ### **3. 分页功能实现** 自动生成分页 SQL,简化分页逻辑。 **示例**:基于方言的分页插件 ```java @Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class PaginationInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 获取参数和SQL信息 Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; Object parameter = args[1]; RowBounds rowBounds = (RowBounds) args[2]; // 判断是否需要分页 if (rowBounds == RowBounds.DEFAULT) { return invocation.proceed(); } // 获取数据库方言,生成分页SQL String dialect = "mysql"; // 可通过配置获取 BoundSql boundSql = ms.getBoundSql(parameter); String originalSql = boundSql.getSql(); // 生成分页SQL(示例为MySQL) String pageSql = originalSql + " LIMIT " + rowBounds.getOffset() + ", " + rowBounds.getLimit(); // 执行分页查询... return invocation.proceed(); } } ``` ### **4. 敏感数据加密 / 解密** 自动对敏感字段(如手机号、身份证号)进行加密存储和查询解密。 **示例**:字段加密插件 ```java @Intercepts({ @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}), @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) public class EncryptionInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 参数处理阶段:加密敏感字段 if (invocation.getTarget() instanceof ParameterHandler) { // 获取参数并加密敏感字段... } // 结果处理阶段:解密敏感字段 if (invocation.getTarget() instanceof ResultSetHandler) { // 解密查询结果中的敏感字段... } return invocation.proceed(); } } ``` ### **5. SQL 注入防护** 对 SQL 参数进行安全校验,防止 SQL 注入攻击。 **示例**:参数净化插件 ```java @Intercepts({ @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}) }) public class SqlInjectionInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget(); // 获取参数对象 Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject"); parameterField.setAccessible(true); Object parameterObject = parameterField.get(parameterHandler); // 检查并过滤参数中的恶意字符 if (parameterObject instanceof String) { String cleanedParam = sanitizeSql((String) parameterObject); parameterField.set(parameterHandler, cleanedParam); } return invocation.proceed(); } private String sanitizeSql(String input) { // 过滤SQL注入风险字符 return input.replaceAll("([';])+|(--)+", ""); } } ``` ### **6. 多数据源路由** 根据业务需求动态切换数据源,实现读写分离或分库分表。 **示例**:读写分离插件 ```java @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class DataSourceRoutingInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 判断SQL类型(读/写) MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; if (ms.getSqlCommandType() == SqlCommandType.SELECT) { // 使用从库 DynamicDataSourceContextHolder.setDataSource("slave"); } else { // 使用主库 DynamicDataSourceContextHolder.setDataSource("master"); } try { return invocation.proceed(); } finally { DynamicDataSourceContextHolder.clearDataSource(); } } } ``` ### **7. 自动填充字段** 自动为实体类的创建时间、更新时间等字段赋值。 **示例**:自动填充创建时间和更新时间 ```java @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) public class AutoFillInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object parameter = invocation.getArgs()[1]; if (parameter != null) { // 自动填充创建时间 if (parameter instanceof BaseEntity && ((BaseEntity) parameter).getCreateTime() == null) { ((BaseEntity) parameter).setCreateTime(LocalDateTime.now()); } // 自动填充更新时间 if (parameter instanceof BaseEntity) { ((BaseEntity) parameter).setUpdateTime(LocalDateTime.now()); } } return invocation.proceed(); } } ``` ### **插件注册配置** 在 MyBatis 配置文件中注册插件: ```java @Configuration public class MyBatisConfig { @Bean public PerformanceInterceptor performanceInterceptor() { return new PerformanceInterceptor(); } @Bean public DataPermissionInterceptor dataPermissionInterceptor() { return new DataPermissionInterceptor(); } // 其他插件... @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); // 添加插件 Interceptor[] interceptors = { performanceInterceptor(), dataPermissionInterceptor() // 其他插件... }; factory.setPlugins(interceptors); return factory.getObject(); } } ``` ### **注意事项** 1. **拦截点限制**:MyBatis 仅允许拦截 `Executor`、`StatementHandler`、`ParameterHandler` 和 `ResultSetHandler` 的方法。 2. **性能影响**:插件会影响 SQL 执行流程,复杂逻辑可能导致性能下降。 3. **兼容性**:插件可能与 MyBatis 版本或其他插件冲突,需谨慎使用。 通过合理使用插件,可以在不修改原有业务逻辑的前提下增强 MyBatis 功能,提升开发效率和系统性能。 ## Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?(重要) * MyBatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。 * 延迟加载 * 全局延迟加载 开启时,所有关联对象都会延迟加载 > ~~~xml > > ~~~ * 局部延迟加载 可以覆盖全局 > 在association或collection上有fetchType属性。 > > fetchType设置: > > * eager 立即加载 > * lazy 延迟加载 * 它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用student.getClazz().getName(),拦截器invoke()方法发现student.getClazz()是null值,那么就会单独发送事先保存好的查询关联班级Clazz对象的sql,把Clazz查询上来,然后调用clazz.setName(name),于是student的对象clazz属性就有值了,接着完成student.getClazz().getName()方法的调用。 ## MyBatis的一级、二级缓存(高频) 1. 一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。 > 一级缓存也是session级缓存,在session未关闭的情况的下,多次执行相同的查询 ,会直接取缓存数据,不再查询数据库 > > sesson是线程不安全的,使用完要关闭 > > 一级缓存 的作用不是很大。 2. 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。 > 二级缓存是命名空间级缓存,在同一命名空间下的查询,都可以使用的缓存。 > > 实现条件: > > * 使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态) > > * 默认二级缓存是没有开启的,要开启,在映射文件中加入如下: > > ~~~xml > > ~~~ > > > 二级缓存效果: > > > > - 映射语句文件中的所有 select 语句的结果将会被缓存。 > > - 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。因为默认属性flushCache为true > > - 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。 > > - 缓存不会定时进行刷新(也就是说,没有刷新间隔)。 > > - 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。 > > - 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。 > > > > 前提:对象必须实现序列化接口 > > > > 序列化 :将对象写入二进制文件中 ObjectOutputStream > > > > 反序列化 :从二进制文件中读取内容转换为对象 ObjectInputStream > > * 二级缓存的对象要实现序列化接口 > * select 语句 添加属性flushCache="true" 执行时也会刷新缓存 ,默认值为false > * useCache 默认为true,会缓存 二级结晶,否则不缓存 > > 缓存属性: > > * eviction 清除策略 可用的清除策略有: > > > - `LRU` – 最近最少使用:移除最长时间不被使用的对象。 > > - `FIFO` – 先进先出:按对象进入缓存的顺序来移除它们。 > > - `SOFT` – 软引用:基于垃圾回收器状态和软引用规则移除对象。 > > - `WEAK` – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。 > > 默认的清除策略是 LRU。 > > * flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。 > > * size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。 > > * readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。 3. 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。 适用场景: 1. 更新频率低 2. 查询频率高 ,如字典数据、权限 ## 在 mapper 中如何传递多个参数?(高频) ### 方法1:顺序传参法(不推荐) ~~~java public User selectUser(String name, int deptId); ~~~ #### arg传参 ```xml ``` 使用arg0 ,arg1 .... 下标从0开始 分别代表第一、第二 ...参数 #### param传参 ~~~xml ~~~ 使用param1,param2 下标从1开始 分别代表第一、第二 参数 ### 方法2:@Param注解传参法(常用) ~~~java public User selectUser(@Param("userName") String name, int @Param("deptId") deptId); ~~~ ```xml ``` \#{}里面的名称对应的是注解@Param括号里面修饰的名称。 这种方法在参数不多的情况还是比较直观的,推荐使用。 ### 方法3:Map传参法 ~~~java public User selectUser(Map params); ~~~ ~~~xml ~~~ #{}里面的名称对应的是Map里面的key名称。 这种方法适合传递多个参数,且参数易变能灵活传递的情况。 ### 方法4:Java Bean传参法 ~~~java public User selectUser(User user); ~~~ ~~~xml ~~~ \#{}里面的名称对应的是User类里面的成员属性。 这种方法直观,需要建一个实体类,扩展不容易,需要加属性,但代码可读性强,业务逻辑处理方便,推荐使用。 ## Mybatis如何执行批量操作(掌握) 使用foreach标签 foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合。foreach标签的属性主要有item,index,collection,open,separator,close。 * item  表示集合中每一个元素进行迭代时的别名,随便起的变量名; * index  指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用; * open  表示该语句以什么开始,常用“(”; * separator表示在每次进行迭代之间以什么符号作为分隔符,常用“,”; * close  表示以什么结束,常用“)”。 在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有一下3种情况: 1. 如果传入的是单参数且参数类型是一个List的时候,collection属性值为list 2. 如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array 3. 如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的 ​ map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key 具体用法如下: ~~~xml //推荐使用 INSERT INTO emp(ename,gender,email,did) VALUES (#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id}) ~~~ 批量删除 ~~~xml delete from emp where id in #{id} ~~~ > delete from emp where id in (1,2,4,5....) ## 如何获取生成的主键 ### 对于支持主键自增的数据库(MySQL) ~~~xml insert into user( user_name, user_password, create_time) values(#{userName}, #{userPassword} , #{createTime, jdbcType= TIMESTAMP}) ~~~ * useGeneratedKeys 设置为true ,允许mybatis获取数据库内部自动生成的主键 * keyProperty 告诉mybatis主键值写入哪里 > 两种情况 : > > 1. 如果保存时参数是javabean,那么keyProperty 指定javabean中的一个属性名 > 2. 如果参数是Map类型 ,那么keyProperty 指定在map中增加或替换指定key的值 * keyColumn 指定数据库表中的主键名称 ### 不支持主键自增的数据库(Oracle) 对于像Oracle这样的数据,没有提供主键自增的功能,而是使用序列的方式获取自增主键。 可以使用<selectKey>标签来获取主键的值,这种方式不仅适用于不提供主键自增功能的数据库,也适用于提供主键自增功能的数据库 <selectKey>一般的用法 ~~~xml ~~~ ![在这里插入图片描述](assets/6af71ba1883d49c3890d7df1754b677e.png) ~~~xml SELECT USER_ID.nextval as id from dual insert into user( user_id,user_name, user_password, create_time) values(#{userId},#{userName}, #{userPassword} , #{createTime, jdbcType= TIMESTAMP}) ~~~ 此时会将Oracle生成的主键值赋予userId变量。这个userId 就是USER对象的属性,这样就可以将生成的主键值返回了。如果仅仅是在insert语句中使用但是不返回,此时keyProperty=“任意自定义变量名”,resultType 可以不写。 Oracle 数据库中的值要设置为 BEFORE ,这是因为 Oracle中需要先从序列获取值,然后将值作为主键插入到数据库中。 ## MyBatis 动态 SQL (掌握) ### MyBatis 动态 SQL 的作用 MyBatis 动态 SQL 是 MyBatis 框架提供的一种强大功能,**其主要作用在于提高 SQL 语句的灵活性和重用性**。通过动态 SQL,开发者可以根据不同的业务逻辑和运行时传入的参数,动态地生成或拼接 SQL 语句,从而避免了手动编写大量条件判断语句和拼接 SQL 的繁琐过程。这不仅减少了代码的重复度,还提高了代码的可维护性和可读性。 ### 动态 SQL 的执行原理 **MyBatis 动态 SQL 的执行原理主要依赖于 XML 映射文件中的动态 SQL 标签和 OGNL(Object-Graph Navigation Language)表达式**。当 MyBatis 加载映射文件时,会解析其中的动态 SQL 标签,并将它们转换为一系列的逻辑表达式和条件判断结构。在 后,MyBatis 会根据这些参数值动态地决定哪些 SQL 片段应该被执行或忽略。具体来说,**MyBatis 使用 OGNL 表达式来解析和计算动态 SQL 中的条件表达式,并在预编译阶段将动态生成的 SQL 发送给数据库进行预编译,然后在执行阶段绑定参数值并最终执行 SQL**。这样既保证了 SQL 的安全性和性能(由于预编译),又实现了 SQL 结构的动态化和灵活性。 ### MyBatis 中的动态 SQL 标签 MyBatis 提供了多种动态 SQL 标签,以便在 XML 映射文件中编写灵活的 SQL 语句。以下是一些主要的动态 SQL 标签及其用途: 1. **if**:用于在生成的 SQL 语句中添加条件判断。可以根据指定的条件决定是否包含某个 SQL 语句片段。 2. **choose**(包含 **when**、**otherwise**):类似于 Java 中的 switch 语句,根据条件选择执行不同的 SQL 语句片段。`choose` 标签可以包含多个 `when` 标签和一个可选的 `otherwise` 标签。 3. **foreach**:用于遍历集合或数组,并根据指定的模板将集合元素或数组元素插入到 SQL 语句中。通常用于处理批量操作或 IN 子查询等场景。 4. **set**:用于在生成的 SQL 语句中添加 SET 子句。它主要用于更新操作,可以根据条件来动态生成需要更新的列。 5. **where**:用于在生成的 SQL 语句中添加 WHERE 子句。它可以自动处理条件语句的前缀,并在有条件语句存在时添加 WHERE 关键字。 6. **trim**:通过修剪 SQL 语句的开头和结尾来动态生成 SQL 片段。它可以用于去除不必要的 SQL 关键字或条件语句,并提供了一些属性来定义修剪规则。 7. **bind**:用于将表达式的结果绑定到一个变量上。可以在 SQL 语句中使用这个变量,避免重复计算表达式。 这些动态 SQL 标签在 MyBatis 中提供了灵活的查询和更新操作的能力,使得开发者可以根据不同的业务逻辑和参数动态地生成 SQL 语句,从而提高了开发的效率和代码的质量。 ## 模糊查询 like 语句该怎么写?(了解) 1. ’%${question}%’ 可能引起SQL注入,不推荐 2. “%”#{question}“%” 注意:因为#{…}解析成sql语句时候,会在变量外侧自动加单引号’ ',所以这里 % 需要使用双引号" ",不能使用单引号 ’ ',不然会查不到任何结果。 3. CONCAT(’%’,#{question},’%’) 使用CONCAT()函数,推荐 4. 使用bind标签 ~~~xml ~~~ ## MyBatis实现一对一有几种方式?具体怎么操作的?(掌握) ### 联合查询(嵌套结果) 联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; > 设计表:会员与商家一对一 > > * 会员表 > * 商家信息表 #### mapper接口 ~~~java public interface MemberMapper { /** * 根据id查询会员及商家信息 * @param id * @return */ Member queryById(int id); } ~~~ #### 映射文件 ~~~xml ~~~ ### 嵌套查询 嵌套查询是先查一个表,根据这个表里面的结果的id 做为参数(前提该id在另一个表中是外键关系 ),去再另外一个表里面查询数据,也是通过association配置,但另外一个表的查询通过select属性配置。 #### 会员接口 ~~~java public interface MemberMapper { /** * 根据id查询会员及商家信息 * @param id * @return */ Member queryById(int id); } ~~~ #### 会员映射 ~~~xml ~~~ #### 商家接口 ~~~java public interface ShopMapper { /** * 根据会员id查询商家信息 * @param id * @return */ Shop queryByMemberId(int id); } ~~~ #### 商家映射 ~~~xml ~~~ ### 一对一映射对比 * sql语句: 嵌套结果sql复杂,是关联查询 ,而嵌套select是简单查询 * 结果映射(resultMap): 嵌套结果是将关联的对象属性直接映射,而嵌套select,指明要查询的sql的key ,传递参数 * sql数量: 嵌套结果 一条sql , 嵌套select是1+N条sql语句 ## MyBatis实现一对多有几种方式,怎么操作的? > 订单: > > * 订单表 订单金额 订单状态(1待付款、2待发货、3待收货、待评价、已完成) 下单人 收货人相关信息 > * 订单明细表 购买的商品、数量、价格 ... ### 联合查询(嵌套结果) 联合查询是几个表联合查询,只查询一次,通过在resultMap里面的collection节点配置一对多的类就可以完成; #### mapper接口 ~~~java public interface OrderMapper { /** * 根据订单id查询订单详情 * @param id * @return */ Order queryById(int id); } ~~~ #### 映射文件 ##### 方式一 手动映射 ~~~xml ~~~ ##### 方式二 自动映射 ~~~xml ~~~ ##### 方式 三 外部引用 ~~~xml ~~~ ### 嵌套查询 嵌套查询是先查一个表,根据这个表里面的 结果的外键id,去再另外一个表里面查询数据,也是通过配置collection,但另外一个表的查询通过select节点配置。 #### 订单Mapper ~~~java public interface OrderMapper { /** * 根据订单id查询订单详情 * @param id * @return */ Order queryById(int id); } ~~~ #### 订单映射 ~~~xml ~~~ #### 订单明细mapper ~~~java public interface OrderDetailMapper { /** * 根据订单id查询订单明细 * @param id * @return */ List queryByOid(int id); } ~~~ #### 订单明细映射 ~~~xml ~~~ ## MyBatis的解析和运行原理(了解) ### MyBatis编程步骤是什么样的? 1. 创建SqlSessionFactory 2. 通过SqlSessionFactory创建SqlSession 3. 通过sqlsession执行数据库操作 4. 调用session.commit()提交事务 5. 调用session.close()关闭会话 ### 请说说MyBatis的工作原理 在学习 MyBatis 程序之前,需要了解一下 MyBatis 工作原理,以便于理解程序。MyBatis 的工作原理如下图 ![在这里插入图片描述](assets/d61d2562d8114d34bcec69616b8430b7.png) 1. 读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。 2. 加载映射文件:映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。 3. 构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。 4. 创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。 5. Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。 6. MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。 7. 输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。 8. 输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。 ### MyBatis的功能架构是怎样的 ![在这里插入图片描述](assets/ea0991de70224a8ab1650485ee3afc5c.png) 我们把Mybatis的功能架构分为三层: * API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。 * 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。 * 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。 ### MyBatis的框架架构设计是怎么样的 ![在这里插入图片描述](assets/9ca0f4cd51a24be583065bbf31504a4f.png) 这张图从上往下看。MyBatis的初始化,会从mybatis-config.xml配置文件,解析构造成Configuration这个类,就是图中的红框。 * 加载配置:配置来源于两个地方,一处是配置文件,一处是Java代码的注解,将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。 * SQL解析:当API接口层接收到调用请求时,会接收到传入SQL的ID和传入对象(可以是Map、JavaBean或者基本数据类型),Mybatis会根据SQL的ID找到对应的MappedStatement,然后根据传入参数对象对MappedStatement进行解析,解析后可以得到最终要执行的SQL语句和参数。 * SQL执行:将最终得到的SQL和参数拿到数据库进行执行,得到操作数据库的结果。 * 结果映射:将操作数据库的结果按照映射的配置进行转换,可以转换成HashMap、JavaBean或者基本数据类型,并将最终结果返回。 ### 为什么需要预编译 #### 定义 SQL 预编译指的是数据库驱动在发送 SQL 语句和参数给 DBMS 之前对 SQL 语句进行编译,这样 DBMS 执行 SQL 时,就不需要重新编译。 #### 为什么需要预编译 JDBC 中使用对象 PreparedStatement 来抽象预编译语句,使用预编译。预编译阶段可以优化 SQL 的执行。预编译之后的 SQL 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的SQL,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。同时预编译语句对象可以重复利用。把一个 SQL 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个SQL,可以直接使用这个缓存的 PreparedState 对象。Mybatis默认情况下,将对所有的 SQL 进行预编译。 ### Mybatis都有哪些Executor执行器?它们之间的区别是什么? Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。 * SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。 * ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。 * BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。 > 作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。 ### Mybatis中如何指定使用哪一种Executor执行器? 在Mybatis配置文件中,在设置(settings)可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数,如SqlSession openSession(ExecutorType execType)。 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 ### Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。 当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。 # SpringBoot ## 什么是 Spring Boot? Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重xml的配置,提供了各种启动器,在运行过程中自动配置, 开发者能快速上手。 > Spring Boot是由Pivotal团队提供的全新[框架](https://baike.baidu.com/item/框架/1212667),其设计目的是用来[简化](https://baike.baidu.com/item/简化/3374416)新[Spring](https://baike.baidu.com/item/Spring/85061)应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。 ## 重要策略(掌握) SpringBoot框架中还有两个非常重要的策略:开箱即用和约定优于配置 ### **开箱即用** ​ 开箱即用,Outofbox,是指在开发过程中,通过在MAVEN项目的pom文件中添加相关依赖包,然后使用对应注解来代替繁琐的XML配置文件以管理对象的生命周期。这个特点使得开发人员摆脱了复杂的配置工作以及依赖的管理工作,更加专注于业务逻辑。 > 扩展: 为什么能够实现开箱既用? > > 1. 自动配置原理 > 2. 介绍SPI服务发现机制 ### **约定优于配置** ​ 约定优于配置(convention over configuration),也称作按约定编程,是一种软件[设计范式](https://baike.baidu.com/item/设计范式),旨在减少软件开发人员需要的配置。 > 本质上是对系统、类库或框架中一些东西(如配置信息)假定一个大众化合理的默认值(缺省值)。 > > 例如在模型中存在一个名为User的类,那么对应到数据库会存在一个名为user的表,只有在偏离这个约定时才需要做相关的配置(例如你想将表名命名为t_user等非user时才需要写关于这个名字的配置)。 > > 简单来说就是假如你所期待的配置与约定的配置一致,那么就可以不做任何配置,约定不符合期待时时才需要对约定进行替换配置。 ## SpringBoot的特点(了解) * 为所有 Spring 开发提供从根本上更快且可广泛访问的入门体验 * 可以创建独立的Spring应用程序,并且基于其Maven或Gradle插件,可以创建可执行的jar包和war包 * 开箱即用,提供自动配置的“starter”项目对象模型(POMS)以简化Maven配置 * 内嵌Tomcat或Jetty等Servlet容器 * 尽可能自动配置Spring容器 * 提供一系列大型项目通用的非功能特性,(例如嵌入式服务器、安全性、指标、健康检查和外部化配置) * 绝对没有代码生成,不需要XML配置。 ## Spring Boot 有哪些优点?(了解) Spring Boot 的优点有: 1、减少开发,测试时间和努力。 2、使用 JavaConfig 有助于避免使用 XML。 3、避免大量的 Maven 导入和各种版本冲突。 4、通过提供默认值快速开始开发。 5、没有单独的 Web 服务器需要。这意味着你不再需要启动 Tomcat,Glassfish或其他任何东西。 6、需要更少的配置 因为没有 web.xml 文件。只需添加用@ Configuration 注释的类,然后添加用@Bean 注释的方法,Spring 将自动加载对象并像以前一样对其进行管理。您甚至可以将@Autowired 添加到 bean 方法中,以使 Spring 自动装入需要的依赖关系中。 7、基于环境的配置 使用这些属性,您可以将您正在使用的环境传递到应用程序:-Dspring.profiles.active = {enviornment}。在加载主应用程序属性文件后,Spring 将在(application{environment} .properties)中加载后续的应用程序属性文件。 ## SpringBoot的核心注解(掌握) 启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解: * @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 * @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能 * @ComponentScan:Spring组件扫描。 ## SpringBoot自动配置原理 (高频) > 重点: > > 1. 核心注解SpringBootApplication > > 2. 应用启动扫描 自动配置类 (**SPI 服务发现机制** 重点说一下) > > 3. 条件注解 根据条件注解 符合条件的自动配置类生效,会向容器注入相关的Bean > > 4. 属性的批量绑定 在自动配置类中,会启用属性的批量绑定,通常是XXXProperties.java文件, > > 提供了可以配置的属性(一般都 有默认值 ) * Spring Boot自动配置的核心注解是@SpringBootApplication,该注解是spring boot的启动类注解,它是一个复合注解 > 注解包含: > > * @SpringBootConfiguration 该注解上有一个 @Configuration注解,表示这个spring boot启动类是一个配置类,最终要被注入到spring容器中 > * @EnableAutoConfiguration 表示开启自动配置,它也是一个复合注解,里面包含: > 1. @AutoConfigurationPackage 该注解上有一个@Import(AutoConfigurationPackages.Registrar.class)注解,其中 Registrar 类的作用是将启动类所在包下的所有子包的组件扫描注入到spring容器中。 > 2. @Import(AutoConfigurationImportSelector.class) 其中AutoConfigurationImportSelector类中有一个getCandidateConfigurations()方法,该方法通过SpringFactoriesLoader.loadFactoryNames()方法查找位于META-INF/spring 文件夹下的所有自动配置类,并加载这些类。 > * @ComponentScan 组件扫描 * @EnableAutoConfiguration注解开启了自动配置功能。它会扫描所有jar包类路径下 所有的自动配置类 > * 2.7之后 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件 > * 2.7之前是 > * META/INF/spring.factories * 自动配置生效 前面加载的所有自动配置类并不是都生效的,每一个xxxAutoConfiguration自动配置类都是在某些特定的条件之下才会生效。这些条件限制是通过@ConditionOnxxx注解实现的。 > 总结: > > * xxxxAutoConfigurartion:自动配置类;给容器中添加组件; > > * xxxxProperties:封装配置文件中相关属性; > **参考描述**: > > Spring Boot 的自动配置原理是其 “约定大于配置” 理念的核心实现,通过一套智能的配置加载机制,让开发者无需手动编写大量样板代码即可快速启动应用。以下从核心机制、关键组件、执行流程等方面详细解析: > > ### **一、自动配置的核心概念** > > Spring Boot 自动配置的本质是:**基于类路径中的依赖和应用配置,动态判断并加载合适的配置类**,避免开发者手动配置大量 Bean。 > > ### **二、关键组件与机制** > > #### 1. **@EnableAutoConfiguration 注解** > > - **作用**:开启自动配置功能,是自动配置的入口。 > - **原理**:该注解通过 `@Import(AutoConfigurationImportSelector.class)` 导入选择器,触发自动配置类的加载。 > > #### 2. **AutoConfigurationImportSelector 类** > > - **核心逻辑**:负责扫描并筛选需要导入的自动配置类。 > - 关键方法 > - `getCandidateConfigurations()`:通过 `SpringFactoriesLoader` 从类路径的 `META-INF/spring.factories` 中获取所有自动配置类。 > - `filter()`:根据条件注解(如 `@Conditional`)过滤不符合条件的配置类。 > > #### 3. **SpringFactoriesLoader 类** > > - **作用**:加载 `spring.factories` 文件中的配置项。 > - 文件位置 > - Spring Boot 核心依赖的 `META-INF/spring.factories` 中定义了大量自动配置类(如 `DataSourceAutoConfiguration`)。 > - 第三方依赖也可通过此文件添加自定义自动配置类。 > > ### **三、自动配置执行流程** > > #### 1. **应用启动时的触发** > > 1. 当应用启动并扫描到 `@SpringBootApplication`(内含 `@EnableAutoConfiguration`)时,自动配置流程启动。 > 2. `AutoConfigurationImportSelector` 被触发,开始加载候选配置类。 > > #### 2. **配置类的加载与筛选** > > - 加载阶段 > - 从所有依赖的 `META-INF/spring.factories` 中读取 `EnableAutoConfiguration` 对应的配置类列表(如 hundreds of configurations)。 > - 筛选阶段 > - 通过 `@Conditional` 系列注解(如 `@ConditionalOnClass`、`@ConditionalOnProperty`)判断配置类是否满足条件。 > - 例如:`DataSourceAutoConfiguration` 会检查类路径中是否存在数据库连接相关类(如 `DataSource`),以及是否有对应的配置属性。 > > #### 3. **配置类的生效与 Bean 注册** > > - 符合条件的配置类被加载后,会通过 `@Configuration` 注解成为配置类,并通过 `@Bean` 注册 Bean。 > - 配置类会结合 `application.properties/yaml` 中的用户配置(如 `spring.datasource.url`)进行参数绑定。 > > ### **四、条件注解(@Conditional)的作用** > > 条件注解用于控制配置类或 Bean 的加载条件,常见类型如下: > > | 注解名称 | 作用描述 | > | --------------------------------- | ------------------------------------------------------------ | > | `@ConditionalOnClass` | 当类路径中存在指定类时生效 | > | `@ConditionalOnMissingClass` | 当类路径中不存在指定类时生效 | > | `@ConditionalOnProperty` | 当指定属性存在且满足条件时生效(如 `spring.datasource.enabled=true`) | > | `@ConditionalOnWebApplication` | 当应用为 Web 应用时生效(检测是否存在 `Servlet` 相关类) | > | `@ConditionalOnNotWebApplication` | 当应用为非 Web 应用时生效 | > | `@ConditionalOnResource` | 当类路径中存在指定资源(如文件)时生效 | > > ### **五、配置的优先级与覆盖机制** > > 1. **用户配置优先**:`application.properties/yaml` 中的配置会覆盖自动配置的默认值。 > 2. **手动配置优先**:用户自定义的 `@Configuration` 类或 Bean 会覆盖自动配置的 Bean。 > 3. 排除自动配置 > - 通过 `@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})` 排除特定配置类。 > - 在 `application.properties` 中配置:`spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration`。 > > ### **六、总结自动配置的核心流程** > > 1. **启动触发**:`@EnableAutoConfiguration` 激活自动配置机制。 > 2. **配置类加载**:通过 `SpringFactoriesLoader` 读取所有候选配置类。 > 3. **条件筛选**:使用 `@Conditional` 系列注解过滤不符合条件的配置类。 > 4. **Bean 注册**:生效的配置类注册 Bean,并结合用户配置完成初始化。 > > 通过这套机制,Spring Boot 实现了 “开箱即用” 的开发体验,让开发者只需关注业务逻辑,而非基础配置。 ## SpringBoot配置多套环境(了解) 可以定义多个配置文件,比如开发,测试,上线。 我们可以在SpringBoot中定义多个application.properties/yml。 我一般都用-名字做区别,比如: * application-dev.properties/yml * application-test.properties/yml * application-prod.properties/yml 之后我们需要在默认的配置文件里面声明一下激活哪些配置文件。 ```properties spring.profiles.active=test ``` 使用java -jar 方式启动的时候也可以添加参数指定配置文件启动 ```shell java -jar mm.jar --spring.profiles.active=dev ``` ## 运行 SpringBoot 有哪几种方式(了解) * 打包用命令或者放到容器中运行 > 1. 打成jar包,使用java -jar xxx.jar运行 > 2. 打成war包,放到tomcat里面运行 > 3. 用jar包生成docker镜像 * 直接用maven插件运行 maven spring-boot:run * 直接执行main方法运行 ## Java SPI服务发现机制(理解) ### Java SPI 服务发现机制详解 Java SPI (Service Provider Interface) 是 JDK 内置的一种服务发现机制,它允许第三方服务提供者通过简单的配置文件来扩展应用程序的功能,而无需修改应用程序的代码。这种机制实现了服务接口与服务实现的解耦,遵循 "开闭原则",是一种强大的插件化架构设计模式。 ### SPI 的核心组件 Java SPI 机制主要由以下几个核心组件构成: 1. **服务接口 (Service Interface)**:定义服务的抽象行为 2. **服务提供者 (Service Provider)**:实现服务接口的具体类 3. **服务配置文件**:位于 `META-INF/services/` 目录下,文件名是接口的全限定名,内容是实现类的全限定名 4. **ServiceLoader**:JDK 提供的服务加载工具类,负责从配置文件中加载服务实现 ### SPI 的工作流程 1. **定义服务接口**:开发者定义一个服务接口 2. **实现服务**:第三方提供服务接口的具体实现 3. **配置服务**:在实现方的 JAR 包中,创建 `META-INF/services/接口全限定名` 文件,内容为实现类的全限定名 4. **加载服务**:应用程序通过 `ServiceLoader` 加载并使用服务实现 ### ServiceLoader 的工作原理 `ServiceLoader` 是 SPI 机制的核心类,它的工作原理如下: 1. **获取配置文件**:根据接口的全限定名,在 `META-INF/services/` 目录下查找对应的配置文件 2. **读取实现类名**:从配置文件中读取所有实现类的全限定名 3. **加载实现类**:使用类加载器加载这些实现类 4. **实例化对象**:创建实现类的实例并返回 ### SPI 的优缺点 **优点**: - 实现了服务接口与服务实现的解耦 - 支持动态加载服务实现,无需修改代码 - 便于第三方扩展,增强了系统的可扩展性 - 实现简单,使用方便 **缺点**: - 无法按需加载,会一次性加载所有实现类 - 无法对加载的实现类进行排序 - 加载机制效率较低,不适合性能敏感的场景 - 配置文件中的实现类必须有无参构造函数 ### 实际应用场景 SPI 机制在许多框架和库中被广泛应用: 1. **JDBC 驱动**:Java 定义了 JDBC 接口,各数据库厂商提供具体实现 2. **日志框架**:SLF4J 定义接口,Logback、Log4j 等提供实现 3. **Spring 框架**:使用 SPI 机制加载各种扩展点 4. **Dubbo 框架**:大量使用 SPI 机制实现可扩展的组件 5. **Java 编译器**:JDK 中的 javax.annotation.processing.Processor 接口通过 SPI 加载注解处理器 ### 与其他机制的比较 1. **与工厂模式的比较**: - 工厂模式需要显式指定实现类 - SPI 通过配置文件隐式指定实现类,更加灵活 2. **与依赖注入的比较**: - 依赖注入通常由框架管理依赖关系 - SPI 是一种轻量级的服务发现机制,不需要依赖特定框架 3. **与 OSGi 的比较**: - OSGi 提供更强大的模块化和生命周期管理 - SPI 更简单,适合简单的服务发现场景 ### 总结 Java SPI 机制是一种简单而强大的服务发现模式,它通过配置文件实现了服务接口与服务实现的解耦,使得系统更易于扩展。虽然它有一些局限性,但在许多场景下仍然是一个很好的选择,特别是需要支持插件化架构的应用程序。 在实际开发中,我们可以根据具体需求选择标准的 SPI 机制或自定义 SPI 加载器,以满足更复杂的服务发现需求。 # Redis > Redis用过吗? > > 1. redis 干什么的? 缓存 > 2. 如何做缓存?如果与数据库结合使用? > 3. 如果使用了缓存,要考虑可能出现的缓存问题及如何解决? > 4. 如何保证缓存与数据库数据一致? > 5. 持久化机制 ## redis是单进程单线程吗(要能分清 了解) Redis 采用单进程单线程模型处理客户端的命令请求。这里的单线程指的是 Redis 服务器使用一个线程来处理所有网络 IO 和命令执行,而不是说 Redis 整个进程只有一个线程。实际上,Redis 在某些情况下会使用后台线程处理一些耗时操作,如持久化和异步删除等。 ### 核心特点: 1. **单线程处理命令**:Redis 使用一个线程处理所有客户端命令,这避免了多线程环境下的锁竞争问题,简化了实现,提高了处理效率。 2. **IO 多路复用**:Redis 借助 epoll、kqueue 等 IO 多路复用技术,实现了高效的并发处理,能够同时监听多个客户端连接。 3. **内存操作高效**:由于大部分操作在内存中完成,单线程也可以达到很高的性能(通常每秒可处理数万到数十万次请求)。 ### 多线程的情况: - **后台线程**:Redis 在 4.0 版本后引入了后台线程,用于处理一些耗时操作,如 `UNLINK` 命令(异步删除大键)、AOF 重写等。 - **多线程 IO**:Redis 6.0 开始支持多线程 IO,主要用于客户端数据的读写,而命令执行仍然是单线程的。 ### 为什么 Redis 选择单线程? - **避免锁开销**:单线程避免了多线程同步带来的锁竞争开销。 - **简化设计**:单线程模型使代码更易维护和调试。 - **内存操作快**:Redis 的性能瓶颈通常不是 CPU,而是内存和网络带宽。 ### 适用场景: - **高速缓存**:适合对读写速度要求极高的缓存场景。 - **计数器**:原子操作保证计数的准确性。 - **排行榜**:利用有序集合实现高性能排行榜。 ### 注意事项: - **避免耗时操作**:单线程环境下,长时间运行的命令(如 `KEYS *`)会阻塞整个服务器。 - **CPU 绑定操作**:不适合处理大量 CPU 密集型任务,更适合内存密集型应用。 如果你需要更高的并发性能,可以通过 Redis Cluster 或 Sentinel 实现分布式部署。 ## Redis 如何保证原子性?(了解) 答案很简单,因为 Redis 是单线程的,所以 Redis 提供的 API 也是原子操作。 但我们业务中常常有先 get 后 set 的业务常见,在并发下会导致数据不一致的情况。 如何解决 1)使用 incr、decr、setnx 等原子操作; 2)客户端加锁; 3)使用 Lua 脚本实现 CAS 操作。 ## Redis 最适合的场景?(掌握 高频) ### 会话缓存(Session Cache) 最常用的一种使用 Redis 的情景是会话缓存(session cache)。用 Redis 缓存会话比其他存储(如 Memcached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗? 幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为人知的商业平台Magento 也提供 Redis 的插件。 ### 全页缓存(FPC) 除基本的会话 token 之外,Redis 还提供很简便的 FPC 平台。回到一致性问题,即使重启了 Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。 再次以 Magento 为例,Magento提供一个插件来使用 Redis 作为全页缓存后端。 此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。 ### 队列 Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得 Redis能作为一个很好的消息队列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop 操作。 如果你快速的在 Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用 Redis 创建非常好的后端工具,以满足各种队列需求。例如,Celery 有一个后台就是使用 Redis 作为 broker,你可以从这里去查看。 ### 排行榜/计数器 Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的 10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可: 当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行: ZRANGE user_scores 0 10 WITHSCORES Agora Games 就是一个很好的例子,用 Ruby 实现的,它的排行榜就是使用 Redis 来存储数据的,你可以在这里看到。 ### 发布/订阅 最后(但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建立聊天系统! ## Redis各数据类型有哪些应用场景?(掌握) Redis 在互联网产品中使用的场景实在是太多太多,这里分别对 Redis 几种数据类型做了整理: 1. String:缓存、限流、分布式锁、计数器、分布式 Session 等。 2. Hash:用户信息、用户主页访问量、组合查询等。 3. List:简单队列、关注列表时间轴。 4. Set:赞、踩、标签等。 5. ZSet:排行榜、好友关系链表。 ## Redis 是单进程单线程的(掌握) Redis 在处理客户端请求和数据操作方面,通常被描述为单进程单线程的。然而,这个描述并不完全准确,因为Redis 在某些操作和模块中实际上使用了多线程或多进程。以下是对 Redis 进程和线程模型的详细解析: ### 单进程单线程的核心模型 1. **网络请求处理**:Redis 服务器使用一个主线程来处理所有客户端的网络请求。这个主线程负责接收客户端的连接、读取请求数据、处理请求,并将结果返回给客户端。这种单线程模型避免了多线程或多进程在处理网络请求时可能出现的竞态条件和同步问题。 2. **数据操作**:在 Redis 中,数据的读取、写入、删除等操作也是由这个主线程来完成的。由于 Redis 的数据存储在内存中,并且操作通常是原子性的,因此这种单线程模型能够高效地处理这些操作。 ### 多线程和多进程的使用 尽管 Redis 的核心网络请求和数据操作是单线程的,但在某些情况下,Redis 也会使用多线程或多进程来优化性能或实现特定的功能。 1. **持久化操作**:Redis 支持将数据持久化到磁盘上,以确保在系统重启后数据不会丢失。在持久化过程中,Redis 可能会使用多线程或多进程来优化性能。例如,在保存 RDB 快照时,Redis 会自动 fork 一个子进程来处理快照文件的生成,而主进程仍然可以对外提供服务。 2. **I/O 多线程**:从 Redis 6.0 版本开始,Redis 引入了 I/O 多线程的功能。这意味着 Redis 可以使用多个线程来处理网络 I/O 操作,如读取客户端发送的数据和将响应数据写回给客户端。然而,需要注意的是,虽然 I/O 操作是多线程的,但数据的实际处理仍然是由主线程来完成的。 ### 总结 综上所述,Redis 在处理客户端请求和数据操作方面是单进程单线程的,这种模型有助于简化并发控制和提高性能。然而,在持久化操作和 I/O 操作方面,Redis 可能会使用多线程或多进程来优化性能。因此,说 Redis 是完全的单进程单线程并不准确,但它确实在核心功能上采用了这种模型。 在实际应用中,是否需要启用 Redis 的多线程功能取决于具体的使用场景和性能需求。对于大多数应用场景来说,Redis 的单线程模型已经足够高效,并且能够提供出色的性能。然而,在高并发场景下,启用 I/O 多线程功能可能会进一步提高 Redis 的处理能力和吞吐量。 ## 说说Redis的基本数据结构类型(掌握) 大多数小伙伴都知道,Redis有以下这五种基本类型: - String(字符串) - Hash(哈希) - List(列表) - Set(集合) - zset(有序集合) 它还有三种特殊的数据结构类型 - Geospatial Redis3.2推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作。 - Hyperloglog 用来做基数统计算法的数据结构,如统计网站的UV。 - Bitmap 用一个比特位来映射某个元素的状态,在Redis中,它的底层是基于字符串类型实现的,可以把bitmaps成作一个以比特位为单位的数组 ## Redis 的五种基本数据类型(参考) ![img](assets/v2-42ecf99d863aadb00f914a23be91e564_720w.webp) ### String(字符串) - 简介:String是Redis最基础的数据结构类型,它是二进制安全的,可以存储图片或者序列化的对象,值最大存储为512M - 简单使用举例: `set key value`、`get key`等 - 应用场景:共享session、分布式锁,计数器、限流 缓存 。 - 内部编码有3种,`int(8字节长整型)/embstr(小于等于39字节字符串)/raw(大于39个字节字符串)` C语言的字符串是`char[]`实现的,而Redis使用**SDS(simple dynamic string)** 封装,sds源码如下: ~~~c struct sdshdr{ unsigned int len; // 标记buf的长度 unsigned int free; //标记buf中未使用的元素个数 char buf[]; // 存放元素的坑 } ~~~ SDS 结构图如下: ![img](assets/v2-3423339183d978aed32dca64447d728d_720w.webp) Redis为什么选择**SDS**结构,而C语言原生的`char[]`不香吗? > 举例其中一点,SDS中,O(1)时间复杂度,就可以获取字符串长度;而C 字符串,需要遍历整个字符串,时间复杂度为O(n) ### Hash(哈希) - 简介:在Redis中,哈希类型是指v(值)本身又是一个键值对(k-v)结构 - 简单使用举例:`hset key field value` 、`hget key field` - 内部编码:`ziplist(压缩列表)` 、`hashtable(哈希表)` - 应用场景:缓存用户信息等。 - **注意点**:如果开发使用hgetall,哈希元素比较多的话,可能导致Redis阻塞,可以使用hscan。而如果只是获取部分field,建议使用hmget。 字符串和哈希类型对比如下图: ![img](assets/v2-35a03c770a54bf4efb6c66292983d428_720w.png) ### List(列表) - 简介:列表(list)类型是用来存储多个有序的字符串,一个列表最多可以存储2^32-1个元素。 - 简单实用举例:`lpush key value [value ...]` 、`lrange key start end` - 内部编码:ziplist(压缩列表)、linkedlist(链表) - 应用场景:消息队列,文章列表, 一图看懂list类型的插入与弹出: ![img](assets/v2-a3eb7e75fa982bb3c94ac8f0bc08e594_720w.png) list应用场景参考以下: - lpush+lpop=Stack(栈) - lpush+rpop=Queue(队列) - lpsh+ltrim=Capped Collection(有限集合) - lpush+brpop=Message Queue(消息队列) ### Set(集合) ![img](assets/v2-bf0ba39f8ed2b1e0c52063dd4c6bf1e8_720w.png) - 简介:集合(set)类型也是用来保存多个的字符串元素,但是不允许重复元素 - 简单使用举例:`sadd key element [element ...]`、`smembers key` - 内部编码:`intset(整数集合)`、`hashtable(哈希表)` - **注意点**:smembers和lrange、hgetall都属于比较重的命令,如果元素过多存在阻塞Redis的可能性,可以使用sscan来完成。 - 应用场景:用户标签,生成随机数抽奖、社交需求。 ### 有序集合(zset) - 简介:已排序的字符串集合,同时元素不能重复 - 简单格式举例:`zadd key score member [score member ...]`,`zrank key member` - 底层内部编码:`ziplist(压缩列表)`、`skiplist(跳跃表)` - 应用场景:排行榜,社交需求(如用户点赞)。 ## Redis 的持久化机制是什么?各自的优缺点?开发中用哪种方案?(高频) Redis提供两种持久化机制 RDB 和 AOF 机制: ### RDB ( Redis DataBase)持久化方式: 是指用数据集快照的方式半持久化模式)记录 redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。 实现方式 : 1. 手动触发 * save 阻塞的 * bgsave 异步非阻塞的 2. 自动触发 优点: 1. 只有一个文件 dump.rdb,方便持久化。 2. 容灾性好,一个文件可以保存到安全的磁盘。 3. 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis的高性能) 4. 相对于数据集大时,比 AOF 的启动效率更高。 缺点: 数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候 ### AOF (Append-only file) 持久化方式: 是指所有的命令行记录以 redis 命令请求协议的格式完全持久化存储)保存为 aof 文件。 优点: 1. 数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。 2. 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof工具解决数据一致性问题。 3. AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall)) 缺点: 1. AOF 文件比 RDB 文件大,且恢复速度慢。 2. 数据集大的时候,比 rdb 启动效率低。 ## redis 过期键的删除策略?(掌握) 删除达到过期时间的key。 1. 定时删除 对于每一个设置了过期时间的key都会创建一个定时器,一旦到达过期时间就立即删除。该策略可以立即清除过期的数据,对内存较友好,但是缺点是占用了大量的CPU资源去处理过期的数据,会影响Redis的吞吐量和响应时间。 2. 惰性删除 当访问一个key时,才判断该key是否过期,过期则删除。该策略能最大限度地节省CPU资源,但是对内存却十分不友好。有一种极端的情况是可能出现大量的过期key没有被再次访问,因此不会被清除,导致占用了大量的内存。 3. 定期删除 每隔一段时间,扫描Redis中过期key字典,并清除部分过期的key。该策略是前两者的一个折中方案,还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时,在不同情况下使得CPU和内存资源达到最优的平衡效果。 在Redis中,同时使用了定期删除和惰性删除。不过Redis定期删除采用的是随机抽取的方式删除部分Key,因此不能保证过期key 100%的删除。 Redis结合了定期删除和惰性删除,基本上能很好的处理过期数据的清理,但是实际上还是有点问题的,如果过期key较多,定期删除漏掉了一部分,而且也没有及时去查,即没有走惰性删除,那么就会有大量的过期key堆积在内存中,导致redis内存耗尽,当内存耗尽之后,有新的key到来会发生什么事呢?是直接抛弃还是其他措施呢?有什么办法可以接受更多的key? ## 假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如何将它们全部找出来? 查找有两种方式: * keys指令 使用 keys 指令可以扫出指定模式的 key 列表。 有一个问题,10w数据多的情况下,会阻塞,这个时候其它指令无法执行 * scan 命令 遍历 异常 不阻塞 且 模式匹配 不是一次返回所有 ### 如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问题? 这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。 ## MySQL 里有 2000w 数据,redis 中只存 20w 的数据,如何保证 redis 中的数据都是热点数据? Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。 Redis 提供 8 种数据淘汰策略: LRU全称Least recently used,意思为淘汰掉最久未使用(即最老)的一条数据; LFU全称Least-frequently used,意思为淘汰掉过去被访问次数最少的一条数据 > 过期策略分两大类: > > * 针对设置了过期时间的key volatile > * 针对所有的key allkeys > > 策略: > > * lru 最近最少使用 > * randome > * lfu > > 特殊: > > * 默认: 没有内存,写入失败,读取正常 > * 有过期时间的 volatile-ttl 马上到期的删除 | | | | | -------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | **淘汰策略名称** | **策略含义** | **人话** | | noeviction | 默认策略,不淘汰数据;大部分写命令都将返回错误(DEL等少数除外) | 不删除任意数据(但redis还会根据引用计数器进行释放),这时如果内存不够时,会直接返回错误。 | | volatile-lru | 从设置了过期时间的数据中根据 LRU 算法挑选数据淘汰(**只针对设置过期的keys**) | 从设置了过期时间的数据集中,选择最近最久未使用的数据释放 | | allkeys-lru***这个是最常用的\*** | 从所有数据中根据 LRU 算法挑选数据淘汰(**所有keys**) | 从数据集中(包括设置过期时间以及未设置过期时间的数据集中),选择最近最久未使用的数据释放 | | allkeys-random | 从所有数据中随机挑选数据淘汰 | 从设置了过期时间的数据集中,随机选择一个数据进行释放; | | volatile-random | 从设置了过期时间的数据中随机挑选数据淘汰 | 从设置了过期时间的数据集中,随机 | | volatile-ttl | 从设置了过期时间的数据中,挑选越早过期的数据进行删除 | 从设置了过期时间的数据集中,选择**马上**就要过期的数据进行释放操作 | | allkeys-lfu | 从所有数据中根据 LFU 算法挑选数据淘汰(4.0及以上版本可用) | 淘汰掉过去被访问次数最少的一条数据 | | volatile-lfu | 从设置了过期时间的数据中根据 LFU 算法挑选数据淘汰(4.0及以上版本可用) | 淘汰掉过去被访问次数最少的一条数据 | ## Redis 和 Mysql 数据库数据如何保持一致性(高频) 1、背景介绍 一般情况下,Redis是用作应用程序和数据库之间读操作的缓存,主要目的是减少数据库IO,还可以提升数据的IO性能。如图所示,这是Redis加MySQL的整体架构设计。 ![img](assets/3685428c79db0c965b901696e8d1b675.jpeg) 当应用程序需要去读取某个数据的时候,首先会先尝试去Redis中加载,如果命中就直接返回。如果没有命中,就从数据库查询,查询到数据后再把这个数据缓存到Redis里面。 在这样一个架构中,会出现一个问题,就是一份数据,同时保存在数据库和Redis中,当数据发生变化的时候,需要同时更新Redis和MySQL,由于更新是有先后顺序的,这种两边写入的环境下,并不能像单纯数据库的操作一样,可以满足ACID特性。因此,就有可能出现一方更新失败,一方更新成功的情况,从而出现数据一致性问题。 ![img](assets/f69170aaa08abb429c4f2c45623fc651.jpeg) 2、解决思路 如果出现数据一致性问题,我们该如何解决呢?一般会想到以下两种解决思路。 要么先更新数据库,再更新缓存; 要么先删除缓存,再更新数据库。 如果是采用先更新数据库,再更新缓存的方案,也会有这样一个问题。假设缓存更新失败,就会导致数据库和Redis中的数据不一致。 ![img](assets/c22c563886faa0ba1f4d37adcf419b86.jpeg) 那如果是先删除缓存,再更新数据库,理想情况是应用下次访问Redis的时候,发现Redis里面的数据是空的,就从数据库加载保存到Redis里面,那么数据是一致的。但是在极端情况下,并不能保证删除Redis和更新数据库这两个操作的原子性,所以这个过程如果有其他线程来访问,还是会存在数据不一致问题。 ![img](assets/543733f2edf3b7d32c596f474eed9deb.jpeg) 所以,如果需要在极端情况下仍然保证Redis和MySQL的数据一致性,就只能采用最终一致性方案。 ![img](assets/d7d8bd7a296d8148be6c7b2e54f7ca3e.jpeg) 如图所示,比如基于RocketMQ的可靠性消息通信,来实现最终一致性。 ![img](assets/c223cd3f187f14d73833d1bdf31f8fbb.jpeg) 再比如,还可以直接通过Canal组件,来监控MySQL中Binlog的日志,把更新后的数据同步到Redis中。 因为这里是基于最终一致性来实现的,如果业务场景不能接受数据的短期不一致性,那就不能使用这个方案来做。 > 参考: > > ![image-20230724180705853](assets/image-20230724180705853.png) ## Redis缓存问题(高频) 在实际的业务场景中,Redis 一般和其他数据库搭配使用,用来减轻后端数据库的压力,比如和关系型数据库 MySQL 配合使用。 Redis 会把 MySQL 中经常被查询的数据缓存起来,比如热点数据,这样当用户来访问的时候,就不需要到 MySQL 中去查询了,而是直接获取 Redis 中的缓存数据,从而降低了后端数据库的读取压力。如果说用户查询的数据 Redis 没有,此时用户的查询请求就会转到 MySQL 数据库,当 MySQL 将数据返回给客户端时,同时会将数据缓存到 Redis 中,这样用户再次读取时,就可以直接从 Redis 中获取数据。流程图如下所示: ![image-20221011073315140](assets/image-20221011073315140.png) 在使用 Redis 作为缓存数据库的过程中,有时也会遇到一些棘手问题,比如常见缓存穿透、缓存击穿和缓存雪崩等问题,下面对这些问题做简单地说明,并且提供有效的解决方案。 ### 缓存穿透 缓存穿透是指当用户查询某个数据时,Redis 中不存在该数据,也就是缓存没有命中,此时查询请求就会转向持久层数据库 MySQL,结果发现 MySQL 中也不存在该数据,MySQL 只能返回一个空对象,代表此次查询失败。如果这种类请求非常多,或者用户利用这种请求进行恶意攻击,就会给 MySQL 数据库造成很大压力,甚至于崩溃,这种现象就叫缓存穿透。 为了避免缓存穿透问题,参考以下解决方案 **缓存空对象** 当 MySQL 返回空对象时, Redis 将该对象缓存起来,同时为其设置一个过期时间。当用户再次发起相同请求时,就会从缓存中拿到一个空对象,用户的请求被阻断在了缓存层,从而保护了后端数据库,但是这种做法也存在一些问题,虽然请求进不了 MSQL,但是这种策略会占用 Redis 的缓存空间。 ### 缓存击穿 缓存击穿是指用户查询的数据缓存中不存在,但是后端数据库却存在,这种现象出现原因是一般是由缓存中 key 过期导致的。比如一个热点数据 key,它无时无刻都在接受大量的并发访问,如果某一时刻这个 key 突然失效了,就致使大量的并发请求进入后端数据库,导致其压力瞬间增大。这种现象被称为缓存击穿。 缓存击穿有两种解决方法: #### 1) 改变过期时间 设置热点数据永不过期。 #### 2) 分布式锁 采用分布式锁的方法,重新设计缓存的使用方式,过程如下: - 上锁:当我们通过 key 去查询数据时,首先查询缓存,如果没有,就通过分布式锁进行加锁,第一个获取锁的进程进入后端数据库查询,并将查询结果缓到Redis 中。 - 解锁:当其他进程发现锁被某个进程占用时,就进入等待状态,直至解锁后,其余进程再依次访问被缓存的 key。 ### 缓存雪崩 缓存雪崩是指缓存中大批量的 key 同时过期,而此时数据访问量又非常大,从而导致后端数据库压力突然暴增,甚至会挂掉,这种现象被称为缓存雪崩。它和缓存击穿不同,缓存击穿是在并发量特别大时,某一个热点 key 突然过期,而缓存雪崩则是大量的 key 同时过期,因此它们根本不是一个量级。 #### 解决方案 缓存雪崩和缓存击穿有相似之处,所以也可以采用热点数据永不过期的方法,来减少大批量的 key 同时过期。再者就是为 key 设置随机过期时间,避免 key 集中过期。 ## 缓存预热(了解) 缓存预热是一种缓存优化技术,主要用于提升系统在高并发情况下的稳定性和性能。以下是对缓存预热的详细解释: ### 一、定义与目的 **定义**:缓存预热是指在系统启动之前或系统达到高峰期之前,通过预先将常用数据加载到缓存中,以提高缓存命中率和系统性能的过程。 **目的**: 1. **减少冷启动影响**:当系统重启或新启动时,缓存是空的,这被称为冷启动。冷启动可能导致首次请求处理缓慢,因为数据需要从慢速存储(如数据库)检索。通过缓存预热,可以避免这种情况。 2. **提高数据访问速度**:通过预先加载常用数据到缓存中,可以确保数据快速可用,从而加快数据访问速度。 3. **平滑流量峰值**:在流量高峰期之前预热缓存可以帮助系统更好地处理高流量,避免在流量激增时出现性能下降。 4. **保证数据的时效性**:定期预热可以保证缓存中的数据是最新的,特别是对于高度依赖于实时数据的系统。 5. **减少对后端系统的压力**:通过缓存预热,可以减少对数据库或其他后端服务的直接查询,从而减轻它们的负载。 ### 二、预热方法 1. **系统启动时加载**:在系统启动时,将常用的数据加载到缓存中,以便后续的访问可以直接从缓存中获取。 2. **定时任务加载**:设置定时任务,定期将常用的数据加载到缓存中,以保持缓存中数据的实时性和准确性。 3. **手动触发加载**:在系统达到高峰期之前,手动触发加载常用数据到缓存中,以提高缓存命中率和系统性能。 4. **用时加载**:在用户请求到来时,根据用户的访问模式和业务需求,动态地将数据加载到缓存中。 5. **缓存加载器**:一些缓存框架提供了缓存加载器的机制,可以在缓存中不存在数据时,自动调用加载器加载数据到缓存中。 ### 三、Redis缓存预热 在分布式缓存中,Redis是常用的缓存解决方案。针对Redis的预热,有以下几个工具和方法: 1. **RedisBloom**:RedisBloom是Redis的一个模块,提供了多个数据结构,包括布隆过滤器等。布隆过滤器可以用于Redis缓存预热,通过将预热数据添加到布隆过滤器中,可以快速判断一个键是否存在于缓存中。 2. **Redis Bulk loading**:这是Redis官方提供的一个基于Redis协议批量写入数据的工具,可以用于缓存预热。 3. **Redis Desktop Manager**:这是一个图形化的Redis客户端,可以用于管理Redis数据库和进行缓存预热。通过Redis Desktop Manager,可以轻松地将预热数据批量导入到Redis缓存中。 ### 四、应用场景与优势 **应用场景**: 1. **高并发系统**:在高并发系统中,缓存预热可以显著提高系统的性能和稳定性。 2. **热点数据场景**:如果系统中存在一些热点数据,即经常被访问的数据,那么缓存预热尤其有效。 3. **稳定的数据集**:缓存预热适用于那些数据集相对稳定的场景。 **优势**: 1. **提高系统性能**:通过减少响应时间,提高系统的性能和响应速度。 2. **减轻后端负载**:减少对后端系统的直接查询,降低后端系统的压力。 3. **改善用户体验**:用户可以获得更快的响应,提高用户体验。 ### 五、注意事项 1. **数据选择与更新**:在进行缓存预热时,需要合理选择预热的数据,并确保数据的实时性和准确性。同时,需要定期更新缓存中的数据,以避免数据过时。 2. **性能评估**:缓存预热可能会对系统性能产生一定影响,特别是在数据量较大或系统资源有限的情况下。因此,在进行缓存预热时,需要进行性能评估,并根据评估结果进行调整和优化。 3. **安全性考虑**:在进行缓存预热时,需要注意数据的安全性,避免敏感数据泄露或被非法访问。 综上所述,缓存预热是一种有效的缓存优化技术,可以显著提高系统的性能和稳定性。在实际应用中,需要根据具体场景和需求选择合适的预热方法和工具,并进行性能评估和优化。 ## Redis分布式锁(高频) 分布式锁并非是 Redis 独有,比如 MySQL 关系型数据库,以及 Zookeeper 分布式服务应用,它们都实现分布式锁,只不过 Redis 是基于缓存实现的。 Redis 分布式锁有很对应用场景,举个简单的例子,比如春运时,您需要在 12306 上抢购回家火车票,但 Redis 数据库中只剩一张票了,此时有多个用户来预订购买,那么这张票会被谁抢走呢?Redis 服务器又是如何处理这种情景的呢?在这个过程中就需要使用分布式锁。 Redis 分布式锁主要有以下特点: - 第一:**互斥性**是分布式锁的重要特点,在任意时刻,只有一个线程能够持有锁; - 第二:锁的**超时时间**,一个线程在持锁期间挂掉了而没主动释放锁,此时通过超时时间来保证该线程在超时后可以释放锁,这样其他线程才可以继续获取锁; - 第三:加锁和解锁必须是由同一个线程来设置;(**释放锁**) - 第四:Redis 是缓存型数据库,拥有很高的性能,因此加锁和释放锁开销较小,并且能够很轻易地实现分布式锁。 > 注意:一个线程代表一个客户端。 ### 分布式锁命令 分布式锁的本质其实就是要在 Redis 里面占一个“坑”,当别的进程也要来占时,发现已经有人蹲了,就只好放弃或者稍做等待。这个“坑”同一时刻只允许被一个客户端占据,也就是本着“先来先占”的原则。 #### 1) 常用命令 Redis 分布式锁常用命令如下所示: - SET key val NX [ex] :仅当key不存在时,设置一个 key 为 value 的字符串,返回1;若 key 存在,设置失败,返回 0; - Expire key timeout:为 key 设置一个超时时间,以 second 秒为单位,超过这个时间锁会自动释放,避免死锁; - DEL key:删除 key。 上述 SETNX 命令相当于占“坑”操作,EXPIRE 是为避免出现意外用来设置锁的过期时间,也就是说到了指定的过期时间,该客户端必须让出锁,让其他客户端去持有。 但还有一种情况,如果在 SETNX 和 EXPIRE 之间服务器进程突然挂掉,也就是还未设置过期时间,这样就会导致 EXPIRE 执行不了,因此还是会造成“死锁”的问题。为了避免这个问题,可能通过set命令同时执行 SETNX 和 EXPIRE 命令,从而解决了死锁的问题。 ~~~ SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL] ~~~ ## 慢查询日志的两个配置项(中) ***slowlog-log-slower-than*** - Redis 慢查询日志的时间阈值,单位微妙。 1) 值为正数,执行时间大于该值设置的微秒时才记录到慢日志中。默认 10000 微秒(0.01 秒)。 2) 值为负数,禁用慢查询日志。 3) 值为 0,所有命令都记录到慢日志中 * slowlog-max-len 1. 慢查询日志长度,最小值为零。默认 128 2. 当记录新命令并且当前慢日志已达到最大长度时,最旧的一条记录将被删除。 可以通过编辑 redis.conf 或者使用 CONFIG GET/SET 命令来进行配置 ~~~shell 127.0.0.1:6379> config get slowlog-log-slower-than 1) "slowlog-log-slower-than" 2) "10000" 127.0.0.1:6379> config get slowlog-max-len 1) "slowlog-max-len" 2) "128" ~~~ ~~~shell 127.0.0.1:6379> config set slowlog-log-slower-than 0 OK 127.0.0.1:6379> config set slowlog-max-len 10 OK ~~~ ### 3. 读取慢日志记录 慢查询日志是记录在内存中的,记录速度非常快。 可以使用 SLOWLOG GET N 命令来读取慢日志,查询最近的 N 条记录。 该命令默认请求条数为 10 ,即 SLOWLOG GET 等价于 SLOWLOG GET 10 参数为 -1 时会获取整个慢日志信息。 日志输出格式 ~~~shell 127.0.0.1:6379> slowlog get 2 1) 1) (integer) 13 2) (integer) 1629523068 3) (integer) 6 4) 1) "get" 2) "a" 5) "127.0.0.1:43942" 6) "lnrcoder" ~~~ 1)每条日志唯一标识符 2)命令执行时的时间戳 3)命令执行消耗的时间,单位微秒 4)执行的命令数组 5)客户端地址和端口 (仅 4.0 以上版本支持) 6)客户端名称 (仅 4.0 以上版本支持,默认名称为空,需要通过 client setname 命令进行设置) ### 4.查询慢日志记录长度 使用 **SLOWLOG LEN** 可以获取慢日志记录的长度。 ~~~shell 127.0.0.1:6379> slowlog len (integer) 2 ~~~ ### 5.重置慢日志 使用 **SLOWLOG RESET** 命令用来重置慢日志。使用该命令进行日志重置后,信息将永远丢失。 ~~~shell 127.0.0.1:6379> slowlog len (integer) 10 127.0.0.1:6379> slowlog reset OK 127.0.0.1:6379> slowlog len (integer) 0 ~~~ ## 布隆过滤器 应对**缓存穿透**问题,我们可以使用**布隆过滤器**。布隆过滤器是什么呢? 布隆过滤器是**一种占用空间很小的数据结构**,它由一个很长的**二进制数组**和**一组Hash映射函数组成**,它用于检索一个元素是否在一个集合中,空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。 **布隆过滤器原理是?**假设我们有个集合A,A中有n个元素。利用**k个哈希散列**函数,将A中的每个元素**映射**到一个长度为a位的数组B中的不同位置上,这些位置上的二进制数均设置为1。如果待检查的元素,经过这k个哈希散列函数的映射后,发现其k个位置上的二进制数**全部为1**,这个元素很可能属于集合A,反之,**一定不属于集合A**。 来看个简单例子吧,假设集合A有3个元素,分别为{**d1,d2,d3**}。有1个哈希函数,为**Hash1**。现在将A的每个元素映射到长度为16位数组B。 ![img](assets/v2-27d1b156e14904a16dcee613cabf8258_720w.png) 我们现在把d1映射过来,假设Hash1(d1)= 2,我们就把数组B中,下标为2的格子改成1,如下: ![img](assets/v2-4c5a3761cd0ac5d3281f28088f966f0b_720w.png) 我们现在把**d2**也映射过来,假设Hash1(d2)= 5,我们把数组B中,下标为5的格子也改成1,如下: ![img](assets/v2-5da7bf2fc42359a9403f50609dd8cf64_720w.png) 接着我们把**d3**也映射过来,假设Hash1(d3)也等于 2,它也是把下标为2的格子标1: ![img](assets/v2-8259602f8017b616bc3530e7081310f2_720w.webp) 因此,我们要确认一个元素dn是否在集合A里,我们只要算出Hash1(dn)得到的索引下标,只要是0,那就表示这个元素**不在集合A**,如果索引下标是1呢?那该元素**可能**是A中的某一个元素。因为你看,d1和d3得到的下标值,都可能是1,还可能是其他别的数映射的,布隆过滤器是存在这个**缺点**的:会存在**hash碰撞**导致的假阳性,判断存在误差。 如何**减少这种误差**呢? - 搞多几个哈希函数映射,降低哈希碰撞的概率 - 同时增加B数组的bit长度,可以增大hash函数生成的数据的范围,也可以降低哈希碰撞的概率 我们又增加一个Hash2**哈希映射**函数,假设Hash2(d1)=6,Hash2(d3)=8,它俩不就不冲突了嘛,如下: ![img](assets/v2-75dd6aaaa9131456a51f09adf27f39f4_720w.png) 即使存在误差,我们可以发现,布隆过滤器并**没有存放完整的数据**,它只是运用一系列哈希映射函数计算出位置,然后填充二进制向量。如果**数量很大的话**,布隆过滤器通过极少的错误率,换取了存储空间的极大节省,还是挺划算的。 目前布隆过滤器已经有相应实现的开源类库啦,如**Google的Guava类库**,Twitter的 Algebird 类库,信手拈来即可,或者基于Redis自带的Bitmaps自行实现设计也是可以的。 ## 布隆过滤器(了解) 布隆过滤器(Bloom Filter)是一种由Burton Howard Bloom在1970年提出的空间效率很高的随机数据结构,主要用于在大规模数据中判断一个元素是否存在。它由一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数)组成。 ### 基本原理 布隆过滤器的核心思想是利用多个哈希函数将集合中的元素映射到位数组中的多个位置,并将这些位置设为1。当需要查询某个元素是否存在于集合中时,同样使用这些哈希函数计算该元素对应的位数组位置,并检查这些位置是否都为1。如果所有位置都为1,则认为该元素可能存在于集合中(注意是可能存在,因为存在误判的可能性);如果任何一个位置不为1,则肯定不存在。 ### 优点 1. **空间效率高**:布隆过滤器相比其他数据结构,如哈希表,能够显著减少存储空间的需求。 2. **查询时间快**:由于只需要进行哈希计算和位运算,布隆过滤器的查询时间非常快,接近于常数时间O(k),其中k是哈希函数的个数。 3. **并行性好**:由于哈希函数之间相互独立,布隆过滤器方便由硬件并行实现,进一步提高查询效率。 4. **不需要存储元素本身**:在某些对保密要求非常严格的场合,布隆过滤器有优势,因为它只存储哈希值,不存储元素本身。 ### 缺点 1. **误判率**:布隆过滤器存在一定的误判率,即有可能将不存在的元素误判为存在。随着存入的元素数量增加,误判率也会随之增加。 2. **删除困难**:布隆过滤器不支持直接删除元素,因为多个元素可能哈希到同一个位置,删除该位置会影响其他元素的判断。虽然有一些变种或方法可以实现删除,但通常并不简单且可能引入新的问题。 ### 应用场景 由于布隆过滤器的优点,它在多个领域都有广泛的应用,包括但不限于: 1. **缓存系统**:避免缓存穿透,即当缓存未命中时,使用布隆过滤器快速判断该请求是否可能存在于数据库中,从而减少对数据库的查询压力。 2. **网络安全**:用于检测恶意请求、过滤垃圾邮件等。 3. **生物信息学**:快速查找DNA测序数据中的基因序列。 4. **大数据分析**:帮助企业进行非结构化大数据分析,找出其中的趋势。 5. **机器学习**:快速处理海量数据,提升模型性能。 ### 总结 布隆过滤器是一种高效的数据结构,适用于大规模数据的快速检索和判断元素是否存在。然而,它也存在一定的误判率和删除困难等缺点。在实际应用中,需要根据具体场景和需求权衡其优缺点,选择合适的数据结构。 # RabbitMQ > 主要的问题: > > 1. 如何防止消息丢失 > > > 分三种情况: > > > > 1. 生产者投递丢失 > > 2. mq服务器宕机或重启丢失 > > 3. 消费者消费丢失 > > 2. 如何防止消息重复消费 ?如何保证消息的幂等性 > > 3. 如何处理消息堆积 > > 问: 用过mq吗? > > 从以下几方面聊: > > 1. 什么mq > > 2. mq的作用 应用场景 > > 3. 使用mq的注意事项 > > > 1. 消息不丢失 > > 2. 消息不重复消费 > > 3. 消息积压 > > 4. 消息高可用 ## 什么是 rabbitmq 采用 AMQP 高级消息队列协议的一种消息队列技术,最大的特点就是消费者并不需要确保提供方存在,实现了服务之间的高度解耦 ## 使用RabbitMQ需要注意什么 消息丢失,消息重复消费等等 ## 为什么要使用 rabbitmq 1. 在分布式系统下具备异步,削峰,负载均衡等一系列高级功能; 2. 拥有持久化的机制,进程消息,队列中的信息也可以保存下来。 3. 实现消费者和生产者之间的解耦。 4. 对于高并发场景下,利用消息队列可以使得同步访问变为串行访问达到一定量的限流,利于数据库的操作。 5. 可以使用消息队列达到异步下单的效果,排队中,后台进行逻辑下单。 ## 使用 rabbitmq 的场景 * 服务间异步通信 * 顺序消费 * 定时任务(订单30分钟未支付 自动取消) * 请求削峰 * 解耦(为面向服务的架构(SOA)提供基本的最终一致性实现) ## RabbitMQ各组件的功能(了解) * Server(Broker):接收客户端的连接,实现AMQP实体服务。 * Connection:连接,应用程序与Server的网络连接,TCP连接。 * Channel:信道,消息读写等操作在信道中进行。客户端可以建立多个信道,每个信道代表一个会话任务。如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 * Connection 极大减少了操作系统建立 TCP connection 的开销 * Message:消息,应用程序和服务器之间传送的数据,消息可以非常简单,也可以很复杂。由Properties和Body组成。Properties为外包装,可以对消息进行修饰,比如消息的优先级、延迟等高级特性;Body就是消息体内容。 * Virtual Host:虚拟主机,用于逻辑隔离。一个虚拟主机里面可以有若干个Exchange和Queue,同一个虚拟主机里面不能有相同名称的Exchange或Queue。 * Exchange:交换器,接收消息,按照路由规则将消息路由到一个或者多个队列。如果路由不到,或者返回给生产者,或者直接丢弃。RabbitMQ常用的交换器常用类型有direct、topic、fanout、headers四种,后面详细介绍。 * Binding:绑定,交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个RoutingKey,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据 * RoutingKey:路由键,生产者将消息发送给交换器的时候,会发送一个RoutingKey,用来指定路由规则,这样交换器就知道把消息发送到哪个队列。路由键通常为一个“.”分割的字符串,例如“com.rabbitmq”。 ## RabbitMQ工作原理 ![image-20240202165617323](assets/image-20240202165617323.png) AMQP 协议模型由三部分组成:生产者、消费者和服务端,执行流程如下: 1. 生产者是连接到 Server,建立一个连接,开启一个信道。 2. 生产者声明交换器和队列,设置相关属性,并通过路由键将交换器和队列进行绑定。 3. 消费者也需要进行建立连接,开启信道等操作,便于接收消息。 4. 生产者发送消息,发送到服务端中的虚拟主机。 5. 虚拟主机中的交换器根据路由键选择路由规则,发送到不同的消息队列中。 6. 订阅了消息队列的消费者就可以获取到消息,进行消费。 ## RabbitMQ消息丢失的情况有哪些?(高频) * 生产者发送消息RabbitMQ Server 消息丢失 > 1. 发送过程中存在网络问题,导致消息没有发送成功 > > 2. 代码问题,导致消息没发送 * RabbitMQ Server中存储的消息丢失 > 消息没有持久化,服务器重启导致存储的消息丢失 * RabbitMQ Server中存储的消息分发给消费者者丢失 > 1. 消费端接收到相关消息之后,消费端还没来得及处理消息,消费端机器就宕机了 > > 2. 处理消息存在异常 ## RabbitMQ消息持久化的条件? * 消息持久化,当然前提是队列必须持久化 * 声明队列必须设置持久化 durable 设置为 true. * 消息推送投递模式必须设置持久化,deliveryMode 设置为 2(持久)。 * 消息已经到达持久化交换器。 * 消息已经到达持久化队列。 ## RabbitMQ死信消息的来源? 介绍一下死信的概念:无法被消费者消费的消息 * 消息 TTL 过期 * 队列达到最大长度(队列满了,无法再添加数据到 mq 中) * 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false ## RabbitMQ死信队列的用处? 可以用于实现延迟队列 、定义任务 ## RabbitMQ支持延迟队列吗? 支持。延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。 ## RabbitMQ延迟队列的使用场景 * 订单在十分钟之内未支付则自动取消 * 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒(时间太长) * 用户注册成功后,如果三天内没有登陆则进行短信提醒 * 用户发起退款,如果三天内没有得到处理则通知相关运营人员 * 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议 ## RabbitMQ实现延迟队列的有什么条件?(了解) 消息设置TTL > 实现方式: > > * 给消息设置TTL > > > 1. 设置策略 > > 2. 消息属性 x-message-ttl > > * 给队列设置TTL > > > x-expires 配置了死信队列 > ~~~java > map.put("x-dead-letter-exchange","dead-ex"); //指明死信交换器 > map.put("x-dead-letter-routing-key","dead");//绑定死信路由键 > ~~~ > > ## 哪些情况下推荐使用RabbitMQ的惰性队列 * 队列可能会产生消息堆积 * 队列对性能(吞吐量)的要求不是非常高,例如TPS 1万以下的场景 * 希望队列有稳定的生产消费性能,不受内存影响而波动 ## RabbitMQ 上的queue 中存放的 message 是否有数量限制? * RabbitMQ本身没有明确的消息数量限制,实际上,这取决于你的系统硬件和配置 * 队列长度的最大限制分为两种情况:队列中消息的总量(max-length)和队列中消息的总字节数(max-length-bytes) ## 队列的溢出行为(了解) 当设置了最大队列长度或大小并达到最大值时,x-overflow属性默认的处理策略是丢掉队列的头部的消息,或者将队列头部的消息投递到死信交换机。 当然也可以通过x-overflow参数来指定处理逻辑。x-overflow参数的可选值有: * drop-head(默认) 默认删除队列中头部的信息 (转换为死信) * reject-publish 拒绝发布 ,真正的丢失 * reject-publish-dlx 将队列满后发送的消息转换为死信 ## 消息如何分发? - 一个生产者,多个消费者 - 多个消费者时,是轮询机制,依次分发给消费者(每个消费者按顺序依次消费) ## 如何确保消息正确地发送至 RabbitMQ? 如何确保消息接收方消费了消息? ### 发送方确认模式 将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID。 一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。 如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(notacknowledged,未确认)消息。 发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。 ### 接收方确认机制 消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。 这里并没有用到超时机制,RabbitMQ 仅通过 Consumer 的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ 给了 Consumer 足够长的时间来处理消息。保证数据的最终一致性; 下面罗列几种特殊情况 1. 如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ 会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重) 2. 如果消费者接收到消息却没有确认消息,连接也未断开,则 RabbitMQ 认为该消费者繁忙,将不会给该消费者分发更多的消息。 ## 如何避免消息重复投递或重复消费? * 在消息生产时,MQ 内部针对每条生产者发送的消息生成一个 inner-msg-id,作为去重的依据(消息投递失败并重传),避免重复的消息进入队列 * 在消息消费时,要求消息体中必须要有一个 bizId(对于同一业务全局唯一,如支付 ID、订单 ID、帖子 ID 等)作为去重的依据,避免同一条消息被重复消费。 ## 消息基于什么传输? 由于 TCP 连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ 使用信道的方式来传输数据。信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制。 ## 消息怎么路由? 消息提供方->路由->一至多个队列消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。通过队列路由键,可以把队列绑定到交换器上。消息到达交换器后,RabbitMQ 会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则); 常用的交换器主要分为三种: * fanout:如果交换器收到消息,将会广播到所有绑定的队列上 * direct:如果路由键完全匹配,消息就被投递到相应的队列 * topic:可以使来自不同源头的消息能够到达同一个队列。 使用 topic 交换器时,可以使用通配符 ## 如何确保消息不丢失? 消息持久化,当然前提是队列必须持久化 RabbitMQ 确保持久性消息能从服务器重启中恢复的方式是,将它们写入磁盘上的一个持久化日志文件,当发布一条持久性消息到持久交换器上时,Rabbit 会在消息提交到日志文件后才发送响应。一旦消费者从持久队列中消费了一条持久化消息,RabbitMQ 会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前 RabbitMQ 重启,那么 Rabbit 会自动重建交换器和队列(以及绑定),并重新发布持久化日志文件中的消息到合适的队列。 ## RabbitMQ如何保证消息的可靠性和避免消息重复消费 1. 持久化消息 设置消息的持久化,既使MQ服务器重启了,消息还要 2. 确认机制 消费者确认机制与生产者确认机制 3. 消息回退 如果消费者无法处理消息,可以将消息重新发送到队列人,等待后续处理 4. TTL (Time-to-Live) 可以设置TTL来限制消息在队列中的存活时间,过期之后直接丢到死信对列 5. 死信队列 通过这些措施,RabbitMQ可以有效地保证消息的可靠性 ## Rabbitmq的手动ACK和自动ACK - 自动ACK:消息一旦被接收,消费者自动发送ACK - 手动ACK:消息接收后,不会发送ACK,需要手动调用ACK ## RabbitMq如何保证消息有序性? 一个队列对应一个消费者 ## RabbitMQ部署方式? 1.单机模式 2.普通集群(主从) 3.镜像集群 ## RabbitMQ如何处理消息堆积情况? 方法:临时扩容,快速处理积压的消息 * 先修复 consumer 的问题,确保其恢复消费速度,然后将现有的 consumer 都停掉; * 临时创建原先 N 倍数量的 queue ,然后写一个临时分发数据的消费者程序,将该程序部署上去消费队列中积压的数据,消费之后不做任何耗时处理,直接均匀轮询写入临时建立好的 N 倍数量的 queue 中; * 接着,临时征用 N 倍的机器来部署 consumer,每个 consumer 消费一个临时 queue 的数据 * 等快速消费完积压数据之后,恢复原先部署架构 ,重新用原先的 consumer 机器消费消息。 这种做法相当于临时将 queue 资源和 consumer 资源扩大 N 倍,以正常 N 倍速度消费。 ## RabbitMQ如何处理消息堆积过程中丢失的数据? > 丢失的消息 转换为死信 消费死信队列 采用**“批量重导”**的方式,在流量低峰期,写一个程序,手动去查询丢失的那部分数据,然后将消息重新发送到mq里面,把丢失的数据重新补回来 ## RabbitMQ如何处理长时间未处理导致写满的情况? 如果消息积压在RabbitMQ里,并且长时间都没处理掉,导致RabbitMQ都快写满了,这种情况肯定是临时扩容方案执行太慢;这种时候只好采用 “丢弃+批量重导” 的方式来解决了。首先,临时写个程序,连接到RabbitMQ里面消费数据,消费一个丢弃一个,快速消费掉积压的消息,降低RabbitMQ的压力,然后在流量低峰期时去手动查询重导丢失的这部分数据。 ## 如何设计一个消息队列? 要考虑三点:伸缩性、持久化、可用性 ### 伸缩性 需要扩容的时候可以快速扩容,增加吞吐量和容量;可以参考kafaka的设计理念,broker -> topic -> partition,每个partition放一个机器,就存一部分数据;资源不够了,给topic增加partition,然后做数据迁移,增加机器; ### 持久化 也就是数据要不要写入磁盘,不写入吧,进程挂了,数据就丢失了,写入磁盘该如何高效写入呢?kafaka的思路:顺序读写,采用磁盘缓存(Page Cache)的策略,操作系统采用预读和后写的方式,对磁盘进行优化。 * 预读:磁盘顺序读取的效率是很高的(不需要寻道时间,只需要很少的旋转时间)。而在读取磁盘某块数据时,同时会顺序读取相邻地址的数据加载到PageCache,这样在读取后续连续数据时,只需要从PageCache中读取数据,相当于内存读写,速度会特别快 * 后写:数据并不是直接写入到磁盘,而是默认先写入到Page Cache,再由Page Cache刷新到磁盘,刷新频率是由操作系统周期性的sync触发的(用户也可以手动调用sync触发刷新操作)。后写的方式大大减少对磁盘的总写入次数,提高写入效率 ### 可用性 分布式系统的高可用几乎都是通过冗余实现的,Kafka同样如此。Kafka的消息存储到partition中,每个partition在其他的broker中都存在多个副本。对外通过主partition提供读写服务,当主partition所在的broker故障时,通过HA机制,将其他Broker上的某个副本partition会重新选举成主partition,继续对外提供服务。 # MQ消息队列 ## MQ 是什么?为什么使用?(掌握) MQ(Message Queue)消息队列,是 “先进先出” 的一种数据结构。 MQ 一般用来解决应用解耦,异步处理,流量削峰等问题,实现高性能,高可用,可伸缩和最终一致性架构。 * 应用解耦:当 A 系统生产关键数据,发送数据给多个其他系统消费,此时 A 系统和其他系统产生了严重的耦合,如果将 A 系统产生的数据放到 MQ 当中,其他系统去 MQ 获取消费数据,此时各系统独立运行只与 MQ 交互,添加新系统消费 A 系统的数据也不需要去修改 A 系统的代码,达到了解耦的效果。 * 异步处理:互联网类企业对用户的直接操作,一般要求每个请求在 200ms 以内完成。对于一个系统调用多个系统,不使用 MQ 的情况下,它执行完返回的耗时是调用完所有系统所需时间的总和;使用 MQ 进行优化后,执行的耗时则是执行主系统的耗时加上发送数据到消息队列的耗时,大幅度提升系统性能和用户体验。 * 流量削峰:MySQL 每秒最高并发请求在 2000 左右,用户访问量高峰期的时候涌入的大量请求,会将 MySQL 打死,然后系统就挂掉,但过了高峰期,请求量可能远低于 2000,这种情况去增加服务器就不值得,如果使用 MQ 的情况,将用户的请求全部放到 MQ 中,让系统去消费用户的请求,不要超过系统所能承受的最大请求数量,保证系统不会再高峰期挂掉,高峰期过后系统还是按照最大请求数量处理完请求。 ## 使用 MQ 的缺陷有哪些?(了解) * 系统可用性降低:以前只要担心系统的问题,现在还要考虑 MQ 挂掉的问题,MQ 挂掉,所关联的系统都会无法提供服务。 * 系统复杂性变高:要考虑消息丢失、消息重复消费等问题。 * 一致性问题:多个 MQ 消费系统,部分成功,部分失败,要考虑事务问题。 ## 你了解哪些常用的 MQ?(了解) * ActiveMQ:支持万级的吞吐量,较成熟完善;官方更新迭代较少,社区的活跃度不是很高,有消息丢失的情况。 * RabbitMQ:延时低,微妙级延时,社区活跃度高,bug 修复及时,而且提供了很友善的后台界面;用 Erlang 语言开发,只熟悉 Java 的无法阅读源码和自行修复 bug。 * RocketMQ:阿里维护的消息中间件,可以达到十万级的吞吐量,支持分布式事务。 * Kafka:分布式的中间件,最大优点是其吞吐量高,一般运用于大数据系统的实时运算和日志采集的场景,功能简单,可靠性高,扩展性高;缺点是可能导致重复消费。 ## MQ 有哪些使用场景?(掌握) * 异步处理:用户注册时,发送注册邮件和注册短信。用户注册完成后,提交任务到 MQ,发送模块并行获取 MQ 中的任务。 * 系统解耦:比如用注册完成,再加一个发送微信通知。只需要新增发送微信消息模块,从 MQ 中读取任务,发送消息即可。无需改动注册模块的代码,这样注册模块与发送模块通过 MQ 解耦。 * 流量削峰:秒杀和抢购等场景经常使用 MQ 进行流量削峰。活动开始时流量暴增,用户的请求写入 MQ,超过 MQ 最大长度丢弃请求,业务系统接收 MQ 中的消息进行处理,达到流量削峰、保证系统可用性的目的。 * 日志处理:日志采集方收集日志写入 kafka 的消息队列中,处理方订阅并消费 kafka 队列中的日志数据。 * 消息通讯:点对点或者订阅发布模式,通过消息进行通讯。如微信的消息发送与接收、聊天室等。 ## 如何保证MQ的高可用? ### ActiveMQ Master-Slave 部署方式主从热备,方式包括通过共享存储目录来实现(shared filesystem Master-Slave)、通过共享数据库来实现(shared database Master-Slave)、5.9版本后新特性使用 ZooKeeper 协调选择 master(Replicated LevelDB Store)。 Broker-Cluster 部署方式进行负载均衡。 ### RabbitMQ 单机模式与普通集群模式无法满足高可用,镜像集群模式指定多个节点复制 queue 中的消息做到高可用,但消息之间的同步网络性能开销较大。 ### RocketMQ 有多 master 多 slave 异步复制模式和多 master 多 slave 同步双写模式支持集群部署模式。 Producer 随机选择 NameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Broker Master 建立长连接,且定时向 Master 发送心跳,只能将消息发送到 Broker master。 Consumer 同时与提供 Topic 服务的 Master、Slave 建立长连接,从 Master、Slave 订阅消息都可以,订阅规则由 Broker 配置决定。 ### Kafka 由多个 broker 组成,每个 broker 是一个节点;topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 存放一部分数据,这样每个 topic 的数据就分散存放在多个机器上的。 replica 副本机制保证每个 partition 的数据同步到其他节点,形成多 replica 副本;所有 replica 副本会选举一个 leader 与 Producer、Consumer 交互,其他 replica 就是 follower;写入消息 leader 会把数据同步到所有 follower,从 leader 读取消息。 每个 partition 的所有 replica 分布在不同的机器上。某个 broker 宕机,它上面的 partition 在其他节点有副本,如果有 partition 的 leader,会进行重新选举 leader。 ## 如何保证消息不被重复消费? (掌握) 消息被重复消费,就是消费方多次接受到了同一条消息。根本原因就是,第一次消费完之后,消费方给 MQ 确认已消费的反馈,MQ 没有成功接受。比如网络原因、MQ 重启等。 所以 MQ 是无法保证消息不被重复消费的,只能业务系统层面考虑。 不被重复消费的问题,就被转化为消息消费的幂等性的问题。幂等性就是指一次和多次请求的结果一致,多次请求不会产生副作用。 保证消息消费的幂等性可以考虑下面的方式: 1. 给消息生成全局 id,消费成功过的消息可以直接丢弃 2. 消息中保存业务数据的主键字段,结合业务系统需求场景进行处理,避免多次插入、是否可以根据主键多次更新而并不影响结果等 ## 如何保证消息不丢失?(掌握) * 生产者丢失消息:如网络传输中丢失消息、MQ 发生异常未成功接收消息等情况。 > 解决办法:主流的 MQ 都有确认或事务机制,可以保证生产者将消息送达到 MQ。如 RabbitMQ 就有事务模式和 confirm 模式。 * MQ 丢失消息:MQ 成功接收消息内部处理出错、宕机等情况。 > 解决办法:开启 MQ 的持久化配置。 * 消费者丢失消息:采用消息自动确认模式,消费者取到消息未处理挂掉了。 > 解决办法:改为手动确认模式,消费者成功消费消息再确认。 ## 什么是消息队列?(了解) 消息队列是一种分布式系统中的通信方式,它通过异步传输消息的方式来解耦消息的生产者和消费者。在消息队列中,生产者将消息发送到一个中心化的队列中,然后消费者从队列中取出消息进行处理。 ## 为什么需要使用消息队列? 使用消息队列可以解决分布式系统中的多种问题,包括异步处理、解耦、流量控制、高可用性、可伸缩性、顺序处理等问题。消息队列可以提高系统的可靠性、可扩展性、灵活性和性能。 ## 什么是消息模型? 消息模型是消息队列系统中的一个概念,用于描述消息的传递方式。常见的消息模型包括点对点模型和发布/订阅模型。 * 点对点模型:生产者将消息发送到队列中,然后消费者从队列中取出消息进行处理。在点对点模型中,每条消息只能被一个消费者消费,即一条消息只有一个接收者。 * 发布/订阅模型:生产者将消息发布到主题(Topic)中,然后多个消费者订阅该主题并接收消息。在发布/订阅模型中,每条消息可以被多个消费者消费,即一条消息可以有多个接收者。 ## 什么是消息堆积?如何解决消息堆积问题?(掌握) 消息堆积是指由于消费者处理速度不足,导致消息队列中积累了大量未处理的消息,从而影响消息的传输和处理。消息堆积可能导致消息的延迟、消息的丢失、消息队列的阻塞等问题。为了解决消息堆积问题,可以采用以下几种方式: * 提高消费者的处理速度,增加消费者的数量或优化消费者的代码。 * 调整消息队列的配置参数,例如扩大队列的容量、调整消息超时时间等。 对于无法处理的消息,可以将其丢弃或者进行重试,避免堆积占用过多的队列资源。 * 实现消息监控和告警机制,及时发现和处理消息堆积问题,保证消息队列的稳定性和可靠性。 * 快速消费(消费一条消息 扔一条消息(放到死信队列)) ## 如何保证消息的可靠性?(掌握) 为了保证消息的可靠性,可以采用以下几种措施: * 使用持久化队列,将消息保存到磁盘中,避免因系统故障导致消息丢失。 * 使用消息确认机制,生产者在发送消息后等待消费者的确认消息,确保消息被消费。 * 使用消息重试机制,当消息未被确认或处理失败时,自动重发消息,确保消息被成功处理。 * 使用事务机制,保证消息的原子性和一致性,即要么全部消息发送成功,要么全部发送失败,避免消息发送和消费的不一致性。 * 使用幂等性处理,保证消息的多次处理具有相同的结果,避免消息的重复处理。 ## 什么是消息延迟队列? ​ 消息延迟队列是一种特殊的消息队列,它可以将消息延迟一定的时间后再进行处理。延迟队列通常用于处理需要在一定时间后才能处理的任务,例如订单超时未支付的处理。 ## 如何保证消息顺序性?(掌握) ​ 在消息队列中,保证消息的顺序性是一个比较复杂的问题,需要考虑多个方面的因素,例如消息生产和消费的速度、消息路由的算法、消息队列的分区方式等。为了保证消息的顺序性,可以采用以下几种措施: * 使用单线程消费者,保证消息的顺序性。(避免多消费者并发消费同一个 queue 中的消息。) * 将消息按照顺序发送到同一个队列中,保证消息的顺序性。(生产者保证消息入队的顺序) * 使用分区的方式将消息分发到多个队列中,但保证同一个分区内的消息被处理的顺序性。 * 使用有序队列(如Kafka中的有序消息队列)来保证消息的顺序性。 ## 什么是消息中间件的负载均衡? ​ 消息中间件的负载均衡是指将消息队列中的消息均匀地分发到多个消费者节点上,从而实现消费者的负载均衡。常见的负载均衡算法包括轮询、随机、最小负载等算法。 ## 什么是消息过滤? ​ 消息过滤是指根据一定的规则对消息进行筛选和过滤,只将符合条件的消息发送给指定的消费者。常见的消息过滤方式包括基于主题的过滤、基于标签的过滤、基于内容的过滤等方式。通过消息过滤,可以实现更加细粒度的消息控制和处理。 ## 什么是消息路由? 消息路由是指消息在消息队列中的传输路径。在消息队列中,消息可以通过不同的路由方式被分发到不同的队列或消费者节点中,例如广播、点对点等路由方式。通过灵活地设置消息路由,可以实现更加精细化的消息控制和处理。 ## 什么是消息队列的持久化? 消息队列的持久化是指将消息保存在持久化存储器中,以保证即使在消息中间件重启后,消息仍然可以被恢复。消息队列的持久化通常分为两种方式:同步持久化和异步持久化。 * 同步持久化将消息先写入到磁盘中,然后再发送到队列中,可以保证消息的可靠性和稳定性,但对性能有一定的影响; * 异步持久化将消息先发送到队列中,然后再异步地将消息写入到磁盘中,可以提高性能,但可能会出现消息丢失的情况。 ## 如何保证消息队列的高可用性? 保证消息队列的高可用性是一个比较复杂的问题,需要从多个方面进行考虑,例如数据可靠性、故障恢复能力、性能优化等。常见的保证消息队列高可用性的措施包括: * 数据备份和数据同步,保证消息队列的数据可靠性和一致性。 * 基于主从或集群的架构,实现故障恢复和负载均衡。 * 基于消息复制机制,实现消息的冗余备份,保证数据的可靠性和恢复能力。 * 采用高性能的存储技术,例如SSD、内存存储等,提高消息队列的性能和响应速度。 * 采用监控和告警机制,及时发现和处理问题,保证消息队列的稳定性和可靠性。 ## RabbitMQ与Kafka有什么区别? RabbitMQ和Kafka都是流行的消息中间件,但它们在设计理念、使用场景和功能特性上存在一些差异。具体来说,它们的区别包括: ### 设计理念 * RabbitMQ采用的是AMQP(高级消息队列协议),并支持多种消息模式和路由方式,适合于较为复杂的消息场景 * Kafka采用的是发布-订阅模式,通过分区和副本机制实现消息的高可用性和可扩展性。 ### 使用场景 * RabbitMQ适合于需要实现复杂路由、事务性操作和高并发的场景,例如电商、金融等领域; * Kafka适合于需要大规模处理实时数据流的场景,例如日志、监控、推荐等领域。 ### 功能特性 * RabbitMQ提供了完整的消息确认、消息持久化、队列优先级、消息顺序等功能特性; * Kafka则提供了更加高效的消息传输和存储,支持消息的实时处理和流式计算等功能特性。 总之,RabbitMQ和Kafka都有自己独特的优势和适用场景,选择合适的消息中间件需要根据具体业务需求和技术架构进行综合评估。 # 微服务 ## 什么是微服务(了解) 微服务是一种分布式系统架构风格,它的核心理念是将传统的单一应用开发为一组微型服务,每个服务运行在独立的进程中,服务之间采用轻量级通信机制进行相互调用。 > 微服务架构是一种架构模式或者说是一种架构风格,它提倡**将单一应用程序划分成一组小的服务**,每个服务运行在其独立的自己的进程中,服务之间互相协调、互相配合,为用户提供最终价值。**每个服务是独立的**,可以是不同的语言,**独立的数据库**。 ## 微服务的优点(了解) 1.单个服务的启动快; 2. 功能的新增、bug的定位、修复、部署、简单; 3. 硬件选型方便 4. 模块精准扩容、降级,节约资源; 5. 可用性强; 6. 语言和框架的转换方便 7. 独立的数据库 8. 解耦,每个人或团队专注一个服务,开发成本大大降低。 9. 持续部署变成可能 ## 微服务的缺点(了解) * 服务互相依赖复杂 * 需要考虑:分布式锁,分布式事务 * 服务太多,不好管理。 * 成本高,还需要注册中心,配置中心,链路追踪等,占用更多的系统资源 ## 微服务之间是如何通讯的?(知道) 1. 同步调用,通过feign组件(feign是对http请求的再度包装,Feign旨在使编写Java Http客户端变得更容易) > * RestTemplate > * Apache http client > * okhttp > * Feign/OpenFeign 2. 异步方式,使用mq ## 微服务与单体架构的区别?(了解) | 维度 | 单体架构 | 微服务架构 | | ------------ | ---------------------- | -------------------------------- | | **部署** | 整体打包部署,耦合度高 | 独立服务部署,弹性扩缩容 | | **技术栈** | 单一技术栈 | 多技术栈混合(技术异构) | | **维护** | 迭代成本高 | 服务拆分后维护更灵活(滚动更新) | | **故障影响** | 一处故障影响整体 | 故障隔离性强 | ## 什么是分布式系统?它与单体系统的区别?(了解) 分布式系统是通过网络连接多个独立节点协同工作的系统,每个节点运行独立进程,通过通信协作完成任务。 **区别**: - **架构复杂度**:分布式需处理网络通信、一致性、容错等,单体系统逻辑集中在单一进程。 - **扩展性**:分布式可通过添加节点横向扩展,单体系统受限于单节点资源。 - **故障影响**:分布式部分节点故障不影响整体(需容错设计),单体系统故障导致整体不可用。 ## 微服务架构中服务注册与发现的原理?常见组件有哪些?(了解) - 原理 - 服务提供者启动时向注册中心注册地址,服务消费者从注册中心获取地址列表并调用。 - 注册中心通过心跳机制检测服务健康状态,剔除失效节点。 - 常见组件 - **Consul**:支持服务注册、健康检查、KV 存储,基于 Raft 算法保证一致性。 - **Nacos**:阿里开源,支持服务注册与配置管理,支持 AP/CP 模式切换。 - **Eureka**:Netflix 开源,基于 AP 模型,已停止维护。 ## 分布式系统中如何实现负载均衡?客户端与服务端负载均衡的区别?(了解) - 实现方式 - **客户端负载均衡**:消费者从注册中心获取服务列表,本地实现负载策略(如 Ribbon、SpringCloudLoadBanlance)。 - **服务端负载均衡**:通过负载均衡器(如 Nginx、LVS)转发请求到后端服务。 - 区别 - 客户端负载均衡无需额外组件,但需在代码中集成逻辑;服务端负载均衡集中管理,但可能成为单点瓶颈。 ## 分布式系统中如何生成全局唯一 ID?常见方案对比(掌握) - **UUID**:基于时间戳 + MAC 地址生成,简单但无序,不适合数据库索引。 - **数据库自增**:单节点可靠,分布式需额外机制(如分库分表时设置步长),性能受限于数据库。 - 雪花算法(Snowflake) - 64 位长整型:1 位符号位 + 41 位时间戳 + 10 位工作机器 ID+12 位序列号。 - 优点:高性能、有序,适合分布式;缺点:依赖时钟同步,时钟回拨可能生成重复 ID。 - **Redis 生成**:使用`INCR`命令自增,性能高但依赖 Redis 集群可用性。 ## 如何监控分布式系统的健康状态?常用组件有哪些?(了解) - 监控维度 - 服务指标:QPS、响应时间、错误率。 - 资源指标:CPU、内存、磁盘 IO、网络流量。 - 分布式指标:节点存活状态、数据同步延迟。 - 常用组件 - **Prometheus + Grafana**:采集指标并可视化展示。 - **ELK Stack**:日志收集(Elasticsearch)、分析(Logstash)、展示(Kibana)。 - **Skywalking**:分布式链路追踪,定位服务调用瓶颈。 ## 什么是服务熔断和限流?常用框架有哪些?(了解) - **熔断**:当目标服务故障比例过高时,暂时切断调用,避免级联失败(如 Sentinel / Hystrix 的断路器模式)。 - **限流**:控制对服务的请求速率,防止流量激增导致服务过载(如限制 QPS≤1000)。 - 常用框架 - **Sentinel**:阿里开源,支持熔断、限流、流量整形,提供控制台可视化配置。 - **Hystrix**:Netflix 开源,已停止维护,功能较单一。 ## 请谈谈对SpringBoot 和SpringCloud的理解(了解) 1. SpringBoot专注于快速方便的开发单个个体微服务。 2. SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、断路器、路由、分布式会话等等集成服务 3. SpringBoot可以离开SpringCloud独立使用开发项目,但是SpringCloud离不开SpringBoot,属于依赖的关系. 4. SpringBoot专注于快速、方便的开发单个微服务个体,SpringCloud关注全局的**服务治理**框架。 ## 提高高并发 1. 硬件的纵深扩展,升级服务器(4核16G),多台服务器,单服务的多实例部署 2. 技术方案:限流、缓存、降级、熔断 (多个nginx、gateway、分库分表等)。 3. 使用缓存,如 redis 4. 使用消息中间件,如mq 5. 页面静态化 ## 熔断降级 ### 服务降级 #### 什么是服务降级 服务降级一般是指在服务器压力剧增的时候,根据实际业务使用情况以及流量,对一些服务和页面有策略的不处理或者用一种简单的方式进行处理,从而**释放服务器资源的资源以保证核心业务的正常高效运行。**说白了,就是尽可能的把系统资源让给优先级高的服务。 #### 为什么需要服务降级 服务器的资源是有限的,而请求是无限的。在用户使用即并发高峰期,会影响整体服务的性能,严重的话会导致宕机,以至于某些重要服务不可用。故高峰期为了保证核心功能服务的可用性,就需要对某些服务降级处理。可以理解为舍小保大 #### 应用场景 多用于微服务架构中,一般当整个微服务架构整体的负载超出了预设的上限阈值(和服务器的配置性能有关系),或者即将到来的流量预计会超过预设的阈值时(比如双11、6.18等活动或者秒杀活动) #### 服务降级策略 * 拒绝服务 * 关闭服务 ### 服务熔断 #### 什么是服务熔断? 应对微服务雪崩效应的一种链路保护机制,类似股市、保险丝,可看作降级的特殊情况 #### 为什么需要服务熔断? 微服务之间的数据交互是通过远程调用来完成的。服务A调用服务,服务B调用服务c,某一时间链路上对服务C的调用响应时间过长或者服务C不可用,随着时间的增长,对服务C的调用也越来越多,导致请求的堆积,然后服务C崩溃了,但是链路调用还在,对服务B的调用也在持续增多,然后服务B崩溃,随之A也崩溃,导致雪崩效应 **服务熔断是应对雪崩效应的一种微服务链路保护机制**。例如在高压电路中,如果某个地方的电压过高,熔断器就会熔断,对电路进行保护。同样,在微服务架构中,熔断机制也是起着类似的作用。**当调用链路的某个微服务不可用或者响应时间太长时,会进行服务熔断,不再有该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路** ### 降级与熔断的区别 * 触发原因不一样,服务熔断由链路上某个服务引起的,服务降级是从整体的负载考虑 * 管理目标层次不一样,服务熔断是一个框架层次的处理,服务降级是业务层次的处理 * 实现方式不一样,服务熔断一般是自我熔断恢复,服务降级相当于人工控制 * 触发原因不同,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑 ### 熔断降级 熔断降级是解决雪崩问题的重要手段。其思路是由**断路器**统计服务调用的异常比例、慢请求比例,如果超出阈值则会**熔断**该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。 ![image-20230804151852804](assets/image-20230804151852804.png) 状态机包括三个状态: - closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态 - open:打开状态,服务调用被**熔断**,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态 - half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。 - 请求成功:则切换到closed状态 - 请求失败:则切换到open状态 断路器熔断策略有三种:慢调用、异常比例、异常数 #### 慢调用 **慢调用**:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。 ![image-20230804152743575](assets/image-20230804152743575.png) RT超过500ms的调用是慢调用,统计最近10000ms内的请求,如果请求量超过10次,并且慢调用比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。 #### 异常比例、异常数 统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。 ![image-20230804152608387](assets/image-20230804152608387.png) 统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于0.4,则触发熔断。 ## 网关 ​ 传统的单体架构中只需要开放一个服务给客户端调用,但是微服务架构中是将一个系统拆分成多个微服务,如果没有网关,客户端只能在本地记录每个微服务的调用地址,当需要调用的微服务数量很多时,它需要了解每个服务的接口,这个工作量很大。那有了网关之后,能够起到怎样的改善呢? ​ ​ 网关作为系统的唯一流量入口,封装内部系统的架构,所有请求都先经过网关,由网关将请求路由到合适的微服务,所以,使用网关的好处在于: 1. 简化客户端的工作。网关将微服务封装起来后,客户端只需同网关交互,而不必调用各个不同服务; 2. 降低函数间的耦合度。 一旦服务接口修改,只需修改网关的路由策略,不必修改每个调用该函数的客户端,从而减少了程序间的耦合性 3. 解放开发人员把精力专注于业务逻辑的实现。由网关统一实现服务路由(灰度与ABTest)、负载均衡、访问控制、流控熔断降级等非业务相关功能,而不需要每个服务 API 实现时都去考虑 > 但是 API 网关也存在不足之处,在微服务这种**去中心化**的架构中,网关又成了一个中心点或**瓶颈点**,它增加了一个我们必须开发、部署和维护的高可用组件。正是由于这个原因,在网关设计时必须考虑即使 API 网关宕机也不要影响到服务的调用和运行,所以需要对网关的响应结果有数据缓存能力,通过返回缓存数据或默认数据屏蔽后端服务的失败。 > > 在服务的调用方式上面,网关也有一定的要求,API 网关最好是支持 I/O 异步、同步非阻塞的,如果服务是同步阻塞调用,可以理解为微服务模块之间是没有彻底解耦的,即如果A依赖B提供的API,如果B提供的服务不可用将直接影响到A不可用,除非同步服务调用在API网关层或客户端做了相应的缓存。因此为了彻底解耦,在微服务调用上更建议选择**异步**方式进行。而对于 API 网关需要通过底层多个细粒度的 API 组合的场景,推荐采用响应式编程模型进行而不是传统的异步回调方法组合代码,其原因除了采用回调方式导致的代码混乱外,还有就是对于 API 组合本身可能存在并行或先后调用,对于采用回调方式往往很难控制。 #### 服务网关的基本功能 ![image-20240203093835736](assets/image-20240203093835736.png) #### 流量网关与服务网关的区别:(面试题,掌握) ![](assets/112k34ae2c786030a5975767a82d6f433ce2 "") 流量网关和服务网关在系统整体架构中所处的位置如上图所示, **流量网关:**(如Nignx,OpenRest,Kong, Envoy)是指提供全局性的、与后端业务应用无关的策略,例如 HTTPS证书认证、Web防火墙、全局流量监控,黑白名单等。 **服务网关:**(如Spring Cloud Gateway)是指与业务紧耦合的、提供单个业务域级别的策略,如服务治理、身份认证等。也就是说,流量网关负责南北向流量调度及安全防护,微服务网关负责东西向流量调度及服务治理。 2 服务网关的部署: 2.1主流网关的对比与选型: ![](assets/112k60df4d1119c95e54baa182e296d970a4 "") > 1. Kong 网关:Kong 的性能非常好,非常适合做流量网关,但是对于复杂系统不建议业务网关用 Kong,主要是工程性方面的考虑 > > 2. Zuul1.x 网关:Zuul 1.0 的落地经验丰富,但是性能差、基于同步阻塞IO,适合中小架构,不适合并发流量高的场景,因为容易产生线程耗尽,导致请求被拒绝的情况 > > 3. gateway 网关:功能强大丰富,性能好,官方基准测试 RPS (每秒请求数)是Zuul的1.6倍,能与 SpringCloud 生态很好兼容,单从流式编程+支持异步上也足以让开发者选择它了。 > > 4. Zuul 2.x:性能与 gateway 差不多,基于非阻塞的,支持长连接,但 SpringCloud 没有集成 zuul2 的计划,并且 Netflix 相关组件都宣布进入维护期,前景未知。 综上,gateway 网关更加适合 SpringCloud 项目,而从发展趋势上看,gateway 替代 zuul 也是必然的。 ## 流控模式 在添加限流规则时,点击高级选项,可以选择三种流控模式: - **直接**:统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式(入门案例里的效果) - **关联**:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流 - **链路**:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流 ## 流控效果 流控效果是指请求达到流控阈值时应该采取的措施,包括三种: * 快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。 * warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值 * 排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长 # SpringCloud [分布式事务](https://juejin.cn/post/7369481217030717449?searchId=20250519144412073452C359B35F9A67C8) [分布式锁](https://juejin.cn/post/7369582512236757018?searchId=20250519144412073452C359B35F9A67C8) [微服务架构理论](https://juejin.cn/post/7381388012276793363) ## 2PC(掌握) #### **一、2PC 的基本概念** 两阶段提交协议(Two-Phase Commit,2PC)是分布式事务中实现强一致性的经典协议,由协调者(Coordinator)和多个参与者(Participant)共同完成事务的提交过程。其核心目标是确保分布式系统中所有节点对事务的最终状态达成一致,避免数据不一致。 #### **二、2PC 的两个阶段** 2PC 将事务提交过程分为 “准备阶段” 和 “提交阶段”,具体流程如下: ##### **1. 阶段一:准备阶段(Prepare)** - 协调者动作 - 向所有参与者发送`Prepare`请求,包含事务操作指令,并等待响应。 - 参与者动作 - 执行事务操作的**预检查**(如资源是否充足、锁是否可获取)。 - 若预检查通过,执行事务操作(但不提交),并将 undo 和 redo 日志写入磁盘,然后回复`Yes`(同意提交);若失败,回复`No`(拒绝提交)。 - 关键特点 - 参与者在此阶段锁定资源(如数据库行锁),直到事务完成。 - 协调者收集所有参与者的响应,决定是否进入提交阶段。 ##### **2. 阶段二:提交阶段(Commit/Abort)** - 情况 1:所有参与者回复 Yes - 协调者动作 - 向所有参与者发送`Commit`请求,通知正式提交事务。 - 参与者动作 - 提交事务,释放锁定的资源,回复`Ack`。 - 情况 2:任一参与者回复 No 或超时 - 协调者动作 - 向所有参与者发送`Abort`请求,通知回滚事务。 - 参与者动作 - 回滚事务,释放资源,回复`Ack`。 - 关键特点 - 提交阶段是事务的最终确认,一旦进入`Commit`状态,事务不可逆转。 - 若协调者在发送`Commit`后崩溃,未收到指令的参与者会一直阻塞,等待协调者恢复(单点故障问题)。 #### **三、2PC 的核心问题与缺陷** | **问题类型** | **具体表现** | | ------------------ | ------------------------------------------------------------ | | **单点故障** | 协调者是唯一控制中心,若在提交阶段崩溃,参与者会永久阻塞,无法自主决定事务状态。 | | **同步阻塞** | 参与者从准备阶段到提交阶段需一直锁定资源,等待协调者指令,系统吞吐量降低。 | | **脑裂导致不一致** | 若协调者在发送`Commit`后与部分参与者网络分区,已收到指令的参与者提交事务,未收到指令的参与者可能因超时回滚,导致数据不一致。 | | **超时机制缺失** | 2PC 未明确规定参与者超时后的处理逻辑(如协调者崩溃时,参与者无法主动提交或回滚)。 | #### **四、2PC 与 3PC 的对比** | **维度** | **2PC** | **3PC** | | -------------- | ------------------------------------ | ------------------------------------ | | **阶段数** | 2 阶段(准备、提交) | 3 阶段(准备、预提交、最终提交) | | **阻塞问题** | 提交阶段若协调者崩溃,参与者永久阻塞 | 引入超时机制,减少阻塞时间 | | **资源锁定** | 准备阶段即锁定资源,直到提交 | 准备阶段不锁定资源,预提交阶段才锁定 | | **一致性保证** | 强一致性,但脑裂风险较高 | 降低脑裂风险,但仍无法完全避免 | #### **五、2PC 的应用与局限性** - 应用场景 - 传统分布式数据库(如 MySQL XA)、分布式事务中间件(如 Atomikos)常基于 2PC 实现强一致性。 - 适用于对数据一致性要求极高、并发量较低的场景(如金融交易)。 - 局限性 - 性能开销大:两次网络通信 + 资源长时间锁定,不适合高并发场景。 - 可用性差:协调者故障可能导致系统不可用。 - 优化方向 - 引入**协调者选举机制**(如 ZooKeeper)解决单点故障。 - 结合**超时回滚策略**减少阻塞,但无法完全避免一致性风险。 #### **六、总结** 2PC 通过 “准备 - 提交” 两阶段确保分布式事务的强一致性,但存在单点故障、同步阻塞和脑裂等核心缺陷。尽管 3PC 对其进行了优化,但分布式事务的本质挑战(如 CAP 定理中的 “一致性” 与 “可用性” 权衡)仍未完全解决。在实际系统设计中,2PC 更适合对一致性要求极高的场景,而高并发场景通常采用柔性事务(如 TCC、SAGA)或最终一致性方案。 ## 3PC(掌握) #### **一、3PC 的基本概念** 三阶段提交协议(Three-Phase Commit,3PC)是分布式事务中用于保证数据一致性的经典协议,是对两阶段提交协议(2PC)的改进。其核心目标是解决 2PC 中存在的单点故障、长时间阻塞等问题,提升分布式系统的可用性和容错性。 #### **二、3PC 的三个阶段** 3PC 将事务提交过程分为三个阶段,涉及协调者(Coordinator)和参与者(Participant)两种角色,具体流程如下: ##### **1. 阶段一:准备阶段(CanCommit)** - 协调者动作 - 向所有参与者发送`CanCommit`请求,询问是否可以执行事务提交,并等待响应。 - 参与者动作 - 检查自身资源(如数据库连接、锁等)是否充足,事务是否可执行。 - 若可行,回复`Yes`,并进入预备状态;否则回复`No`。 - 关键特点 - 此阶段仅做可行性检查,不实际执行事务操作,参与者未锁定资源。 ##### **2. 阶段二:预提交阶段(PreCommit)** - 情况 1:所有参与者回复 Yes - 协调者动作 - 发送`PreCommit`请求,通知参与者正式执行事务操作。 - 参与者执行事务,但不提交(仅写入 redo 和 undo 日志)。 - 参与者动作 - 执行事务操作,记录日志。 - 若成功,回复`Ack`;若失败,回复`No`。 - 情况 2:任一参与者回复 No 或超时 - 协调者动作 - 发送`Abort`请求,通知所有参与者取消事务。 - 参与者动作 - 收到`Abort`后回滚事务。 - 关键特点 - 参与者开始执行事务,但未真正提交,此时仍可回滚。 - 引入超时机制,避免协调者等待时间过长。 ##### **3. 阶段三:最终提交阶段(DoCommit)** - 情况 1:所有参与者 PreCommit 成功 - 协调者动作 - 发送`DoCommit`请求,通知参与者正式提交事务。 - 参与者动作 - 提交事务,释放资源,回复`Ack`。 - 情况 2:任一参与者 PreCommit 失败或超时 - 协调者动作 - 发送`Abort`请求,通知参与者回滚事务。 - 参与者动作 - 回滚事务,释放资源,回复`Ack`。 - 关键特点 - 参与者根据协调者指令完成最终提交或回滚,此时事务不可逆转。 - 若协调者在发送`DoCommit`后崩溃,参与者超时后会自动提交事务(优化了 2PC 的阻塞问题)。 #### **三、3PC 与 2PC 的对比** | **维度** | **2PC** | **3PC** | | -------------- | ------------------------------------ | -------------------------------------------- | | **阶段数** | 2 阶段(准备、提交) | 3 阶段(准备、预提交、最终提交) | | **阻塞问题** | 提交阶段若协调者崩溃,参与者永久阻塞 | 引入超时机制,减少阻塞时间 | | **单点故障** | 协调者是单点,崩溃可能导致不一致 | 部分缓解单点问题(最终阶段参与者可自动提交) | | **一致性保证** | 强一致性,但存在脑裂风险 | 降低脑裂风险,但仍无法完全避免 | | **资源锁定** | 准备阶段即锁定资源,直到提交 | 准备阶段不锁定资源,预提交阶段才锁定 | #### **四、3PC 的优缺点** ##### **优点:** 1. **减少阻塞时间**:通过超时机制和三阶段设计,参与者无需永久等待协调者,提升系统可用性。 2. **降低脑裂风险**:最终提交阶段,若协调者崩溃,参与者超时后会自动提交(前提是预提交成功),减少因协调者故障导致的不一致。 3. **资源利用率更高**:准备阶段不锁定资源,仅在预提交阶段锁定,缩短了资源占用时间。 ##### **缺点:** 1. **协议复杂度增加**:三阶段流程更复杂,实现难度高于 2PC。 2. **仍存在一致性风险**:若在最终提交阶段发生网络分区,协调者向部分参与者发送`DoCommit`后崩溃,未收到指令的参与者超时后可能自动提交,而另一部分可能因未收到指令而回滚,导致数据不一致。 3. **性能开销**:多一轮通信,可能增加事务延迟。 #### **五、实际应用与局限性** - **应用场景**:3PC 在部分分布式数据库(如 CockroachDB)和分布式事务框架中被采用,但并非主流方案。 - **局限性**:3PC 无法完全解决分布式系统的一致性问题,实际应用中常结合**最终一致性**(如补偿机制、重试策略)或其他协议(如 TCC、SAGA)使用。 - **替代方案**:对于高并发、低延迟场景,更倾向于使用柔性事务(如 TCC、SAGA),而不是强一致性的 3PC。 #### **六、总结** 3PC 通过引入预提交阶段和超时机制,优化了 2PC 的阻塞和单点故障问题,但也引入了新的一致性风险。其核心价值在于平衡分布式系统的**一致性**和**可用性**,但无法彻底解决分布式事务的所有挑战。在实际系统设计中,需根据业务场景选择合适的事务模型,或结合多种机制实现更健壮的一致性方案。 ## 分布式事务 TCC 模式详解(了解) #### 一、TCC 基本概念 TCC 是**Try-Confirm-Cancel**的缩写,是分布式事务处理的一种编程模型,核心思想是将一个完整的业务操作分为三个阶段,通过业务逻辑的分解来实现分布式事务的最终一致性。 与传统数据库事务(ACID)不同,TCC 属于**柔性事务**,更适合分布式微服务架构,允许事务在一定时间内存在中间状态,最终达到一致。 #### 二、TCC 三阶段解析 ##### 1. Try 阶段:资源预留与验证 - **目标**:完成所有业务检查(一致性),预留业务所需资源。 - 特点 - 此阶段操作需具备**幂等性**(多次调用结果一致)。 - 预留资源时需考虑隔离性,避免其他事务抢占。 - **示例**:电商下单时,Try 阶段冻结库存、预留资金额度。 ##### 2. Confirm 阶段:正式提交 - **目标**:确认执行业务操作,使用 Try 阶段预留的资源完成最终提交。 - 特点 - 若 Try 阶段成功,Confirm 必须成功(需保证幂等性)。 - 此阶段只需执行 “确认” 逻辑,无需再做业务检查。 - **示例**:下单确认后,扣除冻结的库存和资金。 ##### 3. Cancel 阶段:操作回滚 - **目标**:若 Try 阶段失败或超时,释放 Try 阶段预留的资源。 - 特点 - 需处理空回滚(Cancel 调用时 Try 未执行)和悬挂(Cancel 先于 Try 调用)。 - 同样需保证幂等性,避免多次回滚导致资源异常。 - **示例**:下单失败时,解冻库存和资金额度。 #### 三、TCC 与其他分布式事务方案对比 | 方案 | 核心逻辑 | 一致性级别 | 性能 | 适用场景 | | ------------------------- | --------------------------------- | ---------- | ---- | -------------------------- | | **2PC(两阶段提交)** | 事务管理器协调所有节点提交 / 回滚 | 强一致性 | 较低 | 金融等对一致性要求极高场景 | | **3PC(三阶段提交)** | 增加 “准备阶段” 减少阻塞 | 最终一致性 | 中等 | 部分高并发场景 | | **TCC** | 业务逻辑分解为三阶段 | 最终一致性 | 较高 | 复杂业务流程、高并发场景 | | **本地消息表 / 可靠消息** | 异步消息 + 重试 | 最终一致性 | 高 | 允许短暂不一致的业务 | #### 四、TCC 的核心优势与挑战 ##### 优势: - **业务灵活性**:通过自定义三阶段逻辑,适配复杂业务流程。 - **高性能**:无长时间锁资源,适合高并发场景(如电商大促)。 - **最终一致性**:通过重试机制确保事务最终完成。 ##### 挑战(实现难点): 1. **幂等性处理**: - 原因:网络波动可能导致重复调用(如 Confirm 多次执行)。 - 解决方案:通过唯一事务 ID 标记操作,重复调用时直接返回成功。 2. **空回滚处理**: - 场景:Cancel 调用时 Try 未执行(如事务超时,上游直接发起 Cancel)。 - 解决方案:增加 “事务状态标记”,Cancel 前检查 Try 是否执行。 3. **悬挂处理**: - 场景:Cancel 请求因网络延迟先于 Try 到达,导致资源被误释放。 - 解决方案:Try 执行前检查是否已收到 Cancel 请求,若有则拒绝执行。 4. **事务状态管理**: - 需维护事务各阶段状态(Try 中 / Confirm 成功 / Cancel 成功),通常借助数据库或缓存实现。 #### 五、典型应用场景与案例 ##### 适用场景: - **跨服务资金操作**:如支付、转账(需冻结 - 扣减 - 解冻流程)。 - **库存管理**:多服务协同扣减库存(如电商下单、出库)。 - **订单流程**:跨服务的订单创建、支付、发货链路。 ##### 案例: - **支付宝 / 微信支付**:在分布式支付场景中,使用 TCC 模式处理资金冻结与扣除。 - **电商平台**:下单时通过 TCC 冻结库存,支付成功后扣减,失败则解冻。 - **金融交易系统**:复杂金融产品的资金预授权与结算流程。 #### 六、TCC 框架与工具 - 开源框架 - Apache ServiceComb:华为开源的微服务框架,支持 TCC 事务。 - TCC-transaction:基于 Spring 的 TCC 实现,提供注解式编程。 - ByteTCC:轻量级 TCC 框架,支持多种存储介质(数据库 / Redis)。 - 商业解决方案 - 蚂蚁金服 SOFAStack:包含 SOFA-TCC 组件,支撑支付宝级交易场景。 #### 七、总结 TCC 模式通过将业务逻辑拆解为 “预留 - 确认 - 回滚” 三阶段,在分布式系统中实现了高性能的最终一致性。尽管实现复杂度较高(需处理幂等性、空回滚等问题),但在电商、金融等复杂业务场景中具有不可替代的优势。选择 TCC 时,需根据业务特性权衡一致性、性能与开发成本,结合具体框架实现高效的分布式事务管理。 ## 解释 CAP 定理及其在分布式系统中的应用 > [CAP 理论参考 ](https://juejin.cn/post/7277490138517798931?searchId=202506161557074F84046E302F0E48A372) CAP 理论是分布式系统领域的重要理论,用于描述分布式系统中**一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)** 三者之间的权衡关系。以下是对 CAP 理论的详细解析: ### **一、CAP 理论的核心概念** #### 1. **一致性(Consistency)** - **定义**:所有节点在同一时间看到的数据是一致的(强一致性)。 - **举例**:当用户在分布式数据库中更新数据后,所有节点应立即返回最新数据,否则可能出现读取到旧数据的情况。 #### 2. **可用性(Availability)** - **定义**:系统在任何时候都能对用户的请求做出响应,不出现不可用状态。 - **要求**:即使部分节点故障,系统仍需保证正常响应(返回非错误结果),但不保证数据是最新的。 #### 3. **分区容错性(Partition Tolerance)** - **定义**:当分布式系统的网络分区(如节点间通信中断)发生时,系统仍能正常运行。 - **必要性**:在分布式环境中,网络分区是必然可能发生的(如网络延迟、节点故障),因此分区容错性是系统必须具备的基本能力。 ### **二、CAP 三角:三者不可兼得** CAP 理论的核心结论是:**在分布式系统中,无法同时满足一致性、可用性和分区容错性这三个目标,必须放弃其中一个**。其关系可通过下图表示: ```plaintext 一致性(Consistency) / \ / \ 分区容错性(Partition Tolerance) \ / \ / 可用性(Availability) ``` - 当保证分区容错性时,必须在一致性和可用性之间做出选择: - **牺牲一致性,保证可用性(AP)**:系统在分区故障时仍可响应请求,但可能返回旧数据(如电商购物车,允许暂时读取旧数据,保证用户可操作)。 - **牺牲可用性,保证一致性(CP)**:系统在分区故障时拒绝部分请求,确保数据一致(如银行转账,必须等待分区恢复后操作,避免账户数据不一致)。 ### **三、实际应用中的权衡案例** #### 1. **AP 系统(牺牲一致性,保证可用性)** - **场景**:电商平台、社交网络(如 Facebook、Twitter)。 - 例子 - 购物车数据存储在分布式缓存中,当网络分区时,允许用户继续添加商品(保证可用),但新增数据可能暂时未同步到所有节点(牺牲强一致性)。 - 分布式数据库 Cassandra、DynamoDB 采用 AP 模型,支持分区故障时的读写操作,但数据可能存在短暂不一致。 #### 2. **CP 系统(牺牲可用性,保证一致性)** - **场景**:金融交易、分布式数据库同步。 - 例子 - 银行转账系统在网络分区时,会暂停转账操作(牺牲部分可用性),确保账户余额在所有节点一致后再处理请求。 - 分布式协调工具 ZooKeeper、etcd 采用 CP 模型,通过共识算法(如 Raft)保证数据一致,但分区时可能无法响应部分请求。 ### **四、CAP 理论的延伸:BASE 理论** - **背景**:CAP 理论强调三者的严格权衡,而 BASE 理论(Basically Available, Soft state, Eventual consistency)是对 CAP 中 AP 模型的实际应用扩展。 - 核心思想 - **基本可用(Basically Available)**:系统在故障时提供降级服务(如部分功能受限),而非完全不可用。 - **软状态(Soft state)**:允许数据存在短暂不一致状态。 - **最终一致性(Eventual consistency)**:经过一段时间后,数据最终会达到一致。 - **应用**:适用于对一致性要求不高,但需要高可用的场景(如缓存、日志系统)。 ### **五、总结** CAP 理论揭示了分布式系统的本质约束:**在网络分区不可避免的情况下,必须根据业务场景选择牺牲一致性或可用性**。实际设计中,需结合业务需求(如金融系统对一致性要求高,社交平台对可用性要求高),在 CAP 三角中找到合适的平衡点。 ## 什么是 SpringCloud?它解决了微服务中的哪些问题? SpringCloud 是基于 Spring Boot 的微服务架构开发工具集,封装了分布式系统中的常见解决方案,包括: - **服务注册与发现**(Nacos/Eureka/Consul):解决服务实例动态管理问题; - **负载均衡**(SpringCloudLoadBalance/Ribbon/Feign):实现请求流量分发; - **服务容错**(Sentinel/Hystrix):防止级联故障;限流、熔断降级 - **API 网关**(Zuul/Gateway):统一入口控制; - **配置中心**(Config):集中管理配置; - **链路追踪**(Sleuth/Zipkin):定位分布式系统故障。 ## 什么是Spring Cloud&&Alibaba? * Spring Cloud是Spring开源组织下的一个子项目,提供了一系列用于实现分布式微服务系统的工具集,帮助开发者快速构建微服务应用。 * Spring Cloud Alibaba是Spring Cloud的子项目;包含微服务开发必备组件;基于和符合Spring Cloud标准的阿里的微服务解决方案。 ## 什么是Spring Cloud Alibaba?在Spring Cloud Alibaba中有哪些组件? Spring Cloud Alibaba致力于提供分布式应用服务开发的一站式解决方案,项目包含开发分布式应用服务的必需组件,方便开发者通过Spring Cloud编程模型轻松使用这些组件来开发分布式应用服务。此项目包含的组件主要选自阿里巴巴开源的中间件和阿里云的商业化产品,但也不限定于这些产品。常用产品如下。 * Sentinel:由阿里巴巴开源,把流量作为切入点,从流量控制、熔断和降级、系统负载保护等多个维度保护服务的稳定性。 * Nacos:由阿里巴巴开源,是一个更易于构建云原生应用的服务发现、配置管理和服务管理平台。 * RocketMQ:基于Java的高性能、高吞吐量的分布式消息和流计算平台。 * Dubbo:一款高性能Java RPC框架。 * Seata:由阿里巴巴开源,是一个易于使用的高性能的微服务分布式事务解决方案。 * Alibaba Cloud OSS:阿里云对象存储服务,是阿里云提供的海量、安全、低成本、高可靠的云存储服务。 * Alibaba Cloud SchedulerX:阿里巴巴中间件团队开发的一款分布式任务调度产品,支持周期性触发任务与固定时间点触发任务。 * Alibaba Cloud SMS:覆盖全球的短信服务,拥有友好、高效、智能的互联通信能力,可帮助企业迅速搭建客户触达通道。 ## 什么是 Spring Cloud? Spring Cloud是一个用于构建分布式系统和微服务架构的开发工具包。它基于Spring框架,提供了一系列解决方案和组件,用于实现服务注册与发现、负载均衡、配置管理、断路器等功能。 ## 使用Spring Cloud有什么优势? 使用 Spring Boot 开发分布式微服务时,我们面临以下问题: 1. 与分布式系统相关的复杂性-这种开销包括网络问题,延迟开销,带宽问题,安全问题。 2. 服务发现-服务发现工具管理群集中的流程和服务如何查找和互相交谈。它涉及一个服务目录,在该目录中注册服务,然后能够查找并连接到该目录中的服务。 3. 冗余-分布式系统中的冗余问题。 4. 负载平衡 --负载平衡改善跨多个计算资源的工作负荷,诸如计算机,计算机集群,网络链路,中央处理单元,或磁盘驱动器的分布。 5. 性能-问题 由于各种运营开销导致的性能问题。 6. 部署复杂性-Devops 技能的要求。 ## Spring Cloud的主要优点是什么? - 提供了一套完整的微服务解决方案,简化了微服务架构的开发和部署。 - 与Spring框架无缝集成,提供了丰富的功能和扩展性。 - 支持多种服务注册与发现的实现,如Nacos、Eureka、Consul、Zookeeper等。 - 提供了一套统一的配置管理模块,可以实现动态更新配置。 - 支持断路器模式,提高了系统的稳定性和容错能力。 ## Spring Cloud的核心组件是什么? Spring Cloud的核心组件包括: - 服务注册与发现:如Nacos、Eureka、Consul、Zookeeper等。 - 负载均衡:如LoadBanlance 、Ribbon、Feign等。 - 断路器:如Sentinel、Hystrix、Resilience4j等。 - 网关:如Zuul、Gateway等。 - 配置管理:如Nacos、Config Server、Bus等。 ## 服务注册和发现是什么意思?Spring Cloud 如何实现? * eureka * nacos 当我们开始一个项目时,我们通常在属性文件中进行所有的配置。随着越来越多的服务开发和部署,添加和修改这些属性变得更加复杂。有些服务可能会下降,而某些位置可能会发生变化。手动更改属性可能会产生问题。 服务注册和发现可以在这种情况下提供帮助。由于所有服务都在 **注册中心**服务器上注册并通过调用**注册中心**服务器完成查找,因此无需处理服务地点的任何更改和处理。 ## Spring Cloud Gateway? Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。 使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。 # 高并发 ## 你了解 QPS、TPS、RT、吞吐量 这些高并发性能指标吗? QPS(每秒查询率)、TPS(每秒事务处理量)、RT(响应时间)以及吞吐量是高并发性能评估中的关键指标,它们各自具有不同的含义和用途。以下是对这些指标的详细解释: ### QPS(每秒查询率) - **定义**:QPS(Queries Per Second)是衡量信息检索系统(如搜索引擎或数据库)在一秒钟内接收到的搜索流量的一种常见度量,也被称为每秒请求数(RPS,Request Per Second)。它表示一台服务器每秒能够响应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 - **应用**:QPS数值越高,意味着系统的处理能力越强。在建设一个能抗住高并发的系统时,需要基于实际的需求和硬件条件来设定一个合理的QPS目标。例如,一个电商网站的QPS如果是10W,就意味着它能在每秒钟内响应10W个用户的查询请求。 ### TPS(每秒事务处理量) - **定义**:TPS(Transactions Per Second)是软件测试结果的测量单位,表示每秒的事务数。一个事务是指一个客户端向服务器发送请求然后服务器做出响应的过程。客户端在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。 - **应用**:TPS通常用来全面评价系统的性能,特别是在评估系统处理写请求的能力时尤为重要。例如,一个金融交易系统的TPS如果达到了5W,就表示该系统可以在每秒内完成5W个交易请求的处理。 ### RT(响应时间) - **定义**:RT(Response Time)表示执行一个请求从开始到最后收到响应数据所花费的总体时间,即从客户端发起请求到收到服务器响应结果的时间。 - **应用**:响应时间是评价系统性能不可或缺的一个指标,它的数值大小直接反映了系统的快慢。在处理高并发的情况下,响应时间可能会因为系统资源的限制而有所增长。因此,根据业务需求和用户体验要求,设定一个合理的响应时间目标是非常重要的。例如,在一些在线游戏系统中,响应时间被严格要求在100毫秒以内,超出这个阈值可能会影响到玩家的游戏体验。 ### 吞吐量 - **定义**:吞吐量是衡量系统性能的一个综合指标,表示系统在一段给定时间内成功处理的请求总数。这个指标通常按照每秒处理的请求数来计算,与请求对CPU的消耗、外部接口、IO等紧密相关。 - **应用**:吞吐量的大小直接反映了系统的承压能力。单个请求对CPU消耗越高,外部系统接口、IO速度越慢,系统吞吐能力就越低;反之则越高。因此,在优化系统性能时,需要综合考虑这些因素对吞吐量的影响。 ### 指标间的关系 - **QPS(TPS)与并发数、响应时间的关系**:QPS(TPS)等于并发数除以平均响应时间。这个公式可以帮助我们理解系统性能的不同方面是如何相互影响的。例如,如果系统需要处理更多的并发请求(即增加并发数),那么可能需要通过优化响应时间(即减少处理时间和等待时间)来保持或提高QPS(TPS)。 - **吞吐量与QPS(TPS)、并发数、响应时间的关系**:吞吐量的大小与QPS(TPS)、并发数以及响应时间都有关系。一般来说,QPS(TPS)越高、并发数越大、响应时间越短,系统的吞吐量就越大。 综上所述,QPS、TPS、RT以及吞吐量是高并发性能评估中的关键指标,它们各自具有不同的含义和用途,并且相互之间存在密切的关系。在优化系统性能时,需要综合考虑这些指标的影响,以达到最佳的性能表现 ## 谈谈为什么要限流,有哪些限流方案? ### 一、为什么要限流? 在互联网系统中,限流是保障服务稳定性和可用性的关键策略,其核心目标是通过对流量进行控制,避免系统因过载而崩溃。具体原因如下: #### 1. **应对突发流量冲击** - **场景**:电商大促、热点事件曝光、明星官宣等场景下,短时间内会产生海量用户请求(如每秒数万甚至数十万次访问)。 - **风险**:若系统未做限流,可能因资源(如服务器 CPU、内存、数据库连接数)被瞬间耗尽而瘫痪,导致所有用户无法访问。 - **案例**:某电商平台在 “双 11” 期间通过限流保护核心交易链路,确保支付功能正常,而非核心页面(如用户评价)则暂时降级响应。 #### 2. **保护系统资源** - **资源有限性**:服务器、数据库、缓存等基础设施的处理能力存在上限。例如,数据库每秒最多支持 1000 次写入操作,若请求量超过此阈值,会导致响应延迟飙升甚至服务中断。 - **防止级联故障**:单个服务过载可能引发连锁反应。例如,订单服务崩溃会导致库存服务、支付服务等上下游依赖系统被大量无效请求阻塞,最终引发整个系统架构的雪崩。 #### 3. **保障核心服务可用性** - **流量分层策略**:通过限流可优先保障核心功能(如用户登录、交易支付)的稳定运行,牺牲部分非核心功能(如用户积分查询、广告加载)的体验。 - **公平性原则**:避免少数用户或请求占用过多资源,确保大多数用户能获得基本可用的服务。 #### 4. **防御恶意攻击** - **拒绝服务攻击(DDoS)**:黑客可能通过大量伪造请求耗尽系统资源,限流可通过识别异常流量模式(如同一 IP 短时间内高频请求),将攻击流量阻挡在系统之外。 - **刷票 / 薅羊毛**:在抢购、抽奖等场景中,限流可防止脚本或机器人通过高频请求抢占资源,保障活动公平性。 ### 二、常见限流方案 #### 1. **基于流量控制的方案** | **方案 ** | **原理** | **适用场景** | **工具 / 实现** | | ------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------- | ---------------------------------------------------------- | | **计数器限流** | 在固定时间窗口内统计请求次数,超过阈值则拒绝后续请求(如 “每分钟最多 1000 次”)。 | 简单场景下的粗粒度限流。 | 内存变量计数(如 Java 中的 AtomicInteger)、Redis 缓存。 | | **漏桶算法** | 请求先进入漏桶(固定容量队列),漏桶以恒定速率处理请求,超出容量的请求被拒绝。 | 需平滑流量、防止突发流量的场景。 | Guava 库中的 RateLimiter(基于漏桶变种)。 | | **令牌桶算法** | 系统以恒定速率生成令牌存入令牌桶(固定容量),请求需获取令牌才能被处理,无令牌则拒绝。 | 允许一定突发流量,但整体控制速率。 | Guava RateLimiter、Nginx 的 limit_req 模块(基于令牌桶)。 | #### 2. **基于资源的限流方案** - **连接数限流** - 限制服务器同时处理的连接数(如 Tomcat 的 maxThreads 参数),或数据库连接池的最大连接数(如 HikariCP 的 maxPoolSize)。 - **场景**:防止大量并发请求耗尽 TCP 连接资源。 - **CPU / 内存限流** - 通过操作系统或容器(如 Docker)限制进程的 CPU 使用率和内存占用,避免单个服务占用过多资源影响其他服务。 - **工具**:Linux 的 cgroups、Kubernetes 的资源配额(Resource Quota)。 #### 3. **分布式限流方案** - **基于 Redis 的全局限流** - 使用 Redis 的原子操作(如 INCR、SETNX)统计分布式系统中的全局请求量,结合 Lua 脚本实现复杂限流逻辑(如滑动窗口)。 - **示例**:限制某个用户 ID 每分钟最多发起 10 次请求。 - **网关层限流** - 在 API 网关(如 Spring Cloud Gateway、Nginx、Kong)中统一处理限流,对不同服务设置独立的限流规则。 - **优势**:集中管理限流策略,避免重复开发;支持按 IP、用户、接口等维度限流。 #### 4. **自适应限流方案** - **动态调整阈值** - 通过监控系统指标(如 CPU 负载、响应时间、错误率)自动调整限流阈值。例如,当服务器 CPU 使用率超过 80% 时,自动降低限流阈值;负载下降后,逐步恢复阈值。 - **工具**:Prometheus+Grafana 监控,结合开源框架(如阿里巴巴 Sentinel、Hystrix)实现动态限流。 - **流量预测限流** - 基于历史流量数据预测未来请求量,提前调整限流策略。例如,根据往年 “双 11” 的流量曲线,在峰值到来前预设更严格的限流规则。 #### 5. **其他限流策略** - **黑白名单** - 白名单内的请求(如内部测试 IP)不受限流限制;黑名单内的请求(如攻击 IP)直接拒绝。 - **场景**:防御 DDoS 攻击或保障特权用户体验。 - **请求优先级队列** - 将请求按优先级分级(如核心交易请求优先级高于查询请求),优先处理高优先级请求,低优先级请求在资源紧张时被丢弃。 - **实现**:通过消息队列(如 RabbitMQ 的优先级队列)或线程池的优先级调度实现。 ### 三、限流方案选择建议 1. **简单场景**:优先使用计数器或 Guava 的 RateLimiter,快速实现单机限流。 2. **分布式场景**:采用 Redis+Lua 或网关层限流(如 Nginx limit_req),确保全局流量控制。 3. **复杂业务**:结合动态调整和流量预测,使用 Sentinel 等专业框架,支持熔断、降级等进阶功能。 4. **微服务架构**:在 API 网关和各服务节点分别设置限流,形成多层防护体系。 **核心原则**:限流需与系统架构深度结合,提前通过压测验证限流规则的有效性,并预留一定的容灾空间,避免因限流过度导致服务不可用。 ## 集群高并发环境下如何保证分布式唯一全局ID生成 > [分布式ID生成方案总结整理](https://blog.csdn.net/u014427391/article/details/127408363) > > [分布式之分布式ID篇:UUID、雪花算法、ID生成器、号段模式](https://juejin.cn/post/7406997325716291595) 在集群高并发环境下,保证分布式唯一全局ID的生成是一个至关重要的问题。以下是一些常见的解决方案: ### 1. UUID(Universally Unique Identifier) - **原理**:UUID是一个128位的全局唯一标识符,可以在不同的计算机和时间上生成。UUID的生成基于MAC地址、时间戳等信息,因此可以保证在分布式环境下的唯一性。 - **优点**:实现简单,无需网络交互,保证了ID的全球唯一性。 - **缺点**:通常不能保证顺序性,ID较长,可能导致存储和索引效率低下。 ### 2. ZooKeeper序列节点 - **原理**:ZooKeeper是一个分布式协调服务,可以用于生成分布式唯一序列节点。每个节点在ZooKeeper上创建一个临时有序节点,节点的名称就可以作为唯一ID。 - **优点**:提供了一种分布式环境下生成唯一ID的机制。 - **缺点**:需要维护ZooKeeper的稳定性和性能,并且可能会对ZooKeeper集群施加一定的压力。 ### 3. 数据库自增主键 - **原理**:在分布式环境中,可以使用数据库的自增主键来生成唯一ID。每个节点将ID的生成请求发送到中央数据库,数据库逐个分配唯一的ID,并将其返回给节点。 - **优点**:简单可靠,保证顺序性。 - **缺点**:可能成为系统的单点故障,对数据库有较高的依赖。在高并发环境下,可能成为性能瓶颈。 ### 4. 雪花算法(Snowflake) - **原理**:雪花算法是Twitter开源的一种分布式ID生成算法。它使用了一个64位的整数,将整数的各个位段分配给不同的组成部分,包括时间戳、机器ID和序列号。通过合理配置这些部分,可以在分布式系统中生成唯一ID。 - **优点**: - ID有时间顺序,长度适中,生成速度快。 - 可以通过机器ID和序列号保证分布式环境下的唯一性。 - **缺点**:对系统时钟有依赖,时钟回拨会导致ID冲突。但这种情况在实际应用中较为罕见,因为现代计算机系统的时钟通常是非常稳定的。 ### 5. MongoDB的ObjectId算法 - **原理**:MongoDB的ObjectId算法是一种基于时间的UUID实现。它由时间信息、机器编号以及随机数构成。时间戳精确到秒级,机器标识通过hash算法得出,剩余的字节组合起来用于避免碰撞情况。 - **优点**:采用了一种随机方式,同时采用基于时间的策略保证ID的顺序性。使用简单,每台MongoDB服务器都能通过自己的IP地址获得唯一性。 - **缺点**:在分布式环境中,需要确保机器编号的唯一性。 ### 6. Redis实现分布式ID生成 - **原理**:利用Redis的原子操作(如INCR和INCRBY命令)来生成唯一的递增数值。在分布式环境中,可以部署多个Redis实例。每个实例可以独立生成ID,或者通过配置不同的起始值和步长来确保ID的全局唯一性。 - **优点**: - 快速、简单且易于扩展。 - 支持高并发环境。 - **缺点**:依赖于外部服务(Redis),需要管理和维护额外的基础设施。 在实际应用中,可以根据系统的具体需求和环境来选择合适的分布式唯一全局ID生成方案。例如,如果系统对ID的顺序性有较高要求,可以考虑使用雪花算法或MongoDB的ObjectId算法;如果系统对性能有较高要求,并且可以接受ID的无序性,可以考虑使用UUID或Redis实现分布式ID生成。同时,为了保证生成的ID的唯一性和高效性,建议对ID生成的算法和相关组件进行充分的测试和评估。 # ElasticSearch > [参考一](https://juejin.cn/post/7125689377699102756?searchId=20250616142310069732446029BF25F367#heading-19) > > [参考二](https://juejin.cn/post/7143048543991758878?searchId=20250616142310069732446029BF25F367) > > [参考三](https://juejin.cn/post/7372393680597221402?searchId=20250616142310069732446029BF25F367#heading-9) ## ElasticSearch是什么?应用场景是什么? Elasticsearch是一个分布式的、开源的、实时的搜索和分析引擎,它是基于Apache Lucene构建的,旨在提供快速、可扩展、高性能的搜索解决方案。以下是对Elasticsearch及其应用场景的详细介绍: ### 一、Elasticsearch概述 1. 分布式特性 - Elasticsearch具有分布式特性,允许多台服务器协同工作,每台服务器可以运行多个Elasticsearch实例。 - 单个Elasticsearch实例称为一个节点(Node),一组节点构成一个集群(Cluster)。 2. 数据类型支持 - Elasticsearch支持多种数据格式,包括文本、数字、地理位置等。 - 它提供了灵活的查询语言来满足各种搜索需求,如模糊查询、范围查询、正则表达式查询等。 3. 倒排索引结构 - Elasticsearch的核心是基于Apache Lucene的倒排索引结构。 - 倒排索引通过列出在所有文档中出现的每个特有词汇,并找到包含每个词汇的全部文档,从而实现快速全文搜索。 4. 开源社区支持 - Elasticsearch是开源软件,拥有活跃的社区和丰富的资源。 - 社区提供了大量的插件和工具来满足不同场景的需求,如安全、报告、监控、工作流自动化、机器学习等。 ### 二、Elasticsearch的应用场景 1. 全文搜索 - Elasticsearch以其强大、可扩展和快速的搜索功能,在全文搜索场景中表现出色。 - 它允许用户以近乎实时的响应执行复杂的查询,并支持大规模文本搜索,如网站搜索、产品目录搜索等。 2. 实时分析 - Elasticsearch能够实时执行分析,适用于跟踪实时数据(如用户活动、交易或传感器输出)的仪表盘。 - 它可以帮助企业实时监控和分析应用程序的性能数据,以及用户行为数据等。 3. 机器学习 - 通过在X-Pack中添加机器学习功能,Elasticsearch可以自动检测数据中的异常、模式和趋势。 - 这有助于企业发现数据中的潜在价值,并做出相应的决策。 4. 地理数据应用 - Elasticsearch通过地理空间索引和搜索功能支持地理数据。 - 这对于需要管理和可视化地理信息的应用程序(如地图和基于位置的服务)非常有用。 - 它使执行邻近搜索和基于位置的数据可视化成为可能。 5. 日志和事件数据分析 - 企业使用Elasticsearch来聚合、监控和分析来自各种来源的日志和事件数据。 - 它是ELK堆栈(Elasticsearch、Logstash、Kibana)的关键组件,常用于管理系统和应用程序日志以识别问题和监控系统运行状况。 6. 安全信息和事件管理(SIEM) - Elasticsearch可用作SIEM工具,帮助企业实时分析安全事件。 - 这有助于企业及时发现并应对潜在的安全威胁。 7. 文档存储与检索 - Elasticsearch还可以作为非结构化或半结构化文档(如PDF、Word文档)的存储和检索解决方案。 - 这使得企业可以更方便地管理和访问这些文档。 8. 元数据和内容管理 - 在内容管理系统中,Elasticsearch用于管理大量内容的元数据搜索和检索。 - 这有助于企业更有效地组织和管理其内容资源。 综上所述,Elasticsearch具有广泛的应用场景和强大的功能特性。它不仅可以用于全文搜索和实时分析,还可以支持地理数据应用、日志和事件数据分析、安全信息和事件管理等。同时,作为ELK堆栈的关键组件之一,Elasticsearch在数据管理和分析方面发挥着重要作用。 ## elasticsearch 的倒排索引是什么 ## ELK(了解) [ELK入门——ELK详细介绍(ELK概念和特点、Elasticsearch/Logstash/beats/kibana安装及使用介绍、插件介绍)](https://blog.csdn.net/Netceor/article/details/114653892) # Linux常用命令 Linux 常用命令涵盖文件目录操作、文件查找、文件打包上传下载、文件权限设置、磁盘存储、性能监控优化、网络命令及其他命令等方面。 ## 一、文件目录操作 ### 1. ls 命令 ls 命令不仅可以查看 linux 文件夹包含的文件而且可以查看文件权限(包括目录、文件夹、文件权限)查看目录信息等等。 #### 命令格式 ls [选项][目录名] #### 常用参数 - -l :列出长数据串,包含文件的属性与权限数据等 - -a :列出全部的文件,连同隐藏文件(开头为.的文件)一起列出来(常用) - -d :仅列出目录本身,而不是列出目录的文件数据 - -h :将文件容量以较易读的方式(GB,kB等)列出来 - -R :连同子目录的内容一起列出(递归列出),等于该目录下的所有文件都会显示出来 #### 使用实例 1.列出 home 目录下的所有文件和目录的详细资料。 ```shell ls -a -l /home ls -al /home ``` 2.列出当前目录下所有以"d"开头的文件目录详情内容。 ```shell ls -l d* ``` ### 2.cd命令 最基本的命令语句,其他的命令语句要进行操作,都是建立在使用 cd 命令上的。用于切换当前目录至dirName。 #### 命令格式 cd [目录名] #### 操作案例 1.从当前目录进入系统根目录。 ```shell cd / ``` 2.跳转到 home/Code 目录。 ```shell cd /home/Code ``` ### 3.pwd 命令 查看"当前工作目录"的完整路径。 #### 命令格式 pwd [选项] #### 常用参数 - -P :显示实际物理路径,而非使用连接(link)路径 - -L :当目录为连接路径时,显示连接路径 #### 操作案例 1.显示当前所在路径。 ```shell pwd ``` ### 4.mkdir 命令 用来创建指定的名称的目录,要求创建目录的用户在当前目录中具有写权限,并且指定的目录名不能是当前目录中已有的目录。 #### 命令格式 mkdir [选项] 目录 #### 常用参数 - -m, --mode=模式,设定权限<模式> (类似 chmod),而不是 rwxrwxrwx 减 umask - -p, --parents 可以是一个路径名称。此时若路径中的某些目录尚不存在,加上此选项后,系统将自动建立好那些尚不存在的目录,即一次可以建立多个目录; - -v, --verbose 每次创建新目录都显示信息 - --help 显示此帮助信息并退出 - --version 输出版本信息并退出 #### 使用实例 1.创建一个空目录。 ```shell mkdir test ``` 2.递归创建多个目录。 ```shell mkdir test/test1 ``` 3.创建权限为777的目录。 ```shell mkdir -m 777 test2 ``` 4.创建目录都显示信息。 ```shell mkdir -v test4 ``` ### 5.rm 命令 删除一个目录中的一个或多个文件或目录,如果没有使用- r选项,则rm不会删除目录。如果使用 rm 来删除文件,通常仍可以将该文件恢复原状。 #### 命令格式 rm [选项] 文件 #### 常用参数 - -f, --force 忽略不存在的文件,从不给出提示。 - -i, --interactive 进行交互式删除 - -r, -R, --recursive 指示rm将参数中列出的全部目录和子目录均递归地删除。 - -v, --verbose 详细显示进行的步骤 - --help 显示此帮助信息并退出 - --version 输出版本信息并退出 #### 使用实例 1.删除文件 test.txt,系统会提示是否删除。 ```shell rm test.txt ``` 2.强制删除 test.txt,系统不再提示。 ```shell rm -f test.txt ``` 3.将 test 子目录及目录中所有档案删除。 ```shell rm -r test ``` ### 6.rmdir 命令 该命令从一个目录中删除一个或多个子目录项,删除某目录时也必须具有对父目录的写权限。 #### 命令格式 rmdir [选项] 目录 #### 常用参数 - p 递归删除目录dirname,当子目录删除后其父目录为空时,也一同被删除。如果整个路径被删除或者由于某种原因保留部分路径,则系统在标准输出上显示相应的信息。 - -v, --verbose 显示指令执行过程 #### 使用实例 1.删除空目录 test1,非空目录无法删除。 ```shell rmdir test1 ``` 2.当子目录被删除后使它也成为空目录的话,则顺便一并删除 ```shell rmdir -p test2 # test 目录下仅有 test2 ``` ### 7. mv 命令 可以用来移动文件或者将文件改名(move (rename) files)。当第二个参数类型是文件时,mv命令完成文件重命名。当第二个参数是已存在的目录名称时,源文件或目录参数可以有多个,mv命令将各参数指定的源文件均移至目标目录中。 #### 命令格式 mv [选项] 源文件或目录 目标文件或目录 #### 常用参数 - -b :若需覆盖文件,则覆盖前先行备份 - -f :force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖 - -i :若目标文件 (destination) 已经存在时,就会询问是否覆盖 - -u :若目标文件已经存在,且 source 比较新,才会更新(update) - -t : --target-directory=DIRECTORY move all SOURCE arguments into DIRECTORY,即指定mv的目标目录,该选项适用于移动多个源文件到一个目录的情况,此时目标目录在前,源文件在后 #### 使用实例 1.将 test1.txt 重命名为 test2.txt。 ```shell mv test1.txt test2.txt ``` 2.移动文件 test1.txt 到目录 test2 ```shell mv test1.txt test2 ``` 3.将文件 test1.txt、test2.txt、test3.txt 移动到目录 test3。 ```shell mv test1.txt test2.txt test3.txt test3 ``` ### 8.cp 命令 将源文件复制至目标文件,或将多个源文件复制至目标目录。 #### 命令格式 cp [选项] 源文件 目录 或 cp [选项] -t 目录 源文件 #### 常用参数 - -t --target-directory 指定目标目录 - -i --interactive 覆盖前询问(使前面的 -n 选项失效) - -n --no-clobber 不要覆盖已存在的文件(使前面的 -i 选项失效) - -f --force 强行复制文件或目录,不论目的文件或目录是否已经存在 - -u --update 使用这项参数之后,只会在源文件的修改时间较目的文件更新时,或是对应的目的文件并不存在,才复制文件 #### 使用实例 1.复制文件 test1.txt 到 test1 目录 ```shell cp test1.txt test1 # 若文件存在,会提示是否覆盖。若不存在直接完成复制 ``` 1. 复制 test1 整个目录到 test2 ```shell cp -a test1 test2 ``` ### 9. touch 命令 touch命令参数可更改文档或目录的日期时间,包括存取时间和更改时间。 #### 命令格式 touch [选项] 文件 #### 常用参数 - -a 或--time=atime或--time=access或--time=use  只更改存取时间 - -c 或--no-create  不建立任何文档 - -d  使用指定的日期时间,而非现在的时间 - -f  此参数将忽略不予处理,仅负责解决BSD版本touch指令的兼容性问题 - -m 或--time=mtime或--time=modify  只更改变动时间 - -r  把指定文档或目录的日期时间,统统设成和参考文档或目录的日期时间相同 -t  使用指定的日期时间,而非现在的时间 #### 使用实例 1.创建不存在的文件test.txt ```shell touch test.txt ``` 2.更新 test.txt 的实践和 test1.txt 时间戳相同 ```shell touch -r test.txt test1.txt ``` ### 10.cat 命令 用来显示文件内容,或者将几个文件连接起来显示,或者从标准输入读取内容并显示,它常与重定向符号配合使用。 #### 命令格式 cat [选项] [文件] #### 常用参数 - -A, --show-all 等价于 -vET - -b, --number-nonblank 对非空输出行编号 - -e 等价于 -vE - -E, --show-ends 在每行结束处显示 $ - -n, --number 对输出的所有行编号,由1开始对所有输出的行数编号 - -s, --squeeze-blank 有连续两行以上的空白行,就代换为一行的空白行 - -t 与 -vT 等价 - -T, --show-tabs 将跳格字符显示为 ^I - -u (被忽略) - -v, --show-nonprinting 使用 ^ 和 M- 引用,除了 LFD 和 TAB 之外 #### 使用实例 1.把 test.log 的文件内容加上行号后输入 test1.log 这个文件里。 ```shell cat -n test.log test1.log ``` 1. 将 test.log 的文件内容反向显示。 ```shell tac test.log ``` ### 11. nl 命令 输出的文件内容自动的加上行号!其默认的结果与 cat -n 有点不太一样, nl 可以将行号做比较多的显示设计,包括位数与是否自动补齐 0 等等的功能。 #### 命令格式 nl [选项] [文件] #### 常用参数 - -b :指定行号指定的方式,主要有两种: - -b a :表示不论是否为空行,也同样列出行号(类似 cat -n) - -b t :如果有空行,空的那一行不要列出行号(默认值) - -n :列出行号表示的方法,主要有三种: - -n ln :行号在萤幕的最左方显示 - -n rn :行号在自己栏位的最右方显示,且不加 0 - -n rz :行号在自己栏位的最右方显示,且加 0 - -w :行号栏位的占用的位数 #### 使用实例 1. 用 nl 列出 test.log 的内容。 ```shell nl test.log ``` 1. 用 nl 列出 test.log 的内容,空本行也加上行号。 ```shell nl -b a test.log ``` ### 12.more 命令 more 命令和 cat 的功能一样都是查看文件里的内容,但有所不同的是more可以按页来查看文件的内容,还支持直接跳转行等功能。 #### 命令格式 more [-dlfpcsu ] [-num ] [+/ pattern] [+ linenum] [file ... ] #### 常用参数 - +n 从笫n行开始显示 - -n 定义屏幕大小为n行 - +/pattern 在每个档案显示前搜寻该字串(pattern),然后从该字串前两行之后开始显示 - -c 从顶部清屏,然后显示 - -d 提示“Press space to continue,’q’ to quit(按空格键继续,按q键退出)”,禁用响铃功能 - -l 忽略Ctrl+l(换页)字符 - -p 通过清除窗口而不是滚屏来对文件进行换页,与-c选项相似 - -s 把连续的多个空行显示为一行 - -u 把文件内容中的下画线去掉 #### 操作指令 - Enter:向下n行,需要定义。默认为1行 - Ctrl+F:向下滚动一屏 - 空格键:向下滚动一屏 - Ctrl+B:返回上一屏 - = :输出当前行的行号 - :f :输出文件名和当前行的行号 - V :调用vi编辑器 - !命令 :调用Shell,并执行命令 - q :退出more #### 使用实例 1.显示文件 test.log 第3行起内容。 ``` more +3 test.log ``` 2.从文件 test.log 查找第一个出现“day3”字符串的行,并从该处前2行开始显示输出。 ``` more +/day3 test.log ``` 1. 设置每屏显示行数 ``` more -5 test.log ``` ### 13. less 命令 less 与 more 类似,但使用 less 可以随意浏览文件,而 more 仅能向前移动,却不能向后移动,而且 less 在查看之前不会加载整个文件。 #### 命令格式 less [参数] 文件 #### 常用参数 - -b <缓冲区大小> 设置缓冲区的大小 - -e 当文件显示结束后,自动离开 - -f 强迫打开特殊文件,例如外围设备代号、目录和二进制文件 - -g 只标志最后搜索的关键词 - -i 忽略搜索时的大小写 - -m 显示类似more命令的百分比 - -N 显示每行的行号 - -o <文件名> 将less 输出的内容在指定文件中保存起来 - -Q 不使用警告音 - -s 显示连续空行为一行 - -S 行过长时间将超出部分舍弃 - -x <数字> 将“tab”键显示为规定的数字空格 #### 操作命令 - /字符串:向下搜索“字符串”的功能 - ?字符串:向上搜索“字符串”的功能 - n:重复前一个搜索(与 / 或 ? 有关) - N:反向重复前一个搜索(与 / 或 ? 有关) - b 向后翻一页 - d 向后翻半页 - h 显示帮助界面 - Q 退出less 命令 - u 向前滚动半页 - y 向前滚动一行 - 空格键 滚动一行 - 回车键 滚动一页 - [pagedown]: 向下翻动一页 - [pageup]: 向上翻动一页 #### 使用实例 1.查看文件 test.log。 ```shell less test.log ``` ### 14. head 命令 head 用来显示档案的开头至标准输出中,默认 head 命令打印其相应文件的开头 10 行。 #### 命令格式 head [参数] [文件] #### 常用参数 - -q 隐藏文件名 - -v 显示文件名 - -c<字节> 显示字节数 - -n<行数> 显示的行数 #### 使用实例 1.显示文件 test.log 的前 5 行 ```shell head -n 5 test.log ``` 2.显示文件 test.log 前 20 个字节 ```shell head -c 20 test.log ``` ### 15.tail 命令 显示指定文件末尾内容,不指定文件时,作为输入信息进行处理。常用查看日志文件。 #### 命令格式 tail [必要参数] [选择参数] [文件] #### 常用参数 - -f 循环读取 - -q 不显示处理信息 - -v 显示详细的处理信息 - -c<数目> 显示的字节数 - -n<行数> 显示行数 - --pid=PID 与-f合用,表示在进程ID,PID死掉之后结束. - -q, --quiet, --silent 从不输出给出文件名的首部 - -s, --sleep-interval=S 与-f合用,表示在每次反复的间隔休眠S秒 #### 使用实例 1.显示文件 test.log 最后 5 行内容。 ```shell tail -n 5 test.log ``` 2.循环查看文件内容 ```shell tail -f test.log ``` ## 二、文件查找 ### 16.which 命令 which指令会在PATH变量指定的路径中,搜索某个系统命令的位置,并且返回第一个搜索结果。 #### 命令格式 which 可执行文件名称 #### 常用参数 - -n  指定文件名长度,指定的长度必须大于或等于所有文件中最长的文件名 - -p  与-n参数相同,但此处的包括了文件的路径 - -w  指定输出时栏位的宽度 - -V  显示版本信息 #### 使用实例 1.查找文件、显示命令路径。 ```shell which pwd ``` 1. 用 which 去找出 which ```shell which which ``` ### 17.whereis 命令 whereis命令是定位可执行文件、源代码文件、帮助文件在文件系统中的位置。 #### 命令格式 whereis [-bmsu] [BMS 目录名 -f ] 文件名 #### 常用参数 - -b 定位可执行文件 - -m 定位帮助文件 - -s 定位源代码文件 - -u 搜索默认路径下除可执行文件、源代码文件、帮助文件以外的其它文件 - -B 指定搜索可执行文件的路径 - -M 指定搜索帮助文件的路径 - -S 指定搜索源代码文件的路径 #### 使用实例 1.将和 svn 文件相关的文件都查找出来。 ```shell whereis svn ``` 2.只将二进制文件查找出来。 ```shell whereis -b svn ``` ### 18.locate 命令 可以很快速的搜寻档案系统内是否有指定的档案。 #### 命令格式 Locate [选择参数] [样式] #### 常用参数 - -e 将排除在寻找的范围之外。 - -1 如果 是 1.则启动安全模式。在安全模式下,使用者不会看到权限无法看到 的档案。这会始速度减慢,因为 locate 必须至实际的档案系统中取得档案的 权限资料。 - -f 将特定的档案系统排除在外,例如我们没有到理要把 proc 档案系统中的档案 放在资料库中。 - -q 安静模式,不会显示任何错误讯息。 - -n 至多显示 n个输出。 - -r 使用正规运算式 做寻找的条件。 - -o 指定资料库存的名称。 - -d 指定资料库的路径 #### 使用实例 1.查找和 pwd 相关的所有文件。 ```shell locate pwd ``` 1. 搜索etc 目录下,所有以 m 开头的文件。 ```shell locate /etc/m ``` ### 19. find 命令 主要作用是沿着文件层次结构向下遍历,匹配符合条件的文件,并执行相应的操作。 #### 命令格式 find [选项] [搜索路径] [表达式] #### 常用参数 - -print find 命令将匹配的文件输出到标准输出 - -exec find 命令对匹配的文件执行该参数所给出的 - shell 命令 - -name 按照文件名查找文件 - -type 查找某一类型的文件 #### 使用实例 1.打印当前目录文件目录列表。 ```shell find . -print ``` 2.打印当前目录下所有不以.txt 结尾的文件名。 ```shell find . ! -name "*.txt" ``` 3.打印当前目录下所有权限为 777 的 php 文件。 ```shell find . -type f -name "*.php" -perm 777 ``` 4.找到当前目录下所有 php 文件,并显示其详细信息。 ```shell find . -name "*.php" -exec ls -l {} \; ``` 5.查找当前目录下所有 c 代码文件,统计总行数。 ```shell find . -type f -name "*.c" | xargs wc -l ``` > xargs 命令可以从标准输入接收输入,并把输入转换为一个特定的参数列表。 命令格式: command | xargs [选项] [command] xargs 命令应该紧跟在管道操作符之后,因为它以标准输入作为主要的源数据流。 常用参数 > > - -n 指定每行最大的参数数量 > - -d 指定分隔符 ## 三、文件打包上传和下载 ### 20.tar 命令 用来压缩和解压文件。tar本身不具有压缩功能。他是调用压缩功能实现的。 #### 命令格式 tar [必要参数] [选择参数] [文件] #### 常用参数 必要参数: - -A 新增压缩文件到已存在的压缩 - -B 设置区块大小 - -c 建立新的压缩文件 - -d 记录文件的差别 - -r 添加文件到已经压缩的文件 - -u 添加改变了和现有的文件到已经存在的压缩文件 - -x 从压缩的文件中提取文件 - -t 显示压缩文件的内容 - -z 支持gzip解压文件 - -j 支持bzip2解压文件 - -Z 支持compress解压文件 - -v 显示操作过程 - -l 文件系统边界设置 - -k 保留原有文件不覆盖 - -m 保留文件不被覆盖 - -W 确认压缩文件的正确性 可选参数: - -b 设置区块数目 - -C 切换到指定目录 - -f 指定压缩文件 - --help 显示帮助信息 - --version 显示版本信息 #### 使用实例 1.将文件打全部打包成tar包。 ```shell tar -cvf test.tar test.log # 仅打包,不压缩! tar -zcvf test.tar.gz test.log # 打包后,以 gzip 压缩 tar -zcvf test.tar.bz2 test.log # 打包后,以 bzip2 压缩 ``` 2.将 tar 包解压缩 ```shell tar -zxvf test.tar.gz ``` ### 21.gzip 命令 使用广泛的压缩程序,文件经它压缩过后,其名称后面会多出".gz"的扩展名。 #### 命令格式 gzip [参数] [文件或者目录] #### 常用参数 - -a或--ascii  使用ASCII文字模式。 - -c或--stdout或--to-stdout  把压缩后的文件输出到标准输出设备,不去更动原始文件。 - -d或--decompress或----uncompress  解开压缩文件。 - -f或--force  强行压缩文件。不理会文件名称或硬连接是否存在以及该文件是否为符号连接。 - -h或--help  在线帮助。 #### 使用实例 1.把 test1 目录下的每个文件压缩成.gz 文件。 ```shell test6 $ gzip * ``` ## 四、文件权限设置 ### 22.chmod 命令 用于改变linux系统文件或目录的访问权限。 #### 命令格式 chmod [-cfvR] [--help] [--version] mode file #### 常用参数 必要参数: - -c 当发生改变时,报告处理信息 - -f 错误信息不输出 - -R 处理指定目录以及其子目录下的所有文件 - -v 运行时显示详细处理信息 - 选择参数: - --reference=<目录或者文件> 设置成具有指定目录或者文件具有相同的权限 - --version 显示版本信息 - <权限范围>+<权限设置> 使权限范围内的目录或者文件具有指定的权限 - <权限范围>-<权限设置> 删除权限范围的目录或者文件的指定权限 - <权限范围>=<权限设置> 设置权限范围内的目录或者文件的权限为指定的值 权限范围: - u :目录或者文件的当前的用户 - g :目录或者文件的当前的群组 - o :除了目录或者文件的当前用户或群组之外的用户或者群组 - a :所有的用户及群组 权限代号: - r:读权限,用数字4表示 - w:写权限,用数字2表示 - x:执行权限,用数字1表示 - -:删除权限,用数字0表示 #### 使用实例 1.增加文件所有用户组可执行权限 ```shell chmod a+x test.log ``` 1. 删除所有用户的可执行权限 ```shell chmod a-x test.log ``` ### 23.chgrp 命令 可采用群组名称或群组识别码的方式改变文件或目录的所属群组。 #### 命令格式 chgrp [选项] [组] [文件] #### 常用参数 必要参数: - -c 当发生改变时输出调试信息 - -f 不显示错误信息 - -R 处理指定目录以及其子目录下的所有文件 - -v 运行时显示详细的处理信息 - --dereference 作用于符号链接的指向,而不是符号链接本身 - --no-dereference 作用于符号链接本身 选择参数: - --reference=<文件或者目录> - --help 显示帮助信息 - --version 显示版本信息 #### 使用实例 1.改变文件的群组属性 ```shell chgrp -v bin test.log ``` 2.改变文件test1.log 的群组属性,使得文件test1.log的群组属性和参考文件test.log的群组属性相同 ```shell chgrp --reference=test.log test1.log ``` ### 24.chown 命令 通过chown改变文件的拥有者和群组。 #### 命令格式 chown [选项] [所有者] [:[组]] 文件 #### 常用参数 必要参数: - -c 显示更改的部分的信息 - -f 忽略错误信息 - -h 修复符号链接 - -R 处理指定目录以及其子目录下的所有文件 - -v 显示详细的处理信息 - -deference 作用于符号链接的指向,而不是链接文件本身 选择参数: - --reference=<目录或文件> 把指定的目录/文件作为参考,把操作的文件/目录设置成参考文件/目录相同拥有者和群组 - --from=<当前用户:当前群组> 只有当前用户和群组跟指定的用户和群组相同时才进行改变 - --help 显示帮助信息 - --version 显示版本信息 #### 使用实例 1.改变拥有者和群组 ```shell chown mail:mail test.log ``` ## 五、磁盘存储 ### 25. df 命令 显示指定磁盘文件的可用空间。 #### 命令格式 df [选项] [文件] #### 常用参数 必要参数: - -a 全部文件系统列表 - -h 方便阅读方式显示 - -H 等于“-h”,但是计算式,1K=1000,而不是1K=1024 - -i 显示inode信息 - -k 区块为1024字节 - -l 只显示本地文件系统 - -m 区块为1048576字节 - --no-sync 忽略 sync 命令 - -P 输出格式为POSIX - --sync 在取得磁盘信息前,先执行sync命令 - -T 文件系统类型 选择参数: - --block-size=<区块大小> 指定区块大小 - -t<文件系统类型> 只显示选定文件系统的磁盘信息 - -x<文件系统类型> 不显示选定文件系统的磁盘信息 - --help 显示帮助信息 - --version 显示版本信息 #### 使用实例 1.显示指定磁盘使用情况 ```shell df -t ext3 ``` ### 26. du 命令 显示每个文件和目录的磁盘使用空间。 #### 命令格式 du [选项] [文件] #### 常用参数 - -a或-all 显示目录中个别文件的大小。 - -b或-bytes 显示目录或文件大小时,以byte为单位。 -- -c或--total 除了显示个别目录或文件的大小外,同时也显示所有目录或文件的总和。 - -k或--kilobytes 以KB(1024bytes)为单位输出。 - -m或--megabytes 以MB为单位输出。 - -s或--summarize 仅显示总计,只列出最后加总的值。 - -h或--human-readable 以K,M,G为单位,提高信息的可读性。 - -x或--one-file-xystem 以一开始处理时的文件系统为准,若遇上其它不同的文件系统目录则略过。 - -L<符号链接>或--dereference<符号链接> 显示选项中所指定符号链接的源文件大小。 - -S或--separate-dirs 显示个别目录的大小时,并不含其子目录的大小。 - -X<文件>或--exclude-from=<文件> 在<文件>指定目录或文件。 - --exclude=<目录或文件> 略过指定的目录或文件。 - -D或--dereference-args 显示指定符号链接的源文件大小。 - -H或--si 与-h参数相同,但是K,M,G是以1000为换算单位。 - -l或--count-links 重复计算硬件链接的文件。 #### 使用实例 1.显示指定目录或文件所占空间 ```shell du test # 目录 du test.log # 文件 ``` ## 六、性能监控和优化命令 ### 27.top 命令 显示当前系统正在执行的进程的相关信息,包括进程ID、内存占用率、CPU占用率等。 #### 命令格式 top [参数] #### 常见参数 - -b 批处理 - -c 显示完整的治命令 - -I 忽略失效过程 - -s 保密模式 - -S 累积模式 - -i<时间> 设置间隔时间 - -u<用户名> 指定用户名 - -p<进程号> 指定进程 - -n<次数> 循环显示的次数 #### 使用实例 1. 显示进程信息。 ```shell top ``` ### 28.free 命令 显示系统使用和空闲的内存情况,包括物理内存、交互区内存(swap)和内核缓冲区内存。 #### 命令格式 free [参数] #### 常见参数 - -b  以Byte为单位显示内存使用情况 - -k  以KB为单位显示内存使用情况 - -m  以MB为单位显示内存使用情况 - -g 以GB为单位显示内存使用情况 - -o  不显示缓冲区调节列 - -s<间隔秒数>  持续观察内存使用状况 - -t  显示内存总和列。 - -V  显示版本信息。 #### 使用实例 1.显示内存情况。 ```shell free free -g #以GB为单位 free -m #以MB为单位 ``` ### 29. vmstat 用来显示虚拟内存的信息。 #### 命令格式 - vmstat [-a] [-n] [-S unit] [delay [ count]] - vmstat [-s] [-n] [-S unit] - vmstat [-m] [-n] [delay [ count]] - vmstat [-d] [-n] [delay [ count]] - vmstat [-p disk partition] [-n] [delay [ count]] - vmstat [-f] - vmstat [-V] #### 常见参数 - -a:显示活跃和非活跃内存 - -f:显示从系统启动至今的fork数量 - -m:显示slabinfo - -n:只在开始时显示一次各字段名称 - -s:显示内存相关统计信息及多种系统活动数量 - delay:刷新时间间隔。如果不指定,只显示一条结果 - count:刷新次数。如果不指定刷新次数,但指定了刷新时间间隔,这时刷新次数为无穷 - -d:显示磁盘相关统计信息 - -p:显示指定磁盘分区统计信息 - -S:使用指定单位显示。参数有 k 、K 、m 、M ,分别代表1000、1024、1000000、1048576字节(byte)。默认单位为K(1024 bytes) #### 使用实例 1.显示活跃和非活跃内存。 ```shell vmstat -a 5 5 # 5秒时间内进行5次采样 ``` ### 30.lostat 命令 通过iostat方便查看CPU、网卡、tty设备、磁盘、CD-ROM 等等设备的活动情况, 负载信息。 #### 命令格式 iostat [参数] [时间] [次数] #### 常见参数 - -C 显示CPU使用情况 - -d 显示磁盘使用情况 - -k 以 KB 为单位显示 - -m 以 M 为单位显示 - -N 显示磁盘阵列(LVM) 信息 - -n 显示NFS 使用情况 - -p[磁盘] 显示磁盘和分区的情况 - -t 显示终端和CPU的信息 - -x 显示详细信息 #### 使用实例 1.定时显示所有信息。 ```shell iostat 2 3 #每隔 2秒刷新显示,且显示3次 ``` ### 31. lsof 命令 用于查看你进程开打的文件,打开文件的进程,进程打开的端口(TCP、UDP)。 #### 命令格式 lsof [参数] [文件] #### 常见参数 - -a 列出打开文件存在的进程 - -c<进程名> 列出指定进程所打开的文件 - -g 列出GID号进程详情 - -d<文件号> 列出占用该文件号的进程 - +d<目录> 列出目录下被打开的文件 - +D<目录> 递归列出目录下被打开的文件 - -n<目录> 列出使用NFS的文件 - -i<条件> 列出符合条件的进程。(4、6、协议、:端口、 @ip ) - -p<进程号> 列出指定进程号所打开的文件 - -u 列出UID号进程详情 #### 使用实例 1.查看谁正在使用bash文件,也就是说查找某个文件相关的进程。 ``` lsof /bin/bash ``` ## 七、网络命令 ### 32.ifconfig 命令 ifconfig 命令用来查看和配置网络设备。 #### 命令格式 ifconfig [网络设备] [参数] #### 常见参数 - up 启动指定网络设备/网卡 - down 关闭指定网络设备/网卡。 - arp 设置指定网卡是否支持ARP协议 - -promisc 设置是否支持网卡的promiscuous模式,如果选择此参数,网卡将接收网络中发给它所有的数据包 - -allmulti 设置是否支持多播模式,如果选择此参数,网卡将接收网络中所有的多播数据包 - -a 显示全部接口信息 - -s 显示摘要信息(类似于 netstat -i) - add 给指定网卡配置IPv6地址 - del 删除指定网卡的IPv6地址 #### 使用实例 1.启动关闭指定网卡 ```shell ifconfig eth0 up ifconfig eth0 down ``` 2.用ifconfig修改MAC地址 ```shell ifconfig eth0 hw ether 00:AA:BB:CC:DD:EE ``` ### 33. route 命令 Route命令是用于操作基于内核ip路由表,它的主要作用是创建一个静态路由让指定一个主机或者一个网络通过一个网络接口,如eth0。 #### 命令格式 route [-f] [-p] [Command [Destination] [mask Netmask] [Gateway] [metric Metric]] [if Interface]] #### 常见参数 - -c 显示更多信息 - -n 不解析名字 - -v 显示详细的处理信息 - -F 显示发送信息 - -C 显示路由缓存 - -f 清除所有网关入口的路由表。 - -p 与 add 命令一起使用时使路由具有永久性。 - add:添加一条新路由。 - del:删除一条路由。 - -net:目标地址是一个网络。 - -host:目标地址是一个主机。 - netmask:当添加一个网络路由时,需要使用网络掩码。 - gw:路由数据包通过网关。注意,你指定的网关必须能够达到。 - metric:设置路由跳数。 - Command 指定您想运行的命令 (Add/Change/Delete/Print)。 - Destination 指定该路由的网络目标。 #### 使用实例 1.显示当前路由 ```shell route route -n ``` 2.添加网关/设置网关 ```shell route add -net 224.0.0.0 netmask 240.0.0.0 dev eth0 ``` ### 34. ping 命令 确定网络和各外部主机的状态;跟踪和隔离硬件和软件问题;测试、评估和管理网络。 #### 命令格式 ping [参数] [主机名或IP地址] #### 常见参数 - -d 使用Socket的SO_DEBUG功能 - -f 极限检测。大量且快速地送网络封包给一台机器,看它的回应 - -n 只输出数值 - -q 不显示任何传送封包的信息,只显示最后的结果 - -r 忽略普通的Routing Table,直接将数据包送到远端主机上。通常是查看本机的网络接口是否有问题 - -R 记录路由过程 - -v 详细显示指令的执行过程 - -c 数目:在发送指定数目的包后停止 - -i 秒数:设定间隔几秒送一个网络封包给一台机器,预设值是一秒送一次 -I 网络界面:使用指定的网络界面送出数据包 -l 前置载入:设置在送出要求信息之前,先行发出的数据包 -p 范本样式:设置填满数据包的范本样式 -s 字节数:指定发送的数据字节数,预设值是56,加上8字节的ICMP头,一共是64ICMP数据字节 -t 存活数值:设置存活数值TTL的大小 #### 使用实例 1. ping 网关 ```shell ping -b 192.168.120.1 ``` ### 35.traceroute 命令 让你追踪网络数据包的路由途径,预设数据包大小是40Bytes,用户可另行设置。 #### 命令格式 traceroute [参数] [主机] #### 常见参数 - -d 使用Socket层级的排错功能 - -f 设置第一个检测数据包的存活数值TTL的大小 - -F 设置勿离断位 - -g 设置来源路由网关,最多可设置8个 - -i 使用指定的网络界面送出数据包 - -I 使用ICMP回应取代UDP资料信息 - -m 设置检测数据包的最大存活数值TTL的大小 - -n 直接使用IP地址而非主机名称 - -p 设置UDP传输协议的通信端口 - -r 忽略普通的Routing Table,直接将数据包送到远端主机上 - -s 设置本地主机送出数据包的IP地址 - -t 设置检测数据包的TOS数值 - -v 详细显示指令的执行过程 - -w 设置等待远端主机回报的时间 - -x 开启或关闭数据包的正确性检验 #### 使用实例 1.traceroute 用法简单、最常用的用法 ```shell traceroute www.baidu.com ``` 1. 跳数设置 ```shell traceroute -m 10 www.baidu.com ``` ### 36.netstat 命令 用于显示与IP、TCP、UDP和ICMP协议相关的统计数据,一般用于检验本机各端口的网络连接情况。 #### 命令格式 netstat [-acCeFghilMnNoprstuvVwx] [-A<网络类型>] [--ip] #### 常见参数 - -a或–all 显示所有连线中的Socket - -A<网络类型>或–<网络类型> 列出该网络类型连线中的相关地址 - -c或–continuous 持续列出网络状态 - -C或–cache 显示路由器配置的快取信息 - -e或–extend 显示网络其他相关信息 - -F或–fib 显示FIB - -g或–groups 显示多重广播功能群组组员名单 - -h或–help 在线帮助 - -i或–interfaces 显示网络界面信息表单 - -l或–listening 显示监控中的服务器的Socket - -M或–masquerade 显示伪装的网络连线 - -n或–numeric 直接使用IP地址,而不通过域名服务器 - -N或–netlink或–symbolic 显示网络硬件外围设备的符号连接名称 - -o或–timers 显示计时器 - -p或–programs 显示正在使用Socket的程序识别码和程序名称 - -r或–route 显示Routing Table - -s或–statistice 显示网络工作信息统计表 - -t或–tcp 显示TCP传输协议的连线状况 - -u或–udp 显示UDP传输协议的连线状况 - -v或–verbose 显示指令执行过程 - -V或–version 显示版本信息 - -w或–raw 显示RAW传输协议的连线状况 - -x或–unix 此参数的效果和指定”-A unix”参数相同 - –ip或–inet 此参数的效果和指定”-A inet”参数相同 #### 使用实例 1. 列出所有端口 ```shell netstat -a ``` ### 37.telnet 命令 执行telnet指令开启终端机阶段作业,并登入远端主机。 #### 命令格式 telnet [参数] [主机] #### 常见参数 - -8 允许使用8位字符资料,包括输入与输出 - -a 尝试自动登入远端系统 - -b<主机别名> 使用别名指定远端主机名称 - -c 不读取用户专属目录里的.telnetrc文件 - -d 启动排错模式 - -e<脱离字符> 设置脱离字符 - -E 滤除脱离字符 - -f 此参数的效果和指定"-F"参数相同 #### 使用实例 1.远程服务器无法访问 ``` telnet 192.168.120.206 ``` ## 八、其他命令 ### 38.ln 命令 为某一个文件在另外一个位置建立一个同步的链接.当我们需要在不同的目录,用到相同的文件时,我们不需要在每一个需要的目录下都放一个必须相同的文件,我们只要在某个固定的目录,放上该文件,然后在 其它的目录下用ln命令链接(link)它就可以,不必重复的占用磁盘空间。 #### 命令格式 ln [参数] [源文件或目录] [目标文件或目录] #### 常用参数 必要参数: - -b 删除,覆盖以前建立的链接 - -d 允许超级用户制作目录的硬链接 - -f 强制执行 - -i 交互模式,文件存在则提示用户是否覆盖 - -n 把符号链接视为一般目录 - -s 软链接(符号链接) - -v 显示详细的处理过程 选择参数: - -S “-S<字尾备份字符串> ”或 “--suffix=<字尾备份字符串>” - -V “-V<备份方式>”或“--version-control=<备份方式>” #### 使用实例 1.为 test.log文件创建软链接linktest。 ```shell ln -s test.log linktest ``` 2.为 test.log创建硬链接lntest。 ```shell ln test.log lntest ``` ### 39.diff 命令 比较单个文件或者目录内容。 #### 命令格式 diff [参数] [文件1或目录1] [文件2或目录2] #### 常用参数 - -c 上下文模式,显示全部内文,并标出不同之处 - -u 统一模式,以合并的方式来显示文件内容的不同 - -a 只会逐行比较文本文件 - -N 在比较目录时,若文件 A 仅出现在某个目录中,预设会显示:Only in 目录。若使用 -N 参数,则 diff 会将文件 A 与一个空白的文件比较 - -r 递归比较目录下的文件 #### 使用实例 1.显示 test1.txt 和 test2.txt 两个文件差异。 ```shell diff test1.txt test2.txt ``` ### 40.grep 命令 一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹 配的行打印出来。 #### 命令格式 grep [option] pattern file #### 常用参数 - -c 计算找到‘搜寻字符串’(即 pattern)的次数 - -i 忽略大小写的不同,所以大小写视为相同 - -n 输出行号 - -v 反向选择,打印不匹配的行 - -r 递归搜索 - --color=auto 将找到的关键词部分加上颜色显示 #### 使用实例 1.将 /etc/passwd 文件中出现 root 的行取出来,关键词部分加上颜色显示。 ```shell grep "root" /etc/passwd --color=auto cat /etc/passwd | grep "root" --color=auto ``` 2.将 /etc/passwd 文件中没有出现 root 和 nologin 的行取出来。 ```shell grep -v "root" /etc/passwd | grep -v "nologin" ``` ### 41.wc 命令 用来显示文件所包含的行、字和字节数。 #### 命令格式 wc [选项] [文件] #### 常用参数 - -c 统计字节数 - -l 统计行数 - -m 统计字符数,这个标志不能与 -c 标志一起使用 - -w 统计字数,一个字被定义为由空白、跳格或换行字符分隔的字符串 - -L 打印最长行的长度 #### 使用实例 1.统计文件的字节数、行数和字符数。 ```shell wc -c test.txt wc -l test.txt wc -m test.txt ``` 2.统计文件的字节数、行数和字符数,只打印数字,不打印文件名。 ```shell cat test.txt | wc -c cat test.txt | wc -l cat test.txt | wc -m ``` ### 42.ps 命令 用来显示当前进程的状态。 #### 命令格式 ps[参数] #### 常用参数 - a 显示所有进程 - -a 显示同一终端下的所有程序 - -A 显示所有进程 - c 显示进程的真实名称 - -N 反向选择 - -e 等于“-A” - e 显示环境变量 - f 显示程序间的关系 - -H 显示树状结构 - r 显示当前终端的进程 - T 显示当前终端的所有程序 - u 指定用户的所有进程 - -au 显示较详细的资讯 - -aux 显示所有包含其他使用者的行程 - -C<命令> 列出指定命令的状况 - --lines<行数> 每页显示的行数 - --width<字符数> 每页显示的字符数 #### 使用实例 1.显示所有进程信息。 ```shell ps -A ``` 1. 显示指定用户信息。 ```shell ps -u root ``` 1. 显示所有进程信息,连同命令行。 ```shell ps -ef ``` ### 43.watch 命令 可以将命令的输出结果输出到标准输出设备,多用于周期性执行命令/定时执行命令。 #### 命令格式 watch [参数] [命令] #### 常用参数 - -n或--interval watch缺省每2秒运行一下程序,可以用-n或-interval来指定间隔的时间。 - -d或--differences 用-d或--differences 选项watch 会高亮显示变化的区域。 而-d=cumulative选项会把变动过的地方(不管最近的那次有没有变动)都高亮显示出来。 - -t 或-no-title 会关闭watch命令在顶部的时间间隔,命令,当前时间的输出。 - -h, --help 查看帮助文档 #### 使用实例 1.每隔一秒高亮显示网络链接数的变化情况 ```shell watch -n 1 -d netstat -ant ``` 2.每隔一秒高亮显示http链接数的变化情况 ```shell watch -n 1 -d 'pstree|grep http' ``` ### 44. at 命令 在一个指定的时间执行一个指定任务,只能执行一次。(需开启atd进程) #### 命令格式 at [参数] [时间] #### 常用参数 - -m 当指定的任务被完成之后,将给用户发送邮件,即使没有标准输出 - -I atq的别名 - -d atrm的别名 - -v 显示任务将被执行的时间 - -c 打印任务的内容到标准输出 - -V 显示版本信息 - -q<列队> 使用指定的列队 - -f<文件> 从指定文件读入任务而不是从标准输入读入 - -t<时间参数> 以时间参数的形式提交要运行的任务 #### 使用实例 1.3天后的下午5点执行/bin/ls ```shell at 5pm+3 days at> /bin/ls at> ``` ### 45.crontab 命令 在固定的间隔时间执行指定的系统指令或 shell script脚本。时间间隔的单位可以是分钟、小时、日、月、周及以上的任意组合。(需开启crond服务) #### 命令格式 crontab [-u user] file 或 crontab [-u user] [ -e | -l | -r ] #### 常用参数 - -u user:用来设定某个用户的crontab服务,例如,“-u ixdba”表示设定ixdba用户的crontab服务,此参数一般有root用户来运行。 - file:file是命令文件的名字,表示将file做为crontab的任务列表文件并载入crontab。如果在命令行中没有指定这个文件,crontab命令将接受标准输入(键盘)上键入的命令,并将它们载入crontab。 - -e:编辑某个用户的crontab文件内容。如果不指定用户,则表示编辑当前用户的crontab文件。 - -l:显示某个用户的crontab文件内容,如果不指定用户,则表示显示当前用户的crontab文件内容。 - -r:从/var/spool/cron目录中删除某个用户的crontab文件,如果不指定用户,则默认删除当前用户的crontab文件。 - -i:在删除用户的crontab文件时给确认提示。 #### 使用实例 1.列出 crontab 文件。 ```shell crontab -l ``` 2.编辑crontab 文件。 ```shell crontab -e ``` #### Crontab 任务实例 1.每1分钟执行一次command ```shell * * * * * command ``` 2.每小时的第3和第15分钟执行 ```shell 3,15 * * * * command ``` 3.在上午8点到11点的第3和第15分钟执行 ```shell 3,15 8-11 * * * command ``` # 扩展面试题 ## TCP三次握手 > [TCP三次握手](https://cloud.tencent.com/developer/article/2157202) [TCP的三次握手与四次挥手](https://www.cnblogs.com/tuyang1129/p/12435772.html) > > [通俗理解](https://blog.csdn.net/spade_Kwo/article/details/119464901) ## 面试题 ![image](assets/image.png) ![image (../../niu/interview_question/2024%25E7%25B2%25BE%25E7%25AE%2580%25E7%2589%2588%25E6%259C%25AC/%25E6%2589%25A9%25E5%25B1%2595%25E9%259D%25A2%25E8%25AF%2595%25E9%25A2%2598/assets/image%2520(1).png)](assets/image%20(1).png) ## Get 和 Post 的区别? HTTP(Hypertext Transfer Protocol)协议中的 **GET 和 POST 是两种常用的请求方法,**它们在用途和使用方式上有一些区别。 1. **用途:** 1. **GET:** 用于请求服务器上的资源,通常是获取数据。GET 请求是幂等的,多次请求返回的结果应该是相同的,而且不应该对服务器产生任何副作用。 1. **POST:** 用于向服务器提交数据,通常是提交表单数据或上传文件等。POST 请求可能对服务器产生副作用,例如在数据库中创建新的资源。 1. **数据传输方式:** 1. **GET:** 数据通过 URL 参数传递,附在 URL 后面。因为数据在 URL 中可见,所以不适合传递敏感信息,且有长度限制。 1. **POST:** 数据通常包含在请求体中,而不是直接暴露在 URL 中。相对于 GET,POST 能够传递更多的数据,且不受长度限制。 1. **缓存:** 1. **GET:** 可以被浏览器缓存,默认情况下可以被收藏夹保存,可以被历史记录保存。 1. **POST:** 通常不会被缓存,不会被保存在浏览器历史记录中。 1. **安全性:** 1. **GET:** 因为数据附在 URL 中,对用户可见,不适合传递敏感信息。由于请求的数据在 URL 中,也可能被浏览器保存,因此对用户的隐私不够安全。 1. **POST:** 数据包含在请求体中,对用户不可见,适合传递敏感信息。由于不会暴露在 URL 中,相对于 GET 更安全。 1. **幂等性:** 1. **GET:** 幂等,多次请求应该返回相同的结果。 1. **POST:** 不幂等,多次相同的请求可能产生不同的结果,例如创建相同的资源多次可能导致多个资源的创建。 总的来说,选择使用 GET 还是 POST 取决于你的业务需求。如果是获取数据,使用 GET;如果是提交数据,使用 POST。在实际应用中,经常会根据 RESTful 设计原则,合理地选择 GET 和 POST 的使用场景。 ## HTTP 中重定向和请求转发的区别? 实现 **转发**:用 request 的 getRequestDispatcher()方法得到 ReuqestDispatcher 对象,调用 forward()方法 request.getRequestDispatcher("other.jsp").**forward**(request, response); **重定向**:调用 response 的 sendRedirect()方法 response.**sendRedirect**("other.jsp"); 1> 重定向 2 次请求,请求**转发** 1 次请求 2> 重定向地址栏会变,请求转发地址栏不变 3> 重定向是浏览器跳转,请求转发是服务器跳转 4> 重定向可以跳转到任意网址,请求**转发**只能跳转当前项目 5>重定向会丢失请求参数, 请求转发不会丢失请求数据 ## http和https的区别 http协议和https协议的区别主要是:**传输信息安全性不同、连接方式不同、端口不同、证书申请方式不同** ### 1.安全性不同 http协议:是超文本传输协议,信息是明文传输。如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息。 https协议:HTTPS通过使用SSL/TLS加密技术,可以确保数据传输的安全性,更加健壮安全。 ### 2.连接方式不同 HTTP协议是基于TCP协议之上的,在发送请求和接收响应之后就会断开连接。 https协议:HTTP协议的基础之上,使用了SSL/TLS协议,通过握手过程建立了一条加密通道,通信双方可以在通道上进行长时间的、安全的数据传输。 ### 3.端口不同 http协议:使用的端口是80。 https协议:使用的端口是443. ### 4.证书申请方式不同 http协议:无证书。 https协议:需要到ca申请证书,一般免费证书很少,需要交费。 ## mybatis对应接口的底层原理 ## 如何在k8s部署一个应用程序 ## 用过nignx吗?nignx相关配置 ## 写一个类,作为模板让别人调用。应考虑哪几个方面 (**开闭原则**) ## 什么是Nacos? ## 说一下你的工作经历。 ## 如果redis数据库有百万数据,如何查询 # **学生面试分享** ## **张子豪** 1. springcloud事务的了解,数据库锁的了解,项目中用过悲观锁乐观锁吗如何使用,你在项目中用乐观锁的时候直接在数据库加版本号字段么? 2. synchronized 和 Lock的区别 3. 分布式锁 4. mysql和es如何保持数据一致 5. es和mysql如何实现数据同步 6. 说一下cloud 7. springcloud的组件,你用过那些 8. nacos在项目中如何实现文件自动更新 9. 秒杀 10. aop以及实现原理 11. 如何编写高效的sql并进行调优 ## **叶青松** 1. 如何做到持续性高并发,任何时间涌入大量请求服务器都支持高并发。 2. 什么是短链接,长链接。 3. 后端方向如何预防和防止网络攻击。 4. 除了使用#{}防止SQL注入,还用什么方法可以既可以解决SQL注入攻击,又可以满足SQL语句拼接。 5. 除了线上阿里云项目部署之外线下会不会企业内会不会项目部署。 1. 前后端传输数据的步骤。 > 1. 前端用请求框架axios 构建请求并发送给服务器 > 2. 后端接收参数,验证处理、做出响应 2. erp是否有高并发, 3. 高并发在哪个项目用的 4. 秒杀 5. 秒杀在什么时候扣减库存 什么时候锁单 6. 秒杀下单的时候有没有前后顺序,现场设计出下单前后顺序 7. Redis缓存穿透,缓存雪崩, 8. 什么是socket 9. 什么是mqtt协议 10. 有没有设计过高并发场景下的数据库 11. 有没有做过数据库优化 1. 设计模式在项目中是如何应用的 手写单例模式 2. 手写冒泡排序算法 3. maven中的pom.xml 4. MVC框架和boot框架区别 5. SQL优化explain中具体的字段 6. jvm模型 gc垃圾回收算法 标记清除算法的缺点 7. docker命令 8. sentinel限流策略结合项目 9. seata底层结合项目 10. nacos底层结合项目 11. 事务传播机制 12. 仓储管理系统是如何保证出入库原子性的 13. redis在项目中是如何使用的 14. MySQL和redis如何保证数据一致性,仅用延时双删策略就够了吗 15. es倒排索引的原理 16. 项目都是某某管理系统,你们的后台是用现成的框架改的还是自己搭建的 17. 项目架构是改的还是自己搭建的 18. 解释Spring Security安全框架 结合项目 19. 解释RBAC——基于角色权限的模型 结合项目 ## **李俊硕** 1. 有一个非常大的视频,然后需要去上传,然后怎么做才能让他上传的更快 [java实现大文件的分片上传与下载](https://blog.csdn.net/weixin_50799082/article/details/128547482) 2. DM(达梦数据库) 如果面试的公司做政府或金融项目的,了解一下: [快速上手官方文档](https://eco.dameng.com/document/dm/zh-cn/start/) ## **黄金亮笔试** 1. 请简单描述Java数据类型 2. 请简单描述线程状态 3. 请描述线程池,你是怎么创建线程池的 4. 请简单描述下数据库的索引 5. 请列举Redis的常用命令 6. 请简单描述下你理解的mybatis 7. 请简单描述下你对springboot的理解 面试 1. mongodb相对于redis有什么优势? 2. es为什么快?存储结构? 3. 击穿解决方案分 4. 分布式锁在什么时候加 ## **陈忍忍** 1. 如何判断一个表是锁定状态的 2. 如何保证es和数据库的数据同步 3.说一下进程与线程的区别,项目中哪方面用到了多线程 ## **王文超** 2.8面试 1.自我介绍 2.项目的某个逻辑(我说的是电商下单逻辑) 3.RabbitMQ还有在那个地方使用到过 4.linux命令查看文件内容是哪个命令 ~~~shell cat a.txt | more # 分页查看 ~~~ 5.java容器有哪些 2.9面试 1.介绍一下你最近做的项目职责模块 2.介绍你的仓储项目 3.仓储的过期出库是怎么实现的 4.介绍一下你的门禁项目 ## **万彦峰** ***\*以下都是数字马力相关面试题,仅供参考,每个公司面试的都不一样!\**** ### **面试一(郑州)** 1. 异常处理 > 1. 参考 项目开发中如何处理异常 如自定义异常(BusinessException) 在业务层抛出异常 最后在Mvc里做全局异常处理(为了方便前端请求时 给一个统一的数据格式 ) > 2. 在描述时 可以穿插 sql异常 类找不到 .... 2. 这个项目做了多长时间,有多少开发人员 > 项目时间: 如一个项目开发了2月(代码编写一个月,测试 修复完善 一个月),我在做其它项目,同时维护之前的项目 > > 开发人员: > > * 2个后端(接口和中后台)+1个前端 > * 1个高级后端+2个初级 > * 纯后端 如易凯erp 4个后端 > > UI/PS 美工 设计 > > 测试人员 > > 运维人员 > > 项目经理 > > 说一下项目开发的流程? 3. 微信登录功能,客户端是怎么去校验这个JWT令牌的 4. 公钥私钥是什么,都有那些作用 5. 用户数据大概有多少 6. 购物车缓存是先删再更新,还是删过之后再更新 7. 修改购物车的时候并发多少 8. 讲一下OAuth2 9. https为什么是安全的 10. CA是什么 11.浏览器和服务器和CA怎么协作 12. 事务的原理,底层是怎么实现的 ### **面试二(郑州)** 1自我介绍+简单介绍一下项目 2.项目中遇到最难的一个问题,以及怎么解决的。 3.对并发的理解,有没有遇到过一个难啃的问题。 4. 怎么避免回表 5.之前的项目有没有觉得哪里设计额不合理的,我觉得哪里不合理的? 6.你们是怎么保证你们的研发质量。 7.你们有没有用过单元测试 8.单元测试和集成测试有没有,有没有可能不写呢,有没有什么可以管理 9.前端经过后端经过那些节点,分别说一下 10你们是怎么使用Aop和IOC? 11.Aop是怎么实现的? 12.动态代理和普通代理的区别? 13.动态代理哪里好? 14.你有没有读过那些源码? 15.HashMap的结构 ### **面试三(长沙)** 1.自我介绍 2.项目里面有用了分布式的一些框架吗 3.服务之间的调用用的什么 4.你这个项目分了多少个模块 5.优惠券如何核销的 6.java里面实现锁的方式有哪些 7.synchronized和lock在实现上有啥区别 8.ReentrantLock的底层原理 9.多线程如何获取异步执行结果 10.jvm内存过大如何排查 11.java锁和分布式锁有什么区别 ### **面试四(郑州)** 1.审核业务流程 2.人工审核方案 3.定时任务实时更新问题 4.如果有大量任务导致定时任务无法扫完 5.你们微服务大概布了多少台服务器 6.好友如何获取共同点赞视频 8.AOP 7.springboot的核心注解 9.动态代理的类型 10.动态代理和静态代理的区别 11.MyBatis的一级缓存和二级缓存 12.redis的基础数据结构 13.set和zset的区别 14.Redis缓存三剑客 ### **面试五(郑州)** 1、15分钟网页手撸代码并讲一下你的代码逻辑用java语言实现一个函数输入是一个元素都是自然数的数组,打印这个数组出现频率最多的元素,如果频率最大的有并列,请全部打印出来,例子,输入1,7,3,6,4,2,7,1,则输出1,7 2、介绍第一个项目 3、怎么用redis实现的点赞功能如果要实现取消点赞,怎么做 4、es搜索准确性如何衡量 5、mysql的性能瓶颈 6、如何保证数据一致性redis和mysql 7、如何保证redis的数据量不过大,springcache的过期时间和redis的缓存过期如何统一 8、提高接口的响应速度方案 9、怎么定位慢sql、怎么判断索引是否失效 10、sgl优化方案索引是怎么实现的 11、freemarker和接口响应速度有关吗?解决了什么问题 12、为什么用springcache+redis不用freemarker 13、Freemarker模板缓存存储时间怎么设置 14、AES算法 15、加密的密钥存储在哪里 16、介绍一下你知道的非对称加密的算法 17、对称加密和非对称加密的使用场景 18、事务传播行为 19、REQUIRED实现逻辑 20、介绍一下嵌套事务 21、外部事务回滚,子事务也必然会回滚吗 22、事务的实现原理 23、常见实现分布式事务的方案 24、二阶段肯定能保证强一致性吗? 25、讲解一下三阶段 26、 seata如何实现分布式事务 27、常见的垃圾收集器垃圾回收算法 ### **面试六(郑州)** 1.项目的业务功能 2.难点业务上的亮点功能、业务流程 3.雪化算法的亮点 4.如果用户中间断开如何解决 5.如何确保外接接口不会出问题 6雪花算法类似的微服务id 7.AOP的底层原理 ### **面试七(郑州)** 1.在项目开发中承担了一个什么样的角色 2.什么是AQS 3.基于AQS的一些工具或Java类有哪些 4.AQS中的可重入锁和CountDownLatch有使用过吗,如何使用的 5.可重入锁的重入是如何实现的,锁是如何获取的,变量是如何变化的,重入的时候是如何判断的 6.什么是Synchronized锁升级机制,什么情况下会触发锁升级, 7. springboot自动装配的机制(spi机制) 8.线程池参数 9.核心线程数已满会开启临时线程吗?核心线程已满,任务队列已满会开启临时线程吗? 10.如何在核心线程数已满后直接开启临时线程 11.原生线程池的方法有哪些 12.Sleep和wait的区别 13.多线程环境下如何根据线程id实现线程顺序执行 14.ES存数据的流程 15.消息中间件的作用 16.能用同步解决为什么使用异步,你对异步的理解 17.异步在什么情况下会产生内存泄漏 18.创建什么对象会导致内存泄漏 19.如何判断内存中那些对象需要回收 ### **面试八(长沙)** 1.自我介绍 2.Java里面怎么自定义线程池 3.用过的中间件,分别的使用场景 4.MQ的消息确认机制 5.ACK的配置,代码中如何调用 6.分布式锁 7.sql优化 ### **面试九(武汉** **中软国际(泰康人寿))** 1.hashMap底层结构 2.hashMap是线程安全的吗 3.ConCurrentHashMap为什么是线程安全的 4.i++是原子操作么 5mybatis的一级缓存和二级缓存 6.mybatis的缓存支持延迟加载么 7.springboot的核心注解 ### **面试十(郑州)** 1.自我介绍 2.String/StringBuffer/StringBuilder 3.HashMap的扩容机制 4.HashMap的尾插法解决了头插法的什么问题 5.什么是深拷贝和浅拷贝 6.Mysql删除一张表有哪些操作 7.delete和drop的区别 8.id自增到5删除全部数据下一条数据id是1还是6 8delete清除所有表约束是否还在 9.sql使用like关键字要注意什么 10,后缀固定用like查询百分号在左边怎么优化 like ‘%小’ 11Redis的持久化方式 12.生成当前时间点的RDP文件用什么命令 13.save和bgsave的区别 ### **面试十一(郑州)** 1.自我介绍 2.项目介绍 3.微服务中为什么要用服务注册 5.兑换码的状态变化怎么实现的 4.优惠中心的核心表有什么,兑换码的表核心字段有什么 6.兑换码如何保证不会重复 7.数据库事务隔离级别 8.MVCC了解吗 ### **面试十二** 1.自我介绍 2.mysql的优化从哪些方面优化 3.数据库索引有哪些 4.复合索引最左匹配原则 5.数据库的事务特性 6.事物特性一致性什么意思 7.常用的集合有哪些 8.ArrayList和LinkedList分别适用于哪些场景 9.ArrayList初始化大小 10.这两个集合是线程安全的吗 11.线程安全的集合 12.HashMap数据结构 13.HashMap添加元素的原理 14.JVM类加载 15.双亲委派模型的好处 16.redis的使用场景 17.怎么保证缓存和mysql数据的一致性18.缓存击穿 19.缓存穿透 20.redis有哪些锁 21.乐观锁 22.synchronized锁膨胀 23.线程池参数 24.分布式事务有哪些常见的解决方案 25.seata原理 26.seata有哪些模式 27.介绍AT模式 28.介绍项目 29.主要负责功能 30 后端横型怎么设计的美结构怎么设计的 ### **面试十三** 1.有挑战的问题 2.RabbitMQ的使用和可靠性 3.RabbitMQ和kafaka之间的对比 4.Redisson分布式锁的使用 5.主要是围绕你写的技术栈展开问 6.Redis并发竞争key的问题 7.Redis是单线程,服务是多核,如何提高redis的利用率 8.Mysql部署情况 9.Sql调优,具体说明 10.为什么mysql使用索引,查询速度快,索引的原理 11.InnoDB为什么使用自增ID作为主键 12.你们使用ES做什么,ES倒排索引原理 13.JVM的内存结构,相关信息 14.Linux中查找某个文件中的某句话 grep i love you 1.txt ## **季浩洋** 1. 数组和链表的区别 2. list和set的contains方法有什么区别 3. 在项目中你们是如何使用es的 4. 已经上线的项目如果出现bug你们是怎么做的 5. 支付不是自己做的,那订单那一块你们是怎么和支付一起联调的 6. 微服务怎么集群的 ## **李建州** ![img](assets/adcf742fb6a79412b50381a24636afb8b25730c5.png) ![img](assets/90cb4f37a531c0730eec5ce81b06684b04b5cefc.png) ![img](assets/298b4e9b14916da5da4991d9ecdc29256d7a04bb.png) ## **梁雪静** 1、文件导入使用,捕获抛出异常注意的地方 FileNotFoundException IOException 2、谈一下对高并发的理解,平时怎么处理高并发问题 3、用过nignx吗?nignx相关配置和调优 4、写一个类,作为模板让别人调用。应考虑哪几个方面 5、关于实例类的扩展字段如何设计 6、什么是Nacos? ​ ![img](assets/152979a2145f5937d7ee08a5cb5cfe9ca7a45e80.png) ## 甄意 ### java反射机制 ![image-20250512175829935](assets/image-20250512175829935.png) # 情景面试 * [电商库存扣减如何设计?如何防止超卖?](https://blog.csdn.net/u012108607/article/details/143118133) * [订单30分钟未支付自动取消怎么实现?](https://blog.csdn.net/Tyson0314/article/details/141868121) ## **框架与项目实战场景** **场景描述**:结合 Spring Boot、MyBatis、Spring MVC 等框架考察项目经验和问题解决能力。 ### Spring Boot 项目中,如何实现一个接口的限流功能?请说明具体方案和用到的组件 **答题思路**: 1. 方案选择 - **基于 Spring AOP + 限流注解**:自定义注解(如 `@RateLimiter`),通过 AOP 拦截方法,结合限流算法(令牌桶、滑动窗口)实现。 - **使用 Redis + 分布式锁**:利用 Redis 的 `INCR` 和 `EXPIRE` 命令记录请求次数,设置过期时间实现滑动窗口。 - **集成开源组件**:如 `Hystrix`、`Resilience4j` 或 `Spring Cloud Sentinel`(推荐,配置简单)。 2. 示例代码(AOP + 令牌桶) - 自定义注解: ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { int permitsPerSecond() default 5; // 每秒允许请求数 } ``` - AOP 切面: ```java @Aspect @Component public class RateLimitAspect { private final TokenBucket tokenBucket = TokenBucket.create(5); // 初始化令牌桶 @Around("@annotation(rateLimit)") public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { if (tokenBucket.tryConsume(1)) { // 尝试获取令牌 return joinPoint.proceed(); } throw new RuntimeException("请求过于频繁,请稍后再试!"); } } ``` ### 在项目中遇到过 SQL 性能问题吗?如何优化?(高) **答题思路**: 1. 定位问题 - 使用数据库慢查询日志(`slow_query_log`)或监控工具(如 `explain` 分析执行计划)。 2. 优化手段 - **索引优化**:为查询条件字段添加索引(避免全表扫描),注意联合索引的最左匹配原则。 - **SQL 改写**:避免子查询(改用 `JOIN`)、减少 `SELECT *`、优化 `LIKE` 语句(非前缀模糊查询无法使用索引)。 - **分页优化**:大页数场景使用 `LIMIT OFFSET` 优化(如 `WHERE id > lastId LIMIT 10`)。 - **缓存优化**:对高频读、低频写的数据使用 Redis 缓存(如 `@Cacheable` 注解)。 ## **分布式与高并发场景** **场景描述**:考察多线程、分布式事务、缓存一致性等复杂场景的处理能力。 ### 如何解决 Redis 缓存与数据库的一致性问题(高) **答题思路**: 1. 常见策略 - **先更新数据库,再删除缓存**(主流方案,需注意删除失败时的补偿机制,如消息队列重试或异步任务扫描)。 - **延时双删**:更新数据库后,先删缓存,再延迟一段时间(如 1 秒)后再次删缓存,解决主从数据库延迟导致的脏读。 - **异步缓存失效**:通过 Canal 监听数据库 binlog,异步删除缓存(适用于高并发场景)。 2. 注意事项 - 避免同时更新缓存和数据库(一致性难以保证)。 - 对读多写少的场景优先使用缓存,写多读少的场景可弱化缓存(如设置短过期时间)。 ### 设计一个分布式锁,要求支持可重入、超时释放,说明实现原理和可能的坑 **答题思路**: 1. 基于 Redis 的实现 - **核心命令**:`SET key value NX PX timeout`(原子性加锁,`NX` 保证唯一,`PX` 设置超时避免死锁)。 - **可重入**:锁值记录线程 ID 和重入次数(如 `value = "threadId:count"`),加锁时校验线程 ID 是否一致,一致则 count 自增。 - 释放锁 用 Lua 脚本保证原子性删除(避免误删其他线程的锁): ```lua if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end ``` 2. 潜在问题 - **主从同步延迟**:主节点加锁后未同步到从节点,主节点宕机导致锁失效(可通过 Redlock 算法解决)。 - **超时时间设置**:业务执行时间超过锁超时时间,导致锁被提前释放(可通过续约机制,如 Redisson 的看门狗)。 ## **线上故障排查场景** **场景描述**: 线上系统突然报错,日志显示 `NullPointerException`,且接口响应时间从 200ms 飙升至 5s,导致大量请求超时。假设你是值班开发,如何快速定位并解决问题? **考察点**: 1. **故障排查流程**:能否遵循 “复现问题 → 定位根源 → 临时修复 → 永久解决” 的逻辑。 2. 工具使用能力 - 日志分析:如何通过日志定位具体代码行(如查看异常堆栈、上下文参数)。 - 性能监控:使用 `jstack` 分析线程状态(是否有死锁、线程阻塞),`jstat` 查看 JVM 内存占用(是否内存泄漏)。 - 链路追踪:是否了解分布式链路追踪工具(如 SkyWalking、Sleuth),排查是否为依赖服务超时。 3. 应急处理能力 - 临时方案:快速回滚代码、开启熔断或限流、增加日志级别获取更多信息。 - 永久方案:修复空指针(如增加判空逻辑、初始化对象)、优化慢 SQL 或重构业务逻辑。 **应答建议**: - 分步骤说明排查工具和思路,强调 “先止损再根治”,例如:“首先通过 `tail -f` 查看实时日志,定位报错代码;然后用 `jstack` 分析线程状态,确认是否存在线程阻塞导致的性能问题……” ## **代码优化与重构场景** **场景描述**: 现有一个老旧的 Java 系统,采用单例模式实现配置中心,但随着业务扩展,配置项增多,出现了线程安全问题和配置更新不及时的问题。你会如何优化? **考察点**: 1. **设计模式理解**:能否识别单例模式的适用场景(如全局唯一实例)与当前问题的矛盾(多线程下的并发修改)。 2. 优化思路 - 线程安全:将单例模式改为线程安全的实现(如 `Double-Checked Locking` 或 `Enum Singleton`),或引入 `ConcurrentHashMap` 存储配置项。 - 动态更新:增加配置热加载机制(如监听文件变更、通过 Spring Event 发布配置更新事件)。 - 重构方向:是否考虑将配置中心独立为微服务(如 Spring Cloud Config),解耦业务逻辑。 3. **灰度发布意识**:优化后如何验证(如单元测试、压测),是否支持灰度发布以降低风险。 **应答建议**: - 分阶段说明优化步骤,先解决线程安全,再处理动态更新,例如:“首先将单例类的配置存储改为 `ConcurrentHashMap`,并使用 `ReentrantLock` 保证线程安全;然后添加一个定时任务,每隔 5 分钟从配置文件重新加载数据,同时提供手动刷新接口……” ## **分布式系统场景** **场景描述**: 在分布式系统中,多个服务需要更新同一份数据(如用户余额),如何保证数据一致性?假设使用 Redis 缓存和 MySQL 数据库,且存在多个写操作节点。 **考察点**: 1. **分布式一致性理论**:是否了解 CAP 定理、BASE 理论,能否选择合适的一致性级别(如最终一致性)。 2. 解决方案 - 分布式锁:使用 Redis 分布式锁(如 `setNx`)保证同一时刻只有一个节点操作数据。 - 消息队列:通过异步消息(如 Kafka)实现最终一致性,结合重试机制和幂等性设计(如为操作添加唯一 ID)。 - 数据库层面:使用事务(如 MySQL 的 InnoDB 行锁)、乐观锁(版本号控制)。 3. **异常处理**:如何处理锁超时、消息重复消费等问题(如设置合理的锁过期时间、消费端去重)。 **应答建议**: - 结合具体场景选择方案,例如:“对于高并发写场景,优先使用 Redis 分布式锁 + 数据库乐观锁,确保同一时间只有一个服务更新数据,同时通过消息队列异步补偿可能的不一致;对于非实时场景,可采用最终一致性,通过定时对账任务修复数据……” **面试准备建议**: 1. **复盘真实项目**:整理自己参与过的需求设计、故障排查等实际案例,用 STAR 法则(场景、任务、行动、结果)梳理思路。 2. **熟悉常用工具**:如 JVM 调优工具(jmap、jconsole)、日志分析工具(Logback、ELK)、分布式组件(Spring Cloud、Dubbo)。 3. **模拟演练**:与朋友或同事进行角色扮演,模拟面试中的场景问答,锻炼临场反应能力。 ## 什么是 Java 内存泄漏?如何排查?(中) **参考答案**: **内存泄漏**:对象不再使用,但垃圾回收器无法回收其占用的内存(如静态集合持有对象引用、未关闭资源等)。 **排查方法**: 1. **工具分析**:使用 VisualVM、MAT(Memory Analyzer Tool)等工具生成堆转储(Heap Dump)。 2. **检查引用链**:找出长期存活的对象及其引用路径。 3. **代码审查**:检查静态集合、监听器 / 回调未注销、资源未关闭等常见问题。 # 项目上线相关 以下是关于 “项目上线” 相关的常见面试题及参考答案,涵盖流程、风险、协作、技术等多个维度,适用于开发、测试、项目管理等岗位: ### **一、项目上线流程相关问题** #### 1. 请描述你熟悉的项目上线流程(从需求到上线的完整链路) **参考答案**: 通常包括以下阶段: 1. **需求阶段**:明确需求文档(PRD),评审需求可行性,确定技术方案和排期。 2. **开发阶段**:前后端开发、数据库设计、接口联调,使用版本控制工具(如 Git)管理代码。 3. 测试阶段 - 单元测试(开发自测)→ 功能测试(QA 执行用例)→ 集成测试(跨模块 / 系统联调)→ 性能测试(压力测试、负载测试)→ 安全测试(漏洞扫描)。 - 修复缺陷并回归测试,直至测试通过。 4. 预发布阶段 - 在预生产环境(Pre-Prod)模拟线上环境进行最终验证,确保配置、依赖服务一致。 - 准备上线清单(如 SQL 脚本、配置变更、灰度策略),制定回滚方案。 5. 上线执行阶段 - 选择低峰期(如夜间)部署,按计划发布代码(蓝绿部署 / 滚动更新 / 灰度发布)。 - 监控上线过程:检查服务启动日志、接口响应、数据库连接等。 6. 验证与监控阶段 - 验证核心功能、性能指标(如响应时间、吞吐量)。 - 开启实时监控(如 APM 工具、日志系统),设置告警阈值,观察用户反馈。 7. 收尾阶段 - 清理临时资源,更新线上文档(如 API 文档、运维手册)。 - 召开复盘会,总结经验,记录问题与优化点。 #### 2. 上线前需要准备哪些关键事项? **参考答案**: - 技术层面 - 代码冻结(避免临时变更),备份数据库和配置文件。 - 确认依赖服务(如中间件、第三方 API)的稳定性。 - 协作层面 - 同步上线时间给相关团队(运维、产品、客服),明确各角色职责。 - 准备应急预案(如回滚脚本、故障处理流程)。 - 验证层面 - 预发布环境全流程演练,确保无遗漏场景。 - 检查版本号、编译产物一致性(避免打错包)。 ### **二、项目上线风险与应对** #### 1. 上线过程中可能遇到哪些风险?如何规避? **参考答案**: **常见风险**: - 代码部署失败(如编译错误、配置文件缺失)。 - 线上功能异常(如逻辑漏洞、兼容性问题)。 - 性能瓶颈(如流量突增导致服务崩溃)。 - 数据问题(如 SQL 执行错误导致数据丢失或不一致)。 - 依赖服务故障(如第三方接口变更未同步)。 **应对措施**: - 技术规避 - 采用灰度发布,逐步放量观察用户行为。 - 编写自动化回滚脚本,确保 10 分钟内可回滚。 - 对数据库变更执行预检查(如通过 SQL Review 平台),先在测试环境验证。 - 流程规范 - 制定详细的上线 checklist,按步骤执行并记录状态。 - 上线前进行跨团队会议,确认所有依赖方就绪。 - 监控预警 - 部署后立即监控关键指标(CPU / 内存使用率、接口成功率、错误日志)。 - 配置告警通道(如短信、企业微信),确保故障快速响应。 #### 2. 如果上线后发现严重 bug,该如何处理?(高) **参考答案**: 1. 快速定位 - 查看实时日志、监控指标,复现问题场景(优先使用线上沙箱环境或影子库)。 - 对比上线前后代码变更,排查可能影响的模块。 2. 紧急止损 - 若影响范围大,立即触发回滚(使用预验证的回滚脚本)。 - 若回滚成本高,可通过配置开关(Feature Flag)临时关闭问题功能。 3. 修复与验证 - 开发人员紧急修复 bug,在测试环境完成回归测试。 - 重新部署修复版本(建议再次灰度发布)。 4. 复盘改进 - 分析问题根源(如测试用例缺失、代码评审不严格)。 - 补充测试覆盖,完善监控规则,避免同类问题再次发生。 ### **三、协作与团队管理问题** #### 1. 如何协调开发、测试、运维团队确保上线顺利? **参考答案**: - 明确职责 - 开发:提供可部署的代码包、技术文档、已知问题列表。 - 测试:确认测试报告,输出 “上线准入单”(如通过率≥95%)。 - 运维:检查服务器资源、网络配置,准备部署脚本。 - 流程同步 - 使用项目管理工具(如 Jira、TAPD)跟踪任务状态,设置关键节点(如提测、预发布验收)。 - 每日站会同步进展,提前暴露风险(如资源冲突、延期)。 - 演练与沟通 - 上线前进行跨团队演练(如模拟部署、故障响应)。 - 建立统一的沟通渠道(如企业微信群),确保信息实时同步。 #### 2. 当上线时间临近,仍有未解决的缺陷,该如何决策? **参考答案**: - 评估影响 - 按优先级分类缺陷(如 P0/P1 级为阻塞类问题,P2/P3 为次要问题)。 - 若 P0/P1 级缺陷未解决,原则上暂停上线,需与产品、业务方沟通延期。 - 权衡方案 - 若必须按时上线(如业务强需求),可: - 对非核心功能的缺陷,通过配置开关屏蔽问题模块。 - 制定上线后紧急修复计划(如 24 小时内补丁发布),并提前告知用户可能的影响。 - 记录决策 - 所有变更需留痕(如会议纪要、风险确认邮件),明确责任人和后续跟进时间。 ### **四、技术与工具相关问题** #### 1. 常用的上线部署工具有哪些?如何保证部署过程的幂等性? **参考答案**: - 常用工具 - 代码部署:Jenkins、GitLab CI/CD、Argo CD(K8s 场景)。 - 配置管理:Ansible、Chef、Puppet。 - 容器化:Docker、Kubernetes(滚动更新、蓝绿部署)。 - 幂等性保证 - 数据库变更:使用事务或版本号控制,避免重复执行 SQL。 - 接口设计:支持幂等性(如通过唯一请求 ID 防止重复提交)。 - 部署脚本:确保多次执行结果一致(如先停止旧服务再启动新服务)。 #### 2. 如何监控上线后的系统稳定性?(中) **参考答案**: - 基础监控 - 服务器指标:CPU、内存、磁盘 I/O、网络流量(如 Prometheus+Grafana)。 - 应用指标:QPS、响应时间、错误率(如 APM 工具 New Relic、SkyWalking)。 - 日志监控 - 收集应用日志、慢 SQL 日志,通过 ELK(Elasticsearch+Logstash+Kibana)分析异常堆栈。 - 配置日志告警(如 ERROR 级别日志触发通知)。 - 用户行为监控 - 埋点统计(如 Google Analytics、Mixpanel),分析用户操作路径是否出现异常中断。 - 实时舆情监控(如社交媒体、客服反馈),快速捕捉用户投诉。 ### **五、场景题** #### 1. 假设你负责的电商系统即将大促上线,如何保障高并发下的稳定性? **参考答案**: - 事前准备 - 性能压测:模拟峰值流量(如 10 万 QPS),优化数据库索引、缓存策略(如 Redis 预热数据)。 - 限流降级:配置网关限流(如 Nginx+Lua、Sentinel),对非核心功能(如评论)进行降级。 - 扩容资源:提前增加服务器节点、数据库读写分离、CDN 加速静态资源。 - 事中监控 - 实时监控交易链路(如支付接口成功率、库存扣减延迟)。 - 动态调整流量:通过灰度发布观察不同配置的性能表现,逐步全量。 - 事后优化 - 分析压测报告,修复内存泄漏、线程锁竞争等问题。 - 归档大促期间的监控数据,为下次大促提供参考。 #### 2. 如果上线后发现数据库连接数飙升,你会如何排查? **参考答案**: 1. 初步排查 - 查看数据库服务器状态(如 show processlist),确认是否存在大量未释放的连接。 - 检查应用日志,是否有循环调用数据库、事务未提交等问题。 2. 定位根源 - 使用慢查询日志分析是否存在低效 SQL(如全表扫描)。 - 检查连接池配置(如最大连接数、超时时间),是否设置不合理导致耗尽。 3. 临时解决 - 重启数据库服务(谨慎操作,需先备份)或增加连接池上限。 - 优化 SQL 或添加索引,减少数据库压力。 4. 长期优化 - 引入连接池监控(如 HikariCP 的监控指标),设置告警阈值。 - 对高频查询增加缓存,减少数据库访问次数。 # 说说你在公司里项目开发的流程(参考) 在公司的项目开发中,规范的流程有助于提升效率、保证质量并降低风险。以下是常见的项目开发流程,通常涵盖 **需求分析、设计、开发、测试、部署、维护** 等核心阶段,不同公司或项目可能根据实际情况(如技术栈、团队规模、项目复杂度)进行调整: ### **一、需求分析与规划阶段** **目标**:明确项目目标、范围和用户需求,形成可执行的方案。 **关键步骤**: 1. 需求调研 - 与客户、产品经理或业务部门沟通,收集原始需求(如业务痛点、功能期望、性能指标等)。 - 分析需求的合理性、优先级和可行性,识别潜在风险(如技术难度、成本超限等)。 2. 需求评审与确认 - 组织团队(开发、测试、设计等)对需求进行评审,澄清模糊点(如功能边界、交互逻辑)。 - 输出 **《需求规格说明书》**,明确功能清单、技术要求、验收标准,经各方签字确认后作为开发依据。 3. 项目规划 - 制定 **项目计划**:拆解任务(如设计、开发、测试)、分配资源(人员、时间)、设定里程碑(如 Demo 演示节点、上线日期)。 - 确定技术方案:选择开发框架、工具链、部署环境(如云计算平台)等。 ### **二、设计阶段** **目标**:将需求转化为可实现的技术方案和用户体验。 **关键步骤**: 1. 架构设计 - 技术架构:确定系统分层(如前端、后端、数据库)、通信协议(如 HTTP、gRPC)、数据存储方案(如 MySQL、Redis)等。 - 模块设计:划分功能模块(如用户中心、支付系统),定义模块间的接口和依赖关系。 2. UI/UX 设计 - 交互设计:绘制流程图、原型图(如使用 Axure、Figma),定义页面跳转逻辑和用户操作流程。 - 视觉设计:输出高保真设计稿,明确界面风格、配色、图标等,确保符合产品定位和用户习惯。 3. 设计评审 - 开发团队与设计团队确认技术可行性(如复杂动效的实现成本),测试团队参与评估可测性(如边界条件的覆盖)。 ### **三、开发阶段** **目标**:按设计方案实现功能,确保代码质量和可维护性。 **关键步骤**: 1. 任务分配与开发 - 按模块分配开发任务,开发人员领取需求并进行编码(遵循编码规范,如代码注释、命名规则)。 - 使用版本控制工具(如 Git)管理代码,通过分支策略(如 Git Flow)隔离功能开发与线上代码,避免冲突。 2. 技术选型与实现 - 选择合适的技术组件(如前端框架 Vue/React,后端框架 Spring Boot/Node.js),必要时进行技术预研(如第三方 API 对接测试)。 - 开发过程中同步编写单元测试代码,确保单个模块的逻辑正确性。 3. 持续集成(CI) - 定期将代码合并到主干分支,通过自动化工具(如 Jenkins、GitHub Actions)执行编译、单元测试,及时发现集成问题。 - 开发团队每日站会(Scrum 模式)同步进度,解决阻塞问题(如接口依赖未完成)。 ### **四、测试阶段** **目标**:验证功能正确性、性能稳定性和安全性,修复缺陷。 **关键步骤**: 1. 功能测试 - 测试团队根据《需求规格说明书》编写测试用例,覆盖正常流程、异常场景(如输入非法数据)和边界条件(如最大文件上传大小)。 - 执行冒烟测试(验证核心功能可用性)和回归测试(修复缺陷后确保未影响其他功能)。 2. 非功能测试 - **性能测试**:使用工具(如 JMeter、LoadRunner)模拟高并发场景,检测系统响应时间、吞吐量、内存泄漏等问题。 - **安全测试**:扫描代码漏洞(如 OWASP Top 10)、进行渗透测试,防范 SQL 注入、XSS 攻击等风险。 - **兼容性测试**:在不同浏览器、设备、操作系统上验证功能一致性(如移动端适配)。 3. 缺陷管理 - 通过缺陷管理工具(如 Jira、TAPD)记录问题,开发人员修复后由测试人员重新验证(闭环管理)。 - 测试完成后输出 **《测试报告》**,总结缺陷分布、修复情况和系统质量评估。 ### **五、部署与上线阶段** **目标**:将系统部署到生产环境,确保稳定运行。 **关键步骤**: 1. 预发布环境验证 - 在与生产环境配置一致的预发布环境中进行最终验证,模拟真实用户流量(如灰度发布),观察系统稳定性。 2. 部署方案执行 - 使用自动化部署工具(如 Docker、Kubernetes)打包应用,通过脚本或平台一键发布,减少人工操作风险。 - 制定回滚方案:若上线后出现严重问题,可快速回退到上一稳定版本。 3. 上线后监控 - 实时监控系统指标(如 CPU / 内存使用率、接口响应时间),通过日志分析工具(如 ELK Stack)追踪异常日志。 - 收集用户反馈,处理上线后紧急问题(如数据异常、功能遗漏)。 ### **六、维护与迭代阶段** **目标**:持续优化系统,响应用户需求变更。 **关键步骤**: 1. 日常维护 - 修复线上缺陷(如紧急 bug),定期更新依赖组件(如升级开源库版本,防范安全漏洞)。 2. 版本迭代 - 收集用户新需求或运营数据,评估后纳入下一版本开发计划(如新增功能、性能优化)。 - 重复 “需求分析→设计→开发→测试→部署” 流程,实现系统持续演进(如敏捷开发模式中的 Sprint 迭代)。 3. 技术复盘 - 项目结项后召开复盘会,总结开发过程中的经验教训(如需求变更频繁导致延期、技术方案缺陷),优化流程或工具链。 ### **常见开发模式补充** - **敏捷开发(Agile)**:适用于需求多变的项目,通过短周期迭代(如 2 周一个 Sprint)快速交付可运行版本,强调团队协作和用户反馈。 - **瀑布模型(Waterfall)**:适用于需求明确且稳定的项目,阶段分明(需求→设计→开发→测试→上线),后期变更成本较高。 - ** DevOps 模式 **:开发(Dev)与运维(Ops)团队协作,通过自动化工具链实现持续集成(CI)和持续部署(CD),提升交付效率。 通过标准化流程,团队可高效协作、控制风险,并确保项目按时、高质量交付。实际操作中需根据项目特点灵活调整,例如小型项目可简化部分评审流程,复杂项目则需强化跨团队沟通和风险管理。 # 说说你们软件开发团队人员组成(参考) 软件开发团队的人员组成会根据项目规模、技术复杂度和公司业务需求有所差异,以下是常见的核心角色及职责分工: ### **一、管理层 / 决策层** #### 1. **项目经理(Project Manager, PM)** - 职责 - 主导项目规划,制定时间表、分配任务,协调资源(人力、技术、预算)。 - 监控项目进度,管理风险(如需求变更、技术难题),确保项目按时交付。 - 沟通对接客户或内部需求方,推动需求澄清与验收。 - **能力要求**: 项目管理方法论(如敏捷开发、瀑布模型)、沟通协调能力、风险预判能力。 #### 2. **技术负责人 / 架构师(Technical Lead/Architect)** - 职责 - 设计系统架构(如分层架构、微服务架构),制定技术选型(编程语言、框架、数据库等)。 - 解决关键技术难题,指导开发团队实现技术方案,确保系统可扩展性、稳定性和安全性。 - **能力要求**: 深厚的技术功底、系统设计经验、前沿技术敏感度。 ### **二、核心开发团队** #### 1. **前端开发工程师(Frontend Developer)** - 职责 - 实现用户界面(UI)和用户体验(UX),开发网页、移动端或桌面端的可视化界面。 - 处理页面交互逻辑(如按钮点击、表单验证),优化性能(加载速度、兼容性)。 - **常用技术**: HTML/CSS/JavaScript、框架(React/Vue/Angular)、移动端开发(Flutter/React Native)。 #### 2. **后端开发工程师(Backend Developer)** - 职责 - 开发服务器端逻辑,设计数据库结构,实现 API 接口供前端调用。 - 处理业务逻辑(如用户认证、数据存储、订单流程),保障系统稳定性和安全性。 - **常用技术**: 编程语言(Java/Node.js/Python/Go 等)、数据库(MySQL/PostgreSQL/MongoDB)、云服务(AWS/Azure/ 阿里云)。 #### 3. **移动端开发工程师(Mobile Developer)** - 职责 - 开发 iOS 或 Android 原生应用,或跨平台应用(如 Flutter)。 - 对接后端接口,优化应用性能和续航,适配不同设备型号。 - 常用技术 - iOS:Swift/Objective-C、Xcode; - Android:Kotlin/Java、Android Studio; - 跨平台:Flutter/Dart、React Native。 #### 4. **全栈开发工程师(Full Stack Developer)** - **职责**: 同时负责前端和后端开发,独立完成小型项目或快速验证需求。 - **能力要求**: 前后端技术栈均有涉猎,适合初创团队或敏捷开发场景。 ### **三、测试与质量保障** #### 1. **测试工程师(QA Engineer)** - 职责 - 编写测试用例,执行功能测试、性能测试、兼容性测试、安全测试等。 - 跟踪和反馈缺陷,推动开发团队修复问题,确保软件质量。 - 分类 - 手动测试:侧重用户体验和场景覆盖; - 自动化测试:编写脚本(如 Selenium/Jenkins)提升测试效率。 #### 2. **自动化测试工程师(Automation Test Engineer)** - **职责**: 设计和维护自动化测试框架,实现持续集成 / 持续部署(CI/CD)流程,减少重复测试工作。 ### **四、运维与部署** #### 1. **运维工程师(DevOps Engineer/SRE)** - 职责 - 管理服务器、网络和云基础设施,确保系统稳定运行。 - 实现自动化部署(如 Docker/Kubernetes)、监控系统性能(如 Prometheus/Grafana)、处理故障应急。 - **能力要求**: 云服务管理、脚本编写(Shell/Python)、故障排查能力。 ### **五、其他支持角色** #### 1. **产品经理(Product Manager, PM)** - **职责**: 调研用户需求,制定产品路线图,编写需求文档(PRD),协调开发与设计团队实现功能。 - **关键能力**: 需求分析、竞品分析、跨部门协作。 #### 2. **UI/UX 设计师(UI/UX Designer)** - 职责 - 设计用户界面视觉风格(如色彩、图标、布局),输出高保真原型图(Figma/Sketch)。 - 优化用户体验流程(如交互逻辑、操作动线),提升产品易用性。 #### 3. **技术文档工程师(Technical Writer)** - **职责**: 编写开发文档、API 手册、用户指南等,确保技术信息清晰易懂,便于团队协作和用户使用。 ### **团队协作模式** - **敏捷开发(Agile Development)**: 通过短周期迭代(如 Scrum 的冲刺周期)、每日站会、看板管理等方式,快速响应需求变化,适合需求不确定的项目。 - **瀑布模型(Waterfall Model)**: 按阶段(需求分析→设计→开发→测试→部署→维护)线性推进,适合需求明确、流程规范的大型项目。 ### **总结** 一个完整的软件开发团队需覆盖**需求分析、技术设计、开发实现、测试验证、部署运维**全流程,不同角色各司其职又紧密协作。小型团队可能存在角色重叠(如全栈开发兼顾前后端、产品经理兼任项目经理),而大型团队则更注重专业化分工(如独立的安全工程师、数据工程师等)。实际配置需根据项目特性灵活调整。 # **面试加分技巧** 1. 主动提问 - 面试结束前可问:“这个岗位的核心技术挑战是什么?”“团队目前在做哪些项目?” 2. 结合项目经验 - 回答时尽量关联实际项目,例如:“在我们的电商项目中,为了解决库存超卖问题,我们使用了 Redis 分布式锁 + 数据库乐观锁(版本号控制)的方案。” 3. 展示思考过程 - 遇到难题时,先说出思路框架,再逐步细化,如:“这个问题可能需要分三步处理:首先... 其次... 最后...,其中可能遇到的难点是..., 我认为可以通过... 来解决。” # 阅读 1. [Redis布隆过滤器与布谷鸟过滤器](https://www.cnblogs.com/Courage129/p/14337466.html) 2. [distinct效率更高还是group by效率更高?](https://blog.csdn.net/weixin_42615847/article/details/118342524) 3. [Java 内存泄漏了,怎么排查?](https://mp.weixin.qq.com/s?__biz=MzIwNjg4MzY4NA==&mid=2247514509&idx=2&sn=a4242dd8613600900c2bb4b984510ab7&chksm=97182786a06fae906fba454aa688971f56e2f45cad09dc9a5e734ce9177c490c944927c0b929&scene=21#wechat_redirect) 4. [mysql 最大建议行数 2000w, 靠谱吗?](https://my.oschina.net/u/4090830/blog/5559454) 5. [MySQL为什么选择B+树作为索引结构](https://www.cnblogs.com/kismetv/p/11582214.html) 6. [说说 Cookie、Session、Token、JWT?](https://mp.weixin.qq.com/s?__biz=MzIwNjg4MzY4NA==&mid=2247513131&idx=2&sn=1960ce81cb2067452eb8a75ef1a9458b&chksm=97182a20a06fa336b5025954e98eb53e1439e6dfcf72769d39d13935eb374065ed051504bec7&scene=21#wechat_redirect) 7. [MySQL分库分表方案](https://www.cnblogs.com/littlecharacter/p/9342129.html) 8. [商品的SPU和SKU及其之间的关系](https://juejin.cn/post/7110553774069907492?searchId=20241227154328501691F50E973D2CFDD6) 9. [什么是SPU、SKU、SKC、ARPU](https://juejin.cn/post/7089036566315827237?searchId=20241227154328501691F50E973D2CFDD6) 10. [常见库存设计方案](https://juejin.cn/post/7210073831893008442?searchId=202412271604057DE88FC4DA7BD63239C5) 11. [电商库存系统的防超卖和高并发扣减方案](https://mp.weixin.qq.com/s/XvmQQ1_EtXKTm8J0k3K63Q) 12. [vivo全球商城:库存系统架构设计与实践](https://juejin.cn/post/7208793449427599418?searchId=202412271604057DE88FC4DA7BD63239C5) 13. [如何使用Redis实现电商系统的库存扣减?](https://juejin.cn/post/7049205829316116511?searchId=202412271604057DE88FC4DA7BD63239C5) 14. [十八张图,看懂八种分布式事务机制的全貌!](https://juejin.cn/post/7379262453728018447?searchId=202412271635046C7A2C0E97E0B9319AD4#heading-38) 15. [关于分布式锁的面试题](https://juejin.cn/post/6844904137172189198?searchId=2024122717272790C781CACD221D39E1A3) 16. [理解那些工作在分布式系统底层的一致性模型](https://juejin.cn/post/7352762468844830759?searchId=20241227171139387CDC9776EE343097D2) 17. [分布式之分布式ID篇:UUID、雪花算法、ID生成器、号段模式](https://juejin.cn/post/7406997325716291595) 18. [Java泛型](https://juejin.cn/post/7402076531294470144) 19. [分布式的基础理论CAP和核心理论BASE](https://juejin.cn/post/7345821800880422975) 20. [创建线程的方式](https://juejin.cn/post/7241395267797942329) 21. [SQL 优化](https://juejin.cn/post/7374986809477185547) 22. [JVM](https://juejin.cn/post/7373897666096627723) 23. [Elasticsearch](https://juejin.cn/post/7372393680597221402) 24. [Redis](https://juejin.cn/post/7372472076047155251) 25. [Nginx](https://juejin.cn/post/7370713457454612507) 26. [MySQL](https://juejin.cn/post/7370184763678670857) 27. [说说真实Java项目的开发流程,以及面试前的项目准备说辞](https://zhuanlan.zhihu.com/p/574240101) 28. [项目开发的详细步骤](https://blog.csdn.net/weixin_40482577/article/details/138139877) # 参考 ## 二叉树、平衡二叉树、B-Tree、B+Tree ### 二分搜索树 #### 概念 ​ 二分搜索树(英语:Binary Search Tree),也称为 二叉查找树 、二叉搜索树 、有序二叉树或排序二叉树。简写BST,是满足某些条件的特殊二叉树。 一棵二叉树,可以为空;如果不为空,满足以下性质: * 非空左子树的所有键值小于其根结点的键值。 * 非空右子树的所有键值大于其根结点的键值。 * 左、右子树都是二叉搜索树。 示例如图: ![img](assets/PbZvFQEItGIFirEP.png) #### 二分查找法 查找结点值的方法就是二分查找法:查找次数就是树的高度。二叉查找树可以任意地构造 如果向一方倾斜的二叉树是不平衡的,查询效率就低了,二叉查找树变成了一个链表,如下: ![img](assets/FUFgJIP8BBaptBY7.png) > 二叉搜索树的查找操作是和这棵树的高度相关的。如上右图,二叉搜索树为右斜树, 其退化成了单链表,执行效率降低为O(n)。 在此二叉搜索树中查找元素 6 需要查找 6 次 > > 二叉搜索树的查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率。同样的序列 将其改为上图左 的方式存储,查找元素 6 时只需比较 3 次,查找效率提升一倍。 > > 可以看出当节点数目一定,保持树的左右两端保持平衡,树的查找效率最高。 ### 平衡二叉树 #### 概念 **平衡二叉查找树**:简称平衡二叉树。由前苏联的数学家 **A**delse-**V**elskil 和 **L**andis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。它具有如下几个性质: * 可以是空树。 * 假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过 1。 平衡之意,如天平,即两边的分量大约相同。 #### 平衡因子 **定义:**某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor),平衡二叉树中不存在平衡因子大于 1 的节点。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左子树比较高,右子树比较高。 > 平衡因子=左树高度-右树高度 ##### 平衡因子为0 image-20240129101023484 ##### 平衡因子为1 image-20240129101047756 ##### 平衡因子为-1 image-20240129101131992 #### 示例1 image-20240129102617219 左边是AVL树,它的任何节点的两个子树的高度差<=1;右边的不是AVL树,其根节点的左子树高度为3,而右子树高度为1; #### 示例2 ##### 正例 image-20240129101417755 ##### 反例 在此平衡二叉树插入节点 99 ,树结构变为: ![动图](assets/v2-2ddb0f9b832fff594e294dffc299b373_b.webp) 节点 66 的左子树高度为 1,右子树高度为 3,此时平衡因子为 -2,树失去平衡。 ### B树(B-Tree) B树和平衡二叉树不同,B树属于**多叉树**又名**平衡多路查找树**(查找路径不只两个),数 据结构。 B-Tree是为磁盘等外存储设备设计的一种平衡查找树 * 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 * InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。InnoDB存储引擎中默认每个页的大小为16KB,可通过参数innodb_page_size将页的大小设置为4K、8K、16K,在MySQL中可通过如下命令查看页的大小 ![image-20240129103424995](assets/image-20240129103424995.png) 而系统一个磁盘块的存储空间往往没有这么大,因此InnoDB每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小16KB。InnoDB在把磁盘数据读入到内存时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。 B-Tree结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同。 B-Tree中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个3阶的B-Tree: > 一棵m阶(M阶代表一个树节点最多有多少个查找路径,M=M路,当M=2则是2叉树,M=3则是3叉)的B-Tree有如下特性: > > 1. 每个节点最多有m个孩子。 > 2. 除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子。 (ceil()是个朝正无穷方向取整的函数,如ceil(1.1)结果为2) > 3. 若根节点不是叶子节点,则至少有Ceil(m/2)个孩子 > 4. 所有叶子节点都在同一层,且不包含其它关键字信息 > 5. 每个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn) > 6. 关键字的个数n满足:ceil(m/2)-1 <= n <= m-1 > 7. ki(i=1,…n)为关键字,且关键字升序排序。 > 8. Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1) ![索引](assets/20160202204827368.png) 每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分成的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。 模拟查找关键字29的过程: 1. 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】 2. 比较关键字29在区间(17,35),找到磁盘块1的指针P2。 3. 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】 4. 比较关键字29在区间(26,30),找到磁盘块3的指针P2。 5. 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】 6. 在磁盘块8中的关键字列表中找到关键字29。 分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作。由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。而3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。 ### B+Tree B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。 ![image-20240129105345366](assets/image-20240129105345366.png) 从上面B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。 在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。 B+Tree相对于B-Tree有几点不同: 1. 非叶子节点只存储键值信息。 2. 所有叶子节点之间都有一个链指针。 3. 数据记录都存放在叶子节点中。 由于B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构如下图所示: ![img](assets/20160202205105560.png) 通常在B+Tree上有两个指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。 因此可以对B+Tree进行两种查找运算: * 一种是对于主键的范围查找和分页查找 * 一种是从根节点开始,进行随机查找。 可能上面例子中只有22条数据记录,看不出B+Tree的优点,下面做一个推算: InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为〖10〗^3)。也就是说一个深度为3的B+Tree索引可以维护10^3 * 10^3 * 10^3 = 10亿 条记录。 实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2\~4层。mysql的InnoDB存储引擎在设计时**是将根节点常驻内存**的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作。 数据库中的B+Tree索引可以分为聚集索引(clustered index)和辅助索引(secondary index)。上面的B+Tree示例图在数据库中的实现即为聚集索引,聚集索引的B+Tree中的叶子节点存放的是整张表的行记录数据。辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的聚集索引键,即主键。当通过辅助索引来查询数据时,InnoDB存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据。 ### 总结 B+Tree 结构是从二叉查找树,平衡二叉树和B-Tree这三种数据结构演化来的,大致如下: * 二叉查找树是基于二分查找法来提高数据查找速度的二叉树的数据结构,减少无关数据的检索,提升了数据检索的速度。非叶子节点只能允许最多两个子节点存在,每一个非叶子节点数据分布规则为左边的子节点小当前节点的值,右边的子节点大于当前节点的值,每个节点只存储一个键值和数据的。 * 平衡二叉树满足二叉查找树特性的基础上,如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过 1。 * B-Tree和平衡二叉树不同,B-Tree属于多叉树又名平衡多路查找树, B-Tree相对于平衡二叉树,每个节点存储了更多的键值(key)和数据(data),并且每个节点拥有更多的子节点。 * B+Tree和B-Tree不同,B+Tree在非叶子节点上,不保存数据,只存储指针,能存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的IO次数有会再次减少,数据查询的效率也会更快。并且B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。那么B+树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。 ### 参考 * [深入理解(二叉树、平衡二叉树、B-Tree、B+Tree )的区别](https://zhuanlan.zhihu.com/p/270389432) * [BTree和B+Tree详解](https://blog.csdn.net/yin767833376/article/details/81511377) ## 哈希索引和B+树索引的区别 ### Hash索引概念 ​ 说到Hash,老铁们很容易联想到HashMap,没错,Hash索引的结构和HashMap相类似,键值 key 通过 Hash 映射找到桶 bucket。在这里桶(bucket)指的是一个能存储一条或多条记录的存储单位。一个桶的结构包含了一个内存指针数组,桶中的每行数据都会指向下一行,形成链表结构,当遇到 Hash 冲突时,会在桶中进行键值的查找。 InnoDB中采用除法散列函数(取余法),冲突机制采用链接法 ### Hash索引和B+tree索引查询效率 ​ 采用 Hash 进行检索效率非常高,基本上一次检索就可以找到数据,而 B+ 树需要自顶向下依次查找,多次访问节点才能找到数据,中间需要多次 I/O 操作,理论上来说 Hash 比 B+ tree更快。下图是引用网上的Hash索引图片和 B+tree 索引图片,便于直观的理解2种索引结构。 1、Hash索引图片 ![img](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzg0MTY5Mw==,size_16,color_FFFFFF,t_70.png) B+tree索引图片 ![img](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzg0MTY5Mw==,size_16,color_FFFFFF,t_70-1706498517840-13.png) ### Hash索引和B+tree索引的区别 * 在查询速度上,如果是等值查询,那么Hash索引明显有绝对优势,因为只需要经过一次 Hash 算法即可找到相应的键值,复杂度为O(1);当然了,这个前提是键值都是唯一的。如果键值不是唯一(或存在Hash冲突),就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据,这时候复杂度会变成O(n),降低了Hash索引的查找效率。所以,Hash 索引通常不会用到重复值多的列上,比如列为性别、年龄的情况等(当然B+tree索引也不适合这种离散型低的字段上) * Hash 索引是无序的,如果是范围查询检索,这时候 Hash 索引就无法起到作用,即使原先是有序的键值,经过 Hash 算法后,也会变成不连续的了。 > 1. Hash 索引只支持等值比较查询、无法索成范围查询检索,B+tree索引的叶子节点形成有序链表,便于范围查询。 > 2. Hash 索引无法做 like ‘xxx%’ 这样的部分模糊查询,因为需要对 完整 key 做 Hash 计算,定位bucket。而 B+tree 索引具有最左前缀匹配,可以进行部分模糊查询 > 3. Hash索引中存放的是经过Hash计算之后的Hash值,而且Hash值的大小关系并不一定和Hash运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算。B+tree 索引的叶子节点形成有序链表,可用于排序。 * Hash 索引不支持多列联合索引,对于联合索引来说,Hash 索引在计算 Hash 值的时候是将索引键合并后再一起计算 Hash 值,不会针对每个索引单独计算 Hash 值。因此如果用到联合索引的一个或者几个索引时,联合索引无法被利用; * 因为存在哈希碰撞问题,在有大量重复键值情况下,哈希索引的效率极低。B+tree 所有查询都要找到叶子节点,性能稳定; ### 场景区分 * 大多数场景下,都会有组合查询,范围查询、排序、分组、模糊查询等查询特征,Hash 索引无法满足要求,建议数据库使用B+树索引。 * 在离散型高,数据基数大,且等值查询时候,Hash索引有优势。 ## 分库分表 ### 单库单表存在的问题 假设你要设计一个电商网站,在一开始,User表、Order表、Product表等等各种表都在同一个数据库中,每个表都包含了大量的字段。在用户量比较少,访问量也比较少的时候,单库单表不存在问题。 但是公司可能发展的比较好,用户量开始大量增加,业务也越来越繁杂。一张表的字段可能有几十个甚至上百个,而且一张表存储的数据还很多,高达几千万数据,更难受的是这样的表还挺多。于是一个数据库的压力就太大了,一张表的压力也比较大。试想一下,我们在一张几千万数据的表中查询数据,压力本来就大,如果这张表还需要关联查询,那时间等等各个方面的压力就更大了。 * 单库太大:数据库里面的表太多,所在服务器磁盘空间装不下,IO次数多CPU忙不过来。 * 单表太大:一张表的字段太多,数据太多。查询起来困难。 此时就开始考虑如何解决问题了。 ### 主从复制架构 单库单表下越来越不满足需求,此时我们先考虑进行读写分离。我们将数据库的写操作和读操作进行分离, 使用多个从库副本(Slaver)负责读,使用主库(Master)负责写, 从库从主库同步更新数据,保持数据一致。 这在一定程度上可以解决问题,但是用户超级多的时候,比如几个亿用户,此时写操作会越来越多,一个主库(Master)不能满足要求了,那就把主库拆分,这时候为了保证数据的一致性就要开始进行同步,此时会带来一系列问题: 1. 写操作拓展起来比较困难,因为要保证多个主库的数据一致性。 2. 复制延时:意思是同步带来的时间消耗。 3. 锁表率上升:读写分离,命中率少,锁表的概率提升。 4. 表变大,缓存率下降:此时缓存率一旦下降,带来的就是时间上的消耗。 注意,此时主从复制还是单库单表,只不过复制了很多份并进行同步。 主从复制架构随着用户量的增加、访问量的增加、数据量的增加依然会带来大量的问题,那就要考虑换一种解决思路。就是今天所讲的主题,分库分表。 ### 分库分表 不管是分库还是分表,都有两种切分方式:水平切分和垂直切分。下面我们分别看看如何切分。 #### 分表 ##### 垂直分表 > 表中的字段较多,一般将不常用的、 数据较大、长度较长的拆分到“扩展表“。一般情况加表的字段可能有几百列,此时是按照字段进行数竖直切。注意垂直分是列多的情况。 ![img](assets/v2-cd623744d880d155a6c513079e52b7af_r.jpg) * 概念:以**字段**为依据,按照字段的活跃性,将**表**中字段拆到不同的**表**(主表和扩展表)中。 * 结果: > - 每个**表**的**结构**都不一样; > - 每个**表**的**数据**也不一样,一般来说,每个表的**字段**至少有一列交集,一般是主键,用于关联数据; > - 所有**表**的**并集**是全量数据; * 场景:系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。 * 分析:可以用列表页和详情页来帮助理解。垂直分表的拆分原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据。 ##### 水平分表 > 单表的数据量太大。按照某种规则(RANGE,HASH取模等),切分到多张表里面去。 但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。这种情况是不建议使用的,因为数据量是逐渐增加的,当数据量增加到一定的程度还需要再进行切分。比较麻烦。 ![img](assets/v2-5a5447ec26f1391c00fcb1c97e416433_720w.webp) * 概念:以**字段**为依据,按照一定策略(hash、range等),将一个**表**中的数据拆分到多个**表**中。 * 结果: > - 每个**表**的**结构**都一样; > - 每个**表**的**数据**都不一样,没有交集; > - 所有**表**的**并集**是全量数据; * 场景:系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。 * 分析:表的数据量少了,单次SQL执行效率高,自然减轻了CPU的负担。 #### 分库 ##### 垂直分库 > 一个数据库的表太多。此时就会按照一定业务逻辑进行垂直切,比如用户相关的表放在一个数据库里,订单相关的表放在一个数据库里。注意此时不同的数据库应该存放在不同的服务器上,此时磁盘空间、内存、TPS等等都会得到解决。 ![img](assets/v2-6484e798eeb54df53020af70c4d6645d_r.jpg) * 概念:以**表**为依据,按照业务归属不同,将不同的**表**拆分到不同的**库**中。 * 结果: > - 每个**库**的**表结构**都不一样; > - 每个**库**的**数据**也不一样,没有交集; > - 所有**库**的**并集**是全量数据; * 场景:系统绝对并发量上来了,并且可以抽象出单独的业务模块。 * 分析:到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化。 ##### 水平分库 > 水平分库理论上切分起来是比较麻烦的,它是指将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。 水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。 ![img](assets/v2-c6de98fbf6dd9cd1cef6ced803294f8e_r.jpg) * 概念:以**字段**为依据,按照一定策略(hash、range等),将一个**库**中的数据拆分到多个**库**中。 * 结果 > - 每个**库**的**结构**都一样; > - 每个**库**的**数据**都不一样,没有交集; > - 所有**库**的**并集**是全量数据; * 场景:系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。 * 分析:库多了,io和cpu的压力自然可以成倍缓解。 ### 分库分表之后的问题 #### 联合查询困难 联合查询不仅困难,而且可以说是不可能,因为两个相关联的表可能会分布在不同的数据库,不同的服务器中。 #### 需要支持事务 分库分表后,就需要支持分布式事务了。数据库本身为我们提供了事务管理功能,但是分库分表之后就不适用了。如果我们自己编程协调事务,代码方面就又开始了麻烦。 #### 跨库join困难 分库分表后表之间的关联操作将受到限制,我们无法join位于不同分库的表,也无法join分表粒度不同的表, 结果原本一次查询能够完成的业务,可能需要多次查询才能完成。 我们可以使用全局表,所有库都拷贝一份。 #### 结果合并麻烦 比如我们购买了商品,订单表可能进行了拆分等等,此时结果合并就比较困难。 ### 分库分表工具 MySQL分库分表工具是用于帮助实现MySQL数据库分库分表功能的软件或中间件,它们通过数据路由、负载均衡、数据分片等方式,提高数据库的性能、可扩展性和可维护性。以下是一些常见的MySQL分库分表工具及其特点: #### 1. ShardingSphere - **概述**:ShardingSphere是一款开源的数据库中间件,前身是Sharding-JDBC。它提供了数据分片、分布式事务和数据库治理等核心能力。 - 特点: - 支持多种数据库,如MySQL、PostgreSQL、Oracle等。 - 提供丰富的分片策略,如哈希分片、范围分片、一致性哈希等。 - 支持分布式事务、读写分离、缓存等高级功能。 - 社区活跃,文档齐全,易于上手。 - 适用于各种复杂业务场景。 #### 2. MyCAT - **概述**:MyCAT是一款开源的数据库中间件,专注于数据库分库分表和读写分离。 - 特点: - 实现了MySQL协议的服务器,前端用户可以把它看作是一个数据库代理。 - 提供了数据多节点之间的读写分离功能。 - 支持自动故障切换与恢复,具有高可用性。 - 提供了强大的数据路由功能,支持复杂的数据分片规则。 #### 3. TDDL(Taobao Distribute Data Layer) - **概述**:TDDL是淘宝开发的分布式数据层,用于处理大规模分布式数据库的访问问题。 - 特点: - 提供了数据分库分表、读写分离、数据迁移等能力。 - 针对淘宝业务场景进行了优化,具有较高的性能和稳定性。 - 支持多种分片策略,可以根据业务需求灵活选择。 #### 4. MySQL自带的分库分表功能 - **概述**:从MySQL 5.7版本开始,MySQL支持在线DDL操作,可以实现简单的分库分表。 - 特点: - 无需额外安装中间件,可以直接在MySQL数据库上进行操作。 - 适合小型项目或简单业务场景。 - 需要注意的是,MySQL自带的分库分表功能相对简单,可能无法满足复杂业务场景的需求。 #### 选择建议 在选择MySQL分库分表工具时,需要根据具体的业务需求、数据规模、系统架构等因素进行综合考虑。如果业务场景复杂、数据量大、需要高性能和高可用性,建议选择ShardingSphere或MyCAT等专业的数据库中间件。如果业务场景相对简单、数据量较小,可以考虑使用MySQL自带的分库分表功能或第三方提供的轻量级解决方案。 ### 参考 [MySQL:互联网公司常用分库分表方案汇总](https://zhuanlan.zhihu.com/p/137368446) # 收集(参考) ## **集合** - 经常用到哪些 Map - 这几种 Map 的区别 - CocurrentHashMap 怎么保证线程安全 - CocurrentHashMap 在 JDK 1.8 前后的锁有什么区别 - 聊下 HashMap 的原理 - HashMap 在 Put 时,新链表节点是放在头部还是尾部 - HashMap 扩容时的流程 - HashMap 在 JDK 1.8 有什么改变 - CocurrentHashMap 在 JDK 1.8 有什么改变 - TreeMap 的原理 - Map、List、Set 分别说下你知道的线程安全类和线程不安全的类 ## **多线程、锁** - 线程池使用的是哪种 - 线程池参数怎么配置 - 线程池各个参数的作用 - 线程池的参数配置要注意什么 - 线程池的工作流程 - JDK 中的并发类知道哪些 - AQS 的底层原理 - 介绍下悲观锁和乐观锁 - 使用过哪些锁 - synchronized 和 Lock 的区别、使用场景 - synchronized 原理 - synchronized 作用于静态方法、普通方法、this、Lock.class 的区别 - 为什么引入偏向锁、轻量级锁,介绍下升级流程 - 死锁的必要条件,如何预防死锁 - 介绍下 CountDownLatch 和 CyclicBarrier - 介绍下 CAS,存在什么问题 - 介绍下 ThreadLocal,存在什么问题 ## **网络** - HTTPS 是怎么加密的 - 普通 Hash 和一致性 Hash 原理 - 一致性 Hash 的缺点 - TCP 三次握手过程,为什么需要三次握手 - 为什么 TIME_WAIT 状态需要经过 2MSL 才能返回到 CLOSE 状态 - TCP 的拥塞控制 - TCP 如何解决流控、乱序、丢包问题 - 为什么会出现粘包和拆包,如何解决 ## **Spring、Mybatis** - Mybatis 中 # 和 $ 的区别 - 怎么防止 SQL 注入 - 使用 Mybatis 时,调用 DAO(Mapper)接口时是怎么调用到 SQL 的 - 介绍下 Spring IoC 的流程 - BeanFactory 和 FactoryBean 的区别 - Spring 的 AOP 是怎么实现的 - Spring 的事务传播行为有哪些,讲下嵌套事务 - 什么情况下对象不能被代理 - Spring 怎么解决循环依赖的问题 - 要在 Spring IoC 容器构建完毕之后执行一些逻辑,怎么实现 - @Resource 和 @Autowire 的区别 - @Autowire 怎么使用名称来注入 - bean 的 init-method 属性指定的方法里用到了其他 bean 实例,会有问题吗 - @PostConstruct 修饰的方法里用到了其他 bean 实例,会有问题吗 - Spring 中,有两个 id 相同的 bean,会报错吗,如果会报错,在哪个阶段报错 - Spring 中,bean 的 class 属性指定了一个不存在的 class,会报错吗,如果会报错,在哪个阶段 - Spring 中的常见扩展点有哪些 ## **MySQL** - MySQL 索引的数据结构 - 为什么使用 B+ 树,与其他索引相比有什么优点 - 各种索引之间的区别 - B+ 树在进行范围查找时怎么处理 - MySQL 索引叶子节点存放的是什么 - 联合索引(复合索引)的底层实现 - MySQL 如何锁住一行数据 - SELECT 语句能加互斥锁吗 - 多个事务同时对一行数据进行 SELECT FOR UPDATE 会阻塞还是异常 - MySQL 使用的版本和执行引擎 - MySQL 不同执行引擎的区别 - MySQL 的事务隔离级别 - MySQL 的可重复读是怎么实现的 - MySQL 是否会出现幻读 - MySQL 的 gap 锁 - MySQL 的主从同步原理 - 分库分表的实现方案 - 分布式唯一 ID 方案 - 如何优化慢查询 - explain 中每个字段的意思 - explain 中的 type 字段有哪些常见的值 - explain 中你通常关注哪些字段,为什么 ## **JVM** - 运行时数据区 - 服务器使用的什么垃圾收集器 - CMS 垃圾收集的原理 - G1 垃圾收集的特点,为什么低延迟 - 有哪些垃圾回收算法,优缺点 - 哪些对象可以作为 GC Roots - 有哪些类加载器 - 双亲委派模式,哪些场景是打破双亲委派模式 - 线上服务器出现频繁 Full GC,怎么排查 - 定位问题常用哪些命令 - 介绍下 JVM 调优的过程 ## **Kafka** - 为什么使用 Kafka - 介绍下 Kafka 的各个组件 - 如何保证写入 Kafka 的数据不丢失 - 如何保证从 Kafka 消费的数据不丢失 - Kafka 为什么性能这么高 - 零拷贝技术使用哪个方法实现 - Java 中也有类似的零拷贝技术,是哪个方法 - Kafka 怎么保证消息的顺序消费 - Kafka 怎么避免重复消费 - 什么是 HighWatermark 和 LEO - 什么是 ISR,为什么需要引入 ISR ## **Redis** - 项目中使用的 Redis 版本 - Redis 在项目中的使用场景 - Redis 怎么保证高可用 - Redis 的选举流程 - Redis 和 Memcache 的区别 - Redis 的集群模式 - Redis 集群要增加分片,槽的迁移怎么保证无损 - Redis 分布式锁的实现 - Redis 删除过期键的策略 - Redis 的内存淘汰策略 - Redis 的 Hash 对象底层结构 - Redis 中 Hash 对象的扩容流程 - Redis 的 Hash 对象的扩容流程在数据量大的时候会有什么问题吗 - Redis 的持久化机制有哪几种 - RDB 和 AOF 的实现原理、优缺点 - AOF 重写的过程 - 哨兵模式的原理 - 使用缓存时,先操作数据库还是先操作缓存 - 为什么是让缓存失效,而不是更新缓存 - 缓存穿透、缓存击穿、缓存雪崩 - 更新缓存的几种设计模式 ## **Zookeeper** - Zookeeper 的使用场景 - Zookeeper 怎么实现分布式锁 - Zookeeper 怎么保证数据的一致性 - ZAB 协议的原理 - Zookeeper 遵循 CAP 中的哪些 - Zookeeper 和 Eureka 的区别 - Zookeeper 的 Leader 选举 - Observer 的作用 - Leader 发送了 commit 消息,但是所有的 follower 都没有收到这条消息,Leader 就挂了,后续会怎么处理 ## **分布式** - CAP 理论 - BASE 理论 - 分布式事务 2PC 和 TCC 的原理 - TCC 在 cancel 阶段如果出现失败怎么处理