面试虐我千百遍 我待面试如初恋(面经分享)

懒得区分公司了!好几问的深挖 一般死在最后一问!还有一些弱智的自己没回答好也写上了   

1.springboot的starter了解吗?

在Spring Boot应用中,starter是一组预配置的依赖项,您可以通过一个依赖声明将其包含在项目中。Spring Boot starter的目的是通过提供一组常用的依赖项和配置来简化新项目的设置过程;

2.MySQL的B+树和哈希树有什么区别

  1. 数据分布:哈希树将数据按照哈希函数的结果进行分布,而B+树则将数据按照顺序存储在叶子节点上。

  2. 查询效率:哈希树在具有相等查询时具有O(1)的查询效率,但在范围查询时效率往往较差。而B+树虽然在查找单个记录时的效率略低于哈希树,但在区间查询时显得更加友好,可以更快地获取到需要的数据。

  3. 索引维护:哈希树需要进行哈希函数的计算,并且在数据增删时需要重新计算哈希值,因此对于频繁的数据更新操作,哈希树的维护成本比较高。而B+树在插入和删除操作时只需要对树进行局部调整,维护成本较低。

  4. 存储空间:哈希树通常需要进行扩容操作来保证哈希冲突的数量不会过多,因此可能会浪费一部分存储空间。而B+树则可以灵活地进行节点分裂和合并,更加高效地利用存储空间;

3.md5算法加密是对称还是非对称的?

         简介:MD5算法加密是一种单向哈希函数,属于非对称加密算法。它可以将任意长度的消息转换为一个固定长度(128位)的输出,该输出通常被称为摘要或指纹。MD5算法具有不可逆性,即无法从摘要反推出原始数据

        场景:在文件传输过程中,可以通过计算文件的MD5值来检查文件是否在传输过程中被篡改。此外,MD5算法也可以用于存储密码等敏感信息的加密

4.挥手中服务端等待客户端响应超时怎么解决?

        介绍:在TCP连接中,当服务端发送FIN报文后进入LAST_ACK状态时,需要等待客户端发送ACK报文确认断开连接。如果客户端没有及时响应,服务端就会一直等待,导致连接资源无法释放,从而影响服务器的性能和可靠

1.调整TCP超时时间服务端可以通过调整TCP协议栈的参数来控制客户端响应超时时间。例如,可以将TCP keepalive机制的间隔时间调短,以便更快地检测到连接状态异常。

2.设置TCP连接最大空闲时间服务端可以设置TCP连接的最大空闲时间,如果连接在这个时间内没有活动,就自动关闭连接。这样可以避免因客户端未响应而导致服务端一直等待的情况。

3.实现心跳机制服务端可以实现一个心跳机制,定期向客户端发送心跳消息,以检测连接是否正常。如果客户端长时间没有响应,服务端可以主动关闭连接。

4.强制关闭连接如果客户端长时间未响应,服务端也可以选择强制关闭连接,释放连接资源。虽然这种方法会丢失未传输完成的数据,但是可以保证连接资源得到释放,从而避免影响服务器性能和可靠性

5.数据库的索引和多表查询

多表查询一般包括以下几个步骤:

  1. 确定需要查询的表和字段,以及它们之间的关系。
  2. 根据关系使用 JOIN 连接不同的表。
  3. 使用 WHERE 子句筛选符合条件的数据。
  4. 可以对查询结果进行排序、分组等操作。

下面是一个示例代码展示:假设我们有两张表,一张是用户信息表 user,另一张是订单信息表 order。它们之间通过用户 ID 建立关联。

 

这条 SQL 查询首先将用户信息表 user 和订单信息表 order 使用 INNER JOIN 进行连接,并根据用户 ID 来建立关联。然后使用 WHERE 子句筛选出用户姓名为 'Bob' 的记录。最后返回查询结果集中的所有字段

6.Mytabis是怎么防止Sql注入的

1.Mybatis通过预编译的方式来防止SQL注入,它会将用户输入的参数转义后再拼接到SQL语句中,而不是直接将参数拼接在SQL语句中。这样可以避免恶意用户通过输入特殊字符来修改原始SQL语句。

2.同时,Mybatis也支持使用动态SQL语句,这使得在构建SQL语句时可以根据不同的情况来动态拼接SQL语句,从而更加灵活和安全。例如,可以使用if语句来判断某个参数是否为空,如果为空则不拼接该参数对应的SQL语句,以避免不必要的错误。

3.总的来说,Mybatis提供了多种方法来防止SQL注入攻击,包括预编译、转义参数、动态SQL等,开发人员只需要按照规范使用Mybatis即可有效地防止SQL注入攻击

7.B+树的叶子节点都是有序的吗

是的,B+树的叶子节点通常都是有序的

1.在B+树中,所有的关键字都存储在叶子节点上,而非叶子节点只存储索引信息。由于每个非叶子节点存储的是一组关键字的范围,因此它们本身并不需要保持有序。

2.但是,为了支持范围查询,B+树的叶子节点必须按照关键字的大小顺序进行排序。这样,当需要查找某个范围内的数据时,可以通过遍历叶子节点来快速定位所需数据。

3.因此,B+树的叶子节点通常都是有序的。对于插入删除操作,虽然会导致叶子节点的顺序变化,但B+树会通过一些调整操作(如节点分裂、合并等)来保持叶子节点的有序性

8.Mysql如果一个查询比较慢的话怎么优化

1.索引优化:索引是提高查询效率的重要手段。如果查询语句中的表没有合适的索引,就会导致MySQL进行全表扫描,从而影响查询性能。因此,可以通过创建合适的索引来提高查询效率。

2.SQL语句优化:SQL语句的编写方式也会影响查询速度。一些常见的优化方法包括避免在WHERE子句中使用函数、避免SELECT *、避免使用OR等。

3.分析和优化查询计划:MySQL会根据SQL语句生成执行计划,并决定如何执行查询操作。因此,可以通过分析查询计划来找出可能导致查询缓慢的地方,并对其进行优化。

4.优化数据库结构数据库的表结构设计也会直接影响查询效率。例如,在大数据量的情况下,可以将数据表分成多个表或者使用分区表来减少查询时间。

5.MySQL服务器参数优化:MySQL服务器本身也有很多配置参数,可以根据实际需求进行调整以提高查询性能。例如,可以增加缓存大小、调整并发连接数、调整临时表空间等

9.SpringBoot项目在启动的时候是怎么获取Bean

  1. 在启动过程中,Spring Boot会扫描所有被@Component相关注解(如@Service、@Controller等)标注的类,并生成对应的Bean定义。

  2. Spring Boot会将所有生成的Bean定义注册到容器中。

  3. 在注册完所有Bean定义后,Spring Boot会进行依赖注入(DI),即将Bean之间的依赖关系进行绑定。这个过程是根据各个Bean之间的依赖关系进行的。

  4. 当需要获取某个Bean时,Spring Boot会从容器中查找该Bean实例,并将其返回给调用方。具体查找方式可以是按照Bean名称查找、按照类型查找等。

10.Spring Bean管理,原理知道吗?

  1. 首先,在启动时,Spring容器会根据配置文件或注解扫描等方式读取所有Bean的定义。

  2. 接着,Spring容器会创建所有Bean的实例,并将其存储在内部数据结构中。

  3. Spring容器会根据Bean之间的依赖关系,自动将各个Bean进行注入,形成完整的Bean依赖关系。

  4. 当应用程序需要一个Bean时,Spring容器会从之前创建好的Bean实例中查找并返回对应的实例。

  5. 当应用程序关闭时,Spring容器会自动销毁所有已创建的Bean实例,释放资源。

11.类加载完实在哪里的

在Java中,类加载完成后会被存储在方法区(Method Area)中。方法区是一块用于存储类信息、常量、静态变量等数据的内存区域,属于堆内存的一部分。

当Java程序执行时,虚拟机会将需要使用的类加载到方法区中,并进行链接和初始化操作。其中,链接包括验证、准备和解析三个步骤,初始化则是执行类构造器<clinit>()方法的过程。

在方法区中,每个类都有一个唯一的Class对象与之对应,该对象存储了类的属性、方法等信息。同时,方法区还包括运行时常量池、各种类型的符号引用以及类加载器等信息。

12.Jvm的类加载过程

    1.加载:

在加载阶段,虚拟机会通过类加载器(Class Loader)查找并读入类的二进制数据,并存储在方法区中。类加载器按照特定的顺序从不同的地方加载类,例如本地文件系统、网络上的远程服务器等。一般情况下,类加载器可以分为以下几种:

  • 引导类加载器(Bootstrap Class Loader):用于加载Java核心库,是虚拟机自带的类加载器。
  • 扩展类加载器(Extension Class Loader):用于加载Java扩展库。
  • 应用程序类加载器(Application Class Loader):用于加载应用程序类路径上的类。
  • 自定义类加载器:开发人员可以根据需要实现自己的类加载器。

    2.链接:

在链接阶段,虚拟机会对类的二进制数据进行校验、准备和解析等处理,确保类可以被正确加载和执行。

  • 校验:检查类的二进制数据是否符合JVM规范,并保证安全性和完整性。
  • 准备:为类的静态变量分配内存,并设置默认值。
  • 解析:将类中的符号引用替换为直接引用,以便后续的访问操作能够正常执行。

   3.初始化 :  

在初始化阶段,虚拟机会执行类的<clinit>()方法,该方法包含了类的静态变量赋值和静态代码块的执行等内容。如果一个类没有被初始化,则其静态变量不会被分配内存或设置默认值,并且静态代码块也不会被执行。

总之,JVM的类加载过程是将类的二进制数据读入并存储在方法区中,然后对其进行校验、准备和解析等处理,最后执行<clinit>()方法进行初始化。

13.索引什么时候会失效

  1. 数据分布不均:如果某个表中的数据分布不均匀,部分值的出现频率很高,而另一部分的出现频率很低,则使用索引可能会反而降低查询效率。

  2. 索引列上进行函数操作:当对索引列进行函数操作时,如使用了函数、类型转换等,会导致索引失效。因为索引的建立是基于原始列值的,函数操作会改变列值,使得索引无法匹配原始值。

  3. 非前缀索引的模糊查询:如果使用LIKE语句进行模糊查询,同时没有使用前缀匹配,索引也会失效。因为这种情况下需要扫描整个索引树,而非只扫描部分。

  4. 多表关联的查询:如果进行多表关联的查询,且关联条件没有建立索引,则查询速度会变慢。

  5. 数据库中数据量过大:如果数据库中某张表中的数据量特别大,超过了索引可以处理的数据量,那么索引的查询效率就会大大降低

14.为什么使用Md5加密(我只用过Md5加密...)

  1. 压缩性:任意长度的输入数据经过哈希算法计算后,得到的输出结果都是固定长度的128位二进制数。

  2. 碰撞概率低:MD5算法的输出结果非常难以通过逆运算还原出原始数据,因此MD5算法被广泛应用于数据校验和数字签名等场景。

  3. 快速性:MD5算法的计算速度非常快,适用于处理大量数据。

15.讲一讲设计模式的七大原则(从tm没注意过这玩意)

  1. 单一职责原则(SRP):一个类只负责完成一个职责或者功能,避免出现功能耦合和复杂度增加的问题。

  2. 开放封闭原则(OCP):软件实体应该对扩展开放,对修改关闭。即在不修改原有代码的情况下,通过扩展已有的代码来实现新的功能。

  3. 里氏替换原则(LSP):子类必须能够替换掉父类并且表现出预期的行为,即子类不能破坏父类的功能性。

  4. 依赖倒置原则(DIP):高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖具体实现,具体实现应该依赖抽象。

  5. 接口隔离原则(ISP):客户端不应该被强迫依赖那些它不需要的接口,即接口应该小而专,避免出现“胖接口”的情况。

  6. 合成复用原则(CRP):尽量使用对象组合、聚合而不是继承关系达到代码复用的目的。因为继承关系耦合度高、灵活性差。

  7. 迪米特法则(LoD):也称为最少知识原则,一个对象应该对其他对象有尽可能少的了解,即只与直接的朋友通信,在设计过程中要保持低耦合性和高内聚性。

16.你能说说僵尸进程吗?(回答了等于没回答,了解的确实不多啊)

1.当一个进程终止时,内核会为其保留一定时间的资源,以便父进程可以获取它的退出状态。这段时间称为进程的“僵尸期”。如果父进程没有及时地获取子进程的退出状态,则子进程就会成为一个“僵尸进程”。

2.僵尸进程在系统中占用着有限的资源(如进程号、程序计数器等),如果存在大量的僵尸进程,就会导致系统资源的浪费和性能下降。

3.避免僵尸进程的方法是让父进程及时处理子进程的退出状态。可以使用wait()或waitpid()等函数来等待子进程的退出并获取其退出状态。在父进程中注册SIGCHLD信号的处理程序,当子进程退出时,该处理程序会被调用,父进程就可以在信号处理程序中获取子进程的退出状态。

4.另外,还可以通过设置进程的信号处理方式为SIG_IGN来忽略子进程的SIGCHLD信号,从而让内核自动回收子进程的资源,避免产生僵尸进程。

17.进程间如何通信,可以具体讲讲它们的区别吗(进程间的通信方式不必须烂熟于心?)

  1. 管道(Pipe):管道是一种单向通信机制,它可以在父子进程之间进行通信。它可以通过调用系统函数pipe()来创建一个半双工的管道,在父子进程中分别使用管道的写端和读端进行通信。

  2. 命名管道(Named Pipe):命名管道也是一种单向通信机制,它可以在不同进程之间进行通信。与管道不同的是,命名管道具有独立的文件名和文件描述符,可以通过mkfifo()系统调用来创建。

  3. 共享内存(Shared Memory):共享内存是一种高效的通信机制,它可以让多个进程直接访问同一块物理内存空间,避免了数据拷贝的开销。共享内存的实现需要借助于操作系统提供的API,如shmget()、shmat()等。

  4. 信号量(Semaphore):信号量是一种用于进程同步和互斥的机制,它可以保证多个进程对共享资源的访问顺序和数量。信号量的实现需要借助于操作系统提供的API,如semget()、semop()等。

  5. 消息队列(Message Queue):消息队列是一种通过消息传递的通信机制,它可以在不同进程之间进行通信。消息队列的实现需要借助于操作系统提供的API,如msgget()、msgsnd()、msgrcv()等

18.HTTP是无状态的协议,当客户端登录后,一次请求完成,连接会释放,但是又希望服务器能够记住客户端的登录状态,可以怎么做?(回答了cookies与session)

  1. Cookie:服务器通过Set-Cookie头部将一个Cookie信息发送给客户端,在后续的请求中,客户端会自动在Cookie头部将该信息带回服务器。服务器可以利用Cookie来保留客户端的登录状态等信息。

  2. Session:服务器为每个客户端创建一个会话,为其分配一个唯一的SessionID,并将该SessionID放入Cookie中返回给客户端。客户端在后续请求中会把Cookie中的SessionID带回给服务器,服务器据此找到对应的会话信息。

  3. Token:客户端提交用户名和密码等认证信息,服务器验证通过后生成一个Token并返回给客户端,客户端在后续请求中在HTTP头部添加Authorization字段,将Token带回给服务器。服务器据此判断客户端身份。

  4. URL重新写入:如果浏览器不支持cookie, 可以通过URL重写来实现。服务器会将sessionid 放在url 的path部分,这样就可以通过URL传递sessionid了

19.synchronized和lock有什么区别(sb!一时间短路只想起了与volatile的区别)

  1. 使用范围:synchronized可以用于修饰方法、代码块,而Lock只能用于代码块。

  2. 锁的获取方式:synchronized会自动释放锁,而Lock必须显式地调用unlock()方法才能释放锁。

  3. 粒度:在Java 5之前,synchronized锁对象的粒度是针对整个方法或代码块的,而Lock可以实现更灵活的锁定,例如可以针对某个数据结构中的一部分进行加锁。

  4. 可重入性:synchronized是可重入的锁,即同一个线程可以多次获取同一个锁,而不会被阻塞。Lock也是可重入的锁,但需要注意避免死锁问题。

  5. 性能:在Java 5之前,synchronized的性能往往比Lock差,因为它要涉及到操作系统级别的互斥信号量。但是在Java 5之后,synchronized的性能得到了大幅度优化,在大部分情况下已经与Lock相当甚至更好了。

  6. 等待可中断:Lock提供了tryLock()方法,可以尝试获取锁,并在指定时间内等待锁释放。同时还提供了lockInterruptibly()方法,可以在等待过程中被中断

20.多线程环境下要怎么使用ArrayList?(没搞懂这问题,线程不安全可以使用相关并发集合类)

1.使用Collections.synchronizedList()方法将ArrayList转换为线程安全的List。这个方法返回一个线程安全的包装器,可以确保在访问集合时只有一个线程同时进行操作

21.一个对象从new出来到被回收经历哪些流转过程?(没回答好)

  1. 创建阶段:使用 new 操作符来创建一个对象时,JVM 会在堆内存中分配一块连续的空间用来存储该对象以及对象所包含的成员变量。

  2. 初始化阶段:在内存空间被分配后,JVM 将会对这块空间进行初始化。在此过程中,对象所包含的各个成员变量都会被赋上初始值,例如 int 型成员变量默认为0,引用类型成员变量默认为 null 等。

  3. 使用阶段:当对象完成初始化后,即可被使用。对象可以被传递给其他方法或者赋值给其他变量,应用程序也可以通过调用对象的方法来操作对象。在这个阶段,对象会被使用和修改。

  4. 被引用阶段:如果对象仍然被其他变量引用,则对象仍然处于被引用状态,直到所有引用它的变量失效为止。

  5. 垃圾回收阶段:如果对象不再被引用,则 JVM 的垃圾回收器会回收该对象占用的内存。垃圾回收器会在某个时间点自动运行,检测到哪些对象已经不再被引用,就会释放它们所占用的内存空间

22.二叉树有哪些?二叉搜索树查询时间复杂度多少?最坏情况?(忘记了变成链表)

1.二叉树是一种树形数据结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。常见的二叉树包括:普通二叉树(Binary Tree)、满二叉树(Full Binary Tree)、完全二叉树(Complete Binary Tree)等。

2.二叉搜索树(Binary Search Tree, BST)是一种特殊的二叉树,它的左子树中所有节点的值都小于根节点的值,而右子树中所有节点的值都大于根节点的值。这个规则保证了二叉搜索树在查找、插入和删除操作时的高效性。

3.在二叉搜索树中进行查找操作的时间复杂度为 O(log n),其中 n 表示二叉搜索树中节点的数量。这是因为每次查找都会将搜索区间缩小一半,所以查找的时间复杂度与树的高度相关。在最坏情况下,即二叉搜索树退化成链表的情况下,查找的时间复杂度为 O(n)。

4.需要注意的是,在进行插入、删除操作时,可能会导致二叉搜索树不再平衡,从而使树的高度增加,进而影响查找的时间复杂度。因此,在实际应用中,需要采用一些特殊的二叉搜索树,如红黑树、AVL 树等,来保证二叉搜索树的平衡性,从而提高查找操作的效率

23.讲讲spring容器,怎么理解IOC?

TCP 头部的大小是 20 字节,其中包括以下内容:

  1. 源端口和目标端口:用于唯一标识传输数据的源地址和目标地址。

  2. 序列号和确认号:用于实现数据传输的可靠性。序列号表示本次传输发送的第一个字节在整个数据流中的编号,确认号表示期望接收的下一个数据包的序号。

  3. 数据偏移和保留位:数据偏移指示 TCP 报文头部的长度(单位为 32 位),保留位预留给以后使用,必须设置为 0。

  4. 控制位:包括 ACK、SYN、FIN、RST、PSH 和 URG 六个标志位,用于控制 TCP 连接和数据传输。

  5. 窗口大小:用于流量控制,表示接收方可以接收的最大数据量。

  6. 校验和:用于检查数据在传输过程中是否发生错误。

  7. 紧急指针:用于处理 TCP 的紧急数据,指示紧急数据的结束位置。(可选)

  8. 选项:提供了一些可选的 TCP 功能,例如选择报文、时间戳等。(可选)

总之,TCP 头部大小为 20 字节,包含了源端口、目标端口、序列号、确认号、数据偏移、保留位、控制位、窗口大小、校验和以及选项等内容,其中控制位包括 ACK、SYN、FIN、RST、PSH 和 URG 六个标志位。

24.TCP头部的大小?TCP头部有哪些内容?

TCP头部的大小为20字节,每个字段的大小为4字节。

TCP头部包括以下信息:

  • 源端口号(Source Port):发送端使用的端口号。
  • 目标端口号(Destination Port):接收端使用的端口号。
  • 序列号(Sequence Number):用于序列化数据流的字节序列号。
  • 确认号(Acknowledgment Number):期望收到的下一条数据报的序列号。
  • 头部长度(Header Length):指示TCP头部的长度。
  • 保留(Reserved):暂时保留字段,未来可能被使用。
  • 控制位(Flags):例如SYN、ACK和FIN等控制位,用于建立和终止连接以及进行错误处理。
  • 窗口大小(Window Size):接收方可接收的数据量。
  • 校验和(Checksum):用于检测TCP头部和数据在传输过程中是否出现了错误。
  • 紧急指针(Urgent Pointer):用于指示紧急数据的位置。
  • 选项(Options):占用可变数量的字节,包含一些可选的TCP功能。

25.内存泄露如何定位?

  1. 使用工具进行检测:可以使用一些专门的工具来检测内存泄露,例如 Java 中的 Profiler 工具、Eclipse MAT(Memory Analysis Tool) 等。这些工具可以帮助开发人员查找应用程序中的内存泄露问题,并提供详细的分析和报告。

  2. 代码审查:通过对代码进行仔细审查,寻找可能导致内存泄露的地方,如未关闭文件、未释放数据库连接、未释放锁等。另外,需要特别注意循环引用和静态变量等情况,这些往往也是导致内存泄露的重要原因之一。

  3. 分析堆转储文件:当程序出现内存泄露时,可以使用 JVM 提供的 HeapDump 工具(如jmap)生成堆转储文件。然后,可以使用 MAT 或其他类似工具对堆转储文件进行分析,从而查找内存泄露的原因。

  4. 代码注入:在代码中注入记录堆栈和对象信息的代码,以追踪内存泄露的来源。这种方法可以用于非实时系统或者测试环境下,但需要谨慎使用,避免对代码性能造成影响

26.如何保证redis的热点数据不过期

  1. 设置合适的过期时间:对于那些经常被访问的热点数据,可以设置一个较长的过期时间,以确保它们不会过期。在设置过期时间时需要考虑到业务需求和硬件设备的性能,避免内存占用太高或者对系统性能造成不良影响。

  2. 使用 Redis 持久化功能:Redis 支持多种持久化方式,包括 RDB 和 AOF。在使用这些持久化方式时,可以将热点数据定期写入磁盘,以确保即使 Redis 服务崩溃或重启,这些数据也不会丢失。通过持久化功能,可以实现数据的可靠性和持久性,并降低数据丢失的风险。

除了以上两个方法,还可以结合 Redis 集群、主从复制等技术来实现热点数据的高可用性和可靠性。例如可以使用 Redis 集群将热点数据分布到多个节点上,以提高负载均衡和可用性;又或者可以使用 Redis 主从复制来实现热点数据的备份和读写分离,以提高系统的并发处理能力和稳定性。

27.Java创建对象的几种方式

  1. 使用关键字new来创建对象。

  2. 使用类的反射机制Class.newInstance()方法。通过调用类的Class实例的newInstance()方法,可以动态地创建一个该类的对象,前提是该类必须要有无参构造函数。

  3. 使用构造器(Constructor)对象的newInstance()方法。通过获取到类的Constructor对象,再调用该对象的newInstance()方法,也可以动态创建一个该类的对象。

  4. 使用clone()方法。在Java中,每个对象都有一个clone()方法,可以用于创建该对象的一份副本。

  5. 反序列化。通过使用Java中的序列化和反序列化机制,可以将对象写入到文件中或者通过网络传输,然后再从文件或网络中读取该对象,从而得到一个新的对象

28.如何实现动态代理

基于接口的动态代理:通过Java自带的Proxy类和InvocationHandler接口实现。步骤如下:

  1. 定义一个接口和实现该接口的类;
  2. 实现InvocationHandler接口,并重写invoke()方法,在此方法中处理目标对象的方法调用;
  3. 通过Proxy.newProxyInstance()方法创建代理对象。

基于类的动态代理:通过CGLIB(Code Generation Library)实现。步骤如下:

  1. 添加cglib库的依赖;

  2. 定义一个目标对象(即要被代理的类);

  3. 创建MethodInterceptor接口的实现类,并重写intercept()方法,在其中处理目标对象的方法调用;

  4. 通过Enhancer.create()方法创建代理对象。

29.如何解析Post请求报文?(回答不全)

在Java中,可以使用Servlet API和Spring Framework等框架来解析HTTP POST请求报文。

对于Servlet API来说,可以通过HttpServletRequest对象获取HTTP POST请求报文中的参数和内容。具体步骤如下:

  1. 获取HttpServletRequest对象。
  2. 调用request.getParameter()方法获取请求参数值。
  3. 调用request.getReader()方法获取请求报文内容的字符输入流。
  4. 使用BufferedReader读取字符输入流中的内容。

对于Spring Framework来说,Spring MVC提供了一系列内置的参数解析器(Parameter Resolver),包括RequestBody和ModelAttribute等注解,可以方便地将HTTP POST请求报文中的参数或者内容映射成Java对象。具体方式如下:

  1. 指定控制器方法的参数注解为@RequestBody或者@ModelAttribute。

  2. Spring会自动解析POST请求中的参数或者内容,并将其转换成注解所对应的类型。

30.sql查询很慢怎么排查(知道解决 一下子问排查短路了)

  1. 确认查询语句是否正确:检查SQL查询语句是否正确,包括表名、列名等是否正确,SQL语法是否正确等。

  2. 检查索引是否合理:索引是提高查询效率的重要手段。可以使用EXPLAIN命令来分析查询语句,了解MySQL如何处理查询语句,从而判断是否需要增加或优化索引。

  3. 优化数据库参数:调整MySQL的一些关键参数,例如缓存大小、连接池大小、查询缓存等,可以提高查询效率。可以使用SHOW VARIABLES命令来查看当前的参数设置情况。

  4. 分析慢查询日志:通过开启慢查询日志功能并分析日志文件,可以找出执行时间超过阈值(默认为10秒)的查询语句和相关信息,并进行分析和优化。

  5. 使用Profiling功能:可以使用MySQL Profiling功能对查询进行

31.MySQL的主从复制过程

1.MySQL主从复制是一种常见的高可用性部署方案,它可以将一个MySQL服务器(称为主节点)的数据同步到多个备份服务器(称为从节点)。 主从复制通常通过以下步骤实现:

  • 首先,在主节点上启用二进制日志(binlog),该日志记录所有对主节点中数据库的更改。
  • 然后,在每个从节点上配置连接到主节点的复制账户和复制策略。通常使用CHANGE MASTER TO语句来完成这个任务。
  • 一旦从节点连接到主节点并开始复制,它会首先从主节点获取当前的binlog文件和位置(即“复制点”)。
  • 从节点随后开始持续地读取主节点的binlog,并将其中的操作应用到自己的数据库中。此过程称为“应用事件

32.聚簇索引和非聚簇索引的区别

  • 聚簇索引:按照索引列的值重新组织表中的数据,使得具有相似索引值的行物理上存储在一起。因此,聚簇索引只能有一个,并且通常是主键索引。聚簇索引可以提高查询效率,但插入、删除和更新数据时会导致数据移动,影响性能。
  • 非聚簇索引:不会重新组织表中的数据,而是将索引列的值与对应行的指针存储在一个单独的数据结构中。因此,非聚簇索引可以有多个,并且通常使用普通索引或唯一索引实现。非聚簇索引可以提高查询效率,但它需要额外的空间来存储索引数据

33.问非对称加密你了解多少?

非对称加密(Asymmetric Encryption)是一种加密方式,其特点是需要两个密钥:公钥和私钥。公钥用于加密数据,私钥用于解密数据。非对称加密广泛应用于数字签名、TLS/SSL安全传输协议、SSH远程登录等场景中。其优点是保证了通信的可靠性和机密性,但缺点是加密解密的速度较慢,因此通常只用于加密少量数据

34.MVCC,具体流程举例

  • 当一个事务开始时,MySQL会为该事务创建一个唯一的事务ID。
  • 在执行INSERT、UPDATE、DELETE等操作时,MySQL不会直接修改原始数据,而是将新数据插入到一个新的版本中,并记录下该版本的创建时间戳和所属事务ID(也就是版本号)。
  • 当其他事务读取数据时,MySQL会根据该事务的隔离级别和版本号选择合适的数据版本进行读取。如果读取的是旧版本,则会被阻塞或者重新读取。
  • 当一个事务提交时,MySQL会将该事务所做的所有修改变成永久的,并释放该事务的锁。同时,MySQL还会将该事务开始之后被创建的所有版本标记为“已过期”,以便在之后的清理过程中删除它们

36.Nginx如何查看后端Tomcat状态

1.在Nginx中,可以使用upstream模块来管理后端的Tomcat服务器。通过设置upstream服务器的状态检查参数,我们可以查看后端Tomcat服务器的状态。具体地说,可以在配置文件中添加以下代码:

其中check指令用于设置后端服务器状态检查的参数,interval表示每隔多久进行一次检查,rise表示连续几次检查成功后将服务器视为“健康”的状态,fall表示连续几次检查失败后将服务器视为“宕机”的状态,timeout表示检查超时时间,type表示检查的方式,这里设置为http。

我们可以通过访问Nginx服务器的/status页面来查看Tomcat服务器的状态。如果状态为up,则表示该服务器是“健康”的;如果状态为down,则表示该服务器已经被标记为“宕机”。

37.如何把请求进行分配(轮询策略、最少请求数、优先级 ojbk)

  1. 在分布式系统中,请求的分配通常使用负载均衡技术来实现。常见的负载均衡策略包括轮询(Round Robin)、最少请求数(Least Connections)、IP散列(IP Hash)等。
  • 轮询策略:按照服务器列表的顺序依次将请求转发到每个服务器上;
  • 最少请求数策略:将请求转发到当前连接数最少的服务器上;
  • IP散列策略:根据请求的源IP地址计算出一个哈希值,并将请求转发到对应的服务器上

38.32位和64位具体指什么;

32位和64位主要指CPU的寻址能力,即能够处理内存的最大容量。32位CPU的寻址能力为2^32,即4GB。而64位CPU的寻址能力为2^64,可以处理的内存容量极大,几乎没有限制

39.讲讲分布式锁 结合项目讲Redisson的使用

分布式锁是在多个节点的系统中,为了协调分布式环境下资源的使用而采用的一种机制。在分布式系统中,由于各个节点之间的通信可能存在延迟、丢失等问题,传统的锁机制难以满足分布式环境下的并发需求。因此,我们需要通过分布式锁来确保在多个节点同时访问共享资源时能够保持正确的同步。

Redisson 是 Redis 的一个 Java 客户端,它提供了实现分布式锁的功能。具体来说,Redisson 支持以下几种类型的分布式锁:

  • 可重入锁(Reentrant Lock)
  • 公平锁(Fair Lock)
  • 联锁(MultiLock)
  • 红锁(Red Lock)
  • 读写锁(ReadWrite Lock)

其中,可重入锁和公平锁与 Java 中的 ReentrantLock 和 FairLock 类似,用于在分布式环境下实现可重入和公平的互斥锁。联锁可以将多个锁绑定在一起,从而实现一次性获取多个锁的需求。红锁则是用于在不同节点之间实现分布式锁的高级方案,可以确保在大部分节点正常工作的情况下,仍然能够获得锁。最后,读写锁则类似于 Java 中的 ReadWriteLock,用于实现读写分离的并发控制。

在 Redisson 中,我们可以通过调用 RLock 对象的 lock() 方法来获取锁,调用 unlock() 方法来释放锁。同时,Redisson 还提供了其他丰富的功能,比如可重入锁可以设置过期时间、公平锁可以设置等待队列长度等。此外,Redisson 还提供了一些高级特性,比如异步执行任务、延迟队列、流式布局等

40.redis主从复制、

1.Redis 主从复制是指在 Redis 集群中,主节点将自己的数据同步到从节点,从而实现数据备份和负载均衡的机制。

2.当主节点接收到一个写请求时,它会先将数据更新到自己的内存中,然后通过网络发送给从节点,并在本地记录下已发送的信息的偏移量。

3.从节点接收到数据后,会将其更新到自己的内存中,并在本地记录下自己已经接收到的信息的偏移量。

4.当主节点发生故障或者网络不稳定时,从节点可以使用本地记录的偏移量继续向其他从节点请求数据,从而避免数据丢失

41.redis原子性的实现机制(单线程 没反应过来)

Redis的原子性是通过单线程执行和事务机制来实现的,其主要步骤如下:

  1. 客户端发送MULTI命令表示开始一个新的事务,并将多个操作打包成一个队列。

  2. Redis将这些操作暂存起来,并返回一个"QUEUED"响应。

  3. 客户端继续发送其他命令,Redis将这些命令放入队列中。

  4. 客户端发送EXEC命令,表示提交整个事务。

  5. Redis依次执行队列中的所有命令,如果其中任何一个命令执行失败,则整个事务都会被回滚。

  6. Redis将所有结果打包成一个数组,返回给客户端。

在事务过程中,客户端还可以使用WATCH命令监视一个或多个键,当这些键在事务执行期间被修改时,事务会被终止并返回一个错误响应。同时,还可以使用DISCARD命令取消一个事务。

42.最左前缀,聚集索引、非聚集索引、回表查询这些

最左前缀:

        最左前缀指在查询中使用的索引字段,是指从左边开始不断地选择索引列作为条件进行过滤,直到选出不在索引列中的列为止。最左前缀可以高效地定位符合查询条件的记录

应用场景:当查询条件包含多列时,可以优先选择与索引列相同的列作为条件过滤以提高查询效率

代码示例:

聚集索引:

        聚集索引是一种数据存储方式,将表数据按照索引关键字所表示的顺序存储在磁盘上。一个表只能有一个聚集索引,因为表数据只能按照一种方式排序,并且是物理存储顺序

应用场景:适合于经常需要按照某个列排序或分组的情况,可以大大提高查询速度。

代码实现:

非聚集索引:

        非聚集索引也是一种数据存储方式,将索引关键字和对应的行指针存储在磁盘上。一个表可以有多个非聚集索引,因为每个索引都可以独立地进行排序和查找​​​​​​​

应用场景:适用于经常需要使用特定列进行查询的情况,可以提高查询速度

代码展示:

回表查询:

        回表查询是指根据非聚集索引检索到索引列的内容后,还需要通过行指针再次查找到完整的数据行。这会增加额外的IO操作和处理时间

应用场景:当需要查询的列不在索引列中时,需要进行回表查询,会增加额外的IO操作和处理时间。因此,在设计索引时应尽量包含所有需要查询的列

代码展示:

​​​​​​​

43.注册中心上保存的什么东西?(nacos作为注册中心)

  1. 注册中心通常是微服务架构中的一个组件,用于管理和协调各个微服务实例。它通常会保存以下一些信息:
  • 微服务实例的网络位置信息(比如IP地址、端口号等)
  • 微服务实例的元数据信息(比如服务名称、版本号、环境信息等)
  • 微服务实例的健康状态信息(比如存活状态、负载情况等)
  • 微服务实例的路由信息(比如路由规则、负载均衡策略等)

44.L1,L2,L3缓存是所有CPU所有核心共享的嘛?既然L1,L2缓存属于不同核心私有那不同核心L1,L2缓存如何通信?

L1、L2和L3缓存都是CPU的缓存,但它们之间的共享方式不同。

1.L1缓存是每个CPU核心私有的,意味着一个CPU有多个核心,就会有多个L1缓存。而L2缓存通常也是每个核心私有的,但在某些架构中,也可能将其设计成多个核心共享一个L2缓存。最后,L3缓存则是在整个CPU芯片上共享的(有些架构可能有多个L3缓存),因此所有CPU核心都可以访问相同的L3缓存。

2.当不同核心需要访问彼此的缓存时,需要进行缓存一致性协议。这些协议确保了不同核心之间对主内存中数据的访问的正确性。在缓存一致性协议中,当一个核心修改了共享内存中的数据时,它会发送一个信号给其他核心,告诉它们需要更新它们的缓存。

3.具体来说,在L1和L2缓存之间的通信通常采用总线协议,而在L3缓存之间的通信则使用网络互连协议。这些协议确保了缓存中的数据在各个核心之间的一致性,避免了数据冲突和竞态条件的发生。

45.什么是CAS?Unsafe类再操作系统层面是如何实现CAS的?CAS会一直自旋嘛?

1.CAS(Compare and Swap)是一种乐观锁技术,用于实现多线程环境下的无锁同步。CAS 操作包含三个操作数:内存位置 V、期望值 A 和新值 B。如果内存位置 V 的值等于期望值 A,则将内存位置 V 的值更新为新值 B;否则不做任何操作。

2.在 Java 中,CAS 可以通过 java.util.concurrent.atomic 包提供的 Atomic 类或者 Unsafe 类来进行操作。Unsafe 类是一个类似于 C 语言指针的工具类,它提供了一组低级别的、不安全的操作方法,包括 CAS 操作。在操作系统层面,CAS 是由 CPU 提供支持的原子操作指令,比如 x86 架构上的 CMPXCHG 指令。

3.当调用 Unsafe.compareAndSwapInt() 方法时,会先从内存地址中读取当前值,然后与期望的旧值比较。如果相等,则把新值写入到这个内存地址中,并返回 true 表示修改成功。否则返回 false,表示修改失败。由于CAS是一种忙等待的方式,所以如果同时有很多线程尝试修改同一个内存位置,就会导致大量的自旋操作浪费 CPU 资源。

4.为了避免自旋带来的性能问题,JDK8 引入了适应性自旋技术。该技术会根据当前 CAS 操作的历史执行时间和当前线程调度情况来动态地调整自旋次数,从而减少自旋带来的性能损失。此外,在 JDK8 中还引入了基于 CPU Cache 和锁膨胀机制等优化技术,可以显著提高 CAS 的性能和并发度。

46.TCP第二握手如果数据包丢失了怎么办?此时客户端和服务端处于什么状态?

TCP的三次握手是建立连接的过程,其中第二次握手是客户端向服务端发送SYN(同步)包,并等待服务端回复SYN+ACK(同步确认)包。如果在这个过程中数据包丢失了,客户端会一直等待服务端的回复,而服务端也不会收到客户端发来的SYN包,从而导致连接无法建立。

在这种情况下,客户端和服务端处于不同的状态:

  • 客户端处于SYN_SENT状态,表示已经向服务端发送了SYN包,但还没有收到服务端的响应;
  • 服务端则不会进入任何状态,因为它并没有收到客户端的SYN包。

为了解决这个问题,TCP协议使用了超时重传机制。当客户端发送SYN包后,如果一定时间内没有收到服务端的回复,则会重新发送SYN包。服务端收到SYN包后,会回复SYN+ACK包,以进行第三次握手。如果这个SYN+ACK包也丢失了,那么客户端会继续等待一段时间并执行重试,直到连接建立成功或者达到最大重试次数为止

47.Mysql一条插入语句,服务端保存数据的流程? binlog和redolog的区别?

当MySQL服务端接收到一条插入语句时,它会按照以下流程进行处理:

  1. 服务端首先将该语句解析成对应的语法树,并进行语义检查,以确保语句的正确性和合法性。

  2. 然后,服务端会将该语句转换为对应的内部格式,并将其存储在缓存中,以便之后执行。

  3. 当需要执行该语句时,服务端会将其发送给存储引擎,存储引擎会将数据写入内存缓冲区。

  4. 如果该表启用了事务,那么MySQL服务端会在内存缓冲区中记录该操作,但不会立即将其持久化到磁盘上。此时,事务日志(Transaction Log)会记录下这个操作,以便在回滚或恢复操作时使用。

  5. 当事务提交时,MySQL服务端会将所有内存中的数据以及事务日志一起刷入磁盘上,以确保数据的持久化和一致性。

binlog和redolog是MySQL中两种不同的日志文件,主要作用是记录MySQL中的操作和状态变更信息,以实现数据恢复、备份和复制等功能。它们的主要区别如下:

  1. binlog(二进制日志)记录的是所有对数据库的修改操作,可以用来实现数据恢复和备份。而redolog(重做日志)只记录正在进行的事务的操作,用来实现崩溃恢复和灾难恢复。

  2. binlog是逻辑日志,记录的是SQL语句执行的结果,而redolog是物理日志,记录的是对磁盘块的修改记录。因此,binlog可以跨平台复制,而redolog不能。

  3. binlog记录的是逻辑信息,因此其开销相对较小,但缺点是在数据恢复时需要重新执行SQL语句,速度较慢。而redolog记录的是物理信息,因此其开销较大,但是在数据恢复时速度更快,因为只需要将磁盘块恢复到某个时间点即可。

总之,binlog和redolog都是MySQL中的重要日志文件,其主要作用是记录MySQL中的操作和状态变更信息,以实现数据恢复、备份和复制等功能。它们之间的区别主要在于记录的内容、性能开销和使用方式等方面。

48.SpringMVC流程?为什么所有请求都会经过DispatcherServlet?

SpringMVC的流程如下:

  1. 客户端发送请求到DispatcherServlet。

  2. DispatcherServlet接收到请求后,根据请求的URL路径,选择合适的Controller进行处理。

  3. Controller处理请求并返回一个ModelAndView对象。

  4. DispatcherServlet通过视图解析器将ModelAndView对象转换成具体的视图,并将响应发送给客户端。

在SpringMVC中,所有请求都会经过DispatcherServlet。这是因为DispatcherServlet充当了前端控制器(Front Controller)的角色,它负责接收所有请求,并协调整个流程的各个组件,

具体来说,DispatcherServlet会根据请求的URL路径,使用HandlerMapping将请求映射到对应的Controller进行处理。然后,Controller会根据请求参数和业务逻辑生成一个ModelAndView对象,该对象包含了需要渲染的视图名称和模型数据。最后,DispatcherServlet将该对象传递给ViewResolver,将其转换成具体的视图,并将响应发送给客户端。

总之,在SpringMVC中,DispatcherServlet是整个流程的核心,它负责接收和分派所有请求,并协调整个流程的各个组件,从而实现灵活高效的Web应用程序。

49.SpringAOP的底层实现?动态代理需要实现InvocationHandler接口,InvocationHandler底层是如何实现的?

Spring AOP 的底层实现是基于 JDK 动态代理和 CGLIB 代码生成技术的。

1.在使用纯 Java 类型的 Spring Bean 的时候,Spring AOP 使用了 JDK 动态代理技术。JDK 动态代理是通过反射机制动态地生成一个实现了被代理接口的代理类,在代理类中增强原有方法的逻辑,并在执行原有方法前后执行增强逻辑。JDK 动态代理要求被代理的类必须实现至少一个接口,因此只能代理接口中定义的方法。

2.而对于没有实现接口的类,Spring AOP 则使用 CGLIB 代码生成技术来实现动态代理。CGLIB(Code Generation Library)是一个功能强大的代码生成库,它可以在运行时动态地生成指定类的子类,并覆盖其中的方法来实现动态代理。通过继承被代理对象,CGLIB 能够对类的所有方法进行代理,包括 final 方法。

3.在 JDK 动态代理和 CGLIB 技术中,都需要实现 InvocationHandler 接口,该接口只有一个 invoke() 方法,该方法会在代理对象调用方法时被调用。在 invoke() 方法中,可以对被代理方法进行增强处理,并在原有方法的前后执行增强逻辑。具体来说,当代理对象调用方法时,invoke() 方法会接收到代理方法的 Method 对象、参数列表和代理对象本身,通过反射机制来执行增强逻辑,并返回一个代理方法的返回值。

4.总之,Spring AOP 的底层实现基于 JDK 动态代理和 CGLIB 代码生成技术,并通过 InvocationHandler 接口来实现对原有方法的增强

50.MVCC?readview活跃事务 ID 列表是怎么获取到的?

MVCC(Multi-Version Concurrency Control)是一种多版本并发控制机制,用于在并发环境下保证数据的一致性和可重复读。

在 MVCC 中,每个事务都会获得一个唯一的事务 ID(Transaction ID),事务对数据库中的数据进行修改时会生成一个新版本,并将该版本与事务 ID 关联起来。因此,当事务执行查询操作时,它只能看到已提交的事务生成的版本,而无法看到还未提交的事务生成的版本。这种机制可以有效地避免脏读、不可重复读等并发问题。

readview 是 MySQL 实现 MVCC 的关键组件之一,用于记录当前事务可见的历史版本。readview 包含两个部分:

  1. 版本链表:包含了所有已提交的事务ID,以及它们在事务提交时生成的版本号。

  2. 活跃事务 ID 列表:包含了当前活跃的事务ID,也就是还未提交的事务ID。

当一个事务开始时,MySQL 会根据当前系统中的所有活跃事务生成一个 readview,并将其绑定到该事务上。readview 生成后,在整个事务执行期间都会保持不变。当事务执行查询操作时,MySQL 会根据 readview 中的版本链表和活跃事务 ID 列表,确定该事务可见的历史版本。

活跃事务 ID 列表的获取方式如下:

  1. 遍历 InnoDB 存储引擎的事务链表,得到所有当前活跃的事务ID。

  2. 如果一个事务已经提交了(commit),但还未释放锁资源,那么它也会被视为活跃事务。

  3. 执行查询时,根据 readview 中的版本链表和活跃事务ID列表,确定该事务可见的历史版本。

51.AtomicInteger之类的原子类型用到过没?CAS存在什么缺陷?

CAS 算法是一种乐观锁技术,它通过比较内存中的值与期望值是否相等来判断是否需要更新。如果相等,则将内存中的值更新为新值;否则,不执行任何操作。由于 CAS 操作是在一个原子操作中完成的,因此可以保证操作的原子性和线程安全性。

然而,CAS 也存在一些缺陷:

  1. ABA 问题:CAS 只能检测到值是否有变化,但无法检测值的变化过程。例如,线程 A 将值从 1 修改为 2,然后再修改回 1,此时线程 B 修改该值时仍旧会认为其没有被修改过,导致出现 ABA 问题。

  2. 自旋时间过长:CAS 操作在并发量较大的情况下可能会导致自旋时间过长,严重影响系统性能。

  3. 只能保证单个变量的原子性:CAS 只能针对单个变量实现原子操作,不能针对多个变量实现复合操作。

52.HashMap类型put和get的底层实现?

在 HashMap 中,put() 方法会将键值对存储到哈希表中。具体的实现步骤如下:

  1. 首先,根据键的 hashCode() 方法计算出一个哈希码(即对应的数组下标)。

  2. 如果该位置没有被占用,则直接将键值对存储在该位置上,并返回 null。

  3. 如果该位置已经被占用,则需要检查该位置上的键是否与要插入的键相同。如果相同,则更新该位置上的值;否则,使用开放寻址法或链式存储法解决哈希冲突。

  4. 如果当前 HashMap 的 size 大于等于负载因子(默认为 0.75),则需要将哈希表扩容。扩容后,原来的键值对需要重新哈希并放置到新的位置上。

get() 方法会根据指定的键返回对应的值。具体的实现步骤如下:

  1. 根据键的 hashCode() 方法计算出哈希码。

  2. 在哈希表中查找该哈希码对应的位置。如果该位置为空,则返回 null。

  3. 如果该位置不为空,则需要根据键的 equals() 方法检查该位置上的键是否与要查找的键相同。如果相同,则返回该位置上的值;否则,使用开放寻址法或链式存储法在哈希表中继续查找。

总之,在 HashMap 中,put() 方法通过将键值对映射到哈希表中的一个索引位置来实现插入操作,而 get() 方法则通过根据键的哈希码和 equals() 方法来查找对应的值。同时,为了解决哈希冲突,HashMap 可以使用开放寻址法或链式存储法等技术进行优化

53.敏感词过滤有了解吗?简单说一下。(我回答:前缀树),追问:前缀树有哪些优化方案?简单说说。

敏感词过滤是指在用户输入的文本中查找并过滤掉敏感词汇,以保护用户和其他利益相关方的合法权益。前缀树是一种常用的敏感词过滤算法,它可以在 O(m) 的时间复杂度内查找长度为 m 的字符串是否为敏感词。

前缀树(Prefix Tree),也称为字典树(Trie Tree),它是一种自动机数据结构,用于高效地存储和查找字符串集合。在前缀树中,每个节点表示一个字符串的前缀,而从根节点到叶子节点的路径则表示一个完整的字符串。为了实现敏感词过滤,我们可以将敏感词库构建成一个前缀树,并在用户输入的文本中查找是否存在前缀树中的某个字符串即可。

除了基本的前缀树算法外,还有一些优化方案可以提高前缀树的效率,例如:

  1. 压缩前缀树:将多个只有一个子节点的节点合并成一个节点,可以减少空间开销和查询次数。

  2. 双数组前缀树:使用两个数组来表示前缀树的状态转移,可以大大提高性能。

  3. Aho-Corasick 算法:将多个前缀树合并成一个 AC 自动机,可以快速地查找一个字符串中是否存在多个敏感词汇。

总之,在实现敏感词过滤时,前缀树是一种高效的算法,在实际应用中可以根据具体的场景使用合适的优化方案来提高效率和准确度

54.说说红黑树?他与平衡二叉树有啥区别

红黑树是一种自平衡二叉查找树,它的每个节点都被标记为红色或者黑色。红黑树满足以下性质:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点是黑色。
  3. 每个叶子节点(NULL节点)是黑色。
  4. 如果一个节点是红色的,则它的两个子节点都必须是黑色的(反之不一定成立)。
  5. 从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。

红黑树与平衡二叉树的主要区别在于平衡策略的不同。平衡二叉树维护的是树的高度的平衡,而红黑树则维护了更多的性质,使得操作的复杂度能够保证在 O(log n) 级别。

具体来说,在平衡二叉树中,通过旋转操作来调整节点的位置以达到平衡,而红黑树则通过变换节点的颜色和进行旋转操作来达到平衡。这样,在插入、删除等基本操作时,红黑树能够比平衡二叉树更快地调整平衡,使得整棵树的高度控制在 O(log n) 级别。因此,红黑树是一种更加高效的自平衡二叉查找树。

55.请你设计一个电梯调度的算法,简单说说原理

常见的电梯调度算法有以下几种:

  1. FCFS(First Come First Serve)调度算法:按照乘客请求的先后顺序,将其分配给空闲的电梯或正在运行的电梯。这种算法较为简单,但是不能很好地适应高峰期的需求。

  2. SSTF(Shortest Seek Time First)调度算法:选择距离当前楼层最近的空闲电梯进行服务。它可以减少乘客的等待时间,但是可能会出现某些电梯长时间处于闲置状态的情况。

  3. SCAN 调度算法:电梯在一端开始运行,直到到达另一端,然后返回原点。在该过程中,按照楼层从低到高的顺序进行服务。这种算法可以保证每个乘客的服务时间相对公平,并且可以避免某些电梯长时间处于闲置状态的情况。

  4. C-SCAN(Circular SCAN)调度算法:与 SCAN 调度算法类似,但是电梯到达另一端时立即返回原点,并且只能按照一个方向进行服务。这种算法可以避免长时间等待的乘客出现。

56.建立TCP连接后,客户端下线了会发生什么

  1. 服务器无法感知客户端下线:TCP 是面向连接的协议,只有在两端正常关闭连接时才能够感知到对方状态的变化。因此,如果客户端强制断开连接或者出现异常崩溃,服务器是无法及时感知到客户端下线的状态。

  2. 服务器的连接资源可能被浪费:由于服务器无法及时感知客户端下线,可能会导致服务器上的连接资源得不到释放,从而造成资源的浪费。如果同时有大量客户端连接到服务器,这种资源浪费可能会导致服务器瘫痪。

  3. 服务器会发送一定量的数据:如果客户端下线时,服务器正在向客户端发送数据,那么服务器会继续发送一定量的数据(称为“风暴数据”),直到超时或者收到确认信息为止。这些风暴数据也可能会造成网络拥塞和资源浪费等问题。

因此,在实际应用中,需要通过心跳机制、断线重连等技术来处理客户端下线的情况,以保证连接资源的合理利用和系统的稳定性。

58.springboot线程池创建

在 Spring Boot 中,你可以通过配置 ThreadPoolTaskExecutor 或 TaskScheduler 来创建线程池。下面是基本的步骤:

  1. 在你的 Spring Boot 应用程序的配置类中定义一个 Bean,并指定它的类型为 ThreadPoolTaskExecutor 或 TaskScheduler。

  2. 配置线程池参数,如核心线程数、最大线程数、队列容量等。

  3. 使用线程池来执行需要异步处理的任务。

以下是一个使用 ThreadPoolTaskExecutor 创建线程池的示例:

在这个例子中,我们实现了 AsyncConfigurer 接口,并重写了 getAsyncExecutor 方法来创建一个线程池。然后我们设置了线程池的核心线程数、最大线程数和队列容量,并将其初始化。

接着,在需要异步执行的方法上添加 @Async 注解即可使用该线程池来执行异步任务: 

 

59.单点登录

单点登录(Single Sign-On,简称 SSO)指的是用户只需要登录一次就能够访问多个相互信任的应用系统。以下是实现单点登录的基本步骤:

  1. 创建一个认证中心(Identity Provider,简称 IdP),该认证中心负责处理用户的身份验证并颁发令牌(Token)。

  2. 在每个需要单点登录的应用系统上创建一个服务提供者(Service Provider,简称 SP),该服务提供者接收来自认证中心的令牌并验证其合法性。

  3. 当用户尝试访问某个应用系统时,该应用系统将重定向用户到认证中心,并在 URL 中包含一个标识符。

  4. 认证中心弹出登录界面,提示用户输入用户名和密码进行身份验证。如果验证成功,认证中心会生成一个令牌并将其发送回服务提供者。

  5. 服务提供者接收到令牌后,验证其合法性。如果令牌有效,则允许用户访问该应用系统。

  6. 如果用户尝试访问其他应用系统,则重复以上步骤。

总的来说,实现单点登录需要完成两个主要任务:身份验证和令牌传递。认证中心负责处理身份验证并颁发令牌,而服务提供者则接收令牌并验证其合法性

 这样就可以实现基于 OAuth2 的单点登录了。当用户访问服务提供者时,系统会自动重定向到认证中心进行身份验证。如果验证成功,认证中心会生成一个令牌并将其发送回服务提供者。服务提供者接收到令牌后,验证其合法性并允许用户访问应用程序

60.Mybatis缓存

MyBatis 是一种流行的持久层框架,它提供了一种缓存机制来提高系统的性能。MyBatis 缓存主要分为两种类型:一级缓存和二级缓存。

  1. 一级缓存:也称为本地缓存,是 MyBatis 默认开启的缓存,它是基于线程的,在同一个 SqlSession 中共享。当多次相同的查询操作时,MyBatis 可以直接从本地缓存中获取数据,避免了频繁访问数据库带来的性能损耗。

  2. 二级缓存:二级缓存是基于命名空间的,可以跨 SqlSession 共享缓存。当某个 SqlSession 执行完毕后,会将查询结果写入到二级缓存中,下次其他 SqlSession 执行同样的查询操作时,可以直接从缓存中获取数据,避免了重复执行 SQL 的开销。

在使用 MyBatis 缓存时,需要注意以下几点:

  1. 缓存的有效性:MyBatis 的缓存机制默认情况下是开启的,但有些时候缓存并不是最新的。因此需要考虑如何控制缓存的有效性,例如进行定时清理、手动刷新缓存或者设置缓存过期时间等。

  2. 缓存的粒度:MyBatis 的缓存机制是以命名空间或 SqlSession 为粒度的。按照粒度不同,缓存的效果也会有所差异,需要根据具体项目需求来选择合适的缓存粒度。

  3. 缓存的清除:MyBatis 的缓存机制中提供了多种清除缓存的方式,包括基于时间、基于对象或者手动清除等。需要根据具体情况选择合适的清除方式。

61.你的mysql存储很大数据量怎么解决,思路

如果 MySQL 存储的数据量很大,可能会导致数据库性能下降、查询速度变慢等问题。以下是几种解决大数据量问题的思路:

  1. 数据库分区:将大表拆分成多个小表,并将这些小表存储在不同的分区中,可以提高数据库的查询性能和可用性。同时,也需要考虑如何根据业务需求划分分区,以及数据量扩增时如何动态调整分区方案。

  2. 索引优化:添加合适的索引可以提高数据库查询效率。但是,过多或者不必要的索引会对写入操作造成影响,因此需要权衡索引数量和查询需求。

  3. SQL 优化:优化 SQL 查询语句可以减少数据库的负载,提高查询效率。如避免全表扫描、尽量使用简单的 JOIN、使用 UNION ALL 代替 UNION 等。

  4. 分库分表:将数据存储在多个独立的数据库中,可以降低单个数据库的压力,提高并发处理能力。但是分库分表也会带来一些问题,例如事务处理、跨库查询等问题。

  5. 缓存优化:缓存技术可以将热点数据存储在内存中,避免频繁的数据库读取操作,提高系统响应速度和并发性能。

  6. 数据库服务器升级:增加数据库服务器的处理能力、存储容量和带宽等硬件资源,可以提高系统的负载能力和性能表现。

  7. 数据备份和恢复:及时备份数据可以避免数据丢失和系统故障带来的影响。同时也需要考虑如何快速恢复大规模数据以保证系统正常运行。

62.redis怎么处理高并发,几种思路

Redis 作为一种内存数据库,对于高并发场景有着良好的支持和优化思路。下面介绍几种 Redis 处理高并发的常见思路:

  1. 消息队列:Redis 可以作为消息队列来处理高并发场景。通过将任务存储在 Redis 中,并使用多个消费者同时处理任务,可以提高系统的并发性能和处理能力。

  2. 分布式锁:Redis 的分布式锁机制可以用于保证数据的原子性和一致性。通过添加锁来限制只有一个客户端能够访问共享资源,避免并发操作带来的问题。

  3. 缓存:Redis 的缓存机制可以用于加速读取操作,减少对后端数据库的访问。通过将热点数据存储在 Redis 中,可以大幅降低数据库压力,提高系统性能。

  4. 集群:Redis 支持集群模式,通过横向扩展节点数量,可以提高系统的负载能力和可用性。同时也需要注意集群中的数据同步与节点故障的处理等问题。

63.redis实现原理

Redis 是一种高性能的开源内存数据库,采用键值存储方式,支持多种数据类型和复杂的数据结构。下面是 Redis 的实现原理:

  1. 内存数据库:Redis 将所有数据都存储在内存中,因此具有极快的读写速度。同时,为了避免内存溢出等问题,Redis 也提供了持久化机制,将数据定期写入磁盘。

  2. 单线程模型:Redis 使用单线程模型来处理客户端请求,通过使用非阻塞 I/O、事件驱动等技术,从而能够提高系统并发性能和响应速度。

  3. 客户端/服务器模型:Redis 采用客户端/服务器模型,可以同时服务多个客户端,并通过网络协议(例如 TCP/IP)进行通信。

  4. 多种数据类型:Redis 支持多种数据类型,包括字符串(String)、哈希表(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)。每种数据类型都有特定的操作命令,例如 GET、SET、HGETALL、LPUSH、SADD 和 ZRANGE 等。

  5. 操作原子性:Redis 中的大部分操作都是原子性的,意味着在执行这些操作时,要么全部成功完成,要么全部失败回滚。这样可以保证数据的一致性和可靠性。

  6. 高可用性:Redis 提供了多种高可用性方案,包括主从复制、哨兵和集群等。通过这些机制,可以提高系统的可用性和容错性。

64.怎么做数据库优化

数据库优化是提高系统性能和可用性的关键步骤之一。下面列出了几种常见的数据库优化方法:

  1. 索引优化:索引是数据库查询性能的重要因素,可以通过为经常访问的列添加索引来提高查询效率。但过多或者不必要的索引会对写入操作造成影响,需要权衡索引数量和查询需求。

  2. SQL 优化:优化 SQL 查询语句可以减少数据库的负载,提高查询效率。如避免全表扫描、尽量使用简单的 JOIN、使用 UNION ALL 代替 UNION 等。

  3. 数据库连接池优化:连接池是数据库连接管理的关键组件,可以通过调整最大连接数和空闲连接回收时间等参数来优化连接池性能。

  4. 分库分表:将数据存储在多个独立的数据库中,可以降低单个数据库的压力,提高并发处理能力。

  5. 缓存优化:缓存技术可以将热点数据存储在内存中,避免频繁的数据库读取操作,提高系统响应速度和并发性能。

  6. 定期清理无用数据:及时清理无用数据可以释放数据库空间,提高查询和更新效率,同时也可以降低备份和恢复的时间和成本。

  7. 数据库安全性优化:保障数据的安全性和完整性是任何数据库系统必须考虑的重要问题,可以通过限制访问权限、使用加密技术等手段来提高数据库的安全性。

65.事务原理

事务的实现原理可以用以下四个关键字来描述:

  1. 开始事务(BEGIN):在事务开始之前,需要使用 BEGIN 关键字开启一个事务,并初始化相关参数。

  2. 执行操作(EXECUTE):在事务中执行一系列数据库操作,例如 SELECT、INSERT、UPDATE、DELETE 等操作。这些操作会被记录到一个日志缓冲区中,同时也会在数据库中进行相应的修改。

  3. 提交事务(COMMIT):如果在执行操作过程中没有出现错误,那么可以使用 COMMIT 关键字提交事务,将所有修改操作永久写入数据库。

  4. 回滚事务(ROLLBACK):如果在执行操作过程中发生了错误或者其他异常情况,那么可以使用 ROLLBACK 关键字回滚事务,撤销所有已经执行的操作,并恢复到事务开始之前的状态。

在实际应用中,为了提高事务的并发性能和可靠性,通常会采用以下两种技术手段:

  1. 乐观锁(Optimistic Locking):在事务开始时,记录数据的版本号或者时间戳等信息;在提交事务时,检查数据的版本号或者时间戳是否一致。如果不一致,则说明在该事务执行期间有其他事务对数据进行了修改,并且已经提交。此时,当前事务需要回滚并重新执行。

  2. 悲观锁(Pessimistic Locking):在事务开始时,使用 SELECT ... FOR UPDATE 或者 SELECT ... LOCK IN SHARE MODE 等 SQL 语句对要修改的数据进行加锁,防止其他事务同时修改同一行数据。在事务提交或者回滚后,释放相应的锁。

总之,事务是保证数据库操作的一致性和可靠性的重要机制。在实际应用中,需要结合具体的业务需求和性能特点,采用乐观锁、悲观锁等技术手段来提高事务的并发性能和可靠性

66.redis有一个key有过期时间,这时候一直有客户端的命令去set这个key,会有什么问题?

  1. 过期时间无效:如果客户端一直更新这个 key 的值,那么过期时间就会失效,从而导致这个 key 永不过期。

  2. 性能损耗:每次客户端更新这个 key 的值,都会导致 Redis 执行一次 SET 命令并将值写入内存。如果客户端频繁更新这个 key 的值,就会导致 Redis 内存频繁写入和回收操作,降低系统的性能表现。

  3. 内存泄漏:如果客户端更新这个 key 的值的速度远大于过期时间,那么 Redis 内存中可能会存在大量已经过期但是未被删除的 key,从而导致内存泄漏。这种情况下,需要使用 Redis 的内存淘汰策略或者手动清除过期的 key,以避免内存溢出等问题。

67.百万QPS登录怎么解决?

  1. 高性能硬件设备:首先需要使用高性能的硬件设备,例如高速网络接口卡、高速磁盘等,以满足海量请求的处理需求。

  2. 负载均衡:为了分摊并发请求的压力,可以采用负载均衡技术将传入的请求分散到多台服务器上进行处理。常见的负载均衡算法有轮询、随机、最少连接数等。

  3. 分布式缓存:使用分布式缓存技术(例如 Redis、Memcached 等)来缓存用户登录状态信息,减少数据库访问次数,提高系统响应速度和并发处理能力。

  4. 集群化部署:将系统部署在多台服务器上组成集群,实现分布式处理,提高系统的可用性和扩展性。需要注意避免数据不一致等问题。

  5. 异步处理:使用异步处理技术(例如消息队列、线程池等)来异步处理登录请求,降低系统的响应延迟和资源消耗,并提高系统的稳定性和抗压能力。

  6. 优化 SQL 查询:尽量避免复杂的 SQL 查询操作和大量的数据库读写操作,可以通过索引优化、分库分表等技术来提高数据库查询效率和吞吐量。

  7. 限流控制:在系统出现瞬时高并发请求时,需要采用限流措施(例如令牌桶、漏桶算法)来控制请求的并发数,保证系统不会因为请求过多导致崩溃或者响应变慢

68.final和finally和finalize的区别

1.final, finally 和 finalize 是三个完全不同的概念。final 用于声明不可变的变量,

2.finally 用于指定在 try-catch 结构中无论是否发生异常都将被执行的代码块,

3.而 finalize 是一个方法名,是 Object 类定义的一个方法,用于在垃圾回收器清除对象之前执行必要的操作。

69.调用System.gc后,Java内存会不会马上进行回收(只是隐约记得此命令能直接触发FullGC)

1.调用System.gc()方法只是向JVM发出一个垃圾回收的请求,但并不能保证会立即进行回收。实际上,JVM会根据自身的垃圾回收算法和策略来决定何时进行垃圾回收。

2.通常情况下,当Java内存达到一定阈值时,或者空闲内存不足时,JVM才会触发垃圾回收。此外,在程序执行期间,当某些对象变得不可达(即没有任何引用指向它们),也可能立即被回收。

3.但需要注意的是,过于频繁地调用System.gc()方法并不是一个好习惯,因为它会导致系统在垃圾回收方面花费过多的时间,从而影响程序的性能。建议只在确实需要强制进行垃圾回收时才调用该方法

70.乐观锁和悲观锁之间的区别(回答了,但是回答的不够明白)

1.乐观锁

乐观锁假设在访问共享资源时不会有其他线程修改该资源,因此不会主动加锁,而是在更新操作时检查数据是否被其他线程修改过。如果没有被修改,则更新成功;否则,需要回滚操作或重试更新。

一般情况下,乐观锁实现需要记录版本号或时间戳等信息,用于判断当前数据是否已经被其他线程修改。Java中,常用的乐观锁实现方式是CAS(Compare and Swap)算法,它是由硬件提供原子级别的操作,保证数据的一致性。

代码展示:

2.悲观锁

悲观锁认为在访问共享资源时,其他线程会修改该资源,因此它主动加锁来保证数据的一致性。Java中,常用的悲观锁实现方式是synchronized关键字和ReentrantLock类。

代码实现:

区别:

乐观锁和悲观锁的区别在于对锁机制的使用方式不同。乐观锁假设访问共享资源时不会有竞争,所以不主动加锁;而悲观锁则认为访问共享资源时会存在竞争,所以需要主动加锁来保证数据的一致性。

场景应用:

乐观锁通常适用于读操作频繁,写操作较少的场景,比如缓存等;而悲观锁则通常适用于写操作频繁的场景,比如数据库事务等

71.sql语言怎么实现乐观锁和悲观锁

1.乐观锁的实现:

(1)添加版本号字段:在表中增加一个版本号字段 version,用于记录数据更新次数。

(2)获取当前版本号:在查询数据时,将版本号一同返回给应用程序。

(3)更新数据时比较版本号:在更新数据时,先比较当前版本号是否与数据库中的版本号一致。如果一致,则执行更新操作,并将版本号+1;否则表示有其他线程修改了数据,需要进行相应的处理。

2.悲观锁的实现:

(1)使用 SELECT ... FOR UPDATE 或者 SELECT ... FOR SHARE:在查询数据时,使用 SELECT ... FOR UPDATE 或者 SELECT ... FOR SHARE 语句来对查询结果进行加锁,以避免其他事务同时访问或修改此数据。

(2)执行事务:在事务中执行读取和更新操作,然后提交事务。在提交事务之前,这个事务会一直持有该行数据的锁,其他事务无法修改此数据。

总之,在 SQL 语言中实现乐观锁和悲观锁需要根据具体业务需求选择不同的实现方式,如使用版本号机制实现乐观锁或使用 SELECT ... FOR UPDATE 或者 SELECT ... FOR SHARE 实现悲观锁等。但是需要注意的是,过度的乐观锁或悲观锁都可能影响系统性能,因此需要进行合理的权衡和优化。

72.为什么用JWT,常见的实现登录的方式有哪些(cookie+session,redis+token,JWT)

JWT(JSON Web Token)是一种用于身份验证的标准,并且在现代应用程序中被广泛使用。它将用户的身份和授权信息加密后封装成一个 token,方便在客户端和服务端之间进行传输和验证,具有以下优点:

  1. 无状态:JWT 将用户的身份和授权信息封装成 token,不需要在服务器端保存 session 等状态信息。

  2. 跨平台性:由于 JWT 是基于 JSON 格式的标准,因此可以在多种编程语言和平台上进行解析和生成。

  3. 安全性:JWT 使用了强大的算法对 token 进行签名或加密,保证了数据的安全性和完整性。

常见的实现登录的方式有三种:

  1. 基于 cookie+session 的认证方式:用户登录后,服务端在 cookie 中记录 session id,并将 session 数据存储在服务器端,以后每次请求都带上 session id,服务端就可以根据 session id 找到对应的 session 数据。

  2. 基于 redis+token 的认证方式:用户登录后,服务端生成一个 token,并将 token 存储在 redis 中,以后每次请求都带上 token,服务端就可以根据 token 找到对应的用户信息。

  3. 基于 JWT 的认证方式:用户登录后,服务端生成一个 JWT token,并将用户的身份和授权信息加密到 token 中,发送给客户端。以后每次请求都带上 token,服务端就可以解密出用户信息进行认证。

JWT 与其他方式相比,具有无状态、跨平台性和易于扩展性等优点,并且可以支持多种算法进行签名或加密,是一种非常优秀的身份验证机制

74.cookie和session的区别(按照四个方面说了,位置,安全性,生命周期,保存数据类型)

Cookie和Session都是Web应用程序中常见的身份验证机制,但它们的实现方式略有不同。

  • 位置:Cookie存储在客户端浏览器中,而Session存储在服务器端。
  • 安全性:由于Cookie存储在客户端,因此容易受到恶意攻击。Session相对更加安全,因为其存储在服务器端,并且可以通过加密等方法进行保护。
  • 生命周期:Cookie可以设置一个固定的过期时间,也可以在浏览器关闭后自动删除。Session则可以在服务器端设置过期时间,也可以在用户退出或关闭浏览器后删除。
  • 保存数据类型:Cookie只能存储字符串类型数据,而Session可以存储任何类型的数据。

75.cookie中存了什么样的信息,可以判断用户有否处于登录态(Session ID)

​​​​​​​1.​​​​​​​Cookie通常用于存储少量的信息,例如用户ID、过期时间等。如果系统是基于Session实现的,则通常会将一个Session ID存储在Cookie中,以便服务器端能够识别并验证用户的身份。

2.通过检查Cookie中是否存在有效的Session ID来判断用户是否处于登录态。如果Session ID已经失效或者被篡改,则需要重新登录。

76.cookie被拿到以后,换一个机器访问,可以访问吗

1.如果用户的 cookie 被拿到后,换一个机器访问是无法直接访问的。

2.因为 cookie 是保存在客户端浏览器的本地存储中的,

3.不同的机器上使用的是不同的浏览器本地存储,所以不能直接访问。

4.但是如果攻击者能够获得用户的 cookie,则可以冒充该用户进行一些操作,例如进行 CSRF(跨站请求伪造)攻击等

77.cookie被伪造的话,会导致什么问题,怎么解决

  1. 使用 HttpOnly 属性:设置 HttpOnly 属性后,浏览器将禁止 JavaScript 访问 cookie,减少了被 XSS 注入攻击盗取 cookie 的风险。

  2. 设置 Secure 属性:设置 Secure 属性后,cookie 只能通过 HTTPS 协议传输,避免了中间人窃听和篡改数据的风险。

  3. 将敏感信息存储在服务器端:应该把敏感的信息存储在服务器端,而不是保存在 cookie 中或者通过客户端传输。这样即使 cookie 被伪造,攻击者也无法获取到敏感信息。

  4. 合理设置 cookie 的过期时间:合理设置 cookie 的过期时间,在一定程度上可以减少被盗用的风险。

  5. 采用专业的加密技术:使用专业的加密技术对 cookie 进行加密,以防止数据泄露和被篡改。

总之,在保护 cookie 安全方面应该采取多种措施,包括设置 HttpOnly、Secure 属性、将敏感信息存储在服务器端、合理设置 cookie 的过期时间以及采用专业的加密技术等。

79.流量削峰怎么做?

流量削峰是一个常见的系统性能优化技术,目的是尽可能地避免系统因为突发的高并发流量而崩溃。具体做法包括以下几个方面:

  1. 缓存:使用缓存可以减轻对数据库或其他后端服务的流量压力。可以将热点数据放置在 Redis 等缓存中,降低对数据库的访问频率。

  2. 队列:通过消息队列来实现流量削峰,把请求持久化到消息队列中,再按照一定的规则进行消费。这样可以将短时间内的流量进行平滑处理,避免瞬间的高并发导致系统宕机。

  3. 限流:限流可以控制系统的最大并发数,防止超出系统承载能力。可以采用令牌桶、漏桶等算法对请求进行限流。

  4. 异步处理:将一些处理量较大的业务异步化处理,例如图片上传、邮件发送等操作。这样可以将大量的计算和 I/O 操作移至异步线程中,释放主线程的资源。

  5. 分层架构:对于高并发的系统,可以采用分层架构以及微服务架构,将任务分配到不同的节点上,实现负载均衡。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

网友小浩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值