Java知识点整理
✨1、在jdk1.8中HashMap的数组、链表或红黑树存放键值对吗
数组并不直接存放键值对,数组的每个元素称为桶(bucket),每个桶可能是一个链表,也可能是一个红黑树。每个数组元素是一个链表的头节点或红黑树的根节点,用来解决哈希冲突。链表或红黑树节点才是存放键值对的具体位置。
在 Java 中的 HashMap 中,链表(或红黑树节点)存放的是键值对(key-value pair),其中键和值都是作为节点的属性存储的。链表节点的结构类似于下面的示例:
class Node<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next;
Node(int hash, K key, V value, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
✨2、hashcode()和equals()
equals一定hashcode,hashcode不一定equals
- 如果两个对象相等,则hashcode一定也是相同的(重写了equals方法和hashcode方法);
- 两个对象相等,对两个对象分别调用equals方法都返回true;
- hashcode值相同,两个对象不一定相等equals;
- hashcode值不同,两个对象也可能相等equals(只重写了equals()的方法,没有重写hashcode方法,这种情况比较少);
如果两个对象通过equals
方法比较相等,那么这两个对象的hashCode
值必须相同。
✨3、可以只重写equals方法不重写hashcode方法吗
在Java中,理论上是可以只重写equals方法而不重写hashCode方法的。然而,这样做可能会导致一些潜在的问题,因为它违反了Object类对hashCode和equals方法的约定。
根据Object类的文档:
- 如果两个对象根据equals方法是相等的,那么它们的hashCode值应该相等。
- 如果一个类重写了equals方法,就必须同时重写hashCode方法,以确保对象在放入哈希表等数据结构时能够正确工作。
如果一个类只重写了equals方法而没有重写hashCode方法,那么它会继承自Object类的hashCode方法,该方法是基于对象的内存地址计算的,这意味着相等的对象可能具有不同的hashCode值,这违反了hashCode与equals方法之间的关系。
✨4、Java中线程池的状态有哪些
在Java中,线程池的状态通常由ThreadPoolExecutor类的几个状态常量表示,主要包括以下几种状态:
- RUNNING:线程池处于运行状态,接受新任务并处理排队的任务。
- SHUTDOWN:线程池处于关闭状态,不再接受新任务,但会继续处理已经提交的任务。
- STOP:线程池处于停止状态,不再接受新任务,不处理已经提交的任务,并会中断正在执行的任务。
- TIDYING:所有任务都已经终止,工作线程数量为0,线程转换为TIDYING状态将运行terminated()钩子方法。
- TERMINATED:线程池处于终止状态,terminated()方法完成后就会到达这个状态。
这些状态常量可以通过ThreadPoolExecutor类的get方法来获取线程池当前的状态。
✨5、Java中线程的状态有哪些
在Java中,线程的状态通常由Thread类的State枚举表示,主要包括以下几种状态:
- NEW(新建):线程对象已经创建,但尚未启动。
- RUNNABLE(可运行):线程正在Java虚拟机中执行,可能正在等待CPU时间片。
- BLOCKED(被阻塞):线程被阻塞等待监视器锁,例如在synchronized块或方法内部尝试获取锁时。
- WAITING(等待):线程无限期地等待另一个线程执行特定操作,直到其他线程显式地唤醒它。
- TIMED_WAITING(计时等待):线程在等待一段有限的时间,时间到达后会自动恢复。
- TERMINATED(终止):线程执行完毕或者因异常退出,处于终止状态。
通过Thread类的getState()方法可以获取线程的状态,了解线程的状态有助于我们对线程的行为进行监控和调试。
注:
- 也有一种解释把就绪状态加进来了
就绪(Runnable):当调用start()方法启动线程后,线程进入就绪状态。表示线程已经准备好运行,等待CPU的调度执行。
- Java中线程池的状态和线程的状态是不同的概念,线程池的状态通常指的是线程池本身的状态,而线程的状态指的是具体线程的状态。
✨6、什么情况索引失效
(1)违反最左前缀法则,索引会失效。如果索引了多列,要遵守最左前缀法则(指的是查询从索引的最左前列开始,并且不跳过索引中的列)。
(2)使用范围查询时,右边的列不能使用索引(索引失效)
(3)不要在索引列上进行运算操作, 索引将失效
(4)字符串不加单引号,造成索引失效。(类型转换)
(5)以%开头的Like模糊查询,索引失效
✨7、前缀索引
前缀索引(Prefix Index)是一种索引策略,它只对索引列的前缀部分进行索引,而不是对整个列的值进行索引。通过将索引限制在列值的前缀部分,可以减少索引的存储空间和维护成本。
假设有一个包含用户姓名和姓氏的表,其中姓名列包含较长的字符串。如果希望对姓名列进行索引以支持查询操作,但又不希望索引占用过多的存储空间,这时就可以考虑使用前缀索引。
假设有如下的用户表结构:
CREATE TABLE users (
id INT PRIMARY KEY,
first_name VARCHAR(100),
last_name VARCHAR(100)
);
现在希望对 first_name
列进行索引以支持查询操作。如果使用普通的索引,会占用较大的存储空间,而且维护成本也比较高。为了减少存储空间和提高查询性能,可以考虑使用前缀索引来只对 first_name
列的前缀部分进行索引。
CREATE INDEX idx_first_name_prefix ON users (first_name(10));
在上面的例子中,创建了一个名为 idx_first_name_prefix
的索引,它只对 first_name
列的前 10 个字符进行索引。通过限制索引长度,可以减少索引占用的存储空间,并提高查询性能。
✨8、复合索引(联合索引)
复合索引(也称为复合索引或多列索引)指的是在数据库中同时对多个字段创建的索引,底层数据结构是B+树。与单个字段索引相比,复合索引涵盖了多个字段的数值组合,可以提高某些查询的性能和效率。
当数据库表中的查询经常涉及多个字段的组合条件时,使用复合索引可以加快这些查询的速度。通过在多个字段上创建索引,数据库系统可以更快地定位符合所有条件的记录,而不是逐一扫描整个表。
但需要注意的是,复合索引也有一些限制和注意事项:
- 复合索引的字段顺序很重要:通常应该将区分度最高的字段放在前面,以便数据库系统更快地过滤数据。
- 不要创建过多的复合索引:过多的复合索引可能会增加数据库维护和存储成本。
- 考虑查询的频率和模式:只有当查询中经常用到的字段组合才适合创建复合索引。
✨9、最左前缀法则
最左前缀法则(The Leftmost Prefix Rule)是在数据库查询优化中使用的一种规则。它指的是在复合索引(Composite Index)中,只有最左侧的连续列被用于查询时,索引才能够被充分利用。
当使用复合索引时,索引的键由多个列组成。根据最左前缀法则,在查询时,只有按照索引键的最左侧连续列进行查询,索引才会被有效地使用。也就是说,如果查询条件中的列不是索引的最左侧连续列,那么索引将不会被充分利用,可能需要进行全表扫描或者额外的索引查找操作,从而导致查询性能下降。
举个例子,假设有一个复合索引 (col1, col2, col3),按照最左前缀法则,以下查询可以有效利用索引:
SELECT * FROM table WHERE col1 = 'value';
SELECT * FROM table WHERE col1 = 'value' AND col2 = 'value';
SELECT * FROM table WHERE col1 = 'value' AND col2 = 'value' AND col3 = 'value';
但是以下查询将无法充分利用索引,可能需要进行全表扫描或其他额外的操作:
SELECT * FROM table WHERE col2 = 'value';
SELECT * FROM table WHERE col2 = 'value' AND col3 = 'value';
SELECT * FROM table WHERE col3 = 'value';
在设计复合索引时,应根据查询的需求和频率来选择最适合的索引顺序。**最常用的列应该放在最左侧,以便索引能够被充分利用。**如果查询中经常使用的列不是索引的最左侧连续列,可能需要重新考虑索引的设计或者调整查询语句的顺序。
总结起来,最左前缀法则指的是在复合索引中,只有按照索引键的最左侧连续列进行查询,索引才能够被充分利用。在设计复合索引和编写查询语句时,需要考虑最左前缀法则以提高查询性能。
✨10、调用第三方api时有遇到什么难点
- 文档不清晰或不完整:有些第三方API的文档可能不够清晰或者不完整,导致开发者难以理解如何正确地调用API。
- 认证和授权:有些API需要进行认证和授权才能访问,这涉及到处理API密钥、令牌等安全性问题。
- 错误处理:调用第三方API时可能会出现各种错误情况,包括网络错误、超时、无效参数等,需要合理地处理这些错误并进行适当的重试机制。
- 版本兼容性:一些第三方API可能会更新版本,而旧版本的API可能会废弃或不再支持。因此需要确保使用的API版本与代码兼容。
- 性能优化:有时调用第三方API可能会影响应用程序的性能,需要考虑如何进行异步调用、缓存结果等优化策略。
- 数据格式处理:第三方API返回的数据可能是各种格式(如JSON、XML等),需要解析和处理这些数据并转换成Java对象。
- 异常处理:需要谨慎处理调用第三方API可能抛出的异常,以保证系统的稳定性和健壮性。
- 依赖管理:对于依赖较多的API,可能需要管理各种依赖库的版本冲突和兼容性。
✨11、Java实现快速排序
public class QuickSort {
public static void main(String[] args) {
int[] arr = {5, 2, 6, 1, 3, 4};
quickSort(arr, 0, arr.length - 1);
System.out.println("排序结果:" + Arrays.toString(arr));
}
// 快速排序的主要方法
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int partitionIndex = partition(arr, low, high);
quickSort(arr, low, partitionIndex - 1);
quickSort(arr, partitionIndex + 1, high);
}
}
// 分区方法
public static int partition(int[] arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准点
int i = low - 1; // i指向小于基准点的元素的位置
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
// 交换array[i]和array[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 将基准点放到正确的位置上,交换arr[i+1]和arr[high]
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
}
✨12、倒排索引
倒排索引(Inverted Index)是一种用于快速检索文档的数据结构,常用于搜索引擎等信息检索系统中。倒排索引的基本思想是将文档中的每个词(或者称为项)映射到包含该词的文档列表中。
具体来说,对于一组文档,首先会对文档进行分词处理,将文档中的词语提取出来,并建立词语与文档的映射关系。然后,倒排索引会维护一个词语到文档ID(搜索的关键子到文档ID)的映射表,以及每个文档ID对应的词语列表。这样,在进行搜索时,只需查找包含指定词语的文档ID,即可快速找到包含该词语的文档。
倒排索引的优点是能够快速进行全文搜索,适用于大规模的文档库。它可以有效地减少搜索时间,提高搜索效率,是搜索引擎等信息检索系统中常用的技术之一。
✨13、Java中注解的底层原理实现
在 Java 中,注解的底层原理是通过反射(Reflection)机制来实现的。当我们在代码中使用注解时,编译器会将注解信息保存在编译后的 .class 文件中,然后在运行时通过反射机制读取这些注解信息。
具体来说,注解的底层原理实现主要包括以下几个步骤:
- 定义注解:通过 @interface 关键字定义注解,并在注解中定义元素(属性)。
- 编译器处理:编译器在编译时会将注解信息保存到 .class 文件的元数据中。
- 运行时反射:在程序运行时,通过反射机制可以获取类、方法、字段等元素上的注解信息。
- 解析注解:通过反射 API 可以获取注解的类型、属性值等信息,并根据需要进行相应的处理逻辑。
通过反射机制,我们可以在运行时动态地获取和处理注解信息,实现类似配置、自动化处理等功能。注解在许多框架和库中发挥着重要作用,如 Spring 框架中的依赖注入、MyBatis 中的 SQL 映射等。
✨14、SpringBoot的生命周期
Spring Boot的生命周期主要围绕Spring Boot应用的启动和关闭两个过程。以下是Spring Boot生命周期的详细说明:
初始化环境:Spring Boot首先加载应用的配置(包括
application.properties
或application.yml
文件中的配置),并初始化Spring环境及上下文。在此阶段,Spring Boot也会处理所有的SpringApplicationRunListener
监听器,允许在应用运行过程中的不同环节执行特定逻辑。创建Spring应用上下文:Spring Boot使用所提供的配置创建一个新的应用上下文
ApplicationContext
。这是Spring管理所有Spring Bean的容器。注册并加载Spring Bean:在创建了应用上下文之后,Spring Boot将根据@ComponentScan注解扫描路径下的组件,并注册到Spring应用上下文中。同时,会解析配置类
@Configuration
,注册配置类中通过@Bean
方法定义的各种bean。自动配置(Auto-configuration):Spring Boot的自动配置特性将基于类路径和已定义的Bean尝试自动配置应用。这包括数据源、事务管理器、Spring MVC等等。
应用准备事件(ApplicationReadyEvent):在整个Spring应用准备好接受请求之前,Spring Boot将发布一个
ApplicationReadyEvent
事件。你可以监听这个事件来知道你的应用何时准备好了。运行Spring应用:此时,Spring Boot应用已经初始化完成并准备好处理请求。应用将根据情况运行Embedded Servlet Container(如Tomcat、Jetty或Undertow)。
应用关闭:当Spring Boot应用收到停止命令时(例如通过调用
SpringApplication.exit()
或者是操作系统信号),应用将开始关闭流程。这涉及到优雅地关闭Spring应用上下文,并且清理资源。
✨15、用户注册是否要考虑并发操作问题?
需要。并发操作可能导致以下一些常见问题:
重复注册:在高并发情况下,多个用户同时进行注册操作可能会导致相同的用户名或邮箱被多次注册,从而造成重复注册的情况。
数据一致性:并发注册可能导致数据不一致的问题,例如同时对同一用户进行注册和更新操作,可能导致数据混乱或丢失。
性能问题:大量并发注册请求同时到达系统可能会对系统性能造成压力,影响系统的响应速度和稳定性。
为了解决这些并发操作问题,可以采取以下一些措施:
使用唯一索引:在数据库中设置唯一索引,保证用户名、邮箱等关键字段的唯一性,避免重复注册。
悲观锁/乐观锁:在注册过程中使用悲观锁或乐观锁来确保并发操作的数据一致性。
限流控制:对注册接口进行限流控制,限制单位时间内的注册请求数量,避免系统被大量注册请求压垮。
分布式锁:使用分布式锁来控制并发操作,确保同一时刻只有一个线程可以执行注册操作。
异步处理:将注册请求异步化处理,减少注册过程中的同步操作,提高系统的并发处理能力。
✨16、spring mvc,spring ,springboot的关系是怎么样的?
在Java开发中,Spring框架是一个非常流行的轻量级开源框架,用于构建企业级应用程序。Spring框架提供了各种功能和模块,如控制反转(IoC)、依赖注入、AOP(面向切面编程)等,帮助开发者简化企业级应用程序的开发过程。
Spring MVC是Spring框架的一个子模块,用于构建基于MVC(Model-View-Controller)设计模式的Web应用程序。它提供了丰富的功能和组件,如控制器、视图解析器、数据绑定等,帮助开发者构建灵活、可扩展的Web应用程序。
Spring Boot是Spring框架的另一个重要项目,它是一个用于快速开发生产就绪的Spring应用程序的工具。Spring Boot通过自动配置和约定优于配置的原则,简化了Spring应用程序的配置和部署过程,使开发者能够更快地搭建和运行Spring应用程序。
因此,可以理解为**Spring框架是整个生态系统的核心,Spring MVC是其中的一个Web模块,而Spring Boot则是为了简化Spring应用程序的开发和部署而设计的工具。**这三者之间相互关联,共同构成了现代Java应用程序开发的基础。
✨17、如何实现文件上传与下载
(1)SpringBoot:文件上传
Spring Boot 默认包含了文件上传所需的依赖。在Spring Boot的配置文件中配置上传文件的保存路径,例如:
spring.servlet.multipart.enabled=true
spring.servlet.multipart.file-size-threshold=2KB
spring.servlet.multipart.max-file-size=200MB
spring.servlet.multipart.max-request-size=215MB
spring.servlet.multipart.location=upload-dir
创建一个Controller来处理文件上传请求。通过@RequestMapping注解和@RequestBody注解获取前端传递的文件数据,然后将文件保存到指定路径。
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
try {
// 获取文件名称
String fileName = file.getOriginalFilename();
// 获取文件保存路径
String filePath = "upload-dir/" + fileName;
// 保存文件
file.transferTo(new File(filePath));
return "File uploaded successfully";
} catch (IOException e) {
return "Error uploading file";
}
}
在前端页面中添加文件上传表单,并提交到上述Controller中。
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="Upload" />
</form>
(2)SpringBoot:文件下载
创建一个Controller来处理文件下载请求。通过@RequestParam注解指定要下载的文件名,并使用ResponseEntity返回文件数据。
@GetMapping("/download")
public ResponseEntity<Resource> downloadFile(@RequestParam("fileName") String fileName) {
// 获取文件路径
String filePath = "upload-dir/" + fileName;
// 创建文件对象
File file = new File(filePath);
// 创建文件资源对象
Resource resource = new FileSystemResource(file);
// 设置响应头,指定文件下载名和类型
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName);
headers.add(HttpHeaders.CONTENT_TYPE, Files.probeContentType(file.toPath()));
// 返回ResponseEntity对象
return ResponseEntity
.ok()
.headers(headers)
.body(resource);
}
在前端页面中添加文件下载链接,指向上述Controller的下载接口。
<a href="/download?fileName=example.pdf">Download File</a>
(3)SpringMVC:文件上传
Maven 依赖:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
创建一个 Spring MVC 控制器,在该控制器中处理文件上传的逻辑:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
@Controller
public class FileUploadController {
@PostMapping("/upload")
@ResponseBody
public String uploadFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return "Please select a file to upload";
}
try {
// 保存文件到本地
String uploadDir = "/path/to/upload/directory/";
File destFile = new File(uploadDir + file.getOriginalFilename());
file.transferTo(destFile);
return "File uploaded successfully";
} catch (IOException e) {
e.printStackTrace();
return "Failed to upload file";
}
}
}=
创建一个包含文件上传表单的页面,例如使用 HTML 表单来提交文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>File Upload Form</title>
</head>
<body>
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">Upload File</button>
</form>
</body>
</html>
(4)SpringMVC:文件下载
创建一个 Spring MVC 控制器,处理文件下载请求:
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Controller
@RequestMapping("/download")
public class FileDownloadController {
private static final String FILE_DIRECTORY = "/path/to/download/directory/";
@GetMapping("/{fileName:.+}")
@ResponseBody
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) throws IOException {
Path filePath = Paths.get(FILE_DIRECTORY).resolve(fileName).normalize();
Resource resource = new org.springframework.core.io.UrlResource(filePath.toUri());
if (!resource.exists()) {
throw new RuntimeException("File not found");
}
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"");
return ResponseEntity.ok()
.headers(headers)
.contentLength(resource.getFile().length())
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
}
在页面上创建一个链接或按钮,用户点击后可以触发文件下载:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>File Download Page</title>
</head>
<body>
<a href="/download/fileName.ext" target="_blank">Download File</a>
</body>
</html>
✨18、commons-fileupload和commons-io实现文件上传与下载
(1)文件上传
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
添加 commons-fileupload 和 commons-io 依赖库到项目中。在 Spring Boot 的配置文件中添加以下配置:
# 禁用 Spring Boot 默认的文件上传功能
spring.servlet.multipart.enabled=false
创建一个 Controller 来处理文件上传请求。在这个 Controller 中,使用 commons-fileupload 进行文件上传处理,并使用 commons-io 保存上传的文件。
import org.apache.commons.io.FileUtils;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
// ...
@PostMapping("/upload")
public String handleFileUpload(HttpServletRequest request) {
// 检查请求是否为文件上传请求
if (!ServletFileUpload.isMultipartContent(request)) {
return "Only file upload requests are allowed";
}
// 创建文件上传处理工厂
DiskFileItemFactory factory = new DiskFileItemFactory();
// 设置临时文件存储目录
factory.setRepository(new File("/tmp"));
// 创建文件上传处理器
ServletFileUpload upload = new ServletFileUpload(factory);
try {
// 解析上传请求
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
// 如果是文件域表单字段,则保存文件
if (!item.isFormField()) {
String fileName = item.getName();
File file = new File("upload-dir", fileName);
FileUtils.copyInputStreamToFile(item.getInputStream(), file);
}
}
return "File uploaded successfully";
} catch (FileUploadException | IOException e) {
return "Error uploading file";
}
}
在前端页面中添加文件上传表单,并将表单数据提交到上述 Controller 中。
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="Upload" />
</form>
(2)文件下载
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
创建一个 Spring MVC 控制器来处理文件下载请求:
import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
@Controller
public class FileDownloadController {
@GetMapping("/download")
@ResponseBody
public void downloadFile(@RequestParam("fileName") String fileName, HttpServletResponse response) {
// 文件路径(根据实际情况修改)
String filePath = "path/to/your/files/" + fileName;
File file = new File(filePath);
// 检查文件是否存在
if (!file.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 设置响应头
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
response.setContentLength((int) file.length());
// 使用 Commons IO 将文件内容写入响应输出流
try (OutputStream out = response.getOutputStream()) {
FileUtils.copyFile(file, out);
} catch (IOException e) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
在前端页面中添加文件下载链接,指向上述 Controller 的下载接口。
<a href="/download?fileName=myfile.txt">Download File</a>
✨19、@Resource和@Autowired的区别
@Resource和@Autowired都是用于依赖注入的注解,但它们之间存在一些区别:
来源:@Autowired是由Spring提供的,而@Resource是由JSR 250提供的,是J2EE的一部分。
注入方式:@Autowired是按类型装配(byType)依赖关系,可以配合@Qualifier注解进行按名称(byName)装配。@Resource默认是按名称(byName)装配,当找不到与名称匹配的bean时,会回退为按类型(byType)装配。
可选性:如果@Autowired标注的对象在容器中不存在,Spring会抛出异常。但是,你可以将@Autowired的required属性设置为false来改变这种行为。相比之下,如果@Resource标注的对象在容器中不存在,它不会抛出异常,只会把依赖设为null。
使用位置:@Autowired可以用于构造器、属性、setter方法和配置方法上,而@Resource只能用于属性和setter方法上。
✨20、@RequestParam和@PathVariable
@RequestParam可以用来从请求中获取参数并将其绑定到方法的参数上。假设我们有一个处理GET请求的方法,其URL为"/example?name=John
",我们可以使用@RequestParam来获取name参数:
// 路径/example?name=John
@GetMapping("/example")
public String exampleMethod(@RequestParam("name") String name) {
// 处理name参数的逻辑
}
@PathVariable可以将URI中的模板变量或占位符部分提取出来,然后将其绑定到方法的参数上。使用@PathVariable注解将其中的"id"提取出来并作为方法的参数:
// 路径/example/1
@GetMapping("/example/{id}")
public String exampleMethod(@PathVariable("id") String id) {
// 方法体
}
✨21、介绍Redisson的MultiLock(连锁、多重锁)机制
MultiLock(多重锁)是一种用于并发编程的技术,允许多个线程在同一时间段内持有多个锁。在某些情况下,为了避免死锁或提高并发性能,需要同时持有多个锁。MultiLock技术可以确保多个锁的安全获取和释放,从而避免死锁并确保线程安全。
MultiLock通常需要实现以下功能:
- 一次性获取多个锁:MultiLock机制允许线程一次性获取多个锁。
- 阻塞等待:如果某个锁已被其他线程占用,MultiLock会阻塞等待直到所有需要的锁都被成功获取或超时。
- 释放所有锁:一旦线程完成了需要使用多个锁的操作,MultiLock会确保所有锁都被安全释放,以避免潜在的资源泄露或死锁。
MultiLock实现可以依赖于底层的锁机制,如互斥锁、读写锁等。通过MultiLock,可以简化处理多个锁时的代码逻辑,避免出现死锁等问题。同时,MultiLock还提供了一些便捷的方法,如tryLock()
来尝试获取锁、lock()
来阻塞获取锁等,使得对多个锁的管理更加灵活和高效。
✨22、介绍一二三级缓存
在Java中,一级缓存、二级缓存和三级缓存的概念可能会因不同的上下文而有所不同。这里将从两个角度来解释这些概念:
(1)Spring框架中的缓存:
- 一级缓存(singletonObjects):用于存放就绪状态的Bean。保存在该缓存中的Bean所实现Aware子接口的方法已经回调完毕,自定义初始化方法已经执行完毕,也经过BeanPostProcessor实现类的postProcessorBeforeInitialization、postProcessorAfterInitialization方法处理。
- 二级缓存(earlySingletonObjects):用于存放早期曝光的Bean,一般只有处于循环引用状态的Bean才会被保存在该缓存中。
- 三级缓存(singletonFactories):用于存放创建用于获取Bean的工厂类-ObjectFactory实例。在IoC容器中,所有刚被创建出来的Bean,默认都会保存到该缓存中。
(2)Java应用中的缓存:
- 一级缓存:通常指的是CPU缓存,也就是直接集成在CPU中的缓存。在MyBatis中,一级缓存也称为本地缓存,是指在同一个会话(SqlSession)中,对同一条SQL语句的查询结果进行缓存。
- 二级缓存:是CPU与主内存之间的临时存储器。在Java应用中,二级缓存通常指的是应用服务器(如Tomcat)中的缓存,如EHCache、Redis等。在MyBatis中,二级缓存是指在同一个Mapper映射文件中,对同一条SQL语句的查询结果进行缓存。
- 三级缓存:通常指的是分布式缓存系统,如Redis集群、Memcached集群等。
✨23、使用Caffeine做什么用
Caffeine是一个基于Java的高性能缓存库,**通常用于实现一级和二级缓存。**虽然Caffeine本身不提供对分布式缓存的支持,但可以结合其他工具来实现类似三级缓存的功能。
- 一级缓存:Caffeine非常适合作为一级缓存的实现。它提供了高性能、低延迟的内存缓存,可以快速存取数据以提高系统响应速度。
- 二级缓存:Caffeine也可以用作二级缓存的实现。通过将Caffeine与持久化存储技术(如文件、数据库)结合使用,可以实现更大容量、更持久的缓存,适用于存储频繁访问的数据。
- 三级缓存:虽然Caffeine本身不支持分布式缓存,但可以结合其他分布式缓存工具(如Redis、Memcached等)来实现类似三级缓存的功能。可以在Caffeine中设置一个适当的缓存策略,使得频繁访问的数据缓存在本地内存(一级缓存)、较少访问的数据缓存在二级缓存(如文件或数据库),而共享数据则缓存在分布式缓存中。
✨24、解释下面的mybatis动态sql查询
<select id="getEmpByConditionOne" resultType="Emp">
select * from t_emp where 1=1
<if test="empName != null and empName != ''">
emp_name = #{empName}
</if>
<if test="age != null and age != ''">
and age = #{age}
</if>
<if test="sex != null and sex != ''">
and sex = #{sex}
</if>
<if test="email != null and email != ''">
and email = #{email}
</if>
</select>
<select id="getEmpByConditionOne" resultType="Emp">
:定义了一个 id 为 "getEmpByConditionOne" 的查询语句,指定了查询返回的结果类型为 "Emp"。select * from t_emp where 1=1
:这是查询语句的开始部分,其中的where 1=1
其实是为了方便后续动态拼接查询条件,因为每个条件之前都有一个and
,这样就可以省去第一个条件多余的and
的处理。<if>
标签:这个标签用于根据条件动态拼接 SQL,<if>
标签中的test
属性用于指定条件,根据条件的成立与否来决定是否拼接 SQL。比如在这个例子中,根据不同的属性(empName、age、sex、email)是否存在来动态拼接查询条件。#{}
占位符:这是 MyBatis 用于接收传入参数的占位符,可以帮助防止 SQL 注入。在这个例子中,#{}
占位符用于接收参数,并将参数值插入到 SQL 中。
<if test="empName != null and empName != ''">
这个条件是指数据库中对应的属性不为空和null,还是只传入的属性不为空和null?
这里的条件 <if test="empName != null and empName != ''">
是指只传入的属性不为空(不为 null)且字符串长度不为0。这个条件在动态 SQL 中用于判断是否拼接查询语句中的一部分,它并不直接将条件与数据库中的属性进行比较,而是判断传入的参数是否满足特定的条件。如果传入的 empName
参数既不为 null 也不为空字符串,就会在 SQL 中拼接上。这个条件并不会控制SQL是否被执行,而是控制了SQL中的一部分是否被拼接进去。比如:如果传递的Emp对象的empName
属性不为空,则会将empName = #{empName}
加入到查询条件中;如果empName
为空,将不会加入到查询条件中。
✨25、MyBatisPlus动态SQL
public interface UserMapper extends BaseMapper<User> {
// 下面的这个Sql也可以写在UserMapper.xml中,@Param("ew") QueryWrapper中只能是ew
@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper);
}
@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
:这是一个使用了@Select
注解的自定义 SQL 查询操作,尽管使用了@Select
注解,但实际上是一条更新操作,因为它执行了一个更新的 SQL 操作。void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper)
:这是一个方法定义,用于更新用户余额信息。该方法使用了@Param
注解,指定了方法参数与 SQL 语句中的参数的对应关系。@Param("money") int money
意味着方法中的money
参数与 SQL 语句中的#{money}
参数对应。${ew.customSqlSegment}
: 这里使用了 MyBatis-Plus 的动态 SQL 功能,在运行时将根据传入的查询条件动态地生成相应的 SQL 语句段。如果删除${ew.customSqlSegment}
的话,将不再能够在运行时动态生成 SQL 语句,而是需要直接在 SQL 语句中写入具体的条件。这样做将使得查询条件变得静态化,不再能够根据外部传入的查询条件动态变化。
✨26、mybatis plus中增删改查分别使用哪个条件构造器

QueryWrapper、UpdateWrapper、LambdaQueryWrapper、LambdaUpdateWrapper
- 查询(Query):
QueryWrapper
和LambdaQueryWrapper
都适用。 - 删除(Delete): 可以使用
QueryWrapper
和LambdaQueryWrapper
来指定删除条件。 - 更新(Update): 使用
UpdateWrapper
和LambdaUpdateWrapper
来构造更新条件和指定更新字段。 - 新增(Insert): 增加操作通常不涉及条件构造器,直接通过 Mapper 的
insert
方法进行。
QueryWrapper和LambdaQueryWrapperi通常用来构建select、delete、update的where条件部分,当然也可以构建select部分。
UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用。
无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。虽然
UpdateWrapper
是专门设计来构建更新条件的,但在实际使用中,QueryWrapper
同样可以用于定义哪些记录需要被更新,配合使用update
方法进行数据更新,特别是在更新条件相对简单的情况下,举例:User user = new User(); user.setAge(30); // 设置需要更新的字段和值 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", "张三"); // 设置更新条件 int updateCount = userMapper.update(user, queryWrapper); // 执行更新操作
✨27、IO 模型有哪些
同步阻塞I/O(BIO) 在阻塞 I/O 模型中,当应用程序发起 I/O 操作时,它会被阻塞直到操作完成。这意味着应用程序会等待,直到数据读取或写入完成。在这种情况下,CPU会在等待 I/O 操作完成期间处于空闲状态。
同步非阻塞I/O(NIO) 在非阻塞 I/O 模型中,当应用程序发起 I/O 操作时,它会立即返回并允许应用程序执行其他任务。应用程序需要定期轮询以检查操作是否完成。这种模型需要不断地轮询状态,可能会导致比较高的 CPU 占用率。
I/O 多路复用(I/O multiplexing): 在 I/O 多路复用模型中,一个线程可以监视多个文件描述符的 I/O 事件。通过调用像 select()、poll() 或 epoll() 这样的系统调用,可以监视多个 I/O 操作的状态。一旦有文件描述符准备好进行 I/O 操作,线程将被唤醒并处理相应的事件。
信号驱动I/O: 在信号驱动 I/O 模型中,应用程序发起 I/O 操作后,会继续执行其他任务,同时系统会向应用程序发送一个信号来通知 I/O 操作的完成。
异步I/O(AIO) 异步 I/O 模型中,应用程序发起 I/O 操作后,可以立即进行其他任务,而不需要等待操作完成。一旦操作完成,系统会通过回调函数或事件通知应用程序。同步非阻塞模型类似,程序可以继续执行其他任务。不同之处在于,程序不需要定期轮询或查询操作的状态,而是通过回调函数或事件处理机制来处理操作完成后的结果。
同步阻塞 I/O: Block I/O
同步非阻塞 I/O: Non-Block I/O
I/O 多路复用: I/O Multiplexing
信号驱动I/O: Signal Driven I/O
异步I/O: Asynchronous I/O
✨28、IO密集型的线程数设置,三个线程顺序执行,Thread类的方法
线程池是一个用于管理线程的工具,可以有效控制线程的数量,避免过多线程导致系统负载过重。IO密集型的线程数一般设置为2*N,其中N是CPU核心数。实现三个线程顺序执行可以使用CountDownLatch或者使用Thread类的join方法。Thread类的常用方法包括start()、run()、sleep()、yield()、join()等。
✨29、ThreadLocal,父子线程ThreadLocal值传递
ThreadLocal是一个提供线程局部变量的工具。可以在父线程创建ThreadLocal变量,子线程可以通过inheritableThreadLocal来继承父线程的值。
✨30、垃圾回收算法,1.8默认GC算法,细说CMS,如果所有流量都来自于RabbitMQ,不提供任何接口,老年代应该使用哪种GC算法
垃圾回收算法包括标记-清除、标记-整理、复制、分代等。在JDK 1.8中,默认的垃圾收集器是Parallel GC。CMS(Concurrent Mark-Sweep)是一种老年代垃圾回收算法,它会在主线程和用户线程并发工作,目的是最大程度地减少垃圾收集时暂停用户线程的时间。如果所有流量都来自于RabbitMQ且不提供任何接口,老年代应该考虑使用并发垃圾收集器(Concurrent Mark-Sweep,CMS算法),以最大程度地减少暂停时间。
✨31、Spring中的bean什么时候用单例,什么时候用session
在Spring中,当需要确保整个应用中只有一个实例存在时,可以使用单例模式。而当需要为每次请求或每个会话(session)创建新的实例时,可以使用session 。例如,对于事务管理器和数据源等对象,通常使用单例模式;对于Web应用中每个用户的会话信息,通常使用session。
✨32、JVM内存模型,堆及堆里面的划分,设置堆大小的参数
JVM内存模型包括堆和栈。堆内存用于存储对象实例,可以使用参数**-Xms和-Xmx**来设置堆的初始大小和最大大小。栈内存主要用于存储局部变量和方法调用。设置堆大小的参数为-Xms和-Xmx,可以通过这两个参数设置堆的初始大小和最大大小。
✨33、SpringBoot中固定的端点,如监控堆大小
在Spring Boot中,固定的端点包括但不限于:/actuator/health(健康检查)、/actuator/info(应用信息)、/actuator/metrics(监控指标)等。要监控堆大小,可以使用/actuator/metrics/jvm.memory.max端点来获取最大堆内存的信息。
✨34、比较SpringBoot和SpringMVC或者比较SpringBoot和Spring
Spring Boot是一个基于Spring框架的快速开发微服务的工具。相比于传统的SpringMVC和Spring框架,Spring Boot简化了应用程序的配置和部署。Spring Boot提供了自动化的配置,内嵌的应用服务器和诸多开箱即用的功能,使得开发者能够更快速地开发和部署应用。
SpringMVC是Spring框架中用于开发Web应用的一部分,而Spring Boot是基于Spring框架的快速开发工具。因此,Spring Boot和SpringMVC并不是严格意义上的对比关系,而是在不同层面上的使用。
✨35、SpringBoot的@SpringBootApplication的作用
@SpringBootApplication注解是Spring Boot框架的核心注解之一,它用于标识一个主配置类,通常用于启动Spring Boot应用。该注解包含了@ComponentScan和@EnableAutoConfiguration注解。@ComponentScan用于扫描被@Component及其衍生注解标注的类,@EnableAutoConfiguration用于启用自动化配置,根据classpath下的jar包自动配置Spring应用。
✨36、@Bean为什么会生效
(1)扫描路径配置错误:
- 默认情况下,SpringBoot的扫描路径为启动类所在包及其子包。
- 如果配置了component-scan扫描路径,则SpringBoot启动时会去扫描指定路径下的Bean。
- 如果@Bean定义不在扫描路径下,则不会将该Bean注册到IOC容器中。
(2)@Bean + @Conditional修饰,但不满足Conditional条件
(3)beanName相同:
- 如果配置spring.main.allow-bean-definition-overriding=true,并且有多个BeanName相同的Bean定义,这种情况下,其中的某些Bean就会被覆盖。
- @Bean修饰的方法,如果不指定BeanName,默认会以方法名作为BeanName。
✨37、AOP切面都有哪些东西,怎么实现,AOP的底层原理
AOP切面通常包括切点(Pointcut)、通知(Advice)和切面(Aspect)。
**切点定义了通知将会被执行的位置,通知定义了切面在特定切点执行的动作。**在Spring中,AOP可以通过XML配置、注解或者纯Java方式来实现。AOP的底层原理是基于动态代理技术,Spring AOP底层使用了JDK动态代理或者CGLIB来实现AOP功能。
✨38、简单介绍SpringCache
Spring Cache是Spring框架中提供的缓存抽象框架,它提供了在方法调用时缓存方法的返回值,并能够在下次同样的参数调用时直接返回缓存结果的功能。Spring Cache可以通过在方法上添加注解(如@Cacheable、@CacheEvict等)来实现缓存功能。
✨39、redis的数据结构,redis命令
Redis是一个非关系型的内存数据库,支持多种数据结构,包括字符串、列表、集合、有序集合、散列表等。常用的Redis命令包括set/get(用于设置和获取字符串值)、lpush/rpush(用于向列表左侧或右侧添加元素)、sadd/smembers(用于添加和获取集合中的成员)、zadd/zrange(用于有序集合的添加和获取)、hset/hget(用于散列表的添加和获取)等。 Redis还支持事务、发布/订阅、持久化等功能。
✨40、redis内存回收策略,项目中用哪一种
Redis内存回收策略包括:定期删除、惰性删除、内存淘汰机制和使用过期时间。
定期删除指定期删除设置了过期时间的key,而惰性删除指在获取key时,如果发现已经过期,则删除。内存淘汰机制用于在内存达到设定的上限时,按照一定的策略删除部分数据。在项目中选择哪种内存回收策略取决于实际情况,一般情况下,会根据业务特点和性能需求进行选择。
✨41、MySQL事务隔离级别,说说MVCC,历史版本链
MySQL的事务隔离级别包括:读未提交、读提交、可重复读和串行化。
MVCC(Multi-Version Concurrency Control)是MySQL数据库引擎使用的行级锁的实现方式,它对每行数据都保存了多个版本,不同事务只能看到符合自己隔离级别的数据版本。历史版本链是指在MVCC中,当进行更新操作时,MySQL会生成新的版本链,旧的版本不会立刻被删除,而是在事务提交后由后台的purge线程来进行清理。
✨42、redolog、undolog、binlog的用处,MyISAM有redolog和binlog吗
Redo log是InnoDB存储引擎特有的日志,用于记录数据页的物理操作,用于保证事务的持久性。
Undo log用于事务的原子性和一致性,用于事务回滚和MVCC的实现。
Binlog(Binary log)是MySQL的服务器层产生的日志,用于记录数据变更的SQL语句,用于主从复制和恢复等操作。
MyISAM引擎不支持事务,因此没有Redo log和Undo log。
✨43、如何解决缓存穿透,缓存空值时如果1w个线程来请求都会打到数据库吗,如果加锁的话集群怎么加锁
缓存穿透是指查询一个不存在的数据,导致每次请求都会穿透到数据库,解决方法包括:使用布隆过滤器拦截请求、空值缓存(缓存空对象或空值)、对于高并发的请求,可以在缓存为空时设置一个短暂的空值,避免同时全部请求都击穿数据库。
如果有1万个线程同时请求不存在的数据,会导致缓存空值被频繁访问,从而可能会打到数据库。但是,相比每个请求都直接访问数据库,使用缓存空值仍然可以大大减轻数据库的负担,因为缓存空值的获取通常比直接访问数据库要快得多。
如果加锁的话,可以使用分布式锁或者分布式信号量来控制并发,可以使用分布式缓存工具如Redis的分布式锁来实现。在集群环境中,需要考虑分布式锁的可靠性和性能。
✨44、mybatis的编程步骤
定义实体类:首先定义需要操作的实体类,通常对应数据库中的表结构。实体类的属性与表字段保持一致。
编写Mapper接口:创建一个Java接口,定义数据访问的方法。可以使用@Mapper注解或在配置文件中配置Mapper扫描路径。
编写Mapper XML文件:编写Mapper XML文件,配置SQL语句,映射接口方法与具体的SQL语句,以及参数和结果集的映射关系。
配置数据源和SqlSessionFactory:在配置文件中配置数据源信息和SqlSessionFactory,用于创建SqlSession对象。
获取SqlSession:通过SqlSessionFactory获取SqlSession对象,用于执行SQL语句。
String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session = sqlSessionFactory.openSession();
调用Mapper方法:通过SqlSession获取Mapper接口的代理对象,调用接口方法执行SQL语句。
处理结果:处理SQL执行结果,获取查询结果集或影响的行数等。
提交事务和关闭资源:对于涉及到事务的操作,需要手动提交或回滚事务;最后需要关闭SqlSession对象释放资源。
✨45、HTTP、SOCKET和TCP的区别
- HTTP(超文本传输协议)是一种用于传输文字、图像、视频和其他多媒体文件的网络协议。它是一种无状态的应用层协议,通常基于TCP协议,有时也可以基于TLS/SSL进行加密。HTTP协议通常用于Web浏览器与Web服务器之间的通信,它定义了客户端(例如Web浏览器)发送请求、服务器端对请求进行处理并返回响应的过程。
- Socket是一个抽象的概念,它表示在网络上进行通信的一种方式。Socket是通信的两端之间通过 TCP/IP 网络进行通信时使用的一种机制。在软件开发中,Socket是一组支持TCP/IP的API,用于创建网络通信的程序。
- TCP(传输控制协议)是一种传输层协议,负责在通信的两端之间建立可靠的连接。TCP协议提供了数据的高可靠性、有序性和完整性,它通过数据包确认和重传机制来确保数据的可靠传输。TCP协议对于网络通信是非常重要的,因为它建立了传输层协议的基本标准。
因此,HTTP是一种应用层协议,用于传输Web内容;Socket是一种通信机制,用于网络上进程间的通信;TCP是一种基础的传输层协议,提供了可靠的连接和数据传输。
✨46、有一个联合索引(a,b,c),a, ab , abc , bc , ac走索引吗
- a:会走索引。因为a是联合索引的第一个字段,索引可以直接用来加速对a的查询。
- ab:会走索引。因为ab是按照索引的前两个字段进行的查询,索引可以用来加速查询。
- abc:会走索引。这是最理想的情况,查询条件涵盖了整个索引的字段,可以充分利用索引进行优化。
- bc:不会直接利用索引。因为b不是索引的第一个字段,直接查询bc时,索引对加速查询没有帮助。但是,如果数据库优化器认为通过扫描全索引来过滤结果比全表扫描更有效,它可能会选择使用索引。
- ac:不会直接利用索引。和bc的情况类似,因为a虽然是索引的第一个字段,但是c不是紧随a之后的字段,在没有b的情况下,索引对于加速查询ac是不直接有效的。
✨47、Spring框架中的单例bean是线程安全的吗?
在Spring框架中,默认情况下,bean 是单例的(singleton scope)。这意味着每个Spring IoC容器中只会创建bean的单个实例。
Spring中的单例bean本身在创建上是线程安全的,但它们的使用(特别是状态管理)可能不是线程安全的。确保线程安全通常需要额外的设计考虑或同步控制措施。在设计Spring应用程序时,最好将单例bean设计为无状态的,以避免线程安全问题。
如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用由“singleton”变更为“prototype”。
✨48、bean的作用域“singleton”和“prototype”有什么区别
在Spring框架中,bean的作用域(scope)决定了Spring容器如何创建和管理bean的实例。主要的两种作用域是“singleton”和“prototype”,每种作用域都适用于不同的使用场景:
Singleton:
- 定义:Singleton是Spring的默认作用域。当一个bean定义为singleton作用域时,Spring IoC容器将为每个容器创建一个且只有一个bean实例。这意味着所有对该bean的请求都将返回同一个对象实例。
- 适用场景:这种作用域适合用于无状态的bean,或者那些bean的状态不会在多个请求之间改变的情况。由于只有一个实例,singleton作用域可以帮助节省资源。
- 线程安全性:singleton作用域的bean本身在实例化过程中是线程安全的,但如果bean有内部可变状态,那么在并发访问时可能不是线程安全的。
Prototype:
- 定义:当一个bean定义为prototype作用域时,每次请求该bean时,Spring容器都会创建一个新的bean实例。这意味着每个通过容器获取的prototype bean都是一个全新的对象。
- 适用场景:这种作用域适合于那些状态依赖于具体使用情况的bean,或者当你希望每个使用bean的用户都有一个新的实例时。它确保了bean的状态不会在不同的请求或交互之间共享。
- 线程安全性:因为每个请求都会生成一个新的实例,prototype作用域的bean在多线程环境下自然是线程安全的,因为不同线程间不会共享bean实例。然而,如果一个prototype bean依赖于一个singleton bean,那么仍然需要考虑singleton bean本身的线程安全性。
总结:
- Singleton作用域用于在应用程序的整个生命周期中共享单一实例,适用于需要共享配置或数据的场合。
- Prototype作用域提供每次请求的新实例,适合于每个实例都需要不同状态的情况。
✨49、订单接口的幂等性怎么实现?
意思就是post请求带着:一个用户id,一个优惠卷id。发送多次请求,如何保证只有一个成功?
唯一交易号 (Unique Transaction Number): 当客户端开始一个新的订单创建流程时,为这个订单分配一个全局唯一的交易号(比如使用UUID或其他唯一生成策略)。这个唯一交易号随用户ID和优惠卷ID一起作为POST请求的一部分发送到服务端。服务端对每个唯一交易号只处理一次。无论这个POST请求被发送多少次,因为交易号不变,所以服务端都能识别出重复请求,并只处理一次。
在数据库中可以为这个唯一交易号建立唯一索引确保不会重复插入同一个订单。
乐观锁 (Optimistic Locking): 在创建订单之前,检查数据库中是否已经存在了该用户ID和优惠券ID的订单。你可以使用版本号或时间戳字段实现乐观锁。如果订单已存在,则返回错误或忽略新的订单请求。
使用分布式锁: 在处理订单之前,基于用户ID和优惠券ID获取一个分布式锁。如果该组合的锁已经被获取(意味着另一个订单正在处理或已处理),则新的请求应返回错误或等待锁释放。如果获取锁成功,则创建订单,并在完成后释放锁。
幂等性键: 提供一个从客户端生成的幂等性键,如一个特定的字符串或者数字,并在服务端记录这个幂等性键的每次请求。如果这个键对应的请求已经处理过,直接返回上一次的处理结果。
✨50、Java中的异常

✨51、图片如何存储在数据库中
直接存储图片文件:将图片文件存储在数据库中的BLOB字段(二进制大对象)中。这种方法的优点是方便管理和备份,缺点是数据库可能变得庞大且变慢。
存储图片路径:将图片保存在服务器的文件系统中,然后在数据库中存储图片的路径。这种方法可以减轻数据库的负担,并且更容易管理和处理图片文件。
存储图片的Base64编码:将图片转换为Base64编码,然后将编码后的字符串存储在数据库中。这种方法便于在Web应用程序中显示图片,但Base64编码会增加数据存储和传输的负担。
使用第三方存储服务:可以将图片存储在专门的存储服务(如Amazon S3、Google Cloud Storage等),并在数据库中存储图片的URL。这种方法可以有效地管理大量图片,同时减轻数据库压力。
✨52、volatile和synchronized的区别
volatile
和synchronized
是Java中用于实现线程安全的关键字,它们有以下区别:
(1) 作用范围:
volatile
关键字主要用于修饰变量,用来确保线程之间对该变量的可见性。synchronized
关键字可以修饰方法和代码块,用于实现线程之间的互斥访问和同步。
(2) 实现方式:
volatile
关键字通过禁止线程将被修饰的变量缓存在寄存器或对其他线程不可见的地方,保证了变量的可见性。当一个线程修改了volatile
变量的值,其他线程能够立即看到修改后的值。synchronized
关键字通过获取对象的锁来实现线程之间的同步和互斥。一个线程在执行synchronized
代码块或方法时,会尝试获取对象的锁,如果锁被其他线程持有,则该线程将被阻塞,直到获取到锁为止。
(3) 适用场景:
volatile
关键字适用于以下情况:- 对变量的写操作不依赖于当前值,或者只有单个线程修改变量的值。
- 保证变量的可见性,但不保证原子性。
synchronized
关键字适用于以下情况:- 多个线程需要互斥访问共享资源,同一时间只允许一个线程访问。
- 提供了原子性操作,一个线程在执行
synchronized
代码块或方法时,其他线程无法访问该代码块或方法。
(4) 性能开销:
volatile
关键字比synchronized
关键字的性能开销较小,因为它不涉及线程的阻塞和唤醒,仅仅是保证变量的可见性。synchronized
关键字在获取锁和释放锁的过程中,涉及线程的上下文切换和调度,性能开销相对较大。
需要注意的是,volatile
关键字只能保证可见性,不能保证原子性,而synchronized
关键字可以同时保证可见性和原子性。因此,在需要同时保证可见性和原子性的情况下,应该使用synchronized
关键字。
✨53、volatile和synchronized能保证线程安全吗
synchronized关键字可以更可靠地保证线程安全,因为它可以提供可见性和原子性的保证。而volatile关键字只能保证可见性,不能保证原子性,因此在需要同时保证可见性和原子性的情况下,应该使用synchronized关键字。
✨54、HashMap的键和值可以为null或空值吗?
在Java的HashMap中,键和值都可以为null,但不能是空值(键和值都不能为null,即键和值不能为空对象)。这是因为在Java中,空值并不是一个合法的值。空值与null不同,null表示对象不存在,而空值通常意味着空字符串或空集合等。
✨55、HashSet可以存储空值或者null吗
HashSet可以存储空值或null,但只能存储一个。如果尝试存储多个空值或null,后面的会覆盖前面的。
✨56、介绍Spring Cloud Config和Spring Cloud Bus
Spring Cloud Config是一个用于集中管理应用程序配置的组件。它包括以下主要功能:
- 集中配置管理:Spring Cloud Config Server允许将所有应用程序的配置存储在一个中央位置(如Git、SVN或文件系统)。客户端应用程序在启动时从Config Server获取配置。
- 环境和配置文件:支持按环境(如开发、测试、生产)和配置文件(如application.yml)进行配置管理。可以为不同的环境和应用程序提供不同的配置。
- 动态刷新配置:与Spring Cloud Bus结合使用时,可以实现配置的动态刷新,无需重启应用程序。
Spring Cloud Bus是一个用于传播集群中配置变化和事件的消息总线。它基于消息代理(如RabbitMQ或Kafka)实现,主要用于以下用途:
- 配置刷新:当配置中心的配置发生变化时,可以通过Spring Cloud Bus通知集群中所有相关服务刷新配置。
- 事件传播:可以在微服务之间传播自定义事件,简化微服务之间的通信。
Spring Cloud Config和Spring Cloud Bus共同工作,实现了集中配置管理和配置的动态刷新。 Spring Cloud Config从仓库中更新配置,Spring Cloud Bus通过消息队列通知集群中所有相关服务刷新配置。
✨57、redis的事务机制
Redis 的事务机制可以通过使用 MULTI、EXEC、DISCARD 和 WATCH 命令来实现。事务可以确保一组命令的原子性执行,即要么所有命令都执行,要么一个都不执行。
以下是一些 Redis 事务相关命令的示例:
- MULTI: 标记事务的开始。之后的命令会被放入事务队列,而不是立即执行。
- EXEC: 执行事务队列中的所有命令。
- DISCARD: 丢弃事务队列中的所有命令,取消事务。
- WATCH: 监视一个或多个键,如果在事务执行之前这些键被修改了,事务将被中止。
举例:假设有两个键 account1 和 account2,我们希望从 account1 转账 10 个单位到 account2。
127.0.0.1:6379> SET account1 100
OK
127.0.0.1:6379> SET account2 50
OK
# 开始事务
127.0.0.1:6379> MULTI
OK
# 在事务中执行命令
127.0.0.1:6379> DECRBY account1 10
QUEUED
127.0.0.1:6379> INCRBY account2 10
QUEUED
# 执行事务,可以看到,事务中的两个命令都被执行了
127.0.0.1:6379> EXEC
1) (integer) 90
2) (integer) 60
# 使用 WATCH 进行乐观锁控制
127.0.0.1:6379> WATCH account1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY account1 10
QUEUED
127.0.0.1:6379> INCRBY account2 10
QUEUED
# 假设在执行 EXEC 之前,account1 的值被其他客户端修改
127.0.0.1:6379> SET account1 80
OK
# 现在尝试执行事务,由于 account1 被修改过,事务失败返回 nil,所有事务中的命令都没有被执行
127.0.0.1:6379> EXEC
(nil)
✨58、Java中的四种引用
在Java中,有四种引用类型,用来控制对象的生命周期和垃圾回收行为。它们分别是:
(1) 强引用 (Strong Reference):
- 强引用是Java中最常见的引用类型。
- 使用强引用的对象在垃圾回收时不会被回收,除非没有任何强引用指向这个对象。
- 示例:
String str = new String("Hello, World!");
- 在上述代码中,变量
str
是一个强引用,指向一个String
对象。
(2) 软引用 (Soft Reference):
- 软引用是一种较弱的引用类型,在系统内存不足时才会被回收。
- 软引用常用于实现内存敏感的缓存。
- 示例:
SoftReference<String> softRef = new SoftReference<>(new String("Hello, World!"));
- 使用
SoftReference
类创建软引用。
(3) 弱引用 (Weak Reference):
- 弱引用比软引用更弱,当垃圾回收器运行时,无论系统内存是否足够,只要对象只有弱引用指向它,就会被回收。
- 弱引用常用于实现规范映射和监听器等。
- 示例:
WeakReference<String> weakRef = new WeakReference<>(new String("Hello, World!"));
- 使用
WeakReference
类创建弱引用。
(4) 虚引用 (Phantom Reference):
- 虚引用是最弱的引用类型,不能通过虚引用来取得对象实例。
- 虚引用的主要作用是跟踪对象被垃圾回收的状态,用于清理资源等。
- 示例:
PhantomReference<String> phantomRef = new PhantomReference<>(new String("Hello, World!"), new ReferenceQueue<>());
- 使用
PhantomReference
类创建虚引用,并需要提供一个ReferenceQueue
对象。
下面是一个简要的示例,展示如何使用这些引用:
import java.lang.ref.*;
public class ReferenceDemo {
public static void main(String[] args) {
// 强引用
String strongRef = new String("Strong Reference");
// 软引用
SoftReference<String> softRef = new SoftReference<>(new String("Soft Reference"));
// 弱引用
WeakReference<String> weakRef = new WeakReference<>(new String("Weak Reference"));
// 虚引用
PhantomReference<String> phantomRef = new PhantomReference<>(new String("Phantom Reference"), new ReferenceQueue<>());
System.out.println("Strong Reference: " + strongRef);
System.out.println("Soft Reference: " + softRef.get());
System.out.println("Weak Reference: " + weakRef.get());
System.out.println("Phantom Reference: " + phantomRef.get()); // Always returns null
}
}
✨59、Java中的反射
在Java中,通过反射可以获取类的属性、方法和构造器。反射是一种允许程序在运行时检查和操作自身结构的机制。
获取类的Class对象: 使用
Class.forName("类的全限定名")
或对象.getClass()
获取类的Class对象。获取类的属性: 使用
getFields()
、getDeclaredFields()
方法获取公共或所有属性。获取类的方法: 使用
getMethods()
、getDeclaredMethods()
方法获取公共或所有方法。获取类的构造器: 使用
getConstructors()
、getDeclaredConstructors()
方法获取公共或所有构造器。
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectionExample {
public static void main(String[] args) {
try {
// 获取Class对象
Class<?> clazz = Class.forName("com.example.MyClass");
// 获取所有公共属性
Field[] publicFields = clazz.getFields();
System.out.println("公共属性:");
for (Field field : publicFields) {
System.out.println(field.getName());
}
// 获取所有属性(包括私有属性)
Field[] allFields = clazz.getDeclaredFields();
System.out.println("所有属性:");
for (Field field : allFields) {
System.out.println(field.getName());
}
// 获取所有公共方法
Method[] publicMethods = clazz.getMethods();
System.out.println("公共方法:");
for (Method method : publicMethods) {
System.out.println(method.getName());
}
// 获取所有方法(包括私有方法)
Method[] allMethods = clazz.getDeclaredMethods();
System.out.println("所有方法:");
for (Method method : allMethods) {
System.out.println(method.getName());
}
// 获取所有公共构造器
Constructor<?>[] publicConstructors = clazz.getConstructors();
System.out.println("公共构造器:");
for (Constructor<?> constructor : publicConstructors) {
System.out.println(constructor.getName());
}
// 获取所有构造器(包括私有构造器)
Constructor<?>[] allConstructors = clazz.getDeclaredConstructors();
System.out.println("所有构造器:");
for (Constructor<?> constructor : allConstructors) {
System.out.println(constructor.getName());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
注:
- 权限问题:当试图访问私有属性或方法时,可能会遇到权限问题。可以使用
setAccessible(true)
方法来绕过权限检查。 - 异常处理:反射操作可能会抛出各种异常(如
ClassNotFoundException
、NoSuchMethodException
、IllegalAccessException
等),需要适当处理这些异常。 - 性能问题:反射相比直接调用会有一定的性能开销,应谨慎使用。
👉之前的笔记:
获取反射中的Class对象:
第一种,使用 Class.forName 静态方法。
Class clz = Class.forName("java.lang.String");
第二种,使用 .class 方法。
Class clz = String.class;
第三种,使用类对象的 getClass() 方法。
String str = new String("Hello");
Class clz = str.getClass();
通过反射创建类对象:
第一种:通过 Class 对象的 newInstance() 方法。
Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();
第二种:通过 Constructor 对象的 newInstance() 方法
Class clz = Apple.class;
Constructor c = clz.getConstructor();
Apple apple = (Apple)c.newInstance();
通过反射获取类属性、方法、构造器:
通过 Class 对象的 getFields() 方法可以获取 Class 类的属性,但无法获取私有属性。
使用 Class 对象的 getDeclaredFields() 方法则可以获取包括私有属性在内的所有属性。
✨60、HashMap的加载因子
在Java中,HashMap
是一个基于哈希表的集合类,提供了键值对的存储和快速查找功能。HashMap
的性能在很大程度上取决于其内部的哈希表的负载情况,这由“加载因子”(load factor)来控制。
(1)什么是加载因子
加载因子是一个介于0和1之间的浮点数,用来衡量HashMap
在自动扩容之前可以达到的填充程度。公式为:
默认情况下,Java中HashMap
的加载因子是0.75。这意味着当哈希表中的元素数量达到桶数量的75%时,HashMap
将进行扩容(rehash),即将哈希表的容量翻倍,并重新分配所有元素。
(2)如何设置加载因子
在创建HashMap
实例时,可以通过构造函数来指定初始容量和加载因子。例如:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// 创建一个初始容量为16,加载因子为0.75的HashMap
HashMap<String, Integer> map = new HashMap<>(16, 0.75f);
// 添加元素到HashMap中
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
// 输出HashMap
System.out.println(map);
}
}
在这个例子中,HashMap
被创建时指定了初始容量为16,加载因子为0.75。
(3)加载因子的作用和影响
性能平衡:
- 较低的加载因子(如0.5)会导致更多的空间浪费,但可以减少冲突,提高查询和插入的性能。
- 较高的加载因子(如0.9)会更有效地利用空间,但可能增加冲突,降低查询和插入的性能。
扩容机制: 当
HashMap
中的元素数量达到capacity * load factor
时,哈希表将进行扩容。扩容时,HashMap
的容量会翻倍,并将所有的键值对重新哈希到新的桶中。这个过程会消耗一定的计算资源,因此在性能关键的场景中需要合理设置初始容量和加载因子,以减少扩容的频率。线程安全:
HashMap
本身不是线程安全的,在并发环境下应使用Collections.synchronizedMap
或者ConcurrentHashMap
。
✨61、偏向锁、轻量级锁、重量级锁
偏向锁:偏向锁是一种锁优化机制,旨在减少线程之间不必要的竞争。它的核心思想是,如果一个锁对象大部分时间都由同一个线程访问,那么每次这个线程访问该锁时就不需要进行同步操作,而是直接使用之前的锁定信息。
轻量级锁:轻量级锁的核心思想是利用CAS操作来避免线程在锁竞争中的阻塞和唤醒操作,从而减少系统开销。轻量级锁适用于低竞争或短时间持有锁的场景。
重量级锁:用于控制多个线程对共享资源的访问的锁机制。相比于轻量级锁,重量级锁具有更高的开销和复杂性,但也提供了更强的同步和安全性保障。
✨62、Monitor实现的锁属于重量级锁,了解过锁升级吗?
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
描述 | |
---|---|
重量级锁 | 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。 |
轻量级锁 | 线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性。 |
偏向锁 | 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令。 |
一旦锁发生了竞争,都会升级为重量级锁。
✨63、锁升级和锁降级
锁升级:当竞争加剧时,锁会从偏向锁升级为轻量级锁,再升级为重量级锁。
锁降级:JVM在某些情况下可以将锁从重量级锁降级为轻量级锁或偏向锁以减少开销。
✨64、线程池的核心参数
corePoolSize(核心线程数):线程池的基本大小,即使没有任务执行,也会尝试复用已有的空闲线程。除非设置了allowCoreThreadTimeOut,否则核心线程不会因为空闲而被销毁。
maximumPoolSize(最大线程数):线程池允许创建的最大线程数。当线程池的工作队列满了,并且已创建的线程数小于最大线程数时,线程池会再创建新的线程来执行任务。
keepAliveTime(线程存活时间):线程池中的非核心线程闲置后,保持存活的时间。如果在这段时间内,线程没有被复用,那么线程池会销毁这些线程。
unit(时间单位):keepAliveTime的时间单位,常见的有TimeUnit.SECONDS(秒)和TimeUnit.MILLISECONDS(毫秒)。
workQueue(工作队列):用于存放待执行的任务的队列。常见的队列有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue和PriorityBlockingQueue等。队列的选择会影响线程池的性能。
threadFactory(线程工厂):用来生产一组相同任务的线程。
RejectedExecutionHandler(拒绝策略):当任务无法被执行时,如工作队列已满且线程池已满,所采取的策略。常见的策略有AbortPolicy(默认,直接抛出异常)、CallerRunsPolicy(调用者运行策略)、DiscardPolicy(丢弃策略)和DiscardOldestPolicy(丢弃最旧策略)。
✨65、线程池的种类有哪些
在Java中,常见的线程池种类有以下几种:
(1) FixedThreadPool
(固定大小线程池):该线程池固定线程数量,适用于执行长期的任务,限制线程数量可以控制系统资源的消耗。
//创建一个固定大小的线程池,核心线程数和最大线程数都是3
ExecutorService executorService = Executors.newFixedThreadPool(3);
(2) CachedThreadPool
(缓存线程池):该线程池根据需要创建新线程,如果线程在60秒内未被使用,则会被终止并从线程池中移除。适用于执行大量短期任务的场景。
//创建一个缓存的线程,没有核心线程数,最大线程数为Integer.MAX_VALUE
ExecutorService exec = Executors.newCachedThreadPool();
(3) SingleThreadPool
(单线程池):该线程池只有一个工作线程,适用于需要保证任务按顺序执行的场景。
//单个线程池,核心线程数和最大线程数都是1
ExecutorService exec = Executors.newSingleThreadExecutor();
(4) ScheduledThreadPool
(定时任务线程池):该线程池可以定期执行任务或延迟执行任务。
//按照周期执行的线程池,核心线程数为2,最大线程数为Integer.MAX_VALUE
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
这些线程池都是通过Java标准库中的java.util.concurrent.Executors
类来创建的。
✨66、线程池的创建方式
【65】的四种方式和ThreadPoolExecutor方式(推荐这种方式)。
ThreadPoolExecutor方式创建线程池:
int corePoolSize = 5; // 核心线程数
int maximumPoolSize = 10; // 最大线程数
long keepAliveTime = 60; // 非核心线程闲置时的存活时间
TimeUnit unit = TimeUnit.SECONDS; // 时间单位
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); // 任务队列
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue
);
✨67、覆盖索引
覆盖索引指的是在查询中所需要的所有字段都能够从索引中获取,而无需访问实际的表数据。这样可以显著提高查询性能,因为读取索引比读取表数据通常要快。
假设有一个包含 id、name 和 age 列的表 people,且经常需要查询 name 和 age 列。创建覆盖索引的SQL语句可能如下:
CREATE INDEX idx_people_name_age ON people(name, age);
这样,当执行如下查询时:
SELECT name, age FROM people WHERE name = 'Alice';
数据库可以直接从 idx_people_name_age 索引中获取 name 和 age 的值,而无需访问 people 表,从而加快查询速度。
✨68、mysql中索引列可以有null值吗
可以。
- 在 B-Tree 索引中(包括普通索引和唯一索引),NULL 值是被允许的。可以对包含 NULL 值的列创建索引,并且 MySQL 能够在查询中使用这些索引。
- 在唯一索引中,多个 NULL 值是不被视为相等的,因此允许多个 NULL 值。
- 在复合索引(即多列索引)中,索引可以包含 NULL 值,并且 MySQL 可以在查询中使用这些索引。
✨69、mysql中索引列可以有空值吗
可以。
- 普通索引:可以包含空字符串。这对索引没有特殊要求。
- 唯一索引:空字符串被视为一个有效的值,必须在唯一索引的约束下是唯一的。
✨70、复合索引和组合索引有什么区别
(1)联合索引通常也称为复合索引或多列索引。
复合索引是指一个索引包含多个列,这些列组合在一起形成一个索引。
假设有一个表 people,包含列 first_name、last_name 和 age,可以创建一个复合索引:
CREATE INDEX idx_name_age ON people(first_name, last_name, age);
这个索引可以优化如下查询:
SELECT * FROM people WHERE first_name = 'John' AND last_name = 'Doe';
SELECT * FROM people WHERE first_name = 'John' AND last_name = 'Doe' AND age = 30;
SELECT * FROM people WHERE first_name = 'John';
但不能有效优化如下查询:
SELECT * FROM people WHERE last_name = 'Doe';
SELECT * FROM people WHERE age = 30;
因为索引的顺序是 first_name、last_name、age,查询条件必须从最左边的列开始。
(2)组合索引通常是指在多个列上分别创建单列索引。这种方式虽然也可以用于优化查询,但与复合索引不同,其优化效果有限。
假设有一个表 people,可以分别为 first_name、last_name 和 age 创建单列索引:
CREATE INDEX idx_first_name ON people(first_name);
CREATE INDEX idx_last_name ON people(last_name);
CREATE INDEX idx_age ON people(age);
这样可以优化如下查询:
SELECT * FROM people WHERE first_name = 'John';
SELECT * FROM people WHERE last_name = 'Doe';
SELECT * FROM people WHERE age = 30;
但如果查询包含多个条件,比如:
SELECT * FROM people WHERE first_name = 'John' AND last_name = 'Doe';
MySQL 可能无法同时使用多个单列索引进行优化,虽然有时会使用索引合并,但其效率通常不如复合索引。
✨71、redis中的延迟双删
延时双删是一种在Redis中处理缓存雪崩的技术。
延时双删的思路是在缓存失效时,不立即删除缓存,而是设置一个较短的过期时间,同时异步任务在这个过期时间后再次确认缓存是否存在,如果存在则延长其生存时间。这样可以避免大量缓存同时失效的情况,分散了请求对数据库的压力。
实现步骤:
- 当缓存失效时,不立即删除缓存,而是设置一个较短的过期时间。
- 启动一个异步任务,在这个较短的过期时间后检查缓存是否还存在。
- 如果缓存还存在,则再次设置缓存的过期时间,延长其生存时间。
- 如果缓存不存在,则不进行任何操作,让缓存自然过期。
延迟双删,如果是写操作,先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。
✨72、java中集合可以存储基本数据类型吗
在 Java 中,集合不能直接存储基本数据类型(如 int, char, double 等)。这是因为 Java 集合类(如 ArrayList, HashSet, HashMap 等)只能存储对象(即引用类型),而基本数据类型不是对象。
但Java提供了对应的包装类,例如:
- int -> Integer
- char -> Character
- double -> Double
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
// 创建一个存储整数的ArrayList
ArrayList<Integer> list = new ArrayList<>();
// 添加基本数据类型int的包装类Integer
list.add(10);
list.add(20);
list.add(30);
// 输出列表内容
for (Integer num : list) {
System.out.println(num);
}
}
}
✨73、为什么要重写equals?为什么重写equals()也要重写hashCode()?
在Java中,equals()方法用于比较两个对象的内容是否相等。默认情况下,equals()方法在Object类中定义,它比较的是对象的引用,而不是对象的内容。也就是说,如果没有重写equals()方法,那么当使用这个方法来比较两个对象时,它会检查这两个对象是否指向内存中的同一个位置,而不是它们的内容是否相等。
然而,在许多情况下,我们可能更关心对象的内容是否相等,而不是它们是否指向内存中的同一个位置。例如,我们可能有两个Person对象,它们的name和age属性都相同,但是它们在内存中的位置不同。在这种情况下,我们可能希望equals()方法返回true,因为从我们的角度来看,这两个Person对象是相等的。
因此,我们需要重写equals()方法,以便它能够根据我们的需求来比较对象的内容,而不是它们的引用。这通常涉及到比较对象的每个重要字段,以确定它们是否相等。
同时,当我们重写equals()方法时,我们也需要重写hashCode()方法,以保持equals()方法和hashCode()方法的一致性。这是因为在Java中,当两个对象相等时(即equals()方法返回true),它们的哈希码(hashCode()方法的返回值)也应该相等。如果你只重写了equals()方法而没有重写hashCode()方法,那么可能会导致当你将对象存储在哈希表(如HashSet或HashMap)中时出现问题。因为哈希表是通过哈希码来确定对象存储位置的,如果两个内容相等的对象哈希码不同,那么它们就会被错误地存储在不同的位置,从而导致哈希表的行为不正确。因此,当你重写equals()方法时,也应该重写hashCode()方法,以保持它们的一致性。这是Java编程的一个重要规则。
✨74、如何重写equals和hashcode
重写 equals
和 hashCode
方法是为了确保对象在集合类(如 HashSet
, HashMap
等)中能够正确地比较和使用。
(1)equals
方法的重写
equals
方法用于比较两个对象在逻辑上是否相等。通常的步骤是:
- 检查是否为同一个对象引用,如果是直接返回
true
。 - 检查是否为
null
或者不同的类,如果是直接返回false
。 - 将参数对象转换为当前类的类型。
- 比较每一个关键字段是否相等。
例如,假设有一个 Person
类,重写 equals
方法如下:
import java.util.Objects;
public class Person {
private String name;
private int age;
// Constructor, getters, setters...
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true; // 如果是同一个对象返回true
}
if (obj == null || getClass() != obj.getClass()) {
return false; // 如果对象为空或者类型不一样返回false
}
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name); // 逻辑相等则返回true
}
// Other methods...
}
(2)hashCode
方法的重写
hashCode
方法返回对象的哈希码,它通常和 equals
方法一起重写。主要步骤包括:
- 选择一个非零的常数值,例如
31
。 - 初始化一个结果变量,例如
result
,并给它赋一个非零的常数值。 - 对象的每个关键字段,利用
result
乘以31
,然后加上该字段的hashCode()
的值。 - 返回
result
。
例如:通过比较 this 和传入的对象 obj,可以确定它们是否引用同一个对象或是否具有相同的属性值。
import java.util.Objects;
public class Person {
private String name;
private int age;
// Constructor, getters, setters...
@Override
public boolean equals(Object obj) {
if (this == obj) { // this 指的是调用 equals 方法的那个对象
return true; // 如果是同一个对象返回true
}
// getClass() 方法返回当前对象的类
if (obj == null || getClass() != obj.getClass()) {
return false; // 如果对象为空或者类型不一样返回false
}
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name); // 逻辑相等则返回true
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
// Other methods...
}
注:
- 在重写
equals
方法时,始终要重写hashCode
方法。这是因为,如果两个对象根据equals
方法比较是相等的,那么它们的hashCode
方法应该返回相同的值。 - 在计算
hashCode
时,通常使用Objects.hash
方法来简化计算过程,避免手动计算哈希码时的错误和重复。
✨74、Arrays.sort()的时间复杂度是多少
Arrays.sort()
方法在Java中用于对数组进行排序。对于基本数据类型,它使用双轴快速排序算法(Dual-Pivot Quicksort);对于对象数组,它使用的是改进的归并排序(Timsort)。
- 对于双轴快速排序(Dual-Pivot Quicksort),其平均时间复杂度和最好的情况下的时间复杂度都是𝑂(𝑛log𝑛),其中n是数组的长度。然而,在最坏的情况下,时间复杂度可能会达到𝑂(𝑛2)。
- 对于Timsort,无论在最好、最坏还是平均情况下,时间复杂度都是𝑂(𝑛log𝑛)。
✨75、Java中队列如何使用
在 Java 中,队列(Queue)是一种常用的数据结构,用于存储元素并支持特定的操作,例如插入(enqueue)和移除(dequeue)元素。Java 提供了多种队列的实现,常见的包括 LinkedList
和 ArrayDeque
。
(1)使用 LinkedList
实现队列
LinkedList
可以很方便地实现队列,因为它实现了 Queue
接口,同时也是一个双向链表。
import java.util.LinkedList;
import java.util.Queue;
public class QueueExample {
public static void main(String[] args) {
// 创建一个 LinkedList 对象,作为队列
Queue<Integer> queue = new LinkedList<>();
// 添加元素到队列尾部(enqueue)
queue.offer(5);
queue.offer(3);
queue.offer(8);
// 打印队列中的元素
System.out.println("队列中的元素:" + queue);
// 从队列头部移除一个元素(dequeue)
int removedElement = queue.poll();
System.out.println("移除的元素:" + removedElement);
// 查看队列头部的元素,但不移除
int peekedElement = queue.peek();
System.out.println("头部的元素:" + peekedElement);
// 打印队列当前的元素
System.out.println("队列中的元素:" + queue);
}
}
(2)使用 ArrayDeque
实现队列
ArrayDeque
是另一个实现 Deque
接口的双端队列,也可以用作普通队列。
import java.util.ArrayDeque;
import java.util.Queue;
public class QueueExample {
public static void main(String[] args) {
// 创建一个 ArrayDeque 对象,作为队列
Queue<Integer> queue = new ArrayDeque<>();
// 添加元素到队列尾部(enqueue)
queue.offer(5);
queue.offer(3);
queue.offer(8);
// 打印队列中的元素
System.out.println("队列中的元素:" + queue);
// 从队列头部移除一个元素(dequeue)
int removedElement = queue.poll();
System.out.println("移除的元素:" + removedElement);
// 查看队列头部的元素,但不移除
int peekedElement = queue.peek();
System.out.println("头部的元素:" + peekedElement);
// 打印队列当前的元素
System.out.println("队列中的元素:" + queue);
}
}
队列操作方法解释:
offer(E e)
:将元素添加到队列的尾部(enqueue),如果成功返回true
,如果队列已满则返回false
。poll()
:移除并返回队列头部的元素(dequeue),如果队列为空则返回null
。peek()
:查看队列头部的元素,但不移除,如果队列为空则返回null
。
✨76、Java中对象使用 = 号赋值时,引用的是地址还是值?
在 Java 中,使用 =
赋值操作符时,对于引用类型(即类的实例对象、数组等),实际上是复制了引用(即内存地址),而不是复制对象本身的内容或值。所以修改本对象的值会影响之前的对象的值。
✨77、如果要引用对象的值如何实现
可以通过深拷贝(deep copy)来实现。深拷贝会创建一个新的对象,并复制原对象中所有字段的值。
以下是几种常见的深拷贝方法:
(1)手动复制构造函数
创建一个复制构造函数,显式地复制对象的每一个字段。
class MyClass {
private int value;
private String name;
public MyClass(int value, String name) {
this.value = value;
this.name = name;
}
// 复制构造函数
public MyClass(MyClass other) {
this.value = other.value;
this.name = new String(other.name); // 确保字符串是新对象
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "MyClass{" +
"value=" + value +
", name='" + name + '\'' +
'}';
}
}
public class Main {
public static void main(String[] args) {
MyClass obj1 = new MyClass(10, "Original");
MyClass obj2 = new MyClass(obj1); // 使用复制构造函数
obj2.setValue(20);
obj2.setName("Copy");
System.out.println("obj1: " + obj1); // obj1: MyClass{value=10, name='Original'}
System.out.println("obj2: " + obj2); // obj2: MyClass{value=20, name='Copy'}
}
}
(2)实现 Cloneable
接口
实现 Cloneable
接口并覆盖 clone()
方法。
class MyClass implements Cloneable {
private int value;
private String name;
public MyClass(int value, String name) {
this.value = value;
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
MyClass cloned = (MyClass) super.clone();
cloned.name = new String(this.name); // 确保字符串是新对象
return cloned;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "MyClass{" +
"value=" + value +
", name='" + name + '\'' +
'}';
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
MyClass obj1 = new MyClass(10, "Original");
MyClass obj2 = (MyClass) obj1.clone(); // 使用 clone 方法
obj2.setValue(20);
obj2.setName("Copy");
System.out.println("obj1: " + obj1); // obj1: MyClass{value=10, name='Original'}
System.out.println("obj2: " + obj2); // obj2: MyClass{value=20, name='Copy'}
}
}
(3)使用序列化和反序列化
通过将对象序列化到字节流,然后反序列化回一个新的对象。这种方法适用于对象比较复杂时。
import java.io.*;
class MyClass implements Serializable {
private int value;
private String name;
public MyClass(int value, String name) {
this.value = value;
this.name = name;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "MyClass{" +
"value=" + value +
", name='" + name + '\'' +
'}';
}
public MyClass deepCopy() {
try {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteOut);
out.writeObject(this);
ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
ObjectInputStream in = new ObjectInputStream(byteIn);
return (MyClass) in.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
public class Main {
public static void main(String[] args) {
MyClass obj1 = new MyClass(10, "Original");
MyClass obj2 = obj1.deepCopy(); // 使用 deepCopy 方法
obj2.setValue(20);
obj2.setName("Copy");
System.out.println("obj1: " + obj1); // obj1: MyClass{value=10, name='Original'}
System.out.println("obj2: " + obj2); // obj2: MyClass{value=20, name='Copy'}
}
}
✨78、mybatis中${}和#{}有什么区别
(1)${}
(字符串替换):
- 直接进行字符串替换,不会对参数做任何处理。
- 存在SQL注入风险,因为参数值直接替换到SQL语句中。
- 用于传递表名、列名等需要动态拼接SQL的场景。
SELECT * FROM ${tableName} WHERE ${columnName} = '${value}'
在上述例子中,${tableName}
和${columnName}
的值会直接替换到SQL语句中。
(2)#{}
(预编译参数):
- 采用预编译方式,会将参数值传递给SQL语句中的占位符。
- 安全性较高,因为参数值会通过JDBC的
PreparedStatement
设置,避免了SQL注入问题。 - 用于传递参数值,特别是用户输入的参数。
SELECT * FROM users WHERE id = #{userId}
在上述例子中,#{userId}
会被替换成一个占位符,实际执行时MyBatis会将userId
的值传递给这个占位符。
(3)SQL注入示例:
假设有以下参数:
String tableName = "users";
String columnName = "id";
String value = "1 OR 1=1"; // 潜在的SQL注入攻击
int userId = 1;
使用${}
的SQL语句:
SELECT * FROM ${tableName} WHERE ${columnName} = '${value}'
生成的SQL可能是:
SELECT * FROM users WHERE id = '1 OR 1=1'
这条SQL语句会返回所有用户,因为条件总是为真,暴露了SQL注入的风险。
使用#{}
的SQL语句:
SELECT * FROM users WHERE id = #{userId}
生成的SQL是:
SELECT * FROM users WHERE id = ?
MyBatis会将userId
的值设置为参数1
,这不会引发SQL注入问题。
✨79、Linux命令中whereis 和which的区别
which
主要用于查找可执行文件的路径,并且只在用户的PATH
中查找。whereis
不仅查找可执行文件,还查找相关的源代码和手册页文件,查找范围也更广泛。
(1)which
命令
- 用途:用于在环境变量
PATH
中查找并显示可执行文件的位置。 - 查找范围:只在
PATH
环境变量中定义的目录中查找。 - 输出:返回找到的第一个匹配的可执行文件路径。
- 常用选项:
-a
:显示所有匹配的可执行文件路径,而不仅仅是第一个。
which ls
# 输出类似于:/bin/ls
which -a ls
# 输出类似于:
# /bin/ls
# /usr/bin/ls
(2)whereis
命令
- 用途:用于查找命令的可执行文件、源代码文件和 man 手册页的位置。
- 查找范围:不仅查找
PATH
环境变量中的目录,还查找预定义的标准位置(如/usr/bin
,/usr/local/bin
,/bin
等等),有时甚至包括文档和源代码的位置。 - 输出:返回可执行文件、源代码文件和手册页的路径。
- 常用选项:
-b
:只查找可执行文件。-m
:只查找 man 手册页文件。-s
:只查找源代码文件。
whereis ls
# 输出类似于:ls: /bin/ls /usr/share/man/man1/ls.1.gz
whereis -b ls
# 输出类似于:ls: /bn/ls
✨80、MyBatis-Plus基本结构
UserService.java
是服务层接口,定义了业务逻辑方法。UserServiceImpl.java
实现了服务层接口,并使用UserMapper
进行数据库操作。UserMapper.java
是数据访问层接口,定义了数据库操作方法。UserMapper.xml
是SQL映射文件,为UserMapper.java
接口提供自定义的SQL语句。
UserService.java接口
import com.baomidou.mybatisplus.extension.service.IService;
public interface UserService extends IService<User> {
// 可以在这里定义一些自定义的方法
}
UserServiceImpl.java实现类
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
// 可以在这里实现自定义的方法
}
Mapper接口UserMapper.java:用于定义对User
实体进行数据库操作的方法。
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 可以在这里定义一些自定义的查询方法
}
UserMapper.xml:一般的数据库操作MyBatis-Plus都提供了,这个文件写自定义的SQL。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yourpackage.mapper.UserMapper">
<!-- 示例:自定义查询 -->
<select id="selectUserById" resultType="com.yourpackage.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 示例:自定义插入 -->
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (name, age, email) VALUES (#{name}, #{age}, #{email})
</insert>
<!-- 示例:自定义更新 -->
<update id="updateUser">
UPDATE user SET name = #{name}, age = #{age}, email = #{email} WHERE id = #{id}
</update>
<!-- 示例:自定义删除 -->
<delete id="deleteUserById">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper>
✨81、Spring Data JPA、MyBatis、MyBatis-Plus区别
Spring Data JPA和MyBatis-Plus基本的SQL不需要自己写,框架已提供,但复杂的需要自己写。MyBatis需要写所有的SQL,简单的也需要自己手动写。
✨82、mybatis-plus和spring data jpa键的自增策略
(1)MyBatis-Plus 主键自增策略
- IdType.AUTO:数据库ID自增
- IdType.NONE:该类型为未设置主键类型(即默认值)
- IdType.INPUT:自行输入
- IdType.ID_WORKER:全局唯一ID(雪花算法)
- IdType.UUID:全局唯一ID(UUID)
- IdType.ID_WORKER_STR:字符串全局唯一ID(雪花算法)
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
// getters and setters
}
(2)Spring Data JPA 主键自增策略
- GenerationType.AUTO:JPA 自动选择合适的策略
- GenerationType.IDENTITY:依赖数据库的自增字段
- GenerationType.SEQUENCE:使用数据库序列,通常在 Oracle 等数据库中使用
- GenerationType.TABLE:使用表保存id值
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// getters and setters
}
✨83、ArrayList底层原理
ArrayList在底层是用一个Object数组实现的。
- ArrayList内部使用Object[] elementData数组存储元素,随着元素的添加,数组容量会自动增加。
- 在初始化时,elementData的长度为10,也就是默认容量是10。
- 当添加元素时,如果元素数量超过数组长度,就会进行扩容操作:
- 扩容为原来容量的1.5倍。
- 新建一个更大的数组,将原有元素复制过来。
- 将elementData指向新数组。
- get操作直接通过下标访问elementData数组即可获取元素值。
- remove操作是通过将下标后面的元素前移来删除的。
- 由于底层使用数组实现,随机访问效率高,但增加和删除元素效率不高,因为可能需要移动大量元素。
- 所有方法都是同步的,是线程安全的。
✨84、Java中的底层接口有哪些
主要的java底层接口有:
(1)java.lang.Iterable
Iterable接口是Iterator模式的基础,它定义了iterator()方法来获取遍历元素使用的Iterator对象。实现Iterable接口的类可以使用foreach循环进行遍历。
(2)java.util.Collection
Collection接口是集合的主要基础接口,它扩展了Iterable接口。它定义了很多与集合操作有关的方法,如add(),remove()等。List、Set、Queue等常用集合类都实现了这个接口。
(3)java.util.Map
Map接口定义了键值对映射的基本功能,比如put()方法、get()方法等。它与Map.Entry内部类一起定义了Java中的映射功能。常用的HashMap、TreeMap等都实现了这个接口。
(4)java.lang.Comparable
Comparable接口用于定义元素的自然顺序,实现这个接口的类可以比较大小。它只定义了一个compareTo()方法。此接口很常用于构建Collections.sort()等排序功能。
(5)java.lang.Cloneable
Cloneable接口是一个标记接口,它告诉JVM这个类支持clone操作。Object类的clone()方法检查此接口来决定是否进行对象拷贝。
✨85、怎么获取一个子线程的执行结果
获取一个子线程的执行结果主要有以下几种方法:
- 把子线程需要返回的结果放入共享变量中
子线程执行完成后,将结果放入一个共享变量或队列中,主线程再从中获取结果。
- 使用join()方法等待子线程完成
主线程调用子线程的join()方法,等待子线程执行完成,然后主线程可以获取子线程执行的返回值。
- 使用Future模式获取异步结果
使用Callable和FutureTask,子线程执行Callable并将结果传递给FutureTask,主线程通过FutureTask的get()方法获取结果。
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
// child thread task
return result;
});
int result = future.get();
- 使用CountDownLatch等待子线程
主线程利用CountDownLatch等待子线程计算完成,子线程计算完成后将结果通过参数或共享变量传回。
- 使用线程池的awaitTermination获取执行结果
子线程计算结束后将结果放入共享变量或返回,主线程在等待线程池任务执行完后获取结果。
✨86、什么类型的sql操作会引起表级锁
- INSERT操作:对表进行插入时,会造成表的写锁。
- UPDATE操作:对表数据进行更新时,会造成表的写锁。
- DELETE操作:对表进行删除数据时,会造成表的写锁。
- TRUNCATE操作:清空表所有数据时,会造成表的写锁。
- ALTER TABLE操作:对表结构进行修改,比如添加/删除字段等,会造成表的写锁。
- CREATE INDEX操作:创建表索引时,会造成表的写锁。
- DROP INDEX操作:删除表索引时,会造成表的写锁。
- OPTIMIZE TABLE操作:对表进行优化时,会造成表的写锁。
- REPAIR TABLE操作:修复表数据时,也会造成表的写锁。
- SELECT ... FOR UPDATE:搭配FOR UPDATE后缀的查询操作,同样会造成表的写锁。
- Group by或Distinct查询:如果涉及排序操作,可能会造成间隙锁升级成表锁。
- 复杂的联合查询:涉及多个表连接或子查询时,可能会造成锁升级。
✨87、怎么用Redis实现一个简易的消息队列
- 定义消息队列名称
例如用字符串"my_queue"表示一个消息队列。
- 消息入队
使用LPUSH命令将消息添加到队列尾部:
LPUSH my_queue message1
LPUSH my_queue message2
- 消息出队
使用RPOP命令从队列尾部移除并返回一个消息:
RPOP my_queue
- 消息确认
消费者处理完消息后,需要使用DEL命令删除该消息,表示消息已确认:
DEL processed_message
- 超时处理
可以为每个消息设置过期时间,如果在一段时间内未确认,则重新入队。
- 并发控制
Redis本身是单线程的,可使用WATCH保证原子性操作。
- 错误处理
拦截异常情况下的消息后再重新入队。
- 调试和监控
使用Redis内置命令监控队列状态。
✨88、znode节点状态stat的属性
在 ZooKeeper 中,每个 znode 都有一个称为 stat
的数据结构,它包含了有关该 znode 当前状态的元数据信息。假设有一个 znode /exampleNode
,其 stat
结构如下:
czxid = 0x1000
mzxid = 0x2000
ctime = 1632174023123
mtime = 1632174028123
version = 5
cversion = 3
aversion = 1
ephemeralOwner = 1234567890
dataLength = 50
numChildren = 2
czxid
和mzxid
属性表示了 znode 的创建和最后修改时的事务 ID。ctime
和mtime
表示了 znode 的创建时间和最后修改时间。version
、cversion
和aversion
属性分别表示 znode 数据版本号、子节点版本号和 ACL 版本号。ephemeralOwner
显示了这个 znode 是否是临时节点,以及它的拥有者会话 ID。dataLength
表示 znode 存储的数据的长度。numChildren
表示该 znode 的子节点数量。
✨89、spring、springmvc、springboot有什么区别和联系
- Spring 是一个综合性的框架,提供了核心的功能和特性。
- Spring MVC 是 Spring 框架的一部分,专注于 Web 应用程序开发,实现了 MVC 模式。
- Spring Boot 是基于 Spring 框架的快速开发框架,简化了 Spring 应用程序的初始化和配置。
✨90、java.time.LocalDate和 java.util.Date有什么区别
java.time.LocalDate
和java.util.Date
是Java中表示日期的两种不同的类,它们有以下区别:
数据类型:
java.time.LocalDate
是Java 8引入的日期类,属于Java日期时间API(Date and Time API)的一部分,而java.util.Date
是旧版Java中的日期类。可变性:
java.time.LocalDate
是不可变类,一旦创建就不能修改,而java.util.Date
是可变类,可以通过方法修改其值。精度:
java.time.LocalDate
只表示日期,不包含时间和时区信息,精确到天,而java.util.Date
表示日期和时间,并且精确到毫秒级别。时区处理:
java.time.LocalDate
没有时区信息,它是与时区无关的日期,而java.util.Date
在内部存储了一个表示自1970年1月1日以来的毫秒数的长整型值,可以通过设置时区来调整显示的日期和时间。API设计:
java.time.LocalDate
提供了丰富的日期操作方法,如计算日期差异、比较日期、格式化日期等,而java.util.Date
的API相对较少,对日期操作需要使用java.util.Calendar
类。
由于java.time.LocalDate
是较新的日期类,它在功能和性能上都优于java.util.Date
。推荐在新的Java项目中使用java.time.LocalDate
来处理日期,以获得更好的可读性和易于使用的API。如果需要与旧版Java代码进行兼容,可以使用java.util.Date
,但要注意在不同类之间进行转换时可能需要使用java.time
包中的类来进行转换。
✨91、IOC与AOP
IOC(BeanFactory)和AOP(ApplicationContext)是Spring框架中的两个核心特性,简化开发工作,提高代码的可维护性和可重用性。
IOC(控制反转):将对象的创建交给容器进行管理。
在传统的编程模式中,程序通常由一个主控制流来控制对象的创建和调用,这样代码之间的依赖关系会变得非常紧密,修改一个模块可能需要修改多个模块,这样会导致代码的可维护性和可测试性降低。而IOC则通过将控制权交给容器来管理对象之间的依赖关系,从而实现代码的解耦和复用。
IOC通常通过依赖注入(Dependency Injection)的方式来实现。依赖注入是指容器在创建对象时,自动将该对象所依赖的其他对象注入到该对象中,从而实现对象之间的关联和通信。通过依赖注入,我们可以将对象之间的依赖关系从代码中抽象出来,更好地管理和维护这些关系。
AOP(面向切面编程)
在传统的面向对象编程中,我们通常通过继承和组合等方式来实现代码的复用,但这种方式有时会导致代码的耦合性增加,难以维护。而AOP则提供了一种新的编程思想,通过将代码的某些功能(例如日志记录、异常处理、安全校验等)从主业务逻辑中分离出来,形成一个独立的切面(Aspect),从而实现代码的解耦和复用。
AOP通常使用切面(Aspect)和连接点(Join Point)来描述代码的功能和执行时机。**切面是一组横跨多个对象的公共行为,例如日志记录或性能统计等。**连接点则是程序执行过程中的一个特定点,例如方法调用或异常抛出等。通过在连接点处织入切面,就可以实现对程序行为的增强,例如在方法执行前后添加日志记录、在异常抛出时进行统一处理等。
AOP面向切面编程,将代码中重复的部分抽取出来,使用动态代理技术,在不修改源码的基础上对方法进行增强。如果目标对象实现了接口,默认采用JDK动态代理,也可以强制使用CGLib,如果目标对象没有实现接口,采用CGLib的方式。常用的场景包括权限认证、自动缓存、错误处理、日志、调试和事务等。
✨92、Spring容器的作用
Spring容器是Spring框架的核心部分之一,它的主要作用是管理和组织应用程序中的对象(也称为Bean)的生命周期和依赖关系。具体来说,Spring容器提供了以下几个重要的功能:
- 依赖注入(Dependency Injection):Spring容器负责将应用程序中的对象相互连接,并通过依赖注入的方式将对象所需的依赖项自动注入到对象中。这样,对象之间的耦合性减低,使得应用程序更加灵活、可扩展和易于测试。
- Bean的生命周期管理:Spring容器负责管理Bean对象的生命周期,包括实例化、初始化、调用初始化方法、销毁等。通过配置和扩展Spring容器,可以在Bean的不同生命周期阶段执行自定义的操作。
- 面向切面编程(AOP):Spring容器支持面向切面编程,通过AOP机制可以在应用程序的不同模块中,通过声明的方式插入横切关注点(如日志、事务管理等),从而实现对横切关注点的集中管理和复用。
- 配置管理:Spring容器允许通过配置文件(如XML、注解或Java配置类)来定义和管理应用程序中的Bean对象及其相关信息,包括Bean的属性、依赖关系、作用域、生命周期等。这种配置的方式使得应用程序的配置更加灵活和易于维护。
- 提供额外的服务:Spring容器提供了许多其他的服务和功能,例如国际化、事件机制、缓存管理、安全性等,以帮助开发人员更方便地构建企业级应用程序。
✨93、BeanDefinition
BeanDefinition
是 Spring 框架中的一个核心概念,它用于描述和定义在 Spring 容器中创建的 Bean 对象。BeanDefinition
定义了 Bean 的属性、依赖关系、作用域以及其他相关信息,它充当了 Bean 实例化和配置的元数据。
BeanDefinition
中包含了以下重要的信息:
- 类名(Class Name):指定了要创建的 Bean 对象的类名。
- 作用域(Scope):指定了 Bean 对象的作用域,例如单例(Singleton)、原型(Prototype)、会话(Session)、请求(Request)等。
- 构造函数参数(Constructor Arguments):描述了在创建 Bean 对象时所需的构造函数参数,包括参数类型和值。
- 属性(Properties):指定了 Bean 对象的属性,包括属性名称和对应的值。
- 依赖关系(Dependencies):定义了 Bean 对象之间的依赖关系,指定了其他 Bean 对象作为当前 Bean 对象的依赖。
- 初始化方法(Initialization Method):指定了在实例化 Bean 对象后调用的初始化方法。
- 销毁方法(Destroy Method):指定了在容器销毁 Bean 对象时调用的销毁方法。
通过 BeanDefinition
的定义,Spring 容器可以根据配置文件、注解或编程方式来创建和管理 Bean 对象。在容器启动时,它会解析 BeanDefinition
的信息,并根据定义实例化 Bean 对象,并在需要时进行依赖注入、初始化和销毁。
✨94、Spring启动流程
Spring的IoC容器在实现控制反转和依赖注入的过程中,可以划分为两个阶段:
容器启动阶段
Bean实例化阶段
容器启动阶段
加载配置
分析配置信息
将Bean信息装配到BeanDefinition
将Bean信息注册到相应的BeanDefinitionRegistry
其他后续处理
Bean实例化阶段
根据策略实例化对象
装配依赖
Bean初始化前处理
对象初始化
对象其他处理
注册回调接口
✨95、BeanDefinitionRegistry
BeanDefinitionRegistry
是 Spring 框架中的一个接口,用于管理和操作 Bean 定义(BeanDefinition
)。它扩展了 org.springframework.beans.factory.support.BeanFactory
接口,并在其基础上提供了额外的方法,用于注册、获取和操作 Bean 定义。
BeanDefinitionRegistry
提供了以下常用方法:
- 注册 Bean 定义(registerBeanDefinition):通过调用
registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
方法,可以向注册表中注册一个新的 Bean 定义。beanName
是要注册的 Bean 的名称,而beanDefinition
是对应的 Bean 定义对象。 - 移除 Bean 定义(removeBeanDefinition):通过调用
removeBeanDefinition(String beanName)
方法,可以从注册表中移除指定名称的 Bean 定义。 - 获取 Bean 定义(getBeanDefinition):通过调用
getBeanDefinition(String beanName)
方法,可以根据名称获取注册表中指定 Bean 的定义。 - 检查 Bean 定义是否存在(containsBeanDefinition):通过调用
containsBeanDefinition(String beanName)
方法,可以检查注册表中是否存在指定名称的 Bean 定义。
通过 BeanDefinitionRegistry
,可以在运行时动态地注册、移除和获取 Bean 定义。这对于需要根据条件或特定业务逻辑来动态创建和管理 Bean 的情况非常有用。
BeanDefinitionRegistry
接口的实现类包括 DefaultListableBeanFactory
和 GenericApplicationContext
等,它们是 Spring 框架中常用的容器实现类。这些实现类提供了对 Bean 定义的管理和操作的功能,并与其他 Spring 组件(如 BeanFactoryPostProcessor
和 BeanPostProcessor
)紧密集成,实现了 Spring 容器的核心功能。
✨96、依赖注入
将对象的依赖关系通过外部方式注入到对象中的过程(依赖注入),注意和实例化Bean的区别。Bean实例化使用不同的策略(单例、原型等),spring 容器会根据配置文件或注解的定义,调用适当的构造函数或工厂方法来创建对象。
DI (dependcy injection):依赖注入,Spring 框架核心 IOC 的具体实现方式。即让框架自动把对象传入,不需要使用者自动去获取。
- 构造函数注入(Constructor Injection):通过构造函数注入,Spring 容器会通过构造函数创建对象实例,并将依赖的其他 bean 或常量值作为构造函数的参数传入。这种方式要求目标类必须拥有对应的构造函数,并且在配置文件或注解中指定构造函数的参数。
- Setter 方法注入(Setter Injection):通过 setter 方法注入,Spring 容器会先调用默认构造函数创建对象实例,然后通过 setter 方法设置依赖的其他 bean 或常量值。这种方式要求目标类必须提供对应的 setter 方法,并在配置文件或注解中指定属性的值。
- 工厂方法(Factory Method):在某些情况下,对象的实例化可能需要进行一些特殊的处理,此时可以使用工厂方法来创建对象。工厂方法是一种通过调用静态方法或实例方法来创建对象的方式。Spring 容器可以通过配置文件或注解指定工厂方法的调用,从而获取对象实例。
- 静态工厂方法(Static Factory Method):静态工厂方法是一种通过调用类的静态方法来创建对象的方式。与普通工厂方法不同的是,静态工厂方法不需要先创建工厂类的实例。Spring 容器可以通过配置文件或注解指定静态工厂方法的调用,从而获取对象实例。
- 实例工厂方法(Instance Factory Method):实例工厂方法是一种通过调用实例方法来创建对象的方式。与静态工厂方法不同的是,实例工厂方法需要先创建工厂类的实例,然后通过实例方法创建对象。Spring 容器可以通过配置文件或注解指定实例工厂方法的调用,从而获取对象实例。
✨97、实例化Bean
创建对象
https://blog.csdn.net/m0_52228992/article/details/125850810
- 构造方法
<bean id="bookDao" name="dao dao1 dao2 bookDao2" class="org.example.dao.impl.BookDaoImpl" scope="prototype" />
- 静态工厂
<bean id="orderDao" class="org.example.factory.OrderDaoFactory" factory-method="getOrderDao"/>
实例化工厂
<bean id="orderFactory" class="org.example.factory.OrderDaoFactory"/> <bean id="OrderDao" factory-bean="orderFactory" factory-method="getorderDao"/>
FactoryBean
是实例化工厂方法的延申
<bean id="orderdao" class="org.example.factory.OrderDaoFactoryBean"/>
✨98、实例化对象的策略
- 单例(Singleton):这是默认的实例化策略,表示 Spring 容器只会创建一个单例对象,并在容器的整个生命周期内重用该对象。在容器启动时,会立即创建单例对象,并在后续的请求中返回同一个对象实例。
- 原型(Prototype):原型是另一种常见的实例化策略,表示每次请求时都会创建一个新的对象实例。在容器启动时,并不会创建原型对象,而是在每次请求时根据原型定义创建新的对象。
- 会话(Session):会话是 Web 应用中的实例化策略,表示在每个会话期间(即用户会话期间)创建一个对象实例。在 Web 应用中,每个用户会话都可以有一个独立的对象实例。
- 请求(Request):请求是 Web 应用中的实例化策略,表示在每个请求中创建一个对象实例。在 Web 应用中,每个请求都可以有一个独立的对象实例。
✨99、BeanFactory和FactoryBean
BeanFactory是Factory也就是IOC容器或对象工厂,FactoryBean是Bean。
在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。但对于FactoryBean而言,这个Bean不是简单的Bean,而是一个能产生或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。
BeanFactory:Bean工厂,是一个工厂(Factory),我们Spring IoC容器的最顶层接口就是这个BeanFactory,它的作用是管理Bean,即实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。
FactoryBean:工厂Bean,是一个Bean,作用是产生其他bean实例。通常情况下,这种bean没有什么特别的要求,仅需要提供一个工厂方法,该方法用来返回其他bean实例。通常情况下,bean无须自己实现工厂模式,Spring容器担任工厂角色;但少数情况下,容器中的bean本身就是工厂,其作用是产生其它bean实例。
✨100、AOP中的通知
https://blog.csdn.net/blackball1998/article/details/115386800
通知有多种类型:前置通知、后置通知、环绕通知、异常通知、最终通知。
在面向切面编程(AOP)中,通知(Advice)是指在特定的切点(Join Point)上执行的一段代码逻辑。通知是AOP的核心概念之一,它用于在目标方法执行前、执行后或发生异常时插入额外的逻辑。
AOP中的通知有以下几种类型:
- 前置通知(Before Advice):在目标方法执行之前执行的通知。它可以用于执行一些准备操作或参数校验等。
- 最终通知(After Advice):在目标方法执行之后执行的通知。它可以用于执行一些清理操作或日志记录等。
- 返回通知(也叫后置通知,After Returning Advice):在目标方法成功执行并返回结果后执行的通知。它可以用于处理目标方法的返回值或进行后续操作。
- 异常通知(After Throwing Advice):在目标方法抛出异常时执行的通知。它可以用于处理目标方法抛出的异常或进行异常处理。
- 环绕通知(Around Advice):在目标方法执行前后包围着执行的通知。它可以在目标方法执行前后执行自定义的逻辑,并决定是否继续执行目标方法或修改返回值。
以上通知类型可以根据需求进行组合和使用,以实现对目标方法的横切关注点的插入。通过使用通知,可以将横切关注点(如日志记录、性能监控、事务管理等)与核心业务逻辑进行解耦,提高代码的可维护性和重用性。
在 AOP 中,通知与切点(定义切面的位置)一起组成了切面(Aspect),切面是AOP的另一个重要概念。通过配置或使用注解,可以将通知绑定到特定的切点上,从而在目标方法的执行过程中触发通知的执行。
✨101、springboot的启动流程
(1)启动类的执行
Spring Boot 应用通常有一个主类,包含 main
方法,并使用 @SpringBootApplication
注解。这个注解是一个组合注解,包含了 @Configuration
、@EnableAutoConfiguration
和 @ComponentScan
。
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
(2)SpringApplication
类的初始化
SpringApplication.run
方法是启动 Spring Boot 应用的入口。它会创建一个 SpringApplication
实例,并调用其 run
方法。
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return new SpringApplication(primarySource).run(args);
}
(3)设置应用上下文
SpringApplication
类会根据应用的类型(如 Web 应用或非 Web 应用)来决定使用哪种 ApplicationContext
。对于 Web 应用,通常会使用 AnnotationConfigServletWebServerApplicationContext
。
(4)加载应用配置
SpringApplication
会加载应用的配置,包括 application.properties
或 application.yml
文件中的配置,以及通过 @Configuration
注解定义的 Java 配置类。
(5)准备环境
SpringApplication
会准备应用运行的环境,包括设置系统属性、环境变量和配置文件。
(6)创建并刷新应用上下文
SpringApplication
会创建应用上下文,并调用 refresh
方法来初始化 Spring 容器。这一步包括以下几个子步骤:
- Bean 定义加载:扫描并加载所有的 Bean 定义。
- Bean 实例化:根据 Bean 定义创建 Bean 实例。
- 依赖注入:注入 Bean 之间的依赖关系。
- 初始化 Bean:调用 Bean 的初始化方法(如
@PostConstruct
注解的方法)。
(7)启动嵌入式服务器
对于 Web 应用,Spring Boot 会启动一个嵌入式服务器(如 Tomcat、Jetty 或 Undertow)。这一步包括以下几个子步骤:
- 创建 Web 服务器:创建并配置嵌入式 Web 服务器。
- 注册 Servlet 和过滤器:注册应用的 Servlet 和过滤器。
- 启动 Web 服务器:启动嵌入式 Web 服务器,使其开始监听 HTTP 请求。
(8)执行 CommandLineRunner
和 ApplicationRunner
在应用上下文刷新完成后,Spring Boot 会执行所有实现了 CommandLineRunner
或 ApplicationRunner
接口的 Bean。这些接口允许开发者在应用启动后执行一些自定义逻辑。
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// 自定义启动后逻辑
}
}
(9)应用启动完成
当所有的初始化步骤完成后,Spring Boot 应用就启动完成了。此时,应用已经准备好处理请求或执行其他任务。
(10)总结
Spring Boot 的启动流程可以总结为以下几个主要步骤:
- 执行启动类的
main
方法。 - 初始化
SpringApplication
实例。 - 设置应用上下文。
- 加载应用配置。
- 准备环境。
- 创建并刷新应用上下文。
- 启动嵌入式服务器(对于 Web 应用)。
- 执行
CommandLineRunner
和ApplicationRunner
。 - 应用启动完成。
✨102、RabbitMQ中,消息什么情况下会进入到死信队列
- 消息被拒绝并且不重新入队。当消费者使用
basicReject
或basicNack
方法拒绝消息,并且requeue
参数设置为false
,消息会被拒绝且不重新入队。这种情况下,消息会被路由到死信交换机(DLX),然后进入到死信队列。 - 消息过期。
- 队列达到最大长度。
- 消息被消费者否认并且不重新入队。消费者处理消息失败并使用
basicNack
拒绝消息(并且不重新入队),这也会使消息进入死信队列。
✨103、如何避免消息的重复消费问题?(消息消费时的幂等性)
可以使用全局唯一 ID + Redis解决非幂等性问题。
生产者在发送消息时,为每条消息设置一个全局唯一的 messageId,消费者拿到消息后,使用 setnx 命令,将 messageId 作为 key 放到 redis 中:setnx(messageId, 1),若返回1,说明之前没有消费过,正常消费;若返回 0,说明这条消息之前已消费过,抛弃。
- 消费者使用
SETNX
命令尝试将messageId
存储到Redis中。 - 如果
SETNX
返回1,表示Redis中不存在该messageId
,这是第一次处理该消息,消费者会正常处理消息并确认(ack)。 - 如果
SETNX
返回0,表示Redis中已经存在该messageId
,说明该消息已经被处理过,消费者会丢弃该消息并确认(ack),避免重复处理。
SETNX
命令:如果键不存在,SETNX
会设置键值,并返回1。如果键已经存在,SETNX
不会设置键值,并返回0。
✨104、rabbitmq中默认集群模式和镜像集群模式的区别
默认集群模式,只会把交换机、队列、虚拟主机等元数据信息在各个节点同步,而具体队列中的消息内容不会在各个节点中同步。
镜像模式是把所有的队列数据完全同步,包括元数据信息和消息数据信息,当然这对性能肯定会有一定影响,当对数据可靠性要求较高时,可以使用镜像模式。
在默认集群模式的基础上,执行下面的命令就可以把一个默认的集群模式变成镜像集群模式:
# 需要根据实际情况补充完整命令
./rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority]
✨105、布隆过滤器解决缓存穿透的原理
布隆过滤器的原理:
- 数据结构:布隆过滤器包含一个比特数组(通常是一个较大的数组)和若干个哈希函数。
- 插入元素:当一个元素被插入到布隆过滤器中时,会经过多个哈希函数,生成多个哈希值,然后将对应的比特数组位置设为1。
- 查询元素:当检查一个元素是否存在于布隆过滤器中时,同样会经过多个哈希函数,生成多个哈希值,然后检查对应的比特数组位置。如果所有对应的比特数组位置都为1,则认为该元素可能存在;如果有任何一个比特数组位置为0,则可以确定该元素一定不存在。
应用于缓存穿透的解决方案:
- 当一个请求到达时,首先将请求的参数(例如查询的主键或者关键字,不一定是键的ID)经过布隆过滤器检查。
- 如果布隆过滤器判断该参数不在集合中(即元素一定不存在),则可以直接返回缓存未命中,避免进行昂贵的数据库查询或其他操作。
- 如果布隆过滤器判断该参数可能存在(即元素可能存在),再进一步查询缓存或者数据库,以获取实际数据并存入缓存。
布隆过滤器中生成哈希值的对象不限于键的 ID,实际上可以是任何能转换为比特数组索引的对象或数据。
✨106、401、403状态码有什么区别
身份验证 vs. 权限:401 是关于身份验证问题,客户端未提供有效的认证信息。403 是关于权限问题,客户端已认证但无权访问资源。
响应头:401 通常会包含 WWW-Authenticate
头,指示客户端如何进行认证。403 通常不会包含此头。
处理方式:对于 401,客户端可以重试请求并提供认证信息。对于 403,客户端应该停止请求或联系管理员,因为它无权访问资源。
适用场景:
- 401 Unauthorized
- 登录页面。
- 需要身份验证的 API 端点。
- 访问受保护的资源时未提供凭据。
- 403 Forbidden
- 用户尝试访问不属于其角色或权限范围内的资源。
- IP 地址被服务器列入黑名单。
- 尝试访问管理员页面但用户角色不是管理员。
✨105、相对于Spring Data JPA,Mybatis有什么优点
灵活性和控制力:适用于需要对SQL进行精细控制和优化的场景。
复杂查询支持:适合复杂查询和业务逻辑处理,SQL语句清晰可见,便于调试和优化。
性能调优:手动优化查询,缓存机制提高性能,适用于高性能需求的应用。
✨106、Synchronized和ReentrantLock的区别
简单的同步场景可以使用Synchronized,复杂控制和高性能关键代码可以选择ReentrantLock。
- 实现机制:
Synchronized是JVM层面实现的, intrinsic lock,也就是监视器锁。
ReentrantLock是通过依赖AQS(AbstractQueuedSynchronizer)来实现锁的功能的。
- 使用方法:
Synchronized无需用户调用,只要其修饰的代码块或方法即表示被同步,采用的lock是当前实例对象或者Class类。
ReentrantLock需要用户手动调用
lock()
和unlock()
来进行锁的获取和释放,用户可以决定使用哪个实例进行同步。
- 等待可中断:
Synchronized不支持中断等待线程的功能,如果持有锁的线程长时间不释放锁,那么等待的线程将无法被中断。
ReentrantLock支持通过
lock.lockInterruptibly()
方法将等待锁的线程设置成可中断的,后续可以通过线程的interrupt()
唤醒等待线程。
- 公平性:
Synchronized所使用的锁是非公平的,没有顺序保证。
ReentrantLock可以在构造的时候选择是否公平,公平锁会按照线程等待的顺序来获取锁。
- 性能:
Synchronized具有较高的内存开销和影响执行速度,适用于快速上锁-解锁的简单同步场景。
ReentrantLock的性能较Synchronized略优,适用于更复杂的同步控制需求。
✨107、Synchronized修饰静态方法和实例方法有什么区别
静态方法使用的是类的类锁:当一个线程执执行某个类的静态同步方法时,只要此类的类锁还被持有,那么其他线程就无法访问此类的任何静态方法。
实例方法使用的是对象的锁:当一个线程执行某个对象的实例同步方法时,只要此对象锁被当前线程持有,那么其它线程就无法访问此对象的任何实例方法。
✨108、Spring Cloud GateWate基本配置
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
spring:
cloud:
gateway:
routes:
- id: example_route
uri: http://httpbin.org:80
predicates:
- Path=/get
filters:
- AddRequestHeader=Example, HeaderValue
id
是路由的唯一标识符。uri
是目标 URI。predicates
定义了路由的条件,这里使用了Path
谓词,表示当请求路径匹配/get
时,路由将生效。filters
定义了路由的过滤器,这里使用了AddRequestHeader
过滤器,表示在请求中添加一个名为Example
的请求头,其值为HeaderValue
。
✨109、服务降级和服务熔断的区别
服务降级是从应用层面的负载分担策略,服务熔断则更在底层的一个保护机制。
- 触发条件不同:
服务降级是当后端服务压力太大或者性能故障时,为了保证前端服务的响应效率而自动调整业务流程,比如返回部分数据而不是全部数据等。
服务熔断是当后端服务不可用的情况下(如超时或异常等),为了避免请求去拖慢或是引起系统崩溃,断路器会自动打开(类似保险丝断路),同时返回一个预定义的值。
- 作用不同:
服务降级重在降低后端服务压力,是一种防御措施。
服务熔断主要是快速失败,旨在尽快返回请求以避免长时间等待不可用的服务,同时保护入口服务。
- 实施方法不同:
服务降级常使用配置定义fallback方法或兜底业务处理逻辑。
服务熔断功能需要使用断路器组件实现,比如Hystrix,断路器根据服务可用性自动执行断路。
- 影响范围不同:
服务降级的影响范围针对单个后端服务。
服务熔断后,相应的RPC或HTTP请求都会直接失败短路,影响范围较大。
✨110、synchronized底层原理
synchronized
关键字在 Java 中用于实现同步,其底层原理涉及到 JVM 和操作系统的多种机制:
- 对象头(Object Header):存储对象的元数据,包括锁状态标志。
- Monitor(监视器):每个对象都有一个与之关联的 Monitor,用于实现线程的互斥和协作。
- 锁的状态:包括无锁状态、偏向锁、轻量级锁和重量级锁。
- 锁的升级和降级:锁可以在不同级别之间升级和降级,以优化性能。
- synchronized 的实现:通过
monitorenter
和monitorexit
字节码指令实现。 - 锁优化技术:包括偏向锁、轻量级锁、锁消除、锁粗化和自适应自旋。
描述 | |
---|---|
重量级锁 | 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。 |
轻量级锁 | 线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性。 |
偏向锁 | 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令。 |
一旦锁发生了竞争,都会升级为重量级锁。
✨111、==和equals的区别
==
运算符
- 用于比较基本数据类型的值是否相等。
- 用于比较引用类型的对象是否指向同一个内存地址。
equals
方法
- 用于比较引用类型的对象内容是否相等。
- 默认情况下,
Object
类的equals
方法比较的是内存地址,但许多类(如String
、Integer
等)重写了equals
方法,以比较对象的内容。
✨112、Redis中的setnx命令
SETNX
命令主要用于以下场景:
- 实现分布式锁:通过
SETNX
命令可以确保只有一个客户端能够成功设置某个键,从而实现分布式锁。 - 初始化值:在某些情况下,需要确保某个键只在第一次访问时被初始化。
# 尝试设置键 "mykey" 的值为 "myvalue"
SETNX mykey myvalue
# 返回 1,因为 "mykey" 不存在,设置成功
# 再次尝试设置键 "mykey" 的值为 "othervalue"
SETNX mykey othervalue
# 返回 0,因为 "mykey" 已经存在,设置失败
# 获取键 "mykey" 的值
GET mykey
# 返回 "myvalue"
Redis 2.6.12 版本之后,SET
命令支持 NX
和 PX
参数,可以在一个命令中实现 SETNX
和设置过期时间的功能。
SET key value NX PX milliseconds
# 尝试设置键 "mykey" 的值为 "myvalue",并设置过期时间为 10000 毫秒
SET mykey myvalue NX PX 10000
# 返回 OK,如果键不存在并且设置成功
# 返回 (nil),如果键已经存在,设置失败
✨113、什么是类加载器,类加载器有哪些?
类加载器:用于装载字节码文件(.class文件)
类加载器
JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。
现有的类加载器基本上都是java.lang.ClassLoader的子类,该类的只要职责就是用于将指定的类找到或生成对应的字节码文件,同时类加载器还会负责加载程序所需要的资源。
类加载器种类
类加载器根据各自加载范围的不同,划分为四种类加载器:
启动类加载器(BootStrap ClassLoader):
该类并不继承ClassLoader类,其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。
扩展类加载器(ExtClassLoader):
该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。
应用类加载器(AppClassLoader):
该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。
自定义类加载器:
这是用户自己定义的类加载器,继承自
java.lang.ClassLoader
。通过重写findClass()
方法,用户可以定制类加载的方式。
上述类加载器的层次结构如下:

类加载器的体系并不是“继承”体系,而是委派体系,类加载器首先会到自己的parent中查找类或者资源,如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次。
✨113、类装载的执行过程
类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。

114、线程池中如果使用默认的拒绝策略,在实际的项目当中,多余的任务怎么办
在Java的线程池中,默认的拒绝策略是 AbortPolicy
,这意味着当线程池和任务队列都满时,提交的额外任务将会被拒绝,并抛出 RejectedExecutionException
。
在实际项目中,如果使用默认的拒绝策略 AbortPolicy
,并且出现了任务被拒绝的情况,你可以考虑以下几种方法来处理多余的任务:
(1)增加线程池大小和队列容量
如果系统资源允许,可以增加线程池的核心线程数、最大线程数和队列容量,以减少任务被拒绝的可能性。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queueCapacity)
);
(2)使用其他拒绝策略
CallerRunsPolicy
该策略会让调用线程执行被拒绝的任务。这种方式可以减缓任务提交速度,防止任务丢失。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queueCapacity),
new ThreadPoolExecutor.CallerRunsPolicy()
);
DiscardPolicy
该策略会直接丢弃被拒绝的任务,不抛出异常。如果任务丢失可以被接受,这是一个简单的选择。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queueCapacity),
new ThreadPoolExecutor.DiscardPolicy()
);
DiscardOldestPolicy
该策略会丢弃队列中最旧的任务,并尝试重新提交被拒绝的任务。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queueCapacity),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
(3)自定义拒绝策略
可以实现 RejectedExecutionHandler
接口,定义自己的拒绝策略。例如,可以将被拒绝的任务记录到日志或保存到数据库中以便后续处理。
public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定义处理逻辑,例如记录日志或保存到数据库
System.out.println("Task " + r.toString() + " rejected from " + executor.toString());
}
}
// 使用自定义拒绝策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queueCapacity),
new CustomRejectedExecutionHandler()
);
(4)优化任务提交和处理方式
调整任务的粒度
如果任务过于细粒度,可以考虑将多个小任务合并为一个大任务,从而减少任务的数量。
限流
在提交任务之前,对任务进行限流控制,防止任务提交速度超过线程池处理能力。
import java.util.concurrent.Semaphore;
public class TaskLimiter {
private final Semaphore semaphore;
public TaskLimiter(int maxConcurrentTasks) {
semaphore = new Semaphore(maxConcurrentTasks);
}
public void executeTask(Runnable task, ThreadPoolExecutor executor) {
try {
semaphore.acquire();
executor.execute(() -> {
try {
task.run();
} finally {
semaphore.release();
}
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 使用限流器
TaskLimiter taskLimiter = new TaskLimiter(maxConcurrentTasks);
taskLimiter.executeTask(() -> {
// Task implementation
}, executor);
(5)使用消息队列
将任务提交到消息队列(如RabbitMQ、Kafka等)中,线程池从队列中消费任务。这种方式可以有效解耦任务生产和消费,平滑处理突发流量。
// 示例:使用RabbitMQ作为消息队列
import com.rabbitmq.client.*;
public class TaskProducer {
private final static String QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
String message = "Task message";
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
}
}
public class TaskConsumer {
private final static String QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
// Process the task
} finally {
System.out.println(" [x] Done");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
}
}
✨115、使用@Scheduled的定时任务在分布式系统中如何处理并发问题
在分布式系统中,使用 @Scheduled
注解实现定时任务时,可能会遇到并发问题。例如,多个实例可能会同时执行同一个定时任务。为了避免这种情况,需要使用一些机制来确保只有一个实例在某个时刻执行特定的任务。
以下是一些常见的解决方案:
(1)基于数据库的分布式锁
可以使用数据库来实现分布式锁,通过在数据库中创建一个锁表来控制任务的执行。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.jdbc.core.JdbcTemplate;
@Component
public class ScheduledTask {
@Autowired
private JdbcTemplate jdbcTemplate;
@Scheduled(fixedRate = 5000)
@Transactional
public void performTask() {
try {
if (acquireLock()) {
// 执行任务
System.out.println("Task executed by instance " + getInstanceId());
// 释放锁
releaseLock();
}
} catch (Exception e) {
e.printStackTrace();
}
}
private boolean acquireLock() {
String sql = "INSERT INTO scheduled_task_locks (task_name, locked) VALUES ('myTask', true) " +
"ON DUPLICATE KEY UPDATE locked = IF(locked = false, true, false)";
return jdbcTemplate.update(sql) > 0;
}
private void releaseLock() {
String sql = "UPDATE scheduled_task_locks SET locked = false WHERE task_name = 'myTask'";
jdbcTemplate.update(sql);
}
private String getInstanceId() {
return System.getenv("INSTANCE_ID");
}
}
(2)基于Redis的分布式锁
Redis是一个高性能的内存数据存储,可以用来实现分布式锁。可以使用 setnx
命令来获取锁,并在执行任务后释放锁。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class ScheduledTask {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_KEY = "scheduledTaskLock";
@Scheduled(fixedRate = 5000)
public void performTask() {
String lockValue = getInstanceId() + "-" + System.currentTimeMillis();
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, lockValue, 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(acquired)) {
try {
// 执行任务
System.out.println("Task executed by instance " + getInstanceId());
} finally {
// 释放锁
if (lockValue.equals(redisTemplate.opsForValue().get(LOCK_KEY))) {
redisTemplate.delete(LOCK_KEY);
}
}
}
}
private String getInstanceId() {
return System.getenv("INSTANCE_ID");
}
}
(3)基于ZooKeeper的分布式锁
ZooKeeper是一个分布式协调服务,可以用来实现分布式锁。可以使用Curator库来简化与ZooKeeper的交互。
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class ScheduledTask {
@Autowired
private CuratorFramework curatorFramework;
private final InterProcessMutex lock = new InterProcessMutex(curatorFramework, "/scheduled_task_lock");
@Scheduled(fixedRate = 5000)
public void performTask() {
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// 执行任务
System.out.println("Task executed by instance " + getInstanceId());
} finally {
lock.release();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private String getInstanceId() {
return System.getenv("INSTANCE_ID");
}
}
(4)基于Spring Cloud的分布式任务调度(如Spring Cloud Scheduler)
Spring Cloud Scheduler是一种高层次的解决方案,它可以更方便地在分布式环境中调度任务。
import org.springframework.cloud.scheduler.spi.core.TaskLauncher;
import org.springframework.cloud.scheduler.spi.core.LaunchState;
import org.springframework.cloud.scheduler.spi.core.TaskLaunchRequest;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class ScheduledTask {
private final TaskLauncher taskLauncher;
public ScheduledTask(TaskLauncher taskLauncher) {
this.taskLauncher = taskLauncher;
}
@Scheduled(fixedRate = 5000)
public void performTask() {
TaskLaunchRequest request = new TaskLaunchRequest(
"task-docker-image",
null,
null,
null,
"scheduledTask"
);
LaunchState state = taskLauncher.launch(request);
System.out.println("Task executed with state: " + state);
}
}
(5)使用Quartz Scheduler
Quartz是一个功能强大的任务调度库,可以在分布式环境中使用数据库来实现分布式锁和任务调度。
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class ScheduledTask implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 执行任务
System.out.println("Task executed by instance " + getInstanceId());
}
private String getInstanceId() {
return System.getenv("INSTANCE_ID");
}
}
✨116、MySQL中慢查询怎么查看,怎么处理
在 MySQL 中查看和处理慢查询可以通过以下步骤实现:
(1)查看慢查询
启用慢查询日志
首先,确保慢查询日志已启用。在 MySQL 配置文件(通常是
my.cnf
或my.ini
)中添加或修改以下设置:[mysqld] slow_query_log = 1 slow_query_log_file = /path/to/your/slow_query.log long_query_time = 2
以上配置将启用慢查询日志,并将查询时间超过 2 秒的查询记录到指定的文件中。修改完配置文件后,重启 MySQL 服务以使更改生效。
sudo service mysql restart
实时查看慢查询
通过以下 SQL 命令可以启用或关闭慢查询日志,而无需重启 MySQL 服务:
-- 启用慢查询日志 SET GLOBAL slow_query_log = 'ON'; -- 设定慢查询时间阈值为 2 秒 SET GLOBAL long_query_time = 2;
检查当前的慢查询日志设置:
SHOW VARIABLES LIKE 'slow_query_log%'; SHOW VARIABLES LIKE 'long_query_time';
查看慢查询日志
慢查询日志文件通常存储在指定的路径中,可以通过以下命令查看日志内容:
cat /path/to/your/slow_query.log
也可以通过 SQL 命令查看:
SHOW GLOBAL STATUS LIKE 'Slow_queries';
(2)处理慢查询
分析慢查询
使用
mysqldumpslow
工具分析慢查询日志:mysqldumpslow -s c -t 10 /path/to/your/slow_query.log
以上命令将列出执行次数最多的前 10 条慢查询。
优化查询
- 使用索引:确保慢查询中使用的列已建立适当的索引。
- 查询重写:检查是否可以重写查询以提高效率。
- 表结构优化:根据查询模式调整表结构。
- 分区表:对大表进行分区以提高查询速度。
使用
EXPLAIN
命令使用
EXPLAIN
命令分析慢查询的执行计划:EXPLAIN SELECT * FROM your_table WHERE your_conditions;
通过分析输出结果,可以了解查询的执行流程,发现性能瓶颈。
调整服务器配置
- 调整 MySQL 的缓冲区和缓存设置,如
query_cache_size
、innodb_buffer_pool_size
等。 - 增加硬件资源,如 CPU、内存和磁盘 I/O 能力。
- 调整 MySQL 的缓冲区和缓存设置,如
✨117、如何分析一条SQL

EXPLAIN
的输出会包含以下主要字段:
- id: 查询的顺序标识符。
- select_type: 查询的类型(如 SIMPLE, PRIMARY, UNION 等)。
- table: 当前操作的表。
- type: 访问类型(如 ALL, index, range, ref, eq_ref, const, system, NULL)。
- possible_keys: 查询中可能使用的索引。
- key: 实际使用的索引。
- key_len: 使用的索引的长度。
- ref: 哪个列或常量与索引一起使用。
- rows: MySQL 估计需要读取的行数。
- Extra: 额外的信息(如 Using filesort, Using temporary)。
根据 EXPLAIN
的输出,可以分析以下几个方面:
访问类型 (
type
)优化目标是尽量避免全表扫描 (
ALL
),应优先使用以下访问类型:const
: 常量表访问,单行匹配。eq_ref
: 使用唯一索引,单行匹配。ref
: 非唯一索引,匹配多行。range
: 范围扫描。index
: 全索引扫描。ALL
: 全表扫描,性能最差。- NULL:查询不会返回任何结果或不需要访问表
使用的索引 (
key
)确认查询使用了合适的索引。如果
possible_keys
包含多个索引,但key
为空或未选择最佳索引,需要检查索引是否适用或是否需要调整。扫描的行数 (
rows
)rows
字段表示估计扫描的行数,行数越少,查询性能越高。额外信息 (
Extra
)检查
Extra
字段是否包含Using filesort
,Using temporary
等,这些操作通常会导致性能问题。image-20240717151913344
✨118、MySQL中char(10)和varchar(10)的区别,其中的10是字符还是字节,一个中文是多少字节
char(10):
char(10)
表示存储固定长度为10个字符的字符串。- 如果插入的字符串长度小于10个字符,MySQL会用空格填充到指定长度。
- 如果插入的字符串长度大于10个字符,MySQL会截断超过部分,只存储前10个字符。没有错误或警告,只是简单地截断。
- 存储空间的大小为指定长度乘以每个字符的字节长度(在大多数情况下为1字节)。
varchar(10):
varchar(10)
表示可以存储最大长度为10个字符的可变长度字符串。- 如果插入的字符串长度小于或等于10个字符,MySQL会正常存储整个字符串。
- 如果插入的字符串长度超过10个字符,MySQL会截断超过部分,只存储前10个字符。同样地,没有错误或警告,只是截断超出的部分。
- 实际存储时,它只占用实际存储的字符长度加上一个额外的字节作为长度前缀。存储空间的大小为实际存储的字符数加上一个字节。
一个普通的中文汉字在 UTF-8 编码下通常占用 3 个字节,在 GBK 编码下通常占用 2 个字节。
在SQL中,
char(10)
和varchar(10)
是用来定义字段类型和长度的两种方式,它们有以下区别:
存储方式:
char(10)
:固定长度的字符类型。无论实际存储的数据长度是多少,都会占用固定的存储空间。如果存储的数据少于指定长度,后面会用空格来填充。varchar(10)
:可变长度的字符类型。它只会占用实际存储的数据长度加上一些额外的字节来记录长度信息。存储空间:
char(10)
:无论实际存储的数据长度如何,都会占用10个字符的存储空间。例如,如果存储的数据是"abc",那么实际占用的空间也是10个字符,剩余的7个字符会用空格填充。varchar(10)
:会根据实际存储的数据长度来动态分配存储空间。例如,存储"abc"时,只会占用3个字符的存储空间,不会有额外的填充。性能和存取:
- 由于
char
是固定长度的,所以在查询时可能会比varchar
更快,因为数据库知道每个字段的确切位置。但是,它可能会浪费空间,特别是对于存储大量短文本的场景。varchar
适合存储长度不固定的文本数据,可以节省存储空间。但是,由于需要存储额外的长度信息,可能会在某些情况下增加一些存取开销。适用场景:
- 使用
char
通常适合存储长度固定的数据,如邮政编码、国家代码等。- 使用
varchar
适合存储长度不固定的数据,如用户名、评论内容等。
✨119、如果有一个sql查询频繁使用需不需要建立索引?什么情况下不需要建立索引?
在SQL中,频繁使用的查询是否需要建立索引取决于查询的性能需求以及数据表的大小和结构。通常情况下,频繁使用的查询可以通过索引来提高查询性能,但并非所有情况下都需要建立索引。以下是一些情况下不需要建立索引的情况:
- 小表:如果数据表非常小(比如少于几百行),查询通常很快,即使没有索引也不会有明显的性能问题。
- 低基数列:如果数据列的基数(不同值的数量)很低,即使建立索引也不会显著提高查询性能。例如,一个性别列通常只有两种值,为其建立索引可能不会带来显著的性能提升。
- 查询结果覆盖索引:如果查询可以通过覆盖索引(Covering Index)来获取所有需要的数据,而不需要访问实际的数据行,则可能不需要额外的索引。
- 频繁更新的表:如果数据表经常发生插入、更新和删除操作,索引维护可能会带来额外的开销,并且不必要的索引可能会降低更新操作的性能。
- 全表扫描更快:在某些情况下,全表扫描(Table Scan)比使用索引更快,特别是当大部分数据需要被检索时,使用索引可能导致不必要的索引查找和数据访问。
✨120、Spring中,A方法有事务,B方法没事务,B调A或者A调B,事务会失效吗
@Service
public class MyService {
@Transactional
public void methodA() {
// 方法A的事务
// 调用methodB
methodB();
}
public void methodB() {
// 方法B没有事务
}
}
事务是否会失效,取决于具体的事务传播行为以及调用顺序:
- B调用A:
- 如果A方法的事务传播行为是
REQUIRED
,事务不会失效,A方法会运行在一个事务中。 - 如果A方法的事务传播行为是
REQUIRES_NEW
,A方法会创建一个新的事务,B方法的上下文与A方法的事务隔离。
- 如果A方法的事务传播行为是
- A调用B:
- B方法会在A方法的事务上下文中执行。如果A方法提交,B方法的操作也会提交;如果A方法回滚,B方法的操作也会回滚。
只要正确配置事务传播行为,事务是不会失效的。但如果事务传播行为配置不当,可能会导致事务的意外行为,比如挂起和新建事务等。
✨121、Nacos注册原理
服务启动: 微服务在启动时会向 Nacos 注册中心发送注册请求。这个请求通常包含服务名称、实例 ID、IP 地址、端口号、元数据(如服务版本、环境等)以及心跳间隔等信息。
注册请求: 微服务通过 Nacos 客户端 SDK(如 Spring Cloud Alibaba Nacos Discovery)向 Nacos 注册中心发送 HTTP 或者 gRPC 请求,将自身的信息注册到 Nacos。
服务实例存储: Nacos 注册中心接收到注册请求后,将服务实例信息存储在内存中(Nacos 使用一个内存数据结构来存储这些信息)。
心跳机制: 为了保持服务实例的活跃状态,微服务会定期向 Nacos 注册中心发送心跳请求。默认情况下,心跳请求的间隔为5秒。如果 Nacos 在一定时间(默认是15秒)内没有接收到某个实例的心跳,就会认为该实例已经不可用,并将其从注册表中移除。
网络分区: 如果某个服务实例与 Nacos 注册中心之间的网络出现问题,心跳请求无法发送成功,Nacos 会在超时时间内将该实例标记为不可用。网络恢复后,服务实例会重新注册并继续发送心跳。
✨122、MQ如何避免消息丢失
RabbitMQ整个消息投递的路径为:producer —> exchange —> queue —> consumer
- 确认模式:消息从 producer 到 exchange 则会返回一个 confirmCallback;
- 退回模式:消息从 exchange –> queue 投递失败则会返回一个 returnCallback;可以利用这两个 callback 控制消息的可靠性投递。
持久化消息:
- 确保消息在发送到队列之前被持久化到存储介质,比如硬盘。这样即使消息队列系统崩溃或重启,消息也不会丢失。
确认机制:
- 使用消息确认机制(Acknowledgement),确保消息在被消费者处理后才从队列中删除。这样可以防止在消息被处理前就丢失。
消息重试:
- 实现消息重试机制,当消费者处理消息失败时,消息可以重新发送到队列等待重新处理。通常结合指数退避(exponential backoff)策略,避免频繁地重试失败的消息。
高可用性和备份:
- 部署具有高可用性的消息队列系统,确保在主节点故障时能够切换到备用节点,以防止消息丢失。
事务支持:
- 如果消息队列支持事务,确保生产者和消费者能够在事务中发送和接收消息,从而保证消息的完整性和可靠性。
监控和报警:
- 实施监控系统来检测消息队列系统的健康状态和性能。及时发现问题并通过报警系统通知运维人员处理,可以有效减少消息丢失的风险。
消息序列化和版本控制:
- 确保消息的序列化和反序列化过程是可靠的,避免由于版本兼容性或数据格式错误导致消息丢失。
异常处理和日志记录:
- 在消息处理过程中实现良好的异常处理机制,并记录日志以追踪处理失败或丢失的消息,便于后续分析和排查。
✨123、分布式事务怎么解决
分布式事务是指涉及多个独立数据源或服务的事务操作,这些操作需要保证在所有参与方之间的一致性。
(1)两阶段提交(Two-Phase Commit, 2PC)
两阶段提交协议是经典的分布式事务处理方法。它分为两个阶段:准备阶段和提交阶段。
- 准备阶段:协调者向所有参与者发送准备请求,并等待参与者返回准备好的响应。
- 提交阶段:如果所有参与者都返回准备好的响应,协调者发送提交请求;如果有任何一个参与者返回失败,协调者发送回滚请求。
优点:
- 保证了强一致性。
缺点:
- 网络故障或协调者失败时,可能导致系统不可用。
- 性能开销较大。
Java实现:使用Java Transaction API (JTA) 和分布式事务管理器(如Atomikos、Bitronix)。
(2)TCC (Try-Confirm-Cancel)
TCC是一种灵活的分布式事务模式,分为三个步骤:Try、Confirm和Cancel。
- Try:资源预留阶段。
- Confirm:执行确认操作。
- Cancel:执行取消操作。
优点:
- 更灵活,适用于长事务。
- 可以实现更好的性能。
缺点:
- 需要应用程序开发者手动实现这三个阶段的逻辑。
Java实现:实现TCC的框架如TCC-Transaction,可以帮助开发者实现TCC模式。
(3)可靠消息最终一致性(Reliable Message Final Consistency)
通过消息中间件来保证消息传递的可靠性,实现最终一致性。
- 事务消息:在本地事务成功后,发送半消息(不可见消息),然后提交本地事务,最后确认消息。
- 消息确认:消费者在处理完消息后,确认消息已成功处理。
优点:
- 性能较好,适用于对实时性要求不高的场景。
- 更加解耦,易于扩展。
缺点:
- 需要引入消息中间件,增加系统复杂度。
- 最终一致性模型可能导致短暂的不一致。
Java实现:使用消息中间件如Apache Kafka、RabbitMQ、RocketMQ,并结合Spring事务管理。
✨123、Seata如何解决分布式事务?
Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
- AT 模式:适合简单的事务场景,开发成本低,性能较好。
- TCC 模式:适合复杂的业务逻辑,需要灵活的事务控制。
- Saga 模式:适合长时间运行的事务,强调最终一致性。
- XA 模式:适合需要强一致性的关键业务场景,性能相对较低。
Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成(微服务)。
一个典型的分布式事务过程,可以用分布式处理过程的一ID+三组件模型来描述。
- 一ID(全局唯一的事务ID):Transaction ID XID,在这个事务ID下的所有事务会被统一控制
- 三组件:
- Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;(Server端,为单独服务器部署)
- Transaction Manager (TM):事务管理器,控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
- Resource Manager (RM):资源管理器,控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚
✨124、jekins如何自动化部署项目(自动化流水线部署流程)
安装和配置 Jenkins 安装必要插件 配置凭据 创建 Jenkins Pipeline 编写 Pipeline 脚本 配置自动触发 运行和监控流水线
(1)安装必要的插件
安装 Git、Pipeline、SSH 和其他你需要的插件。
(2) 创建 Jenkins Pipeline脚本
- 进入 Jenkins 首页
- 点击
New Item
- 输入任务名称,选择
Pipeline
类型,点击OK
- 在任务配置页面中,找到
Pipeline
部分,选择Pipeline script
选项。 - 编写你的 Pipeline 脚本,实现从代码拉取、构建、测试到部署的自动化过程。
(3)触发构建
- 手动触发:可以手动点击
Build Now
来触发构建和部署流程。 - 自动触发:可以配置 Jenkins 自动触发构建,比如在代码仓库有变更时触发。
✨125、介绍JUC
JUC(Java Util Concurrent)是Java并发包,提供了很多并发编程的基础类和工具类。主要特性和作用如下:
原子类(atomic):比如AtomicInteger、AtomicLong等,支持无锁操作的并发类,替代传统锁的方式提供线程安全性。
锁(locks):比如ReentrantLock和ReentrantReadWriteLock,提供锁实现功能,比synchronized更灵活。
同步器(synchronizers):Condition、CountDownLatch、CyclicBarrier等用来协调多个线程执行。
线程池(executors):工具类ThreadPoolExecutor可管理和重用线程,避免创建和销毁线程开销。
同步队列(BlockingQueue):接口阻塞式队列如LinkedBlockingQueue,多线程之间以队列方式安全进行资源交互。
并发集合(concurrent collections):比如ConcurrentHashMap,作为线程安全的高性能集合替代传统同步集合。
同步工具(utilities):如FutureTask表示一个任务的结果,PhaseredAction协调线程组时钟等。
无锁数据结构(non-blocking):比如跳表和定长队列,支持锁 free 实现。
✨126、什么是聚集索引(聚簇索引)和非聚集索引(非聚簇索引、二级索引),二者优缺点和区别
回表查询需要使用到聚集索引和非聚集索引。
聚集索引:主要是指数据与索引放到一块,B+树的叶子节点保存了整行数据,必须有且只有一个,一般情况下主键在作为聚集索引的。
- 优点:
- 性能最佳,数据和索引物理上连续存储,避免两次寻址,查询效率高
- 插入删除效率也较高
- 缺点:
- 只能为一列设置聚集索引
- 当数据量大时,索引越来越占存储空间
非聚集索引: 指的是数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个,一般我们自己定义的索引都是非聚集索引。
- 优点:
- 可以对表的多个列建立索引
- 索引量小,对存储空间影响小
- 缺点:
- 查询效率差,需要两次寻址,一次到索引查找行指针,再到数据行查找实际数据
- 插入删除效率较低
区别:
- 聚集索引的数据行和索引行在物理上是并列存储的,非聚集索引索引行和数据行是分离存储的
- 一张表只能设一个聚集索引,但可以设多个非聚集索引
- 常规查询性能:聚集索引 > 非聚集索引
- 插入删除性能:聚集索引 > 非聚集索引
- 空间占用:聚集索引 >> 非聚集索引
✨127、为什么要有最左前缀法则?
最左前缀法则的含义是:在WHERE子句中使用索引时,从索引的最左边列开始指定那么索引是有效的,如果不从索引的最左边列指定,那这个索引就不起作用了。
为什么要有这样的原则?主要原因如下:
效率考虑。如果不从最左边列开始指定,数据库不知道从哪个键值起查找,必须扫描整个索引树,效率很低。
索引组织数据的方式决定的。索引内部是通过B树这种数据结构组织的,每个节点保存右子节点的指针。只有从最左边列开始指定,才能得出明确的键值范围,进行有效的范围查找。
和索引物理存储对应。索引的各个组成列物理上存储的是按顺序存储的,首列排在最前面。
减小优化查询成本。如果可以通过任意列使用索引会给查询优化带来很大难度。限定从最左边列开始使用可以降低优化难度。
所以,最左前缀法则能保证使用索引的效率,也更合理地対应了索引内部的组织结构,这是数据库设计需要遵守的一个重要原则。忽略这个原则很可能导致索引无法正常使用。
✨128、https如何建立连接的?
HTTPS建立连接的过程如下:
客户端向服务器发送一个连接请求,请求中包含客户端支持的最新TLS版本号和加密套件列表等信息。
服务器根据客户端支持能力,选择一个合适的TLS版本和加密套件,向客户端回应。
客户端和服务器进行TLS握手阶段,利用RSA或ECDHE算法进行公钥加密交换。客户端生成对话密钥,使用服务器的公钥加密后发送给服务器。
服务器使用对应的私钥解密得到对话密钥,客户端和服务器建立起对话密钥。
客户端和服务器使用此对话密钥进行信息加密传输。对话密钥会定期更换。
通过数字证书核对客户端和服务器的身份。证书中包含主体信息、签名密钥等。
使用数字签名算法进行身份验证。客户端使用服务器公钥验证服务器数字证书的合法性。
如果证书验证通过,那么此连接就是一个安全信道,可以使用此信道进行后续HTTPS应用层通信。
HTTPS建立加密通信信道后,应用层将会进行HTTP请求和响应。数据采用对话密钥进行对称加密传输。
所以HTTPS采用TLS进行握手阶段的公钥加密和数字证书验证,建立加密通信信道,以提供HTTPS请求的数据安全传输。
✨129、linux中查询一个端口的占用情况使用哪个命令
- 查询所有TCP端口情况:
netstat -tnp
-t:只显示tcp相关信息 -n:直接显示端口号,而不是服务名 -p:显示每个端口对应的进程号(PID)
- 根据端口号查询占用情况:
netstat -tnp | grep <port>
比如查询端口8080:
netstat -tnp | grep 8080
- 根据PID查询占用进程:
netstat -tnp | grep <pid>
还有一些其他命令也可以查询端口占用:
lsof -i:<port>
: 查询指定端口占用ss -tunlp
: socket统计信息pidof <process name>
: 根据进程名查询PID
✨130、SpringMVC中过滤器和拦截器的区
(1)工作层级
过滤器:
- 过滤器是Servlet规范的一部分,属于Servlet容器层级。它们在请求到达Servlet之前和响应从Servlet返回之后进行处理。
- 过滤器可以应用于任何Web应用程序,无需依赖于Spring框架。
拦截器:
- 拦截器是Spring MVC框架的一部分,属于Spring容器层级。它们在处理请求的Controller之前和之后进行处理。
- 拦截器只能用于Spring MVC应用程序,不能独立于Spring框架使用。
(2)配置方式
过滤器:
- 过滤器在
web.xml
中配置,或者通过Spring Boot的方式使用@WebFilter
注解进行配置。 - 过滤器的配置通常包括URL模式和过滤器类。
- 过滤器在
拦截器:
- 拦截器在Spring的配置文件(如
applicationContext.xml
)中配置,或者通过Java配置类使用WebMvcConfigurer
接口进行配置。 - 拦截器的配置包括拦截器类和需要拦截的请求路径模式。
- 拦截器在Spring的配置文件(如
(3)作用范围
- 过滤器可以对所有进入Web应用程序的请求进行处理,包括静态资源、JSP、Servlet等。
- 拦截器主要对Spring MVC处理的请求进行拦截,通常只对Controller层的请求起作用,不会处理静态资源。
(4)使用场景
过滤器:
- 常用于实现通用的过滤逻辑,如编码设置、日志记录、跨域处理等。
- 适合在整个应用程序范围内进行一些通用的请求和响应处理。
拦截器:
- 常用于特定的业务逻辑处理,如用户认证、权限控制、日志记录等。
- 适合在Spring MVC框架内对Controller层进行细粒度的请求拦截和处理。
✨140、sleep和wait的区别
sleep()
常用于模拟延时、定时任务等场景,而wait()
用于线程间的协调和通信。sleep()
是Thread
类的静态方法,wait()
是Object
类的实例方法。sleep()
不释放锁,wait()
释放锁。sleep()
在指定时间后自动恢复,wait()
需要被notify()
或notifyAll()
唤醒。
✨141、接口和抽象类的区别
接口:
- 用
interface
关键字定义、使用implements
关键字实现接口 - 只能包含抽象方法(Java 8及以后可以包含默认方法
default methods
和静态方法static methods
);所有方法默认是public
和abstract
。 - 只能包含常量(默认是
public
,static
,final
);不允许有实例变量。 - 不能有构造器。
- 支持多重继承,一个类可以实现多个接口。
- 适用于定义功能契约,强调可以做什么,适合定义不同类之间的共同行为。
抽象类:
- 用
abstract class
关键字定义,类使用extends
关键字继承抽象类; - 可以包含抽象方法和非抽象方法;可以有任何访问修饰符。
- 可以包含实例变量和常量。
- 不支持多重继承,一个类只能继承一个抽象类。
- 适用于代码复用和提供模板方法,强调是什么,适合共享代码和状态。