1. 1. 1 Java语言基础
    1. 1.1. 1.1 面向对象与Java基础
      1. 1.1.1. 1.1.1 基础概念
        1. 1.1.1.1. JDK,JVM,JRE
        2. 1.1.1.2. Java C++ Go的区别
        3. 1.1.1.3. Java是编译型语言吗?
        4. 1.1.1.4. JDK17有哪些新特性?为什么许多从JDK1.8转到JDK17的?
    2. 1.2. 许多开发者从 JDK 1.8 转到 JDK 17 的原因如下:1. 新特性的吸引力: - 代码简洁性提升:JDK 17 中的局部变量类型推断、增强的 switch 表达式、文本块和记录类等特性,使代码更加简洁易读,减少了样板代码的编写,提高了开发效率。例如,记录类可以快速定义数据类,无需手动编写 toString()、equals() 和 hashCode() 等方法,大大简化了代码5。 - 更强大的功能:密封类可以更好地控制类的继承关系,增强了类型安全;模式匹配的改进使类型检查和转换更加方便;新的 API 和功能增强为开发者提供了更多的工具和选择,方便解决各种复杂的问题6。2. 性能优化: - 垃圾回收改进:JDK 17 对垃圾回收器的优化可以减少垃圾回收的停顿时间,提高应用程序的响应速度和吞吐量,对于大型应用程序和对性能要求较高的场景非常重要。例如,ZGC 的性能提升可以更好地满足高并发、低延迟的业务需求。 - 并发性能提升:对并发相关的改进使得多线程应用程序的性能和并发处理能力得到提高,能够更好地利用多核处理器的性能,提高程序的执行效率5。3. 安全性增强:JDK 17 在安全方面的改进,如反射权限控制和包扫描权限控制的增强,有助于开发者构建更安全的应用程序,保护用户数据和系统安全。在当前对软件安全要求越来越高的背景下,这是一个重要的考虑因素。4. 长期支持:JDK 17 是一个长期支持(LTS)版本,会得到较长时间的维护和更新,这对于企业级应用和长期运行的项目来说非常重要。开发者可以放心地使用 JDK 17 进行开发,不用担心版本的快速迭代和缺乏长期支持6。5. 生态系统和工具支持:随着 JDK 17 的广泛应用,相关的开发工具和框架也逐渐对其进行了支持和优化。主流的 IDE(如 IntelliJ IDEA、Eclipse 等)都提供了对 JDK 17 的良好支持,开发者可以享受到更好的开发体验和工具支持。同时,Java 生态系统中的各种库和框架也在不断适配 JDK 17,使得开发者能够使用最新的技术和工具来构建应用程序35。6. 技术发展和行业趋势:Java 技术在不断发展,新的版本会带来更好的性能、更多的功能和更高的安全性。随着时间的推移,越来越多的项目开始采用新的 JDK 版本,开发者也需要不断学习和掌握新的技术,以保持竞争力。从 JDK 1.8 升级到 JDK 17 是顺应技术发展和行业趋势的选择3。### 1.1.2 数据类型#### 变量命名规范1. 标识符:变量名必须是一个有效的Java标识符,这意味着它必须以字母、美元符号($)或下划线( _ )开始,后续字符可以是字母、数字、美元符号或下划线。2. 保留字:变量名不能是Java的保留字,例如 class、public、private 等。3. 大小写敏感:Java是大小写敏感的语言,因此 variable、Variable 和 VARIABLE 是三个不同的变量名。4. 数字开头:变量名不能以数字开头。#### Java有哪些基本数据类型?Java 中有 8 种基本数据类型,分别为:- 6 种数字类型: - 4 种整数型:byte、short、int、long - 2 种浮点型:float、double- 1 种字符类型:char- 1 种布尔型:boolean。
      1. 1.2.0.1. 基本数据类型和包装数据类型有什么区别?
      2. 1.2.0.2. 什么装箱和拆箱?
  • 2. 装箱(Boxing)是指将基本数据类型转换为对应的包装类对象的过程。拆箱(Unboxing)是指将包装类对象转换回基本数据类型的过程。在 Java 中,自动装箱和拆箱是语言特性,允许基本类型和它们的包装类之间隐式转换。1234567891011121314151617181920public class BoxingUnboxingExample { public static void main(String[] args) { // 装箱:将基本数据类型 int 转换为包装类 Integer int primitiveInt = 100; Integer wrapperInt = Integer.valueOf(primitiveInt); // 显式装箱 // 或者使用自动装箱 Integer autoBoxedInt = primitiveInt; // 拆箱:将包装类 Integer 转换为基本数据类型 int int unboxedInt = wrapperInt.intValue(); // 显式拆箱 // 或者使用自动拆箱 int autoUnboxedInt = autoBoxedInt; System.out.println("原始int值: " + primitiveInt); System.out.println("装箱后的Integer对象: " + wrapperInt); System.out.println("拆箱后的int值: " + unboxedInt); System.out.println("自动装箱和拆箱后的int值: " + autoUnboxedInt); }}在 Java 5 及以后的版本中,编译器会自动处理装箱和拆箱,不需要显式调用 valueOf() 和 intValue() 方法。#### == 和equeals hashcode
    1. 2.0.0.1. == 和equals的区别是什么?分别在什么场景下使用?
    2. 2.0.0.2. Java中的参数传递是值传递还是引用传递?
    3. 2.0.0.3. 深拷贝、浅拷贝以及引用拷贝
  • 2.0.1. 1.1.3 面向对象
    1. 2.0.1.1. 面向对象三大特点
    2. 2.0.1.2. Java创建对象的方式
    3. 2.0.1.3. 接口和抽象类有什么共同点和区别?
    4. 2.0.1.4. 为什么Java不支持多重继承?
    5. 2.0.1.5. 什么是内部类?有什么作用?
    6. 2.0.1.6. Consumer接口
    7. 2.0.1.7. Java方法的重载和重写有什么区别?
    8. 2.0.1.8. Enum枚举类,其能否被继承?
    9. 2.0.1.9. 什么是函数式接口?
    10. 2.0.1.10. Java 8 中新增的常用函数式接口
  • 2.0.2. 1.1.4 修饰符
    1. 2.0.2.1. 变量修饰符作用范围
    2. 2.0.2.2. static 静态类 静态方法
    3. 2.0.2.3. final
  • 2.0.3. 1.1.4 String
    1. 2.0.3.1. 为什么String是不可变的?
    2. 2.0.3.2. String#equals() 和 Object#equals() 有何区别?
    3. 2.0.3.3. String,Stringbiulder,SrtingBuffer有什么区别?
    4. 2.0.3.4. StringBuilder是怎么实现的?
    5. 2.0.3.5. StringBuffer是如何实现线程安全的?
    6. 2.0.3.6. 使用 new String(“keriko”) 语句在 Java 中会创建多少个对象?
    7. 2.0.3.7. 字符串拼接 “+”与StringBuilder.append( )有什么区别?
  • 2.0.4. 1.1.5 其他
    1. 2.0.4.1. try-catch-finally的执行顺序
    2. 2.0.4.2. 是否一定执行 finally 块?
    3. 2.0.4.3. 什么是java中的异常处理
    4. 2.0.4.4. checked异常和unchecked异常有什么区别?
    5. 2.0.4.5. Exception和Error有什么区别?
    6. 2.0.4.6. 什么是注解?
    7. 2.0.4.7. 常见的注解
    8. 2.0.4.8. Java中的动态代理是什么?
    9. 2.0.4.9. 如何实现动态代理?
    10. 2.0.4.10. 了解 Java 的序列化和反序列化吗?能解释一下序列化的过程和作用吗?
    11. 2.0.4.11. 序列化的作用
    12. 2.0.4.12. 什么是反射机制?
    13. 2.0.4.13. 什么是SPI机制?
    14. 2.0.4.14. SPI和API的区别是什么?
    15. 2.0.4.15. 什么是Java的泛型?
    16. 2.0.4.16. SPI机制的缺陷?
    17. 2.0.4.17. BIO NIO AIO
    18. 2.0.4.18. NIO与普通IO的区别
    19. 2.0.4.19. 简单叙述一下Restful API
    20. 2.0.4.20. 什么是OOM(内存溢出),如何排查
  • 2.1. 1.2 集合类框架
    1. 2.1.0.1. Collection接口下面有哪些集合类_3?List, Set, Queue, Map 四者的区别?
  • 2.1.1. 1.2.1 ArrayList相关
    1. 2.1.1.1. ArrayList和LinkedList有什么区别?
    2. 2.1.1.2. ArrayList的扩容机制了解吗?
    3. 2.1.1.3. 什么是迭代器?
    4. 2.1.1.4. CopyOnWriteArrayList是如何保证线程安全的?
    5. 2.1.1.5. Java的CopyOnWriteArrayList 和Collections.synchronizedList 有什么区别?分别有什么优缺点?
  • 2.1.2. 1.2.2 HashMap相关
    1. 2.1.2.1. Hash函数的构造方法
    2. 2.1.2.2. Hash冲突的解决方法
    3. 2.1.2.3. HashMap的数据结构
    4. 2.1.2.4. HashMap和HashTable的区别
    5. 2.1.2.5. hashSet和HashMap的区别
    6. 2.1.2.6. 什么是负载因子?为什么HashMap的负载因子是0.75?
    7. 2.1.2.7. 使用HashMap时,有什么提升性能的技巧?
    8. 2.1.2.8. HashMap的扩容机制
    9. 2.1.2.9. HashMap的链表是头插法还是尾插法?
    10. 2.1.2.10. 在链表变为红黑树后,若元素数量低于8会变回来吗?
    11. 2.1.2.11. ConcurrentHashmap为什么安全?
    12. 2.1.2.12. ConcurrentHashmap的 put()操作流程
    13. 2.1.2.13. TreeMap LinkedhashMap WeakHashMap IdentityHashMap
  • 2.2. 1.3 java并发
    1. 2.2.1. 1.3.1 Java并发理论基础
      1. 2.2.1.1. 多线程的出现是要解决什么问题的?
      2. 2.2.1.2. 🌟线程不安全是指什么? 举例说明
      3. 2.2.1.3. 如何保证线程安全?有哪些方法?
      4. 2.2.1.4. 🌟线程安全的实现方法
      5. 2.2.1.5. sleep和wait方法的主要区别?使用时会对线程状态有什么影响
      6. 2.2.1.6. 解释 Java 中的线程,如何创建和启动线程?
      7. 2.2.1.7. 为什么调用start()方法时会执行run()方法,那怎么不直接调用run()方法?
      8. 2.2.1.8. 写一个程序,两个线程交替打印 hello world
      9. 2.2.1.9. ThreadLocal是什么?
      10. 2.2.1.10. ThreadLocal原理?
      11. 2.2.1.11. ThreadLocalMap了解吗?
      12. 2.2.1.12. 谈谈你对Java内存模型JMM的理解
    2. 2.2.2. 各种关键字及锁
      1. 2.2.2.1. 什么是volatile关键字
      2. 2.2.2.2. volatile的底层原理是什么?
      3. 2.2.2.3. volatile保证操作的原子性吗?
      4. 2.2.2.4. 乐观锁与悲观锁在Java中如何实现
      5. 2.2.2.5. CAS(比较并交换)
      6. 2.2.2.6. CAS操作存在的问题
      7. 2.2.2.7. Synchronized的实现原理及使用方式
      8. 2.2.2.8. ReentrantLock实现原理?
      9. 2.2.2.9. synchronized和reentrantlock的区别
      10. 2.2.2.10. 自旋锁
      11. 2.2.2.11. AQS
      12. 2.2.2.12. CountDownLatch
      13. 2.2.2.13. CyclicBarrier(同步屏障)了解吗?
      14. 2.2.2.14. Semaphore
      15. 2.2.2.15. AtomicInteger
    3. 2.2.3. 线程池
      1. 2.2.3.1. 什么是线程池?
      2. 2.2.3.2. 为什么要用线程池?
      3. 2.2.3.3. 线程池的工作流程
      4. 2.2.3.4. 如何创建线程池?
      5. 2.2.3.5. 线程池有哪些常见参数?
      6. 2.2.3.6. 线程池的拒绝策略有哪些?
      7. 2.2.3.7. 线程池设计场景题
  • 2.3. 1.4 JVM
    1. 2.3.0.1. JVM 的内存区域是如何划分的?
    2. 2.3.0.2. 对象创建的过程了解吗?
    3. 2.3.0.3. Java中堆和栈的区别是什么?
    4. 2.3.0.4. Java的类加载过程
    5. 2.3.0.5. Java类的生命周期
    6. 2.3.0.6. 什么是双亲委派机制?
    7. 2.3.0.7. 为什么要使用双亲委派机制?
    8. 2.3.0.8. Tomcat的类加载机制
  • 2.3.1. 1.4.3 JVM GC(垃圾回收机制)
    1. 2.3.1.1. 简单讲讲Java的垃圾回收机制
    2. 2.3.1.2. 什么是Java里的垃圾回收?如何触发垃圾回收?
    3. 2.3.1.3. Full GC在什么时候触发?
    4. 2.3.1.4. Java堆的内存分区了解吗?
    5. 2.3.1.5. 为什么 Java 的垃圾收集器将堆分为老年代和新生代?
    6. 2.3.1.6. 对象的四种引用方式 强、软、弱、虚
    7. 2.3.1.7. Java 中常见的垃圾收集器有哪些?
    8. 2.3.1.8. Java 中如何判断对象是否是垃圾?不同垃圾回收方法有何区别?
    9. 2.3.1.9. Java 中有哪些垃圾回收算法?
    10. 2.3.1.10. 常用的 JVM 配置参数有哪些?
  • 3. 2 计算机网络
    1. 3.1. 2.1 计算机网络–综合
      1. 3.1.0.1. 从输入网址到获得页面的过程
      2. 3.1.0.2. 扫二维码到进入页面的过程发生了什么
      3. 3.1.0.3. 讲讲DNS的解析过程
      4. 3.1.0.4. ISO/OSI 七层模型与TCP/IP四层模型
      5. 3.1.0.5. 数据在各层之间是怎么传输的呢?
      6. 3.1.0.6. 常见协议工作层次及端口
      7. 3.1.0.7. 层次结构中端到端通信有哪几层?端到端通信和点到点通信有什么区别?
  • 3.2. 2.2 TCP篇
    1. 3.2.1. 2.2.1 TCP基础
      1. 3.2.1.1. 简述TCP是什么?
    2. 3.2.2. 2.2.2 TCP与UDP
      1. 3.2.2.1. TCP和UDP的报文头
      2. 3.2.2.2. TCP和UDP的区别
      3. 3.2.2.3. TCP 和 UDP 应用场景:
      4. 3.2.2.4. 除了常见的拥塞控制、滑动窗口等机制外,TCP还有什么机制可以保证可以?比如报文上的一些检验等?
      5. 3.2.2.5. TCP为什么安全?
      6. 3.2.2.6. tcp十六位校验和怎么实现的?
      7. 3.2.2.7. tcp 粘包问题 (拆包和分包)
      8. 3.2.2.8. 不同协议能否监听同一个端口?
      9. 3.2.2.9. 常见TCP的连接状态有哪些?
    3. 3.2.3. 2.2.3 TCP连接建立
      1. 3.2.3.1. TCP连接建立过程(三次握手)
      2. 3.2.3.2. 为什么是三次握手?
      3. 3.2.3.3. 为什么不能用两次握手进行连接?
      4. 3.2.3.4. 为什么不是四次握手?
      5. 3.2.3.5. 第一、二、三次握手丢失了,分别会发生什么?
      6. 3.2.3.6. 如果已经建立了连接,但是客户端/服务端突然出现故障了怎么办?
      7. 3.2.3.7. TCP SYN攻击
    4. 3.2.4. 2.2.4 TCP连接断开
      1. 3.2.4.1. TCP连接断开过程
      2. 3.2.4.2. 为什么连接的时候是三次握手,关闭的时候却是四次挥手?
      3. 3.2.4.3. 第1/2/3/4次挥手丢失了,会发生什么?
      4. 3.2.4.4. timewait状态
      5. 3.2.4.5. 为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
      6. 3.2.4.6. TIME_WAIT 状态过多会导致什么问题?怎么解决?
      7. 3.2.4.7. closewait状态
      8. 3.2.4.8. 保活计时器有什么用?
    5. 3.2.5. 2.2.5 TCP流量控制
      1. 3.2.5.1. 流量控制的原理
      2. 3.2.5.2. 说说TCP的流量控制及滑动窗口原理?
      3. 3.2.5.3. 流量控制方法
    6. 3.2.6. 2.2.6 TCP拥塞避免
      1. 3.2.6.1. 拥塞避免与流量控制的区别是什么?
      2. 3.2.6.2. TCP采用了哪些机制来保证拥塞避免?
      3. 3.2.6.3. TCP有哪些重传机制?
      4. 3.2.6.4. TCP有哪些问题?你能提供一些解决思路吗?
    7. 3.2.7. 6. 安全性问题
      1. 3.2.7.1. 简述 TCP 的粘包问题及解决方案。
    8. 3.2.8. 2.2.7 UDP
      1. 3.2.8.1. 为什么QQ采用UDP协议?
      2. 3.2.8.2. 为什么域名解析用UDP协议而不用TCP?
      3. 3.2.8.3. QUIC (快速UDP网络传输协议)
  • 3.3. 2.3 HTTP
    1. 3.3.0.1. HTTP 请求的过程与原理?
    2. 3.3.0.2. HTTP有哪些数据请求方式?
    3. 3.3.0.3. GET和POST的区别
    4. 3.3.0.4. GET的长度限制?
    5. 3.3.0.5. POST比GET安全吗?
    6. 3.3.0.6. 常见的HTTP状态码有哪些?
    7. 3.3.0.7. 解释什么是 HTTP 的无状态性,如何在应用层维护状态。
    8. 3.3.0.8. http 报文介绍;http的请求头
    9. 3.3.0.9. URI和URL有什么区别?
    10. 3.3.0.10. 分别介绍 http 1.1 2.0 3.0
    11. 3.3.0.11. 什么是长连接和短连接?它们各有什么优缺点?
    12. 3.3.0.12. HTTP 如何实现长连接?在什么时候会超时?
    13. 3.3.0.13. http的keep-alive机制?
    14. 3.3.0.14. HTTPS的过程
    15. 3.3.0.15. 为什么需要 非对称加密 和 对称加密?
    16. 3.3.0.16. http和https的区别
    17. 3.3.0.17. HTTPS 解决了 HTTP 的哪些问题
    18. 3.3.0.18. HTTPS 是如何解决Http的三个风险的?
    19. 3.3.0.19. 客户端怎么去校验证书的合法性?
    20. 3.3.0.20. https一定是安全的吗,会被中间人攻击吗?
    21. 3.3.0.21. 什么是中间人攻击?
    22. 3.3.0.22. 常见的网络攻击原理及方式(xss、csrf、ddos)
    23. 3.3.0.23. Cookies和Session的区别,如何选择?
    24. 3.3.0.24. JWT Token是什么?
    25. 3.3.0.25. JWT验证过程
  • 3.4. 2.4 其他层
    1. 3.4.0.1. CDN (IP和应用)
    2. 3.4.0.2. VLAN (IP层)
    3. 3.4.0.3. TTL在网络通信中的作用 (IP层)
    4. 3.4.0.4. 路由器和交换机的区别?
    5. 3.4.0.5. TCP, UDP对应的应用层协议
    6. 3.4.0.6. UDP(用户数据报协议)对应的应用层协议:
    7. 3.4.0.7. UDP的广播和多播
  • 3.4.1. UDP多播
    1. 3.4.1.1. DNS地址解析的过程(应用层)
    2. 3.4.1.2. DHCP 动态主机配置协议(应用层)
    3. 3.4.1.3. ARP 地址解析协议(数据链路层)
    4. 3.4.1.4. 什么是IP协议?
    5. 3.4.1.5. 路由选择协议(RIP,OSPF,BGP 网络层)
    6. 3.4.1.6. 子网掩码,划分子网
  • 3.5. 2.5 网络安全
    1. 3.5.0.1. 简单说说有哪些常见的网络安全攻击?
    2. 3.5.0.2. 什么是DNS劫持?如何应对?
    3. 3.5.0.3. 什么是 CSRF 攻击?如何避免?
    4. 3.5.0.4. 什么是 DoS、DDoS、DRDoS 攻击?
    5. 3.5.0.5. SYN_FLOOD
    6. 3.5.0.6. 什么是 XSS 攻击,如何避免?
    7. 3.5.0.7. 有了解SQL注入吗?如何避免?
    8. 3.5.0.8. 对称加密与非对称加密有什么区别?
    9. 3.5.0.9. 简单讲讲AES和RSA?
    10. 3.5.0.10. 平时在进行Web开发时,应当怎样保证网络安全?
  • 4. 3 计算机系统
    1. 4.1. 3.1 操作系统–综合
      1. 4.1.0.1. 操作系统的四大特性
      2. 4.1.0.2. 操作系统的主要功能
      3. 4.1.0.3. 用户态与内核态
      4. 4.1.0.4. 宏内核与微内核
      5. 4.1.0.5. 操作系统中用到的数据结构
      6. 4.1.0.6. 操作系统中的调度算法
      7. 4.1.0.7. 系统调用
      8. 4.1.0.8. 讲讲Linux操作系统的启动过程
  • 4.2. 3.2 进程管理
    1. 4.2.0.1. 并发和并行有什么区别?
    2. 4.2.0.2. 进程、线程(用户级、内核级)的区别
  • 4.2.1. 3.2.1 进程
    1. 4.2.1.1. 什么是进程调度(上下文切换)?
    2. 4.2.1.2. 进程的状态模型
    3. 4.2.1.3. 有哪些进程调度算法?
    4. 4.2.1.4. 进程间有哪些通信方式?
    5. 4.2.1.5. 什么是僵尸进程?
    6. 4.2.1.6. 什么是孤儿进程?
    7. 4.2.1.7. 一个进程崩溃会对其他进程产生很大影响吗?
  • 4.2.2. 3.2.2 线程
    1. 4.2.2.1. 线程上下文切换有了解吗?
    2. 4.2.2.2. 线程有哪些实现方式?
    3. 4.2.2.3. 线程间有哪些通信方式?
  • 4.2.3. 3.2.3 进程同步模型
    1. 4.2.3.1. 什么是同步,互斥?
    2. 4.2.3.2. 怎么解决进程同步问题?
    3. 4.2.3.3. 经典的进程同步问题
  • 4.2.4. 3.2.4 死锁
    1. 4.2.4.1. 死锁怎么产生的?怎么避免?
    2. 4.2.4.2. 如何解决死锁?
    3. 4.2.4.3. 如何避免死锁?
    4. 4.2.4.4. 3.2.5 中断
    5. 4.2.4.5. 中断的作用是什么?
    6. 4.2.4.6. Linux中异常和中断的区别?
    7. 4.2.4.7. 讲讲中断的流程
    8. 4.2.4.8. 中断的类型有哪些?
  • 4.3. 3.3 内存管理
    1. 4.3.0.1. 计算机的存储结构
    2. 4.3.0.2. 虚拟内存(逻辑地址–>物理地址)
    3. 4.3.0.3. 一页为什么是 4 KB?
    4. 4.3.0.4. 为什么要使用虚拟内存?
    5. 4.3.0.5. 段页式内存管理
    6. 4.3.0.6. 虚拟内存的实现方式
    7. 4.3.0.7. 🌟分页与分段的区别
    8. 4.3.0.8. 多级页表与TLB快表
    9. 4.3.0.9. 内存满了,会发生什么?
    10. 4.3.0.10. 有哪些页面置换算法?
    11. 4.3.0.11. 抖动:频繁的页面调度行为
  • 4.4. 3.4 文件系统
    1. 4.4.0.1. 硬链接和软链接有什么区别?
    2. 4.4.0.2. 简单介绍一下零拷贝
    3. 4.4.0.3. 磁盘调度算法
  • 4.5. 3.4 (网络)I/O系统
    1. 4.5.0.1. 🌟什么是I/O多路复用?
    2. 4.5.0.2. select poll 和 epoll 最大的差别是什么?
    3. 4.5.0.3. 阻塞/非阻塞式IO
    4. 4.5.0.4. 同步/异步 IO
    5. 4.5.0.5. 一次普通 IO 的过程,什么时候用到了系统调用?
  • 4.6. 3.5 Linux
    1. 4.6.0.1. 🌟如何在 Linux 中查看系统资源使⽤情况?⽐如内存、CPU、⽹络端⼝
    2. 4.6.0.2. 如何查看CPU的硬件信息
    3. 4.6.0.3. 系统相关(运行级别,关机,重启)
    4. 4.6.0.4. 查看/关闭进程
    5. 4.6.0.5. 网络相关
    6. 4.6.0.6. 创建、复制、移动和删除文件或目录?
    7. 4.6.0.7. 权限系统
    8. 4.6.0.8. find 查找
    9. 4.6.0.9. swap
    10. 4.6.0.10. 文件查看
    11. 4.6.0.11. 文件系统挂载
    12. 4.6.0.12. 用户组
    13. 4.6.0.13. Rsync
    14. 4.6.0.14. 日志
    15. 4.6.0.15. 查询修改时间
    16. 4.6.0.16. 管道符和重定向
    17. 4.6.0.17. Linux内核版本与发行版本的区别是什么?
    18. 4.6.0.18. Linux中的硬链接与软链接
    19. 4.6.0.19. top指令的底层原理是什么?
    20. 4.6.0.20. CVM、ECS、轻量级应用服务器等虚拟机、Docker的区别
    21. 4.6.0.21. 不同系统下可以用同样的Docker镜像吗?
    22. 4.6.0.22. Linux如何查看哪些端口建立tcp连接?这些端口都有哪些状态?
    23. 4.6.0.23. 操作系统的临界区
  • 4.7. 3.6 组原拾遗
    1. 4.7.0.1. 大端和小端有什么区别?
    2. 4.7.0.2. 计算机的特点
  • 4.8. 3.7 数据结构常考
    1. 4.8.0.1. KMP算法
    2. 4.8.0.2. 中缀表达式如何转为后缀表达式
    3. 4.8.0.3. 二叉树的常用结论
    4. 4.8.0.4. N 叉树的常用结论
    5. 4.8.0.5. 平衡二叉树
    6. 4.8.0.6. 哈夫曼树
    7. 4.8.0.7. 排序算法比较
    8. 4.8.0.8. 快速排序
    9. 4.8.0.9. 每一轮排序算法的特征
    10. 4.8.0.10. 堆排序
    11. 4.8.0.11. 普利姆 克鲁斯卡尔 和迪杰斯特拉算法分别是什么?
  • 5. 4 关系型数据库 (MySQL)
    1. 5.1. 4.1 SQL 基础
      1. 5.1.0.1. 什么是内连接、外连接、交叉连接、笛卡尔积呢?
      2. 5.1.0.2. 说一下数据库的三大范式?
      3. 5.1.0.3. 常用SQL语句
      4. 5.1.0.4. varchar与char的区别?
      5. 5.1.0.5. where 和 Having的区别
      6. 5.1.0.6. in exists
      7. 5.1.0.7. drop、delete与truncate的区别?
  • 5.2. 4.2 MySQL 基础
    1. 5.2.0.1. MySQL 数据库命名规范
    2. 5.2.0.2. 执行一条SQL语句,期间发生了什么?
    3. 5.2.0.3. 🌟1条SQL语句的执行顺序?
    4. 5.2.0.4. InnoDB 和 MyISAM有什么区别?
  • 5.3. 4.3 索引
    1. 5.3.0.1. 使用索引的最佳实践
    2. 5.3.0.2. 什么是索引?
    3. 5.3.0.3. 不同角度的索引类型分类?(B+;聚簇;主键)
    4. 5.3.0.4. 索引底层数据类型有哪些?
    5. 5.3.0.5. B树与B+树的区别?
    6. 5.3.0.6. 数据库索引为什么选择B+树?
    7. 5.3.0.7. MySQL里主键索引为什么比其他索引快?
    8. 5.3.0.8. ⚠️从数据页的角度看B+树?
    9. 5.3.0.9. 聚簇索引与非聚簇(二级)索引
    10. 5.3.0.10. 覆盖索引
    11. 5.3.0.11. 什么是回表?怎么减少回表?⚠️回表出现错误怎么办?
    12. 5.3.0.12. 联合索引最左匹配原则
    13. 5.3.0.13. 联合索引实例
    14. 5.3.0.14. 索引下推
    15. 5.3.0.15. 为什么使用索引会加快查询?
    16. 5.3.0.16. 什么时候需要 / 不需要创建索引?
    17. 5.3.0.17. 创建索引有哪些注意点?
    18. 5.3.0.18. ⚠️什么情况下会索引失效?
    19. 5.3.0.19. 索引的优缺点
    20. 5.3.0.20. 使用索引一定会加快查询速度吗?
    21. 5.3.0.21. 选择哪个字段作为索引?
    22. 5.3.0.22. 怎么知道MySQL 有没有利用索引?
  • 5.4. 4.4 事务
    1. 5.4.0.1. 事务的ACID四大特性?
    2. 5.4.0.2. 事务的关键字
    3. 5.4.0.3. InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?
    4. 5.4.0.4. 事务的并发一致性问题
    5. 5.4.0.5. 幻读是什么,如何解决?
    6. 5.4.0.6. 不可重复读和幻读的区别?
    7. 5.4.0.7. SQL事务隔离级别
    8. 5.4.0.8. 事务的各个隔离级别都是如何实现的?
    9. 5.4.0.9. MySQL的默认隔离级别
    10. 5.4.0.10. 为了解决并发一致性问题,并发事务的控制方式
    11. 5.4.0.11. ⚠️ 讲一讲MVCC?
    12. 5.4.0.12. 当前读与快照读?
    13. 5.4.0.13. 当前读,快照读和MVCC之间是什么关系呢?
    14. 5.4.0.14. MVCC能解决什么问题,好处是什么?
    15. 5.4.0.15. MVCC的实现原理?
    16. 5.4.0.16. Read View 在 MVCC 里如何工作的?
    17. 5.4.0.17. 可重复读的MVCC实现
    18. 5.4.0.18. 已提交读的MVCC实现
    19. 5.4.0.19. MVCC能保证不产生幻读吗?
  • 5.5. 4.5 锁
    1. 5.5.0.1. 锁的分类
    2. 5.5.0.2. 悲观锁和乐观锁
    3. 5.5.0.3. 共享锁和排他锁
    4. 5.5.0.4. 意向锁(表级锁)
    5. 5.5.0.5. 表级锁和行级锁
    6. 5.5.0.6. InnoDB 有哪几类行锁?
    7. 5.5.0.7. 什么是数据库死锁,如何避免死锁?
  • 5.6. 4.6 日志
    1. 5.6.0.1. MySQL日志文件有哪些?分别介绍下作用?
    2. 5.6.0.2. binlog 和 redolog有什么区别?
    3. 5.6.0.3. 一条更新语句怎么执行的了解吗?
    4. 5.6.0.4. 为什么更新语句需要两段提交?
    5. 5.6.0.5. (区分)分布式事务的两阶段提交
    6. 5.6.0.6. redo log 输入磁盘
  • 5.7. 4.7 SQL优化
    1. 5.7.0.1. 慢SQL如何定位呢?
    2. 5.7.0.2. 数据库调优–explain关注哪些字段
    3. 5.7.0.3. mysql 有什么加快查询的方法?如何进行性能优化?
    4. 5.7.0.4. 数据库备份
    5. 5.7.0.5. 数据库连接池
    6. 5.7.0.6. 数据库水平分区以及垂直分区
    7. 5.7.0.7. 数据库调优
    8. 5.7.0.8. 数据库分片
    9. 5.7.0.9. 什么是数据库约束,列举一下常见的数据库约束
    10. 5.7.0.10. 不加索引怎么优化连接操作?
    11. 5.7.0.11. 主从同步(异步,半同步)
    12. 5.7.0.12. 海量数据存SQL该如何优化?
    13. 5.7.0.13. 分库分表可能造成的问题
    14. 5.7.0.14. 慢查询优化基本步骤
  • 6. 5 SSM(JavaWeb)
    1. 6.1. 5.1 Spring
      1. 6.1.0.1. Spring有哪些特性呢?
      2. 6.1.0.2. 简单讲讲AOP和IOC
      3. 6.1.0.3. Spring用到了哪些设计模式?
      4. 6.1.0.4. Spring 中的Bean是单例吗?
      5. 6.1.0.5. 说一说什么是IOC?什么是DI?
      6. 6.1.0.6. 能简单说一下Spring IOC的实现机制吗?
      7. 6.1.0.7. Spring Bean的生命周期吗?
      8. 6.1.0.8. 有哪些依赖注入的方法?
      9. 6.1.0.9. Spring有哪些自动装配的方法?
      10. 6.1.0.10. 🌟 Bean的循环依赖?
      11. 6.1.0.11. AOP和面向对象的区别?能取代面向对象吗?
      12. 6.1.0.12. 有在实际编程中使用过AOP吗?
      13. 6.1.0.13. Spring 事务?
      14. 6.1.0.14. Spring 的事务隔离级别?
  • 6.2. 5.2 SpringBoot
    1. 6.2.0.1. 说说你对springboot的理解,以及他和spring的区别?
    2. 6.2.0.2. 解释CORS原理,如何在前端/后端解决跨域问题?
    3. 6.2.0.3. SpringBoot常用注解和作用
    4. 6.2.0.4. 注解的本质是什么?
    5. 6.2.0.5. @Autowire 和 @Resource的区别
    6. 6.2.0.6. SpringBoot的启动流程
    7. 6.2.0.7. SpringBoot有哪些传入配置的方法?(及覆盖顺序)
    8. 6.2.0.8. SpringBoot自动配置原理了解吗?
    9. 6.2.0.9. SpringBoot 的自动装配流程
    10. 6.2.0.10. 自动配置的方式
    11. 6.2.0.11. 如何实现基于注解驱动的Spring Boot Starter?
  • 6.3. Spring Cloud
    1. 6.3.0.1. 什么是微服务?
    2. 6.3.0.2. 微服务架构主要要解决哪些问题?
  • 6.4. 5.3 Mybatis
    1. 6.4.0.1. #{} 和 ${} 的区别是什么?
  • 7. 6 常用中间件
    1. 7.1. 6.1 Redis
      1. 7.1.1. Redis基础
        1. 7.1.1.1. 什么是Redis?
        2. 7.1.1.2. 为什么使用Redis?
        3. 7.1.1.3. Redis的数据结构
        4. 7.1.1.4. Redis的使用场景(场景题)
        5. 7.1.1.5. 关系型数据库和非关系型数据库的区别?
        6. 7.1.1.6. Redis常见的操作
        7. 7.1.1.7. Redis的命令是原子的吗?如何理解
        8. 7.1.1.8. Redis为什么快?
        9. 7.1.1.9. Redis为什么早期选择单线程?
        10. 7.1.1.10. Redis6.0使用多线程是怎么回事?
        11. 7.1.1.11. Redis的数据结构
        12. 7.1.1.12. Redis数据结构的底层实现
        13. 7.1.1.13. Redis 的 SDS(简单动态字符串) 和 C 中字符串相比有什么优势?
        14. 7.1.1.14. 跳表有了解吗?
        15. 7.1.1.15. 压缩列表 ziplist
        16. 7.1.1.16. redis 持久化的方式(RDB, AOF)
        17. 7.1.1.17. RDB的流程及优缺点
        18. 7.1.1.18. AOF持久化流程及优缺点
        19. 7.1.1.19. RDB和AOF如何选择?
        20. 7.1.1.20. Redis如何进行数据恢复?
      2. 7.1.2. 6.1.4 Redis高可用(主从复制、哨兵和集群)
        1. 7.1.2.1. 高可用–主从复制机制
        2. 7.1.2.2. 主从复制主要的作用?
        3. 7.1.2.3. Redis主从有几种常见的拓扑结构?
        4. 7.1.2.4. 主从复制的原理?
        5. 7.1.2.5. 主从复制的数据同步方式?
        6. 7.1.2.6. 主从复制存在哪些问题?
        7. 7.1.2.7. 高可用–哨兵模式
        8. 7.1.2.8. 哨兵模式的实现原理?
        9. 7.1.2.9. 领导者Sentinel节点选举了解吗?
        10. 7.1.2.10. 哨兵模式的故障转移过程
        11. 7.1.2.11. Redis集群
        12. 7.1.2.12. 集群中的数据是如何分片的?
        13. 7.1.2.13. 集群的工作原理
        14. 7.1.2.14. Redis集群中各个节点是如何实现数据一致性的?
      3. 7.1.3. Redis 高并发
        1. 7.1.3.1. Redis事务是什么?怎么实现ACID的?
        2. 7.1.3.2. 布隆过滤器
        3. 7.1.3.3. 🌟缓存穿透,缓存击穿,缓存雪崩
        4. 7.1.3.4. redis 如何应对热点数据?
        5. 7.1.3.5. 缓存大小设置,淘汰策略
        6. 7.1.3.6. 缓存如何保证与数据库的数据一致性?
        7. 7.1.3.7. 如何保证缓存和数据库数据的⼀致性?
        8. 7.1.3.8. 如何保证本地缓存和分布式缓存的一致?
        9. 7.1.3.9. 热点Key
        10. 7.1.3.10. ⭐️若 QPS 达到十万级别,如何确保 Redis 正常工作?
      4. 7.1.4. 6.1.6 Redis场景题
    2. 7.2. 6.2 RabbitMQ
      1. 7.2.0.1. 消息队列的应用场景
      2. 7.2.0.2. 有了解过其他消息队列吗?如何做选型?
      3. 7.2.0.3. 简单介绍一些rabbitMQ的底层数据结构和实现方式
      4. 7.2.0.4. 交换器的类型
      5. 7.2.0.5. 消息队列有什么作用?
      6. 7.2.0.6. RabbitMQ如何保证消息的顺序一致性?
      7. 7.2.0.7. 如何保证消息可靠性?
      8. 7.2.0.8. 如何将消息持久化?
      9. 7.2.0.9. RabbitMQ有什么特点?
      10. 7.2.0.10. 使用消息队列需要注意哪些问题?
      11. 7.2.0.11. 🌟讲讲RabbitMQ的消息确认机制
      12. 7.2.0.12. 如何解决消息堆积的?
      13. 7.2.0.13. 如何保证消息不被重复消费?
      14. 7.2.0.14. 什么是死信队列?
      15. 7.2.0.15. RabbitMQ是如何实现死信队列的?
  • 7.3. 6.3 Dubbo
    1. 7.3.0.1. 简单介绍一下Dubbo
    2. 7.3.0.2. Dubbo的工作原理是什么样的?
    3. 7.3.0.3. Dubbo有哪些负载均衡策略?
    4. 7.3.0.4. Dubbo有哪些容错机制?
  • 7.4. 6.4 Etcd
    1. 7.4.0.1. Etcd 如何保证数据一致性?
  • 7.5. 6.5 Docker
  • 7.6. 6.6 K8s
    1. 7.6.0.1. 讲一讲什么是k8s
  • 7.7. 6.7 ElasticSearch
    1. 7.7.0.1. 简单讲讲什么是ES?
    2. 7.7.0.2. 什么是倒排索引?
    3. 7.7.0.3. 为什么倒排索引不用B+树?
  • 7.8. 6.8 Nginx
    1. 7.8.0.1. 什么是代理服务器,正向代理和反向代理的有什么区别
  • 7.9. 6.8 其他中间件
    1. 7.9.0.1. Memcached
    2. 7.9.0.2. 什么是负载均衡?
    3. 7.9.0.3. 二、三、四、七层负载是什么意思?
  • 7.10. 6.9 前端
    1. 7.10.0.1. vue中v-if, v-for, v-show,v-else, v-bind, v-on 的区别是什么
    2. 7.10.0.2. Vue和React有什么主要区别?
    3. 7.10.0.3. 讲讲Vue的双向数据绑定和React的单向数据流动
  • 8. 7 设计模式
    1. 8.0.0.1. 设计模式的分类
    2. 8.0.0.2. 单例模式及创建方式
    3. 8.0.0.3. 单例模式的实现
    4. 8.0.0.4. 双锁单例模式,为什么要双重校验?
    5. 8.0.0.5. 1. 工厂模式
    6. 8.0.0.6. 2. 代理模式
    7. 8.0.0.7. 3. 适配器模式
    8. 8.0.0.8. 4. 装饰器模式
    9. 8.0.0.9. 5. 观察者模式
    10. 8.0.0.10. 6. 策略模式
    11. 8.0.0.11. 7. 模板模式
  • 9. 8 场景题
    1. 9.1. 并发相关
      1. 9.1.0.1. 一个线程需要拿到a,b,c三个线程的结果才能执行,用Java如何保证?
  • 9.2. 7.1 Redis相关
    1. 9.2.0.1. 🌟如何使用redis实现一个分布式锁?
    2. 9.2.0.2. Redis来实现一小时内只能发送10次短信验证码–Redis List
    3. 9.2.0.3. Redis延时操作
    4. 9.2.0.4. 做一个排行榜 (sorted set)
    5. 9.2.0.5. 统计网站UV (HyperLogLog)
    6. 9.2.0.6. 订单在30min之内未支付则自动取消 –RabbitMQ 死信队列
    7. 9.2.0.7. 数据压缩存储 bitmap
    8. 9.2.0.8. 如何快速判断海量数据中是否存在某一个元素?(布隆过滤器)
    9. 9.2.0.9. 如何在海量文本文件查找某一个单词在哪个文档中?(倒排索引)
    10. 9.2.0.10. top-k问题
    11. 9.2.0.11. 维护热搜
    12. 9.2.0.12. 如何基于 UDP 协议实现可靠传输?
    13. 9.2.0.13. 大文件查询
    14. 9.2.0.14. 两个大文件找重复行
  • 9.3. 系统设计
    1. 9.3.0.1. 秒杀系统设计
    2. 9.3.0.2. 曝光系统设计
    3. 9.3.0.3. 排行榜设计
    4. 9.3.0.4. 信息流系统
    5. 9.3.0.5. 站内信(私信,@)
    6. 9.3.0.6. 设计邀请码
    7. 9.3.0.7. 如何实现点赞(实时显示点赞数)?
  • 9.4. 分布式系统
    1. 9.4.0.1. CAP
  • 10. 9 智力题
  • 11. 人工智能相关
    1. 11.0.0.1. 常见机器学习算法
    2. 11.0.0.2. 常见的神经网络模型
    3. 11.0.0.3. 简单介绍一下transformer
    4. 11.0.0.4. 有哪些常用损失函数?
    5. 11.0.0.5. 有哪些常用的激活函数?
  • 【工作】秋招后端八股文

    [TOC]

    1 Java语言基础

    1.1 面向对象与Java基础

    ![[Pasted image 20240722192625.png]]

    1.1.1 基础概念

    JDK,JVM,JRE

    JDK(Java Development Kit),它是功能齐全的 Java SDK,是提供给开发者使用,能够创建和编译 Java 程序的开发套件。
    JRE 是 Java 运行时环境,仅包含 Java 应用程序的运行时环境和必要的类库。
    Java 虚拟机(Java Virtual Machine, JVM)是运行 Java 字节码的虚拟机。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
    ![[Pasted image 20240708202322.png]]

    Java C++ Go的区别

    • Java
      • 🌟跨平台,⼀次编写可以在多个操作系统上运⾏
      • 🌟生态完善,丰富的类库,可以快速开发应⽤程序
      • ❌由于JVM的存在,运⾏速度相对较慢
      • ❌对于实时性要求较⾼的场景,Java的表现可能不如C++和Go
      • 🏩适合开发企业级应⽤程序、后端服务等
    • C++
      • 🌟速度快,适合编写需要⾼性能的应⽤程序
      • 🌟应⽤⼴泛,特别是在游戏开发、操作系统和嵌⼊式系统开发⽅⾯
      • 🌟灵活性⾼,可以直接访问硬件和内存
      • ❌学习难度较⾼,需要掌握指针、内存管理等底层知识
      • ❌由于程序员掌握访问内存等功能,容易出现内存泄漏和指针错误等问题
      • 🏩适合开发需要⾼性能和⾼可靠性的应⽤程序,特别是在游戏开发、操作系统和嵌⼊式系统开发⽅⾯
    • Go
      • 🌟⾼并发,天⽣⽀持协程,能够轻松编写⾼效的并发程序
      • 🌟静态类型语⾔,可以避免⼀些潜在的运⾏时错误
      • 🌟快速编译,可以快速构建和部署应⽤程序
      • ❌缺乏丰富的类库,与 Java 和 C++ 相⽐有些不⾜
      • ❌在⼀些性能要求极⾼的场景中可能不如 C++ 表现
      • 🏩适合开发⾼并发的后端服务、微服务、容器化应⽤程序等

    Java是编译型语言吗?

    先编译再解释执行。
    Java既是编译型语言,也是解释型语言。Java源代码首先被编译成字节码(.class文件),这种字节码是一种中间代码,它可以在任何安装了Java虚拟机(JVM)的平台上运行。然后,Java字节码在运行时被JVM解释执行。所以,Java的执行过程包含了编译和解释两个阶段。

    JDK17有哪些新特性?为什么许多从JDK1.8转到JDK17的?

    JDK 17 有以下新特性:

    1. 语言和语法改进
      • 局部变量类型推断增强:虽然局部变量类型推断(使用 var 关键字)在 JDK 10 中就已引入,但在 JDK 17 中使用起来更加稳定和成熟。这使得开发者可以在不明确指定变量类型的情况下声明局部变量,代码更加简洁,类型由编译器根据初始化表达式自动推断。
      • 增强的 switch 表达式:在之前版本对 switch 表达式改进的基础上,JDK 17 进一步增强了其功能。使用箭头语法和 yield 关键字,开发者可以更灵活地控制 switch 语句的返回值,代码更加简洁、易读,并且减少了代码中的样板代码。
    2. 类和对象相关特性36:
      • 密封类(Sealed Classes):允许开发者控制哪些类可以扩展或实现特定的类或接口。密封类及其子类可以声明为 final(不能被进一步扩展)或 non-sealed(可以被进一步扩展)。这有助于创建更安全和更可预测的继承结构,防止不适当的类继承,增强了类型安全。
      • 记录类(Record Classes):提供了一种简明的方式来创建数据载体类,类似于传统的 POJO 类,但代码更加简洁。记录类会自动生成 toString()equals() 和 hashCode() 等方法,减少了开发者编写样板代码的工作量,同时也提高了代码的可读性和可维护性。
    3. 模式匹配改进:在 instanceof 操作符中的模式匹配得到了进一步增强。现在,开发者可以直接在 instanceof 检查中引入变量绑定,从而减少不必要的类型转换代码,使类型检查和转换操作更加简洁、安全36。
    4. 文本块(Text Blocks):虽然文本块在 Java 15 中首次引入,但 JDK 17 对其进行了进一步改进,使其更加实用。文本块允许多行字符串文字,在创建 SQL 查询、JSON 字符串或其他需要多行文本的场景中非常有用,减少了处理多行字符串的痛苦,代码更加清晰易读6。
    5. 性能优化和垃圾回收改进
      • 垃圾回收器改进:JDK 17 对垃圾回收器进行了优化,引入了新的垃圾收集器或对现有垃圾收集器进行了改进,提高了垃圾回收的效率和性能,减少了垃圾回收的停顿时间,从而提高了应用程序的响应速度和吞吐量。例如,ZGC(Z Garbage Collector)在 JDK 17 中得到了进一步的优化和改进,适用于对延迟要求较高的应用场景。
      • 并发性能提升:对并发相关的类和框架进行了改进,提高了多线程应用程序的性能和并发处理能力。例如,对 ConcurrentHashMap 等并发集合类的性能进行了优化,使得在多线程环境下对集合的操作更加高效。
    6. 安全增强
      • 权限控制优化:对反射调用进行了权限控制,通过 setAccessible() 方法可以启动或禁止访问安全检查开关。当参数值为 true 时,反射的对象在使用时取消安全检查,提高反射的效率;当参数值为 false 时,反射的对象执行安全检查。这样的优化使得在处理反射调用时,可以更加灵活地控制访问权限。
      • 包扫描权限控制增强:在之前的版本中,Java 的包扫描是基于类的,而在 JDK 17 中,扩展到了对整个包的权限控制。这使得开发者可以更加精细地控制对特定包的访问权限。
    7. 文件系统和 I/O 操作改进4:
      • 改进的文件系统 API:引入了改进的文件系统 API,使得开发者能够以更高效、更安全的方式操作文件和目录。这些 API 遵循统一的、现代的文件系统概念,提供了更好的跨平台兼容性。
      • 类文件改进:类文件格式更新,增强了对新特性的支持,如模块系统和并发 API 的改进。类文件加载和验证的优化提高了编译和运行时的性能。
    8. 其他改进6:
      • 新引入的 API:例如 RandomGenerator,提供了一种更统一和灵活的方式来生成随机数。
      • 隐式类路径的改进:简化了隐式类路径的处理,特别是在模块化应用中。这一改进有助于减少类路径配置的复杂性,使模块化应用的开发更加简单和高效。
      • 移除过时的功能:移除了几个已经废弃的功能,例如 Applet API 和 SecurityManager,这些功能的移除有助于简化 JDK,并鼓励开发者使用更现代和安全的替代方案。

    许多开发者从 JDK 1.8 转到 JDK 17 的原因如下:
    1. 新特性的吸引力
    - 代码简洁性提升:JDK 17 中的局部变量类型推断、增强的 switch 表达式、文本块和记录类等特性,使代码更加简洁易读,减少了样板代码的编写,提高了开发效率。例如,记录类可以快速定义数据类,无需手动编写 toString()equals() 和 hashCode() 等方法,大大简化了代码5。
    - 更强大的功能:密封类可以更好地控制类的继承关系,增强了类型安全;模式匹配的改进使类型检查和转换更加方便;新的 API 和功能增强为开发者提供了更多的工具和选择,方便解决各种复杂的问题6。
    2. 性能优化
    - 垃圾回收改进:JDK 17 对垃圾回收器的优化可以减少垃圾回收的停顿时间,提高应用程序的响应速度和吞吐量,对于大型应用程序和对性能要求较高的场景非常重要。例如,ZGC 的性能提升可以更好地满足高并发、低延迟的业务需求。
    - 并发性能提升:对并发相关的改进使得多线程应用程序的性能和并发处理能力得到提高,能够更好地利用多核处理器的性能,提高程序的执行效率5。
    3. 安全性增强:JDK 17 在安全方面的改进,如反射权限控制和包扫描权限控制的增强,有助于开发者构建更安全的应用程序,保护用户数据和系统安全。在当前对软件安全要求越来越高的背景下,这是一个重要的考虑因素。
    4. 长期支持:JDK 17 是一个长期支持(LTS)版本,会得到较长时间的维护和更新,这对于企业级应用和长期运行的项目来说非常重要。开发者可以放心地使用 JDK 17 进行开发,不用担心版本的快速迭代和缺乏长期支持6。
    5. 生态系统和工具支持:随着 JDK 17 的广泛应用,相关的开发工具和框架也逐渐对其进行了支持和优化。主流的 IDE(如 IntelliJ IDEA、Eclipse 等)都提供了对 JDK 17 的良好支持,开发者可以享受到更好的开发体验和工具支持。同时,Java 生态系统中的各种库和框架也在不断适配 JDK 17,使得开发者能够使用最新的技术和工具来构建应用程序35。
    6. 技术发展和行业趋势:Java 技术在不断发展,新的版本会带来更好的性能、更多的功能和更高的安全性。随着时间的推移,越来越多的项目开始采用新的 JDK 版本,开发者也需要不断学习和掌握新的技术,以保持竞争力。从 JDK 1.8 升级到 JDK 17 是顺应技术发展和行业趋势的选择3。
    ### 1.1.2 数据类型
    #### 变量命名规范
    1. 标识符:变量名必须是一个有效的Java标识符,这意味着它必须以字母、美元符号($)或下划线( _ )开始,后续字符可以是字母、数字、美元符号或下划线。
    2. 保留字:变量名不能是Java的保留字,例如 classpublicprivate 等。
    3. 大小写敏感:Java是大小写敏感的语言,因此 variableVariableVARIABLE 是三个不同的变量名。
    4. 数字开头:变量名不能以数字开头。
    #### Java有哪些基本数据类型?
    Java 中有 8 种基本数据类型,分别为:
    - 6 种数字类型:
    - 4 种整数型:byteshortintlong
    - 2 种浮点型:floatdouble
    - 1 种字符类型:char
    - 1 种布尔型:boolean

    1. byte
      • 取值范围:-128 至 127
      • 占用空间:1 字节(8位)
    2. short
      • 取值范围:-32,768 至 32,767
      • 占用空间:2 字节(16位)
    3. int
      • 取值范围:-2^31 至 2^31-1,即 -2,147,483,648 至 2,147,483,647 (大概约 正负20亿)
      • 占用空间:4 字节(32位)
    4. long
      • 取值范围:-2^63 至 2^63-1,即 -9,223,372,036,854,775,808 至 9,223,372,036,854,775,807
      • 占用空间:8 字节(64位)
    5. float
      • 取值范围:大约 ±3.40282347E+38F(6-7个十进制数精度)
      • 占用空间:4 字节(32位)
    6. double
      • 取值范围:大约 ±1.79769313486231570E+308(15个十进制数精度)
      • 占用空间:8 字节(64位)
    7. char
      • 取值范围:‘\u0000’ (即为0) 至 ‘\uffff’ (即为65,535),表示 Unicode 字符
      • 占用空间:2 字节(16位)
    8. boolean
      • 取值范围:true 或 false
      • 占用空间:JVM规范没有明确指定,通常视为1位,但实际上占用空间的大小依赖于JVM实现和底层硬件。
      • -使用boolean数组类型,它的占用空间是一个字节,当我们单独使用boolean类型的时候,它其实被当作int类型了,那么也就是4个字节

    基本数据类型和包装数据类型有什么区别?

    因为 Java 是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将int、double等类型放进去的。因为集合的容器要求元素是 Object类型。为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型”包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

    • 基本类型与包装类型的区别
      1.默认值不同:基本类型的默认值是 0,false 等,包装类默认为 null
      2.初始化的方式不同:一个需要采用 new 的方式创建,一个则不需要
      3.存储方式有所差异:基本类型主要保存在栈上面,包装类对象保存在堆上

    什么装箱和拆箱?

    装箱(Boxing)是指将基本数据类型转换为对应的包装类对象的过程。拆箱(Unboxing)是指将包装类对象转换回基本数据类型的过程。在 Java 中,自动装箱和拆箱是语言特性,允许基本类型和它们的包装类之间隐式转换。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class BoxingUnboxingExample {
    public static void main(String[] args) {
    // 装箱:将基本数据类型 int 转换为包装类 Integer
    int primitiveInt = 100;
    Integer wrapperInt = Integer.valueOf(primitiveInt); // 显式装箱
    // 或者使用自动装箱
    Integer autoBoxedInt = primitiveInt;

    // 拆箱:将包装类 Integer 转换为基本数据类型 int
    int unboxedInt = wrapperInt.intValue(); // 显式拆箱
    // 或者使用自动拆箱
    int autoUnboxedInt = autoBoxedInt;

    System.out.println("原始int值: " + primitiveInt);
    System.out.println("装箱后的Integer对象: " + wrapperInt);
    System.out.println("拆箱后的int值: " + unboxedInt);
    System.out.println("自动装箱和拆箱后的int值: " + autoUnboxedInt);
    }
    }


    在 Java 5 及以后的版本中,编译器会自动处理装箱和拆箱,不需要显式调用 valueOf() 和 intValue() 方法。
    #### == 和equeals hashcode

    • 用于比较基本数据类型时,比较的是值是否相等。
    • 当用于比较对象时,== 检查的是两个对象的引用是否相同,即它们是否指向内存中的同一个对象。
      .equals() 方法:
    • equals() 是 Object 类的一个方法,默认实现是比较两个对象的引用是否相同,🌟与 == 的行为一致。
      .hashCode() 方法:
    • hashCode() 也是 Object 类的一个方法,它返回一个整数值,这个值是对象哈希码的一个表示。
    • object类中的默认实现会根据对象的内存地址生成哈希码

    == 用于比较对象的引用,而 equals() 用于比较对象的内容(如果被覆盖的话)。hashCode() 用于在哈希表中快速定位对象,与 equals() 一起使用来确保哈希表的正确性。

    • equeals与hashcode的合约
      如果两个对象根据 equals 方法被认为是相等的,那么它们必须具有相同的哈希码
      如果两个对象具有相同的哈希码,它们并不一定相等,但会被放在同一个哈希桶中。

    • 自定义equals()方法的约束

    • 自反性:对于任何非 null 的对象 xx.equals(x) 应该返回 true

    • 对称性:对于任何非 null 的对象 x 和 y,如果 x.equals(y) 返回 true,则 y.equals(x) 也应该返回 true

    • 传递性:对于任何非 null 的对象 xy 和 z,如果 x.equals(y) 返回 true 并且 y.equals(z) 返回 true,则 x.equals(z) 也应该返回 true

    • 一致性:如果对象 x 和 y 在比较过程中没有被修改,多次调用 x.equals(y) 应该始终返回同一个结果。

    • 对于任何非 null 的对象 xx.equals(null) 应该返回 false

    • 当一个类覆盖了 equals() 方法,它通常也应该覆盖 hashCode() 方法,以确保相等的对象具有相同的哈希码。

    • hashCode() 方法的正确实现应该满足以下条件:

      • 如果两个对象根据 equals(Object) 方法比较是相等的,那么调用这两个对象中任意一个对象的 hashCode() 方法必须产生相同的整数结果。
      • 如果两个对象根据 equals(Object) 方法比较是不相等的,那么调用这两个对象中任意一个对象的 hashCode() 方法,不要求产生不同的整数结果,但是程序员应该意识到为不相等的对象生成不同的哈希码可能会提高哈希表的性能。

    == 和equals的区别是什么?分别在什么场景下使用?

    ==

    • 用于比较基本数据类型时,比较的是值是否相等。

    • 当用于比较对象时,== 检查的是两个对象的引用是否相同,即它们是否指向内存中的同一个对象。
      .equals() 方法:

    • equals() 是 Object 类的一个方法,默认实现是比较两个对象的引用是否相同,🌟与 == 的行为一致。
      但是equals()往往被覆盖重写,用于比较两个对象的内容是否一致。

    • 使用 == 比较基本数据类型的值 (如 intchar 等),或者判断两个引用是否指向同一个对象。

    • 使用 equals 比较两个对象的内容。尤其是对于对象比较,如 StringInteger 或自定义对象时,应使用 equals 方法来确保比较的是内容而非引用。

    • 重写 equals 方法:如果在自定义类中希望根据对象的内容判断相等性,应重写 equals 方法(通常还会重写 hashCode 方法,以确保在使用哈希集合时表现一致)。

    Java中的参数传递是值传递还是引用传递?

    Java是基于值传递的。这意味着当一个对象被传递给方法时,传递的实际上是对象的一个副本,而不是对象本身。这个副本被称为对象的引用,但它实际上是对象值的一个拷贝。因此,如果你在方法内部修改了这个副本,它不会影响到原始对象。

    深拷贝、浅拷贝以及引用拷贝

    浅拷贝(Shallow Copy)

    • 定义:浅拷贝是指创建一个新对象,这个新对象的非基本数据类型的成员变量(如数组、对象引用等)与原对象的相应成员变量引用同一个对象。也就是说,浅拷贝只是复制了对象的基本数据类型成员变量的值和对象引用,而没有复制引用对象本身。
    • 实现:可以通过实现 Cloneable 接口并重写 clone() 方法来实现浅拷贝。

    深拷贝(Deep Copy)

    • 深拷贝是指创建一个新对象,并且递归地复制原对象所包含的所有对象,包括基本数据类型成员变量的值和引用对象本身。这样,新对象和原对象及其内部的引用对象完全独立,修改其中一个对象不会影响另一个对象。
    • 实现:可以通过实现 Cloneable 接口并重写 clone() 方法,在 clone() 方法中为每个可变成员变量创建新的实例,或者通过序列化与反序列化来实现深拷贝。

    引用拷贝

    • 引用拷贝只复制对象的引用,而不是对象本身。
    • 这意味着原始对象和引用拷贝对象指向内存中的同一个对象,任何对对象的修改都会反映在另一个对象上。
    • 实现:简单地将一个对象的引用赋值给另一个变量即可。

    在 Java 中,基本数据类型的赋值是值拷贝,例如int a = 5; int b = a;,这里b得到了a的值的拷贝。
    而对于对象,赋值操作是引用拷贝。例如Object obj1 = new Object(); Object obj2 = obj1;obj2obj1指向同一个对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // 假设有一个类 Person,它有一个成员变量 name
    class Person implements Cloneable {
    private String name;

    public Person(String name) {
    this.name = name;
    }

    // 浅拷贝的 clone 方法
    @Override
    protected Object clone() throws CloneNotSupportedException {
    return super.clone();
    }

    // 省略了 getter 和 setter 方法
    }

    public class CopyExample {
    public static void main(String[] args) throws CloneNotSupportedException {
    // 创建原始对象
    Person original = new Person("Alice");

    // 浅拷贝
    Person shallowCopy = (Person) original.clone();

    // 深拷贝(假设 name 是一个可变对象,这里简单使用 String,它实际上是不可变的)
    Person deepCopy = new Person(new String(original.getName()));

    // 引用拷贝
    Person referenceCopy = original;

    // 测试拷贝
    System.out.println(original.getName() == shallowCopy.getName()); // true,因为 String 是不可变的
    System.out.println(original.getName() == deepCopy.getName()); // false,因为创建了新的 String 实例
    System.out.println(original == referenceCopy); // true,因为它们是同一个对象的引用
    }
    }

    1.1.3 面向对象

    面向对象三大特点

    1. 封装: 封装是指将数据和操作数据的方法绑定在一起,形成一个相对独立的类。封装的目的是隐藏内部实现细节,只暴露必要的接口给外部使用。通过封装,可以实现信息的隐藏和保护,提高代码的可维护性和安全性。此外,封装还可以降低代码的耦合度,提高代码的重用性和可扩展性。
    2. 继承: 继承是指一个类可以继承另一个类的属性和方法。通过继承,子类可以直接使用父类的成员,无需重新编写相同的代码。继承可以实现代码的重用,提高开发效率。同时,继承还可以实现代码的扩展和灵活性。子类可以在继承父类的基础上进行修改和增加,满足不同的需求。
    3. 多态: 多态是指同一类事物的多种表现形态。在面向对象编程中,多态可以通过继承和接口实现。通过多态,可以实现面向接口编程,提高代码的灵活性和可扩展性。
    多态其实是一种抽象行为,它的主要作用是让程序员可以面对抽象编程而不是具体的实现类,这样写出来的代码扩展性会更强。
    多态可以使代码更加通用,减少重复的代码。同时,多态还可以实现运行时的动态绑定,提高程序的可扩展性和可维护性

    • 多态就像是一个东西可以有多种表现形式。比如,你可以把动物想象成一个大的概念,而猫、狗、兔子都是动物这个概念下的不同具体形态。在编程里,一个类可以有多种形态的表现。在面向对象编程中,多态可以通过继承和接口实现。通过多态,可以实现面向接口编程,提高代码的灵活性和可扩展性。

    Java创建对象的方式

    在Java中,创建对象的方式主要有以下几种:

    1. 使用new关键字:这是最常见的创建对象的方式。通过使用new关键字,我们可以调用构造函数来创建类的实例。

      1
      MyClass obj = new MyClass();
    2. 【反射】使用Class类的newInstance方法:这种方式需要调用特定类的newInstance方法,
      该方法要求类必须有一个无参的构造函数,并且它的访问权限必须是public。

      1
      MyClass obj = MyClass.class.newInstance();
    3. 使用Constructor类的newInstance方法:这种方式可以用来创建类的实例,
      无论构造函数是否是public,也可以用来调用非public的构造函数。

      1
      2
      Constructor<MyClass> constructor = MyClass.class.getConstructor();
      MyClass obj = constructor.newInstance();
    4. 使用克隆方法:这种方式需要实现Cloneable接口并覆盖clone()方法。

      1
      MyClass obj = (MyClass) anotherObj.clone();
    5. 使用反序列化:反序列化是从字节流中重新构造对象。

      1
      2
      3
      ObjectInputStream in = new ObjectInputStream(new FileInputStream("obj.bin"));
      MyClass obj = (MyClass) in.readObject();
      in.close();

    使用设计模式创建
    6. 使用工厂方法模式:工厂方法模式是一种用于创建对象的设计模式,而不是直接使用new关键字。

    1
    MyClass obj = MyFactory.createInstance();
    1. 使用单例模式:单例模式确保类有且只有一个对象。
      1
      MyClass obj = SingletonClass.getInstance();
      以上每种方式都有其特定的使用场景,例如,反射方式适合在运行时创建对象,而无需预先知道要创建的类型;克隆方法适合需要创建对象的浅拷贝的场景;工厂模式适合当创建对象的逻辑可能会变化的场景。

    接口和抽象类有什么共同点和区别?

    接口和抽象类的联系

    • 抽象性:接口和抽象类都不能被实例化,它们都包含未实现的方法声明,要求派生类必须实现这些方法。
    • 定义规范:接口和抽象类都用于定义对象的行为规范或约定,不关心实现细节。

    接口和抽象类的区别

    • 继承方式:一个类可以实现多个接口,但只能继承一个抽象类。
    • 成员实现:抽象类可以包含有实现的成员和没有实现的成员(抽象方法),而接口只能包含没有实现的成员(方法、属性、索引器和事件的签名)。
    • 访问修饰符:抽象类可以包含不同访问修饰符的成员,而接口中的成员默认都是public的,且不能使用访问修饰符。
    • 构造函数:抽象类可以有构造函数,接口不能有构造函数。
    • 多继承:接口支持多继承,一个类可以实现多个接口,而抽象类不支持多继承。
    • 目的和使用场景:抽象类用于表示具有一些共有属性和方法的类,但不需要完全实现所有方法,可以作为基类供其他子类继承;接口用于定义操作的规范或约定,主要用于实现多继承和解耦。

    为什么Java不支持多重继承?

    Java不支持多重继承是为了避免菱形继承问题,即一个类继承自两个或多个具有相同方法的父类时,会导致方法调用的不确定性,增加程序复杂性并可能导致编译错误。通过只支持单一继承并引入接口,Java简化了继承结构,降低了出错的可能性。
    ![[Pasted image 20240819212702.png]]
    BC继承了A,然后D继承了 BC,假设此时要调用D 内定义在A的方法,因为8和C都有不同的实现,此时就会出现歧义,不知道应该调用哪个了。

    什么是内部类?有什么作用?

    内部类(Inner Class)是在一个类的内部定义的类。在 Java 中,内部类有几种不同的类型,包括成员内部类(Member Inner Class)、局部内部类(Local Inner Class)、匿名内部类(Anonymous Inner Class)和静态内部类(Static Nested Class)。

    1. 封装:内部类可以访问外部类的私有成员,而不需要提供公共接口,这有助于封装实现细节。
    2. 提高代码的可读性和可维护性:内部类可以使得代码结构更加清晰,因为它可以隐藏在它所服务的类内部。
    • 成员内部类:定义在另一个类中的类,可以使用外部类的所有成员变量以及方法,包括 private 的。
    • 静态内部类:只能访问外部类的静态成员变量以及方法,其实它就等于一个顶级类,可以独立于外部类使用,所以更多的只是表明类结构和命名空间
    • 局部内部类:定义在方法内的类,不常用
    • 匿名内部类:指的是没有类名的内部类。仅在创建对象时使用

    Consumer接口

    在 Java 中,Consumer 接口是一个函数式接口,属于 java.util.function 包。Consumer 接口的主要作用是接收一个输入参数并对其进行操作(消费),但不返回任何结果。

    Consumer 接口的特点

    • 它有一个抽象方法:accept(T t),其中 T 是输入参数的类型。这个方法用于执行某种操作,消费输入参数。
    • Consumer 接口还提供了一个默认方法 andThen(Consumer<? super T> after),用于组合多个 Consumer,以便依次对相同的输入进行不同的操作。

    Consumer 接口常用于需要对某个对象执行操作但不需要返回值的场景。例如,在遍历一个集合时,可以使用 Consumer 来对集合中的每个元素执行某种操作(如打印、更新等)。

    • printName 是一个 Consumer,用于输出一个带有问候语的字符串。
    • printLength 是另一个 Consumer,用于输出名字的长度。
    • andThen 方法将两个 Consumer 组合起来,使它们依次执行操作

    Java方法的重载和重写有什么区别?

    • 重载:在同一个类中定义多个方法,它们具有相同的名字但参数列表不同。主要用于提供相同功能的不同实现。
    • 重写:在子类中定义一个与父类方法具有相同签名的方法,以便提供子类的特定实现。主要用于实现运行时多态性。

    Enum枚举类,其能否被继承?

    在 Java 中,Enum 是一个特殊的类,用于表示一组固定常量集合。枚举类通常用于定义有限数量的选项,例如一周的天数、交通信号灯的颜色、性别类型等。枚举类通过关键字 enum 来定义。

    枚举类的特点

    1. 内置 Enum:所有的枚举类型都隐式地继承自 java.lang.Enum 类,因此无法继承其他类,也不能被继承。
    2. 枚举类是final的,因此我们无法再继承它了
    3. 线程安全:枚举实例在 Java 中是单例的(由 JVM 保证),因此是线程安全的。

    枚举类不能被继承,这是因为:

    1. 所有的枚举类型都隐式地继承自 java.lang.Enum。Java 不支持多重继承,所以枚举类不能继承其他类,也不能被其他类继承。
    2. 枚举类型设计的初衷是提供一种有限的常量集合,枚举的实例是有限且固定的。如果允许继承,会破坏这一设计原则,导致实例不再有限。
    3. Java 编译器会强制要求枚举类型为 final,因此任何尝试继承枚举类的操作都会报编译错误。

    什么是函数式接口?

    在 Java 中,函数式接口(Functional Interface)是一个只包含一个抽象方法的接口。这样的接口可以使用 Lambda 表达式、方法引用或构造方法引用来提供该接口的实现。

    引入函数式接口的主要目的是增强 Java 对函数式编程 的支持,使 Java 更具表现力和简洁性,特别是在处理集合操作、异步编程、并行处理等方面。函数式接口的引入为 Java 语言带来了 Lambda 表达式,简化了代码编写,提高了开发效率。

    Java 8 中新增的常用函数式接口

    以下是 Java 8 中 java.util.function 包提供的常用函数式接口列表:

    函数式接口 描述
    Predicate<T> 表示一个条件判断的函数接口,返回 boolean
    Function<T, R> 接受一个参数,返回一个结果。
    Consumer<T> 接受一个参数,没有返回值。
    Supplier<T> 提供一个值,没有输入参数。
    UnaryOperator<T> 表示对单个参数进行操作的函数,输入输出类型相同。
    BinaryOperator<T> 表示对两个相同类型的参数进行操作的函数。
    BiPredicate<T, U> 接受两个参数,返回一个 boolean
    BiFunction<T, U, R> 接受两个参数,返回一个结果。
    BiConsumer<T, U> 接受两个参数,没有返回值。

    1.1.4 修饰符

    变量修饰符作用范围

    四种作用范围由小到大 private<缺省/默认(空着不写)< protected < public

    修饰符 同一个类中 同一个包中其他类 不同包下的子类 不同包下的无关类
    private
    缺省 v
    protected
    public
    实际开发一般只用private和public 。

    static 静态类 静态方法

    在Java中,static关键字是一个非常重要的元素,它可以用于变量、方法、代码块和内部类。以下是static关键字的不同用法及其作用:

    1. 静态变量(类变量)

      • 当一个类的字段被声明为static时,该字段称为静态变量或类变量。
      • 静态变量属于类本身,而不是类的某个对象实例。
      • 同一个类的所有实例共享同一个静态变量。
      • 可以通过类名直接访问静态变量,而无需创建类的实例。
    2. 静态方法

      • 静态方法是属于类的方法,而不是属于类的某个对象实例。
      • 静态方法可以直接通过类名调用,而无需创建类的实例。
      • 静态方法不能直接访问非静态成员(字段和方法),因为非静态成员必须通过对象实例来访问。
    3. 静态代码块

      • 静态代码块是一段被声明为static的代码块。
      • 静态代码块在类被加载到JVM时执行,而且只执行一次。
      • 静态代码块通常用于初始化静态变量。
    4. 静态内部类

      • 静态内部类是嵌套在类内部的静态类。
      • 静态内部类可以访问外部类的静态成员,但不能直接访问非静态成员。
      • 静态内部类不需要对外部类对象的引用。

    以下是static关键字的一些具体作用:

    • 共享资源:静态变量可以用来存储所有实例共享的数据,例如常量、配置信息等。
    • 全局访问点:静态方法提供了一个全局访问点,可以在不创建类实例的情况下执行操作。
    • 初始化:静态代码块可以用于初始化静态变量,确保在类加载时执行一些必要的操作。
    • 减少内存消耗:由于静态成员属于类而不是对象实例,因此可以减少内存消耗。
    • 独立性:静态内部类不依赖于外部类的实例,因此可以在没有外部类实例的情况下独立存在。

    final

    在 Java 中,final 关键字⽤于表示⼀个不可变的常量或⼀个不可变的变量。

    在 Java 中,final 关键字可以修饰类、⽅法和变量,作⽤如下:

    1. final 修饰类,表示该类不能被继承。final 类中的⽅法默认都是 final 的,不能被⼦类重写。
    2. final 修饰⽅法,表示该⽅法不能被⼦类重写。
    3. final 修饰变量,表示该变量只能被赋值⼀次。final 修饰的变量必须在声明时或构造函数中初始化,且不能再被修改。常⽤于定义常量
      修饰基本数据类型:变量存储的数据值不能发生改变
      修饰引用数据类型:变量存储的地址值不能发生改变;对象内部的可以改变

    1.1.4 String

    为什么String是不可变的?

    从底层实现角度,String是通过字符数组实现的。
    private final char value[ ]

    • 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
    • String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

    从应用的角度来看:

    1. 缓存字符串常量: 由于字符串在Java中是不可变的,它们可以被字符串常量池缓存起来并在多个地方重复使用,这有助于节省内存并提高性能。
    2. 内容安全: 字符串常用于存储敏感信息,如密码、文件路径等。如果String是可变的,那么任何对字符串内容的修改都会影响到所有使用该字符串的地方,这可能会导致安全问题。不可变性保证了字符串内容一旦创建就不会被改变,从而保证了安全性。
    3. 线程安全: 由于字符串不可变,它们可以在多个线程之间共享,而不需要额外的同步措施。

    String#equals() 和 Object#equals() 有何区别?

    String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。
    Objectequals 方法是比较的对象的内存地址。

    String,Stringbiulder,SrtingBuffer有什么区别?

    StringStringBuilder, 和 StringBuffer 在 Java 中都是用于处理字符串的类,但它们之间有以下几个主要区别:

    1. 可变性:
      • String 是不可变的,意味着一旦创建,它的值就不能更改。
      • StringBuilder 和 StringBuffer 是可变的,允许修改字符串内容而不创建新的对象。
    2. 线程安全性:
      • String 和 StringBuilder 不是线程安全的。
      • StringBuffer 是线程安全的,因为它的大部分方法都是同步的。
    3. 性能:
      • 由于 StringBuffer 的同步特性,StringBuilder 通常比 StringBuffer 有更好的性能。
      • String 在进行大量字符串操作时(如拼接)通常比 StringBuilder 和 StringBuffer 慢,因为它会创建多个临时对象。
    4. 使用场景:
      • 当字符串值不经常改变时,使用 String
      • 当在单线程环境中需要频繁修改字符串时,使用 StringBuilder
      • 当在多线程环境中需要频繁修改字符串时,使用 StringBuffer

    StringBuilder是怎么实现的?

    StringBuilder 是 Java 中一个可变的字符序列类。它的实现原理可以简单概括如下:

    1. 内部数组StringBuilder 内部维护了一个字符数组,用于存储字符串的字符序列。
    2. 容量和长度StringBuilder 有两个重要的属性,一个是容量(capacity),表示内部数组可以容纳的字符数量;另一个是长度(length),表示已经添加的字符数量。长度不能超过容量。
    3. 扩容机制:当向 StringBuilder 添加的字符数量超过了当前容量时,StringBuilder 会自动进行扩容,通常是创建一个新的更大的数组,并将原数组的内容复制到新数组中。
    4. 追加和修改StringBuilder 提供了 appendinsert 和 delete 等方法来修改字符序列。这些方法直接操作内部数组,因此修改非常高效。
    5. 线程不安全StringBuilder 不是线程安全的,因为它没有提供同步机制来防止多线程同时修改。
    6. 转换为字符串:当需要将 StringBuilder 的内容作为字符串使用时,可以调用它的 toString 方法,该方法会根据内部数组的内容创建一个新的字符串。

    StringBuffer是如何实现线程安全的?

    StringBuffer通过在其方法上使用synchronized关键字来保证线程安全。这意味着在任意时刻,只有一个线程能够访问StringBuffer对象的方法。synchronized关键字确保了当一个线程正在执行这个方法时,其他线程必须等待当前线程完成这个方法后才能执行这个方法。

    由于synchronized会引入额外的开销(如上下文切换),所以如果不需要线程安全,推荐使用StringBuilder,因为它更快。如果确实需要线程安全,那么StringBuffer是合适的选择。在Java 5之后,StringBuilder通常是首选,因为你可以通过其他方式来确保线程安全,例如使用局部变量或者通过并发库中的类。

    使用 new String(“keriko”) 语句在 Java 中会创建多少个对象?

    在 Java 中,使用 new String("keriko") 语句会创建至少一个对象,但通常情况下会创建两个对象。

    1. 字符串常量池中的对象:当 Java 虚拟机(JVM)加载包含该语句的类时,会首先在字符串常量池中查找是否有相同内容的字符串常量 "keriko"。如果没有,JVM 会在字符串常量池中创建一个字符串对象 "keriko"
    2. 堆内存中的对象:接着,执行 new String("keriko") 时,会在堆内存中创建一个新的 String 对象。这个对象的内容是通过引用字符串常量池中的 "keriko" 来初始化的。

    字符串拼接 “+”与StringBuilder.append( )有什么区别?

    Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
    可以看出,字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

    不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象

    1.1.5 其他

    try-catch-finally的执行顺序

    在 Java 中,try-catch-finally 块的执行顺序是非常明确的:

    1. try:首先执行 try 块中的代码。
    2. catch:如果 try 块中抛出了一个异常,并且该异常类型与 catch 块中的异常类型匹配,那么会跳转到相应的 catch 块并执行其中的代码。
    3. finally:无论 try 块是否抛出异常,finally 块中的代码都会执行。即使在 trycatch 块中存在 returnbreakcontinuethrow 语句,finally 块仍然会执行。

    是否一定执行 finally 块?

    通常情况下,finally 块中的代码是一定会执行的,但有以下几个例外情况:

    1. trycatch 块中调用了 System.exit() 方法,这将会导致 JVM 终止,finally 块不会被执行。
    2. 程序所在的线程被强制终止或 JVM 崩溃。
    3. 硬件故障(如电源断电等)。

    什么是java中的异常处理

    异常是指程序运行过程中发生的非预期事件,例如除零错误、数组越界、文件未找到等。这些事件会导致程序的正常执行流程中断,因此需要对这些事件进行检测和处理。
    在 Java 中,就提供了一种 异常处理(Exception Handling)机制,用于捕获和处理在程序执行过程中发生的异常情况,避免程序终止运行。Java 提供了一种结构化的方式来捕获和处理异常,通过使用 try-catch-finally 语句块,以确保程序的健壮性和稳定性。

    checked异常和unchecked异常有什么区别?

    1. Checked 异常
    • 定义:Checked 异常是指那些在编译时被强制检查的异常。这些异常通常是由于外部原因(如 I/O 错误、数据库访问错误等)引起的,程序员必须显式地处理这些异常,否则编译器会报错。
    • 处理方式:必须通过 try-catch 块捕获处理,或在方法签名中使用 throws 关键字声明。
    • 继承关系:Checked 异常是 Exception 类的子类,但不包括 RuntimeException 的子类。
      示例:常见的 Checked 异常
      • IOException:输入输出操作时可能抛出的异常。
      • SQLException:进行数据库操作时可能抛出的异常。
      • ClassNotFoundException:在尝试加载类时,如果找不到指定的类,则抛出此异常。

    Unchecked 异常

    • 定义:Unchecked 异常是指那些在编译时不被强制检查的异常,这类异常通常是由于编程错误(如逻辑错误或不当的 API 使用)引起的,在运行时才会出现。
    • 处理方式:可以选择性地处理,不处理也不会有编译错误。程序员可以根据实际需求决定是否捕获和处理这些异常。
    • 继承关系:Unchecked 异常是 RuntimeException 类及其子类。
      示例:常见的 Unchecked 异常
      • NullPointerException:尝试对 null 对象调用方法或访问字段时抛出的异常。
      • ArrayIndexOutOfBoundsException:数组访问越界时抛出的异常。
      • IllegalArgumentException:传递非法参数给方法时抛出的异常。

    Exception和Error有什么区别?

    在 Java 中,Exception 和 Error 都是继承了 Throwable 类的子类,但它们有不同的用途和含义:

    1. Exception:
      • Exception 类表示程序运行时可能遇到的异常情况,通常是可以通过编程手段处理的。
      • 它分为两种类型:检查型异常(Checked Exceptions)和非检查型异常(Unchecked Exceptions)。
        • 受检异常(checked exception)其实就是编译时异常,继承自 Exception,即在编译阶段检査代码中可能会出现的异常,需要开发者显式的捕获(catch)或声明抛出(throw)这种异常,否则编译就会报错,这是一种强制性规范。
          • IOException、SQLException、FileNotFoundException
        • 非受检异常(unchecked exception)就是运行时异常,继承自RuntimeException 类,是指在运行期间可能会抛出的异常,编译期不强制要求处理,之所以不强制是因为它可以通过完善代码避免报错。
          • NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException;空指针,数组越界,除0
    2. Error:
      • Error 类表示编译时和系统错误,通常是不可恢复的情况,比如Java虚拟机(JVM)崩溃或者内存溢出。
      • 程序通常不应该捕获 Error 类型的异常,因为它们指示的是严重的问题,这些问题超出了普通应用程序能够处理的范围。
      • 出现 Error 的时候,Java虚拟机通常会终止线程或者退出程序。
        虚拟机错误(VirtualMachineError)、内存溢出错误(OutOfMemoryError

    简而言之,Exception 是指那些可能被程序捕获并处理的异常情况,而 Error 指的是那些表示严重问题且程序通常无法处理的异常情况。

    • 处理方式Exception 需要被处理,而 Error 通常不需要也不应该被捕获。
    • 可恢复性Exception 有可能是可恢复的,而 Error 通常表示不可恢复的情况。
    • 编译时要求:检查型异常需要在编译时处理,而 Error 和运行时异常不需要。
    • 用途Exception 用于处理应用程序级别的异常情况,Error 用于处理系统级别的错误。

    什么是注解?

    注解主要用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它主要的作用有以下四方面:

    • 生成文档,通过代码里标识的元数据生成javadoc文档。
    • 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
    • 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
    • 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。

    常见的注解

    • Java 提供了一系列内置的注解(Annotations),这些注解在标准库中被广泛使用,以下是一些常见的内置注解:
    1. @Override - 表示一个方法声明打算重写一个超类中的方法。如果方法没有实际重写任何超类方法,编译器会报错。
    2. @SuppressWarnings - 告诉编译器忽略特定的警告。你可以指定要忽略哪些警告,例如 uncheckeddeprecation 等。
    3. @SafeVarargs - 当你声明一个带有可变数量参数的方法或者构造函数,并且确信对泛型类型参数使用时是类型安全的,可以用这个注解来抑制未检查的警告。
    4. @FunctionalInterface - 表示一个接口类型声明是函数式接口,即该接口有且只有一个抽象方法。
    • Java 还提供了用于元注解(注解的注解)的几个注解:
    1. @Retention - 指定注解的保留策略,即注解信息保留到什么阶段(源代码、class文件或运行时)。
    2. @Target - 指定注解可以用于哪些元素(例如方法、字段、类等)。
    3. @Documented - 表示这个注解应该被 javadoc 工具记录。
    4. @Inherited - 表示注解类型可以被继承。

    第三方注解:

    • Spring框架注解
      • @Component 及其衍生注解如 @Service@Repository 用于标记组件类。
      • @Autowired 用于自动装配依赖。
      • @RequestMapping 用于映射HTTP请求到控制器方法。
      • @Transactional 用于声明事务边界。
    • Java Persistence API (JPA) 注解
      • @Entity 用于标记实体类。
      • @Table 和 @Column 用于指定数据库表和列的映射。
      • @Id 和 @GeneratedValue 用于标记主键字段。
    • JUnit注解
      • @Test 用于标记测试方法。
      • @Before 和 @After 用于标记在每个测试方法之前或之后执行的方法。
      • @BeforeClass 和 @AfterClass 用于标记在所有测试方法之前或之后执行的方法。

    Java中的动态代理是什么?

    Java 中的动态代理是一种运行时创建代理对象的能力,它允许你在运行时动态地创建一个符合某一接口的对象,而不必事先定义这个类的具体实现。动态代理主要涉及两个类:java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler
    动态代理是实现面向切面编程(AOP)的一个关键机制。

    动态代理的用途非常广泛,包括但不限于:

    • 日志记录:记录方法的调用。
    • 事务管理:在方法调用前后开始和结束事务。
    • 安全检查:在方法调用前检查权限。
    • 性能监控:计算方法调用的耗时。

    如何实现动态代理?

    1. 接口:首先,你需要定义一个或多个接口,这些接口定义了代理对象应该实现的方法。
    2. 实现 InvocationHandler 接口,这个接口只有一个方法 invoke。当代理对象的方法被调用时,这些调用会被转发到 InvocationHandler 的 invoke 方法。
      1
      2
      3
      4
      public interface InvocationHandler {
      public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable;
      }
      • Object proxy:代理对象。
      • Method method:被调用的方法。
      • Object[] args:被调用方法的参数。
    3. Proxy:使用 Proxy 类的 newProxyInstance 方法来创建代理实例。
      1
      2
      3
      public static Object newProxyInstance(ClassLoader loader,
      Class<?>[] interfaces,
      InvocationHandler h)
      • ClassLoader loader:用于加载代理类的类加载器。
      • Class<?>[] interfaces:代理类要实现的接口列表。
      • InvocationHandler h:处理接口方法调用的处理器。
    4. 使用代理:一旦创建了代理对象,它就可以像其他任何对象一样使用。当代理对象的方法被调用时,这些调用会被转发到 InvocationHandler 的 invoke 方法,你可以在 invoke 方法中添加自定义逻辑。

    了解 Java 的序列化和反序列化吗?能解释一下序列化的过程和作用吗?

    Java对象只存在Java虚拟机中,如果要保存或者传输就要用到序列化。序列化(Serialization)和反序列化(Deserialization)是处理对象持久化和传输的两个重要概念。它们允许将对象的状态转换为一种可存储或传输的格式,并在需要时重新构造对象。

    序列化(Serialization)
    序列化是将对象的状态转换为字节流的过程,这样对象可以被存储到磁盘、传输到网络上,或者在不同的系统之间进行共享。序列化的目的是为了对象的持久化(例如存储到文件中)或远程传输。
    序列化的过程

    1. 实现 Serializable 接口:要使一个对象能够被序列化,它的类必须实现 java.io.Serializable 接口。这个接口是一个标记接口,不包含任何方法。
    2. 创建对象并序列化:通过 ObjectOutputStream 将对象写入一个输出流,这个流可以是文件流、网络流等。
    3. 将对象写入流ObjectOutputStream.writeObject() 方法将对象的状态转换为字节流,并写入到输出流中。

    反序列化(Deserialization)
    反序列化是将字节流转换回对象的过程。这使得我们可以从持久化存储中恢复对象的状态。
    反序列化的过程

    1. 读取字节流:通过 ObjectInputStream 从输入流中读取字节流。
    2. 恢复对象ObjectInputStream.readObject() 方法将字节流转换回对象实例。

    序列化的作用

    1. 对象持久化:序列化允许将对象的状态保存到文件或数据库中,从而在系统重启或关闭后能够恢复对象的状态。
    2. 远程通信:在分布式系统中,序列化可以用于通过网络传输对象,使得不同系统之间可以交换对象数据。
    3. 深拷贝:序列化可以用于实现深拷贝(deep copy)操作,通过序列化和反序列化将对象的完整副本复制到新的实例中。
    4. 缓存:序列化可以用于缓存对象的状态,使得在以后需要时可以快速恢复对象。

    什么是反射机制?

    JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

    1. Class对象:反射机制的基础是类的Class对象,它包含了与类有关的信息。在运行时,Java虚拟机为每个类管理一个Class对象,可以通过它来获取类的各种信息。
    2. 获取Class对象:有多种方式可以获取一个类的Class对象:
      • 调用对象的getClass()方法。
      • 使用.class语法,例如String.class
      • 使用Class.forName(String className)方法,其中className是类的全限定名。
        ![[Pasted image 20240820213909.png]]

    什么是SPI机制?

    SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦
    ![[Pasted image 20240726095418.png]]

    SPI和API的区别是什么?

    ![[Pasted image 20240820220528.png]]
    一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。

    • 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
    • 当接口存在于调用方这边时,这就是 SPI 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。

    什么是Java的泛型?

    Java 中的泛型是一种在编译阶段提供类型安全的机制,它主要有以下特点:

    1. 类型参数化

    • 允许在定义类、接口和方法时使用类型参数,而不是具体的类型。例如,ArrayList<T>中的T就是类型参数,可以在创建ArrayList对象时指定具体类型,如ArrayList<String>

    2. 增强类型安全

    • 在编译期进行类型检查,避免在运行时出现类型转换异常。比如,没有泛型时,从一个集合中取出元素可能需要强制类型转换,容易导致ClassCastException;而使用泛型后,编译器能确保放入和取出的元素类型符合定义。

    3. 代码复用性

    • 编写通用的代码,可以适用于不同的类型。例如,定义一个泛型方法来交换两个变量的值,这个方法可以用于IntegerString等各种类型的变量交换。

    4. 泛型类和泛型接口

    • 泛型类:如class Box<T> { private T t; },在类的内部可以使用类型参数T来定义成员变量、方法参数和返回值类型等。
    • 泛型接口:类似地,interface Generator<T> { T next(); },实现接口时指定具体的类型。

    SPI机制的缺陷?

    • 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
    • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
    • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。

    BIO NIO AIO

    BIO(blocking I/O) : 就是传统的IO,同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过连接池机制改善(实现多个客户连接服务器)。

    NIO :全称 java non-blocking IO,是指 JDK 提供的新 API。从JDK1.4开始,Java提供了一系列改进的输入/输出的新特性,被统称为NIO(即New IO)。NIO是同步非阻塞的,服务器端用一个线程处理多个连接,客户端发送的连接请求会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理。

    AIO:JDK 7 引入了 Asynchronous I/O,是异步非阻塞的 IO。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理,完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
    ![[Pasted image 20240906201141.png]]

    NIO与普通IO的区别

    1. 面向流与面向缓冲
      • 传统I/O是面向流的,意味着每次只能从流中读取或写入一个字节,数据是顺序处理的。
      • NIO是面向缓冲的,数据从通道读取到缓冲区中,或者从缓冲区写入到通道中,缓冲区提供了数据的缓存,可以更高效地处理数据。
    2. 阻塞与非阻塞I/O
      • 传统I/O的操作是阻塞的,这意味着在数据读取或写入过程中,线程会一直等待,直到操作完成。
      • NIO支持非阻塞模式,通过选择器,一个线程可以管理多个通道,不必等待每个通道的I/O操作完成,从而提高了效率。
    3. 选择器
      • 传统I/O没有选择器的概念,每个操作都需要单独的线程来处理。
      • NIO的选择器允许单个线程监控多个通道的事件(如数据可读、可写等),这使得单个线程可以处理多个通道的I/O操作,大大减少了线程数量和上下文切换的开销。

    简单叙述一下Restful API

    概念方面

    • REST(Representational State Transfer)是一种软件架构风格,而基于这种风格设计的应用程序接口就是 Restful API。它通过使用统一的接口和标准的 HTTP 方法来对资源进行操作。

    资源操作

    • 资源是 Restful API 的核心概念,它可以是任何实体,比如用户、订单、产品等。
    • 通过 HTTP 方法对这些资源进行操作,常见的 HTTP 方法包括:
      • GET:用于获取资源的信息,类似于查询操作。
      • POST:通常用于创建新的资源。
      • PUT:一般用于更新整个资源。
      • PATCH:常用于对资源进行部分更新。
      • DELETE:用于删除指定的资源。

    接口设计特点

    • 无状态性:每个请求都包含了理解和处理该请求所需的所有信息,服务器不会在不同请求之间保存客户端的状态信息。
    • 可寻址性:每个资源都有一个唯一的标识符,通常是一个 URL,通过这个 URL 可以对资源进行操作。
    • 基于资源的操作:所有的操作都是围绕资源展开的,设计接口时重点考虑资源的表示和对资源的操作方式。

    什么是OOM(内存溢出),如何排查

    含义

    • 指程序在申请内存时,没有足够的内存空间供其使用,导致程序无法正常运行的情况。

    产生原因

    • 内存中加载的数据量过大
      • 例如,在处理大规模数据时,将大量数据一次性加载到内存中,而这些数据的大小超过了系统所能提供的内存容量。比如,在进行大数据分析时,如果没有合理地分批次处理数据,而是直接将一个非常大的数据集全部读入内存,就容易导致 OOM。
    • 内存泄漏
      • 程序中存在内存泄漏问题,即一些不再使用的对象没有被及时回收,导致内存被这些无用的对象占用。常见于一些长期运行的程序,比如 Web 服务器。如果程序中存在对对象的引用没有正确释放的情况,随着时间的推移,这些无用对象会不断积累,最终耗尽内存。
    • 虚拟机内存分配不合理
      • 在 Java 等使用虚拟机的环境中,如果虚拟机的堆内存(-Xmx)、栈内存(-Xss)等参数设置不当,也可能导致 OOM。例如,将堆内存设置得过小,而程序运行过程中又需要创建大量的对象,就会超出堆内存的限制,引发 OOM。

    如何排查?

    从日志分析方面

    • 查看错误日志
      • 程序发生 OOM 时,通常会在日志中记录相关的错误信息。例如在 Java 中,会在日志中显示java.lang.OutOfMemoryError以及可能的导致原因的提示信息,如Java heap space(堆内存溢出)、GC overhead limit exceeded(垃圾回收时间过长)等。仔细分析这些错误信息可以初步判断 OOM 的原因。
    • 分析 GC 日志
      • 如果程序运行环境有垃圾回收(GC)机制(如 Java),分析 GC 日志可以了解内存的使用和回收情况。通过查看 GC 的频率、回收的内存量、GC 停顿时间等信息,可以判断是否存在内存泄漏或者内存分配不合理的情况。例如,频繁的 Full GC 且回收的内存量很少,可能意味着存在内存泄漏。
        从内存监控方面
    • 使用内存分析工具
      • Java VisualVM:可以实时监控 Java 程序的内存使用情况、线程状态、类加载等信息。它能够生成堆内存的快照,通过对比不同时间点的快照,可以找出内存中对象数量异常增长的类,从而发现可能的内存泄漏点。
      • MAT(Memory Analyzer Tool):专门用于分析 Java 堆内存的工具。它可以分析堆内存快照,找出占用内存最多的对象、对象之间的引用关系等信息,帮助定位内存泄漏的根源。
      • 其他工具:如 YourKit Java Profiler 等,这些工具提供了更详细的内存和性能分析功能。
        从代码审查方面
    • 检查资源释放情况
      • 检查程序中资源的使用和释放逻辑,特别是对内存占用较大的资源,如文件流、数据库连接、网络连接等。确保在使用完这些资源后,及时关闭或释放,以防止资源泄漏导致内存占用不断增加。例如,在使用文件流读取文件后,应该在 finally 块中确保文件流被关闭
    • 检查缓存使用情况
      • 如果程序中使用了缓存机制,检查缓存的大小是否合理,以及是否存在缓存数据无限增长的情况。例如,在一个 Web 应用中,如果使用缓存来存储用户会话信息,需要根据实际情况设置缓存的最大容量,并定期清理过期的缓存数据。

    1.2 集合类框架

    集合相关类和接口都在java.util中,主要分为3种:List(列表)、Map(映射)、Set(集)。
    ![[Pasted image 20240717231005.png]]

    Collection接口下面有哪些集合类_3?List, Set, Queue, Map 四者的区别?

    List 接口:

    • ArrayList:基于动态数组,查询速度快,插入、删除慢。
    • LinkedList:基于双向链表,插入、删除快,查询速度慢。
    • Vector:线程安全的动态数组,类似于 ArrayList,但开销较大,
      Set 接口:
    • HashSet:基于哈希表,元素无序,不允许重复。
    • LinkedHashSet:基于链表和哈希表,维护插入顺序,不允许重复
    • TreeSet:基于红黑树,元素有序,不允许重复。
      Queue 接口:
    • PriorityQueue:基于优先级堆,元素按照自然顺序或指定比较器排序。
    • LinkedList:可以作为队列使用,支持 FIFO(先进先出)操作。

    Map 接口 (不属于Collection)

    • HashMap:基于哈希表,键值对无序,不允许键重复
    • LinkedHashMap:基于链表和哈希表,维护插入顺序,不允许键重复
    • TreeMap:基于红黑树,键值对有序,不允许键重复
    • Hashtable:线程安全的哈希表,不允许键或值为 nul
    • ConcurrentHashMap:线程安全的哈希表,适合高并发环境

    1.2.1 ArrayList相关

    [[ArrayList源码解析]]

    ArrayList和LinkedList有什么区别?

    • 数据结构不同
      • ArrayList基于数组实现
      • LinkedList基于双向链表实现
        基于此,ArrayList和LinkedList的区间就变为了数组和链表的区别
    • 多数情况下,ArrayList更利于查找,LinkedList更利于增删
    • ArrayList支持随机访问,而Linked只支持顺序访问
    • ArrayList需要一块连续的空间,而LinkedList基于链表,内存空间不连续

    在项目中一般是不会使用到 LinkedList 的,需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且性能通常会更好。

    ArrayList的扩容机制了解吗?

    ArrayList是基于数组的集合,数组的容量是在定义的时候确定的,如果数组满了,再插入,就会数组溢出。所以在插入时候,会先检查是否需要扩容,如果当前容量+1超过数组长度,就会进行扩容。ArrayList的扩容是创建一个1.5倍的新数组,然后把原数组的值拷贝过去。
    无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。

    什么是迭代器?

    迭代器(Iterator)是一种设计模式,用于在集合对象(如列表、集合、数组等)上提供一种方法,使之能够顺序地访问集合中的各个元素,而不暴露其内部的表示。在Java中,迭代器是通过Iterator接口来实现的,该接口包含三个基本方法:next()hasNext()remove()

    CopyOnWriteArrayList是如何保证线程安全的?

    CopyOnWriteArrayList主要是通过“写时复制”(Copy-on-Write)策略来保证线程安全。

    CopyOnWriteArrayList 内部维护了一个数组 elementData 来存储元素,并使用一个 volatile 字段来确保数组的可见性。这意味着当一个线程修改了数组之后,其他线程能够立即看到最新的数组版本。

    1. 读操作
    • 读操作(如 get() 和 iterator())不会锁定整个列表,而是返回当前的数组引用。
    • 由于 elementData 是 volatile 的,所以读操作总是能看到最新的数组状态。
    • 这意味着读操作可以并发执行而不会阻塞。
    1. 写操作
    • CopyOnWriteArrayList内部有一个可重入锁(ReentrantLock)来保证线程安全,但这个锁只在写操作时才会被使用。
    • 当进行修改操作时,线程会先获取锁,然后复制底层数组,并在新数组上执行修改。
    • 修改完成后,通过volatile关键字修饰的引用来确保新的数组对所有线程可见。
    • 由于读操作不需要获取锁,因此多个线程可以同时进行读操作,而不会相互干扰。

    Java的CopyOnWriteArrayList 和Collections.synchronizedList 有什么区别?分别有什么优缺点?

    CopyOnWriteArrayList 和 Collections.synchronizedList 都是Java中用于实现线程安全的列表类。

    1. 实现机制
      CopyOnWriteArrayList:
    • 采用写时复制(Copy-on-Write)策略,每次修改操作(添加、删除、设置)都会创建并重新发布一个新的底层数组副本。
    • 读取操作不需要加锁,因为数组在迭代器创建时已经固定,不会改变。
      Collections.synchronizedList:
    • 通过对普通列表(如ArrayList)进行包装,所有公共方法都通过同步代码块来保证线程安全。
    • 读取和写入操作都需要加锁,以防止并发修改导致的数据不一致。
    1. 性能特点的区别:
      CopyOnWriteArrayList:
    • 优点:读取操作非常快,因为不需要加锁。
    • 缺点:写操作性能较差,因为每次修改都需要复制整个数组,内存占用较高,特别是对于大数据量的列表。
      Collections.synchronizedList:
    • 优点:写操作性能相对较好,因为不需要复制整个数组。
    • 缺点:读取操作性能较差,因为需要加锁,多个线程同时读取时会有竞争。
    1. 使用场景的区别:
      CopyOnWriteArrayList:
    • 适用于读多写少的场景,例如事件通知系统,其中监听器注册(写操作)不频繁,但事件通知(读操作)非常频繁。
      Collections.synchronizedList:
    • 适用于读操作和写操作都相对频繁的场景,或者当数据量较大且写操作的性能成为瓶颈时。

    1.2.2 HashMap相关

    Hash函数的构造方法

    1. 直接定址法
      • 这种方法直接使用关键字的某个线性函数作为哈希地址,即H(key) = a * key + b,其中ab是常数。
    2. 平方取中法
      • 对关键字做平方运算,然后取平方结果的中间几位作为哈希地址。这种方法适用于关键字中的每一位数字分布比较均匀的情况。
    3. 除留余数法
      • 这是最为常用的一种哈希函数构造方法,H(key) = key mod p,其中p是一个不大于哈希表长度的质数。这种方法简单,能够较好地分布关键字。

    Hash冲突的解决方法

    1. 链地址法(Separate Chaining)
      • 在这种方法中,哈希表的每个槽位(bucket)维护一个链表。当冲突发生时,冲突的元素将被添加到对应槽位的链表中。查找时,需要遍历链表以找到特定的元素。
    2. 开放地址法(Open Addressing)
      • 当发生冲突时,寻找下一个空槽位并将元素插入。有以下几种探测序列:
        • 线性探测(Linear Probing):从发生冲突的位置开始,依次探测下一个槽位,直到找到空槽位。
        • 二次探测(Quadratic Probing):使用一个二次函数来计算下一个槽位的位置。
        • 双重哈希(Double Hashing):使用第二个哈希函数来计算探测的步长。
        • 平方探测:
    3. 再哈希法(Rehashing)
      • 当哈希表中的元素太多,冲突频繁时,可以创建一个更大的新哈希表,并使用一个新的哈希函数将所有元素重新插入到新表中。

    HashMap的数据结构

    HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。
    HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个

    JDK1.8以前,HashMap是由数组+链表实现的,用拉链法来解决哈希冲突。
    JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

    • 但是其实哈希冲突能达到8个的概率非常非常之下,基本上都是数组+链表。

    HashMap和HashTable的区别

    HashMap 和 Hashtable 都是Java中用于存储键值对的数据结构。

    1. Hashtable:是Java 1.0的遗留类。- HashMap:在Java 1.2中引入,是Java集合框架的一部分。
    2. 线程安全
      • HashMap:不是线程安全的。如果多个线程同时访问HashMap,并且至少有一个线程在结构上修改了map(添加或删除任何元素),则必须外部同步。
      • Hashtable:是线程安全的,所有的公共方法都是同步的。这意味着在多线程环境中,一个线程访问Hashtable时,其他线程必须等待。
    3. 性能 HashMap通常提供比Hashtable更高的性能,因为它不是同步的。
    4. HashMap允许使用一个null键和多个null值;Hashtable:不允许使用null键或null值。

    由于Hashtable的同步机制,在不需要线程安全的场景下,HashMap通常是更好的选择,因为它提供了更好的性能和更大的灵活性。如果需要线程安全,则应该考虑使用ConcurrentHashMap,它比Hashtable提供了更好的并发性能。从Java 1.5开始,Hashtable已经被认为是过时的,建议在新代码中使用HashMap或者ConcurrentHashMap

    hashSet和HashMap的区别

    1
    public HashSet() { map = new HashMap<>();}
    • HashSet
      • HashSet 实现了 Set 接口,它是一个集合,用于存储不包含重复元素的集合。
      • 主要用途是确保集合中的元素唯一性,不关心元素的顺序。
      • 只存储单一的元素
    • HashMap
      • HashMap 实现了 Map 接口,它是一个映射,用于存储键值对(key-value pairs)。
      • 主要用途是通过唯一的键快速检索对应的值.
      • 存储键值对,每个键对应一个值。

    HashSet 底层就是基于 HashMap 实现的。在其他方面就没有什么不同之处。

    什么是负载因子?为什么HashMap的负载因子是0.75?

    负载因子(Load Factor)是衡量哈希表(如Java中的HashMap)满程度的指标,它是哈希表中已存储的键值对数量(称为”加载量”或”元素数量”)与哈希表容量的比值。用数学公式表示为:
    负载因子=元素数量/哈希表容量​

    负载因子用于确定何时对哈希表进行扩容(即增加哈希表的容量,并重新分配所有现有的键值对)。以下是关于负载因子的几个要点:

    1. 控制哈希冲突:负载因子越低,哈希表中的空位就越多,这有助于减少哈希冲突的概率,但同时也意味着空间利用率较低。
    2. 空间与时间效率的平衡:较高的负载因子可以提高空间利用率,但同时也增加了哈希冲突的概率,导致性能下降。
    3. 扩容阈值:当哈希表中的元素数量达到负载因子与哈希表容量的乘积时,哈希表会进行扩容。

    选择0.75作为HashMap的默认负载因子是经过权衡的。以下是几个原因:

    1. 平衡空间与时间效率:0.75是一个经验值,它在时间和空间效率之间提供了一个较好的平衡。如果负载因子设置得太高,虽然空间利用率会增加,但哈希冲突的概率也会增加,导致性能下降。如果设置得太低,虽然冲突减少,但会浪费更多的空间。
    2. 泊松分布:在理想情况下,哈希函数会将元素均匀地分布在整个哈希表中。当负载因子为0.75时,根据泊松分布,哈希桶(bucket)中的元素分布大致符合以下规律:一个桶中有一个元素的概率约为0.75,有两个元素的概率约为0.75^2,以此类推。这种分布使得大多数桶中的元素数量不会太多,有助于保持HashMap操作的平均时间复杂度为O(1)。
    3. 实践经验:经过多年的使用和优化,0.75被证明是一个适用于大多数场景的默认值。当然,对于特定的应用场景,可以根据实际情况调整负载因子。

    使用HashMap时,有什么提升性能的技巧?

    1)合理设置初始容量:
    如果在使用时可以预估 HashMap 存储的数据量大小,那么需要在创建时设置一个合适的初始容量,以避免频繁的扩容操作。 Java 中 HashMap 默认初始容量是 16。
    2)调整负载因子:
    官方提供的默认负载因子是 0.75。
    可以根据具体应用场景调整这个值,较低的负载因子会减少冲突,提高查找效率,但会占用更多内存。较高的负载因子则会减少内存消耗,但可能增加)冲突的概率,降低查找效率。
    3)确保 hashCode 均匀分布:
    对应 key 的 hashCode0 方法生成的哈希值需均匀分布,减少哈希冲奕。避免使用质量不高的哈希函数,防止大量键映射到相同的槽位上,造成性能瓶颈。

    4)并发场景
    在多线程环境中,如果 HashMap 的线程安全是一个问题,考虑使用 ConcurrentHashMap,它提供了更好的并发性能。
    5)内存占用
    如果内存使用是一个问题,考虑使用 HashMap 的子类 LinkedHashMap 或 WeakHashMap,它们可以根据需要释放内存。

    HashMap的扩容机制

    1. 扩容条件
      • HashMap中的元素数量达到capacity * load factor(容量乘以负载因子)时,就会触发扩容。默认的负载因子是0.75。
    2. 计算新容量
      • 扩容时,通常将哈希表的容量扩大为原来的两倍。如果当前容量已经是最大容量(MAXIMUM_CAPACITY = 1 << 30,即2的30次方),则不再扩容。
    3. 创建新哈希表
      • 创建一个新的哈希表,其容量是原容量的两倍。
    4. 重新哈希
      • 遍历旧哈希表中的所有元素,并重新计算每个元素在新哈希表中的位置。由于容量增加了,计算哈希地址的模数也相应增加了,因此大多数元素的位置会改变。
    5. 转移元素
      • 将旧哈希表中的所有元素重新插入到新哈希表中。这个过程涉及到重新计算每个元素的哈希值,并根据新的容量确定它们在新表中的位置。
    6. 释放旧哈希表
      • 一旦所有元素都被转移到了新的哈希表中,旧哈希表中的元素就可以被垃圾回收器回收了。

    HashMap的链表是头插法还是尾插法?

    在Java 8之前,HashMap使用的是头插法来处理散列冲突,因为这样操作比较快,新元素总是被插入到链表的头部。然而,这种做法在多线程环境下可能会导致死循环。因此,从Java 8开始,HashMap改为使用尾插法,新元素会被插入到链表的尾部。

    1. 迭代顺序稳定性:尾插法可以保持插入的顺序,遵循先进先出的原则,这对于某些应用场景来说是有好处的。例如,在HashMap用于缓存实现的时候,维持插入顺序可能会更符合缓存的失效策略。
    2. 并发修改安全:在对链表进行遍历的时候,如果同时有新的元素插入,头插法可能会导致新插入的元素在不期望的时刻被访问到,而尾插法则因为是在链表末尾插入,不会影响到当前的遍历进度。
    3. 扩容安全问题:使用头插法在多线程环境下扩容的话,可能会产生循环链表,导致无限取值。
      https://blog.csdn.net/zoey_peak/article/details/139896908

    在链表变为红黑树后,若元素数量低于8会变回来吗?

    在Java 8及之后的版本中,当链表中的元素数量达到8时,链表被转换成红黑树。同样地,如果因为删除操作导致红黑树中的元素数量减少到6时,红黑树会被转换回链表。这个转换的过程是为了在元素数量较少时减少红黑树带来的额外开销,因为链表在元素数量较少时通常具有更好的性能。

    以下是转换的逻辑:

    • 链表转红黑树:当链表的长度超过TREEIFY_THRESHOLD(默认为8)时,如果数组的长度大于MIN_TREEIFY_CAPACITY(默认为64),则会触发扩容操作,而不是立即转换为红黑树。如果数组的长度小于MIN_TREEIFY_CAPACITY,链表会被转换成红黑树。
    • 红黑树转链表:当红黑树中的节点数量减少到UNTREEIFY_THRESHOLD(默认为6)时,红黑树会被转换回链表。

    这些阈值的选择是基于对性能的权衡,确保在大多数情况下HashMap都能提供良好的性能。需要注意的是,这些阈值是可以调整的,但通常不建议修改,除非有明确的需求和充分的测试。

    ConcurrentHashmap为什么安全?

    1ConcurrentHashmap线程安全在jdk1.7版本是基于分段锁实现,在jdk1.8是基于CAS+synchronized 实现。(+下面一起答)

    • JDK1.7:分段锁 (ReentrantLock)
      从结构上说,JDK1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment段数组,Segment继承于ReentrantLock(可重入锁),Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key、value的能力能指向下一个节点的指针。实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。

    • ConcurrentHashMap去掉了分段锁,改用CAS操作和synchronized关键字来保证并发安全性。有以下几个方面:

      • CAS(Compare And Swap)是一种无锁算法,通过比较并交换的方式来更新值,保证了操作的原子性。
      • 在JDK 1.8中,ConcurrentHashMap在内部使用了一个叫做Node的结构来存储键值对,当对某个桶(bucket)进行结构修改时(如添加或删除节点),会使用synchronized锁定这个桶的头节点。这种锁的粒度更细,从而减少了锁竞争,提高了并发性。
      • ConcurrentHashMap中的很多变量都被声明为volatile,这意味着对这些变量的读写操作都是直接在主存中进行,任何一个线程对变量的修改都会立即对其他线程可见,保证了内存的可见性。

    ConcurrentHashmap的 put()操作流程

    1. 计算键的哈希值:首先对传入的键(key)计算哈希值。
    2. 定位桶位置:通过哈希值找到对应的桶位置,如果该位置没有元素,则使用CAS操作尝试将新节点放入该位置。
    3. 添加元素到链表或树
      • 如果当前位置为null,则通过CAS写入,如果CAS写入失败,通过自旋保证写入成功
      • 如果该位置已经有元素,则对该桶的头节点进行加锁(synchronized),这是细粒度锁的应用。
      • 如果当前桶的数据结构是链表,遍历链表,如果发现键已经存在,则更新对应的值并返回旧值。
      • 如果链表长度超过一定阈值(默认为8),则将链表转换为红黑树,以提高搜索效率。
    4. 添加元素到红黑树:如果当前桶的数据结构是红黑树,则在树结构中添加新的节点。
    5. 扩容:如果当前元素个数达到扩容阈值,即当前元素个数 >= (加载因子 * 桶数量),则进行扩容操作。
      ![[d94d4dc97e26afde7093c92ce0288f9c.jpg]]
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      public V put(K key, V value) {
      return putVal(key, value, false);
      }

      /** Implementation for put and putIfAbsent */
      final V putVal(K key, V value, boolean onlyIfAbsent) {
      if (key == null || value == null) throw new NullPointerException();
      //1、计算出hash值
      int hash = spread(key.hashCode());
      int binCount = 0;
      for (Node<K,V>[] tab = table;;) {
      Node<K,V> f; int n, i, fh;
      //2、判断当前数据结构是否从未放过数据,即是否未初始化,为空则先执行初始化
      if (tab == null || (n = tab.length) == 0)
      tab = initTable();
      //3、通过key的hash判断当前位置是否为null
      //(通过数组长度减一和hash做与运算得到要判断的当前数组位置)
      else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
      //如果当前位置为null,则通过CAS写入,如果CAS写入失败,通过自旋保证写入成功
      if (casTabAt(tab, i, null,
      new Node<K,V>(hash, key, value, null)))
      break; // no lock when adding to empty bin
      }
      //4、当前hash值等于MOVED(-1)时,需要进行扩容
      else if ((fh = f.hash) == MOVED)
      //扩容
      tab = helpTransfer(tab, f);
      else {
      V oldVal = null;
      //5、当上面的内容都不满足时,采用synchronized阻塞锁,来将数据进行写入
      synchronized (f) {

      }
      if (binCount != 0) {
      //6、如果数量大于TREEIFY_THRESHOLD(8),需要转化为红黑树
      }
      }
      }
      addCount(1L, binCount);
      return null;
      }

    TreeMap LinkedhashMap WeakHashMap IdentityHashMap

    1. TreeMap 保证排序顺序
    • 特点TreeMap 是基于红黑树实现的,它可以保证键的排序顺序。
    • 排序:键必须实现 Comparable 接口或者提供一个 Comparator,以便 TreeMap 可以根据键的自然顺序或者指定的比较器进行排序。
    • 性能:插入和删除操作的时间复杂度为 O(log n),因为需要维护树的平衡。
    • 用途:当需要按键的顺序遍历键值对时,TreeMap 是一个很好的选择。
    1. LinkedHashMap 保证插入顺序
    • 特点LinkedHashMap 继承自 HashMap,并且维护了一个双向链表,用来记录元素的插入顺序。
    • 顺序:迭代 LinkedHashMap 时,元素会按照它们被插入的顺序返回,或者按照它们最后一次被访问的顺序(如果使用了 accessOrder 参数)。
    • 性能:与 HashMap 相比,LinkedHashMap 的性能略低,因为它需要维护额外的链表。
    • 用途:当需要按照插入顺序或者访问顺序遍历键值对时,LinkedHashMap 是一个合适的选择。
    1. WeakHashMap
    • 特点WeakHashMap 的键是弱引用类型。如果一个键对象没有被其他强引用所持有,那么垃圾回收器可能会回收它,即使它在 WeakHashMap 中。
    • 用途:当键的生命周期较短或者需要自动清理键值对时,WeakHashMap 是一个有用的选择。
    • 性能:由于键是弱引用,WeakHashMap 的性能可能略低于 HashMap
    1. IdentityHashMap
    • 特点IdentityHashMap 使用 == 而不是 equals 来比较键,这意味着它比较的是对象引用而不是对象的内容。
    • 用途:当需要基于对象身份(而不是对象值)进行映射时,IdentityHashMap 是一个合适的选择。
    • 性能:由于使用了简单的引用比较,IdentityHashMap 的性能通常比其他基于 equals 的 Map 实现要快。

    1.3 java并发

    1.3.1 Java并发理论基础

    多线程的出现是要解决什么问题的?

    1. 提高程序的响应速度:在单线程程序中,如果任务执行时间较长,那么整个程序将在这段时间内无法响应用户操作。通过多线程技术,可以将任务分解为多个子任务并行执行,从而提高程序的响应速度。
    2. 简化模型:多线程可以简化程序设计,将复杂的异步事件处理转换为简单的同步模型。例如,GUI程序常常使用一个线程来处理用户界面,而另一个线程来处理后台任务。
    3. 异步处理:多线程允许程序进行异步处理,即程序可以在等待某些操作完成的同时继续执行其他任务。
    • CPU 增加了缓存,以均衡与内存的速度差异;// 导致 可见性问题
    • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致 原子性问题
    • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致 有序性问题

    🌟线程不安全是指什么? 举例说明

    1. 可见性问题:一个线程对共享变量的修改,另外一个线程能够立刻看到。但是因为CPU缓存的问题可能会导致线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
    2. 原子性问题:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。一条程序语句会被分成多条微指令运行,由于分时复用和流水线等机制,会导致某些操作不生效。
    3. 有序性问题:程序执行的顺序按照代码的先后顺序执行。执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。多线程之间重排可能会导致程序没有按照预先设定的程序执行。

    如何保证线程安全?有哪些方法?

    Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括:

    • volatile、synchronized 和 final 三个关键字
    • Happens-Before 规则:Happens - Before 是 Java 内存模型(JMM)中的一个重要概念,用于确定在多线程环境下操作之间的可见性和顺序性。如果一个操作 A Happens - Before 另一个操作 B,那么操作 A 的结果对操作 B 是可见的,并且操作 A 在时间顺序上先于操作 B 发生(这里的 “先于” 是一种逻辑上的顺序,不一定是实际执行顺序)。
    1. 从原子性角度来说,Java内存模型(JMM)保证了基本读取和赋值是原子性操作,通过synchronized和Lock可以保证代码块的原子性执行。
    2. 从可见性角度来说,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
    3. 从有序性角度来说,Java不仅可以通过volatile关键字来保证一定的“有序性”,还可以通过synchronized和Lock来保证每个时刻是有一个线程执行同步代码,同时还可以通过Happens-Before 规则来保证有序性。

    🌟线程安全的实现方法

    1. 互斥同步
      互斥同步属于一种 悲观 的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁。
      互斥同步是常见的一种线程安全实现方法,主要是通过锁来实现。
    • synchronized 关键字
      • 同步方法:当一个线程访问一个对象的 synchronized 方法时,其他线程不能同时访问该对象的其他 synchronized 方法。
      • 同步代码块:通过 synchronized 块对需要进行同步的代码进行包裹,实现对关键资源的同步访问。
    • 重入锁(ReentrantLock)
      • java.util.concurrent.locks 包下的 ReentrantLock 提供了与 synchronized 相似的功能,但提供了更丰富的功能,如可中断的锁获取、尝试非阻塞地获取锁、支持多个条件变量等。
    1. 非阻塞同步:
      非阻塞同步机制是指线程在获取不到资源时不会阻塞,而是会立即返回,并在稍后尝试重新获取资源。
    • **比较并交换(CAS)
      • 硬件实现。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
    • 原子操作类(如 AtomicInteger)
      • 位于 java.util.concurrent.atomic 包下,提供了一系列原子性操作类,这些类通过 CAS(Compare And Swap)操作实现线程安全。
    • volatile 关键字
      • volatile 是一种轻量级的同步机制,它确保对变量的修改对其他线程立即可见,并且每次访问变量都是直接从主内存中进行,而不是从线程的本地缓存中读取。
    1. 无同步方案
    • 线程局部变量(ThreadLocal)
      • ThreadLocal 为每个使用该变量的线程提供了一个独立的变量副本,从而实现线程间的数据隔离。
    • 不可变对象
      • 不可变对象(如 String、Integer 等)的状态在创建后就不能改变,因此线程安全。可以通过 final 关键字来确保对象的不可变性。
    • 纯函数式编程
      • 在函数式编程中,不修改变量状态,而是返回新的状态,这样可以避免并发中的线程安全问题。不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。

    sleep和wait方法的主要区别?使用时会对线程状态有什么影响

    以下是sleepwait方法的主要区别以及对线程状态的影响:

    区别:

    • 所属类不同
      • sleep是Thread类的静态方法。
      • wait是Object类的方法。
    • 是否释放锁
      • sleep方法在执行时不会释放锁资源。
      • wait方法在执行时会释放锁资源,直到被notify或者notifyAll方法唤醒。
    • 使用场景不同
      • sleep通常用于暂停线程的执行一段时间,不涉及线程间的同步通信。
      • wait用于线程间的协作,需要在同步代码块中使用,等待其他线程的通知。

    对线程状态的影响:

    • 当线程调用sleep方法时,线程会进入TIMED_WAITING(限时等待)状态,在指定的时间到达后,线程会自动恢复到RUNNABLE(可运行)状态。
    • 当线程调用wait方法时,线程会进入WAITING(无限等待)状态,直到被其他线程调用notify或者notifyAll方法唤醒,被唤醒后的线程会进入BLOCKED(阻塞)状态,等待获取锁资源,当获取到锁后,线程进入RUNNABLE状态。

    解释 Java 中的线程,如何创建和启动线程?

    在Java中,线程是程序执行流程的单一顺序,它是轻量级进程,是程序执行的最小单元。每个线程都有其独立的执行路径,可以执行程序的一部分。Java提供了强大的线程模型,支持多线程编程。

    在Java中,有几种方式可以创建和启动线程:

    1. 继承Thread类:重写run()方法,在该方法中定义线程要执行的任务。创建这个新类的实例,并调用其start()方法来启动线程。这将自动调用run()方法。

    2. 通过实现Runnable接口,实现run()方法,在该方法中定义线程要执行的任务。创建Runnable接口实现类的实例,然后将其传递给Thread类的构造器来创建一个Thread对象。

    3. 上面两种都是没有返回值的,但是如果我们需要获取线程的执行结果,该怎么办呢?实现Callable接口,重写call()方法,这种方式可以通过FutureTask获取任务执行的返回值
      调用Thread对象的start()方法来启动线程。这种方法主要是因为Java不支持多继承。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      public class MyRunnable implements Runnable {
      @Override
      public void run() {
      // 线程执行的代码
      System.out.println("线程运行中...");
      }

      public static void main(String[] args) {
      // 创建Runnable实例
      Runnable myRunnable = new MyRunnable();
      // 创建Thread实例,并将Runnable实例传递给Thread
      Thread thread = new Thread(myRunnable);
      // 启动线程
      thread.start();
      }
      }

    为什么调用start()方法时会执行run()方法,那怎么不直接调用run()方法?

    JVM执行start方法,会先创建一条线程,由创建出来的新线程去执行thread的run方法,这才起到多线程的效果。
    如果直接调用Thread的run()方法,那么run方法还是运行在主线程中,相当于顺序执行,就起不到多线程的效果。

    写一个程序,两个线程交替打印 hello world

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    public class HelloWorldAlternate {

    public static void main(String[] args) {
    Object lock = new Object();
    Thread t1 = new Thread(() -> {
    synchronized (lock) {
    try {
    while (true) {
    System.out.println("hello");
    lock.notify();
    lock.wait();
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    });

    Thread t2 = new Thread(() -> {
    synchronized (lock) {
    try {
    while (true) {
    lock.wait();
    System.out.println("world");
    lock.notify();
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    });

    t1.start();
    t2.start();
    }
    }

    ThreadLocal是什么?

    ThreadLocal,也就是线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

    ThreadLocal原理?

    ThreadLocalMap了解吗?

    ThreadLocalMap虽然被叫做Map,其实它是没有实现Map接口的,但是结构还是和HashMap比较类似的,主要关注的是两个要素:元素数组散列方法

    谈谈你对Java内存模型JMM的理解

    • Java 内存模型(JMM)是一种抽象的概念,用于定义 Java 程序中多线程之间的内存交互规范。它的目的是为了屏蔽不同硬件和操作系统内存访问差异,确保在多线程环境下 Java 程序能够正确、一致地运行。
    • 所有线程共享主内存,用于存储对象实例、静态变量等数据。所有线程都可以访问主内存中的数据,但不能直接操作,需要通过工作内存来进行交互。每个线程都有自己独立的工作内存,工作内存是线程的私有数据区域。它是 JVM 在每个线程中划分出来的一小块内存,用于存储该线程从主内存中读取的数据副本,以及在该线程中执行字节码时产生的临时变量等。

    各种关键字及锁

    什么是volatile关键字

    内存可见性方面

    • 当一个变量被声明为 volatile 时,对该变量的修改会立即被更新到主内存中,并且当其他线程读取该变量时,会直接从主内存中读取最新的值,而不是使用线程本地缓存的值。这确保了不同线程对该变量的操作是基于最新值进行的,保证了数据的 “可见性”。
      禁止指令重排序方面
    • 编译器和处理器为了优化程序的执行效率,可能会对指令进行重新排序。但是对于 volatile 变量,在其赋值操作前后的指令不会被重新排序,从而保证了程序执行的顺序符合开发者的预期。
      使用场景举例
    • 状态标记:例如在一个线程需要根据另一个线程设置的某个标志来决定是否执行某个操作时,使用 volatile 关键字可以确保标志的更改对所有线程立即可见。

    volatile的底层原理是什么?

    在 Java 中,volatile关键字的底层原理主要涉及到内存可见性和禁止指令重排序两个方面。
    一、内存可见性

    1. 缓存一致性协议
      • 在多处理器系统中,每个处理器都有自己的高速缓存。为了保证各个处理器缓存之间的数据一致性,现代计算机系统通常使用缓存一致性协议,如 MESI 协议。
      • 当一个变量被声明为volatile时,JVM 会在对该变量的读写操作前后插入特定的内存屏障指令。这些指令会触发缓存一致性协议的操作,确保其他处理器能够及时看到该变量的最新值。
    2. 写 - 读内存屏障
      • 在对volatile变量进行写操作时,会在写操作后插入一个写屏障指令。这个指令会强制将当前处理器缓存中的修改立即刷新到主内存中,并且使其他处理器缓存中的该变量副本无效。
      • 在对volatile变量进行读操作时,会在读操作前插入一个读屏障指令。这个指令会强制当前处理器从主内存中读取变量的值,而不是从本地缓存中读取,从而保证读取到的是最新的值。
        二、禁止指令重排序
    3. 编译器和处理器重排序
      • 为了提高程序的执行效率,编译器和处理器可能会对指令进行重排序。这种重排序可能会改变程序的执行顺序,但不会影响单线程程序的正确性。
      • 然而,在多线程环境下,指令重排序可能会导致一些难以察觉的错误。
    4. volatile与内存屏障
      • 当一个变量被声明为volatile时,JVM 会在对该变量的读写操作前后插入特定的内存屏障指令。这些指令除了保证内存可见性之外,还会禁止特定类型的指令重排序。
      • 具体来说,对于volatile变量的写操作,编译器和处理器不能将其与后面的指令重排序;对于volatile变量的读操作,编译器和处理器不能将其与前面的指令重排序。

    volatile保证操作的原子性吗?

    volatile关键字不能保证原子性。volatile关键字只能保证变量的可见性,即当一个线程修改了volatile变量的值时,其他线程可以立刻看到这个修改。但是并不能保证多个线程同时对一个volatile变量进行操作时的原子性,因此在多线程环境下需要保证原子性的操作,还需要使用其他的同步机制,比如synchronized关键字或者使用原子类

    乐观锁与悲观锁在Java中如何实现

    • 悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

    • Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

    • 乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了

    • 乐观锁一般会使用版本号机制或 CAS 算法实现

    CAS(比较并交换)

    CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
    CAS 涉及到三个操作数:

    • V:要更新的变量(Var)
    • E:预期值(Expected)
    • N:拟写入的新值(New)
      CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。
      Java 语言并没有直接实现 CAS,CAS 相关的实现是通过Unsafe类以及 C++ 内联汇编的形式实现的。

    CAS操作存在的问题

    • ABA 问题:如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 “ABA”问题。
      解决方案:加版本号
    • 循环时间长开销大:CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
      解决方案:限制自旋次数
    • CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。
      解决方案:可以考虑改用锁来保证操作的原子性;可以考虑合并多个变量,将多个变量封装成一个对象,通过AtomicReference来保证原子性。

    Synchronized的实现原理及使用方式

    ReentrantLock实现原理?

    synchronized和reentrantlock的区别

    ReentrantLock和synchronized都是Java中用来实现同步的手段,它们的功能都是实现线程的互斥访问。

    • 共同点
      1.都是用来协调多线程对共享对象、变量的访问
      2.都是可重入锁,同一线程可以多次获得同一个锁
      3.都保证了可见性和互斥性
    • 不同点
      ReentrantLock 是API 级别的,synchronized 是 JVM 级别的
      1. 锁的获取方式不同
        synchronized锁的获取是隐式的,即在进入同步代码块或方法时自动获取锁,退出时自动释放锁;
        ReentrantLock锁的获取是显式的,需要手动去调用lock()方法获取锁,unlock()方法释放锁。
      2. 锁的公平性不同
        synchronized是非公平锁,不能保证等待时间最长的线程最先获取锁;
        ReentrantLock可以是公平锁,可以保证等待时间最长的线程最先获取锁。
      3. 锁的灵活性不同
        ReentrantLock提供了很多synchronized不具备的功能,例如可以设置超时时间,可以判断锁是否被其他线程持有,可以使用Condition类实现线程等待/通知机制等。
        ![[Pasted image 20241003143642.png]]

    自旋锁

    自旋锁(Spin Lock)是一种用于多线程同步的锁机制。主要目的是在多线程环境下对共享资源进行保护,确保在同一时刻只有一个线程能够访问被保护的资源。

    • 自旋锁是一种忙等待(busy - waiting)类型的锁。当一个线程试图获取一个已经被其他线程持有的自旋锁时,它不会像传统的互斥锁那样进入阻塞状态(例如使线程进入睡眠等待被唤醒),而是会一直循环检查锁是否可用,这个循环检查的过程就像线程在 “自旋”。

    • 优点:与传统的基于阻塞的锁机制相比,自旋锁在获取和释放锁时不需要进行上下文切换(context switch)。上下文切换涉及保存当前线程的执行状态并恢复另一个线程的执行状态,这是一个相对耗时的操作。由于自旋锁避免了上下文切换,所以在锁被占用时间较短的情况下,它可以提供更高的性能。

    • 缺点:如果一个线程长时间自旋等待一个一直被占用的锁,那么它会一直占用 CPU 资源进行无意义的循环检查,这会导致 CPU 资源的浪费,尤其是在单 CPU 或 CPU 核心数量有限的系统中。不适合长期持有锁的情况。

    AQS

    AQS ,即抽象同步队列,是 Java 中用于构建锁和同步器的框架。

    • AQS 内部维护了一个先进先出(FIFO)的同步队列,用于管理等待获取同步状态的线程。当多个线程竞争同一个锁或共享资源时,未获取到资源的线程会被包装成节点(Node)并加入到这个同步队列中等待。
    • AQS 使用一个 volatile 修饰的 int 类型的成员变量 state 来表示同步状态,修改同步状态成功即为获得锁,volatile 保证了变量在多线程之间的可见性,修改 State 值时通过 CAS 机制来保证修改的原子性
    • 获取state的方式分为两种,独占方式和共享方式,一个线程使用独占方式获取了资源,其它线程就会在获取失败后被阻塞。一个线程使用共享方式获取了资源,另外一个线程还可以通过CAS的方式进行获取。
    • AQS 采用了模板方法模式。通过AQS可以实现Reentrantlock CountDownLatch 等并发工具类。

    CountDownLatch

    CountDownLatch 是一个倒计时计数器,你可以在构造函数中指定初始的计数次数。

    • 计数器减至零时释放所有等待的线程CountDownLatch 维护了一个内部计数器,当计数器的值减到零时,所有因调用 await() 方法而在该 CountDownLatch 上等待的线程将被释放。

    • 计数器无法被重置:一旦计数器达到零,CountDownLatch 就会处于“结束”状态,计数器不能再被重置或重新开始使用。

    • 是一次性的CountDownLatch 只能使用一次,计数器到达零后,它就不能再被重用了。

    • 启动信号:一个主线程需要等待多个工作线程完成初始化操作后才能开始执行。

    • 结束信号:多个工作线程都需等待某个操作完成才能继续执行。

    CyclicBarrier(同步屏障)了解吗?

    CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

    • CountDownLatch是一次性的,而CyclicBarrier则可以多次设置屏障,实现重复利用;
    • CountDownLatch中的各个子线程不可以等待其他线程,只能完成自己的任务;而CyclicBarrier中的各个线程可以等待其他线程
      ![[Pasted image 20241009142909.png]]

    Semaphore

    AtomicInteger

    线程池

    什么是线程池?

    • 线程池是一种多线程处理形式,它管理着多个线程,这些线程准备好执行分配给它们的任务。线程池包含了一组已经创建好的线程,这些线程可以被重复利用来执行不同的任务,而不是每次需要执行任务时都创建一个新的线程。

    为什么要用线程池?

    • 降低资源消耗:创建和销毁线程是有开销的,包括时间和系统资源。线程池通过重复利用已创建的线程,减少了频繁创建和销毁线程所带来的资源消耗。
    • 提高响应速度:当需要执行新任务时,如果线程池中有空闲线程,任务可以立即被执行,而不需要等待线程的创建过程,从而提高了系统对任务的响应速度。
    • 控制并发线程数量:线程池可以根据系统的资源状况和任务的特性,限制并发执行的线程数量,避免因创建过多线程而导致系统资源耗尽或性能下降。

    线程池的工作流程

    线程池的执行流程基本是:核心线程、工作队列、非核心线程、拒绝策略
    ![[Pasted image 20241008141133.png]]

    1. 当出现一个新的线程任务时,线程池会先在线程池中分配一个空闲线程,来执行这个任务
    2. 如果出现新任务时线程池中没有空闲线程,这时线程池就会判断当前“存活线程数”是否小于核心线程数corePoolSize
      1.  “存活线程数” < corePoolSize:   线程池就会创建一个新的线程,用来处理新任务
      2. “存活线程数”  = corePoolSize:   这时线程池就要判断工作队列了(队列中存储的就是等待执行的线程)
        1.   工作队列未满(即还有位置可以存放任务):就将该线程放入工作队列中进行等待,等线程池中出现空闲线程,就按照“先进先出”的规则分配执行
        2. 工作队列已经满了: 判断当前存活线程数是否已经达到最大线程数
          1.    “存活线程数”  <  “最大线程数”:创建一个新线程执行新任务
          2.    “存活线程数”  >   “最大线程数”:直接采用拒绝策略

    corePoolSize:核心线程数。核心线程默认不超时。
    maximumPoolSize:最大线程数。
    keepAliveTime:非核心线程的空闲等待时间。临时线程执行完任务后,会主动去任务队列里获取任务。如果经过 keepAliveTime 没有获取到,临时线程会被销毁。
    workQueue:等待队列。
    handler:拒绝策略、饱和策略。

    如何创建线程池?

    使用Executor框架来创建线程池,ExecutorService是主要的接口。

    • 通过Executors工厂类的静态方法来创建不同类型的线程池

    线程池有哪些常见参数?

    1
    2
    3
    4
    5
    6
    7
    1. corePoolSize(核心线程数)
    2. maximumPoolSize(最大线程数)
    3. keepAliveTime(空闲线程存活时间)
    4. unit(存活时间单位)
    5. workQueue(任务队列)
    6. threadFactory(线程工厂)
    7. handler(拒绝策略)
    1. corePoolSize
      此值是用来初始化线程池中核心线程数,当线程池中线程池数< corePoolSize>时,系统默认是添加一个任务才创建一个线程池。当线程数 = corePoolSize时,新任务会追加到workQueue中。
    2. maximumPoolSize
      maximumPoolSize表示允许的最大线程数 = (非核心线程数+核心线程数),当BlockingQueue也满了,但线程池中总线程数 < maximumPoolSize时候就会再次创建新的线程。
    3. keepAliveTime
      非核心线程 =(maximumPoolSize - corePoolSize ) ,非核心线程闲置下来不干活最多存活时间。
    4. unit 线程池中非核心线程保持存活的时间的单位
    5. workQueue
      线程池等待队列,维护着等待执行的Runnable对象。当运行当线程数= corePoolSize时,新的任务会被添加到workQueue中,如果workQueue也满了则尝试用非核心线程执行任务,等待队列应该尽量用有界的。
    6. threadFactory
      创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。
    7. handler
      corePoolSizeworkQueuemaximumPoolSize都不可用的时候执行的饱和策略。线程池,创建线程的方法,

    线程池的拒绝策略有哪些?

    线程池的拒绝策略主要有以下几种: 抛出丢弃, 你干我扔

    • AbortPolicy(默认策略) 当线程池无法接受新任务时(例如工作队列已满且线程数达到最大线程数),直接抛出RejectedExecutionException异常。这种策略适用于不希望任务丢失并且能够及时发现任务提交失败的情况,在开发过程中可以快速定位问题。
    • CallerRunsPolicy 当线程池无法接受新任务时,将任务在提交任务的调用者线程中执行。这样做的好处是,不会直接丢弃任务,并且可以让调用者线程承担一部分工作负载,减缓线程池的压力。例如,如果主线程提交任务到线程池被拒绝,那么主线程会直接执行这个任务。
    • DiscardPolicy 当线程池无法接受新任务时,直接丢弃新提交的任务,并且不做任何通知。这种策略适用于对任务丢失不敏感的场景,例如日志收集任务中的部分日志记录,丢失个别日志记录不会对系统整体功能产生重大影响。
    • DiscardOldestPolicy 当线程池无法接受新任务时,会丢弃工作队列中最旧的任务(也就是最早进入队列还未被执行的任务),然后将新提交的任务加入到工作队列中。这种策略适用于新任务比旧任务更重要,并且对旧任务丢失可以接受的场景。

    线程池设计场景题

    在设置核心线程数之前,需要先熟悉一些执行线程池执行任务的类型
    IO密集型任务
    一般来说:文件读写、DB读写、网络请求等
    推荐:核心线程数大小设置为 2N+1 (N为计算机的CPU核数)

    CPU密集型任务
    一般来说:计算型代码、Bitmap转换、Gson转换等
    推荐:核心线程数大小设置为 N+1 (N为计算机的CPU核数)

    1.4 JVM

    JVM 的内存区域是如何划分的?

    按照最简单的划分方式可以分为
    JVM内存分为线程私有区和线程共享区,其中方法区是线程共享区,虚拟机栈本地方法栈程序计数器是线程隔离的数据区。
    1、程序计数器
    程序计数器(Program Counter Register)也被称为PC寄存器,是一块较小的内存空间。
    它可以看作是当前线程所执行的字节码的行号指示器。
    2、Java虚拟机栈
    Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。
    Java虚拟机栈描述的是Java方法执行的线程内存模型:方法执行时,JVM会同步创建一个栈帧,用来存储局部变量表、操作数栈、动态连接等。
    3、本地方法栈
    本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。Java 虚拟机规范允许本地方法栈被实现成固定大小的或者是根据计算动态扩展和收缩的。

    4、Java堆
    对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java里“几乎”所有的对象实例都在这里分配内存。
    5.方法区
    方法区是比较特别的一块区域,和堆类似,它也是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

    对象创建的过程了解吗?

    在JVM中对象的创建,我们从一个new指令开始:

    • 首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用
    • 检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就先执行相应的类加载过程
    • 类加载检查通过后,接下来虚拟机将为新生对象分配内存。
    • 内存分配完成之后,虚拟机将分配到的内存空间(但不包括对象头)都初始化为零值。
    • 接下来设置对象头,请求头里包含了对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。

    Java中堆和栈的区别是什么?

    • 生命周期管理:栈内存由系统自动管理,而堆内存的分配和回收需要垃圾回收器介入。
    • 存储内容:栈存储基本类型和对象的引用,堆存储对象和数组。
    • 大小限制:栈的大小有限,而堆的大小相对较大,可以达到GB级别。
    • 内存回收:栈内存的回收是确定的,方法调用结束后即回收;堆内存的回收是不确定的,取决于垃圾回收器的策略和时机。

    Java的类加载过程

    类加载指的是将类的.class 文件中的二进制数据读入到内存中。
    在加载过程,JVM要做三件事情: 获取类的二进制字节流 –> 结构化静态存储结构 –> 在内存中生成Class对象

    • 1)通过一个类的全限定名来获取定义此类的二进制字节流,也就是Class文件。
    • 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

    加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了。类型数据妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象, 这个对象将作为程序访问方法区中的类型数据的外部接口。

    Java类的生命周期

    加载 验证 准备 解析 初始化 使用 卸载
    一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading),其中验证、准备、解析三个部分统称为连接(Linking)。
    ![[Pasted image 20240905213102.png]]
    Java的类加载过程是Java运行时环境(JRE)的一部分,它负责将类(.class文件)加载到Java虚拟机(JVM)中。这个过程大致可以分为以下几个步骤:

    1. 加载(Loading)
      • JVM通过类加载器(ClassLoader)找到.class文件。
      • 将.class文件的数据读入到JVM的方法区内,形成一个与这个类对应的java.lang.Class对象。
    2. 验证(Verification)
      • 确保加载的类信息符合JVM规范,没有安全问题。
      • 包括文件格式验证、元数据验证、字节码验证、符号引用验证等。
    3. 准备(Preparation)
      • 为类的静态变量分配内存,并设置默认初始值(如int类型的默认值为0)。
      • 这一步进行的是静态变量的内存分配,而不是初始化。
    4. 解析(Resolution)
      • 将类、接口、字段和方法的符号引用转换为直接引用。
      • 符号引用是一组符号来描述目标,而直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
    5. 初始化(Initialization)
      • 执行类的构造器方法<clinit>(),按照语句在源码中的顺序来执行静态初始化语句和静态块。
      • 为静态变量赋予正确的初始值。
    6. 使用(Using)
      • 类加载完成后,类的Class对象可以被用来创建对象实例或者被其他类引用。
    7. 卸载(Unloading)
      • 当类不再被使用时,JVM的垃圾回收器(GC)会卸载这个类,释放内存。

    什么是双亲委派机制?

    双亲委派机制(Parent Delegation Model)是Java类加载机制中的一个核心原则。这个机制要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。当一个类加载器尝试加载某个类时,它应该先将这个请求委托给父类加载器去执行,如果父类加载器能够完成类加载任务,就成功返回;如果父类加载器无法完成此加载任务,子加载器才尝试自己去加载。

    流程

    1. 类加载器收到类加载请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每一个层次都是如此。
    2. 请求最终到达最顶层的启动类加载器(Bootstrap ClassLoader),如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成此加载任务,子类加载器才会尝试自己去加载。
    3. 如果加载失败,则抛出ClassNotFound异常

    为什么要使用双亲委派机制?

    1. 避免类的多次加载:通过委派机制确保一个类在JVM中只会被加载一次,无论它被请求加载多少次。
    2. 提供了一种安全机制:防止核心API库被随意篡改。例如,如果用户想要加载一个名为java.lang.Object的类,那么不管这个类请求是从哪里发出的,最终都会由顶层的启动类加载器加载,而不是用户自己定义的类加载器。
    3. 使得Java的类加载器可以有层次结构:不同层次的类加载器可以相互协作,形成一个层次结构,这使得Java应用更加灵活和可扩展。

    Tomcat的类加载机制

    Tomcat实际上也是破坏了双亲委派模型的。

    Tomact是web容器,可能需要部署多个应用程序。不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。如多个应用都要依赖hollis.jar,但是A应用需要依赖1.0.0版本,但是B应用需要依赖1.0.1版本。这两个版本中都有一个类是com.hollis.Test.class。如果采用默认的双亲委派类加载机制,那么无法加载多个相同的类。

    所以,Tomcat破坏了双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。每一个WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交CommonClassLoader加载,这和双亲委派刚好相反。

    1.4.3 JVM GC(垃圾回收机制)

    简单讲讲Java的垃圾回收机制

    1. 在 Java 中,垃圾回收(简称 GC)是一种自动内存管理机制。程序运行过程中,会创建大量的对象,这些对象会占用内存空间。当某些对象不再被程序引用时,它们就变成了垃圾,垃圾回收器负责回收这些不再使用的对象所占用的内存空间,从而使程序员不需要手动去释放内存,减少了内存泄漏和悬空指针等问题出现的可能性。
    2. 回收的主要是没有被任何引用变量指向的对象,即非强引用的对象。或者是作用域结束后的局部对象,比如在方法内部定义的局部对象。
    3. JVM堆中将对象分为新生代和老年代。大部分对象的生命周期都很短。在新生代中,新创建的对象可以快速地被分配内存空间,并且可以频繁地进行垃圾回收操作,回收那些生命周期短、很快就不再使用的对象。而存活时间较长的对象会被转移到老年代,减少对这些相对稳定的对象的频繁检查和回收操作。
    4. JVM会根据自身的算法和运行时的情况自动决定何时进行垃圾回收。当新生代空间满时会触发垃圾回收。当老年代空间满时会触发 Full GC,同时回收新生代、老年代的内存。也可以使用使用System.gc()方法来建议JVM 进行垃圾回收,但 JVM 并不一定会立即执行垃圾回收操作。
    5. 垃圾回收算法主要分为标记-清除法、标记-整理法以及复制法。标记-清楚法是从根对象(如栈中的引用变量、静态变量等)开始,递归地标记所有可达的对象。然后回收未被标记的对象所占用的内存空间。但是这样会会产生内存碎片,可能导致后续分配大对象时找不到足够的连续内存空间。而标记-整理算法将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。解决了产生内存碎片,缺点是移动对象的成本较高。这两种方法主要用于不是经常需要回收的老年代空间。
      复制算法(Copying)主要用于新生代。将内存分为大小相等的两块,每次只使用其中一块。当进行垃圾回收时,将存活的对象复制到另一块空闲的内存区域,然后直接清空原来使用的那块内存区域。简单高效,不会产生内存碎片;但是内存利用率低。

    什么是Java里的垃圾回收?如何触发垃圾回收?

    什么是垃圾回收

    • 在 Java 中,垃圾回收(Garbage Collection,简称 GC)是一种自动内存管理机制。程序运行过程中,会创建大量的对象,这些对象会占用内存空间。当某些对象不再被程序引用时,它们就变成了垃圾,垃圾回收器负责回收这些不再使用的对象所占用的内存空间,从而使程序员不需要手动去释放内存,减少了内存泄漏和悬空指针等问题出现的可能性。

    如何触发垃圾回收

    • 自动触发:Java 虚拟机(JVM)会根据自身的算法和运行时的情况自动决定何时进行垃圾回收。例如,当堆内存中的可用内存不足时,JVM 通常会触发垃圾回收来回收不再使用的内存空间。
    • 手动触发(不推荐在一般情况下使用):
      • 使用System.gc()方法:这是一种建议性的请求,它通知 JVM 可以进行垃圾回收,但 JVM 并不一定会立即执行垃圾回收操作。
      • 使用Runtime.getRuntime().gc():本质上和System.gc()是一样的,都是向 JVM 发出执行垃圾回收的请求。

    Full GC在什么时候触发?

    1. 老年代空间不足:当老年代空间不足时,会触发 Full GC 来对整个堆内存进行垃圾回收,以尽可能地释放未使用的对象。
    2. 永久代空间不足(Java 7 及以下):在 Java 7 及以下版本中,永久代用于存储类的结构信息、静态变量和常量池等数据,当永久代空间不足时也会引发 Full GC。
    3. 显式调用 System.gc():虽然调用 System.gc() 方法相当于建议JVM full GC,但是并不一定会立即触发 Full GC,
    4. Concurrent Mode Failure(CMS GC):在使用 CMS(Concurrent Mark-Sweep)垃圾回收器时,如果并发标记过程中老年代空间不足,会触发 Full GC 来执行完整的垃圾回收操作。

    Full GC 的发生通常会导致长时间的系统暂停,因为在 Full GC 过程中会暂停应用程序的所有线程,直到垃圾回收完成。为了降低 Full GC 的发生频率,可以通过调整堆内存大小、选择合适的垃圾回收器和优化应用程序等手段来进行优化。

    Java堆的内存分区了解吗?

    按照垃圾收集,将Java堆划分为新生代 (Young Generation)老年代(Old Generation) 两个区域,新生代存放存活时间短的对象,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
    而新生代又可以分为三个区域,eden、from、to,比例是8:1:1,而新生代的内存分区同样是从垃圾收集的角度来分配的。

    为什么 Java 的垃圾收集器将堆分为老年代和新生代?

    1. 对象生命周期特点
      • 大部分对象的生命周期都很短。在新生代中,新创建的对象可以快速地被分配内存空间,并且可以频繁地进行垃圾回收操作,回收那些生命周期短、很快就不再使用的对象。
      • 而存活时间较长的对象会被转移到老年代,减少对这些相对稳定的对象的频繁检查和回收操作。
    2. 垃圾回收效率
      • 新生代采用复制算法,这种算法在处理大量新创建和死亡的对象时效率较高。因为新生代中的大部分对象都是朝生夕死的,复制算法只需要复制少量存活的对象到新的内存区域,然后清理掉原来的区域即可。
      • 老年代通常对象存活率较高,适合使用标记 - 清除或者标记 - 整理算法进行垃圾回收。
    3. 内存管理的优化
      • 通过这种分代设计,垃圾收集器可以根据不同代的特点,分别调整垃圾回收的策略和频率,更好地利用内存资源,提高整体的垃圾回收效率和系统性能。

    对象的四种引用方式 强、软、弱、虚

    在 Java 中,引用(Reference)是对象在内存中的一种表示方式。Java 提供了四种引用类型,它们定义了对象的可达性以及垃圾回收的行为。这四种引用类型分别是:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)

    • 强引用:默认的引用类型,内存不足时不回收。
    • 软引用:内存不足时才会被回收,适合做缓存。
    • 弱引用:在垃圾回收时无论内存是否足够都会被回收,适合做非强制引用的缓存。
    • 虚引用:无法通过虚引用访问对象,用于跟踪对象被回收的状态。
    1. 强引用(Strong Reference)
    • 定义:最常见的引用方式。在 Java 中,默认的对象引用就是强引用。
    • 特点:只要某个对象存在强引用,垃圾收集器就不会回收该对象,即使内存不足,JVM 也会抛出 OutOfMemoryError,而不是回收这些对象。
    • 使用方式:直接创建一个对象并赋值给变量。
      1
      Object obj = new Object(); // obj 是一个强引用

    使用场景:强引用是 Java 中最常见的引用类型,通常用于需要长期存活或重要的数据对象。

    1. 软引用(Soft Reference)
    • 定义:软引用在内存不足的时候会被垃圾收集器回收。
    • 特点:对于软引用指向的对象,只有在 JVM 内存不足的时候才会尝试回收它们。在发生垃圾回收时,如果对象仅被软引用指向,且内存不够,则会回收该对象。如果内存足够,该对象会被保留。软引用通常用于实现缓存。
    • 使用方式:通过 SoftReference 类来创建软引用。
      1
      2
      3
      4
      5
      import java.lang.ref.SoftReference;

      Object obj = new Object();
      SoftReference<Object> softRef = new SoftReference<>(obj); // 创建软引用
      obj = null; // 去除强引用
      使用场景:适用于缓存设计,在内存充足时保留缓存对象,内存不足时释放缓存对象。
    1. 弱引用(Weak Reference)
    • 定义:弱引用在垃圾收集器运行时,无论内存是否充足,只要没有强引用指向该对象,都会被回收。
    • 特点:弱引用的生命周期非常短暂,通常在下一次垃圾收集时被回收。弱引用非常适合使用在不需要强制引用的场景,比如:规范映射、缓存或监听器。
    • 使用方式:通过 WeakReference 类来创建弱引用。
      1
      2
      3
      4
      5
      import java.lang.ref.WeakReference;

      Object obj = new Object();
      WeakReference<Object> weakRef = new WeakReference<>(obj); // 创建弱引用
      obj = null; // 去除强引用
      使用场景:弱引用常用于映射缓存、监听器、数据结构(如 WeakHashMap)等场景,需要频繁访问但又希望允许对象被回收。
    1. 虚引用(Phantom Reference)
    • 定义:虚引用仅用于追踪对象的垃圾回收状态,与弱引用相比,虚引用更弱。
    • 特点:虚引用不能单独使用或访问对象。在创建虚引用时,必须和引用队列(ReferenceQueue)一起使用。当垃圾收集器准备回收一个对象时,如果发现它还有虚引用,就会在回收该对象的内存之前,将这个虚引用加入到与之关联的引用队列中。通过判断虚引用是否被加入到队列中,程序可以在对象被内存回收之前采取一些必要的行动。
    • 使用方式:通过 PhantomReference 类来创建虚引用。
      1
      2
      3
      4
      5
      6
      7
      import java.lang.ref.PhantomReference;
      import java.lang.ref.ReferenceQueue;

      Object obj = new Object();
      ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
      PhantomReference<Object> phantomRef = new PhantomReference<>(obj, refQueue); // 创建虚引用
      obj = null; // 去除强引用
      使用场景:主要用于一些特殊场景,比如对象被回收之前的清理操作、实现 DirectByteBuffer 中的直接内存的释放等。

    Java 中常见的垃圾收集器有哪些?

    Serial 收集器

    • 这是一个单线程的收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
    • 它简单高效,对于限定单个 CPU 的环境来说,由于没有线程交互的开销,专心做垃圾收集可能效率更高。

    ParNew 收集器

    • 它是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为包括控制参数、收集算法、回收策略等都与 Serial 收集器一样。
    • 在多 CPU 环境下,它可以更高效地利用系统资源。

    Parallel Scavenge 收集器

    • 这个收集器的目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间))。
    • 它也使用多线程进行垃圾收集,可通过参数调节吞吐量大小。

    CMS(Concurrent Mark Sweep)收集器

    • 以获取最短回收停顿时间为目标的收集器。
    • 它在垃圾收集过程中,主要的几个阶段(如初始标记、重新标记)仍然需要暂停用户线程,但在标记和清理阶段可以和用户线程并发执行,从而减少垃圾收集对用户程序的影响。

    G1(Garbage - First)收集器

    • 它是一款面向服务端应用的垃圾收集器。
    • G1 将整个 Java 堆划分为多个大小相等的独立区域(Region),在进行垃圾收集时,可以根据各个区域的垃圾堆积情况灵活地选择回收区域,避免了全堆扫描,同时可以控制垃圾收集的停顿时间。

    Java 中如何判断对象是否是垃圾?不同垃圾回收方法有何区别?

    在 Java 中主要通过可达性分析算法来判断对象是否是垃圾。从一系列被称为 “GC Roots” 的根对象开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连(即不可达)时,则证明此对象是不可用的,可被判定为垃圾。常见的 GC Roots 对象包括:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。

    Java 中有哪些垃圾回收算法?

    标记 - 清除算法(Mark - Sweep)

    • 标记阶段:从根对象(如线程栈中的局部变量、静态变量等)开始,遍历对象图,标记所有可达的对象。
    • 清除阶段:遍历堆,回收未被标记的对象所占用的内存空间。
    • 缺点:这种算法会产生内存碎片,可能导致后续分配大对象时找不到足够的连续内存空间而触发另一次垃圾回收。

    标记 - 整理算法(Mark - Compact)

    • 标记过程:和标记 - 清除算法类似,先标记出所有存活的对象。
    • 整理过程:将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
    • 优点:解决了标记 - 清除算法内存碎片的问题。

    复制算法(Copying)

    • 将内存划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另一块内存上,然后把使用过的那块内存空间全部清理掉。
    • 优点是实现简单,运行高效,不会产生内存碎片;缺点是内存利用率较低,只有一半的内存空间可用于分配新对象。

    分代收集算法(Generational Collection)

    • 根据对象存活周期的不同将内存划分为不同的代,如新生代和老年代。
    • 新生代中的对象通常朝生夕死,适合使用复制算法;老年代中的对象存活率较高,一般采用标记 - 清除或标记 - 整理算法。

    常用的 JVM 配置参数有哪些?

    内存相关参数

    • -Xms:初始堆大小,例如 -Xms512m 表示初始堆内存为 512MB。
    • -Xmx:最大堆大小,如 -Xmx1024m 设定最大堆内存为 1GB。这两个参数可以让你控制 Java 程序运行时堆内存的大小范围。
    • -Xmn:年轻代大小,例如 -Xmn256m 表示年轻代大小为 256MB,合理设置年轻代大小可以优化垃圾回收性能。
    • -XX:PermSize:永久代初始大小(Java 8 之前)。
    • -XX:MaxPermSize:永久代最大大小(Java 8 之前),在 Java 8 中被元空间(Metaspace)取代。
    • -XX:MetaspaceSize:元空间初始大小(Java 8+),例如 -XX:MetaspaceSize=128m。
    • -XX:MaxMetaspaceSize:元空间最大大小(Java 8+)。

    垃圾回收相关参数

    • -XX:+UseSerialGC:使用串行垃圾回收器。
    • -XX:+UseParallelGC:启用并行垃圾回收器。
    • -XX:+UseConcMarkSweepGC:使用并发标记清除垃圾回收器(CMS)。
    • -XX:+UseG1GC:使用 G1 垃圾回收器。

    其他参数

    • -XX:PrintGCDetails:打印详细的垃圾回收信息,方便排查内存相关问题时分析垃圾回收过程。
    • -XX:HeapDumpOnOutOfMemoryError:在发生内存溢出错误(OutOfMemoryError)时生成堆转储文件,可用于事后分析内存使用情况。

    2 计算机网络

    https://mp.weixin.qq.com/s/yAlErlC09GnjaVvwUo3Acg
    https://mp.weixin.qq.com/s/yAlErlC09GnjaVvwUo3Acg

    2.1 计算机网络–综合

    从输入网址到获得页面的过程

    从输入网址到获得页面的过程,涉及到多个网络协议和层级,具体包括DNS解析、路由寻址、TCP连接、HTTP请求处理、数据渲染等步骤。以下是详细的解释:

    1. DNS解析
      • 用户在浏览器中输入网址(例如 www.baidu.com)后,浏览器首先会检查本地缓存中是否已有该域名的IP地址记录。
      • 如果没有,浏览器会向本地DNS服务器发送一个DNS查询请求。
      • 如果本地DNS服务器没有缓存,它会向根域名服务器请求,然后可能经过一系列的顶级域名服务器和权威域名服务器,最终获得该域名对应的IP地址。
    2. 网络寻址建立端到端的连接(亮点)
      • 浏览器获得IP地址后,会尝试寻找到域名对应IP地址的服务器。浏览器所在的主机首先确定目标IP地址是否在本地网络中。如果不是,数据包会被发送到默认网关,通常是本地网络的路由器。
      • 路由器负责将数据包转发到更远的目标网络。它会查看数据包的目的IP地址,并决定最佳的路径。这个过程可能涉及到多个路由器的跳转。
      • 当数据包到达目标网络时,最后一跳的路由器会将其转发到目标网络的交换机。交换机通过ARP地址解析协议来查找目标IP地址对应的MAC地址。ARP请求会被广播到本地网络中的所有设备,目标设备会响应其MAC地址。交换机一旦获得MAC地址,就会将数据包直接发送到目标设备。当然每一次IP寻址都涉及到这一过程。
      • 🌟这个过程确保了数据包能够从源主机通过网络层到达目标主机。由于TCP/IP的分层特性,传输层(如TCP或UDP)及以上层级都是基于端到端的连接,它们不需要关心数据包在网络层如何被路由和传输的细节。传输层负责在两个端点之间建立可靠的连接,并确保数据的正确传输。
    3. 建立TCP连接
      • 在网络连接建立后,传输层就可以工作。网络层以下的操作对上层完全透明。浏览器就使用TCP协议的三次握手过程与目标服务器的80端口(HTTP默认端口)建立连接。
      • 三次握手包括:客户端发送一个带有SYN标志的数据包给服务器,服务器接收到后回复一个带有SYN/ACK标志的数据包,最后客户端再回复一个带有ACK标志的数据包,完成连接的建立。
    4. 发送HTTP/HTTPS请求
      • 请求报文:浏览器向服务器发送HTTP或HTTPS请求,请求报文包括请求行(请求方法、URL、HTTP版本)、请求头(如Cookie、User-Agent等)和请求体(如POST数据)。
      • SSL/TLS握手(仅HTTPS):如果是HTTPS协议,客户端和服务器将进行SSL/TLS握手,协商加密算法并交换加密密钥。
    5. 浏览器处理HTTP响应
      • 服务器软件:如Nginx、Apache或其它,接收到HTTP请求后,会将请求转发给相应的应用程序(如PHP、Node.js,或后端服务)。
      • 应用逻辑:服务器应用逻辑处理请求,进行数据库查询、业务逻辑处理等。
      • 生成响应:服务器将处理后的结果生成HTTP响应,可能包括响应行(状态码、HTTP版本)、响应头(如Content-Type、Cache-Control等)、和响应体(HTML、JSON等数据)。
      • 传输:服务器将HTTP响应数据通过TCP链路(可能加密/压缩)传输到客户端浏览器。
      • 关闭连接:传输完毕后,服务器和浏览器之间会关闭TCP连接(可选四次挥手,或直接关闭)。
    6. 渲染页面
      • 浏览器根据收到的HTML文件,CSS样式和JavaScript代码,对DOM树进行渲染,生成页面的布局和样式。
      • 如果页面包含JavaScript,浏览器会执行JavaScript代码,这可能会导致页面内容的改变或其他动态行为。
    7. 页面加载完成
      • 子资源请求:过程中还会下载网页中所需要的资源,如图片、音视频文件、其它CSS和JavaScript文件。每次资源的下载都可能引发新的DNS查找、TCP连接和HTTP请求。
      • 事件处理:页面加载完毕后,浏览器开始处理用户交互,如点击事件、表单提交等,可能会触发进一步的请求,实现动态更新。
        更详细版 https://www.xiaolincoding.com/network/1_base/what_happen_url.html
        ![[Pasted image 20240901210148.png]]

    扫二维码到进入页面的过程发生了什么

    1. 识别二维码
      • 用户使用手机上的相机应用或专门的二维码扫描应用对二维码进行扫描。
      • 扫描应用使用图像识别技术来解析二维码中的信息,这通常涉及到图像处理算法来识别二维码的图案和编码。
    2. 解码
      • 一旦二维码被识别,扫描应用会将二维码中的图案转换为相应的数据,通常是URL链接、文本信息或其他类型的数据。
        如果是URL链接的话,就相当于访问页面+上一题1~7
    • 扫码登录

    讲讲DNS的解析过程

    • 首先会查找浏览器的缓存,看看是否能找到 **www.baidu.com 对应的IP地址,找到就直接返回;否则进行下一步。
    • 将请求发往给本地DNS服务器,如果查找到也直接返回,否则继续进行下一步;
    • 本地DNS服务器向根域名服务器发送请求,根域名服务器返回负责com的顶级域名服务器的IP地址的列表。
    • 本地DNS服务器再向其中一个负责com的顶级域名服务器发送一个请求,返回负责baidu.com的权限域名服务器的IP地址列表。
    • 本地DNS服务器再向其中一个权限域名服务器发送一个请求,返回www.baidu.com 所对应的IP地址。

    ISO/OSI 七层模型与TCP/IP四层模型

    ISO/OSI 七层模型
    “物联网淑慧试用”
    物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

    TCP/IP协议分层
    网络接口层、网络层、传输层、应用层

    • 分层的好处?
      下层为上层提供服务,只能通过相邻层之间的接口实现。各层之间相互独立、灵活性好,易于抽象和标准化。
    OSI模型 说明 功能
    物理层 定义数据终端设备和数据通信设备的物理和逻辑连接方法 在物理媒体上透明地传输原始比特流
    数据链路层 提供相邻结点间地通信,检测并校正物理层传输介质上产生的传输差错,使链路对网络层显现为一条无差错的,可靠的数据传输线路 成帧,差错控制,流量控制,传输管理和控制对共享信道的访问
    网络层 把网络层的协议数据单元(分组)从源端传到目的端,为分组交换网上的不同主机提供通信服务 流量控制,拥塞控制,差错控制和网际互联等
    传输层 提供可靠的端到端(或进程到进程)的数据传输服务 端到端的传输管理,差错控制,流量控制和复用分用
    会话层 向表示层实体或者用户进程提供给建立连接并在连接上有序地传输数据,这就是会话 建立、管理进程间的会话
    表示层 处理在两个通信系统中交换信息的表示方式 数据压缩,加密和解密,数据格式转换
    应用层 为特定类型的网络应用提供访问OSI环境的手段 用户和网络的界面
    TCP/IP模型 对应OSI模型 典型协议 典型设备
    网络接口层 物理层、数据链路层 PPP,HDLC 交换机
    网络层(仅支持无连接) 网络层(支持无连接和面向连接) IP,ICMP 路由器
    传输层(支持无连接和面向连接) 传输层(仅支持面向连接) TCP、UDP
    应用层 会话层、表示层、应用层 FTP,DNS,SMTP,HTTP
    • 比较
      • 都基于分层的体系结构,将庞大且复杂的问题划分为若干个较容易处理的,范围较小的问题;基于独立的协议栈的概念;解决异构网络互联问题。
      • OSI定义了服务,协议,接口,符合面向对象思想;TCP/IP没有明确区分,不符合软件工程思想。
      • OSI不偏向任何特定的协议,TCP/IP排斥其他协议栈
        ![[Pasted image 20240804211848.png]]
    ISO/OSI TCP/IP
    网络层 无连接+面向连接 无连接
    传输层 面向连接 无连接+面向连接
    • 各层常见设备

      • 物理层:集线器,中继器,放大器
      • 数据链路层:交换机,网桥(隔离冲突域,不隔离广播域)
      • 网络层:路由器(隔离冲突域和广播域)
    • 各层常见协议

      • 物理层:RJ45,CLOCK,IEEE802.3
      • 数据链路层:PPP,FR,HDLC,VLAN,MAC
      • 网络层:IP,ICMP,ARP,RARP,IPX,RIP,IGRP
      • 传输层:TCP,UDP,SPX
      • 应用层:FTP,DNS,TeInet,SMTP,HTTP,WWW,NFS

    数据在各层之间是怎么传输的呢?

    对于发送方而言,从上层到下层层层包装,对于接收方而言,从下层到上层,层层解开包装。

    • 发送方的应用进程向接收方的应用进程传送数据
    • AP先将数据交给本主机的应用层,应用层加上本层的控制信息H5就变成了下一层的数据单元
    • 传输层收到这个数据单元后,加上本层的控制信息H4,再交给网络层,成为网络层的数据单元
    • 到了数据链路层,控制信息被分成两部分,分别加到本层数据单元的首部(H2)和尾部(T2)
    • 最后的物理层,进行比特流的传输

    ![[Pasted image 20240901210052.png]]

    常见协议工作层次及端口

    DHCP工作在应用层,基于UDP协议;
    HTTP 80;HTTPS 443;
    FTP 21; SSH 22;RDP 3389;IMAP 143;DNS 53;
    SMTP25;POP3 110; IMAP 143
    MySQL 3306;PgSQL 5432;Redis 6379;TomCat 8080;

    层次结构中端到端通信有哪几层?端到端通信和点到点通信有什么区别?

    • 从本质上说,由物理层、数据链路层和网络层组成的通信子网为网络环境中的主机提供点到点的服务,而传输层为网络中的主机提供端到端的通信
    • 直接相连的结点之间的通信叫点到点通信.它只提供一台机器到另一台机器之间的通信不会涉及程序或进程的概念。同时点到点通信并不能保证数据传输的可靠性,也不能说明源主机与目的主机之间是哪两个进程在通信,这些工作都是由传输层来完成的。
    • 端到端通信建立在点到点通信的基础上.它是由一段段的点到点通信信道构成的,是比点到点通信更高一级的通信方式,以完成应用程序(进程)之间的通信。“端”是指用户程序的端口,端口号标识了应用层中不同的进程。

    2.2 TCP篇

    2.2.1 TCP基础

    ![[Pasted image 20240722172432.png]]

    IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。
    如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。

    TCP 是一个工作在传输层可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。

    简述TCP是什么?

    📚TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

    • 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
    • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
    • 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。

    2.2.2 TCP与UDP

    TCP和UDP的报文头

    TCP 报文头部结构较为复杂,因为它需要支持可靠的数据传输,有20字节的固定长度,以及可变选项。包括了源端口、目的端口,序列号,确认应答号、首部、校验和等。
    UDP则只有源端口、目的端口、数据报长度以及校验和共计8个字节。

    TCP和UDP的区别

    连接建立、服务对象、可靠性、拥塞控制和流量控制、首部开销、传输方式、分片…

    1. 连接
      • TCP 是面向连接的传输层协议,传输数据前先要建立连接。
      • UDP 是不需要连接,即刻传输数据。(无连接)
    2. 服务对象
      • TCP 是一对一的两点服务,即一条连接只有两个端点。
      • UDP 支持一对一、一对多、多对多的交互通信
    3. 可靠性
      • TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达
      • UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议
    4. 拥塞控制(慢启动,拥塞避免、快重传、快恢复)、流量控制(滑动窗口协议)
      • TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
      • UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率
    5. 首部开销
      • TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。
      • UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
    6. 传输方式
      • TCP 是流式传输,没有边界,但保证顺序和可靠
      • UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
    7. 分片不同
      • TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片
      • UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。

    TCP 和 UDP 应用场景:

    TCP 经常用于:FTP 文件传输;HTTP / HTTPS;邮件服务(SMTP等)
    UDP 经常用于:包总量较少的通信,如 DNSSNMP 等;视频、音频等多媒体通信;广播通信;DHCP,DNS查询

    除了常见的拥塞控制、滑动窗口等机制外,TCP还有什么机制可以保证可以?比如报文上的一些检验等?

    除了TCP本身提供的可靠传输机制即序号和确认号、超时重传、滑动窗口、确认机制、拥塞控制之外,还有其他层上的方式可以保证数据的可靠传输。

    比如在数据链路层和物理层上,常用的技术包括循环冗余校验(CRC)、帧检验序列(FCS)等,用于检测和纠正数据传输中的错误。

    在应用层上,常用的方法包括数据重传、数据校验等。例如,HTTP协议通常会在应用层上进行数据重传,以保证数据的可靠传输。另外,应用层协议也可以使用一些校验算法,如MD5、SHA等,来验证数据的完整性,以保证数据在传输过程中不被篡改。

    总之,在不同层次上都可以采用不同的技术和机制来保证数据的可靠传输,这些机制相互配合,共同保障了数据的安全和可靠性。

    TCP为什么安全?

    TCP(传输控制协议)被认为相对安全的原因主要包括以下几点:

    1. 可靠的连接:TCP使用三次握手过程来建立连接,这个过程确保了通信双方都同意开始数据传输,并且为数据传输准备好了接收缓冲区。连接管理
    2. 数据包顺序:TCP确保数据按照发送的顺序到达目的地。如果数据包到达的顺序出现错误,TCP协议会重新排序,保证了数据的完整性。序列号/确认应答
    3. 数据完整性:TCP使用校验和(checksum)来验证数据在传输过程中是否被修改或损坏。如果检测到错误,TCP会请求重传数据包。 校验和
    4. 流量控制:TCP使用滑动窗口机制来控制数据的传输速率,避免网络拥塞。这有助于防止因为数据过多而导致的网络问题。流量控制
    5. 拥塞控制:TCP具有拥塞控制机制,如慢启动、拥塞避免、快速重传和快速恢复,这些机制有助于网络在面临拥塞时保持稳定。拥塞避免
    6. 端到端通信:TCP提供端到端的通信服务,这意味着数据传输的责任从发送端一直延续到接收端,而不是在中间节点终止。
    7. 会话管理:TCP连接是持久的,直到通信双方明确断开连接。这有助于防止未授权的访问和数据泄露。

    tcp十六位校验和怎么实现的?

    1. 将TCP数据段(包括伪头部)分割成16位的字。
    2. 将所有16位的字相加(包括伪头部的字段)。
    3. 处理所有16位字的进位。
    4. 将求和结果取反,得到校验和。

    tcp 粘包问题 (拆包和分包)

    TCP 的粘包和拆包更多的是业务上的概念!

    什么是TCP粘包和拆包?

    • TCP 是面向流,没有界限的一串数据。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。
      ![[Pasted image 20240804211652.png]]

    为什么会产生粘包和拆包呢?

    • 要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包;
    • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;
    • 要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包;
    • 待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。即 TCP 报文长度 - TCP 头部长度 > MSS。

      那怎么解决呢?

    • 发送端将每个数据包封装为固定长度
    • 在数据尾部增加特殊字符进行分割
    • 将数据分为两部分,一部分是头部,一部分是内容体;其中头部结构大小固定,且有一个字段声明内容体的大小。

    不同协议能否监听同一个端口?

    不同的协议(如TCP和UDP)可以监听同一个端口号,但同一协议的不同服务不能监听同一个端口号。

    常见TCP的连接状态有哪些?

    1. CLOSED
    • 初始状态,没有建立连接。
    1. LISTEN
    • 服务器端套接字正在等待来自客户端的连接请求。
    1. SYN_SENT
    • 客户端已经发送了一个SYN报文来建立一个新的连接,并等待来自服务器的确认。
    1. SYN_RECEIVED
    • 服务器端收到了一个SYN报文,并已经发送了一个SYN+ACK作为响应,等待客户端的确认。
    1. ESTABLISHED
    • 连接已经建立,数据传输可以进行。
    1. FIN_WAIT_1
    • 套接字已经发送了FIN报文,并等待来自对端的确认。
    1. FIN_WAIT_2
    • 套接字已经收到了对端对FIN报文的确认,并等待对端发送FIN报文。
    1. CLOSE_WAIT
    • 套接字已经收到了对端的FIN报文,并等待本地应用程序关闭连接。
    1. CLOSING
    • 套接字已经发送了FIN报文,并正在等待对端的确认,同时也收到了对端的FIN报文。
    1. LAST_ACK
    • 套接字已经发送了最后的ACK报文,并等待确认,之后将进入CLOSED状态。
    1. TIME_WAIT
    • 在连接关闭后,套接字会等待足够长的时间以确保对端收到了最后的ACK报文。这是连接终止前的最后一个状态。

    2.2.3 TCP连接建立

    TCP连接建立过程(三次握手)

    ![[Pasted image 20240320191410.png]]
    客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。

    1. 客户端随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接。客户端处于 SYN-SENT 状态。
    2. 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1,接着把 SYNACK 标志位置为 1。最后把该报文发给客户端。之后服务端处于 SYN-RCVD 状态。
    3. 客户端收到服务端报文后,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。

    服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
    第三次握手是可以携带数据的,前两次握手是不可以携带数据的

    为什么是三次握手?

    1. 防止旧的重复连接初始化造成混乱【主要原因】

      • 客户端连续发送多次 SYN 建立连接的报文,在网络拥堵情况下,「旧 SYN 报文」比「最新的 SYN 」早到达了服务端,服务端返回 SYN + ACK,ACK是旧的客户端序号+1
      • 客户端收到后 ACK 与期望不符,回复 RST 报文请求终止旧的连接
      • 服务端收到 RST 报文后,就会释放连接
      • 后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。
        在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费
    2. 同步双方初始序列号
      当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收
      那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
      两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。

    3. 避免资源浪费
      没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接。
      如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。

    为什么不能用两次握手进行连接?

    • 为了防止服务器端开启一些无用的连接增加服务器开销 防止端口占用
    • 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 避免旧报文

    由于网络传输是有延时的(要通过网络光纤和各种中间代理服务器),在传输的过程中,比如客户端发起了 SYN=1 的第一次握手。
    如果服务器端就直接创建了这个连接并返回包含 SYN、ACK 和 Seq  等内容的数据包给客户端,这个数据包因为网络传输的原因丢失了,丢失之后客户端就一直没有接收到服务器返回的数据包。如果没有第三次握手告诉服务器端客户端收的到服务器端传输的数据的话,服务器端是不知道客户端有没有接收到服务器端返回的信息的。服务端就认为这个连接是可用的,端口就一直开着,等到客户端因超时重新发出请求时,服务器就会重新开启一个端口连接。这样一来,就会有很多无效的连接端口白白地开着,导致资源的浪费。

    还有一种情况是已经失效的客户端发出的请求信息,由于某种原因传输到了服务器端,服务器端以为是客户端发出的有效请求,接收后产生错误。

    通过第三次握手的数据告诉服务端,客户端有没有收到服务器“第二次握手”时传过去的数据,以及这个连接的序号是不是有效的。若发送的这个数据是“收到且没有问题”的信息,接收后服务器就正常建立 TCP 连接,否则建立 TCP  连接失败,服务器关闭连接端口。由此减少服务器开销和接收到失效请求发生的错误。

    • 本质是还是因为传输层一下并不保证可靠交付,TCP又需要向上层保证可靠。

    为什么不是四次握手?

    服务端回复 ACK 和 发送 SYN 的过程可以合并。通过第二、三次握手,客户端和服务器已经互相同步了数据序号seq,且知道对方能收到自己的信息。

    三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

    第一、二、三次握手丢失了,分别会发生什么?

    • 第一次握手服务端未收到SYN报文
      客户端超时重传: 客户端在等待一定时间后没有收到服务器的响应,会进行超时重传,多次重传失败后,客户端可能会放弃连接尝试,并通知应用层连接失败。

    • 第二次握手客户端未收到服务端响应的ACK报文
      客户端未收到SYN+ACK: 如果客户端没有收到服务器的SYN+ACK报文,可能是网络问题或者客户端端口被防火墙阻挡。客户端在超时后会重传SYN报文。
      服务器未收到客户端的ACK: 如果客户端发送了ACK报文但服务器没有收到,服务器会认为客户端没有收到SYN+ACK,并会等待一段时间后重新发送SYN+ACK。

    • 第三次握手服务端为收到客户端发送过来的ACK报文
      服务器未收到ACK: 如果服务器没有收到这个ACK报文,它会认为第三次握手没有完成,通常会重新发送SYN+ACK报文。如果多次尝试后仍然失败,服务器可能会关闭这个半开连接。
      客户端认为连接已建立: 即使服务器没有收到ACK,客户端可能认为连接已经建立并开始发送数据。如果服务器没有响应,客户端会最终超时并通知应用层连接问题。

    如果已经建立了连接,但是客户端/服务端突然出现故障了怎么办?

    • 客户端故障

        1. 服务端的处理:
      • 超时重传: 服务端会开始超时重传机制,尝试重新发送数据。
      • 到达重传次数上限: 如果重传次数达到上限,服务端会认为客户端已经不可达,随后会关闭这个TCP连接。
      • 发送RST(Reset)报文: 在某些情况下,服务端可能会发送一个RST报文来立即终止连接。
    • 服务端故障

        1. 客户端的处理:
      • 超时重传: 客户端会开始超时重传机制,尝试重新发送数据。
      • 到达重传次数上限: 如果重传次数达到上限,客户端会认为服务端已经不可达,随后会关闭这个TCP连接。
      • 发送RST(Reset)报文: 客户端可能会发送一个RST报文来立即终止连接。

    TCP SYN攻击

    攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
    ![[Pasted image 20240901212001.png]]

    • TCP 半连接和全连接队列
      在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是
      • 半连接队列,也称 SYN 队列;TCP 三次握手时,客户端发送 SYN 到服务端,服务端收到之后,便回复 ACK 和 SYN,状态由 LISTEN 变为 SYN_RCVD,此时这个连接就被推入了 SYN 队列,即半连接队列。
      • 全连接队列,也称 accept 队列;当客户端回复 ACK, 服务端接收后,三次握手就完成了。这时连接会等待被具体的应用取走,在被取走之前,它被推入 ACCEPT 队列,即全连接队列。
        ![[Pasted image 20240901212317.png]]
    • 避免 SYN 攻击方式
      • 调大 netdev_max_backlog:当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包,调大队列的最大值。

      • 增大 TCP 半连接队列;

      • 减少 SYN-ACK 重传次数:减少 SYN-ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。

      • 开启 tcp_syncookies;
        开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。

        • 当 「 SYN 队列」满之后,后续服务端收到 SYN 包,不会丢弃,而是根据算法,计算出一个 cookie 值;
        • 将 cookie 值放到第二次握手报文的「序列号」里,然后服务端回第二次握手给客户端;
        • 服务端接收到客户端的应答报文时,服务端会检查这个 ACK 包的合法性。如果合法,将该连接对象放入到「 Accept 队列」。
        • 最后应用程序通过调用 accpet() 接口,从「 Accept 队列」取出的连接。
      • SYN Proxy 防火墙:服务器防火墙会对收到的每一个 SYN 报文进行代理和回应,并保持半连接。等发送方将 ACK 包返回后,再重新构造 SYN 包发到服务器,建立真正的 TCP 连接。

    2.2.4 TCP连接断开

    TCP连接断开过程

    ![[Pasted image 20240320191507.png]]
    第一次挥手: 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u,此时,客户端进入FIN_WAIT_1状态。

    第二次挥手: 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时服务器端就进入了CLOSE_WAIT(等待关闭)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器端若发送数据,客户端任然要接受。这个状态还要持续一段时间,也就是整个CLOSE_WAIT状态持续的时间。客户端收到服务器端的确认请求后,此时,客户端就进入了FIN_WAIT_2(终止等待2)状态,等待服务器端发送连接释放报文。

    第三次挥手: 服务器端将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序号为seq=w,此时,服务器就进入了LAST_ACK(最后确认)状态,等待客户端的确认。

    第四次挥手: 客户端收到服务器端的链接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME_WAIT(时间等待)状态。此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命) 的时间后,当客户端撤销响应的TCP后,才进入CLOSED状态。服务器端只要接受到客户端发出的确认,立即进入CLOSED状态。同样,撤销TCP后,就结束这次TCP连接。

    为什么连接的时候是三次握手,关闭的时候却是四次挥手?

    为了安全无误地断开双方连接,防止出现一方接收完数据后还想获取数据,但另外一方已经关闭的情况。
    四次挥手需要两端都发起断开连接的请求,以及都接收到断开连接的请求。

    • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
    • 服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

    从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACKFIN 一般都会分开发送, 因此是需要四次挥手。

    第1/2/3/4次挥手丢失了,会发生什么?

    第一次挥手丢失(客户端发送FIN报文):

    • 客户端发送FIN报文: 客户端发送一个FIN报文以开始关闭连接。
    • 丢失情况:
      • 服务端未响应: 如果服务端没有收到这个FIN报文,客户端在超时后会重传FIN报文。
      • 服务端收到延迟的FIN: 如果服务端最终收到了延迟的FIN报文,它将正常响应,但连接关闭过程会延迟。

    第二次挥手丢失(服务端发送ACK报文):

    • 服务端发送ACK报文: 服务端接收到客户端的FIN报文后,发送一个ACK报文作为响应。
    • 丢失情况:
      • 客户端未收到ACK: 如果客户端没有收到ACK报文,它会认为服务端没有收到它的FIN报文,并会重传FIN报文。
      • 客户端进入FIN_WAIT_1状态: 客户端会保持在FIN_WAIT_1状态,直到收到服务端的ACK。

    第三次挥手丢失(服务端发送FIN报文):

    • 服务端发送FIN报文: 在发送了ACK之后,服务端准备好关闭连接时,会发送自己的FIN报文。
    • 丢失情况:
      • 客户端未收到FIN: 如果客户端没有收到服务端的FIN报文,服务端在超时后会重传FIN报文。
      • 客户端进入TIME_WAIT状态: 如果客户端已经发送了最后的ACK报文并进入TIME_WAIT状态,但它没有收到服务端的FIN报文,服务端会重传FIN报文,客户端最终会收到并响应。

    第四次挥手丢失(客户端发送ACK报文):

    • 客户端发送ACK报文: 客户端接收到服务端的FIN报文后,发送一个ACK报文作为响应,并进入TIME_WAIT状态。
    • 丢失情况:
      • 服务端未收到ACK: 如果服务端没有收到这个ACK报文,它会认为客户端没有收到它的FIN报文,并会重传FIN报文。
      • 服务端进入LAST_ACK状态: 服务端会保持在LAST_ACK状态,直到收到客户端的ACK或超时。

    timewait状态

    主动发起关闭连接的一方,才会有 TIME-WAIT 状态。
    需要 TIME-WAIT 状态,主要是两个原因

    • 防止历史连接中的数据,被后面相同四元组的连接 错误的接收;
      足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
    • 保证「被动关闭连接」的一方,能被正确的关闭;
      等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。

    为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

    首先是为什么要等待?

    1. 确保最后的ACK报文被接收
      当一方发送了FIN报文并进入TIME_WAIT状态时,它需要等待足够长的时间来确认对方收到了它发送的ACK报文。如果对方没有收到这个ACK报文,它可能会重新发送FIN报文。等待2MSL可以确保最后的ACK报文有足够的时间被对方接收,从而避免了对方重复发送FIN报文。
    2. 处理网络中延迟的报文段
      在网络中,由于路由问题或其他原因,可能会出现报文段在网络中长时间滞留的情况。2MSL的等待时间确保了在连接关闭后,网络中任何延迟的报文段都有足够的时间到达目的地并被处理,或者超时而被丢弃。这样可以防止延迟的报文段影响后续的连接。
    3. 避免旧连接的残余数据影响新连接
      如果立即从TIME_WAIT状态转换到CLOSE状态,并重新使用相同的端口号建立新的连接,那么来自旧连接的延迟报文段可能会错误地被新连接接收。这可能会导致数据错乱或安全问题。2MSL的等待时间减少了这种风险,因为它保证了在重新使用端口之前,所有的旧报文段都已经从网络中消失。

    为什么是2MSL?
    MSL 是 Maximum Segment Lifetime,报⽂最⼤⽣存时间,它是任何报⽂在⽹络上存在的最⻓时间,超过这个时间报⽂将被丢弃。考虑发送的ACK在MSL时间内也未到达服务端,服务端一直等不到确认,就会启动重传,考虑最差的情况,在1MSL的时间重传,再经过1MSL,如果客户端收到重传请求了,就会再回复,如果收不到,在2MSL的时候,这个重传报文也将消失。同时也可能表示这个网络信道特别差,没有要等的必要了。

    总之,2MSL的等待时间是TCP协议设计的一部分,以确保在网络中不会有残留的、可能干扰新连接的旧报文段。这是一个经验值,它基于对网络延迟和报文段生存时间的估计,并在实践中被证明是有效的。

    TIME_WAIT 状态过多会导致什么问题?怎么解决?

    如果服务器有处于 TIME-WAIT 状态的 TCP,则说明是由服务器⽅主动发起的断开请求。

    过多的 TIME-WAIT 状态主要的危害有两种:
    第⼀是内存资源占⽤;
    第⼆是对端⼝资源的占⽤,⼀个 TCP 连接⾄少消耗⼀个本地端⼝;

    怎么解决TIME_WAIT 状态过多?

    • 服务器可以设置SO_REUSEADDR套接字来通知内核,如果端口被占用,但是TCP连接位于TIME_WAIT 状态时可以重用端口。
    • 还可以使用长连接的方式来减少TCP的连接和断开,在长连接的业务里往往不需要考虑TIME_WAIT状态。

    closewait状态

    close wait状态是什么?
    如果客户端是主动关闭连接的一方,当客户端发送FIN报文后,服务器会接收到这个FIN报文并发送ACK确认报文,表示已经接收到FIN报文。此时服务端也需要做一些清理工作,如通知应用层读取完所有数据以及发送所有未发送的数据,等待关闭连接。 就是closewait

    为什么要有close wait?
    等待应用层在这个连接上的数据读取完毕,确认数据传输的完整性之后才发送FIN报文,避免引起应用层数据的丢失。

    如果出现了大量 close wait 状态,是因为什么呢?有什么解决办法?
    如果出现了大量CLOSE_WAIT状态,一般是因为连接的主动关闭方没有及时关闭连接,或者被动关闭方没有发送ACK确认,导致连接一直处于CLOSE_WAIT状态。
    解决方法:

    • 增加连接限制和超时设置,保证在连接关闭时及时关闭连接。
    • 可以设置TCP的keepalive机制,在TCP连接长时间无数据交换时,自动发送检测数据包,防止连接长时间处于CLOSE_WAIT状态。

    保活计时器有什么用?

    CP 还有一个保活计时器(keepalive timer)。

    如果客户端突然发生故障。应当有措施使服务器不要再白白等待下去。就需要使用保活计时器了。
    服务器每收到一次客户端的数据,就重新设置保活计时器,时间的设置通常是两个小时。若两个小时都没有收到客户端的数据,服务端就发送一个探测报文段,以后则每隔 75 秒钟发送一次。若连续发送 10 个探测报文段后仍然无客户端的响应,服务端就认为客户端出了故障,接着就关闭这个连接。

    2.2.5 TCP流量控制

    【一般不考】
    由于IP 层是不可靠的,因此 TCP 需要采取措施使得传输层之间的通信变得可靠。

    流量控制的原理

    • 目的是接收方通过TCP头窗口字段告知发送方本方可接收的最大数据量,用以解决发送速率过快导致接收方不能接收的问题。所以流量控制是点对点控制。
    • TCP是双工协议,双方可以同时通信,所以发送方接收方各自维护一个发送窗和接收窗。
      • 发送窗:用来限制发送方可以发送的数据大小,其中发送窗口的大小由接收端返回的TCP报文段中窗口字段来控制,接收方通过此字段告知发送方自己的缓冲(受系统、硬件等限制)大小。
      • 接收窗:用来标记可以接收的数据大小。
    • TCP是流数据,发送出去的数据流可以被分为以下四部分:已发送且被确认部分 | 已发送未被确认部分 | 未发送但可发送部分 | 不可发送部分,其中发送窗 = 已发送未确认部分 + 未发但可发送部分。接收到的数据流可分为:已接收 | 未接收但准备接收 | 未接收不准备接收。接收窗 = 未接收但准备接收部分。
    • 发送窗内数据只有当接收到接收端某段发送数据的ACK响应时才移动发送窗,左边缘紧贴刚被确认的数据。接收窗也只有接收到数据且最左侧连续时才移动接收窗口。

    说说TCP的流量控制及滑动窗口原理?

    TCP是面向字节流的传输。但是一次传输一个字节效率太低,为了解决这个问题,TCP 引入了窗口,它是操作系统开辟的一个缓存空间。窗口大小值表示无需等待确认应答,而可以继续发送数据的最大值。
    TCP 提供了一种机制,可以让发送端根据接收端的实际接收能力控制发送的数据量,这就是流量控制。接受方每次收到数据包,在发送确认报文的时候,同时告诉发送方,自己的缓存区还有多少空余空间,缓冲区的空余空间,我们就称之为接受窗口大小。这就是 win。

    滑动窗口机制的工作流程:

    1. 初始化: 连接建立时,接收方会告诉发送方它的初始窗口大小。表示发送方可以在没有收到确认的情况下发送的最大数据量(以字节为单位)。
    2. 数据传输: 发送方根据窗口大小发送数据,直到达到窗口的上限。
    3. 确认和窗口更新: 接收方收到数据后,会发送确认(ACK)并可能更新窗口大小。如果接收方处理了部分数据,窗口会向右滑动,露出新的缓冲区空间。
    4. 窗口收缩: 如果接收方的缓冲区空间变得有限,它会发送一个较小的窗口大小,导致发送方的窗口收缩。
    5. 窗口扩张: 当接收方的缓冲区空间再次可用时,它会发送一个较大的窗口大小,允许发送方增加发送的数据量。

    流量控制方法

    • 停等协
      停止等待协议就是保证可靠传输,以流量控制为目的的一个协议。其工作原理简单的说就是每发送一个分组就停止发送,等待对方的确认,在收到确认后再发送下一个分组,如果接受方不返回应答,则发送方必须一直等待。
    • 滑动窗口
      停止等待协议就是保证可靠传输,以流量控制为目的的一个协议。其工作原理简单的说就是每发送一个分组就停止发送,等待对方的确认,在收到确认后再发送下一个分组,如果接受方不返回应答,则发送方必须一直等待。
    • 多帧滑动窗口与后退N帧协议(GBN)
      发送方可以连续发送帧。当发送方发现某一帧在计数器超时后仍未返回其确认信息,则判断为出错或丢失,此时发送方就重传该帧之后的所有帧
      接收方只按顺序接收数据帧,当接收方检测出失序的信息帧后,要求发送方重发最后一个正确接收信息之后所有未确认的帧。
      累积确认:收到某个确认帧,默认之前的都已正确接收,无需确认
      ACKn表示接收方已正确接受n号帧及以前的所有帧。
    • 选择重传协议SR
      选择重传协议只重传出现差错的数据帧或者是计时器超时的数据帧。在接收方要设置有相当容量的缓冲区,用来暂存那些未按序正确收到的帧。,等待所缺序号的数据帧收到后再一并交付给上一层。当一个计时器超时时,发送方仅重传该帧。

    2.2.6 TCP拥塞避免

    拥塞避免与流量控制的区别是什么?

    拥塞控制是让网络能够承受现有的网络负荷,是一个全局性的过程,涉及所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。
    相反,流量控制往往是指点对点的通信量的控制,即接收端控制发送端。因为TCP要保证数据包的有序到达,流量控制它所要做的是抑制发送端发送数据的速率,以便使接收端来得及接收。

    TCP采用了哪些机制来保证拥塞避免?

    ![[Pasted image 20240722205521.png]]
    1慢开始算法(接收窗口rwnd,拥塞窗口cwnd)
    在TCP 刚刚连接好并开始发送TCP 报文段时,先令拥塞窗口cwnd = 1, 即一个最大报文段长度MSS 。每收到一个对新报文段的确认后,将cwnd 加1, 即增大一个MSS 。用这样的方法逐步增大发送方的拥塞窗口cwnd, 可使分组注入网络的速率更加合理。使用慢开始算法后,每经过一个传输轮次(即往返时延RTT), 拥塞窗口cwnd 就会加倍,即cwnd 的大小指数式增长。这样,慢开始一直把拥塞窗口cwnd 增大到一个规定的慢开始门限ssthresh(阔值),然后改用拥塞避免算法。
    2、拥塞避免
    拥寒避免算法的做法如下:发送端的拥塞窗口cwnd 每经过一个往返时延RTT 就增加一个MSS的大小,而不是加倍,使cwnd 按线性规律缓慢增长(即加法增大),而当出现一次超时(网络拥塞)时,令慢开始门限ssthresh 等于当前cwnd 的一半(即乘法减小)。
    3、快重传
    快重传技术使用了冗余ACK 来检测丢包的发生。同样,冗余ACK 也用千网络拥塞的检测(丢了包当然意味着网络可能出现了拥塞)。快重传并非取消重传计时器,而是在某些情况下可更早地重传丢失的报文段。当发送方连续收到三个重复的ACK 报文时,直接重传对方尚未收到的报文段,而不必等待那个报文段设置的重传计时器超时。
    4、快恢复
    快恢复算法的原理如下:发送端收到连续三个冗余ACK (即重复确认)时,执行“乘法减小”算法,把慢开始门限ssthresh 设置为出现拥塞时发送方cwnd 的一半。与慢开始(慢开始算法将拥塞窗口cwnd 设置为1) 的不同之处是,它把cwnd 的值设置为慢开始门限ssthresh 改变后的数值,然后开始执行拥塞避免算法(“加法增大”)’使拥塞窗口缓慢地线性增大。由于跳过了cwnd 从1 起始的慢开始过程,所以被称为快恢复。、

    TCP有哪些重传机制?

    1. 超时重传(Retransmission Timeout, RTO)
    • 基本概念: 当发送方发送一个数据段后,它会启动一个计时器。如果在计时器到期之前没有收到确认(ACK),发送方会假设该数据段丢失,并重传该数据段。
    • 计时器管理: RTO的初始值通常基于估计的往返时间(RTT)。随着网络条件的变化,RTO会动态调整。
    1. 快速重传(Fast Retransmit)
    • 基本概念: 当发送方收到三个重复的ACK时,它会立即重传丢失的数据段,而不是等待计时器超时。
    • 适用场景: 当一个数据段丢失,而后续的数据段到达接收方时,接收方会发送重复的ACK,指示下一个期望的数据段号。
    1. 带选择确认的重传(SACK))
    • 基本概念: SACK 机制就是,在快速重传的基础上,接收方返回最近收到报文段的序列号范围,这样发送方就知道接收方哪些数据包是没收到的。这样就很清楚应该重传哪些数据包。
    1. 重复 SACK(D-SACK)
    • D-SACK,英文是 Duplicate SACK,是在 SACK 的基础上做了一些扩展,主要用来告诉发送方,有哪些数据包,自己重复接受了。DSACK 的目的是帮助发送方判断,是否发生了包失序、ACK 丢失、包重复或伪重传。让 TCP 可以更好的做网络流控。
    • SACK(Selective Acknowledgment): TCP使用SACK选项来精确地告诉发送方哪些数据段已经收到,哪些需要重传。

    TCP有哪些问题?你能提供一些解决思路吗?

    1. 建立连接的延迟:
      • 问题: TCP使用三次握手建立连接,这需要一定的时间,特别是在高延迟的网络环境中。
      • 解决思路: 使用TCP快速打开(TFO)来减少握手次数。或者,如QUIC协议所示,可以使用UDP作为基础协议来减少连接建立的延迟。
    2. 队头阻塞(Head-of-Line Blocking):
      • 问题: 如果一个数据包丢失,TCP需要等待该数据包重传,这会导致后续所有已发送的数据包都必须等待,即使是已经成功接收的数据包。
      • 解决思路: 使用多流TCP(Multi-Stream TCP)或多路径TCP(MPTCP),允许不同的数据流通过不同的路径传输,从而减少队头阻塞的影响。
    3. 拥塞控制:
      • 问题: TCP的拥塞控制算法(如慢启动、拥塞避免、快速重传和快速恢复)可能导致网络利用率不高,特别是在高带宽延迟积(BDP)的网络中。
      • 解决思路: 采用更先进的拥塞控制算法,如CUBIC、BBR(Bottleneck Bandwidth and RTT)或Vegas,这些算法可以更好地适应不同的网络条件。
    4. 窗口大小调整:
      • 问题: TCP的窗口大小调整(慢启动和拥塞避免)可能导致网络资源的利用率不足或过度。
      • 解决思路: 实现更智能的窗口调整策略,例如基于网络状态的动态窗口调整。
    5. 移动性和网络变化:
      • 问题: 当移动设备改变网络接入点时(如从Wi-Fi切换到移动数据),TCP连接可能会中断。
      • 解决思路: 使用MPTCP,它可以在不同的网络接口之间无缝迁移连接。
    6. 安全性:
      • 问题: TCP本身不提供加密,容易受到中间人攻击和其他安全威胁。
      • 解决思路: 使用TLS(传输层安全性)与TCP结合,即TLS over TCP,来提供加密和认证。
    7. 配置和优化:
      • 问题: TCP有许多可配置的参数,这些参数需要根据不同的网络环境进行优化,但通常配置不当。
      • 解决思路: 实现自动化的TCP配置和优化工具,或者使用基于机器学习的算法来动态调整参数。

    6. 安全性问题

    问题描述: TCP 本身没有提供加密和认证机制,这使得它容易受到中间人攻击、数据篡改等威胁。
    解决思路:

    • 使用 TLS/SSL:在 TCP 之上实现加密层,使用 TLS/SSL 协议来保护数据的安全性和完整性。
    • 网络隔离和防火墙:使用网络隔离和防火墙来保护网络中的 TCP 连接免受攻击。

    简述 TCP 的粘包问题及解决方案。

    粘包问题指的是在TCP传输过程中,发送方发送的多个数据包在接收方可能会被合并成一个包,或者一个数据包被拆分成多个包,导致接收方难以正确地将数据分割还原成原始的数据包。

    产生粘包问题的原因主要有以下几点:

    1. TCP的流式传输:TCP是一种流式协议,它不会在数据包之间保留任何边界,只保证数据按照顺序到达。
    2. TCP的缓存机制:TCP为了提高传输效率,会在发送端和接收端使用缓存,这可能导致多个数据包在缓存中合并。
    3. 网络状况:网络状况不佳时,TCP可能会选择将多个小数据包合并成一个大数据包发送,以减少网络交互次数。

    针对TCP粘包问题,常见的解决方案有以下几种:

    1. 固定长度:每个数据包都发送固定长度的字节,如果数据不足,可以用空字节填充。这种方法简单,但可能会浪费带宽。
    2. 分隔符:在每个数据包的末尾添加特殊的分隔符来区分不同的数据包。这种方法需要确保数据本身不包含分隔符,或者对分隔符进行转义处理。
    3. 长度字段:在每个数据包的开始部分添加一个表示数据长度的字段。接收方根据这个长度字段来确定每个数据包的边界。
    4. 应用层分包:在应用层实现分包逻辑,通过编码和解码确保数据的完整性。比如HTTP和FTP等就有自己的处理粘包问题的方式。

    2.2.7 UDP

    为什么QQ采用UDP协议?

    非完全UDP实现、无连接消耗、重传易处理、时延较短、服务器压力较小

    • 首先,QQ并不是完全基于UDP实现。比如在使用QQ进行文件传输等活动的时候,就会使用TCP作为可靠传输的保证。
    • 使用UDP进行交互通信的好处在于,延迟较短,对数据丢失的处理比较简单。同时,TCP是一个全双工协议,需要建立连接,所以网络开销也会相对大。
    • 如果使用QQ语音和QQ视频的话,UDP的优势就更为突出了,首先延迟较小。最重要的一点是不可靠传输,这意味着如果数据丢失的话,不会有重传。因为用户一般来说可以接受图像稍微模糊一点,声音稍微不清晰一点,但是如果在几秒钟以后再出现之前丢失的画面和声音,这恐怕是很难接受的。
    • 由于QQ的服务器设计容量是海量级的应用,一台服务器要同时容纳十几万的并发连接,因此服务器端只有采用UDP协议与客户端进行通讯才能保证这种超大规模的服务

    为什么域名解析用UDP协议而不用TCP?

    因为UDP比较快,UDP的DNS协议只要一个请求、一个应答就好了。
    而使用基于TCP的DNS协议要三次握手、发送数据以及应答、四次挥手,但是UDP协议传输内容不能超过512字节。
    不过客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。

    但是DNS在区域传输的时候使用TCP协议。辅域名服务器会定时(一般3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,会执行一次区域传送,进行数据同步。区域传送使用TCP而不是UDP,因为数据同步传送的数据量比一个请求应答的数据量要多得多。

    QUIC (快速UDP网络传输协议)

    主要特点:

    1. 快速连接:
      • 0-RTT:
        • 首次连接建立:在首次与服务器连接时,客户端和服务器会协商一组共享的密钥和一些特定的会话参数。服务器会提供一个叫作“Session Ticket”的凭证,其中包括用于将来连接的所需信息。
        • 再次连接时使用0-RTT:在后续的连接中,客户端可以使用先前获得的Session Ticket来重新构建必要的密钥材料,并使用这些密钥来加密第一波数据。此时,数据可以在握手完成之前立即发送,从而实现0-RTT连接建立。
      • 1-RTT:
        • 客户端发送ClientHello消息:客户端向服务器发送一个ClientHello消息,其中包括支持的协议版本、密码套件、密钥交换参数等。
        • 服务器响应ServerHello消息:服务器接收ClientHello并响应ServerHello消息。在此消息中,服务器选择协议参数并生成临时密钥。
        • 密钥协商:客户端和服务器使用Diffie-Hellman密钥交换或类似的协议计算共享密钥。
        • 握手完成:一旦共享密钥被计算出来,就可以建立双向安全通信。此时,双方可以开始发送加密的应用数据。
    2. 安全性:QUIC都会采用 TLS 1.3 进行加密。
    3. 多路复用:QUIC协议允许多个请求同时在一个连接上进行传输,因此,它能够更有效地处理HTTP队头阻塞的问题。QUIC协议允许同一连接中可以发起不同的Stream ID进行数据传输。
    4. 前向纠错:QUIC协议采用前向纠错技术,这样就能够避免由于网络传输中数据包的丢失而引起的重传时间延迟问题。
      在传输过程中,如果某个数据包丢失或错误,接收方不会立即请求发送方重新发送数据包,而是使用前向纠错技术,自动对下一个数据包进行纠错,直到错误的数据包到达为止。这样,QUIC协议避免了传统TCP协议因为错误包的丢失而导致等待重传时间延迟的问题。
      前向纠错技术本质上是一种通过在数据包中添加一些额外冗余信息的方式来检测和纠正错误的方法。QUIC协议中的前向纠错技术采用了一些轻量级的公共校验码,将数据划分为一些小的数据块,然后将这些小块叠加在原始数据上并发送。接收方则可以使用这些校验码来独立地检查这些数据块,以确定那些数据出现了错误,并进行纠正。

    2.3 HTTP

    HTTP 请求的过程与原理?

    HTTP协议定义了浏览器怎么向服务器请求文档,以及服务器怎么把文档传给浏览器。
    ![[Pasted image 20240804212903.png]]

    • 每个服务器都有一个进程,它不断监听TCP的端口80,以便发现是否有浏览器向它发出连接建立请求
    • 监听到连接请求,就会建立TCP连接
    • 浏览器向服务器发出浏览某个页面的请求,服务器接着就返回所请求的页面作为响应
    • 最后,释放TCP连接

    HTTP有哪些数据请求方式?

    • GET 对服务器获取资源的简单请求 查
    • POST 向服务器提交数据请求 增
    • PUT 修改指定资源 改
    • DELETE 删除URL标记的指定资源 删
    • CONNECT 用于代理服务器
    • TRANCE 主要用于回环测试
    • OPTIONS 返回所有可用的方法
    • HEAD 获取URL标记资源的首部

    GET和POST的区别

    1. 用途: get请求一般是去取获取数据;post请求一般是去提交数据。
    2. 传参类型: get因为参数会放在url中,所以隐私性,安全性较差,请求的数据长度是有限制的;post请求是没有的长度限制,请求数据是放在body中;
    3. 数据库层面: 从数据库层面来看,GET 符合幂等性和安全性,而 POST 请求不符合。这个其实和 GET/POST 请求的作用有关。按照 HTTP 的约定,GET 请求用于查看信息,不会改变服务器上的信息;而 POST 请求用来改变服务器上的信息。正因为 GET  请求只查看信息,不改变信息,对数据库的一次或多次操作获得的结果是一致的,认为它符合幂等性。安全性是指对数据库操作没有改变数据库中的数据。
      get请求刷新服务器或者回退没有影响,post请求回退时会重新提交数据请求。
    4. 能否被缓存: 从其他层面来看,GET 请求能够被缓存,GET 请求能够保存在浏览器的浏览记录里,GET 请求的 URL  能够保存为浏览器书签。这些都是 POST 请求所不具备的。缓存是 GET  请求被广泛应用的根本,他能够被缓存也是因为它的幂等性和安全性,除了返回结果没有其他多余的动作,因此绝大部分的 GET 请求都被 CDN  缓存起来了,大大减少了 Web 服务器的负担。

    GET的长度限制?

    POST比GET安全吗?

    常见的HTTP状态码有哪些?

    ![[Pasted image 20240804212024.png]]
    1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。
    2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。

    • 200 OK」是最常见的成功状态码,表示一切正常。如果是非 HEAD 请求,服务器返回的响应头都会有 body 数据。
    • 204 No Content」也是常见的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。
    • 206 Partial Content」是应用于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。
      3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向
    • 301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。
    • 302 Found」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。
      301 和 302 都会在响应头里使用字段 Location,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。
    • 304 Not Modified」不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,也就是告诉客户端可以继续使用缓存资源,用于缓存控制。
      4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。
    • 400 Bad Request」表示客户端请求的报文有错误,但只是个笼统的错误。
    • 403 Forbidden」表示服务器禁止访问资源,并不是客户端的请求出错。
    • 404 Not Found」表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。
      5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。
    • 500 Internal Server Error」与 400 类型,是个笼统通用的错误码,服务器发生了什么错误,我们并不知道。
    • 501 Not Implemented」表示客户端请求的功能还不支持,类似“即将开业,敬请期待”的意思。
    • 502 Bad Gateway」通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。
    • 503 Service Unavailable」表示服务器当前很忙,暂时无法响应客户端,类似“网络服务正忙,请稍后重试”的意思

    解释什么是 HTTP 的无状态性,如何在应用层维护状态。

    HTTP(超文本传输协议)是一种无状态的协议,这意味着每个HTTP请求都是独立的,服务器在处理请求后不会保存任何关于客户端之前请求的信息。无状态性有以下特点:

    1. 请求独立性:每个HTTP请求都包含了处理该请求所需的所有信息,服务器不需要使用之前的请求信息来处理当前的请求。
    2. 不保存会话信息:服务器不会在请求之间保留任何状态信息,这意味着它不会“记忆”客户端之前的行为或数据。
    3. 简单性:无状态性简化了HTTP协议的设计,因为服务器不需要管理复杂的会话状态。

    尽管HTTP本身是无状态的,但在实际应用中,经常需要维护状态信息(例如,用户登录状态、购物车内容等)。以下是一些在应用层维护状态的方法:

    1. Cookies
      • 服务器通过HTTP响应头发送一个Set-Cookie头部,客户端(通常是浏览器)会存储这些cookie,并在后续的请求中通过Cookie头部将它们发送回服务器。
      • Cookies可以设置过期时间,用于持久化用户会话。
    2. Session
      • 服务器为每个客户端创建一个会话,并通过一个唯一的会话ID来识别客户端。
      • 会话ID通常存储在客户端的cookie中,或者附加在URL参数上(URL重写)。
      • 服务器在内存或数据库中存储会话数据,直到会话过期或被显式销毁。
    3. Token
      • 令牌(如JSON Web Tokens, JWT)是一种自包含的、不透明的数据结构,可以用于在客户端和服务器之间安全地传输信息。
      • 服务器生成令牌并发送给客户端,客户端在随后的请求中携带这个令牌,服务器通过验证令牌来识别用户状态。
    4. 隐藏表单字段
      • 在Web表单中,可以使用隐藏字段来存储状态信息,这些信息在表单提交时被发送到服务器。
    5. URL重写
      • 状态信息可以编码在URL中,例如作为查询参数或路径的一部分。

    http 报文介绍;http的请求头

    HTTP报文有两种,HTTP请求报文和HTTP响应报文:
    ![[Pasted image 20240804213108.png]]
    HTTP报文分为请求报文和响应报文,两者都具有相同的结构。请求报文分为请求行、请求头部、空行和请求正文四个部分,其中请求头部包含了如下内容:

    1. 请求方法(GET、POST、PUT、DELETE等)
    2. 请求的URI(Uniform Resource Identifier)
    3. HTTP协议版本号(HTTP/1.1)
    4. 请求头部字段,包括:
      • Accept:浏览器可接受的MIME类型
      • Accept-Charset:浏览器能够处理的字符集
      • Accept-Encoding:浏览器能够处理的压缩算法
      • Cookie:服务器发送的cookie
      • User-Agent:浏览器或客户端的类型、版本号等信息
      • Referer:请求的来源
      • Host:请求的主机名
      • Content-Type:请求的数据类型(只有在POST请求中才有)
      • Content-Length:请求的数据长度(只有在POST请求中才有)
      • 客户端自定义请求头字段

    URI和URL有什么区别?

    URI,统一资源标识符(Uniform Resource Identifier, URI),标识的是Web上每一种可用的资源,如 HTML文档、图像、视频片段、程序等都是由一个URI进行标
    识的。
    URL,统一资源定位符(Uniform Resource Location),它是URI的一种子集,主要作用是提供资源的路径。
    它们的主要区别在于,URL除了提供了资源的标识,还提供了资源访问的方式。

    分别介绍 http 1.1 2.0 3.0

    HTTP/1.0 默认是短连接,可以强制开启长连接,HTTP/1.1 默认长连接,HTTP/2.0 采用多路复用

    HTTP/1.0

    • 默认使用短连接,每次请求都需要建立一个 TCP 连接。它可以设置Connection: keep-alive 这个字段,强制开启长连接。

    HTTP/1.1

    • 引入了持久连接,即 TCP 连接默认不关闭,可以被多个请求复用。
    • 分块传输编码,即服务端每产生一块数据,就发送一块,用” 流模式” 取代” 缓存模式”。
    • 管道机制,即在同一个 TCP 连接里面,客户端可以同时发送多个请求。

    HTTP/2.0

    • 二进制协议,1.1 版本的头信息是文本(ASCII 编码),数据体可以是文本或者二进制;2.0 中,头信息和数据体都是二进制。
    • 完全多路复用,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应。
    • 报头压缩,HTTP 协议不带有状态,每次请求都必须附上所有信息。Http/2.0 引入了头信息压缩机制,使用 gzip 或 compress 压缩后再发送。
    • 服务端推送,允许服务器未经请求,主动向客户端发送资源。

    HTTP/3
    HTTP/3主要有两大变化,传输层基于UDP、使用QUIC保证UDP可靠性
    HTTP/2存在的一些问题,比如重传等等,都是由于TCP本身的特性导致的,所以HTTP/3在QUIC的基础上进行发展而来,QUIC(Quick UDP Connections)直译为快速UDP网络连接,底层使用UDP进行数据传输。

    HTTP/3主要有这些特点:

    • 使用UDP作为传输层进行通信
    • 在UDP的基础上QUIC协议保证了HTTP/3的安全性,在传输的过程中就完成了TLS加密握手
    • HTTPS 要建⽴⼀个连接,要花费 6 次交互,先是建⽴三次握⼿,然后是 TLS/1.3 的三次握⼿。QUIC 直接把以往的 TCP 和 TLS/1.3 的 6 次交互合并成了 3 次,减少了交互次数。
    • QUIC 有⾃⼰的⼀套机制可以保证传输的可靠性的。当某个流发⽣丢包时,只会阻塞这个流,其他流不会受到影响。
      ![[Pasted image 20240804213409.png]]

    什么是长连接和短连接?它们各有什么优缺点?

    长连接和短连接是网络通信中两种不同的连接管理策略,它们在HTTP协议和其他网络协议中都有应用。

    长连接(Persistent Connection)
    长连接是一种在建立连接后,可以持续用于多次数据传输的连接。在HTTP/1.1中,默认采用长连接。
    优点:

    1. 减少连接开销:由于连接建立和关闭的次数减少,因此减少了TCP握手和挥手所需的时间,降低了网络延迟。
    2. 提高传输效率:在长连接上可以连续发送多个请求和接收响应,无需每次请求都重新建立连接。
    3. 降低资源消耗:减少了频繁建立和关闭连接带来的系统资源消耗。
      缺点:
    4. 占用资源:长连接需要服务器持续保持状态,即使没有数据传输,也会占用服务器资源。
    5. 空闲连接管理:需要服务器和客户端合理管理空闲连接,例如设置超时时间来关闭长时间无活动的连接。
    6. 队头阻塞:在HTTP/1.1中,虽然使用了长连接,但请求还是串行的,如果前面的请求没有完成,后面的请求将会等待,这可能导致队头阻塞问题。

    短连接(Non-Persistent Connection)
    短连接是一种每次数据传输完成后都会关闭连接,下次传输需要重新建立连接的策略。在HTTP/1.0中,默认采用短连接。
    优点:

    1. 简单管理:由于每次请求完成后都会关闭连接,因此服务器不需要维护大量的连接状态。
    2. 及时释放资源:可以及时释放不再需要的连接资源,避免资源浪费。
      缺点:
    3. 连接开销大:每次请求都需要进行TCP握手和挥手,增加了网络延迟和CPU时间。
    4. 传输效率低:频繁的连接建立和关闭会导致传输效率低下。
    5. 增加网络负载:由于频繁的连接操作,会增加网络的数据传输量。

    总结:
    长连接和短连接各有适用的场景。长连接适合于需要频繁交换数据的场景,如Web浏览、API服务等。短连接则适用于偶尔传输数据的场景,如邮件发送。在实际应用中,应根据具体需求和资源状况选择合适的连接策略。随着技术的发展,如HTTP/2引入了多路复用,可以在单个连接上并行处理多个请求,进一步优化了连接的使用效率。

    HTTP 如何实现长连接?在什么时候会超时?

    什么是 HTTP 的长连接?

    1. HTTP 分为长连接和短连接, 本质上说的是 TCP 的长短连接。TCP 连接是一个双向的通道,它是可以保持一段时间不关闭的,因此 TCP 连接才具有真正的长连接和短连接这一说法。
    2. TCP 长连接可以复用一个 TCP 连接,来发起多次的 HTTP 请求,这样就可以减少资源消耗,比如一次请求 HTML,如果是短连接的话,可能还需要请求后续的JS/CSS。

    如何设置长连接?

    1. 通过在头部(请求和响应头)设置 Connection 字段指定为 keep-alive ,HTTP/1.0协议支持,但是是默认关闭的,从 HTTP/1.1 以后,连接默认都是长连接。

    在什么时候会超时呢?

    1. HTTP 一般会有 httpd 守护进程,里面可以设置 keep-alive timeout ,当 tcp 连接闲置超过这个时间就会关闭,也可以在 HTTP 的 header 里面设置超时时间
    2. TCP 的 keep-alive 包含三个参数,支持在系统内核的 net.ipv4 里面设置;当TCP 连接之后,闲置了 tcp_keepalive_time ,则会发生侦测包,如果没有收到对方的 ACK,那么会每隔 tcp_keepalive_intvl 再发一次,直到发送了tcp_keepalive_probes ,就会丢弃该连接。

    http的keep-alive机制?

    HTTP的Keep-Alive机制是一种在单个TCP连接上传输多个HTTP请求/响应的方法。在没有Keep-Alive的情况下,每个HTTP请求/响应交换都需要建立一个全新的TCP连接,完成后立即关闭该连接。这种方式在处理多个请求时效率低下,因为建立和关闭TCP连接都需要时间和资源。以下是Keep-Alive机制的工作原理和好处:

    工作原理:

    1. 请求头设置:客户端在发送HTTP请求时,可以在请求头中添加一个Connection: Keep-Alive字段,表示客户端希望服务器保持连接活跃。
    2. 服务器响应:如果服务器支持Keep-Alive,它会在响应头中也包含Connection: Keep-Alive字段,并指定一个Keep-Alive头,通常包含连接保持活跃的时间(如Keep-Alive: timeout=5)。
    3. 连接保持:在发送完一个响应后,服务器不会立即关闭连接,而是等待下一个请求。如果在指定的时间内没有新的请求到来,服务器才会关闭连接。
    4. 重复使用:客户端可以在同一个连接上发送多个请求,服务器会依次响应,直到连接被关闭。

    好处:

    • 减少延迟:避免了频繁建立和关闭连接所需的时间。
    • 减少资源消耗:减少了TCP连接建立和终止过程中的系统资源消耗。
    • 提高性能:减少了因TCP连接建立时的握手造成的网络拥塞。

    HTTPS的过程

    公私钥、数字证书、加密、对称加密、非对称加密
    ![[Pasted image 20240804213611.png]]

    1. 客户端发起 HTTPS 请求,连接到服务端的 443 端口。
    2. 服务端有一套数字证书(证书内容有公钥、证书颁发机构、失效日期等)。
    3. 服务端将自己的数字证书发送给客户端(公钥在证书里面,私钥由服务器持有)。
    4. 客户端收到数字证书之后,会验证证书的合法性。如果证书验证通过,就会生成一个随机的对称密钥,用证书的公钥加密。
    5. 客户端将公钥加密后的密钥发送到服务器。
    6. 服务器接收到客户端发来的密文密钥之后,用自己之前保留的私钥对其进行非对称解密,解密之后就得到客户端的密钥,然后用客户端密钥对返回数据进行对称加密,之后传输的数据都是密文啦。
    7. 服务器将加密后的密文返回到客户端。
    8. 客户端收到后,用自己的密钥对其进行对称解密,得到服务器返回的数据。

    为什么需要 非对称加密 和 对称加密?

    对称加密只使用一个密钥,运算速度快,密钥必须保密,无法做到安全的密钥交换。
    非对称加密解决了密钥交换问题,但速度慢。

    http和https的区别

    1. HTTP 是超⽂本传输协议,信息是明⽂传输,存在安全⻛险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在TCP 和 HTTP ⽹络层之间加⼊了 SSL/TLS 安全协议,使得报⽂能够加密传输。安全性
    2. HTTP 连接建⽴相对简单, TCP 三次握⼿之后便可进⾏ HTTP 的报⽂传输。⽽ HTTPS 在 TCP 三次握⼿之后,还需进⾏ SSL/TLS 的握⼿过程,才可进⼊加密报⽂传输。加密方式
    3. HTTP 的端⼝号是 80,HTTPS 的端⼝号是 443。 端口
    4. HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。 证书

    HTTPS 解决了 HTTP 的哪些问题

    因为HTTP 是明⽂传输,存在安全上的风险:

    • 窃听⻛险,⽐如通信链路上可以获取通信内容,用户账号被盗。
    • 篡改⻛险,⽐如强制植⼊垃圾⼴告,视觉污染。
    • 冒充⻛险,⽐如冒充淘宝⽹站,用户金钱损失。

    所以引入了HTTPS,HTTPS 在 HTTP 与 TCP 层之间加⼊了 SSL/TLS 协议,可以很好的解决了这些风险:

    • 信息加密:交互信息⽆法被窃取。
    • 校验机制:⽆法篡改通信内容,篡改了就不能正常显示。
    • 身份证书:能证明淘宝是真淘宝。

    所以SSL/TLS 协议是能保证通信是安全的。

    HTTPS 是如何解决Http的三个风险的?

    • 混合加密的方式实现信息的机密性,解决了窃听的风险。
      对称加密只使用一个密钥,运算速度快,密钥必须保密,无法做到安全的密钥交换。非对称加密使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,解决了密钥交换问题但速度慢。

    • 摘要算法的方式来实现完整性,它能够为数据生成独一无二的「指纹」,指纹用于校验数据的完整性,解决了篡改的风险。
      用摘要算法(哈希函数)来计算出内容的哈希值。

    • 将服务器公钥放入到数字证书中,解决了冒充的风险。
      权威的机构就是 CA (数字证书认证机构),将服务器公钥放在数字证书(由数字证书认证机构颁发)中,只要证书是可信的,公钥就是可信的。

    客户端怎么去校验证书的合法性?

    为了让服务端的公钥被⼤家信任,服务端的证书都是由 CA (_Certificate Authority_,证书认证机构)签名的,CA就是⽹络世界⾥的公安局、公证中⼼,具有极⾼的可信度,所以由它来给各个公钥签名,信任的⼀⽅签发的证书,那必然证书也是被信任的。

    https一定是安全的吗,会被中间人攻击吗?

    HTTPS(超文本传输协议安全)通常被认为是安全的,因为它通过SSL/TLS加密来保护数据在互联网上的传输。然而,没有任何系统是完全安全的,HTTPS也有可能受到某些类型的攻击,包括中间人攻击。

    中间人攻击(MITM)是一种攻击方式,攻击者在通信双方之间拦截或篡改数据。尽管HTTPS设计用来防止这种攻击,但在以下情况下HTTPS连接可能仍然面临风险:

    1. SSL/TLS证书无效或过期:如果网站的SSL/TLS证书无效或已过期,浏览器会发出警告,但用户有时可能会忽略这些警告。
    2. 配置错误:如果服务器配置不当,可能会降低HTTPS的安全性,使中间人攻击成为可能。
    3. 弱加密算法:使用弱加密算法的HTTPS连接更容易被破解。
    4. 网络环境不安全:在不受信任的网络环境中(如公共Wi-Fi),攻击者更容易实施中间人攻击。
    5. 软件漏洞:如果浏览器或操作系统存在漏洞,攻击者可能会利用这些漏洞来攻击HTTPS连接。

    因此,虽然HTTPS大大提高了网络通信的安全性,但它并不是无敌的。用户和组织应该采取额外的安全措施,比如保持软件更新、使用强密码、监控网络活动以及教育用户识别潜在的安全威胁。

    什么是中间人攻击?

    中间人攻击(Man-in-the-Middle Attack,简称MITM攻击)是一种网络安全攻击,在这种攻击中,攻击者插入到两个通信实体之间,使得原本直接进行的通信变为通过攻击者进行。攻击者的目的是窃听、篡改或重新路由通信内容。

    中间人攻击的过程通常包括以下几个步骤:

    1. 拦截:攻击者首先需要找到方法插入到通信路径中。这可能涉及到ARP欺骗、DNS欺骗、会话劫持等技术。
    2. 解密和读取:如果通信是加密的,攻击者可能需要解密通信内容才能读取。这通常需要攻击者获取加密密钥或利用加密协议的漏洞。
    3. 篡改:攻击者可能会修改通信内容,例如更改银行转账的收款账户、插入恶意软件链接或更改消息内容。
    4. 重新加密和转发:在篡改通信内容后,攻击者需要重新加密数据(如果之前解密了的话),然后将其转发给原始的接收者,以避免引起怀疑。

    中间人攻击的常见类型包括:

    • 非加密通信:攻击者可以直接读取和修改未加密的通信数据。
    • 加密通信:攻击者可能需要使用更复杂的手段来解密和篡改加密通信。
    • SSL劫持:攻击者通过伪造SSL证书来欺骗用户,让他们认为他们正在与合法的服务器通信。

    常见的网络攻击原理及方式(xss、csrf、ddos)

    • XSS
      XSS(Cross-Site Scripting,跨站脚本攻击)是一种代码注入攻击。攻击者在目标网站上注入恶意代码,当被攻击者登陆网站时就会执行这些恶意代码,这些脚本可以读取 cookie,session tokens,或者其它敏感的网站信息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等。
    • XSS避免方式
      • url参数使用encodeURIComponent方法转义
      • 尽量不是有InnerHtml插入HTML内容
      • 使用特殊符号、标签转义符。
    • CSRF
      CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
    • CSRF避免方式:
      • 添加验证码
      • 使用token
      • 服务端给用户生成一个token,加密后传递给用户
      • 用户在提交请求时,需要携带这个token
      • 服务端验证token是否正确
    • DDoS
      DDoS又叫分布式拒绝服务,全称 Distributed Denial of Service,其原理就是利用大量的请求造成资源过载,导致服务不可用。
    • DDos避免方式
      • 限制单IP请求频率。
      • 防火墙等防护设置禁止ICMP包等
      • 检查特权端口的开放

    Cookies和Session的区别,如何选择?

    • Cookie 是保存在客户端的一小块文本串的数据。客户端向服务器发起请求时,服务端会向客户端发送一个 Cookie,客户端就把 Cookie 保存起来。在客户端下次向同一服务器再发起请求时,Cookie 被携带发送到服务器。服务端可以根据这个Cookie判断用户的身份和状态。
    • Session 指的就是服务器和客户端一次会话的过程。它是另一种记录客户状态的机制。不同的是cookie保存在客户端浏览器中,而session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是session。客户端浏览器再次访问时只需要从该session中查找用户的状态。

    Cookie和Session有什么区别?

    • 存储位置不一样,Cookie 保存在客户端,Session 保存在服务器端。
    • 存储数据类型不一样,Cookie 只能保存ASCII,Session可以存任意数据类型,一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。
    • 有效期不同,Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般有效时间较短,客户端关闭或者 Session 超时都会失效。
    • 隐私策略不同,Cookie 存储在客户端,比较容易遭到不法获取,早期有人将用户的登录名和密码存储在 Cookie 中导致信息被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。
    • 存储大小不同, 单个Cookie保存的数据不能超过4K,Session可存储数据远高于 Cookie。

    JWT Token是什么?

    JWT(JSON Web Token)是一种在网络应用环境间安全地传输信息的一种基于JSON的开放标准(RFC 7519)。它可以在各方之间以JSON对象的形式安全地传输信息,因为它是经过数字签名的,所以可以被验证和信任。

    一个JWT token由三部分组成,以点(.)分隔,分别是:

    1. Header(头部):通常包含两部分,一个是令牌的类型,即JWT,另一个是所使用的签名算法,如HMAC SHA256或RSA。
    2. Payload(负载):包含声明,声明是关于实体(通常是用户)和其他数据的声明。声明分为三种类型:
      • Registered claims:一组预定义的声明,如iss(issuer,发行者)、exp(expiration time,过期时间)等。
      • Public claims:可以随意定义的声明,但为避免冲突,应在IANA JSON Web Token Registry中定义它们,或将其定义为包含抗冲突命名空间的URI。
      • Private claims:是自定义的声明,用于在同意使用它们的各方之间共享信息。
    3. Signature(签名):用于验证消息在整个过程中没有被更改,并且对于使用私钥进行签名的JWT,它还可以验证JWT的发送者。

    JWT的工作流程:

    1. 创建:用户登录后,服务器会创建一个JWT token。
    2. 发送:服务器将JWT token发送给用户。
    3. 验证:用户每次请求时都会携带这个JWT token,服务器端会验证token的有效性。
    4. 信息提取:一旦验证通过,服务器就可以从token中提取用户信息。

    JWT的使用场景:

    • 身份验证:这是使用JWT最常见的情况,一旦用户登录,每个后续请求都会包含JWT,允许用户访问路由、服务和资源。
    • 信息交换:在安全的各方之间传输信息。
      需要注意的是,虽然JWT提供了方便的信息传输方式,但是它不应该用于存储敏感信息,因为它在传输过程中是可解码的。此外,使用HTTPS协议可以增加JWT的安全性。

    JWT验证过程

    1. 创建Header

      • 选择签名算法,如HMAC SHA256或RSA。
      • 创建一个JSON对象,包含token类型(JWT)和签名算法。
      • 对该JSON对象进行Base64Url编码,形成JWT的Header部分。
        1
        2
        3
        4
        {
        "alg": "HS256",
        "typ": "JWT"
        }
    2. 创建Payload

      • 创建一个包含用户信息的JSON对象,这些信息称为claims。
      • claims可以是标准化的(如issexpsub等)或自定义的。
      • 对该JSON对象进行Base64Url编码,形成JWT的Payload部分。
        1
        2
        3
        4
        5
        6
        {
        "sub": "1234567890",
        "name": "John Doe",
        "admin": true,
        "exp": 1602318800
        }
    3. 生成Signature

      • 使用Header中指定的算法,结合Header和Payload的编码结果,以及一个密钥(对于对称算法如HS256)或私钥(对于非对称算法如RS256),生成签名。
      • 签名的目的是确保token在传输过程中没有被篡改。
      • 将签名部分进行Base64Url编码,形成JWT的Signature部分。
        对于HS256算法,签名过程如下:
        1
        2
        3
        4
        5
        HMACSHA256(
        base64UrlEncode(header) + "." +
        base64UrlEncode(payload),
        secret
        )
    4. 组合JWT

      • 将Header、Payload和Signature三部分用点(.)连接起来,形成一个完整的JWT token。
        1
        eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTYwMjMxODgwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

    JWT验证过程:

    1. 获取JWT

      • 用户在请求时,将JWT token附加到Authorization header中,通常是使用Bearer模式。
        1
        Authorization: Bearer <token>
    2. 分解JWT

      • 服务器端接收到JWT后,首先按点(.)分割,提取出Header、Payload和Signature。
    3. 验证Signature

      • 使用Header中指定的算法和密钥(或公钥,对于非对称算法)对Header和Payload部分进行签名。
      • 将计算出的签名与JWT中的Signature部分进行比较。
      • 如果签名不匹配,则JWT无效。
    4. 验证Claims

      • 验证Payload中的claims是否有效,例如检查exp(过期时间)是否已经过期。
      • 验证标准claims是否符合预期的值,例如iss(issuer)是否是可信任的发行者。
    5. 响应请求

      • 如果JWT有效,则继续处理用户请求。
      • 如果JWT无效,则拒绝请求,通常返回401 Unauthorized状态码。

    在整个过程中,确保使用HTTPS来防止中间人攻击,保护JWT在传输过程中的安全性。此外,密钥或私钥应该被安全地存储,不应该泄露给未授权的第三方。

    2.4 其他层

    CDN (IP和应用)

    CDN(Content Delivery Network)即内容分发网络,它是一种通过在网络各处放置节点服务器来实现以下目标的技术:

    • 内容分发
      • 将源站(如网站服务器)的内容缓存到分布在全球不同地理位置的多个边缘服务器上。当用户发起访问请求时,CDN 系统会根据用户的地理位置、网络状况等因素,自动将用户的请求路由到离用户最近的边缘服务器上,由该边缘服务器响应请求并提供内容。
    • 提高性能
      • 因为内容是从离用户更近的边缘服务器传输,减少了数据传输的距离和网络延迟,从而大大提高了用户访问的响应速度。
    • 减轻源站压力
      • 大量的用户请求被分散到各个边缘服务器上,使得源站只需处理无法在边缘服务器上命中缓存的请求,从而大大减轻了源站的负载。

    VLAN (IP层)

    VLAN(Virtual Local Area Network)即虚拟局域网,具有以下特点:

    • 逻辑划分网络
      • 它是在物理网络的基础上,通过软件技术将一个物理的局域网(LAN)在逻辑上划分成多个不同的广播域,这些划分出来的逻辑网络就是 VLAN。
    • 隔离广播域
      • 每个 VLAN 都相当于一个独立的局域网,一个 VLAN 内的广播流量不会转发到其他 VLAN 中,这样就有效地减少了广播风暴的影响范围,提高了网络的整体性能和稳定性
    • 增强网络安全性
      • 不同 VLAN 之间的通信需要通过三层设备(如路由器或三层交换机)进行转发,这样可以在一定程度上实现不同部门之间的网络隔离,增强了网络的安全性。

    TTL在网络通信中的作用 (IP层)

    TTL(Time to Live)在网络通信中是一个非常重要的概念,它存在于IP数据包的头部字段中,用于限定数据包在网络中可以经过的最大路由器数。以下是TTL的主要作用:

    1. 防止数据包无限循环
      • TTL的主要目的是防止数据包在网络中无限循环。如果一个数据包在网络中不断被错误路由,而没有TTL限制,它可能会永远在网络中循环,导致网络资源的浪费。
    2. 限制数据包在网络中的生存时间
      • 每个数据包在发送时都会被赋予一个TTL值,通常这个值是一个整数,例如64、128等。每经过一个路由器,TTL值就会减1。当TTL值减到0时,路由器将不再转发该数据包,而是发送一个ICMP超时消息给源主机。
    3. 网络调试和故障排除
      • 网络管理员可以使用TTL值来帮助诊断网络问题。例如,通过发送TTL值较小的数据包,管理员可以确定数据包在网络中的哪个点被丢弃,从而定位网络故障。
    4. 路径跟踪
      • 工具如traceroute(在Windows上是tracert)利用TTL值来显示数据包到达目标主机的路径。它通过发送一系列TTL值逐渐增加的数据包,记录每个TTL值对应的路由器,从而构建出数据包的传输路径。
    5. 安全措施
      • TTL也可以作为一种简单的安全措施。例如,通过设置较低的TTL值,可以限制数据包在网络中的传播范围,从而在一定程度上防止某些类型的网络攻击,如拒绝服务攻击(DoS)。
    6. 网络优化
      • TTL有助于网络优化,因为它可以减少因错误路由导致的数据包在网络中的无效传输,从而提高网络的整体性能。

    总之,TTL是IP协议的一个重要特性,它有助于维护网络的健康和效率,防止资源浪费,并在网络故障排除和安全性方面发挥着重要作用。

    路由器和交换机的区别?

    1. 工作层次不同:路由器工作在网络层,通过IP地址转发IP数据报;交换机工作在数据链路层,通过MAC地址负责转发数据帧。
    2. 交换机的主要功能是进行数据帧的交换,支持广播和组播,并且可以实现VLAN划分和负载均衡。而路由器除了基本的路由选择外,还提供路径控制、网络地址转换(NAT)、防火墙功能,能够过滤和控制流入和流出的数据包,实现更高级的网络管理和安全控制。
    3. 由于交换机连接的网段属于同一个广播域,广播数据包会在所有连接的网段上传播,可能导致通信拥塞和安全漏洞。路由器能够分割广播域,连接到路由器上的不同网段被分配到不同的广播域中,广播数据不会穿过路由器,从而提高了网络的安全性和效率。

    TCP, UDP对应的应用层协议

    TCP(传输控制协议)对应的应用层协议:

    1. HTTP/HTTPS:超文本传输协议/安全超文本传输协议,用于网页浏览。
    2. FTP:文件传输协议,用于文件传输。
    3. SMTP:简单邮件传输协议,用于电子邮件发送。
    4. IMAP/POP3:互联网消息存取协议/邮局协议版本3,用于电子邮件接收。
    5. Telnet:远程登录协议,用于远程终端访问。
    6. SSH:安全外壳协议,用于安全地访问远程
    7. SMB:服务器消息块协议,用于文件共享和打印服务。

    UDP(用户数据报协议)对应的应用层协议:

    1. DNS(域名系统查询):用于将域名解析为IP地址。
    2. DHCP:动态主机配置协议,用于自动分配IP地址和配置网络参数。
    3. NTP:网络时间协议,用于网络时间同步。
    4. SNMP:简单网络管理协议,用于网络设备管理。
    5. TFTP:简单文件传输协议,用于简单的文件传输。
    6. VoIP:网络电话(如Skype、Zoom等),用于实时语音和视频通信。
    7. 视频流和在线游戏:许多实时视频流和在线游戏应用使用UDP,因为它能提供更低的延迟。

    UDP的广播和多播

    UDP(用户数据报协议)是一种无连接的传输层协议,它提供了一种不可靠的数据传输服务。UDP广播和多播是UDP协议支持的数据传输方式,它们允许数据同时发送给多个接收者。

    UDP广播
    广播是一种网络通信方式,其中数据被发送到网络中的所有设备。在IP网络中,广播通常是通过特殊的IP地址255.255.255.255来实现的,或者通过网络中的广播地址(通常是网络地址加上全1的主机部分)。
    特点:

    • 广播域:广播消息只能在本地网络(广播域)内传播,不会跨越路由器。
    • 所有设备接收:网络上的所有设备都会接收到广播消息,无论它们是否需要这些数据。
    • 无连接:UDP广播不需要建立连接,数据包被直接发送到网络接口。
      用途:
    • 网络发现:例如,使用ARP(地址解析协议)来发现同一网络上的设备。
    • 网络配置:如DHCP(动态主机配置协议)服务器使用广播来分配IP地址。

    UDP多播
    多播是一种允许数据从一个发送者同时传输到多个接收者的通信方式。多播使用多播组地址,其中每个多播组由一个特定的IP地址表示(D类地址,范围从224.0.0.0到239.255.255.255)。
    特点:

    • 选择性接收:只有加入特定多播组的设备才会接收多播数据。
    • 跨越网络:多播数据可以跨越多个网络,但需要网络设备(如路由器)支持多播转发。
    • 效率:与广播相比,多播更有效率,因为它只发送给需要数据的设备。
      用途:
    • 多媒体流:如视频会议和直播电视。
    • 分布式系统:在分布式系统中,多播用于消息分发。

    UDP多播

    DNS地址解析的过程(应用层)

    ![[Pasted image 20240804211927.png]]

    1. 解析域名,浏览器查看(浏览器)缓存,再查看 hosts 文件,看看其中有没有和这个域名对应的规则,如果有的话就直接使用 hosts 文件里面的 ip 地址。
    2. 如果在本地的 hosts 文件没有对应的 ip 地址,浏览器会发出一个 DNS请求到本地DNS服务器
    3. 本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果,此过程是递归的方式进行查询。如果没有,本地DNS服务器还要向DNS根服务器进行查询
    4. 根DNS服务器没有记录具体的域名和IP地址的对应关系,而是告诉本地DNS服务器,你可以到顶级域名服务器上去继续查询,并给出顶级域名服务器的地址。这种过程是迭代的过程。
    5. 本地DNS服务器继续向顶级域名服务器发出请求,在这个例子中,请求的对象是.com顶级域名服务器。.com顶级域名服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的权威域名服务器的地址
    6. 最后,本地DNS服务器向对应的权威域名服务器发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。

    DHCP 动态主机配置协议(应用层)

    常用于给主机动态地分配IP 地址,它提供了即插即用联网的机制,应用层协议,它是基于UDP 的。
    使用客户/服务器(C/S)方式。需要IP 地址的主机在启动时就向DHCP 服务器广播发送发现报文,这时该主机就成为DHCP 客户。本地网络上所有主机都能收到此广播报文,但只有DHCP 服务器才回答此广播报文。DHCP 服务器先在其数据库中查找该计算机的配置信息。若找到,则返回找到的信息。若找不到,则从服务器的IP 地址池中取一个地址分配给该计算机。DHCP 服务器的回答报文称为提供报文。

    ARP 地址解析协议(数据链路层)

    每台主机都设有一个ARP 高速缓存,用来存放本局域网上各主机和路由器的IP地址到MAC 地址的映射表,称ARP 表。使用ARP 来动态维护此ARP 表。

    • 首先,每台主机都会在自己的 ARP 缓冲区中建立一个 ARP 列表,以表示 IP 地址和 MAC 地址的对应关系。
    • 当源主机需要将一个数据包要发送到目的主机时,会首先检查自己的 ARP 列表,是否存在该 IP 地址对应的 MAC 地址;如果有﹐就直接将数据包发送到这个 MAC 地址;如果没有,就向本地网段发起一个 ARP 请求的广播包,查询此目的主机对应的 MAC 地址。此 ARP 请求的数据包里,包括源主机的 IP 地址、硬件地址、以及目的主机的 IP 地址。
    • 网络中所有的主机收到这个 ARP 请求后,会检查数据包中的目的 IP 是否和自己的 IP 地址一致。如果不相同,就会忽略此数据包;如果相同,该主机首先将发送端的 MAC 地址和 IP 地址添加到自己的 ARP 列表中,如果 ARP 表中已经存在该 IP 的信息,则将其覆盖,然后给源主机发送一个 ARP 响应数据包,告诉对方自己是它需要查找的 MAC 地址。
    • 源主机收到这个 ARP 响应数据包后,将得到的目的主机的 IP 地址和 MAC 地址添加到自己的 ARP 列表中,并利用此信息开始数据的传输。如果源主机一直没有收到 ARP 响应数据包,表示 ARP 查询失败。

    什么是IP协议?

    IP协议(Internet Protocol)又被称为互联网协议,是支持网间互联的数据包协议,工作在网际层,主要目的就是为了提高网络的可扩展性。通过网际协议IP,可以把参与互联的,性能各异的网络看作一个统一的网络

    IP协议主要有以下几个作用: 寻址 分组

    • 寻址和路由:在IP数据报中携带源IP地址和目的IP地址来表示该数据包的源主机和目标主机。IP数据报在传输过程中,每个中间节点(IP网关、路由器)只根据网络地址来进行转发,如果中间节点是路由器,则路由器会根据路由表选择合适的路径。IP协议根据路由选择协议提供的路由信息对IP数据报进行转发,直至目标主机。
    • 分段和重组:IP数据报在传输过程中可能会经过不同的网络,在不同的网络中数据报的最大长度限制是不同的,IP协议通过给每个IP数据报分配一个标识符以及分段与组装的相关信息,使得数据报在不同的网络中能够被传输,被分段后的IP数据报可以独立地在网络中进行转发,在达到目标主机后由目标主机完成重组工作,恢复出原来的IP数据报。

    路由选择协议(RIP,OSPF,BGP 网络层)

    路由选择协议是网络中路由器用来相互交换路由信息的规则集合,它们帮助路由器确定数据包从源头到目的地的最佳路径。以下是三种常见的路由选择协议的简要说明:

    1. RIP(路由信息协议)
      • 类型:距离矢量协议。
      • 工作原理:RIP通过跳数来衡量到达目标网络的距离,最大跳数为15,超过15跳则认为网络不可达。
      • 更新方式:周期性广播整个路由表给邻居路由器。
      • 特点
        • 简单易实现。
        • 路由更新频率较高,消耗网络带宽。
        • 收敛速度慢,不适合大型网络。
    2. OSPF(开放最短路径优先)
      • 类型:链路状态协议。
      • 工作原理:OSPF通过计算每个路由器的链路状态来构建网络拓扑图,然后使用迪杰斯特拉算法计算到达每个网络的最短路径。
      • 更新方式:仅在链路状态发生变化时,向所有OSPF路由器发送更新。
      • 特点
        • 支持区域划分,适合大型网络。
        • 收敛速度快,路由信息更加精确。
        • 支持多种路径度量,如带宽、延迟等。
    3. BGP(边界网关协议)
      • 类型:路径矢量协议。
      • 工作原理:BGP主要用于不同自治系统(AS)之间的路由选择,它考虑到达目的地的整个路径,而不仅仅是跳数。
      • 更新方式:通过TCP连接交换更新信息,确保更新可靠传输。
      • 特点
        • 主要用于互联网路由选择,支持大规模网络。
        • 能够实现复杂的路由策略和控制。
        • 路由决策考虑自治系统路径、网络策略等多种因素。
          每种协议都有其适用的网络环境和场景,RIP适用于小型网络,OSPF适用于大型企业网络,而BGP则是互联网上不同自治系统之间路由选择的关键协议。

    子网掩码,划分子网

    子网掩码是一个32位的二进制数字,主要用于将一个大的IP网络划分为若干小的子网络,即子网。在子网掩码中,连续的1代表网络地址部分,而连续的0代表主机地址部分。

    使用子网掩码的原因主要包括以下几点:

    1. 更有效的IP地址分配:通过划分子网,可以将一个大的网络分割成多个小网络,这样可以更有效地利用IP地址空间,避免地址浪费。
    2. 控制广播域:子网可以限制广播的范围,因为广播消息不会跨越子网边界,这有助于减少网络上的广播流量,提高网络性能。
    3. 增强网络安全性:子网通过逻辑上隔离不同的网络段,可以提高网络的安全性,防止未经授权的访问。
    4. 简化网络管理:子网掩码可以帮助网络管理员更容易地管理网络,例如,对不同子网实施不同的网络策略或进行更细致的流量控制。
    5. 支持更灵活的网络设计:子网掩码允许网络根据实际需要灵活地进行设计,比如根据部门或地理位置来划分网络。

    在IP地址与子网掩码结合使用时,可以进行位运算来得到网络地址和主机地址,从而确定一个IP地址属于哪一个子网。

    • 子网划分:IP地址由子网号和主机号两部分组成

    • 主机号全0表示本网络本身,主机号全1表示本网络的广播地址

    • 子网掩码:将一个网络再次划分

    • CIDR:在变长子网掩码上提出一种消除传统A、B、C类网络划分,并实现路由聚合的一种划分方法
      ![[Pasted image 20240722210247.png]]

    IPv4内网地址分为A,B和C类
    以下这些地址都属于内网
    A类地址范围:10.0.0.0 - 10.255.255.255
    B类地址范围:172.16.0.0 - 172.31.255.255
    C类地址范围:192.168.0.0 - 192.168.255.255

    2.5 网络安全

    简单说说有哪些常见的网络安全攻击?

    1. 病毒:通过感染文件或系统,进而自我复制并传播的恶意软件。
    2. 特洛伊木马:伪装成合法软件的恶意程序,一旦被执行,它会帮助黑客未经授权地访问用户的计算机系统。
    3. 钓鱼攻击:通过伪造邮件、网站等手段诱骗用户透露个人信息,如用户名、密码和信用卡信息等。
    4. 中间人攻击:攻击者在通信双方之间拦截或篡改数据。
    5. 分布式拒绝服务(DDoS)攻击:通过大量的网络请求使目标服务器瘫痪,阻止合法用户访问服务。
    6. SQL注入:在数据库查询中插入恶意SQL语句,以控制数据库或窃取数据。
    7. 跨站脚本攻击(XSS):在用户的浏览器中执行恶意脚本,以劫持用户会话或获取用户信息。
    8. 零日攻击:利用软件中未被发现的漏洞进行的攻击,由于软件供应商尚未修补这些漏洞,因此特别危险。

    什么是DNS劫持?如何应对?

    DNS劫持即域名劫持,是通过将原域名对应的IP地址进行替换,从而使用户访问到错误的网站,或者使用户无法正常访问网站的一种攻击方式。

    DNS劫持过程:攻击者在DNS解析过程中介入,可能通过以下方式:
    - 篡改DNS响应:攻击者发送伪造的DNS响应给用户或DNS服务器,使得网址解析为攻击者控制的IP地址。
    - 中毒DNS缓存:攻击者在DNS服务器缓存中插入错误的解析记录。
    - 控制DNS服务器:攻击者直接控制DNS服务器,修改解析记录。

    如何应对DNS劫持:
    1. 使用可靠的DNS服务器:选择知名且信誉良好的DNS服务提供商,如Google DNS、OpenDNS等。
    2. 定期检查DNS设置:确保设备的DNS设置指向正确的服务器。
    3. 直接通过IP地址访问网站,避开DNS劫持

    什么是 CSRF 攻击?如何避免?

    CSRF(跨站请求伪造)攻击是一种利用用户已认证的身份在用户不知情的情况下执行非用户意愿的操作的攻击方式。挟持用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。
    ![[Pasted image 20240902223157.png]]
    如何避免CSRF攻击:

    1. 使用CSRF令牌:在表单或AJAX请求中加入一个CSRF令牌,每次请求时都验证令牌的有效性。
    2. 检查Referer头:服务器可以检查HTTP请求的Referer头,确保请求来源于信任的网站。
    3. 使用双因素认证:对于敏感操作,要求用户进行额外的验证,比如输入验证码或使用二次密码。
    4. 限制cookie的作用域:设置cookie为仅通过HTTPS传输,并限制其作用域,防止通过非HTTPS请求发送。

    什么是 DoS、DDoS、DRDoS 攻击?

    DoS(Denial of Service,拒绝服务)攻击是指攻击者通过某种手段让目标服务器或网络资源无法提供正常的服务,使得合法用户无法访问。常见的DoS攻击手段包括但不限于通过发送大量无效请求来消耗目标系统的资源,使得系统瘫痪。

    DDoS(Distributed Denial of Service,分布式拒绝服务)攻击是DoS攻击的一种升级形式。攻击者控制大量的僵尸主机(被黑客入侵的电脑),通过这些主机向目标发送大量请求,从而使得目标服务器或网络资源因请求过多而无法响应正常的服务请求。

    DRDoS(Reflection and Amplification DDoS,反射和放大DDoS)攻击则是DDoS攻击的一种特殊类型。攻击者利用网络中的某些服务(如DNS、NTP等)的特性,通过伪造源IP地址发送少量请求,使得这些服务向目标发送大量响应数据,从而实现用较小的攻击流量造成目标网络瘫痪的效果。

    防范措施包括:

    1. 网络层防御:
      • 使用防火墙来过滤非法或可疑的流量。
      • 设置访问控制列表(ACLs),限制来自特定IP地址或地址段的流量。
      • 利用入侵检测和防御系统(IDS/IPS)来监控和阻止恶意流量。
    2. 带宽防御:
      • 增加带宽虽然不能完全防御DDoS攻击,但可以减轻攻击的影响。
      • 与ISP合作,当检测到DDoS攻击时,快速切换到清洁的流量路径。
    3. 应用层防御:
      • 对应用层进行优化,确保其可以处理异常流量。
      • 使用负载均衡器分散请求,减少单一服务器的压力。
    4. 反射和放大攻击防御:
      • 限制或关闭不必要的网络服务,特别是那些已知可以被用于放大攻击的服务。
      • 对所有外出流量进行源地址验证,防止IP地址伪造。
    5. 安全审计和监控:
      • 定期进行网络安全审计,检查系统是否存在安全漏洞。
      • 实时监控网络流量,一旦检测到异常流量模式,立即采取措施。

    SYN_FLOOD

    什么是 XSS 攻击,如何避免?

    有了解SQL注入吗?如何避免?

    SQL注入是一种常见的网络攻击技术,它允许攻击者通过在Web应用程序输入字段中插入或“注入”恶意SQL代码来操纵数据库。攻击者可以利用SQL注入执行任意SQL代码,访问、修改或删除数据库中的数据,甚至在某些情况下,可以完全控制数据库服务器。

    SQL注入的工作原理:

    1. 输入验证不足:Web应用程序没有正确地验证或清理用户输入。
    2. 动态SQL构造:应用程序动态地构建SQL查询,并将用户输入直接嵌入到查询中。
    3. 执行恶意SQL:攻击者提供的数据被解释为SQL代码的一部分,导致执行非预期的查询。

    如何避免SQL注入:

    1. 使用预编译的语句(参数化查询):这是避免SQL注入最有效的方法。预编译的语句会提前定义SQL查询的结构,并将用户输入作为参数传递,而不是直接嵌入到查询中。
      例如,在Java中,使用预编译的语句:
      1
      2
      3
      PreparedStatement stmt = connection.prepareStatement("SELECT * FROM products WHERE name = ?");
      stmt.setString(1, userInput);
      ResultSet rs = stmt.executeQuery();
    2. 使用存储过程:存储过程可以限制用户直接与数据库交互,减少SQL注入的风险。
    3. 输入验证和清理:对所有用户输入进行严格的验证,只接受预期格式的输入。移除或转义可能导致SQL注入的特殊字符。
    4. 最小权限原则:数据库连接应该使用权限最低的账户,仅提供执行必要操作的能力。
    5. 错误处理:不要在用户界面上显示详细的数据库错误信息,这可能会为攻击者提供线索。
    6. 安全审计和测试:定期进行安全审计和使用自动化工具进行SQL注入测试。

    对称加密与非对称加密有什么区别?

    简单讲讲AES和RSA?

    平时在进行Web开发时,应当怎样保证网络安全?

    1. 使用安全的编码实践:
    • 了解安全原则:熟悉OWASP安全原则和最佳实践。
    • 数据验证:对所有输入进行严格的验证,包括类型、长度、格式和范围。
    • 数据清理:对用户输入进行清理,以防止跨站脚本攻击(XSS)和SQL注入。
    • 使用安全的API:使用已知安全的库和框架,避免使用有已知漏洞的API。
    1. 使用HTTPS:
    • 始终使用HTTPS:通过SSL/TLS加密数据传输,保护数据不被窃听或篡改。
    • HSTS:实施HTTP严格传输安全,强制浏览器通过HTTPS访问网站。
    1. 管理会话和认证:
    • 安全的会话管理:使用安全的会话ID生成机制,设置适当的会话超时。
    • 密码安全:使用强密码策略,存储密码时使用哈希和盐。
    • 多因素认证:对于敏感操作,实施多因素认证。
    1. 防止常见攻击:
    • 防止SQL注入:使用参数化查询或预编译语句。
    • 防止XSS攻击:输出编码,使用内容安全策略(CSP)。
    • 防止CSRF攻击:使用CSRF令牌。
    • 限制请求频率:防止暴力破解和拒绝服务攻击。
    1. 安全配置:
    • 最小权限原则:确保应用程序和数据库使用最小权限运行。
    • 更新和补丁:定期更新操作系统、Web服务器、数据库和应用程序。
    • 错误处理:不要泄露敏感信息,如堆栈跟踪或错误详情。
    1. 安全测试:
    • 代码审查:进行代码审查,寻找潜在的安全漏洞。
    • 自动化测试:使用自动化工具进行安全测试,如静态应用程序安全测试(SAST)和动态应用程序安全测试(DAST)。
    • 渗透测试:定期进行渗透测试,模拟攻击者的攻击行为。
    1. 安全监控和响应:
    • 日志记录:记录所有安全相关事件,并定期检查日志。
    • 入侵检测系统:使用入侵检测系统(IDS)来监控异常行为。
    • 应急响应计划:制定并实施数据泄露和其他安全事件的应急响应计划。

    3 计算机系统

    【CSDN】这可能最全的操作系统面试题
    ![[Pasted image 20240804185917.png]]

    3.1 操作系统–综合

    操作系统的四大特性

    操作系统:**操作系统是一组控制和管理计算机硬件和软件资源、合理地对各类作业进行调度,以及方便用户的程序集合。

    特征: 并发性,虚拟性,共享性,异步性

    操作系统的主要功能

    • 处理器(CPU)管理:CPU的管理和分配,主要指的是进程管理。
    • 内存管理:内存的分配和管理,主要利用了虚拟内存的方式。
    • 外存管理:外存(磁盘等)的分配和管理,将外存以文件的形式提供出去。
    • I/O管理:对输入/输出设备的统一管理。

    用户态与内核态

    • 内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
    • 用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。

    最大的区别就是权限不同,在运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。

    • 为什么要有这两态?
      需要限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据,或者获取外围设备的数据,并发送到网络,CPU划分出两个权限等级 – 用户态和内核态。

    为了保证内核程序不被应用程序破坏。通过系统调用的方式来使用系统功能,可以保证系统的稳定性和安全性,防止用户随意更改或访问系统的数据或命令。系统调用是由操作系统提供的一个或多个子程序模块实现的。

    • 用户态:读时钟,取数,寄存器清零,内存读写,运算
    • 核心态:置时钟,I/O,判断内存哪里可以存放
      系统调用通过陷进机制,从用户态转换到内核态。
      系统调用过程:传递系统调用参数,执行trap指令,执行相关的服务程序,返回用户态

    宏内核与微内核

    操作系统中用到的数据结构

    操作系统原理课程里面有很多数据结构的实现,部分归纳总结如下:

    • 链表
      进程管理-PCB的连接 外存分配方式-链接分配
    • 队列
      进程通信-消息队列的实现 处理机调度-任务就绪列队的实现
      存储器管理-先进先出算法/时钟置换算法的实现(循环队列)

    • 存储器管理-LRU(Least Recently used)最近最少使用置换算法

    • 进程管理-进程家族关系描述:进程树
    • 散列表
      内存管理-连续分配方式:Hash算法 文件管理-hash文件

    操作系统中的调度算法

    系统调用

    讲讲Linux操作系统的启动过程

    1. 计算机开机后,首先执行的是BIOS(基本输入输出系统)或UEFI(统一可扩展固件接口)。BIOS会进行开机自检(Power-On-Self-Test, POST),对硬件进行检测和初始化。
    2. 自检完成后,磁盘中的第一个分区,也被称为 MBR(Master Boot Record) 主引导记录,被读入到一个固定的内存区域并执行里面的boot程序。
    3. 复制完成后,boot 程序读取启动设备的根目录。boot 程序要理解文件系统和目录格式。然后 boot 程序被调入内核,把控制权移交给内核。直到这里,boot 完成了它的工作。系统内核开始运行。
    4. 内核启动代码是使用汇编语言完成的,主要包括创建内核堆栈、识别 CPU 类型、计算内存、禁用中断、启动内存管理单元等,然后调用 C 语言的 main 函数执行操作系统部分。
    5. 操作系统会进行自动配置,检测设备,加载配置文件,被检测设备如果做出响应,就会被添加到已链接的设备表中,如果没有相应,就归为未连接直接忽略。
    6. 内核初始化完成后,会启动第一个用户空间进程,即init进程(PID为1)。
    7. init进程负责初始化系统环境,并启动其他系统进程。,如网络服务、日志服务等。
    8. 在系统初始化脚本执行完成后,会启动各种用户空间的服务,如SSH、Apache、MySQL等。
    9. 最后,系统会启动登录管理器,如GDM(GNOME Display Manager)、LightDM等。
    10. 用户输入用户名和密码登录后,会启动用户的会话管理器,如GNOME、KDE等。用户可以开始使用操作系统。

    ![[Pasted image 20240804190050.png]]

    3.2 进程管理

    并发和并行有什么区别?

    并发就是在一段时间内,多个任务都会被处理;但在某一时刻,只有一个任务在执行。单核处理器做到的并发,其实是利用时间片的轮转,例如有两个进程A和B,A运行一个时间片之后,切换到B,B运行一个时间片之后又切换到A。因为切换速度足够快,所以宏观上表现为在一段时间内能同时运行多个程序。
    并行就是在同一时刻,有多个任务在执行。这个需要多核处理器才能完成,在微观上就能同时执行多条指令,不同的程序被放到不同的处理器上运行,这个是物理上的多个进程同时进行。

    进程、线程(用户级、内核级)的区别

    • 对于有线程系统:
      • 进程是资源分配的独立单位
      • 线程是处理机调度的独立单位
    • 对于无线程系统:
      • 进程是资源调度、分配的独立单位

    进程是程序的动态化运行,是程序的实体。为了提高进程的并发度,引入了线程,线程不能脱离进程而运行,不单独占有资源,在引入了线程之后,线程就是处理机调度的基本单位。
    区别:

    1. 资源拥有: 进程是资源分配的独立单位,线程一般共享进程的资源。
    2. 通信方式: 进程间的通信需要依赖特定的机制,如管道、消息队列、信号量、共享内存等;同一进程下的不同线程间共享同一进程的地址空间,可以直接读写进程数据段来进行通信
    3. 调度方式: 进程切换需要的开销较大,因为它涉及到不同地址空间和资源的切换;线程切换的开销较小,因为它通常只涉及CPU的寄存器和栈的切换。

    线程和协程(内核级线程和用户级线程)的区别:

    • 内存开销:创建一个协程需要2kb,栈空间不够会自动扩容, 创建一个线程需要1M空间。
    • 创建和销毁:创建线程是在内核态,开销更大;协程是在运行时管理,属于用户态,开销小。
    • 切换成本:线程切换需要保存各种寄存器;协程保存的寄存器比较少,它能执行更多的指令。基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

    3.2.1 进程

    什么是进程调度(上下文切换)?

    对于单核单线程 CPU 而言,在某一时刻只能执行一条 CPU 指令。上下文切换 (Context Switch) 是一种将 CPU 资源从一个进程分配给另一个进程的机制。从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。在切换的过程中,操作系统需要先存储当前进程的状态 (包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。

    进程的状态模型

    在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。

    • 运行状态(Running):该时刻进程占用 CPU;
    • 就绪状态(Ready):可运行,由于其他进程处于运行状态而暂时停止运行;
    • 阻塞状态(Blocked):该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它CPU控制权,它也无法运行;

    进程还有另外两个基本状态:

    • 创建状态(new):进程正在被创建时的状态;
    • 结束状态(Exit):进程正在从系统中消失时的状态;
      ![[Pasted image 20240804173230.png]]
      (三状态;七状态+就绪挂起、阻塞挂起)

      在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存。就需要一个新的状态,来描述进程没有占用实际的物理内存空间的情况,这个状态就是挂起状态

      挂起状态可以分为两种:

      • 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
      • 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行

    有哪些进程调度算法?

    ![[Pasted image 20240804174029.png]]

    • 先来先服务调度算法
      每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。

    • 最短作业优先调度算法

      1
      优先把短作业执行完,再执行长作业。缺点是如果短作业很多,长作业会被搁置。

      优先选择运行时间最短的进程来运行,这有助于提高系统的吞吐量。

    • 高响应比优先调度算法

      1
      2
      是在短作业优先的基础上改进,加上一个随时间叠加的权重。等待时间越长,权重越高。
      这种算法既可以优先完成短作业,又能确保长作业不会长期饥饿。

      每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行
      $$
      相应比优先级 = \frac{等待时间 + 要求服务时间}{要求服务时间}
      $$
      (高响应比优先调度算法是「理想型」的调度算法,现实中是实现不了的)

    • 时间片轮转调度算法

      1
      定义一个时间片长度,平均给每个进程分配时间片,一旦时间片用完,作业就会由进行转为就绪,等待重新被调度。缺点是如果作业比较多,那长作业可能需要好几轮才可以被执行完。

      每个进程被分配一个时间段,称为时间片(Quantum),即允许该进程在该时间段中运行。

      • 如果时间片用完,进程还在运行,那么将会把此进程从 CPU 释放出来,并把 CPU 分配给另外一个进程;
      • 如果该进程在时间片结束前阻塞或结束,则 CPU 立即进行切换;
    • 最高优先级调度算法
      从就绪队列中选择最高优先级的进程进行运行。
      进程的优先级可以分为,静态优先级和动态优先级:

      • 静态优先级:创建进程时候,就已经确定了优先级了,然后整个运行时间优先级都不会变化;
      • 动态优先级:根据进程的动态变化调整优先级,比如如果进程运行时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升高其优先级,也就是随着时间的推移增加等待进程的优先级。
    • 多级反馈队列调度算法

      1
      设置了不同的队列,可以分类为高、中、低优先级。优先级越高,分配的时间片越小。首先刚来的会进入高优先级,如果没执行完进入下一级的优先级。只有上一个队列执行完后,才可以开始下一个队列。缺点还是长作业的问题,加入上一级队列一直有作业,那下一级别的队列的进程就会饥饿

      多级反馈队列(Multilevel Feedback Queue)调度算法是「时间片轮转算法」和「最高优先级算法」的综合和发展。
      ![[Pasted image 20240804174004.png]]

      • 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短
      • 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
      • 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;

    进程间有哪些通信方式?

    每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
    ![[Pasted image 20240804183350.png]]

    管道、消息队列、共享内存、信号量、信号、套接字

    • 管道:简单;效率低,容量有限;

    • 消息队列:不及时,写入和读取需要用户态、内核态拷贝。

    • 共享内存区:能够很容易控制容量,速度快,但需要注意不同进程的同步问题。

    • 信号量:不能传递复杂消息,一般用来实现进程间的同步;

    • 信号:它是进程间通信的唯一异步机制。

    • Socket:用于不同主机进程间的通信。

    • 管道
      管道传输数据是单向的。A进程将数据以字节流写入管道,B进程需要等待A进程将信息写完以后才能读出来。
      比如 ps -f | grep xxx
      缺点:效率低,不适合进程间频繁地交换数据

    • 消息队列
      在发送数据时,按照一个个独立单元发送,发送方和接收方约定好消息的类型和格式
      缺点:不适合比较大数据的传输;存在用户态与内核态之间的数据拷贝开销

    • 共享内存
      申请一块虚拟地址空间,不同进程通过这块虚拟地址空间映射到相同的物理地址空间。无需拷贝。
      缺点:会出现冲突

    • 信号量
      防止多进程竞争共享资源,而造成的数据错乱的一个约束和保护机制。
      信号量表示资源的数量,控制信号量的方式有两种原子操作:P 操作和 V 操作。P 操作为申请资源,V 操作是归还资源。
      信号初始化为 1,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问;

    • 信号
      信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,用户进程可以有几种处理方式:

      • 执行默认操作。Linux 对每种信号都规定了默认操作,例如,SIGTERM 信号,就是终止进程的意思。
      • 捕捉信号。可以为信号定义一个信号处理函数。当信号发生时,执行相应的信号处理函数。
      • 忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILLSEGSTOP,它们用于在任何时候中断或结束某一进程
    • 套接字
      跨网络与不同主机上的进程之间通信

    什么是僵尸进程?

    僵尸进程是已完成且处于终止状态,但在进程表中却仍然存在的进程。

    僵尸进程一般发生有父子关系的进程中,一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中。

    什么是孤儿进程?

    一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程 (进程 ID 为 1 的进程) 所收养,并由 init 进程对它们完成状态收集工作。因为孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。

    一个进程崩溃会对其他进程产生很大影响吗?

    不会。

    • 进程隔离性:每个进程都有自己独立的内存空间,当一个进程崩溃时,其内存空间会被操作系统回收,不会影响其他进程的内存空间。这种进程间的隔离性保证了一个进程崩溃不会直接影响其他进程的执行。
    • 进程独立性:每个进程都是独立运行的,它们之间不会共享资源,如文件、网络连接等。因此,一个进程的崩溃通常不会对其他进程的资源产生影响。

    3.2.2 线程

    线程上下文切换有了解吗?

    • 当两个线程不是属于同⼀个进程,则切换的过程就跟进程上下⽂切换⼀样;
    • 当两个线程是属于同⼀个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据

    所以,线程的上下⽂切换相⽐进程,开销要⼩很多

    线程有哪些实现方式?

    • 用户线程(User Thread):在用户空间实现的线程,是由用户态的线程库来完成线程的管理;
    • 内核线程(Kernel Thread):在内核中实现的线程,是由内核管理的线程;
    • 混合线程实现:现代操作系统基本都是将两种方式结合起来使用。用户态的执行系统负责进程内部线程在非阻塞时的切换;内核态的操作系统负责阻塞线程的切换。即我们同时实现内核态和用户态线程管理。其中内核态线程数量较少,而用户态线程数量较多。每个内核态线程可以服务一个或多个用户态线程。
      ![[Pasted image 20240804174859.png]]

    线程间有哪些通信方式?

    ![[Pasted image 20240804183411.png]]

    1. 共享内存:线程之间通过访问同一块内存区域来交换数据。这种方式简单直接,但需要同步机制来避免竞态条件和数据不一致的问题。
    2. 消息传递:线程之间通过发送和接收消息来通信。这种方式可以避免共享内存中的同步问题,但实现起来可能更复杂。
      +进程的通信方式

    3.2.3 进程同步模型

    什么是同步,互斥?

    同步也称作直接制约关系,是指为完成某种任务而建立的两个或多个进程,因为需要在某些位置需要协调工作次序而等待、传递消息所产生的制约关系。
    互斥也称间接制约关系,当一个进程使用临界资源时,另一个进程必须等待。

    为防止两个进程同时进入临界区,同步机制应该遵循:

    1. 互斥规则:确保当一个进程进入临界区时,其他进程不能同时进入。这可以通过使用互斥锁(Mutex)或信号量(Semaphore)来实现。
    2. 有限等待规则:进程应该在有限的时间内进入临界区,以避免饥饿现象。这意味着没有进程应该无限期地等待进入临界区。
    3. 空闲让进规则:如果临界区空闲,那么应该允许请求进入临界区的进程立即进入,而不是让它等待。
    4. 让权等待规则:如果一个进程不能立即进入临界区,它应该释放处理机,以避免忙等(即进程在等待进入临界区时不断占用CPU)。

    怎么解决进程同步问题?

    软件实现方法:单标志法,双标志先检查法,双标志后检查法,皮特森算法
    硬件实现方法: 中断屏蔽法,硬件指令法

    • 互斥锁
      使⽤加锁操作和解锁操作可以解决并发线程/进程的互斥问题。
      任何想进⼊临界区的线程,必须先执⾏加锁操作。若加锁操作顺利通过,则线程可进⼊临界区;在完成对临界资源的访问后再执⾏解锁操作,以释放该临界资源。
    • 信号量
      信号量是操作系统提供的⼀种协调共享资源访问的⽅法。
      通常信号量表示资源的数量,对应的变量是⼀个整型( sem )变量。
      另外,还有两个原⼦操作的系统调⽤函数来控制信号量的,分别是:
    • P 操作:将 sem 减 1 ,相减后,如果 sem < 0 ,则进程/线程进⼊阻塞等待,否则继续,表明 P操作可能会阻塞;
    • V 操作:将 sem 加 1 ,相加后,如果 sem <= 0 ,唤醒⼀个等待中的进程/线程,表明 V 操作不会阻塞;
      P 操作是⽤在进⼊临界区之前,V 操作是⽤在离开临界区之后,这两个操作是必须成对出现的。

    经典的进程同步问题

    1. 生产者-消费者问题(Producer-Consumer Problem)
      • 描述:这个问题涉及两个进程类型,生产者和消费者,它们共享一个固定大小的缓冲区。生产者的任务是生成数据并将其放入缓冲区,而消费者的任务是从缓冲区取出数据。
      • 关键:确保生产者不会在缓冲区满时放入数据,消费者不会在缓冲区空时取出数据。
    2. 读者-写者问题(Reader-Writer Problem)
      • 描述:这个问题涉及多个读者和写者进程,它们共享一个数据对象。多个读者可以同时读取数据,但写者必须独占访问以更新数据。
      • 关键:确保写者不会与读者或其他写者同时访问数据,同时避免读者饥饿(即写者频繁更新数据,导致读者长时间无法读取)。
    3. 哲学家进餐问题(Dining Philosophers Problem)
      • 描述:这个问题涉及五个哲学家,他们围坐在一张圆桌旁,每个人面前有一根筷子。哲学家们交替进行思考和进餐,进餐时需要同时拿起左右两边的筷子。
      • 关键:避免死锁,即所有哲学家都拿起左边的筷子等待右边的筷子。
    4. 吸烟者问题(Smokers’ Problem)
      • 描述:这个问题涉及一个代理和三个吸烟者,每个吸烟者需要不同的两种原材料来制作香烟。代理负责提供原材料,吸烟者等待所需的原材料。
      • 关键:确保代理不会提供无法被任何吸烟者使用的原材料组合。
    5. 理发师睡觉问题(Sleeping Barber Problem)
      • 描述:这个问题涉及一个理发师、一把理发椅和一些顾客。理发师在没有顾客时睡觉,顾客到达时唤醒理发师,理发师为顾客理发。
      • 关键:确保理发师不会在顾客等待时睡觉,也不会在没有顾客时理发。

    3.2.4 死锁

    死锁怎么产生的?怎么避免?

    死锁:两个或两个以上线程都在等待对方执行完毕释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁

    产生死锁有四个必要条件:

    1. 互斥条件:一个资源只能被一个进程所使用
    2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
    3. 不可抢占条件:进程已获得的资源,在未使用完之前,不能强行剥夺
    4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源的关系

    如何解决死锁?

    主要是针对死锁的四个必要条件来破除。

    1. 破坏互斥条件:一般来说不可破除,可以使用虚拟共享,假脱机SPOLooing等来避免
    2. 破坏请求与保持条件: 允许进程获取初期所有资源后,便开始运行,运行过程中再逐步释放自己占有的资源
    3. 破坏不可抢占条件:系统强制回收,撤销进程,但是代价大,实现复杂;占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
    4. 破坏循环与等待条件:对各进程请求资源的顺序(注意不是执行的顺序)做一个规定,避免相互等待。线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。

    如何避免死锁?

    银行家算法:通过模拟资源分配并确定系统是否会进入安全状态来工作。在分配资源之前,银行家算法会检查这次分配是否可能导致系统进入不安全状态。如果是,则不进行分配,从而避免死锁。

    3.2.5 中断

    中断的作用是什么?

    中断使得计算机系统具备应对对处理突发事件的能力,提高了CPU的工作效率。

    如果没有中断系统,CPU就只能按照原来的程序编写的先后顺序,对各个外设进行查询和处理,即轮询工作方式,轮询方法貌似公平,但实际工作效率却很低,却不能及时响应紧急事件。

    Linux中异常和中断的区别?

    1. 来源: 中断通常是由硬件设备发出的信号,通知操作系统某个事件已经发生,需要处理。例如,I/O操作完成、时钟中断、硬件故障等;异常是由程序执行过程中发生的错误或异常情况引起的,如非法指令、地址越界、除零异常等。
      异常是由CPU产生的,而中断是由硬件设备产生的
    2. 处理方式:中断会打断当前正在执行的进程或线程,强制CPU转去执行中断处理程序;中断处理程序通常较短,目的是快速响应硬件事件。异常通常会导致当前执行的指令或操作终止,并转去执行异常处理程序。异常处理程序负责处理这些错误,可能包括清理资源、恢复状态或终止程序。
    3. 目的: 中断用于处理与外部设备相关的事件,确保系统能够及时响应硬件状态的变化;异常用于处理程序执行过程中的错误,保证程序的稳定性和可靠性。

    讲讲中断的流程

    中断是计算机系统中一种机制,用于在处理器执行指令时暂停当前任务,并转而执行其他任务或处理特定事件。

    1. 中断请求(Interrupt Request, IRQ)
      • 硬件设备或软件通过发送中断请求信号来通知CPU,某个事件需要处理。
    2. 中断识别
      • CPU在执行完当前指令后,检查是否有中断请求。如果有,CPU会暂停当前执行的任务。
    3. 中断服务例程(Interrupt Service Routine, ISR)
      • CPU根据中断号查找中断向量表,找到对应的中断服务例程的地址。
      • CPU执行中断服务例程,处理中断。这可能包括保存当前任务的上下文,如寄存器的值。
    4. 中断处理
      • 中断服务例程执行具体的处理操作,如读取设备数据、更新状态或响应错误。
    5. 中断返回
      • 中断服务例程完成后,恢复之前保存的任务上下文,如寄存器的值。
      • CPU继续执行之前被中断的任务。

    如果有多个中断同时发生,CPU会根据中断优先级决定先处理哪个中断。在某些系统中,中断可以嵌套,即在中断服务例程执行过程中,可以响应更高优先级的中断。

    中断的类型有哪些?

    中断按事件来源分类,可以分为外部中断和内部中断。中断事件来自于CPU外部的被称为外部中断,来自于CPU内部的则为内部中断。

    • 外部中断的中断事件来源于CPU外部,必然是某个硬件产生的,所以外部中断又被称为硬件中断(hardware interrupt)。计算机的外部设备,如网卡、声卡、显卡等都能产生中断。外部设备的中断信号是通过两根信号线通知CPU的,一根是INTR,另一根是NMI。CPU从INTR收到的中断信号都是不影响系统运行的,CPU可以选择屏蔽(通过设置中断屏蔽寄存器中的IF位),而从NMI中收到的中断信号则是影响系统运行的严重错误,不可屏蔽,因为屏蔽的意义不大,系统已经无法运行。
    • 内部中断来自于处理器内部,其中软中断是由软件主动发起的中断,常被用于系统调用(system call);而异常则是指令执行期间CPU内部产生的错误引起的。异常也和不可屏蔽中断一样不受eflags寄存器的IF位影响,区别在于不可屏蔽中断发生的事件会导致处理器无法运行(如断电、电源故障等),而异常则是影响系统正常运行的中断(如除0、越界访问等)。

    3.3 内存管理

    计算机的存储结构

    存储系统层次结构主要体现在”Cache-主存”层次和“主存-辅存”层次。前者主要解决CPU和主存速度不匹配的问题,后者主要解决存储系统的容量问题。在存储体系中, Cache 、主存能与CPU 直接交换信息,辅存则要通过主存与CPU 交换信息;主存与CPU 、Cache 、辅存都能交换信息。

    虚拟内存(逻辑地址–>物理地址)

    为了在多进程环境下,使得进程之间的内存地址不受影响,相互隔离,于是操作系统就为每个进程独立分配一套虚拟地址空间,每个程序只关心自己的虚拟地址就可以,分布到物理地址内存是不一样的。

    每个进程都有自己的虚拟空间,而物理内存只有一个,所以当启用了大量的进程,物理内存必然会很紧张,于是操作系统会通过内存交换技术,把不常使用的内存暂时存放到硬盘(换出),在需要的时候再装载回物理内存(换入)。
    那么对于虚拟地址与物理地址的映射关系,可以有分段分页的方式,同时两者结合都是可以的。
    内存分段是根据程序的逻辑角度,分成了栈段、堆段、数据段、代码段等,这样可以分离出不同属性的段,同时是一块连续的空间。但是每个段的大小都不是统一的,这就会导致外部内存碎片和内存交换效率低的问题。

    于是,就出现了内存分页,把虚拟空间和物理空间分成大小固定的页,如在 Linux 系统中,每一页的大小为 4KB。由于分了页后,就不会产生细小的内存碎片,解决了内存分段的外部内存碎片问题。同时在内存交换的时候,写入硬盘也就一个页或几个页,这就大大提高了内存交换的效率。

    再来,为了解决简单分页产生的页表过大的问题,就有了多级页表,它解决了空间上的问题,但这就会导致 CPU 在寻址的过程中,需要有很多层表参与,加大了时间上的开销。于是根据程序的局部性原理,在 CPU 芯片中加入了 TLB,负责缓存最近常被访问的页表项,大大提高了地址的转换速度。

    Linux 系统主要采用了分页管理,但是由于 Intel 处理器的发展史,Linux 系统无法避免分段管理。于是 Linux 就把所有段的基地址设为 0,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被用于访问控制和内存保护。

    另外,Linux 系统中虚拟空间分布可分为用户态内核态两部分,其中用户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。

    一页为什么是 4 KB?

    在计算机中,一页通常为 4KB 主要有以下几个原因:
    一、历史和兼容性因素
    在早期的计算机体系结构发展过程中,特定的页面大小被选择并逐渐成为一种标准。4KB 的页面大小在很多体系结构中被广泛采用,主要是因为它在历史上被证明是一个较为合理的折中选择。随着时间的推移,软件和硬件系统围绕这个页面大小进行了大量的开发和优化,为了保持兼容性,后续的系统也倾向于继续采用这个页面大小。

    二、性能考虑

    1. 内存访问效率:适中的页面大小可以在内存访问效率和内存管理的复杂性之间取得平衡。如果页面太小,会导致页面切换频繁,增加内存管理的开销,降低系统性能。而如果页面太大,又会浪费内存空间,尤其是当进程只需要访问少量数据时。4KB 的页面大小在大多数情况下能够较好地满足内存访问的需求,减少页面错误的发生,提高内存访问的速度。
    2. 硬件设计:现代计算机的硬件架构通常对特定的页面大小进行了优化。例如,内存控制器和缓存系统可能针对 4KB 的页面进行了设计,以提高数据的传输效率和缓存命中率。这样可以充分利用硬件的特性,提高系统的整体性能。

    三、内存管理的便利性

    1. 页表管理:页面大小的选择会影响页表的结构和大小。4KB 的页面大小使得页表的管理相对简单。页表是用于将虚拟地址转换为物理地址的重要数据结构,适中的页面大小可以减少页表的层级和条目数量,降低页表管理的复杂性和内存占用。
    2. 内存分配和回收:对于操作系统的内存管理模块来说,4KB 的页面大小便于进行内存的分配和回收。可以更灵活地满足不同大小的内存请求,同时减少内存碎片的产生。

    综上所述,4KB 作为一页的大小是在历史发展、性能需求和内存管理便利性等多方面因素的综合考虑下形成的一种标准选择。它在现代计算机系统中被广泛应用,以实现高效的内存管理和良好的系统性能。

    为什么要使用虚拟内存?

    1. 虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
    2. 由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题
    3. 页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。

    段页式内存管理

    内存空间的分配与回收:连续分配(单一连续,固定分区,动态分区);非连续分配(基本分页,基本分段,段页式)

    分段: 程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来。
    分段的办法很好,解决了程序本身不需要关心具体的物理内存地址的问题,但它也有一些不足之处:
    内存碎片的问题。
    内存交换的效率低的问题。

    分页: 分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫(Page)。在 Linux 下,每一页的大小为 4KB
    虚拟地址与物理地址之间通过页表来映射。页表是存储在内存里的,内存管理单元 (MMU)就做将虚拟内存地址转换成物理地址的工作。而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

    段页式内存管理 实现的方式:

    • 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
    • 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;
      这样,地址结构就由段号、段内页号和页内位移三部分组成。
      用于段页式地址变换的数据结构是每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号
      ![[Pasted image 20240722213830.png]]

    虚拟内存的实现方式

    就是在传统的内存分配与回收方式上添加请求调换实现。
    在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。

    虚拟内存是一种计算机系统内存管理技术,使得程序能够使用比物理内存更大的地址空间,并实现内存保护和高效的内存使用。以下是虚拟内存的实现方式的简要概括:

    1. 地址空间划分
      • 虚拟地址空间:每个进程拥有一个独立的虚拟地址空间。虚拟地址空间通常划分为不同的段,如代码段、数据段、堆栈段等。
      • 物理地址空间:实际的物理内存,由RAM提供。
    2. 页表和分页
      • 分页(Paging):虚拟内存和物理内存都被划分成固定大小的块,称为页(page)和页框(page frame)。
      • 页表(Page Table):每个进程维护一个页表,记录虚拟页到物理页框的映射关系。页表存储在内存中。
    3. 地址转换
      • 当CPU访问内存时,会先通过页表将虚拟地址转换为物理地址。
      • 硬件中有一个称为内存管理单元(MMU)的组件负责地址转换过程。
    4. 缺页中断(Page Fault)和换页
      • 当进程访问的虚拟页不在物理内存中时,会触发缺页中断。
      • 操作系统会将所需的页从硬盘加载到物理内存,并更新页表。
      • 如果物理内存已满,操作系统会根据一定的算法(如LRU,最近最少使用)将某些页从物理内存中换出到硬盘。
    5. 内存保护
      • 虚拟内存机制还提供内存保护,防止进程互相干扰。
      • 每个进程的页表使得进程只能访问自己的虚拟地址空间,从而实现内存隔离。
        通过这些机制,虚拟内存可以有效地管理内存资源,提高系统的稳定性和安全性,并使程序能够运行在有限的物理内存上而不会受到内存大小的限制。

    🌟分页与分段的区别

    1、页是信息的物理单位,分页是为实现离散分配方式,以消减内存的外零头,提高内存的利用率;或者说,分页仅仅是由于系统管理的需要,而不是用户的需要。
    段是信息的逻辑单位,它含有一组其意义相对完整的信息。分段的目的是为了能更好的满足用户的需要。
    2、页的大小固定且由系统确定,把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的,因而一个系统只能有一种大小的页面。
    段的长度却不固定,决定于用户所编写的程序,通常由编辑程序在对源程序进行编辑时,根据信息的性质来划分。
    3、分页的作业地址空间是维一的,即单一的线性空间,程序员只须利用一个记忆符,即可表示一地址。
    分段的作业地址空间是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址。

    多级页表与TLB快表

    地址变换结构
    ![[Pasted image 20240722213735.png]]

    内存满了,会发生什么?

    • 当物理内存被完全占用时,新的页面请求会导致频繁的页面置换。
    • 如果页面置换操作不能为新的页腾出空间,系统可能会开始丢弃最不常用的页。
    • 如果丢弃的页是当前正在使用的页,系统会再次触发缺页异常,操作系统需要从硬盘读取这些页回到内存。
    • 频繁的页面置换和硬盘读写操作会导致系统性能显著下降,甚至可能导致系统变得非常缓慢或不可用。
    • 在极端情况下,如果系统无法为关键进程提供足够的内存,可能会导致系统崩溃。

    有哪些页面置换算法?

    分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。

    • 最佳置换算法(OPT)∶在预知一个进程的页面号引用串的情况下,每次都淘汰以后不再使用的或者以后最迟再被使用的页面。该算法不能实现,只能作为一个标准来衡量其他置换算法的优劣。
    • 先进先出算法(FIFO)︰每次总是淘汰最先进入内存的页面,也就是将在内存中驻留时间最长的页面淘汰。(可能会产生 Belady 异常,缺页次数随着分配的物理块的增加而增加)。
    • 最近最少使用算法(LRU)︰选择最近最少未被使用的页面淘汰,其思想是用以前的页面引用情况来预测将来会出现的页面引用情况。利用了局部性原理。
    • 时钟置换算法(CLOCK) ︰是LRU和FIFO的折中,具体方法略。
    • 工作集算法,工作集时钟算法,第二次机会算法
    • 最近未使用(NRU)

    抖动:频繁的页面调度行为

    3.4 文件系统

    硬链接和软链接有什么区别?

    • 硬链接就是在目录下创建一个条目,记录着文件名与 inode 编号,这个 inode 就是源文件的 inode。删除任意一个条目,文件还是存在,只要引用数量不为 0。但是硬链接有限制,它不能跨越文件系统,也不能对目录进行链接。
    • 软链接相当于重新创建⼀个⽂件,这个⽂件有独⽴的 inode,但是这个⽂件的内容是另外⼀个⽂件的路径,所以访问软链接的时候,实际上相当于访问到了另外⼀个⽂件,所以软链接是可以跨⽂件系统的,甚⾄⽬标⽂件被删除了,链接⽂件还是在的,只不过打不开指向的文件了而已。

    简单介绍一下零拷贝

    零拷贝(Zero-Copy)是一种计算机程序设计技术,旨在减少数据在内存之间的拷贝操作,以提高数据传输的效率。在传统的数据传输过程中,数据通常需要在用户空间和内核空间之间多次拷贝,这不仅消耗CPU资源,还增加了延迟。零拷贝技术通过减少这些拷贝操作,直接在内核空间中传输数据,从而提高了效率和性能。

    零拷贝技术实现主要有两种:

    • mmap + write
      mmap() 系统调⽤函数会直接把内核缓冲区⾥的数据「映射」到⽤户空间,这样,操作系统内核与⽤户空间就不需要再进⾏任何的数据拷⻉操作。
      ![[Pasted image 20240804184841.png]]
    • sendfile
      在 Linux 内核版本 2.1 中,提供了⼀个专⻔发送⽂件的系统调⽤函数 sendfile() 。
      ⾸先,它可以替代前⾯的 read() 和 write() 这两个系统调⽤,这样就可以减少⼀次系统调⽤,也就减少了 2 次上下⽂切换的开销。其次,该系统调⽤,可以直接把内核缓冲区⾥的数据拷⻉到 socket 缓冲区⾥,不再拷⻉到⽤户态,这样就只有 2 次上下⽂切换,和 3 次数据拷⻉。
      ![[Pasted image 20240804184908.png]]

    零拷贝的主要优点包括:

    1. 减少CPU使用:由于减少了数据拷贝操作,CPU的负载降低,可以更有效地处理其他任务。
    2. 降低延迟:数据传输的延迟减少,因为数据不需要在用户空间和内核空间之间多次拷贝。
    3. 提高吞吐量:减少了数据拷贝的开销,系统可以处理更多的数据传输,从而提高了整体的吞吐量。

    磁盘调度算法

    磁盘调度算法

    • 先来先服务

    • 最短寻道时间优先(SSF)
      优先选择从当前磁头位置所需寻道时间最短的请求。
      但这个算法可能存在某些请求的饥饿,产生饥饿的原因是磁头在一小块区域来回移动。

    • 扫描算法(电梯算法)
      磁头在一个方向上移动,访问所有未完成的请求,直到磁头到达该方向上的最后的磁道,才调换方向。
      中间部分的磁道会比较占便宜,中间部分相比其他部分响应的频率会比较多,也就是说每个磁道的响应频率存在差异。

    • 循环扫描算法
      只有磁头朝某个特定方向移动时,才处理磁道访问请求,而返回时直接快速移动至最靠边缘的磁道,也就是复位磁头,这个过程是很快的,并且返回中途不处理任何请求,该算法的特点,就是磁道只响应一个方向上的请求。

    • LOOK 与 C-LOOK 算法
      磁头在移动到「最远的请求」位置,然后立即反向移动。

    3.4 (网络)I/O系统

    🌟什么是I/O多路复用?

    • 最基本的 I/O 处理方式就是独占式的I/O。在这种模式下,当一个进程(或线程)执行 I/O 操作(如读取文件或接收网络数据)时,它会一直阻塞直到 I/O 操作完成,这种方式大量浪费系统的资源。
    • 然后是中断式的I/O处理,当一个设备(如磁盘、网卡等)完成 I/O 操作时,它会向 CPU 发送一个中断信号。CPU 收到中断信号后,会暂停当前正在执行的任务,转而执行与该中断相关的中断处理程序,但是频繁的中断处理会带来一定的开销。
    • I/O多路复用(I/O Multiplexing)是一种允许单个线程或进程同时监视多个文件描述符(通常是网络套接字Socket)的可读、可写和异常等事件的技术。这种技术使得应用程序可以在单个线程中处理多个并发I/O操作,而不需要为每个I/O操作创建和管理多个线程或进程,从而提高了效率和资源利用率。

    而I/O多路复用技术又主要分为三种。

    1. select: 将已连接的 Socket 都放到⼀个⽂件描述符集合fd_set,将集合拷⻉到内核⾥,让内核通过遍历的方式来检查是否有⽹络事件产⽣。当检查到有事件产⽣后,将此 Socket 标记为可读或可写, 接着再把整个fd_set拷⻉回⽤户态⾥,然后⽤户态还需要再通过遍历的⽅法找到可读或可写的 Socket,再对其处理。select 使⽤固定⻓度的 BitMap来表示⽂件描述符集合,一般默认是1024,不够灵活。
    2. poll 不再⽤ BitsMap 来存储所关注的⽂件描述符,取⽽代之⽤动态数组,以链表形式来组织,突破了select 的⽂件描述符个数限制,当然还会受到系统⽂件描述符限制。其他方面与select基本类似
    3. epoll对前两种方法进行了改进。
      第⼀点,epoll 在内核⾥使⽤红⿊树来跟踪进程所有待检测的⽂件描述字,红黑树的增删比较高效,且都在内核进行减少了内核和⽤户空间⼤量的数据拷⻉和内存分配。
      第二点,epoll 使⽤事件驱动的机制,内核⾥维护了⼀个链表来记录就绪事件,当某个 socket 有事件发⽣时,通过回调函数,内核会将其加⼊到这个就绪事件列表中,⼤⼤提⾼了检测的效率。

    ![[Pasted image 20240804185748.png]]

    select poll 和 epoll 最大的差别是什么?

    1. select
      • 文件描述符数量限制:select支持的文件描述符数量有限,通常受限于最大打开文件数(ulimit -n)。
      • 每次调用都需要传递文件描述符集合:select每次调用都需要传递所有要监视的文件描述符集合,这可能导致较大的系统调用开销。
      • 轮询机制:select需要轮询所有文件描述符,以确定哪些是就绪的,这可能导致较高的CPU使用率。
    2. poll
      • 文件描述符数量不受限制:poll没有文件描述符数量的限制,可以处理更多的并发连接。
      • 每次调用需要传递文件描述符集合:与select类似,poll每次调用也需要传递所有要监视的文件描述符集合。
      • 轮询机制:poll也使用轮询机制来确定哪些文件描述符是就绪的,但比select提供了更高效的接口。
    3. epoll
      • 文件描述符数量不受限制:epoll没有文件描述符数量的限制,可以处理大量的并发连接。
      • 事件驱动机制:epoll使用事件驱动机制,只有在文件描述符就绪时才会通知应用程序,减少了不必要的轮询,降低了CPU使用率。
      • 内存使用效率:epoll在内部维护一个红黑树来管理文件描述符,这比select和poll的每次调用都传递文件描述符集合要高效。
      • 边缘触发(Edge-Triggered):epoll支持边缘触发模式,这意味着只有在文件描述符的状态发生变化时才会触发事件,这可以提高效率,减少不必要的数据传输。

    阻塞/非阻塞式IO

    • 阻塞I/O
      当⽤户程序执⾏ read ,线程会被阻塞,⼀直等到内核数据准备好,并把数据从内核缓冲区拷⻉到应⽤程序的缓冲区中,当拷⻉过程完成, read 才会返回。
      注意,阻塞等待的是内核数据准备好数据从内核态拷⻉到⽤户态这两个过程
      ![[Pasted image 20240804191230.png]]
    • 非阻塞I/O
      ⾮阻塞的 read 请求在数据未准备好的情况下⽴即返回,可以继续往下执⾏,此时应⽤程序不断轮询内核,直到数据准备好,内核将数据拷⻉到应⽤程序缓冲区, read 调⽤才可以获取到结果。
      ![[Pasted image 20240804191310.png]]
      我们上面的非阻塞I/O有一个问题,什么问题呢?应用程序要一直轮询,这个过程没法干其它事情,所以引入了I/O 多路复⽤技术。
      当内核数据准备好时,以事件通知应⽤程序进⾏操作。

    ⽆论是阻塞 I/O、还是⾮阻塞 I/O、非阻塞I/O多路复用,都是同步调⽤。因为它们在read调⽤时,内核将数据从内核空间拷⻉到应⽤程序空间,过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷⻉效率不⾼,read调⽤就会在这个同步过程中等待⽐较⻓的时间。

    同步/异步 IO

    同步如上。

    真正的异步 I/O 是内核数据准备好数据从内核态拷⻉到⽤户态这两个过程都不⽤等待。
    发起 aio_read 之后,就⽴即返回,内核⾃动将数据从内核空间拷⻉到应⽤程序空间,这个拷⻉过程同样是异步的,内核⾃动完成的,和前⾯的同步操作不⼀样,应⽤程序并不需要主动发起拷⻉动作。
    ![[Pasted image 20240804191515.png]]

    一次普通 IO 的过程,什么时候用到了系统调用?

    一次普通 IO 的过程,大致分为以下几个步骤:

    1. 应用程序调用高级 IO 接口,比如 fopen、read、write 等,将数据读取到缓冲区或者写入到文件中。
    2. 操作系统内核将应用程序提供的参数进行解析,然后将读写请求加入到文件描述符对应的 I/O 队列中。
    3. 内核通过调用底层驱动程序将请求传递给对应的设备驱动程序。
    4. 驱动程序根据硬件特性,调用硬件进行读写操作,并将结果写回内核缓冲区
    5. 内核将数据从内核缓冲区拷贝到用户缓冲区,并返回操作结果给应用程序。

    在这个过程中,操作系统内核在第2、4、5步中使用了系统调用:

    • 第2步中,将请求加入到 I/O 队列时需要使用系统调用,如 Linux 中的 epoll_wait、select、poll 等等。
    • 第4步中,驱动程序调用硬件进行读写操作时需要使用系统调用,如 Linux 中的 read、write 等等。
    • 第5步中,内核将数据从内核缓冲区拷贝到用户缓冲区时需要使用系统调用,如 Linux 中的 read、write 等等。
      因此,系统调用在普通 IO 过程中的作用非常重要,负责将应用程序与硬件设备进行连接,并提供底层的 I/O 功能。
      ![[Pasted image 20240804191117.png]]

    3.5 Linux

    🌟如何在 Linux 中查看系统资源使⽤情况?⽐如内存、CPU、⽹络端⼝

    在Linux系统中,你可以使用多种命令来查看系统资源的使用情况,包括内存、CPU和网络端口。以下是一些常用的命令及其用法:

    查看内存使用情况

    1. free - 显示内存的使用情况
      1
      2
      3
      free -m  # 显示内存使用情况,单位为MB
      free -g # 显示内存使用情况,单位为GB
      free -h # 显示内存使用情况,单位为合适的单位(例如GB、MB)
    2. top - 实时显示系统进程和资源使用情况
      1
      top
    3. htop(如果安装了)- 类似于top,但提供了更丰富的界面和功能
      1
      htop

    查看CPU使用情况

    1. top - 实时显示系统进程和资源使用情况,包括CPU使用率
      1
      top
    2. htop(如果安装了)- 提供了更详细的CPU使用情况
      1
      htop
    3. mpstat - 报告CPU相关统计信息
      1
      mpstat -P ALL 1  # 每秒更新一次所有CPU的使用情况

    查看网络端口使用情况

    1. netstat - 显示网络连接、路由表、接口统计信息等
      1
      2
      netstat -anp  # 显示所有端口和对应的进程
      netstat -tuln # 显示所有监听端口
    2. ss - 类似于netstat,但提供了更多的信息
      1
      2
      ss -anp     # 显示所有端口和对应的进程
      ss -tuln # 显示所有监听端口
    3. lsof - 列出打开的文件,可以用来查看端口使用情况
      1
      2
      lsof -iTCP -sTCP:LISTEN  # 列出所有TCP监听端口
      lsof -i :80 # 列出使用80端口的进程
      这些命令提供了Linux系统资源使用情况的不同视角。你可以根据需要选择合适的命令来获取所需信息。记得,某些命令(如htop)可能需要额外安装。

    在Linux系统中,load average(负载平均值)是指系统在特定时间间隔内的平均负载。它是一个衡量系统繁忙程度的指标,通常由三个值组成,分别表示过去1分钟、5分钟和15分钟的平均负载。这些值可以帮助系统管理员了解系统的实时负载情况,以及系统是否能够处理当前的工作负载。

    负载平均值是根据运行队列(run queue)的长度来计算的,它反映了

    如何查看CPU的硬件信息

    1. cat /proc/cpuinfo
    2. lscpu
    3. hostnamectl

    系统相关(运行级别,关机,重启)

    在Linux系统中,运行级别(Run Level)是一个数字,它定义了系统启动时的服务状态。不同的运行级别对应不同的系统服务状态,包括单用户模式、多用户模式、网络服务模式等。

    Linux运行级别

    • 0:系统停机状态,系统关闭
    • 1:单用户模式,用于系统维护
    • 2:多用户模式,没有网络服务
    • 3:完全的多用户模式,有网络服务
    • 4:未使用,保留给用户或系统管理员
    • 5:图形用户界面(GUI)的多用户模式
    • 6:系统重新启动
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    runlevel   # 查看当前运行级别
    whoami # 查看登录用户

    init 3 # 切换到运行级别3
    init 5 # 切换到运行级别5(图形界面)
    init 6 # 重新启动系统
    init 0 # 关闭系统

    systemctl set-default multi-user.target # 设置默认运行级别为多用户模式
    systemctl isolate runlevel3.target # 切换到运行级别3
    systemctl reboot # 重新启动系统
    systemctl poweroff # 关闭系统

    查看/关闭进程

    查看进程 ps
    根据进程号关闭 kill PID (kill -9 PID 强制关闭)
    根据进程名称 pkill process_name

    网络相关

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    # 查看网络接口配置信息
    ifconfig

    # 查看所有网络接口的地址
    ip addr show

    # 查看所有路由表项
    ip route show

    # 显示网络连接、路由表、接口统计信息等
    netstat -anp

    # 显示所有端口和对应的进程
    netstat -tuln

    # 列出所有TCP监听端口
    lsof -iTCP -sTCP:LISTEN

    # 捕获网络数据包
    tcpdump -i eth0 host 192.168.1.100

    # 捕获通过eth0接口的TCP端口80的数据包
    tcpdump -i eth0 tcp port 80

    # 捕获通过eth0接口的所有数据包,并保存到dump.pcap文件中
    tcpdump -i eth0 -s 0 -w dump.pcap

    # 实时捕获通过eth0接口的所有数据包,并实时显示协议信息
    tcpdump -i eth0 -s 0 -w dump.pcap -A

    # 测试网络连接的可达性
    ping 192.168.1.100

    # 追踪数据包从源到目的地的路径
    traceroute 192.168.1.100

    创建、复制、移动和删除文件或目录?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    touch filename.txt  #创建文件
    vim filename (vi/nano)
    mkdir directory_name # 创建目录
    mkdir -p parent/child
    cp source.txt /path/to/destination/ #复制文件
    cp -r source_directory /path/to/destination/ # 复制牡蛎
    mv source.txt /path/to/destination/ #移动文件
    mv oldname.txt newname.txt #重命名
    mv source_directory /path/to/destination/ #移动目录
    rm filename.txt #删除文件
    rm -rf directory_name #删除目录 (-r 递归 -f 强制)

    权限系统

    Linux权限系统基于三种基本类型的权限:读(read,r)、写(write,w)和执行(execute,x)。这些权限可以针对三种不同的用户类别设置:文件所有者(owner)、文件所属组(group)和其他用户(others)。
    权限可以通过ls -l命令查看

    1
    2
    ls -l
    -rwxr-xr-- 1 user group 4096 Jan 1 12:00 filename

    在这个例子中,-rwxr-xr--表示:

    • 文件所有者(user)有读、写和执行权限(rwx)。
    • 文件所属组(group)有读和执行权限(r-x)。
    • 其他用户有读权限(r--)。
      修改权限
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      # 为文件所有者添加执行权限
      chmod u+x filename

      # 为文件所属组移除写权限
      chmod g-w filename

      # 为其他用户添加读权限
      chmod o+r filename

      # 设置所有用户权限为读、写和执行
      chmod a+rwx filename

      # 使用数字表示法设置权限
      # 数字表示法:r=4, w=2, x=1
      # 因此,7 = 4 + 2 + 1 (rwx),6 = 4 + 2 (rw-),4 = 4 (r--)
      chmod 764 filename

      # 设置目录权限,使得所有者可以读写执行,组和其他用户只能读和执行
      chmod 750 directory_name

    find 查找

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    # 查找当前目录及其子目录下所有名为"example.txt"的普通文件
    find . -type f -name "example.txt"

    # 查找当前目录及其子目录下所有以".txt"结尾的文件,并且是符号链接
    find . -type l -name "*.txt"

    # 查找当前目录及其子目录下大于100M的文件,并且文件所有者有读写权限
    find . -type f -size +100M -perm /u=rw

    # 查找当前目录及其子目录下小于10K的文件,并且组用户有执行权限
    find . -type f -size -10k -perm /g=x

    # 查找当前目录及其子目录下,在最近7天内被修改过的文件,并且文件所有者是"username"
    find . -type f -mtime -7 -user username

    # 查找当前目录及其子目录下,在过去24小时内被访问过的文件,并且文件所属组是"groupname"
    find . -type f -atime -1 -group groupname

    # 查找当前目录及其子目录下所有大于10M且小于100M的文件
    find . -type f -size +10M -size -100M

    # 查找当前目录及其子目录下,文件名为"example"且类型为目录的项
    find . -type d -name "example"

    # 查找当前目录及其子目录下,文件名包含"pattern",并且不是目录的项
    find . ! -type d -name "*pattern*"

    # 删除当前目录及其子目录下所有名为"core"的文件
    find . -type f -name "core" -exec rm -f {} \;

    # 查找当前目录及其子目录下所有名为"*.log"的文件,并显示它们的详细信息
    find . -type f -name "*.log" -exec ls -l {} \;

    # 安全地删除当前目录及其子目录下所有大于100M的文件
    find . -type f -size +100M -ok rm {} \;

    swap

    不足时,系统可以将不常用的数据从RAM移动到swap空间,从而为更活跃的程序腾出空间。这个过程称为页面交换(paging)。

    swap的作用包括:

    1. 内存扩展:允许系统使用比物理RAM更多的内存。
    2. 系统稳定性:防止内存不足导致的系统崩溃。
    1
    2
    3
    4
    5
    6
    7
    free -m # 以MB为单位显示内存和swap信息
    swapon # 显示交换分区的详细信息
    cat /proc/swaps

    swapon /path/to/swapfile # 启用swap分区
    swapoff /path/to/swapfile # 禁用swap分区
    path/to/swapfile none swap sw 0 0 # 永久挂载swap文件

    文件查看

    在Linux中,要查看文件的某一列,可以使用awkcutsed等工具。以下是使用这些工具的示例:

    1
    2
    3
    awk -F, '{print $2}' file.txt
    cut -d, -f2 file.txt
    sed -n 's/[^,]*,[^,]*,\([^,]*\)/\1/p' file.txt
    1
    2
    3
    4
    5
    6
    head -n 10 filename.txt  # 显示文件的前10行
    tail -n 10 filename.txt # 显示文件的最后10行
    cut -d ' ' -f 1 filename.txt # 以空格为分隔符,截取第一列
    awk '{print \$1}' filename.txt # 打印每行的第一列
    grep 'pattern' filename.txt # 显示包含'pattern'的行
    split -l 100 filename.txt # 每100行分割成一个新文件

    文件系统挂载

    在Linux中,文件系统挂载点是指文件系统中的一个目录,它作为其他存储设备(如硬盘分区、光盘、USB驱动器等)的接入点。挂载文件系统意味着将其接入到当前文件系统树中,使得可以像访问本地文件一样访问挂载的文件系统。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 挂载一个分区到指定的挂载点
    mount /dev/sdb1 /mnt/mydrive

    # 挂载一个ISO文件作为loop设备
    mount -o loop image.iso /mnt/isoimage

    # 挂载一个远程NFS共享
    mount -t nfs 192.168.1.100:/path/to/shared /mnt/nfs

    # 卸载挂载的文件系统
    umount /mnt/mydrive

    # 卸载ISO文件挂载的loop设备
    umount /mnt/isoimage

    # 卸载NFS共享挂载点
    umount /mnt/nfs

    用户组

    在Linux操作系统中,用户和用户组是权限管理和安全策略的重要组成部分。

    用户是Linux系统中的基本身份认证单位,每个用户都有一个唯一的用户名和用户ID(UID)。用户可以登录系统,运行程序,管理文件等。系统中的每个文件和目录都属于一个特定的用户。

    用户组(Group)
    用户组是一组用户的集合,每个用户组有一个唯一的组名和组ID(GID)。用户组使得权限可以被分配给一组用户,而不是单个用户。这简化了权限管理,特别是当需要为多个用户分配相同的权限时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 创建新用户组
    sudo groupadd newgroup
    # 创建新用户,并指定其主组(默认组)
    sudo useradd -m -g newgroup newuser

    # 为新用户设置密码
    sudo passwd newuser
    # 将用户添加到额外的用户组(如果需要)
    sudo usermod -aG additionalgroup newuser

    Rsync

    Linux中的rsync是一个开源的文件同步工具,它可以用来同步文件和目录,包括文件的元数据(如权限、修改时间等)。rsync使用远程文件系统协议(如SSH、SFTP等)来传输数据,因此它通常比FTP更快速、更安全。

    1
    2
    3
    4
    5
    6
    7
    rsync -av source_file destination_file  # 同步本地文件
    rsync -avz source_file remote_user@remote_host:destination_file # 同步远程文件


    rsync -avz --exclude '*.log' source_directory destination_directory # 忽略某些文件

    rsync -avz --rsh 'ssh -p 2222' source_directory destination_directory # 使用特定协议传输

    日志

    是的,Linux系统中有多种日志文件,它们记录了系统的各种活动和事件。这些日志文件对于故障排除、安全审计和性能监控都非常重要。以下是一些主要的Linux日志文件类型:

    1. /var/log/messages -
      • 这个文件记录了系统的系统消息、错误信息、警告信息和一般信息。
      • 它是多个日志文件的汇总,包括/var/log/dmesg(内核启动信息)、/var/log/syslog(系统日志)和/var/log/daemon.log(守护进程日志)。
    2. /var/log/syslog -
      • 包含来自各种系统服务的日志信息。
      • 它记录了系统的系统消息、错误信息和警告信息。
    3. /var/log/kern.log -
      • 专门记录内核相关的日志信息。
      • 它包含了内核模块加载、内核错误和内核调试信息。
    4. /var/log/auth.log -
      • 记录了与认证相关的日志信息。
      • 它包含了用户登录、认证尝试、授权请求等信息。
    5. /var/log/secure -
      • 记录了与安全相关的日志信息。
      • 它包含了SSH登录、SSH错误、SSH会话、SELinux事件等信息。
    6. /var/log/warn -
      • 记录了警告级别的日志信息。
      • 它包含了系统发出的警告级别的消息。

    查询修改时间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 查询系统时间
    date
    date +%Y-%m-%d\ %H:%M:%S
    hwclock # 硬件时钟时间
    timedatectl # 时区,NTP服务器等

    # 修改系统时间
    date -s "2023-09-08 12:00:00" # 设置日期和时间
    hwclock --set --date "2023-09-08 12:00:00"
    timedatectl set-time "2023-09-08 12:00:00"
    ntpdate -u pool.ntp.org

    管道符和重定向

    管道符|将一个命令的输出(标准输出)作为另一个命令的输入(标准输入)。这允许你将多个命令组合在一起,从而实现复杂的文本处理和数据转换。
    重定向允许你修改命令的标准输入(<)或标准输出(>)或错误输出(2>)。 重定向就可以把一些程序的输出写入文件或者记录错误日志等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 列出当前目录下的所有文件和目录
    ls -l | grep "^-"

    # 将一个文本文件的内容输出到另一个文本文件
    cat original.txt | tr 'a-z' 'A-Z' > uppercase.txt

    # 将命令的输出发送到文件,而不是标准输出
    ls -l | grep "^-" > filelist.txt


    Linux内核版本与发行版本的区别是什么?

    Linux 内核版本

    • 定义:内核是操作系统的核心部分,负责管理系统的硬件资源、进程调度、内存管理、设备驱动等最基础和关键的功能。
    • 版本特点
      • 由 Linux 内核社区进行开发和维护,其版本号通常采用A.B.C的格式。其中A表示主版本号,B表示次版本号,C表示修订版本号。
      • 当次版本号为偶数时,通常表示这是一个稳定版本(如2.6.x4.4.x等);当次版本号为奇数时,一般表示这是一个开发版本,可能存在一些不稳定的因素,但也包含了最新的特性。
    • 更新频率:内核版本的更新相对频繁,尤其是开发版本,会不断加入新的功能、修复已知的漏洞和优化性能相关的代码。

    Linux 发行版本

    • 定义:基于 Linux 内核,再加上一系列的系统软件(如 shell、文件系统、桌面环境、办公软件、网络工具等)、应用软件、初始化和配置脚本、安装程序等,共同组成了一个完整的、可以直接安装和使用的操作系统。
    • 版本特点
      • 常见的发行版有 Ubuntu、Debian、Red Hat Enterprise Linux(RHEL)、CentOS、Fedora 等。每个发行版都有自己独特的定位、特色和用户群体。
      • 发行版通常会对内核进行定制化的配置和优化,以满足不同场景的需求。例如,面向服务器领域的发行版会更注重稳定性和安全性,而面向桌面领域的发行版则会更注重用户体验和易用性。
    • 更新频率:发行版的更新周期各不相同。有些发行版(如 Arch Linux)追求滚动更新,会频繁地更新系统中的软件包,包括内核;而有些发行版(如 RHEL 和 CentOS)则更注重稳定性,更新周期相对较长。

    Linux中的硬链接与软链接

    top指令的底层原理是什么?

    CVM、ECS、轻量级应用服务器等虚拟机、Docker的区别

    https://pdai.tech/md/devops/docker/docker-01-docker-vm.html
    虚拟机Virtual Machine与容器化技术(代表Docker)都是虚拟化技术,两者的区别在于虚拟化的程度不同。

    Docker是直接运行在宿主操作系统之上的一个容器,使用沙箱机制完全虚拟出一个完整的操作,容器之间不会有任何接口,从而让容器与宿主机之间、容器与容器之间隔离的更加彻底。每个容器会有自己的权限管理,独立的网络与存储栈,及自己的资源管理能,使同一台宿主机上可以友好的共存多个容器。

    一个比较形象的比喻:

    • 服务器:比作一个大型的仓管基地,包含场地与零散的货物——相当于各种服务器资源。

    • 虚拟机技术:比作仓库,拥有独立的空间堆放各种货物或集装箱,仓库之间完全独立——仓库相当于各种系统,独立的应用系统和操作系统。

    • Docker:比作集装箱,操作各种货物的打包——将各种应用程序和他们所依赖的运行环境打包成标准的容器,容器之间隔离。

    • 虚拟机 vs 容器:虚拟机模拟整个操作系统,资源开销大,而容器共享主机操作系统,启动快且资源占用少。

    • ECS vs 轻量级应用服务器:ECS是更通用的弹性计算服务,适用场景广泛;轻量级应用服务器更适合中小型业务,配置和管理更简单。

    • 虚拟机/ECS vs Docker:虚拟机和ECS适合需要完整操作系统环境的应用,而Docker适合快速启动、轻量级的微服务和开发测试环境。

    不同系统下可以用同样的Docker镜像吗?

    • Docker引擎在不同操作系统上提供了相同的容器运行环境,屏蔽了底层操作系统的差异。Docker引擎负责将镜像的内容解包并运行在容器中,使得镜像的运行与主机操作系统无关。
    • 基础镜像的选择:大多数Docker镜像是基于Linux的,但也有适用于Windows的基础镜像。如果需要跨操作系统运行(如从Windows到Linux或反之),需要选择适合目标操作系统的基础镜像。例如,Linux上的镜像通常基于alpineubuntu等,而Windows上的镜像基于microsoft/nanoserver等。

    !!!
    🌟虽然Docker镜像可以跨平台使用,但镜像内部的应用程序需要与目标操作系统兼容。例如,Linux应用程序无法直接在Windows容器中运行,反之亦然。
    跨CPU架构的Docker镜像使用是有一些限制的。具体来说,x86和ARM架构的Linux系统不能直接使用同样的Docker镜像,但可以通过多架构镜像来解决这个问题。

    Linux如何查看哪些端口建立tcp连接?这些端口都有哪些状态?

    操作系统的临界区

    3.6 组原拾遗

    大端和小端有什么区别?

    大端模式,是指数据的低位保存在内存的高地址中,而数据的高位保存在内存的低地址中;
    小端模式,是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中
    大端模式符合人类的阅读习惯,小端模式方便计算机进行处理。

    • 判断方法:
    1. 将int 48存起来,然后取得其地址,再将这个地址转为char* 这时候,如果是小端存储,那么char*指针就指向48;
    2. 定义变量int i=1;将 i 的地址拿到,强转成char*型,这时候就取到了 i 的低地址,这时候如果是1就是小端存储,如果是0就是大端存储。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      void judge_bigend_littleend1()
      {
      int i = 48;
      int* p = &i;
      char c = 0;
      c = *((char*)p);

      if (c == '0')
      printf("小端\n");
      else
      printf("大端\n");
      }
    • 边界对齐: 假设存储字长为32位,可以按照字节、半字、字寻址。在对准边界的32位字长的计算机中,半字地址是2的整数倍,字地址是4的整数倍,当所存数据不能满足此要求时可以填充空白字节。这样保证对齐以后,可以使得每一次取数据都是一次访存取出的。

    计算机的特点

    1. 计算机硬件系统由运算器、存储器、控制器、输入设备和输出设备5 大部件组成。
    2. 指令和数据以同等地位存储在存储器中,并可按地址寻访。
    3. 指令和数据均用二进制代码表示。
    4. 指令由操作码和地址码组成,操作码用来表示操作的性质,地址码用来表示操作数在存储器中的位置。
    5. 指令在存储器内按顺序存放。通常,指令是顺序执行的,在特定条件下可根据运算结果或根据设定的条件改变执行顺序。
    6. 早期的冯诺依曼机以运算器为中心,输入/输出设备通过运算器与存储器传送数据。现代计算机以存储器为中心。

    3.7 数据结构常考

    ![[Pasted image 20240722192520.png]]

    KMP算法

    中缀表达式如何转为后缀表达式

    初始化两个栈:运算符栈S1;操作数栈S2
    从左向右扫描中缀表达式
    遇到操作数时,将其压入到操作数栈S2
    遇到运算符时,比较其与运算符栈S1栈顶运算符的优先级
    如果运算符栈S1为空,或栈顶运算符为左括号“ ( ”,或者优先级比栈顶运算符的优先级较高,则直接将此运算符压入栈中
    否则,将运算符栈S1中栈顶的运算符弹入并压到操作数栈S2中,再次进行与运算符栈S1栈顶运算符的优先级比较
    遇到括号时,如果遇到了左括号“ ( ”,则直接压入运算符栈S1;
    如果遇到右括号“ ) ”,则依次弹出运算符栈S1栈顶的运算符,并压入操作数栈S2,直到遇到左括号” ( “为止,此时将这一对括号丢弃
    重复步骤2至8,直到表达式的最右边
    将运算符栈S1剩余的运算符依次弹出并压入操作数栈S2
    拼接操作数栈S2中的元素并输出,结果即为中缀表达式所对应的后缀表达式
    ![[Pasted image 20241005192450.png]]

    二叉树的常用结论

    ![[Pasted image 20240905112759.png]]

    N 叉树的常用结论

    ![[Pasted image 20240905112823.png]]

    平衡二叉树

    最小二叉平衡树的节点的公式如下 F(n)=F(n-1)+F(n-2)+1

    或者说深度为n的平衡二叉树,至少有F(n)个结点。
    Fibonacci(斐波那契)数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。注意:F(0)=0,F(1)=1。

    得知平衡二叉树的最少的结点的个数为15。而对于二叉树的最多的结点的个数是i2^h-1=31. 

    因而用排除法,得知是20
    ![[Pasted image 20240905113327.png]]

    哈夫曼树

    ![[Pasted image 20240905112926.png]]

    排序算法比较

    ![[Pasted image 20240911085321.png]]

    • 归位性: 每一趟都能能确定元素,冒泡排序,简单选择排序,堆排序,快速排序

    • 稳定性: 相同值的元素在排序前后的相对位置是否发生了变化

    • 冒泡排序:两个数比较大小,较大的数下沉,较小的数冒起来。

    • 选择排序:在长度为N的无序数组中,第一次遍历n-1个数,找到最小的数值与第一个元素交换。

    • 插入排序:在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。

    • 希尔排序:在要排序的一组数中,根据某一增量分为若干子序列,并对子序列分别进行插入排序。

    • 归并排序: 归并排序的基本思想是分治策略,即递归地将数组分成两半,分别对它们进行排序,然后将排序好的两半合并成一个有序数组。

    • 基数排序:首先创建bitmap;然后将每个数放到相应的位置上(例如17放在下标17的数组位置);最后遍历数组,即为排序后的结果。

    • 桶排序:桶排序的基本思想是将数据分布到有限数量的桶里,每个桶内的数据再独立排序,最后将各个桶的数据顺序合并成最终有序序列

    快速排序

    • 基本思想:(分治)

    • 先从数列中取出一个数作为key值;

    • 将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;

    • 对左右两个小数列重复第二步,直至各区间只有1个数。

    • 时间复杂度(最好n x logn,最差,平均n x logn)

    • 空间复杂度(n x logn)

    • 时间复杂度与两侧平衡差值有关,与处理顺序无关。(递归深度)

    • 不稳定算法,适用于顺序表

    • 特征:每完成一轮,都会有至少一个元素出现在正确的位置上。上一个枢轴元素不在两端的话,则下一趟至少有两个出现在正确位置。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      def quick_sort(arr):
      # 递归终止条件:如果数组长度小于等于1,直接返回
      if len(arr) <= 1:
      return arr

      # 选择基准元素,这里选择数组的第一个元素
      pivot = arr[0]

      # 分区操作:将数组分为两部分
      left = [x for x in arr[1:] if x <= pivot] # 小于等于基准元素的部分
      right = [x for x in arr[1:] if x > pivot] # 大于基准元素的部分

      # 递归地对左右两部分进行快速排序,并将结果合并
      return quick_sort(left) + [pivot] + quick_sort(right)

      # 示例
      arr = [5, 3, 8, 4, 2]
      sorted_arr = quick_sort(arr)
      print(sorted_arr) # 输出: [2, 3, 4, 5, 8]
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    //严蔚敏《数据结构》标准分割函数
    def partition1(A, low, high):
    # 选择第一个元素作为基准元素
    pivot = A[low]

    # 使用两个指针,low 和 high,分别从数组的两端向中间移动
    while low < high:
    # 从右向左移动 high 指针,直到找到一个小于基准元素的元素
    while low < high and A[high] >= pivot:
    high -= 1
    # 将找到的小于基准元素的元素放到 low 的位置
    A[low] = A[high]

    # 从左向右移动 low 指针,直到找到一个大于基准元素的元素
    while low < high and A[low] <= pivot:
    low += 1
    # 将找到的大于基准元素的元素放到 high 的位置
    A[high] = A[low]

    # 当 low 和 high 指针相遇时,将基准元素放到相遇的位置
    A[low] = pivot

    # 返回基准元素的最终位置
    return low

    def quick_sort(A, low, high):
    if low < high:
    pivot = partition1(A, low, high)
    quick_sort(A, low, pivot - 1)
    quick_sort(A, pivot + 1, high)

    # 示例
    A = [5, 3, 8, 4, 2]
    quick_sort(A, 0, len(A) - 1)
    print(A) # 输出: [2, 3, 4, 5, 8]

    每一轮排序算法的特征

    1. 冒泡排序(Bubble Sort)
    • 特征:每一轮将当前未排序部分的最大(或最小)元素“冒泡”到正确的位置。
    • 过程:每一轮通过相邻元素的比较和交换,将最大(或最小)元素逐步移动到未排序部分的末尾。
    • 示例:假设数组为 [5, 3, 8, 4, 2],第一轮结束后数组变为 [3, 5, 4, 2, 8]
    1. 选择排序(Selection Sort)
    • 特征:每一轮从未排序部分选择最小(或最大)的元素,并将其放到已排序部分的末尾。
    • 过程:每一轮找到未排序部分的最小(或最大)元素,然后将其与未排序部分的第一个元素交换位置。
    • 示例:假设数组为 [5, 3, 8, 4, 2],第一轮结束后数组变为 [2, 3, 8, 4, 5]
    1. 插入排序(Insertion Sort)
    • 特征:每一轮将未排序部分的第一个元素插入到已排序部分的正确位置。
    • 过程:每一轮从未排序部分取出一个元素,将其插入到已排序部分的适当位置,使得已排序部分仍然有序。
    • 示例:假设数组为 [5, 3, 8, 4, 2],第一轮结束后数组变为 [3, 5, 8, 4, 2]
    1. 快速排序(Quick Sort)
    • 特征:每一轮选择一个基准元素,将数组分为两部分,一部分小于基准元素,另一部分大于基准元素。
    • 🌟每完成一轮,都会有至少一个元素出现在正确的位置上。上一个枢轴元素不在两端的话,则下一趟至少有两个出现在正确位置。
    • 过程:每一轮通过分区操作(partition)将数组分为两部分,然后对这两部分递归地进行快速排序。
    • 示例:假设数组为 [5, 3, 8, 4, 2],第一轮结束后数组可能变为 [3, 2, 4, 5, 8](假设基准元素为5)。
    1. 归并排序(Merge Sort)
    • 特征:每一轮将数组递归地分成两部分,直到每个部分只有一个元素,然后逐步合并成有序的数组。
    • 过程:每一轮将数组分成两部分,分别进行归并排序,然后将两个有序的部分合并成一个有序的数组。
    • 示例:假设数组为 [5, 3, 8, 4, 2],第一轮结束后数组可能变为 [3, 5, 4, 8, 2](假设先分成 [5, 3] 和 [8, 4, 2])。
    1. 堆排序(Heap Sort)
    • 特征:每一轮将堆顶元素(最大或最小)与堆的最后一个元素交换,然后调整堆使其保持堆的性质。
    • 过程:每一轮将堆顶元素与堆的最后一个元素交换,然后将堆的大小减一,并调整堆使其保持堆的性质。
    • 示例:假设数组为 [5, 3, 8, 4, 2],第一轮结束后数组可能变为 [4, 3, 5, 2, 8](假设最大堆)。
    1. 希尔排序(Shell Sort)
    • 特征:每一轮使用不同的间隔(gap)对数组进行插入排序,逐步缩小间隔直到为1。
    • 过程:每一轮使用一个间隔(gap)对数组进行插入排序,然后缩小间隔,直到间隔为1时进行最后一次插入排序。
    • 示例:假设数组为 [5, 3, 8, 4, 2],第一轮结束后数组可能变为 [5, 2, 8, 4, 3](假设初始间隔为2)。

    总结

    • 冒泡排序:每一轮将最大(或最小)元素“冒泡”到末尾。
    • 选择排序:每一轮选择最小(或最大)元素放到已排序部分的末尾。
    • 插入排序:每一轮将未排序部分的第一个元素插入到已排序部分的正确位置。
    • 快速排序:每一轮通过分区操作将数组分为两部分。
    • 归并排序:每一轮将数组分成两部分,然后合并成有序数组。
    • 堆排序:每一轮将堆顶元素与最后一个元素交换,并调整堆。
    • 希尔排序:每一轮使用不同的间隔进行插入排序,逐步缩小间隔。

    堆排序

    https://www.cnblogs.com/chengxiao/p/6129630.html
    堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

    堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了

    1. 步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
    此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。
    ![[Pasted image 20240911090741.png]]
    2. 步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
    ![[Pasted image 20240911090847.png]]

    a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
    b.将堆顶元素与末尾元素交换,将最大元素”沉”到数组末端;
    c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

    普利姆 克鲁斯卡尔 和迪杰斯特拉算法分别是什么?

    1. Prim 算法用于在加权无向图中找到最小生成树(MST, Minimum Spanning Tree),即连接所有顶点的边的权重之和最小的树。
      Prim 算法的核心思想是从一个起始顶点开始,逐步扩展树的节点集合,每一步都选择从树的节点集合到剩余节点集合的最小权重的边,直到所有节点都包含在树中。
      适用于稠密图,使用优先队列实现时复杂度为 ·O(Elog⁡V)O(E \log V)O(ElogV)。
      顶点开始加最小边

    2. Kruskal 算法也是用于寻找图的最小生成树,但它的工作方式不同于 Prim 算法。Kruskal 算法从边的角度出发,而不是从节点出发。Kruskal 算法的核心思想是从所有的边中选取最小权重的边,并保证不会形成环,直到所有节点都连通为止。
      小边开始,不成环

    3. Dijkstra 算法用于求解加权图中单源最短路径问题,即从起始节点到所有其他节点的最短路径。Dijkstra 算法的核心思想是使用贪心策略,从起始节点开始,每次选择当前已知的最短路径节点,然后更新该节点的邻接节点的最短路径值,直到所有节点的最短路径值都确定。
      一个点到其他点的最短路径

    4 关系型数据库 (MySQL)

    ![[Pasted image 20240722192541.png]]

    4.1 SQL 基础

    什么是内连接、外连接、交叉连接、笛卡尔积呢?

    • 内连接是最常见的连接类型,它返回两个表中匹配的行。如果表中有至少一个匹配,则返回行。内连接只返回两个表中有匹配的记录的结果,如果某个记录在另一个表中没有匹配的记录,则不会出现在结果集中。
    • 不只取得两张表中满足存在连接匹配关系的记录,还包括某张表(或两张表)中不满足匹配关系的记录。具体包括左外连接,又外连接和全外连接。
    • 交叉连接返回两个表的笛卡尔积。如果左表有L行,右表有R行,则结果集将包含L x R行。实际上,交叉连接就是没有连接条件的内连接,它没有过滤,会将左表的每一行与右表的每一行组合。
    • 笛卡尔积是两个集合的乘积,在数据库中,它是指两个表所有可能的组合。交叉连接的结果就是一个笛卡尔积。
      ![[Pasted image 20240903215314.png]]

    说一下数据库的三大范式?

    三大范式的作用是为了控制数据库的冗余,是对空间的节省,实际上,一般的设计都是反范式的,通过冗余一些数据,避免跨表跨库,利用空间换时间,提高性能。

    数据库范式有 3 种:

    • 1NF(第一范式):属性不可再分。每一列都不可再分

    • 2NF(第二范式):1NF 的基础之上,消除了非主属性对于码的部分函数依赖。每一行都是唯一,依赖主键

    • 3NF(第三范式):3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。 每一列都不能依赖其他非主键列

    • 第一范式
      属性(对应于表中的字段)不能再被分割,也就是这个字段只能是一个值,不能再分为多个其他的字段了。1NF 是所有关系型数据库的最基本要求 ,也就是说关系型数据库中创建的表一定满足第一范式。

    • 第二范式
      2NF 在 1NF 的基础之上,消除了非主属性对于码的部分函数依赖。如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。
      第一范式到第二范式的过渡。第二范式在第一范式的基础上增加了一个列,这个列称为主键,非主属性都依赖于主键。

    • 第三范式
      3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。符合 3NF 要求的数据库设计,基本上解决了数据冗余过大,插入异常,修改异常,删除异常的问题。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖,所以该表的设计,不符合 3NF 的要求。

    • BC范式
      定义:表满足3NF,并且每一个非平凡的函数依赖项的超键是候选键。
      目的:解决3NF可能仍然存在的某些异常情况,进一步增强数据完整性。

    • 函数依赖

    • 函数依赖(functional dependency):若在一张表中,在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。

    • 部分函数依赖(partial functional dependency):如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。比如学生基本信息表 R 中(学号,身份证号,姓名)当然学号属性取值是唯一的,在 R 关系中,(学号,身份证号)->(姓名),(学号)->(姓名),(身份证号)->(姓名);所以姓名部分函数依赖于(学号,身份证号);

    • **完全函数依赖(Full functional dependency)**:在一个关系中,若某个非主属性数据项依赖于全部关键字称之为完全函数依赖。比如学生基本信息表 R(学号,班级,姓名)假设不同的班级学号有相同的,班级内学号不能相同,在 R 关系中,(学号,班级)->(姓名),但是(学号)->(姓名)不成立,(班级)->(姓名)不成立,所以姓名完全函数依赖与(学号,班级);

    • 传递函数依赖:在关系模式 R(U)中,设 X,Y,Z 是 U 的不同的属性子集,如果 X 确定 Y、Y 确定 Z,且有 X 不包含 Y,Y 不确定 X,(X∪Y)∩Z=空集合,则称 Z 传递函数依赖(transitive functional dependency) 于 X。传递函数依赖会导致数据冗余和异常。传递函数依赖的 Y 和 Z 子集往往同属于某一个事物,因此可将其合并放到一个表中。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖。

    常用SQL语句

    • 基本查询:SELECT column1, column2 FROM table_name;(从指定表中选择特定列的数据)
    • 条件查询:SELECT * FROM table_name WHERE condition;(根据条件筛选数据,例如WHERE column = value
    • 多表查询:
      • 内连接:SELECT * FROM table1 INNER JOIN table2 ON table1.column = table2.column;(获取两个表中匹配的数据)
      • 左连接:SELECT * FROM table1 LEFT JOIN table2 ON table1.column = table2.column;(获取左表所有数据及右表匹配的数据)
      • 右连接:SELECT * FROM table2 RIGHT JOIN table1 ON table1.column = table2.column;
    • 聚合查询:SELECT COUNT(column), SUM(column) FROM table_name;(统计数量、求和等聚合操作,还有AVG求平均、MAX求最大值、MIN求最小值)

    数据插入(INSERT)

    • 插入单条数据:INSERT INTO table_name (column1, column2) VALUES (value1, value2);
    • 插入多条数据:INSERT INTO table_name (column1, column2) VALUES (value1, value2), (value3, value4);

    数据更新(UPDATE)

    • 一般更新:UPDATE table_name SET column1 = value1 WHERE condition;(根据条件修改表中指定列的数据)

    数据删除(DELETE)

    • 删除数据:DELETE FROM table_name WHERE condition;(根据条件删除表中数据)

    数据定义(DDL)

    • 创建表:CREATE TABLE table_name (column1 datatype, column2 datatype,...);(定义表的结构)
    • 修改表:
      • 添加列:ALTER TABLE table_name ADD column_name datatype;
      • 修改列:ALTER TABLE table_name ALTER COLUMN column_name new_datatype;
    • 删除表:DROP TABLE table_name;

    varchar与char的区别?

    char

    • char表示定长字符串,长度是固定的;
    • 因为长度固定,所以存取速度要比varchar快很多,甚至能快50%,但正因为其长度固定,所以会占据多余的空间,是空间换时间的做法;
    • 对于char来说,最多能存放的字符个数为255,和编码无关

    varchar

    • varchar表示可变长字符串,长度是可变的;
    • 对于varchar来说,最多能存放的字符个数为65532

    where 和 Having的区别

    最主要的区别是where筛选原表中的列,Having常用来筛选聚合函数的值
    一、作用的阶段不同

    • WHERE:在查询的 “行选择” 阶段起作用,即在分组之前筛选数据行。它用于对表中的原始数据进行筛选,确定哪些行将参与后续的查询操作,例如选择、聚合等。
    • HAVING:在查询的 “分组过滤” 阶段起作用,即在分组之后对聚合结果进行筛选。它是针对分组后的结果集进行条件判断,决定哪些分组应该被包含在最终的结果集中。
      二、使用的条件不同
    • WHERE:可以使用表中的列名、常量、表达式以及比较运算符、逻辑运算符等来构建筛选条件。例如,可以使用 WHERE column_name > 10 来筛选出某一列的值大于 10 的行。
    • HAVING:通常与聚合函数一起使用,用于对聚合结果进行筛选。例如,可以使用 HAVING SUM(column_name) > 100 来筛选出分组后总和大于 100 的分组。
      三、语法限制不同
    • WHERE:不能直接使用聚合函数。如果在 WHERE 子句中尝试使用聚合函数,会导致错误。例如,WHERE SUM(column_name) > 100 是错误的写法。
    • HAVING:只能用于包含聚合函数的查询中,并且可以使用聚合函数和列名来构建筛选条件。
      四、执行顺序不同
    • 查询执行顺序通常为:FROM(确定数据源) -> WHERE(筛选行) -> GROUP BY(分组) -> HAVING(筛选分组) -> SELECT(选择列) -> ORDER BY(排序)等。

    in exists

    MySQL中的in语句是把外表和内表作hash 连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。我们可能认为exists比in语句的效率要高,这种说法其实是不准确的,要区分情景:

    1. 如果查询的两个表大小相当,那么用in和exists差别不大。
    2. 如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in。
    3. not in 和not exists:如果查询语句使用了not in,那么内外表都进行全表扫描,没有用到索引;而not extsts的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。

    drop、delete与truncate的区别?

    三者都表示删除,但是三者有一些差别:

    |delete|truncate|drop|
    |—|—|—|—|
    |类型|属于DML|属于DDL|属于DDL|
    |回滚|可回滚|不可回滚|不可回滚|
    |删除内容|表结构还在,删除表的全部或者一部分数据行|表结构还在,删除表中的所有数据|从数据库中删除表,所有数据行,索引和权限也会被删除|
    |删除速度|删除速度慢,需要逐行删除|删除速度快|删除速度最快|

    因此,在不再需要一张表的时候,用drop;在想删除部分数据行时候,用delete;在保留表而删除所有数据的时候用truncate。

    4.2 MySQL 基础

    MySQL 数据库命名规范

    MySQL 数据库命名规范主要包括以下方面:

    • 数据库名

      • 只能包含字母、数字和下划线,且不能以数字开头。例如,my_database是一个符合规范的数据库名,而1database则不符合。
      • 应该具有一定的描述性,能够清晰地反映数据库的用途或存储的数据内容,比如ecommerce_orders能让人直观地知道这个数据库可能与电商订单相关。
    • 表名

      • 同样只能由字母、数字和下划线组成,不能以数字开头。
      • 通常采用名词或名词短语来命名,且多个单词之间可以用下划线分隔,如customer_information
    • 列名

      • 遵循与数据库名和表名类似的字符规则。
      • 一般采用有意义的名称来描述该列所存储的数据,比如product_nameorder_date等。
    • 提高代码可读性

      • 清晰、规范的命名使得其他开发人员在查看数据库结构和 SQL 代码时,能够快速理解每个数据库、表和列的含义和用途,减少理解代码的时间成本。
    • 减少错误和冲突

      • 遵循统一的命名规则可以避免因命名随意而导致的混淆和冲突。例如,如果不同的开发人员对同一个概念使用不同的命名方式,可能会在数据关联、查 询等操作中产生错误。
    • 便于维护和管理

      • 当数据库系统不断发展和扩展时,规范的命名有助于开发人员快速定位和修改相关的数据结构,提高维护效率。

    执行一条SQL语句,期间发生了什么?

    ![[Pasted image 20240316103936.png]]

    • 连接器:建立连接,管理连接、校验用户身份;
    • 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块
    • 解析 SQL,通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
    • 执行 SQL:执行 SQL 共有三个阶段:
      • 预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。
      • 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;
      • 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;

    🌟1条SQL语句的执行顺序?

    FROM(确定数据源) -> WHERE(筛选行) -> GROUP BY(分组) -> HAVING(筛选分组) -> SELECT(选择列) -> ORDER BY(排序)
    ![[Pasted image 20240903215834.png]]

    • FROM:对FROM子句中的左表和右表执行笛卡儿积(Cartesianproduct),产生虚拟表VT1
    • ON:对虚拟表VT1应用ON筛选,只有那些符合的行才被插入虚拟表VT2中
    • JOIN:如果指定了OUTER JOIN(如LEFT OUTER JOIN、RIGHT OUTER JOIN),那么保留表中未匹配的行作为外部行添加到虚拟表VT2中,产生虚拟表VT3。如果FROM子句包含两个以上表,则对上一个连接生成的结果表VT3和下一个表重复执行步骤1)~步骤3),直到处理完所有的表为止
    • WHERE:对虚拟表VT3应用WHERE过滤条件,只有符合的记录才被插入虚拟表VT4中
    • GROUP BY:根据GROUP BY子句中的列,对VT4中的记录进行分组操作,产生VT5
    • CUBE|ROLLUP:对表VT5进行CUBE或ROLLUP操作,产生表VT6
    • HAVING:对虚拟表VT6应用HAVING过滤器,只有符合的记录才被插入虚拟表VT7中。
    • SELECT:第二次执行SELECT操作,选择指定的列,插入到虚拟表VT8中
    • DISTINCT:去除重复数据,产生虚拟表VT9
    • ORDER BY:将虚拟表VT9中的记录按照进行排序操作,产生虚拟表VT10。11)
    • LIMIT:取出指定行的记录,产生虚拟表VT11,并返回给查询用户
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      SELECT 
      c.cname AS `课程名称`,
      COUNT(scs.score) AS `不及格人数`
      FROM
      student s
      JOIN
      student_course_score scs ON s.sid = scs.sid
      JOIN
      course c ON scs.cid = c.cid
      WHERE
      s.cno = '302' AND scs.score < 60
      GROUP BY
      c.cid, c.cname
      HAVING
      COUNT(scs.score) > 0
      ORDER BY
      c.cname DESC;

    InnoDB 和 MyISAM有什么区别?

    ![[Pasted image 20241005141700.png]]
    InnoDB是MySQL中最常用的存储引擎,也是默认的存储引擎。它支持ACID事务、行级锁定、外键约束等特性,适用于大型数据库。
    MyISAM是MySQL中较为简单的存储引擎,不支持事务和行级锁定,但它对于大量的SELECT操作非常快,适用于读密集型应用。

    1.  存储结构:每个MyISAM在磁盘上存储成三个文件;InnoDB所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB。
    2. 事务支持:MyISAM不提供事务支持;InnoDB提供事务支持事务,具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全特性。
    3  最小锁粒度:MyISAM只支持表级锁,更新时会锁住整张表,导致其它查询和更新都会被阻塞InnoDB支持行级锁。
    4. 索引类型:MyISAM的索引为聚簇索引,数据结构是B树;InnoDB的索引是非聚簇索引,数据结构是B+树。
    5.  主键必需:MyISAM允许没有任何索引和主键的表存在;InnoDB如果没有设定主键或者非空唯一索引,**就会自动生成一个6字节的主键(用户不可见)**,数据是主索引的一部分,附加索引保存的是主索引的值。
    6.  外键支持:MyISAM不支持外键;InnoDB支持外键

    • 如何选择?
      • 读密集的情况下,如果你不需要事务,也不需要保证数据库的崩溃回复,可以选择 MyISAM
      • 其他时候⼤可放⼼使⽤ InnoDB

    4.3 索引

    使用索引的最佳实践

    数据库索引是一种数据结构,它用于提高数据库查询的效率。在数据库中,索引可以被想象为一本书的目录。
    对于比较常用的InnoDB引擎来说,主键索引是十分必要的。主键通常默认带有唯一索引,且作为唯一标识需要高频查询。
    索引通常选取具有高选择性的字段,这通常意味着每个值对应的行数较少。
    对于一些查询频繁或者经常需要排序的字段,可以为其添加索引。

    什么是索引?

    数据库索引是一种数据结构,它用于提高数据库查询的效率。在数据库中,索引可以被想象为一本书的目录,它允许数据库管理系统(DBMS)快速地定位到数据表中的特定记录,而无需扫描整个表。

    不同角度的索引类型分类?(B+;聚簇;主键)

    按照数据结构维度划分:

    • BTree 索引:MySQL 里默认和最常用的索引类型。只有叶子节点存储 value,非叶子节点只有指针和 key。存储引擎 MyISAM 和 InnoDB 实现 BTree 索引都是使用 B+Tree,但二者实现方式不一样。
    • 哈希索引:类似键值对的形式,一次即可定位。
    • RTree 索引:一般不会使用,仅支持 geometry 数据类型,优势在于范围查找,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
    • 全文索引:对文本的内容进行分词,进行搜索。目前只有 CHARVARCHARTEXT 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。

    按照底层存储方式角度划分:

    • 聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB 中的主键索引就属于聚簇索引。
    • 非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引) 就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。

    按照应用维度划分:

    • 主键索引 :加速查询 + 列值唯一(不可以有 NULL)+ 表中只有一个。

    • 普通索引:仅加速查询。

    • 唯一索引:加速查询 + 列值唯一(可以有 NULL)。

    • 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。

    • 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。

    • 全文索引:对文本的内容进行分词,进行搜索。目前只有 CHARVARCHARTEXT 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替(倒排索引)。

    • 按「字段个数」分类:单列索引、联合索引
      ![[Pasted image 20241005164615.png]]

    索引底层数据类型有哪些?

    • Hash 通过哈希算法,我们可以快速找到 key 对应的 index,找到了 index 也就找到了对应的 value。
      • 落选原因: 主要是因为 Hash 索引不支持顺序和范围查询。假如我们要对表中的数据进行排序或者进行范围查询,那 Hash 索引可就不行了。并且,每次 IO 只能取一个。
    • 二叉查找树BST 左子树所有节点的值均小于根节点的值;右子树所有节点的值均大于根节点的值。
      • 落选原因: 二叉查找树的性能非常依赖于它的平衡程度,这就导致其不适合作为 MySQL 底层索引的数据结构。
    • 平衡二叉树AVL AVL 树的特点是保证任何节点的左右子树高度之差不超过 1,因此也被称为高度平衡二叉树,它的查找、插入和删除在平均和最坏情况下的时间复杂度都是 O(logn)。
      • 落选原因: 由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了数据库写操作的性能。并且, 在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。 磁盘 IO 是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。
    • 红黑树 红黑树是一种自平衡二叉查找树,通过在插入和删除节点时进行颜色变换和旋转操作,使得树始终保持平衡状态。根结点到叶子结点的最长路径不超过最短路径的两倍。
      • 和 AVL 树不同的是,红黑树并不追求严格的平衡,而是大致的平衡。正因如此,红黑树的查询效率稍有下降,因为红黑树的平衡性相对较弱,可能会导致树的高度较高,这可能会导致一些数据需要进行多次磁盘 IO 操作才能查询到,这也是 MySQL 没有选择红黑树的主要原因。也正因如此,红黑树的插入和删除操作效率大大提高了,因为红黑树在插入和删除节点时只需进行 O(1) 次数的旋转和变色操作,即可保持基本平衡状态,而不需要像 AVL 树一样进行 O(logn) 次数的旋转操作。
    • B树&B+树
      • B 树也称 B-树,全称为 多路平衡查找树 ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 Balanced (平衡)的意思。目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。
      • 所有节点关键字是按递增次序排列,并遵循左小右大原则
      • 所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子;
      • B树相对于平衡二叉树的不同是,每个节点包含的关键字增多了,特别是在B树应用到数据库中的时候,数据库充分利用了磁盘块的原理(磁盘数据存储是采用块的形式存储的,每个块的大小为4K,每次IO进行数据读取时,同一个磁盘块的数据可以一次性读取出来)把节点大小限制和充分使用在磁盘快大小范围;把树的节点关键字增多后树的层级比原来的二叉树少了,减少数据查找的次数和复杂度;

    B树与B+树的区别?

    • B 树的所有节点既存放键(key) 也存放数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
    • B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链 指向与它相邻的叶子节点。
    • B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索 很明显。
    • 在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限;而 B+树的范围查询,只需要对链表进行遍历即可。

    综上,B+树与 B 树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。

    数据库索引为什么选择B+树?

    1. 查询性能好:B+树是一种自平衡的树结构,这意味着树的高度始终保持最小,从而减少了磁盘I/O操作次数,提高了查询效率。

    2. 支持范围查询:B+树的所有值都在叶子节点出现,并且叶子节点本身按照值的大小顺序相连,这使得范围查询即顺序扫描变得非常高效。

    3. 适应页大小:B+树的节点设计通常考虑了数据库系统的页大小,使得每个节点可以很好地适应内存页的边界,减少页的内部碎片。

    4. 文件与数据库都是需要较大的存储,也就是说,它们都不可能全部存储在内存中,故需要存储到磁盘上,而所谓索引,则为了数据的快速定位与查找,那么索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数,因此B+树相比B树更为合适。数据库系统巧妙利用了局部性原理与磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入,而红黑树这种结构,高度明显要深的多,并且由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性。

    5. 最重要的是,B+树还方便顺序查询。B树必须用中序遍历的方法按序扫库,而B+树直接从叶子结点挨个扫一遍就完了,B+树支持range-queny非常方便,而B树不支持,这是数据库选用B+树的最主要原因。

    6. B+树查找效率更加稳定,B树有可能在中间节点找到数据,稳定性不够。

    MySQL里主键索引为什么比其他索引快?

    从数据存储结构角度

    • 聚簇索引:主键索引在 InnoDB 存储引擎下是聚簇索引,数据行和索引是存储在一起的,叶子节点直接包含数据行,在通过主键索引查询时可以直接获取数据,减少了磁盘 I/O 次数。而其他非聚簇索引的叶子节点存储的是主键值,通过非主键索引查询时,需要先在索引中找到主键值,再根据主键值去主键索引中查找对应的数据行。
      从索引唯一性角度
    • 唯一性保证:主键具有唯一性,数据库在维护索引时,不需要额外处理重复值的情况,数据的分布更加有序且可预测,这使得在进行数据查找和匹配时效率更高。

    ⚠️从数据页的角度看B+树?

    https://www.cnblogs.com/kukuxjx/p/17420789.html
    https://xiaolincoding.com/mysql/index/page.html#innodb-%E6%98%AF%E5%A6%82%E4%BD%95%E5%AD%98%E5%82%A8%E6%95%B0%E6%8D%AE%E7%9A%84

    1. InnoDB 的数据是按「数据页」为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。数据库的 I/O 操作的最小单位是页,InnoDB 数据页的默认大小是 16KB
    2. 虽然B+树的数据都存放在叶子节点上,但是叶子节点和非叶节点的结构大同小异,都包含了文件头,页目录,用户记录等结构。
    3. 每个页都包含了多条数据行,按照索引的顺序组成了单向链表。同时为了方便查询,又采用了分组及页目录索引的方式。页目录包含了一个个索引槽对应了每个分组的结尾。我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到对应的记录
    4. 但是,当我们需要存储大量的记录时,就需要多个数据页,这时我们就需要考虑如何建立合适的索引,才能方便定位记录所在的页。为了解决这个问题,InnoDB 采用了 B+ 树作为索引
      B+树是一种多路平衡查找树,其查找方式类似与二叉查找树。这样就可以通过二分查找的方法快速检索到记录在哪个分组,来降低检索的时间复杂度。
    5. 同时由于所有的数据行都在叶子节点上,同一个叶子数据页内的行又构成了一个单向链表,加之每个数据页的文件头存放了下一个文件页的指针,这样就可以实现顺序索引。
    6. 总体来说,B+树充分利用了数据页的特点,在数据页内记录多条数据行,通过页目录来索引。大大增加了存储和监所的效率。
      ![[Pasted image 20240912105854.png]]

    聚簇索引与非聚簇(二级)索引

    聚簇索引的 B+Tree 和二级索引的 B+Tree 区别如下:

    • 聚簇索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在聚簇索引的 B+Tree 的叶子节点里;
    • 二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。

    🌟一张表只能有一个聚簇(主键)索引,那为了实现非主键字段的快速搜索,就引出了二级索引(非聚簇索引/辅助索引),它也是利用了 B+ 树的数据结构,但是二级索引的叶子节点存放的是主键值,不是实际数据。
    ![[Pasted image 20240324151931.png]]

    • 聚簇索引的优缺点
      • 查询速度非常快:相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。
      • 对排序查找和范围查找优化 :聚簇索引对于主键的排序查找和范围查找速度非常快。
      • 依赖于有序数据;更新代价大
    • 非聚簇索引的优缺点:
      • 叶子节点不存放数据,更新代价比聚簇索引小。
      • 依赖有序数据;可能会二次查询(回表) 应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。

    覆盖索引

    在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。

    什么是回表?怎么减少回表?⚠️回表出现错误怎么办?

    在InnoDB存储引擎里,利用辅助索引查询,先通过辅助索引找到主键索引的键值,再通过主键值查出主键索引里面没有符合要求的数据,它比基于主键索引的查询多扫描了一棵索引树,这个过程就叫回表。
    ![[Pasted image 20240903221843.png]]

    减少回表的方法:

    1. 覆盖索引:创建一个包含查询字段的复合索引,这样查询时可以直接从索引中获取所需的全部字段,避免了回表操作。这种方式对于返回字段少、索引列和查询列相同的情况效果最佳。
    2. 聚簇索引:聚簇索引是一种特殊的索引类型,它将数据存储在索引中,而不是在表中。因此,如果查询需要的字段都在聚簇索引中,就可以避免回表操作。但是,创建聚簇索引会对表的写入性能产生影响,因此需要根据实际情况进行考虑。
    3. 列存储:列存储是一种将数据按列而非按行存储的方式,它可以提高查询性能和压缩比率,并减少回表的次数。这种方式在分析型应用中比较常用,例如数据仓库、BI等。
    4. 冗余字段:如果查询的字段非常频繁,可以考虑将它们冗余存储在表中,这样可以避免回表的开销。但是这种方式需要考虑数据一致性和空间占用等问题。

    联合索引最左匹配原则

    使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。

    联合索引的 B+Tree 是先按 product_no 进行排序,然后再 product_no 相同的情况再按 name 字段排序。

    最左匹配原则会一直向右匹配,直到遇到范围查询(如 >、<)为止。
    对于 >=、<=、BETWEEN 以及前缀匹配 LIKE 的范围查询,不会停止匹配
    (相关阅读:联合索引的最左匹配原则全网都在说的一个错误结论open in new window

    联合索引实例

    插入a、b、c联合索引
    复合索引也称为联合索引,当我们创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。
    查询条件是a、b、c时,无论是什么顺序,由于优化器优化,都会走INDEX_A_B_C联合索引;
    查询条件是a、b时,会走联合索引;
    查询条件是a、c时,也会走联合索引,但是Extra信息里面多了一行:Using index condition,意思是先条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用WHERE子句中的其他条件去过滤这些数据行,这种情况只有a条件用到联合索引,c条件回表到聚簇索引过滤。
    查询条件是b、c时,不走联合索引;
    查询条件是a时,会走联合索引;
    查询条件是b时,不走联合索引;
    查询条件是c时,不走联合索引

    数据库中,如果对a、c建立联合索引,对b建立单列索引。如果查询条件是a和b,那么哪个索引会生效?

    • 当查询条件同时包含a和b时,数据库通常只会使用一个索引。在这种情况下,数据库的查询优化器会根据索引的选择性、表的数据分布、索引的基数(即索引中不同值的数量)以及查询的具体情况来决定使用哪个索引。

    索引下推

    例如一张表,建了一个联合索引(name, age),查询语句:select * from t_user where name like '张%' and age=10;,由于name使用了范围查询,根据最左匹配原则:
    如果不使用索引下推,引擎层查找到name like '张%'的数据,再由Server层去过滤age=10这个条件,这样一来,就回表了两次,浪费了联合索引的另外一个字段age
    但是,使用了索引下推优化,把where的条件放到了引擎层执行,直接根据name like '张%' and age=10的条件进行过滤,减少了回表的次数。
    ![[Pasted image 20240903222210.png]]
    ![[Pasted image 20240903222224.png]]

    为什么使用索引会加快查询?

    传统的查询方法,是按照表的顺序遍历的,不论查询几条数据,MySQL需要将表的数据从头到尾遍历一遍。
    在我们添加完索引之后,MySQL一般通过BTREE算法生成一个索引文件,在查询数据库时,找到索引文件进行遍历,在比较小的索引数据里查找,然后映射到对应的数据,能大幅提升查找的效率。
    就类似于查找的时间复杂度,从O(n)降到了log级别。

    什么时候需要 / 不需要创建索引?

    什么时候适用索引?

    • 字段有唯一性限制的,比如商品编码;
    • 经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。
    • 经常用于 GROUP BYORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。

    什么时候不需要创建索引?

    • WHERE 条件,GROUP BYORDER BY 里用不到的字段
    • 字段中存在大量重复数据。比如性别字段,只有男女。MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。
    • 表数据太少
    • 经常更新的字段不用创建索引。因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,影响数据库性能

    创建索引有哪些注意点?

    1. 索引应该建在查询应用频繁的字段
      在用于 where 判断、 order 排序和 join 的(on)字段上创建索引。
    2. 索引的个数应该适量
      索引需要占用空间;更新时候也需要维护。
    3. 区分度低的字段,例如性别,不要建索引。
      离散度太低的字段,扫描的行数降低的有限。
    4. 频繁更新的值,不要作为主键或者索引
      维护索引文件需要成本;还会导致页分裂,IO次数增多。
    5. 组合索引把散列性高(区分度高)的值放在前面
      为了满足最左前缀匹配原则
    6. 创建组合索引,而不是修改单列索引。
      组合索引代替多个单列索引(对于单列索引,MySQL基本只能使用一个索引,所以经常使用多个条件查询时更适合使用组合索引)
    7. 过长的字段,使用前缀索引。
      当字段值比较长的时候,建立索引会消耗很多的空间,搜索起来也会很慢。我们可以通过截取字段的前面一部分内容建立索引,这个就叫前缀索引。
    8. 不建议用无序的值(例如身份证、UUID )作为索引
      当主键具有不确定性,会造成叶子节点频繁分裂,出现磁盘存储的碎片化

    ⚠️什么情况下会索引失效?

    1. 查询范围过大导致
      • in中的数据大于总数据的30%会导致索引失效
      • Like % 模糊查询范围过大导致索引失效
      • 范围查找,如果范围过大就会导致索引失效,范围小会使用索引
    2. 更改字段造成失效
      • 字段使用mysql的内置函数 更改字段导致找不到对应的索引
      • 字段不当地使用计算,更改字段导致找不到对应的索引
    3. 字段使用不确认导致索引失效
      • or不知道哪一个字段会使用(如果条件中有or,只要其中一个条件没有索引,其他字段有索引也不会使用。)
    4. 最优选择导致索引失败
      • 为了减少回表造成的资源消耗,Order By会直接使用全表扫描
    5. 未遵循最左匹配原则
      • 联合索引未从左到右使用
        ![[Pasted image 20240731194141.png]]

    select * 不会导致失效,会降低效率

    索引的优缺点

    优点

    • 使用索引可以大大加快 数据的检索速度(大大减少检索的数据量) , 这也是创建索引的最主要的原因。
    • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
      缺点
    • 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
    • 索引需要使用物理文件存储,也会耗费一定空间。

    使用索引一定会加快查询速度吗?

    大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。
    使用索引并不总是能提高查询性能,其效果取决于多种因素。索引是数据库中用于加快数据检索速度的排好序的数据结构,它们可以显著提高某些查询的执行速度,但在某些情况下可能不会带来性能提升,甚至可能对性能产生负面影响。以下是一些影响索引对查询性能影响的因素:

    1. 数据分布:如果数据分布非常不均匀,某些索引可能不会有效,因为它们不能显著减少需要检查的数据量。
    2. 查询类型:对于某些类型的查询,如全文搜索或多表连接,索引可能不会提供显著的性能提升。
    3. 索引选择性:选择性高的索引(即索引值分布广泛的索引)通常更有效,因为它们可以更快地缩小搜索范围。
    4. 更新频率:频繁更新的数据可能会使索引维护成本增加,因为每次数据变动都可能需要更新索引。
    5. 索引数量:过多的索引会增加数据库的存储需求,并可能降低写入性能,因为每次插入、更新或删除操作都需要更新所有相关索引。

    选择哪个字段作为索引?

    查询一个student表,如果where条件中有性别和姓氏,应该选择哪个字段作为索引?

    建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到

    区分度就是某个字段 column 不同值的个数「除以」表的总行数:

    $$
    区分度 = \frac{distinct(column)}{count(*)}
    $$
    比如,性别的区分度就很小,不适合建立索引或不适合排在联合索引列的靠前的位置,而 UUID 这类字段就比较适合做索引或排在联合索引列的靠前的位置。

    因为如果索引的区分度很小,假设字段的值分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比(惯用的百分比界线是”30%”)很高的时候,它一般会忽略索引,进行全表扫描

    怎么知道MySQL 有没有利用索引?

    MySQL 可以通过执行 EXPLAIN 命令来查看查询执行计划,从而判断是否使用了索引。

    具体操作步骤如下:

    1. 打开 MySQL 命令行工具,连接到相应的数据库。
    2. 在命令行中输入 EXPLAIN,接着输入你的查询语句,并以分号结束。
    3. 执行完命令后,MySQL 会返回查询的执行计划。其中会显示是否使用了索引、使用了哪个索引、索引的类型等信息。(对应 type字段、keys 字段)

    4.4 事务

    事务的ACID四大特性?

    • 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成。事务在执行过程中发生错误,会被回滚到事务开始前的状态。
    • 一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
    • 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。
    • 持久性(Durability)事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

    事务的关键字

    事务开始相关

    • START TRANSACTION:用于显式开启一个新的事务。
      事务控制相关
    • COMMIT:提交事务,使事务中对数据库的所有更改永久生效。
    • ROLLBACK:回滚事务,撤销正在进行的事务中对数据库所做的所有修改,将数据恢复到事务开始前的状态。
    • SAVEPOINT:在事务中创建一个保存点,用于将事务划分成更小的部分,之后可以回滚到特定的保存点。
    • ROLLBACK TO SAVEPOINT:回滚到指定的保存点。

    InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?

    • 原子性是通过 undo log(回滚日志) 来保证的;
    • 持久性是通过 redo log (重做日志)来保证的;
    • 隔离性 是通过 MVCC(多版本并发控制) 或锁机制来保证的;
    • 一致性则是通过持久性+原子性+隔离性来保证;

    事务的并发一致性问题

    数据库的并发一致性问题是指在多个用户或应用程序同时访问和修改数据库时,如何确保数据库的数据保持一致性和准确性的问题。当多个事务(Transaction)并发执行时,可能会遇到以下并发问题:

    1. 脏读(Dirty Read):一个事务读取到另一个事务未提交的修改数据。
      如果第一个事务最终回滚(撤销更改),那么第二个事务读取到的数据就是“脏”的,因为它可能基于无效或不完整的信息做出决策。
      ![[Pasted image 20240324170138.png]]
    2. 不可重复读(Non-repeatable Read):一个事务在读取某些数据后,另一个事务对这些数据进行了修改并提交,导致第一个事务再次读取这些数据时得到不同的结果。
      这违反了事务的一致性要求。这种情况下,事务在执行过程中无法保证读取数据的一致性。
      ![[Pasted image 20240324170819.png]]
    3. 幻读(Phantom Read):一个事务(事务2)在读取数据后,另一个事务插入了新的数据并提交,导致第一个事务再次读取时发现有额外的记录。
      这就好像出现了“幻影”行,这些行在之前的查询中并不存在。 count
      ![[Pasted image 20240324170849.png]]
    4. 丢失修改 :在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
      ![[Pasted image 20240324170757.png]]

    幻读是什么,如何解决?

    幻读:前后读取的记录数量不一致。

    • 可串行化隔离级别
      • 这是最高的隔离级别,通过强制事务串行执行,避免了幻读以及其他并发问题。例如在 SQL 中,可以在事务开始时设置隔离级别为 SERIALIZABLE。不过,这种方式会极大地降低数据库的并发性能,因为它限制了多个事务同时对数据进行操作。
    • MVCC + 快照读
      • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
      • 针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

    不可重复读和幻读的区别?

    • 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
    • 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。

    幻读其实可以看作是不可重复读的一种特殊情况,单独把区分幻读的原因主要是解决幻读和不可重复读的方案不一样。

    SQL事务隔离级别

    SQL的事务隔离级别是用来定义事务之间并发执行时的隔离程度,以避免并发事务引起的问题
    四个隔离级别如下:

    • 读未提交,指一个事务还没提交时,它做的变更就能被其他事务看到;最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
    • 读提交,指一个事务提交之后,它做的变更才能被其他事务看到;允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
    • 可重复读,指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
    • 串行化;会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
    隔离级别 脏读 不可重复读 幻读 实现原理
    未提交读 可能 可能 可能 直接读取最新的数据
    已提交读 不可能 可能 可能 在「每个语句执行前」都会重新生成一个 Read View
    可重复读 不可能 不可能 可能 「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。
    串行化 不可能 不可能 不可能 读写锁

    事务的各个隔离级别都是如何实现的?

    读未提交,是读不加锁原理。

    • 事务读不加锁,不阻塞其他事务的读和写
    • 事务写阻塞其他事务写,但不阻塞其他事务读;

    读取已提交&可重复读
    读取已提交和可重复读级别利用了ReadViewMVCC,也就是每个事务只能读取它能看到的版本(ReadView)。

    • READ COMMITTED:每次读取数据前都生成一个ReadView
    • REPEATABLE READ :在第一次读取数据时生成一个ReadView

    串行化
    串行化的实现采用的是读写都加锁的原理。
    串行化的情况下,对于同一行事务,会加写锁会加读锁。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

    MySQL的默认隔离级别

    MySQL InnoDB 存储引擎的默认支持的隔离级别是 可重读。

    在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的。而 select .. for update 语句就不是快照读了,而是当前读了,也就是每次读都是拿到最新版本的数据,但是它会对读到的记录加上 next-key lock 锁。
    MySQL 事务隔离级别详解

    为了解决并发一致性问题,并发事务的控制方式

    MySQL 中并发事务的控制方式无非就两种:MVCC。锁可以看作是悲观控制的模式,多版本并发控制(MVCC,Multiversion concurrency control)可以看作是乐观控制的模式。

    • 控制方式下会通过锁来显示控制共享资源而不是通过调度手段,MySQL 中主要是通过 读写锁 来实现并发控制。

      • 共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
      • 排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。

      读写锁可以做到读读并行,但是无法做到写读、写写并行。
      另外,根据根据锁粒度的不同,又被分为 表级锁(table-level locking)行级锁(row-level locking) 。InnoDB 不光支持表级锁,还支持行级锁,默认为行级锁。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。
      不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类。

    • MVCC 是多版本并发控制方法,即对一份数据会存储多个版本,通过事务的可见性来保证事务能看到自己应该看到的版本。通常会有一个全局的版本分配器来为每一行数据设置版本号,版本号是唯一的。
      MVCC 在 MySQL 中实现所依赖的手段主要是: 隐藏字段、read view、undo log

      • undo log : undo log 用于记录某行数据的多个版本的数据。
      • read view 和 隐藏字段 : 用来判断当前版本数据的可见性。
        InnoDB 存储引擎对 MVCC 的实现

    ⚠️ 讲一讲MVCC?

    MVCC,即多版本并发控制,是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。是一种不通过加锁来解决读-写冲突的无锁并发控制方法,简单来说就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。
    在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。

    具体来说,对于MVCC,有几个关键点:隐式字段、undolog、版本链、快照读&当前读、Read View

    1. 对于InnoDB存储引擎,每一行记录都有两个隐藏列DB_TRX_ID、DB_ROLL_PTR
    • DB_TRX_ID,事务ID,每次修改时,都会把该事务ID复制给DB_TRX_ID
    • DB_ROLL_PTR,回滚指针,指向回滚段的undo日志。

    整体流程:

    • 当一个事务开始时,它会被分配一个事务ID,并创建一个读视图,Read View,这个视图包含了事务开始时所有未提交的事务列表。
    • 当事务读取数据时,它会根据Read View来确定哪些版本的数据对当前事务可见。即如果数据的创建事务ID小于Read View中的最小事务ID,或者数据的删除事务ID在Read View中不存在,那么这个版本的数据对当前事务是可见的。
    • 如果事务需要修改数据,它会在Undo Log中记录数据的旧版本,然后创建数据的新版本。新版本的数据会与当前事务的事务ID关联。
    • 当事务提交时,它的修改会成为数据库的一部分,新版本的数据会与其他事务的Read View隔离,直到它们也提交。
    • 数据库的垃圾收集器会定期清理那些只在Undo Log中存在,且没有被任何活跃事务的Read View引用的旧数据版本。

    当前读与快照读?

    • 当前读是指事务读取数据库中的最新数据,也就是当前版本的数据。在执行当前读时,如果数据正在被另一个事务修改(例如,被锁定或处于待提交状态),当前读可能会等待,直到数据被解锁或事务提交。
    • 快照读是MVCC(多版本并发控制)的一个特性,它允许事务读取数据在某个特定时间点的版本,即快照。在快照读中,即使其他事务在并行修改数据,当前事务读取的也是数据的一个稳定版本,这个版本是在事务开始时创建的。快照读可以避免读锁,允许多个事务并发读取数据,而不会相互阻塞。快照读通常用于普通的查询操作,特别是那些不需要数据最新性的场景。🌟在支持MVCC的数据库系统中,大多数普通的SELECT查询默认就是快照读。

    MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现

    主要区别在于它们如何处理数据的可见性和事务的隔离级别:

    • 当前读提供了更高的隔离级别,可以防止其他事务在当前事务完成之前修改数据,但可能会因为锁定而降低并发性。
    • 快照读则提供了较低的隔离级别,允许更高的并发性,但可能无法看到其他事务的最新更改。

    当前读,快照读和MVCC之间是什么关系呢?

    • 准确的说,MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。仅仅是一个理想概念
    • 而在MySQL中,实现这么一个MVCC理想概念,我们就需要MySQL提供具体的功能去实现它,而快照读就是MySQL为我们实现MVCC理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现

    MVCC能解决什么问题,好处是什么?

    • 读-读:不存在任何问题,也不需要并发控制
    • 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
    • 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

    多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题
    在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

    MVCC的实现原理?

    MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 4个隐式字段undolog ,Read View 来实现的。

    1. 隐式字段
      每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID等字段。用于追踪数据的版本和事务的关联。
    • DB_ROW_ID 6byte, 隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
    • DB_TRX_ID 6byte, 最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
    • DB_ROLL_PTR 7byte, 回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
    • DELETED_BIT 1byte, 记录被更新或删除并不代表真的删除,而是删除flag变了
      ![[Pasted image 20240731213109.png]]
    1. undo log
      InnoDB把这些为了回滚而记录的这些东西称之为undo log。这里需要注意的一点是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo log。

    2. Read View(读视图)
      Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

    整体流程:

    • 当一个事务开始时,它会被分配一个事务ID,并创建一个Read View,这个视图包含了事务开始时所有未提交的事务列表。
    • 当事务读取数据时,它会根据Read View来确定哪些版本的数据对当前事务可见。如果数据的创建事务ID小于Read View中的最小事务ID,或者数据的删除事务ID在Read View中不存在,那么这个版本的数据对当前事务是可见的。
    • 如果事务需要修改数据,它会在Undo Log中记录数据的旧版本,然后创建数据的新版本。新版本的数据会与当前事务的事务ID关联。
    • 当事务提交时,它的修改会成为数据库的一部分,新版本的数据会与其他事务的Read View隔离,直到它们也提交。
    • 数据库的垃圾收集器会定期清理那些只在Undo Log中存在,且没有被任何活跃事务的Read View引用的旧数据版本。

    Read View 在 MVCC 里如何工作的?

    Read View 有四个重要的字段:

    • m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务
    • min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
    • max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;
    • creator_trx_id :指的是创建该 Read View 的事务的事务 id
      ![[Pasted image 20240731215320.png]]

    聚簇索引记录中包含下面两个隐藏列:

    • trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里
    • roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。

    在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:
    ![[Pasted image 20240731215613.png]]

    • 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 已经提交的事务生成的,所以该版本的记录对当前事务可见
    • 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 才启动的事务生成的,所以该版本的记录对当前事务不可见
    • 如果记录的 trx_id 值在 Read View 的 min_trx_idmax_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
      • 如果记录的 trx_id m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见
      • 如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见

    这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。

    可重复读的MVCC实现

    当事务开始读取数据时,数据库会创建一个Read View,它是数据库在事务开始时的一个快照。Read View记录了事务开始时所有活跃的事务和它们对数据的修改。
    在可重复读隔离级别下,事务在整个执行期间都使用同一个Read View。即使在事务期间其他事务提交了修改,这些修改也不会对当前事务可见,从而保证了数据的一致性。

    已提交读的MVCC实现

    读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View
    那么在事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。

    MVCC能保证不产生幻读吗?

    仅仅依靠MVCC无法完全避免幻读的情况发送。
    快照读是可以避免的,
    对于当前读,如果没加间隙锁:

    1
    2
    3
    4
    5
    6
    #事物A:
    select name from user where id > 3;
    #事物B:
    insert into user valus('6','edwin');
    #事物A:
    update user set name = 'xxx' where id = 6;

    事务A修改了事务B插入的数据,update是当前读,所以此时会读取最新的数据(包括其他已经提交的事务)
    解决方案:记录锁+间隙锁

    4.5 锁

    锁的分类

    • 粒度:
      • 全局锁
      • 表级锁
        • 表锁
        • 元数据锁
        • 意向锁
        • AUTO-INC 锁
      • 行级锁
        • Record Lock 记录锁
        • Gap Lock 间隙锁
        • Next-Key Lock 临键锁
        • 意向锁
    • 方式:读锁(共享锁) / 写锁(排他锁、独占锁)
    • 态度:悲观锁 / 乐观锁
      • 悲观锁 (读锁、写锁都是悲观锁)
      • 乐观锁 (乐观锁,需要外部程序实现)
    • 锁模式
      • 记录锁
      • 间隙锁
      • next-key锁
      • 意向锁

    悲观锁和乐观锁

    https://blog.csdn.net/weixin_45433031/article/details/120838045
    乐观锁和悲观锁是并发编程中常用的锁机制,它们各自有不同的优缺点和应用场景。,乐观锁和悲观锁是对数据冲突的两种不同态度。

    乐观锁🔒是对于数据冲突保持一种乐观态度,操作数据不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本号然后进行版本号的对比方式实现);

    悲观锁🔒,顾名思义就是总是假设最坏的情况,每次获取数据的时候都认为别人会修改,所以每次在获取数据的时候都会上锁,这样别人想获取这个数据就会阻塞直到它拿到锁后才可以获取(共享资源每次只给一个线程使用,其它线程阻塞,当前线程用完后再把资源转让给其它线程)。
    传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。

    • 乐观锁和悲观锁是在不同的情况下使用的锁机制,它们各自有不同的优缺点。下面是乐观锁和悲观锁的比较
      1 性能比较
      乐观锁适用于并发访问量较少的情况下,可以提高系统的并发性能和吞吐量。悲观锁适用于并发访问量较大的情况下,可以保证数据的一致性和正确性,但会降低系统的并发性能。
      2 安全性比较
      乐观锁采用版本控制等方式来保证数据的正确性,但在并发访问量较大的情况下容易出现数据冲突和错误。悲观锁采用加锁的方式来保证数据的一致性和正确性,避免了数据冲突和错误,但可能会出现死锁和饥饿等问题。
      3 应用场景比较
      乐观锁适用于读多写少的情况下,例如网站的浏览量统计。悲观锁适用于读写频繁的情况下,例如银行系统的转账操作。

    • 🗝️乐观锁与悲观锁的实现
      乐观锁是一种并发控制机制,主要用于管理数据库中记录的并发修改。它假设多个事务在大多数时间不会同时修改同一条记录,因此在实际提交更改之前不加锁。乐观锁通常通过记录版本号或时间戳来实现。
      在表中添加一个version字段,每次更新记录时,将这个版本号加一。更新操作时,检查版本号是否与数据库中的版本号一致,如果一致,执行更新并将版本号加一;如果不一致,表示记录已经被其他事务更新,此次更新失败。

    共享锁和排他锁

    不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类:

    • 共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
    • 排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。
      排他锁与任何的锁都不兼容,共享锁仅和共享锁兼容。
    S 锁 X 锁
    S 锁 不冲突 冲突
    X 锁 冲突 冲突

    由于 MVCC 的存在,对于一般的 SELECT 语句,InnoDB 不会加任何锁

    意向锁(表级锁)

    如果需要用到表锁的话,就需要判断表中的记录没有行锁呢,一行一行遍历肯定是不行,性能太差。我们需要用到意向锁来快速判断是否可以对某个表使用表锁。
    意向锁是表级锁,共有两种:

    • 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
    • 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。

    意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。

    表级锁和行级锁

    MyISAM 仅仅支持表级锁(table-level locking),一锁就锁整张表,这在并发写的情况下性非常差。InnoDB 不光支持表级锁(table-level locking),还支持行级锁(row-level locking),默认为行级锁。
    行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。

    表级锁和行级锁对比

    • 表级锁: MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。
    • 行级锁: MySQL 中锁定粒度最小的一种锁,是 针对索引字段加的锁 ,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。

    InnoDB 有哪几类行锁?

    InnoDB 行锁是通过对索引数据页上的记录加锁实现的,MySQL InnoDB 支持三种行锁定方式:

    • 记录锁(Record Lock):也被称为记录锁,属于单个行记录上的锁。
    • 间隙锁(Gap Lock):锁定一个范围,不包括记录本身。
    • 临键锁(Next-Key Lock):Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题(MySQL 事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。

    在 InnoDB 默认的隔离级别 REPEATABLE-READ (可重复读)下,行锁默认使用的是 Next-Key Lock(临键锁)
    但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock(记录锁) ,即仅锁住索引本身,而不是范围。

    什么是数据库死锁,如何避免死锁?

    数据库死锁是指两个或多个事务在同一数据库系统中互相占用对方所需的资源,并处于等待对方释放资源的僵持状态,从而导致这些事务都无法继续执行下去的情况。例如,事务 A 锁定了资源 X,同时事务 B 锁定了资源 Y,之后事务 A 又尝试去锁定资源 Y,而事务 B 尝试去锁定资源 X,此时就会发生死锁。

    • 合理的事务设计
      • 尽量减少事务的持有时间:事务应该尽快完成对资源的操作并释放资源,避免长时间占用资源。例如,在一个事务中,如果不需要同时对多个数据进行操作,可以将操作按顺序分解为多个事务,减少资源被锁定的时间。
      • 避免事务嵌套:嵌套事务可能会增加死锁的风险,尽量保持事务的简单性,避免在事务内部再开启新的事务。
    • 资源锁定顺序一致
      • 确保多个事务对多个资源进行操作时,按照相同的顺序获取资源。例如,如果有资源 A、B 和 C,所有事务都应该先获取资源 A,再获取资源 B,最后获取资源 C,这样可以避免循环等待资源的情况。
    • 使用合适的隔离级别
      • 数据库的隔离级别会影响死锁的发生概率。例如,在某些情况下,可以选择较低的隔离级别(如读已提交隔离级别)来减少死锁,但需要注意这可能会带来数据一致性方面的问题,需要根据具体业务场景进行权衡。
    • 定期监测和处理死锁
      • 数据库系统通常提供了监测死锁的工具,通过这些工具可以及时发现死锁的发生。一旦发现死锁,数据库系统会自动进行处理,通常是选择一个事务作为牺牲品进行回滚,释放其占用的资源,以解除死锁状态。

    4.6 日志

    MySQL日志文件有哪些?分别介绍下作用?

    MySQL日志文件有很多,包括 :

    • 错误日志(error log):错误日志文件对MySQL的启动、运行、关闭过程进行了记录,能帮助定位MySQL问题。
    • 慢查询日志(slow query log):慢查询日志是用来记录执行时间超过 long_query_time 这个变量定义的时长的查询语句。通过慢查询日志,可以查找出哪些查询语句的执行效率很低,以便进行优化。
    • 一般查询日志(general log):一般查询日志记录了所有对MySQL数据库请求的信息,无论请求是否正确执行。
    • 二进制日志(bin log):关于二进制日志,它记录了数据库所有执行的DDL和DML语句(除了数据查询语句select、show等),以事件形式记录并保存在二进制文件中。

    还有两个InnoDB存储引擎特有的日志文件:

    • 重做日志(redo log):重做日志至关重要,因为它们记录了对于InnoDB存储引擎的事务日志。
    • 回滚日志(undo log):回滚日志同样也是InnoDB引擎提供的日志,顾名思义,回滚日志的作用就是对数据进行回滚。当事务对数据库进行修改,InnoDB引擎不仅会记录redo log,还会生成对应的undo log日志;如果事务执行失败或调用了rollback,导致事务需要回滚,就可以利用undo log中的信息将数据回滚到修改之前的样子。

    binlog 和 redolog有什么区别?

    • bin log会记录所有与数据库有关的日志记录,包括InnoDB、MyISAM等存储引擎的日志,而redo log只记InnoDB存储引擎的日志。
    • 记录的内容不同,bin log记录的是关于一个事务的具体操作内容,即该日志是逻辑日志。而redo log记录的是关于每个页(Page)的更改的物理情况。
    • 写入的时间不同,bin log仅在事务提交前进行提交,也就是只写磁盘一次。而在事务进行的过程中,却不断有redo ertry被写入redo log中。
    • 写入的方式也不相同,redo log是循环写入和擦除,bin log是追加写入,不会覆盖已经写的文件。

    一条更新语句怎么执行的了解吗?

    更新语句的执行是Server层和引擎层配合完成,数据除了要写入表中,还要记录相应的日志。

    1. 执行器先找引擎获取ID=2这一行。ID是主键,存储引擎检索数据,找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
    2. 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
    3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
    4. 执行器生成这个操作的binlog,并把binlog写入磁盘。
    5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。
      ![[Pasted image 20241005160441.png]]
      不仅如此,在对redo log写入时有两个阶段的提交,一是binlog写入之前prepare状态的写入,二是binlog写入之后commit状态的写入。

    为什么更新语句需要两段提交?

    1. redolog里面保存了事务,binlog里面存放了操作的语句。
    2. 两阶段提交将事务的执行过程拆分成两个阶段,准备(prepare)阶段和提交(commit)阶段。也可称投票阶段和执行阶段。
    3. 两阶段提交是为了让两份日志之间的逻辑一致。如果不是两阶段提交,无论是先写完 redo log 再写 binlog,或者采用反过来的顺序。在两个中间MySQL进程异常重启,都会发生字段的值与原库的值不同。
    4. 先写入redo log,后写入binlog: 在写完redo log之后,数据此时具有crash-safe能力,因此系统崩溃,数据会恢复成事务开始之前的状态。但是,若在redo log写完时候,binlog写入之前,系统发生了宕机。此时binlog没有对上面的更新语句进行保存,导致当使用binlog进行数据库的备份或者恢复时,就少了上述的更新语句。从而使得id=2这一行的数据没有被更新。
    5. 先写入binlog,后写入redo log: 写完binlog之后,所有的语句都被保存,所以通过binlog复制或恢复出来的数据库中id=2这一行的数据会被更新为a=1。但是如果在redo log写入之前,系统崩溃,那么redo log中记录的这个事务会无效,导致实际数据库中id=2这一行的数据并没有更新。

    (区分)分布式事务的两阶段提交

    redo log 输入磁盘

    redo log的写入不是直接落到磁盘,而是在内存中设置了一片称之为redo log buffer的连续内存空间,也就是redo 日志缓冲区

    • log buffer 空间不足时
      log buffer 的大小是有限的,如果不停的往这个有限大小的 log buffer 里塞入日志,很快它就会被填满。如果当前写入 log buffer 的redo 日志量已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
    • 事务提交时
      在事务提交时,为了保证持久性,会把log buffer中的日志全部刷到磁盘。注意,这时候,除了本事务的,可能还会刷入其它事务的日志。
    • 后台线程输入
      有一个后台线程,大约每秒都会刷新一次log buffer中的redo log到磁盘。
    • 正常关闭服务器时
    • 触发checkpoint规则

    4.7 SQL优化

    慢SQL如何定位呢?

    慢SQL的监控主要通过两个途径:

    1. 慢查询日志
      • MySQL:开启慢查询日志(slow query log)可以记录执行时间超过设定阈值的SQL语句。配置文件中设置long_query_time参数来定义“慢”查询的时间阈值(例如,超过1秒的查询被认为是慢查询),并设置slow_query_logON
    2. 性能分析工具
      • EXPLAIN/EXPLAIN ANALYZE:在SQL查询前加上EXPLAINEXPLAIN ANALYZE(取决于数据库系统),可以查看查询的执行计划,帮助理解查询的性能问题。

    数据库调优–explain关注哪些字段

    explain是sql优化的利器,除了优化慢sql,平时的sql编写,也应该先explain,查看一下执行计划,看看是否还有优化的空间。
    直接在 select 语句之前增加explain 关键字,就会返回执行计划的信息。
    ![[Pasted image 20241005143717.png]]

    • type 列:最重要的列之一。表示关联类型或访问类型,即 MySQL 决定如何查找表中的行。性能从最优到最差分别为:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
    • possible_keys 列:显示查询可能使用哪些索引来查找,使用索引优化sql的时候比较重要。
    • key 列:这一列显示 mysql 实际采用哪个索引来优化对该表的访问,判断索引是否失效的时候常用。

    mysql 有什么加快查询的方法?如何进行性能优化?

    1. 优化查询语句:避免使用SELECT *,而是明确指定需要查询的字段,减少数据量,提高效率。使用EXPLAIN分析查询语句,查看是否有效利用了索引。
      • 只选择需要的列,而不是所有列。
      • 使用JOIN代替子查询:当可能的时候,JOIN通常比子查询更高效。
      • 合理使用LIMIT:如果不需要所有结果,则使用LIMIT限制返回的行数。
    2. 使用索引:为查询字段添加合适的索引可以显著提高查询速度。创建索引时,应考虑查询频率和数据更新频率,避免过度索引
      • 单列索引:根据查询需求,对经常用于WHERE子句中的列创建索引。
      • 覆盖索引:如果索引包含所有需要查询的字段,则查询只需在索引中完成,而无需访问实际表。
      • 避免索引失效 :避免索引查询(like)范围过大;避免使用 != 或者 <> 操作符;避免列上函数运算;正确使用联合索引,注意最左匹配原则
    3. 优化数据类型
      • 使用最合适、最小的数据类型可以减小数据的存储空间,从而提高查询性能。
    4. 缓存
      • 利用MySQL的查询缓存功能,对经常访问的查询进行缓存。
      • 注意,某些场景下,查询缓存可能适得其反。需要根据实际情况进行评估。
    5. 分区和分片(分库分表)
      • 通过分区或分片将大表拆分为更小、更易于管理的部分,从而提高查询性能。
    6. **读写分离
      • 通过将读和写操作分离到不同的服务器,可以平衡负载并提高性能。
        ![[Pasted image 20240903220605.png]]

    数据库备份

    • 防止数据丢失:如硬件故障(硬盘损坏、服务器宕机等)、软件错误(数据库软件崩溃、程序漏洞导致数据损坏)、人为误操作(误删除数据、错误的更新操作等)以及自然灾害等意外情况,备份可以使数据得到恢复。

    常见的数据库备份策略有:

    • 完全备份
      • 定义:对数据库中的所有数据进行完整的备份,包括所有的数据库对象(如表、索引、存储过程等)。
      • 特点:备份的数据最全面,但备份时间和占用空间相对较大,恢复时只需这一个备份文件即可恢复到备份时的状态。
    • 增量备份
      • 定义:只备份自上一次备份(可以是完全备份或增量备份)以来发生变化的数据。
      • 特点:每次备份的数据量较小,备份速度快,节省存储空间,但恢复数据时需要先恢复完全备份,再依次恢复每个增量备份。
    • 差异备份
      • 定义:备份自上一次完全备份以来发生变化的数据。
      • 特点:备份的数据量比增量备份大,但比完全备份小。恢复时需要完全备份和最近的一次差异备份。

    数据库连接池

    数据库连接池是一种用于管理数据库连接的技术。它在应用程序启动时创建一定数量的数据库连接,并将这些连接保存在一个 “池” 中。当应用程序需要访问数据库时,它可以从池中获取一个连接,使用完毕后再将连接归还到池中,而不是每次都新建连接和销毁连接。

    使用场景包括以下方面:

    • 高并发的 Web 应用:例如电商网站,在用户访问高峰期,会有大量的用户同时进行商品浏览、下单等操作,这些操作都涉及数据库的读写。使用连接池可以高效地管理众多并发的数据库连接请求,减少连接创建和销毁的开销,提高系统响应速度。
    • 频繁访问数据库的应用程序:像金融交易系统,交易数据需要频繁地读写数据库,数据库连接池可以保证在频繁操作的情况下快速获取和释放连接,提高系统性能。
    • 资源有限的环境:在服务器资源有限的情况下,连接池可以有效控制数据库连接的数量,避免因创建过多连接导致系统资源耗尽。

    数据库水平分区以及垂直分区

    数据库水平分区(Horizontal Partitioning)

    • 定义:也称为行分区,是根据某些规则将一个表中的数据行划分到多个不同的物理存储位置(分区)中,各个分区可以位于同一台服务器的不同文件系统上,也可以位于不同的服务器上。
    • 分区规则举例
      • 范围分区(Range Partitioning):按照某个列的值的范围来进行分区,例如根据日期列将销售数据表按月份范围划分为不同的分区,1 - 3 月的数据在一个分区,4 - 6 月的数据在另一个分区等。
      • 哈希分区(Hash Partitioning):通过对分区键进行哈希运算来确定数据行应该存储在哪个分区,这种方式可以使数据在各个分区中均匀分布,适用于数据分布比较随机的情况。
    • 优点
      • 提高查询性能:针对特定分区的查询只需要搜索相关的分区,而不是整个表,减少了数据的读取量。
      • 可扩展性好:可以方便地添加新的分区来存储新的数据,而无需对现有数据进行大规模的重新组织。

    数据库垂直分区(Vertical Partitioning)

    • 定义:也称为列分区,是将一个表按照列的相关性分解成多个表,每个表只包含原表中的部分列。这些分区表可以存储在相同或不同的数据库服务器上。
    • 优点
      • 减少数据冗余:将不经常使用的列分离到单独的表中,避免在每次查询时都读取这些冗余数据,提高存储效率。
      • 提高查询性能:当查询只涉及某些特定列时,只需要从相关的垂直分区表中获取数据,减少了数据的读取量和 I/O 操作,从而提高查询效率。

    数据库调优

    数据库调优是:对数据库的性能进行优化的过程,目的是使数据库能够更快地响应请求、更高效地利用系统资源、处理更多的并发事务等。

    常用的数据库调优方法如下:

    • 优化 SQL 语句
      • 合理使用索引:通过在经常用于查询条件、连接条件的列上创建索引,可以加快数据的检索速度。但索引也不能过多,因为索引本身也会占用存储空间且在数据更新时需要维护索引,会增加额外的开销。
      • 避免全表扫描:例如在查询时,通过添加合适的查询条件,而不是不加限制地从整个表中查询数据,尽量减少全表扫描的情况。
      • 简化复杂查询:将复杂的嵌套查询、关联查询等进行优化,减少不必要的子查询和冗余的连接操作。
    • 调整数据库配置参数
      • 内存相关参数:比如调整缓冲池(Buffer Pool)的大小,缓冲池用于缓存磁盘上的数据页,增大缓冲池可以减少磁盘 I/O 操作。
      • 并发相关参数:像最大连接数,根据应用程序的并发访问情况合理设置,避免连接数过多导致资源竞争或连接数过少限制系统的并发处理能力。
    • 数据库表结构优化
      • 范式化和反范式化:在设计表结构时,根据实际情况选择合适的范式。范式化可以减少数据冗余,但可能导致多表连接查询的复杂性增加;反范式化则可以通过增加冗余数据来减少表的连接操作,提高查询性能。
      • 数据分区:根据数据的特点和访问模式,对表进行水平分区或垂直分区,以提高查询效率和数据管理的便利性。

    数据库分片

    数据库分片是一种将大型数据库拆分成多个较小的、独立的部分(称为分片)的技术。每个分片可以存储在不同的数据库服务器上,数据根据特定的规则分布在这些分片之中,例如可以按照某个关键列的取值范围或者哈希值来决定数据存储在哪个分片上。

    优势:

    • 提高性能
      • 由于数据分布在多个服务器上,每个分片只处理一部分数据的查询和事务,减少了单个服务器的负载,能够并行处理更多的请求,从而提高了整体的读写性能。
      • 特别是在处理大规模数据和高并发的场景下,数据库分片可以显著降低响应时间。
    • 可扩展性
      • 可以方便地通过增加更多的数据库服务器和分片来满足不断增长的数据存储和处理需求。这种线性的扩展方式使得系统能够轻松应对业务的快速发展。

    挑战:

    • 复杂性增加
      • 数据库分片会使系统架构变得复杂,数据的分布和管理需要精心设计和维护。例如,跨分片的事务处理变得复杂,需要特殊的处理机制来保证事务的一致性。
      • 对数据进行分片、路由查询以及在多个分片上进行数据汇总等操作都增加了系统设计和开发的难度。
    • 数据分布不均
      • 如果分片规则设计不合理,可能导致数据在各个分片之间分布不均匀。有些分片可能存储过多的数据,而有些分片的数据量很少,这样会影响系统的整体性能,部分分片可能会成为性能瓶颈。

    什么是数据库约束,列举一下常见的数据库约束

    数据库约束是:对存入数据库中的数据进行限制和规范的规则,它确保数据的完整性、准确性和一致性。

    常见的数据库约束有以下几种:

    • 主键约束(Primary Key Constraint)
      • 定义:用于唯一标识表中的每一行记录,一个表只能有一个主键。主键列的值不能重复且不能为空。
      • 示例:在学生表中,学生的学号可以设为主键,确保每个学生都有唯一的学号来标识。
    • 外键约束(Foreign Key Constraint)
      • 定义:建立两个表之间的关联关系,确保数据的参照完整性。外键列的值必须是另一个表中主键列的值或者为空。
      • 示例:在订单表中设置客户编号作为外键,关联到客户表中的主键客户编号,这样订单表中的客户编号必须是已经存在于客户表中的有效客户编号。
    • 唯一约束(Unique Constraint)
      • 定义:保证在一列或者一组列中的值是唯一的,但与主键不同,它允许有空值。
      • 示例:在员工表中,员工的电子邮箱列可以设置为唯一约束,确保每个员工的电子邮箱地址是唯一的。
    • 非空约束(Not Null Constraint)
      • 定义:规定某一列的值不能为空,必须包含数据。
      • 示例:在用户注册信息表中,用户的姓名列可以设置为非空约束,因为姓名是必填项。

    不加索引怎么优化连接操作?

    • 减少数据量
      • 限制查询结果:在连接操作前,尽量使用条件(WHERE 子句)对参与连接的表进行筛选,减少不必要的数据参与连接运算。例如,只查询特定时间段、特定类别等相关数据。
      • 分区表:如果数据库支持,可以对表进行分区。在进行连接操作时,只针对相关分区的数据进行操作,从而减少数据处理量。
    • 优化连接顺序
      • 根据表的数据量大小和关联关系,合理安排连接的顺序。先连接筛选性最强的表,这样可以使中间结果集尽可能小。例如,在一个包含订单表(orders)、客户表(customers)和产品表(products)的连接查询中,如果知道大部分查询是针对特定几个客户的订单,那么先连接客户表和订单表,再连接产品表。
    • 避免笛卡尔积
      • 确保在连接操作中始终有明确的连接条件(ON 子句),以防止出现笛卡尔积(两个表的所有行两两组合),导致结果集数据量爆炸式增长。

    主从同步(异步,半同步)

    海量数据存SQL该如何优化?

    优化表设计、优化查询、索引优化、缓存、读写分离
    首先,从海量数据会导致的问题来看。
    一是数据量太多导致查询和更新慢;二是海量数据的高并发读写问题。
    主要解决思路就是分库分表。依照某一种策略将数据尽量平均的分配到多个数据库节点或者多个表中。比如可以按照对主键结点进行哈希来水平分表。
    分库分表之后,每个节点只保存部分的数据。数据的写入请求和读写请求都由单一的主库请求变成了请求多个数据分配节点。 在一定程度上会提升并发写/读入的性能。

    从数据的角度来看,海量的数据一般都有时效性,比如历史订单。一般大量的读写操作都发生在最近的时间段。这种情况就可以对历史数据进行归档。是把大量的历史订单移到另外一张历史订单表中。大多数情况下访问的都是最近的数据,但是订单表里面大量的数据都是不怎么常用的老数据。

    分库分表可能造成的问题

    1. 跨库跨表查询: 关联查询变得复杂,原来在单表或单库中简单的 JOIN 操作可能需要在多个库和表之间进行复杂的数据拉取和合并,可能导致性能下降。
    2. 事务操作复杂:库分表后,一个业务操作可能涉及多个数据库或表,传统的本地事务无法满足需求,需要引入复杂的分布式事务解决方案。
    3. 扩容及数据迁移复杂:在进行分库分表的过程中,以及后续的扩容操作时,涉及大量数据的迁移工作,需要保证数据的完整性和一致性,迁移过程复杂且风险较高。

    慢查询优化基本步骤

    https://pdai.tech/md/db/sql-mysql/sql-mysql-index-improve-mt.html

    0.先运行看看是否真的很慢,注意设置SQL_NO_CACHE
    1.where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高
    2.explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)
    3.order by limit 形式的sql语句让排序的表优先查
    4.了解业务方使用场景
    5.加索引时参照建索引的几大原则
    6.观察结果,不符合预期继续从0分析

    5 SSM(JavaWeb)

    5.1 Spring

    Spring有哪些特性呢?

    1. IOCDI 的支持
      Spring 的核心就是一个大的工厂容器,可以维护所有对象的创建和依赖关系,Spring 工厂用于生成 Bean,并且管理 Bean 的生命周期,实现高内聚低耦合的设计理念。
    2. AOP 编程的支持
      Spring 提供了面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等切面功能。
    3. 声明式事务的支持
      支持通过配置就来完成对事务的管理,而不需要通过硬编码的方式,以前重复的一些事务提交、回滚的JDBC代码,都可以不用自己写了。
    4. 快捷测试的支持
      Spring 对 Junit 提供支持,可以通过注解快捷地测试 Spring 程序。
    5. 快速集成功能
      方便集成各种优秀框架,Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如:Struts、Hibernate、MyBatis、Quartz 等)的直接支持。
    6. 复杂API模板封装
      Spring 对 JavaEE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等)都提供了模板化的封装,这些封装 API 的提供使得应用难度大大降低。

    简单讲讲AOP和IOC

    IOC(控制反转)

    • 核心思想是将对象的创建和依赖关系的管理交给容器,而不是由程序代码直接控制。
    • 例如在传统编程中,对象 A 依赖对象 B 时,在 A 中需要手动 new 出 B。而在 IOC 容器中,只需要在配置文件或者注解中声明 A 需要 B,容器会自动完成 B 的实例化并注入到 A 中。

    AOP(面向切面编程)

    • 简单说,就是把一些业务逻辑中的相同的代码抽取到一个独立的模块中,让业务逻辑更加清爽。
    • 它允许将横切关注点(如日志记录、事务管理、安全检查等)从业务逻辑中分离出来。
    • 这些横切关注点被定义为切面(Aspect),在程序运行过程中,切面可以在特定的点(切点,Pointcut)切入到业务逻辑中执行相应的操作。例如,在执行所有业务方法前自动记录日志,无需在每个业务方法中添加日志代码。

    Spring用到了哪些设计模式?

    • 工厂模式 : Spring 容器本质是一个大工厂,使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
    • 代理模式 : Spring AOP 功能功能就是通过代理模式来实现的,分为动态代理和静态代理。
    • 单例模式 : Spring 中的 Bean 默认都是单例的,这样有利于容器对Bean的管理。
    • 模板模式 : Spring 中 JdbcTemplate、RestTemplate 等以 Template结尾的对数据库、网络等等进行操作的模板类,就使用到了模板模式。
    • 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
    • 适配器模式 :Spring AOP 的增强或通知 (Advice) 使用到了适配器模式、Spring MVC 中也是用到了适配器模式适配 Controller。
    • 策略模式:Spring中有一个Resource接口,它的不同实现类,会根据不同的策略去访问资源。

    Spring 中的Bean是单例吗?

    在 Spring 中,默认情况下,Bean 是单例的(Singleton)。

    这意味着在整个应用程序上下文(ApplicationContext)中,对于一个特定的 Bean 定义,只会创建一个实例,并在每次需要该 Bean 时都返回这个唯一的实例。不过,Spring 也支持其他的作用域,例如:

    • prototype(原型):每次请求该 Bean 时都会创建一个新的实例。
    • request:在一次 HTTP 请求中,同一个 Bean 定义会创建一个实例,该实例在整个请求过程中有效。
    • session:在一个 HTTP Session 中,同一个 Bean 定义会创建一个实例,该实例在整个 Session 期间有效等。

    说一说什么是IOC?什么是DI?

    Java 是面向对象的编程语言,一个个实例对象相互合作组成了业务逻辑,原来,我们都是在代码里创建对象和对象的依赖。

    所谓的IOC(控制反转):就是由容器来负责控制对象的生命周期和对象间的关系。以前是我们想要什么,就自己创建什么,现在是我们需要什么,容器就给我们送来什么。

    也就是说,控制对象生命周期的不再是引用它的对象,而是容器。对具体对象,以前是它控制其它对象,现在所有对象都被容器控制,所以这就叫控制反转

    DI(依赖注入):指的是容器在实例化对象的时候把它依赖的类注入给它。有的说法IOC和DI是一回事,有的说法是IOC是思想,DI是IOC的实现。

    为什么使用IOC?

    最主要的是两个字解耦,硬编码会造成对象间的过度耦合,使用IOC之后,我们可以不用关心对象间的依赖,专心开发应用就行。

    能简单说一下Spring IOC的实现机制吗?

    1. 当应用程序启动时,Spring容器首先会进行初始化。容器通过读取配置信息(可以是XML、注解、Java配置等)来了解需要创建哪些对象以及它们之间的依赖关系。
    2. 容器会将这些配置信息解析成Bean定义(BeanDefinition),这些定义包含了类的名称、属性设置、构造器参数、依赖关系等元数据,并将Bean定义被注册到BeanFactory中。
    3. 当容器启动完成后,如果某个Bean被请求(例如通过getBean()方法),容器会使用反射机制来调用类的构造器创建对象实例。通过构造器注入或设值注入(Setter注入)的方式,将依赖的其它Bean注入到当前Bean中。
    4. 一旦Bean被创建,Spring容器还会负责管理其生命周期,包括调用初始化方法和销毁方法。

    Spring Bean的生命周期吗?

    Spring IOC 中Bean的生命周期大致分为四个阶段:实例化(Instantiation)、属性赋值(Populate)、初始化(Initialization)、销毁(Destruction)。

    • 实例化:第 1 步,实例化一个 Bean 对象
    • 属性赋值:第 2 步,为 Bean 设置相关属性和依赖
    • 初始化:初始化的阶段的步骤比较多,5、6步是真正的初始化,第 3、4 步为在初始化前执行,第 7 步在初始化后执行,初始化完成之后,Bean就可以被使用了
    • 销毁:第 8~10步,第8步其实也可以算到销毁阶段,但不是真正意义上的销毁,而是先在使用前注册了销毁的相关调用接口,为了后面第9、10步真正销毁 Bean 时再执行相应的方法
      ![[Pasted image 20241001161908.png]]

    有哪些依赖注入的方法?

    1. 构造方法注入: 通过调用类的构造方法,将接口实现类通过构造方法变量传入
    2. 属性注入: 通过Setter方法完成调用类所需依赖的注入
    3. 工厂方法注入

    Spring有哪些自动装配的方法?

    Spring IOC容器知道所有Bean的配置信息,此外,通过Java反射机制还可以获知实现类的结构信息,如构造方法的结构、属性等信息。掌握所有Bean的这些信息后,Spring IOC容器就可以按照某种规则对容器中的Bean进行自动装配,而无须通过显式的方式进行依赖配置。

    Spring提供了4种自动装配类型:ByName,ByType,constructor构造器,autodetect自动

    • byName:根据名称进行自动匹配,假设Boss又一个名为car的属性,如果容器中刚好有一个名为car的bean,Spring就会自动将其装配给Boss的car属性
    • byType:根据类型进行自动匹配,假设Boss有一个Car类型的属性,如果容器中刚好有一个Car类型的Bean,Spring就会自动将其装配给Boss这个属性
    • constructor:与 byType类似, 只不过它是针对构造函数注入而言的。如果Boss有一个构造函数,构造函数包含一个Car类型的入参,如果容器中有一个Car类型的Bean,则Spring将自动把这个Bean作为Boss构造函数的入参;如果容器中没有找到和构造函数入参匹配类型的Bean,则Spring将抛出异常。
    • autodetect:根据Bean的自省机制决定采用byType还是constructor进行自动装配,如果Bean提供了默认的构造函数,则采用byType,否则采用constructor。

    🌟 Bean的循环依赖?

    ![[Pasted image 20241001163331.png]]
    Spring 循环依赖:简单说就是自己依赖自己,或者和别的Bean相互依赖。
    只有单例的Bean才存在循环依赖的情况。

    如何解决Spring Bean的循环依赖问题?

    1. 开发人员做好设计,别让Bean循环依赖。(最有效)
    2. 可以修改注入方式。Spring 无法直接解决通过构造器注入产生的循环依赖。可以将构造器注入改为属性注入(或者setter 方法注入)。属性注入允许 Bean 先被创建,然后再注入依赖,这样 Spring 就有机会处理循环依赖。
    3. 对于上述属性注入导致的循环依赖,主要是采用三级缓存机制来解决的。

    一级缓存存放初始化好的单例 Bean 的缓存;二级缓存存放的是早期的单例 Bean,即已经实例化但未完成属性注入;三级缓存存放的是对象工厂,用于创建早期的单例 Bean 实例。
    单例Bean初始化完成,要经历三步:实例化:属性赋值以及初始化。
    在 Spring 容器创建 Bean A 时,先实例化 Bean A,然后发现 Bean A 依赖 Bean B。此时开始创建 Bean B,在创建 Bean B 时又发现 Bean B 依赖 Bean A。由于属性注入的循环依赖,Spring 会利用三级缓存机制。首先,Bean A 的实例化后会将创建 Bean A 实例的工厂放入三级缓存,当 Bean B 需要依赖 Bean A 时,从三级缓存中的工厂创建早期 Bean A 实例放入二级缓存,然后 Bean B 就可以获取这个早期实例完成自己的创建,最后 Bean A 再从二级缓存中获取 Bean B 的实例完成自己的创建并最终放入一级缓存。

    AOP和面向对象的区别?能取代面向对象吗?

    AOP(面向切面编程)与面向对象编程(OOP)是两种不同的编程范式,它们各自有独特的特点和应用场景。

    AOP与OOP的区别:

    1. 核心关注点
      • OOP:侧重于对象和类的封装,继承和多态性,关注将行为和数据封装成对象。
      • AOP:侧重于跨越多个点的横向关注点,如日志、事务、安全等,它将这些关注点从业务逻辑中分离出来,通过“横切”来实现。
    2. 代码组织
      • OOP:通过类和对象组织代码,以模块化和层次结构的方式组织。
      • AOP:通过切面(Aspect)组织代码,这些切面可以在不修改原有代码的基础上,增加额外的行为。
    3. 目的
      • OOP:主要目的是为了更好地管理和维护代码,提高代码的复用性和可维护性。
      • AOP:主要目的是将那些与业务逻辑无关,但处处都需要关注的功能(如日志、事务管理)从业务逻辑中分离出来,达到解耦的目的。

    AOP 不能 完全取代面向对象编程,原因如下:

    1. 关注点不同:AOP关注的是跨多个模块的横向关注点,而OOP关注的是对象的封装和行为。两者解决的问题域不同。
    2. 互补关系:AOP通常被认为是OOP的补充,而不是替代品。它们可以一起使用,使得某些复杂的编程问题得到更优雅的解决。
    3. 适用场景:OOP适用于大多数的业务逻辑处理,而AOP适用于那些横切关注点的处理,如日志、事务、安全等。
    4. 编程习惯:面向对象编程已经被广泛接受和使用,是大多数程序员熟悉的编程范式,而AOP相对较为特殊,只在特定的场景下使用。

    有在实际编程中使用过AOP吗?

    在实际编程中,AOP(面向切面编程)常用于处理横切关注点,例如日志记录、事务管理、权限验证、缓存等。以下是一个使用AOP进行日志记录的简单例子:
    场景描述:
    假设我们有一个用户服务(UserService),它提供了一个注册用户的方法(registerUser)。我们希望在用户注册成功后记录一条日志。
    实现步骤:

    1. 定义切面:创建一个切面类,用于定义切点和通知(Advice)。
    2. 定义切点:在切面类中定义一个切点,用于指定哪些方法会被拦截。
    3. 实现通知:在切面类中实现通知逻辑,例如前置通知、后置通知、环绕通知等。
      以下是具体的代码实现:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      @Aspect
      @Component
      public class LoggingAspect {
      // 定义切点,拦截 UserService 的 registerUser 方法
      @Pointcut("execution(* UserService.registerUser(..))")
      public void registerUserPointcut() {
      }
      // 前置通知,在方法执行前执行
      @Before("registerUserPointcut()")
      public void beforeRegisterUser() {
      // 实现日志记录的逻辑
      System.out.println("User registration attempt.");
      }
      }
      @Service
      public class UserService {
      // 注册用户的方法
      public void registerUser(User user) {
      // 用户注册的业务逻辑
      System.out.println("Registering user: " + user.getUsername());
      // 假设用户注册成功
      }
      }
      在这个例子中:
    • LoggingAspect 类是一个切面,它使用 @Aspect 注解标记。
    • registerUserPointcut 方法定义了一个切点,它使用 @Pointcut 注解来指定拦截 UserService 类的 registerUser 方法。
    • beforeRegisterUser 方法是一个前置通知,它使用 @Before 注解,这意味着它在 registerUser 方法执行之前执行。在这个方法中,我们简单地打印了一条日志信息。
      UserServiceregisterUser 方法被调用时,由于AOP的配置,beforeRegisterUser 方法会在 registerUser 方法执行之前自动被调用,从而实现了日志记录的功能。
      这个例子展示了AOP如何在不修改原有业务逻辑代码的情况下,通过声明式的方式添加额外的功能,从而实现了关注点的分离。

    Spring 事务?

    Spring框架中的事务管理是它最强大的功能之一,它允许开发者以声明式的方式管理事务,而不必手动编码处理事务的开启、提交、回滚等复杂逻辑。以下是Spring事务的基本概念和简单讲解:

    基本概念:

    1. 事务:事务指的是一系列操作,这些操作要么全部成功执行,要么全部失败回滚,保证数据的一致性。
    2. 事务属性
      • 传播行为(Propagation):定义了事务方法和外部调用方法之间的交互方式。
      • 隔离级别(Isolation):定义了一个事务可能受其他并发事务影响的程度。
      • 只读状态(Read Only):表示事务是否只读取数据,不更新数据。
      • 超时时间(Timeout):定义了事务在强制回滚之前可以占用的时间。
      • 回滚规则(Rollback Rule):定义了哪些异常会导致事务回滚。

    事务管理:
    Spring提供了两种事务管理方式:

    1. 编程式事务管理:通过编码的方式手动管理事务的开启、提交和回滚。
    2. 声明式事务管理:通过配置(如使用XML或注解)来管理事务,这是Spring推荐的方式,因为它更简单,也更易于维护。

    声明式事务管理:
    声明式事务管理通常使用以下几种方式:

    • XML配置:在Spring配置文件中定义事务的属性,并通过AOP(面向切面编程)将事务切面织入到目标方法中。
    • 注解配置:在类或方法上使用@Transactional注解来声明事务的属性。

    事务的传播行为:
    Spring定义了以下几种传播行为:

    • REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
    • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
    • MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常。
    • REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。
    • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
    • NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
    • NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与REQUIRED类似的操作。

    Spring 的事务隔离级别?

    Spring的接口TransactionDefinition中定义了表示隔离级别的常量,当然其实主要还是对应数据库的事务隔离级别:

    1. ISOLATION_DEFAULT:使用后端数据库默认的隔离界别,MySQL 默认可重复读,Oracle 默认读已提交。
      1. ISOLATION_READ_UNCOMMITTED:读未提交
      2. ISOLATION_READ_COMMITTED:读已提交
      3. ISOLATION_REPEATABLE_READ:可重复读
      4. ISOLATION_SERIALIZABLE:串行化

    5.2 SpringBoot

    说说你对springboot的理解,以及他和spring的区别?

    Spring Boot是一个开源的Java框架,旨在简化Spring应用程序的创建和部署过程。可以让开发者能够更快地启动和运行Spring应用程序,减少项目搭建的复杂度。

    对Spring Boot的理解:

    1. 自动配置:Spring Boot利用“约定优于配置”的原则,自动配置Spring应用程序。它根据添加到项目中的jar依赖自动配置Spring框架,减少了手动编写和维护配置文件的需求。
    2. 独立运行:Spring Boot应用可以打包成一个独立的Jar包,包含所有的依赖项,可以直接运行,无需部署到外部的应用服务器。
    3. 内置服务器:Spring Boot内嵌了Tomcat、Jetty或Undertow等Servlet容器,使得开发者可以非常方便地运行和测试Web应用程序。

    Spring Boot与Spring的区别:

    1. 配置方式
      • Spring:在传统的Spring框架中,需要大量手动配置。例如,创建一个Spring MVC应用程序需要配置DispatcherServlet、数据库连接、事务管理等。
      • Spring Boot:自动配置是Spring Boot的核心特性之一,它能够根据类路径下的类、Bean和其他因素自动配置Spring应用。
    2. 依赖管理
      • Spring:需要开发者手动管理项目依赖,包括版本兼容性等问题。
      • Spring Boot:通过Starter依赖,简化了依赖管理。开发者只需添加对应的Starter,Spring Boot会自动处理相关的依赖和版本问题。
    3. 部署方式
      • Spring:通常需要部署到独立的应用服务器上,如Tomcat、Jetty等。
      • Spring Boot:可以创建独立运行的Jar包,内嵌了Servlet容器,可以直接运行。

    解释CORS原理,如何在前端/后端解决跨域问题?

    CORS(Cross-Origin Resource Sharing,跨源资源共享)是一种机制,它允许限制资源(例如网页上的不同源脚本)的Web应用程序,使得资源可以被其他源(域、方案或端口)的Web应用程序访问。

    CORS原理:

    1. 同源策略:Web浏览器的同源策略限制了从一个源加载的文档或脚本如何与另一个源的资源进行交互。这是一个重要的安全限制,防止恶意网站读取另一个网站的数据。
    2. 由于目前主流的web开发方式都是前后端分离的,前端和后端程序往往不在同一个服务器上,这时候就需要解决跨域资源共享问题。
    3. CORS的工作方式:当一个资源从与它不同源的另一个源请求时,浏览器会自动发送一个“预检”请求(preflight request)到服务器,以确定服务器是否允许该请求。如果服务器批准了请求,实际的HTTP请求(如GET或POST)才会发送。
    4. HTTP头部:CORS通过HTTP头部字段来工作。以下是几个关键的HTTP头部字段:
      • **Origin**:发送请求的源(协议+域名+端口)。
      • **Access-Control-Allow-Origin**:服务器响应头,指示哪些网站可以访问资源。
      • **Access-Control-Allow-Methods**:服务器响应头,指示哪些HTTP方法可以使用。
      • **Access-Control-Allow-Headers**:服务器响应头,指示哪些HTTP头部可以用来发起请求。

    前端解决跨域问题:

    1. JSONP:通过<script>标签发送GET请求,服务器返回一个包装在函数调用中的JSON数据,从而绕过同源策略。
    2. 代理服务器:在前端使用代理服务器,将请求发送到同源的后端代理服务,由代理服务转发请求到目标服务器。 主要方式
    3. CORS插件:某些浏览器插件可以修改HTTP头部,允许跨域请求。

    后端解决跨域问题:
    1. 设置CORS响应头:在服务器响应中添加适当的CORS头部,允许特定的源或所有源访问资源。
    2. 过滤器:创建一个过滤器来处理跨域请求,并设置相应的CORS头部。
    3. 使用框架支持:某些Web框架提供了CORS支持,例如Spring框架中的@CrossOrigin注解或配置。

    SpringBoot常用注解和作用

    1. @SpringBootApplication

    • 这是一个组合注解,包含了@Configuration@EnableAutoConfiguration@ComponentScan
    • 作用:标记一个类是 Spring Boot 应用的主配置类,开启 Spring Boot 的各项特性。
      2. @RestController
    • @Controller@ResponseBody组合而成。
    • 作用:用于定义 RESTful Web 服务,类中的所有方法默认返回 JSON 格式的数据。
      3. @RequestMapping
    • 可以用于类和方法上。
    • 作用:用于映射 HTTP 请求的 URL 到特定的处理方法上,支持指定请求方法(GET、POST 等)。
      4. @GetMapping、@PostMapping 等(组合注解)
    • 例如@GetMapping@RequestMapping(method = RequestMethod.GET)的缩写。
    • 作用:更简洁地定义针对特定 HTTP 方法的请求映射。
      5. @Autowired
    • 作用:自动装配,由 Spring 容器根据类型匹配自动为属性、构造函数、方法参数等注入依赖的对象。
      6. @Service
    • 作用:标注在业务逻辑层的类上,表明这个类是一个服务组件,被 Spring 容器管理。
      7. @Repository
    • 作用:用于标注数据访问层(如数据库操作相关)的类,使这些类被 Spring 容器管理,同时可以进行一些数据访问相关的异常转换。
      8. @Value
    • 作用:用于从配置文件(如 application.properties 或 application.yml)中读取配置值,并注入到相应的变量中。

    注解的本质是什么?

    在 Java 中,注解(Annotation)的本质是一种元数据(metadata),它提供了关于程序元素(类、方法、变量等)的额外信息。
    一、元数据的形式
    注解就像是给程序元素贴上的标签,用于描述这些元素的特性、行为或用途。它以一种结构化的方式存储信息,这些信息可以在编译时、运行时被读取和处理。
    例如,@Override注解用于标识一个方法是重写了父类的方法。它向编译器提供了一个明确的信号,帮助编译器进行类型检查和错误检测。
    二、编译时处理
    在编译阶段,编译器可以根据注解来进行各种检查和优化。例如,@SuppressWarnings注解可以告诉编译器忽略特定类型的警告,避免不必要的编译错误信息。
    编译器也可以根据注解生成额外的代码。例如,Lombok 库中的@Data注解可以自动为类生成 getter、setter、equals、hashCode 和 toString 等方法,大大减少了手动编写这些方法的工作量。
    三、运行时处理
    在运行时,Java 虚拟机(JVM)可以通过反射机制读取注解信息,并根据这些信息进行动态的行为调整。
    例如,Spring 框架广泛使用注解来进行依赖注入、事务管理等功能。@Autowired注解用于自动装配依赖的对象,Spring 在运行时会根据这个注解找到合适的 Bean 并注入到相应的位置。
    四、自定义注解
    开发人员还可以自定义注解来满足特定的需求。自定义注解可以包含各种属性,用于存储特定的信息。
    例如,可以定义一个@LogExecutionTime注解,用于记录方法的执行时间。在运行时,可以通过反射读取这个注解,并在方法执行前后记录时间戳,计算出方法的执行时间。
    总之,注解的本质是一种元数据,它为程序元素提供了额外的描述信息,使得编译器和运行时环境可以根据这些信息进行各种处理和行为调整。注解的使用大大提高了代码的可读性、可维护性和可扩展性。

    @Autowire 和 @Resource的区别

    以下是 @Autowired 和 @Resource 的区别:
    来源方面

    • @Autowired 是 Spring 框架提供的注解。
    • @Resource 是 Java 标准库(JSR - 250)中的注解。
      注入方式
    • @Autowired:
      • 默认按类型(byType)进行装配。如果一个接口有多个实现类,需要配合 @Qualifier 注解指定具体的实现类的名称来完成注入。
      • 也可以用于构造函数、方法和属性上。
    • @Resource:
      • 默认按名称(byName)进行装配,通过指定 name 属性来匹配 Bean 的名称。
      • 主要用于字段、setter 方法上。
        对非 Spring 管理对象的支持
    • @Resource 可以用于非 Spring 管理的对象(如 JNDI 资源)。
    • @Autowired 只能用于 Spring 管理的 Bean。

    SpringBoot的启动流程

    收集配置,run,运行环境,上下文,Bean

    SpringBoot的启动流程大致分为两个阶段:创建 SpringApplication实例运行 run 方法

    1. 当创建SpringApplication实例时,可以传入不同的参数。如果不传入任何参数,它会自动从当前类路径下查找主配置类(通常是带有@SpringBootApplication注解的类);也会收集多种配置源,包括应用的配置文件(如application.propertiesapplication.yml)、命令行参数等。
    2. 接着就是运行run方法。在run方法启动时,首先会创建并注册一系列的启动监听器(SpringApplicationRunListeners)。这些监听器会在启动的不同阶段被触发,例如在启动前、启动过程中以及启动完成后。
    3. 接着会准备应用的运行环境(ConfigurableEnvironment)。这个环境对象包含了应用的配置信息,如系统属性、环境变量以及配置文件中的属性等。
    4. 根据环境信息创建应用上下文(ApplicationContext)。Spring Boot 默认会根据类路径中的依赖和配置创建合适的应用上下文。
    5. 一旦应用上下文创建完成,就会调用refresh方法来刷新 Spring 容器。在刷新过程中,会执行一系列操作,如解析配置类、创建和注入 Bean、处理 Bean 的生命周期方法等。
    6. 之后就可以发布应用上下文就绪并返回。这样启动就结束了。
      ![[Pasted image 20241002094517.png]]

    SpringBoot有哪些传入配置的方法?(及覆盖顺序)

    (优先级依次降低,即上面的可以覆盖下面的)

    1. 命令行参数: 可以在启动应用时直接在命令行中添加参数,例如:java -jar myapp.jar --server.port = 8081 具有最高的优先级。这意味着如果命令行中设置了某个属性的值,它将覆盖其他来源中相同属性的值。
    2. Java系统属性:可以在启动应用前设置 Java 系统属性,例如通过-D参数在命令行中设置:java -Dserver.port = 8082 -jar myapp.jar
    3. 环境变量:在操作系统中设置环境变量,例如在 Linux 系统中可以使用export SERVER_PORT = 8083来设置环境变量。SpringBoot 会自动读取环境变量并将其转换为对应的配置属性。例如,环境变量SERVER_PORT会被转换为server.port属性。
    4. 配置文件:application.properties,application.yml。在多个配置文件存在的情况下(例如在不同的配置文件中有相同属性的不同设置),外部的配置文件(如config目录下的配置文件)会覆盖内部的(如src/main/resources目录下的配置文件)。也可以在@SpringBootApplication注解中通过exclude属性来排除特定的自动配置类。
    5. 默认属性:Spring Boot 本身有一些默认的属性设置,这些属性是在框架内部定义的。例如,Spring Boot 默认的 HTTP 端口是 8080,如果没有任何其他配置来源设置server.port属性,就会使用这个默认值。

    SpringBoot自动配置原理了解吗?

    SpringBoot自动配置是指在应用程序启动时,SpringBoot根据classpath路径下的jar包自动配置应用程序所需的一系列bean和组件,从而减少开发者的配置工作,提高开发效率。

    1. 当 Spring Boot 应用启动时,@SpringBootApplication注解被解析。
      这是 Spring Boot 应用的核心注解,它是一个组合注解。其包含了@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan三个注解。@Configuration注解,表示该类是一个配置类,可以定义 Bean 等配置信息。@ComponentScan注解用于扫描指定包及其子包下的组件(如@Component@Service@Repository@Controller等注解标注的类),将它们注册为 Spring 容器中的 Bean。
      @EnableAutoConfiguration注解是实现自动配置的关键。它会触发 Spring Boot 的自动配置机制,根据项目中添加的依赖和项目的环境等信息,自动配置 Spring 应用上下文。
    2. @EnableAutoConfiguration触发自动配置机制,它会通过@Import(AutoConfigurationImportSelector.class)导入一个AutoConfigurationImportSelector类。
      这个类会从META - INF/spring.factories文件中查找EnableAutoConfiguration对应的配置类列表。这些自动配置类会根据项目中的条件(如类是否存在、属性是否设置等)来决定是否真正进行配置。
    3. 同时-Spring Boot 中使用了大量的条件注解来决定自动配置是否生效。根据各种条件注解对找到的自动配置类进行筛选,只有满足条件的自动配置类才会被应用,从而完成 Spring 应用上下文的自动配置,使得开发人员可以快速搭建基于 Spring 的应用,减少了大量的手动配置工作。

    SpringBoot 的自动装配流程

    Spring Boot 自动装配是一种机制,它能够根据项目中添加的依赖自动配置 Spring 应用程序。这意味着开发者无需手动编写大量的配置代码,Spring Boot 会在运行时根据类路径中的依赖关系自动进行必要的配置,使得应用能够快速启动和运行。
    一、启动应用
    当你启动 Spring Boot 应用程序时,首先会加载主配置类(通常是带有@SpringBootApplication注解的类)。这个注解开启了自动配置的功能。
    二、扫描类路径
    Spring Boot 会扫描类路径下的所有 JAR 包,查找包含META-INF/spring.factories文件的库。这个文件中定义了各种自动配置类。
    三、加载自动配置类
    根据应用程序的依赖和环境条件,Spring Boot 会选择并加载相应的自动配置类。这些自动配置类通常包含了对特定技术栈(如数据库连接、Web 框架等)的配置。
    四、创建 Bean
    自动配置类会根据配置创建相应的 Bean,并将它们注册到 Spring 应用程序上下文(ApplicationContext)中。这些 Bean 可以是服务类、数据访问对象、控制器等。
    五、注入依赖
    一旦 Bean 被创建并注册到应用程序上下文中,Spring 会自动将依赖注入到需要它们的 Bean 中。这是通过依赖注入(Dependency Injection)机制实现的,例如构造函数注入、属性注入等。
    六、应用程序运行
    在完成自动装配后,应用程序就可以正常运行了。你可以使用自动装配的 Bean 来实现业务逻辑,而无需手动创建和配置它们。
    总之,Spring Boot 的自动装配流程通过扫描类路径、加载自动配置类、创建 Bean 和注入依赖等步骤,实现了快速、便捷的开发体验,大大减少了开发人员的配置工作。

    自动配置的方式

    一、基于依赖的自动配置

    1. 当你在项目中添加特定的依赖时,Spring Boot 会根据这些依赖自动进行相应的配置。
      • 例如,添加 spring-boot-starter-web 依赖后,Spring Boot 会自动配置 Spring MVC 和嵌入式 Servlet 容器等组件,使你的应用成为一个 Web 应用。
      • 如果添加了数据库相关的依赖,如 spring-boot-starter-data-jpa 和对应的数据库驱动,Spring Boot 会自动配置数据源和 JPA 相关的组件。

    二、使用 @EnableAutoConfiguration 注解

    1. 在 Spring Boot 应用的主配置类上通常会添加 @EnableAutoConfiguration 注解来启用自动配置功能。
      • 这个注解会触发自动配置的过程,导入 AutoConfigurationImportSelector,它负责收集所有符合条件的自动配置类。

    三、通过条件注解进行自动配置

    1. Spring Boot 中的自动配置类通常使用条件注解来决定是否应该被应用。
      • 例如,@ConditionalOnClass 注解用于检查类路径中是否存在特定的类。如果存在指定的类,对应的自动配置类才会生效。
      • @ConditionalOnProperty 注解根据配置文件中的属性值来决定是否进行自动配置。可以在配置文件中设置特定的属性,当属性满足条件时,自动配置才会被应用。

    四、自定义自动配置

    1. 可以创建自己的自动配置类来满足特定的需求。
      • 使用 @Configuration 注解标记自定义的配置类,表示这是一个 Spring 配置类。
      • 在自定义配置类中,可以使用条件注解来控制自动配置的条件,就像 Spring Boot 内置的自动配置类一样。
      • 还可以通过实现 ImportBeanDefinitionRegistrar 或 BeanFactoryPostProcessor 接口来手动注册 bean 定义或对 bean 工厂进行后处理,以实现更复杂的自动配置逻辑。

    五、使用配置文件调整自动配置

    1. 可以通过 application.properties 或 application.yml 等配置文件来调整自动配置的行为。
      • 例如,可以设置特定的属性来覆盖自动配置的默认值,或者禁用某些自动配置。
      • 通过配置文件,可以对自动配置进行细粒度的调整,以满足不同环境下的需求。

    如何实现基于注解驱动的Spring Boot Starter?

    1. 在 Java 中,使用 @interface 关键字创建自定义注解。将需要的操作写进注解中。
    2. 编写配置文件,定义属性配置的前缀。
    3. 通过@Configuration 注解创建自动配置类,/resources/META-INF/spring.factories文件中添加自动配置类路径。这样springBoot在启动时候就能扫描到这个类并启用。

    Spring Cloud

    SpringCloud是Spring官方推出的微服务治理框架。
    ![[Pasted image 20241002112753.png]]

    什么是微服务?

    • 微服务架构是一种架构模式,提倡将单一应用程序划分成一组小的服务,服务之间相互协调,互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务之间采用轻量级的通信机制(如HTTP或Dubbo)互相协作,每个服务都围绕着具体的业务进行构建,并且能够被独立的部署到生产环境中,另外,应尽量避免统一的,集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具(如Maven)对其进行构建。

    • 微服务化的核心就是将传统的一站式应用,根据业务拆分成一个一个的服务,彻底地去耦合,每一个微服务提供单个业务功能的服务,一个服务做一件事情,从技术角度看就是一种小而独立的处理过程,类似进程的概念,能够自行单独启动或销毁,拥有自己独立的数据库。

    微服务架构主要要解决哪些问题?

    • 服务很多,客户端怎么访问,如何提供对外网关?
    • 这么多服务,服务之间如何通信? HTTP还是RPC?
    • 这么多服务,如何治理? 服务的注册和发现。
    • 服务挂了怎么办?熔断机制。

    5.3 Mybatis

    #{} 和 ${} 的区别是什么?

    • ${}是 Properties 文件中的变量占位符,它可以用于标签属性值和 sql 内部,属于原样文本替换,可以替换任意内容,比如${driver}会被原样替换为com.mysql.jdbc. Driver
      一个示例:根据参数按任意字段排序:
      1
      select * from users order by ${orderCols}
      orderCols可以是 namename descname,sex asc等,实现灵活的排序。
    • #{}是 sql 的参数占位符,MyBatis 会将 sql 中的#{}替换为? 号,在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的? 号占位符设置参数值,比如 ps.setInt(0, parameterValue),#{item.name} 的取值方式为使用反射从参数对象中获取 item 对象的 name 属性值,相当于 param.getItem().getName()

    6 常用中间件

    6.1 Redis

    Redis基础

    什么是Redis?

    1. Redis是一种基于键值对(key-value)存储的非关系型数据库。
    2. Redis的键值对支持string(字符串)、hash(哈希)、 list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、 HyperLogLog、GEO(地理信息定位)等多种数据结构,能满足不同的应用场景需求。
    3. 而且因为Redis会将所有数据都存放在内存中,所以它的读写性能非常出色。
    4. 不仅如此,Redis还可以将内存的数据利用快照和日志的形式保存到硬盘上,这样在发生类似断电或者机器故障的时候,内存中的数据不会“丢失”。

    为什么使用Redis?

    1. 快速读写:基于内存的,因此可以非常快速地读写数据。
    2. 支持多种数据结构:包括字符串、哈希、列表、集合和有序集合等,可以满足不同的需求。
    3. 支持持久化:支持将数据持久化到磁盘,以便在服务宕机后能够恢复数据。
    4. 支持集群部署:可以横向扩展,提高系统的可伸缩性和容错性

    Redis的数据结构

    常用:字符串String,列表List,集合Set,有序集合 SortedSet,哈希Hash
    消息队列 Sream,地理空间 Geospatital,HyperLogLog(基数统计),位图 Bitmap,位域 Bitfield
    Redis 的 String 字符串是用 SDS 数据结构存储的。

    Redis的使用场景(场景题)

    1. 热点数据的缓存
      缓存是Redis最常见的应用场景,之所有这么使用,主要是因为Redis读写性能优异。
    2. 限时业务的运用
      redis中可以使用expire命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。
    3. 计数器相关问题
      redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。
    4. 分布式锁
      这个主要利用redis的setnx命令进行,setnx:”set if not exists”就是如果不存在则成功设置缓存同时返回1,否则返回0 。
    5. 延时操作
      比如在订单生产后我们占用了库存,10分钟后去检验用户是否真正购买,如果没有购买将该单据设置无效,同时还原库存。 由于redis自2.8.0之后版本提供Keyspace Notifications功能,允许客户订阅Pub/Sub频道,以便以某种方式接收影响Redis数据集的事件。 所以我们对于上面的需求就可以用以下解决方案,我们在订单生产时,设置一个key,同时设置10分钟后过期, 我们在后台实现一个监听器,监听key的实效,监听到key失效时将后续逻辑加上。
      当然我们也可以利用rabbitmq、activemq等消息中间件的延迟队列服务实现该需求。
    6. 排行榜相关问题
      关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的SortedSet进行热点数据的排序。
    7. 点赞、好友等相互关系的存储
      Redis 利用集合的一些命令,比如求交集、并集、差集等。在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。
    8. 简单队列
      由于Redis有list push和list pop这样的命令,所以能够很方便的执行队列操作。

    关系型数据库和非关系型数据库的区别?

    关系型数据库(RDBMS)和非关系型数据库(NoSQL),它们在数据存储、查询方式、扩展性、一致性等方面有着明显的区别。以下是一些主要的不同点:

    1. 数据模型
      • 关系型数据库:基于关系模型,数据以表格形式存储,表与表之间可以建立关系,如通过外键进行关联。
      • 非关系型数据库:不依赖传统的关系模型,数据存储结构多样,可以是文档、键值对、或图数据库还有(最近比较火的)向量数据库。可以针对不同的业务选择不同的非关系型数据库,不同的数据结构。
    2. 查询语言
      • 关系型数据库:使用SQL(结构化查询语言)进行数据查询和操作。
      • 非关系型数据库:查询语言各不相同,根据存储的数据模型定制,没有统一的标准。
    3. 事务处理
      • 关系型数据库:支持ACID(原子性、一致性、隔离性、持久性)事务,保证数据的完整性和可靠性。
      • 非关系型数据库:有些支持BASE(基本可用、软状态、最终一致性)原则,更多关注可扩展性和性能,有些支持有限的事务处理。
    4. 扩展性
      • 关系型数据库:垂直扩展(通过增加单个服务器的资源)为主,水平扩展(通过增加服务器数量)较为复杂。
      • 非关系型数据库:水平扩展(通过增加服务器数量)为主,设计上更容易扩展,适合大规模数据的处理。
    5. 适用场景
      • 关系型数据库:适用于需要复杂查询、事务性高、数据关联性强的应用场景,如金融、会计系统。
      • 非关系型数据库:适用于大数据应用、高并发读写、灵活的数据模型需求,如社交媒体、物联网。
    6. 性能
      • 关系型数据库:在处理复杂查询和事务时性能较高,但在大规模数据集上的读写性能可能受限。
      • 非关系型数据库:在处理大量数据的高并发读写时性能较好,但在事务处理和复杂查询方面可能较弱。

    Redis常见的操作

    基本操作

    1
    2
    3
    redis-server   # 启动Redis
    redis-cli
    telnet 127.0.0.1 # 连接到Redis

    字符串Strings

    1
    2
    3
    4
    5
    6
    SET key value
    SETNX key value # 只有当key不存在时,才设置为value
    GET key value
    APPEND key value # 存在追加不存在设置
    INCR/DECR key # 将key存储的数字+1/-1(原子)
    DEL key

    列表List (双端队列)

    1
    2
    3
    4
    5
    LPUSH key element # 添加到左侧,不存在创建
    RPUSH key element
    LPUSX/RPUSX key element # 不存在才设置
    LPOP/RPOP key # 从左/右移除
    LINDEX key index

    集合Set

    1
    2
    3
    4
    5
    SADD key member
    SREM key member # 清除member,不存在则忽略
    SMEMBERS key # 返回集合中的所有成员
    SISMEMBER key member # 判断member是否为key的成员
    SINTER/SUNION/SDIFF # 交并差集

    有序集合 SortedSet

    1
    2
    3
    4
    5
    6
    7
    ZADD key score member
    ZREM key member
    ZSCORE key member # 返回member的分数值
    ZINCRBY key increment member # 给member的分量加上一个指increment
    ZREVRANK key member # 返回排名
    ZREMRANGEBYSCORE key min max # 移除指定分数区间的成员
    ZREMRANGEBYRANk key start stop # 移除指定排名区间的成员

    哈希 Hashes

    1
    2
    3
    4
    5
    HSET key field value # 将key中filed的值设置为value
    HSETNX key field value
    HGET key field
    HEXISTS key field
    HDEL key field

    Redis的命令是原子的吗?如何理解

    Redis 的命令在一定条件下是原子性的。
    一、原子性的含义
    原子性是指一个操作或者一系列操作要么全部执行成功,要么全部不执行,不会出现部分执行成功、部分执行失败的情况。
    二、Redis 命令的原子性表现

    1. 单个命令的原子性
      • 许多 Redis 命令在执行时是原子性的。例如,对一个键进行 SET 操作,设置一个键的值,这个操作是不可分割的,要么成功设置了值,要么没有进行任何操作,不会出现设置了一半值的情况。
      • INCR 命令对一个整数键的值进行自增操作,这个过程也是原子性的,不会被其他命令打断。
    2. 事务中的原子性
      • Redis 支持事务,使用 MULTI、EXEC、DISCARD 等命令来实现。在一个事务中,可以包含多个命令,当使用 MULTI 命令开启一个事务后,Redis 会将后续的命令放入一个队列中,直到执行 EXEC 命令时,才会一次性地、原子性地执行队列中的所有命令。如果在事务执行过程中出现错误,Redis 会回滚到事务开始前的状态,保证事务的原子性。

    三、理解 Redis 命令原子性的重要性

    1. 数据一致性
      • 原子性确保了在并发环境下,对数据的操作不会出现不一致的情况。如果一个操作不是原子性的,那么在多个客户端同时对同一数据进行操作时,可能会出现数据混乱的问题。而 Redis 的原子性命令和事务可以保证数据的一致性。
    2. 简化并发控制
      • 由于 Redis 命令的原子性,在很多情况下可以简化并发控制的实现。开发者不需要担心多个线程或客户端同时操作数据时可能出现的竞争条件和数据不一致问题,可以更专注于业务逻辑的实现。
    3. 提高性能
      • 原子性操作通常比非原子性操作更高效,因为它们不需要进行复杂的同步和协调。Redis 的原子性命令可以快速地执行,提高了服务器的性能和响应速度。

    总之,Redis 的命令在一定条件下是原子性的,这为开发者提供了一种可靠的数据操作方式,保证了数据的一致性和系统的性能。

    Redis为什么快?

    Redis 能够高效处理并发请求主要基于以下原因:

    • 基于内存操作
      • Redis 所有的数据都存储在内存中,内存的读写速度远远快于磁盘,这使得数据的读写操作可以在极短的时间内完成。例如,从内存中读取一个数据的时间通常在纳秒级别,而从磁盘读取数据可能需要几毫秒甚至更长时间。
    • 高效的数据结构
      • Redis 内部设计了多种高效的数据结构,如简单动态字符串(SDS)、哈希表、跳表、整数集合等。这些数据结构针对不同的应用场景进行了优化,能够快速地进行数据的存储、查询和操作。/
    • 单线程避免了线程切换开销
      • 由于 Redis 的核心业务处理是单线程的,避免了多线程环境下频繁的线程上下文切换带来的额外开销。在多线程系统中,线程切换需要保存和恢复线程的执行上下文,包括寄存器的值、程序计数器等,这个过程需要消耗一定的时间和系统资源。而 Redis 单线程执行命令,就不存在这种线程切换的开销。
    • 非阻塞 I/O 机制
      • Redis 使用了非阻塞 I/O 多路复用机制,在 Linux 系统上,Redis就采用了epoll多路复用。这些机制允许单个线程同时处理多个网络连接的 I/O 事件,而不会被某个连接的 I/O 操作阻塞。

    Redis为什么早期选择单线程?

    因为Redis是基于内存的操作,CPU成为Redis的瓶颈的情况很少见,Redis的瓶颈最有可能是内存的大小或者网络限制。如果想要最大程度利用CPU,可以在一台机器上启动多个Redis实例。

    Redis 4.0 之后开始变成多线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 Key 的删除等等。

    Redis6.0使用多线程是怎么回事?

    Redis6.0的多线程是用多线程来处理数据的读写和协议解析,但是Redis执行命令还是单线程的。这样做的⽬的是因为Redis的性能瓶颈在于⽹络IO⽽⾮CPU,使⽤多线程能提升IO读写的效率,从⽽整体提⾼Redis的性能。
    ![[Pasted image 20240904113348.png]]

    Redis的数据结构

    常用: 字符串String,列表List,集合Set,有序集合 SortedSet,哈希Hash

    新: 消息队列 Sream,地理空间 GeospatitalHyperLogLog(基数统计),位图 Bitmap,位域 Bitfield

    Redis数据结构的底层实现

    Redis有动态字符串(sds)链表(list)字典(ht)跳跃表(skiplist)整数集合(intset)压缩列表(ziplist) 等底层数据结构。
    Redis并没有使用这些数据结构来直接实现键值对数据库,而是基于这些数据结构创建了一个对象系统,来表示所有的key-value。
    比如string就是基于long和简单动态字符串sds构建的;list是根据双端链表和压缩列表实现的。
    ![[Pasted image 20240911211039.png]]

    Redis 的 SDS(简单动态字符串) 和 C 中字符串相比有什么优势?

    C 语言使用了一个长度为 N+1 的字符数组来表示长度为 N 的字符串,并且字符数组最后一个元素总是 \0,这种简单的字符串表示方式 不符合 Redis 对字符串在安全性、效率以及功能方面的要求。
    ![[Pasted image 20240911213901.png]]
    C语言字符串的问题:

    • 获取字符串长度复杂度高 :因为 C 不保存数组的长度,每次都需要遍历一遍整个数组,时间复杂度为O(n);
    • 不能杜绝 缓冲区溢出/内存泄漏 的问题 : C字符串不记录自身长度带来的另外一个问题是容易造成缓存区溢出(buffer overflow),例如在字符串拼接的时候,新的
    • C 字符串 只能保存文本数据 → 因为 C 语言中的字符串必须符合某种编码(比如 ASCII),例如中间出现的 '\0' 可能会被判定为提前结束的字符串而识别不了;

    SDS如何解决?

    • 多增加 len 表示当前字符串的长度:这样就可以直接获取长度了,复杂度 O(1);
    • 自动扩展空间:当 SDS 需要对字符串进行修改时,首先借助于 lenalloc 检查空间是否满足修改所需的要求,如果空间不够的话,SDS 会自动扩展空间,避免了像 C 字符串操作中的溢出情况;
    • 有效降低内存分配次数:C 字符串在涉及增加或者清除操作时会改变底层数组的大小造成重新分配,SDS 使用了 空间预分配惰性空间释放 机制,简单理解就是每次在扩展时是成倍的多分配的,在缩容是也是先留着并不正式归还给 OS;
    • 二进制安全:C 语言字符串只能保存 ascii 码,对于图片、音频等信息无法保存,SDS 是二进制安全的,写入什么读取就是什么,不做任何过滤和限制;

    跳表有了解吗?

    跳跃表(skiplist)本质上就是一种多层的有序链表,通过在有序链表上建立多级索引以实现快速查找、插入和删除操作。其查找时间复杂度为O(logn),类似于二分查找,但基于单链表实现。跳表通过随机函数决定节点在各级索引的分布,动态调整索引层数,以平衡空间和时间复杂度。
    假设原始链表的大小为n,那么第一级索引大约有n/2个结点,第二级索引大约有n/4个结点;对应跳表的空间复杂度为O(n),插入、查找、删除的时间复杂度均为O(logN)
    ![[Pasted image 20240911211816.png]]

    压缩列表 ziplist

    压缩列表是 Redis 为了节约内存 而使用的一种数据结构,是由一系列特殊编码的连续内存快组成的顺序型数据结构。对于整数,它会以尽可能小的字节数来存储,比如可以用 1 个字节存储较小的整数,而不是固定使用 4 个字节或 8 个字节;对于字符串,它会根据字符串的实际长度进行存储,而不会浪费多余的空间。

    • 与普通链表相比:普通链表每个节点除了存储数据外,还需要额外的指针来指向下一个节点,而压缩列表不需要这些额外的指针空间,所以在元素数量较少且元素较小时,它比普通链表更节省空间。
    • 与数组相比:虽然数组也是连续存储,但数组的每个元素通常占用固定大小的空间,而压缩列表可以根据元素的实际情况灵活调整占用的空间大小。

    redis 持久化的方式(RDB, AOF)

    RDB: 在指定的时间间隔内将内存中的数据集快照写入磁盘,它恢复时是将快照文件直接读到内存里。
    手动触发–save:主线程备份;bgsave:fork子线程备份
    AOF: 以日志的形式来记录每个写操作(增量保存),将redis执行过的所有写指令记录下来(读操作不记录),只允追加文件但不可改写文件。
    AOF的工作流程操作:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load)
    混合模式: 内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。

    RDB的流程及优缺点

    Redis会单独创建(fork)一个子进程进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束后,再用这个临时文件替换上次持久化好的文件。
    ![[Pasted image 20240802190410.png]]

    • 优点
      • RDB文件是某个时间节点的快照,默认使用LZF算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;
      • Redis加载RDB文件恢复数据要远远快于AOF方式;
    • 缺点
      • RDB方式实时性不够,无法做到秒级的持久化;
      • 每次调用bgsave都需要fork子进程,fork子进程属于重量级操作,频繁执行成本较高;
      • RDB文件是二进制的,没有可读性,AOF文件在了解其结构的情况下可以手动修改或者补全;

    AOF持久化流程及优缺点

    以日志的形式来记录每个写操作(增量保存),将redis执行过的所有写指令记录下来(读操作不记录),只允追加文件但不可改写文件。
    redis启动之初会读取该文件重新构造数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

    持久化流程:

    • 客户端的请求写命令会被append追加到AOF缓冲区内
    • AOF缓冲区根据AOF持久化策略(always, everysec, no)将操作同步到磁盘的AOF文件中
    • AOF文件超过重写策略时,会对AOF文件进行重写,压缩AOF文件容量
    • redis服务重启时,会重新加载AOF文件中的写操作达到数据恢复的目的

    优点:

    • 实时性好,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。
    • 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
      缺点:
    1. AOF 文件比 RDB 文件大,且 恢复速度慢
    2. 数据集大 的时候,比 RDB 启动效率低

    RDB和AOF如何选择?

    • 一般来说, 如果想达到足以媲美数据库的 数据安全性,应该 同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
    • 如果 可以接受数分钟以内的数据丢失,那么可以 只使用 RDB 持久化
    • 有很多用户都只使用 AOF 持久化,但并不推荐这种方式,因为定时生成 RDB 快照(snapshot)非常便于进行数据备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,除此之外,使用 RDB 还可以避免 AOF 程序的 bug。
    • 如果只需要数据在服务器运行的时候存在,也可以不使用任何持久化方式。

    Redis如何进行数据恢复?

    ![[Pasted image 20241004112326.png]]

    • AOF持久化开启且存在AOF文件时,优先加载AOF文件。
    • AOF关闭或者AOF文件不存在时,加载RDB文件。
    • 加载AOF/RDB文件成功后,Redis启动成功。
    • AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。

    区分混合持久化:
    Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
    于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
    ![[Pasted image 20241004112958.png]]

    6.1.4 Redis高可用(主从复制、哨兵和集群)

    高可用–主从复制机制

    主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为 **主节点(master)**,后者称为 **从节点(slave)**。且数据的复制是 单向 的,只能由主节点到从节点。Redis 主从复制支持 主从同步从从同步 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。

    主从复制主要的作用?

    • 数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
    • 故障恢复: 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 *(实际上是一种服务的冗余)*。
    • 负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 _(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点)_,分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
    • 高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的 基础,因此说主从复制是 Redis 高可用的基础。

    Redis主从有几种常见的拓扑结构?

    一主一从、一主多从(星型)、树状主从结构

    1. 一主一从结构
      • 描述:这是最简单的拓扑结构,由一个主节点(Master)和一个从节点(Slave)组成。主节点负责处理所有的写操作,并将数据变更发送给从节点。从节点则主要负责接收主节点发送的数据,并提供读操作服务。
      • 应用场景:适用于对读写分离需求不太复杂,数据量和并发量相对较小的场景。例如,在小型的 Web 应用中,主节点处理数据写入,从节点分担一部分读请求,减轻主节点的读压力。
    2. 一主多从结构
      • 描述:由一个主节点和多个从节点构成。主节点负责处理写操作,多个从节点从主节点复制数据,可用于分担读负载。从节点可以分布在不同的服务器或网络环境中,从而提高系统的可用性和读性能。
      • 应用场景:在高并发的读操作场景下非常有用,如大型的内容分发网络(CDN)系统或热门的 Web 应用。多个从节点可以根据地理位置或网络负载均衡等因素,合理分担大量的读请求,而主节点专注于处理写操作。
    3. 树状结构(主 - 从 - 从结构)
      • 描述:主节点下有多个从节点,并且部分从节点又可以作为其他从节点的主节点,形成类似树状的结构。这种结构可以进一步扩展系统的读能力,同时在一定程度上减轻主节点的复制压力。
      • 应用场景:适用于大规模的集群部署,当需要在多个层次上扩展读性能时可以采用。例如,在企业级的大规模数据存储和分发系统中,通过多层的从节点扩展,可以覆盖更广泛的区域,满足不同地区用户的读请求需求。

    主从复制的原理?

    1. 保存主节点(master)信息 这一步只是保存主节点信息,保存主节点的ip和port。
    2. 主从建立连接 从节点(slave)发现新的主节点后,会尝试和主节点建立网络连接。
    3. 发送ping命令 连接建立成功后从节点发送ping请求进行首次通信,主要是检测主从之间网络套接字是否可用、主节点当前是否可接受处理命令。
    4. 权限验证 如果主节点要求密码验证,从节点必须正确的密码才能通过验证。
    5. 同步数据集 主从复制连接正常通信后,主节点会把持有的数据全部发送给从节点。
    6. 命令持续复制 接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。
      ![[Pasted image 20241004114316.png]]

    主从复制的数据同步方式?

    主要是全量复制和部分复制
    全量复制一般用于初次复制场景,Redis早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。(传递的持久化内容)
    ![[Pasted image 20241004114906.png]]
    部分复制部分复制主要是Redis针对全量复制的过高开销做出的一种优化措施, 使用psync{runId}{offset}命令实现。当从节点(slave)正在复制主节点 (master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向 主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。

    主从复制存在哪些问题?

    1. 一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预。 – 哨兵模式
    2. 主节点的存储(容量)和写能力受到单机的限制。 – 分布式(集群)

    高可用–哨兵模式

    主从复制存在一个问题,没法完成自动故障转移。所以我们需要一个方案来完成自动故障转移,它就是Redis Sentinel(哨兵)。

    1. 哨兵模式的定义与角色
      • 哨兵模式(Sentinel)是 Redis 的一种高可用性解决方案。在这种模式中,包含三个重要角色:
        • 哨兵(Sentinel):是一种特殊的 Redis 进程,它的主要职责是监控 Redis 主从节点的运行状态。一个哨兵可以同时监控多个主从节点集群。
        • 主节点(Master):负责处理所有的写操作,并将数据变更同步到从节点。
        • 从节点(Slave):从主节点复制数据,主要用于分担读操作负载。
    2. 监控功能
      • 哨兵会不断地向主节点、从节点发送心跳(ping)命令,以检测它们是否可达以及运行是否正常。如果在规定的时间内没有收到节点的响应,哨兵就会将该节点标记为下线状态。
      • 例如,哨兵可以配置为每隔 1 秒向主从节点发送 ping 命令,如果超过一定的超时时间(如 3 秒)没有收到节点的回复,就认为该节点可能出现故障。
    3. 故障自动切换机制(Failover)
      • 当主节点被哨兵判定为故障(主观下线和客观下线)后,哨兵会在从节点中选举出一个新的主节点。
      • 主观下线(Subjectively Down,SD):是指单个哨兵认为某个节点不可达。当一个哨兵检测到主节点没有响应时,它会将主节点标记为主观下线。
      • 客观下线(Objectively Down,OD):当多个哨兵(通常是配置的多数派)都认为主节点不可达时,主节点就会被标记为客观下线。此时,哨兵们会发起故障切换操作。
      • 在选举新主节点时,哨兵会根据一定的规则,如从节点的优先级、复制偏移量等因素进行选择。优先级高且复制偏移量最新的从节点往往更有机会被选举为新的主节点。选举出新主节点后,哨兵会通知其他从节点更新配置,将新主节点作为它们的主节点,从而实现故障自动切换,保障 Redis 服务的可用性。
    4. 通知功能
      • 哨兵可以将主从节点的状态变化情况通知给其他相关的客户端或者系统组件。例如,当发生主节点故障切换时,哨兵可以通知连接到 Redis 的应用程序,让它们更新连接的节点信息,以便继续正常使用 Redis 服务。

    哨兵模式的实现原理?

    哨兵模式是通过哨兵节点完成对数据节点的监控、下线、故障转移。
    ![[Pasted image 20241004115717.png]]
    哨兵结点有三个定时监控任务完成对各个节点发现和监控。

    • Redis Sentinel通过三个定时监控任务完成对各个节点发现和监控:
    1. 每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构
    2. 每隔2秒,每个Sentinel节点会向Redis数据节点的__sentinel__:hello 频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息
    3. 每隔1秒,每个Sentinel节点会向主节点、从节点、其余Sentinel节点发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达
    • 主观下线和客观下线主观下线就是哨兵节点认为某个节点有问题,客观下线就是超过一定数量的哨兵节点认为主节点有问题。

    领导者Sentinel节点选举了解吗?

    领导者哨兵节点选举: Sentinel节点之间会做一个领导者选举的工作,选出一个Sentinel节点作为领导者进行故障转移的工作。Redis使用了Raft算法实现领导者选举。

    • 每个在线的Sentinel节点都有资格成为领导者,当它确认主节点主观 下线时候,会向其他Sentinel节点发送sentinel is-master-down-by-addr命令, 要求将自己设置为领导者。
    • 收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的sentinel is-master-down-by-addr命令,将同意该请求,否则拒绝。
    • 如果该Sentinel节点发现自己的票数已经大于等于max(quorum, num(sentinels)/2+1),那么它将成为领导者。
    • 如果此过程没有选举出领导者,将进入下一次选举。

    哨兵模式的故障转移过程

    ![[Pasted image 20241004120315.png]]

    • 过滤:“不健康”(主观下线、断线)、5秒内没有回复过Sentinel节 点ping响应、与主节点失联超过down-after-milliseconds*10秒。
    • 选择slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。
    • 选择复制偏移量最大的从节点(复制的最完整),如果存在则返 回,不存在则继续。
    • 选择runid最小的从节点。

    Redis集群

    1. Redis 集群的基本概念
      • Redis 集群是 Redis 提供的分布式解决方案,旨在提供高可用性、数据分片和横向扩展能力。它由多个 Redis 节点组成,这些节点通过网络相互连接并协同工作。
      • 在 Redis 集群中,数据被自动分片(partition)存储在不同的节点上,每个节点负责一部分数据的存储和处理,这样可以突破单个 Redis 实例的内存限制,处理大量的数据。
    2. 数据分片机制
      • Redis 集群采用哈希槽(hash slot)的方式进行数据分片。总共定义了 16384 个哈希槽。
      • 当向 Redis 集群写入一个键值对时,首先根据键计算出对应的哈希槽,计算公式为 CRC16 (key) mod 16384。然后根据哈希槽的值将数据存储到对应的节点上。
      • 例如,一个键 “user:1” 经过计算后被分配到某个特定的哈希槽,如果这个哈希槽对应的是节点 A,那么这个键值对就会被存储在节点 A 上。
    3. 节点间的通信
      • 节点之间通过一种称为 Gossip 协议的通信方式来交换信息。Gossip 协议是一种去中心化的通信协议。
      • 每个节点会定期向其他部分节点发送消息,消息内容包括自身的状态(如节点是否正常、负责哪些哈希槽等)以及它所知道的其他节点的信息。通过这种方式,节点可以获取到整个集群的状态信息,例如哪个节点加入或离开了集群,哈希槽的分配是否有变化等。
    4. 高可用性与故障恢复
      • 当一个节点出现故障时,集群会进行故障转移(failover)操作。
      • 其他正常节点会检测到故障节点的状态变化,并且会重新分配故障节点所负责的哈希槽到其他正常节点上。如果故障节点是主节点,那么集群会从其对应的从节点(如果有)中选举出一个新的主节点来接管工作,以确保集群的正常运行和数据的可用性。
    5. 客户端交互
      • 客户端在与 Redis 集群交互时,需要特殊的支持。例如,Jedis 等 Redis 客户端需要能够识别 Redis 集群的拓扑结构,并且根据键计算哈希槽,从而将请求发送到正确的节点上。
      • 如果客户端请求的键所在的节点不可用,客户端需要能够处理这种情况,例如尝试重新发送请求到其他节点或者等待节点恢复。

    集群中的数据是如何分片的?

    节点取余分区,一致性哈希分区、虚拟槽分区

    1. 节点取余分区,非常好理解,使用特定的数据,比如Redis的键,或者用户ID之类,对响应的hash值取余:hash(key)%N,来确定数据映射到哪一个节点上。
      不过该方案最大的问题是,当节点数量变化时,如扩容或收缩节点,数据节点映射关 系需要重新计算,会导致数据的重新迁移。
    2. 一致性哈希分区:将整个 Hash 值空间组织成一个虚拟的圆环,然后将缓存节点的 IP 地址或者主机名做 Hash 取值后,放置在这个圆环上。当我们需要确定某一个 Key 需 要存取到哪个节点上的时候,先对这个 Key 做同样的 Hash 取值,确定在环上的位置,然后按照顺时针方向在环上“行走”,遇到的第一个缓存节点就是要访问的节点。
      但它还是存在问题
      • 缓存节点在圆环上分布不平均,会造成部分缓存节点的压力较大
      • 当某个节点故障时,这个节点所要承担的所有访问都会被顺移到另一个节点上,会对后面这个节点造成力。
    3. 【Redis实际使用】虚拟槽分区:这个方案 一致性哈希分区的基础上,引入了 虚拟节点 的概念。
      Redis 集群使用的便是该方案,其中的虚拟节点称为 槽(slot)。槽是介于数据和实际节点之间的虚拟概念,每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。
      ![[Pasted image 20241004134329.png]]
      在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点 之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有 4 个实际节点,假设为其分配 16 个槽(0-15);
    • 槽 0-3 位于 node1;4-7 位于 node2;以此类推….

    如果此时删除 node2,只需要将槽 4-7 重新分配即可,例如槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4,数据在其他节点的分布仍然较为均衡。

    集群的工作原理

    1. 设置节点Redis集群一般由多个节点组成,节点数量至少为6个才能保证组成完整高可用的集群。每个节点需要开启配置cluster-enabled yes,让Redis运行在集群模式下。
    2. 节点握手是指一批运行在集群模式下的节点通过Gossip协议彼此通信, 达到感知对方的过程。节点握手是集群彼此通信的第一步,完成节点握手之后,一个个的Redis节点就组成了一个多节点的集群。
    3. 分配槽(slot) Redis集群把所有的数据映射到16384个槽中。每个节点对应若干个槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。通过 cluster addslots命令为节点分配槽。
      ||| 故障发现及处理流程
    4. 故障发现: Redis集群中所有的节点都要承担状态维护的任务。通过ping/pong消息实现节点通信,集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong 消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节 点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。
    5. 主观下线: 当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当 半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程。
      ![[Pasted image 20241004143317.png]]
    6. 故障转移: 故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它 的从节点中选出一个替换它,从而保证集群的高可用。
      不是持有槽的主节点或没有从节点,就将他的槽分给其他节点
    7. 节点选举: 每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障 的主节点。准备选举时间 当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该 时间后才能执行后续流程。
      由于在每个配置纪元内持有槽的主节点只能投票给一个 从节点,因此只能有一个从节点获得N/2+1的选票,保证能够找出唯一的从节点。

    Redis集群中各个节点是如何实现数据一致性的?

    Redis 高并发

    Redis事务是什么?怎么实现ACID的?

    Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

    Redis 事务对 ACID 的实现情况如下

    • 原子性(Atomicity)
      • Redis 通过使用 MULTI、EXEC、DISCARD 和 WATCH 等命令来实现事务的原子性。在事务执行过程中,如果在执行 EXEC 命令前客户端断线,或者事务执行过程中服务器出现故障,那么事务中的所有命令都不会执行;而一旦 EXEC 命令执行,事务中的所有命令都会被执行,从而保证了事务的原子性。
    • 一致性(Consistency)
      • Redis 事务保证一致性主要体现在:如果一个事务在执行过程中由于某些原因(如命令语法错误等)导致部分命令无法正常执行,那么 Redis 会对事务进行回滚,使数据库保持在事务开始前的状态。例如,在执行事务时,如果其中一个命令的参数类型错误,那么整个事务会被取消执行,数据库不会被修改,从而保证了数据的一致性。
    • 隔离性(Isolation)
      • Redis 是单线程执行事务的,在事务执行期间,不会被其他客户端的命令所干扰,从而保证了事务的隔离性。
    • 持久性(Durability)
      • Redis 的持久性取决于它的持久化配置。

    布隆过滤器

    多次哈希
    布隆过滤器(Bloom Filter)是一种快速、高效的数据结构,用于判断一个元素是否存在于一个集合中。它的主要作用是用于快速过滤掉不可能存在的元素,从而减少后续的查询和操作,提高系统的效率和性能。

    • 布隆过滤器是一个固定大小的位数组,最初全部设置为0。
    • 它使用多个哈希函数,每个元素都通过这些哈希函数产生多个哈希值。
    • 插入元素时,将这些哈希值对应的位设置为1。
    • 查询元素时,计算其哈希值并检查这些位。如果所有相关位都为1,认为元素“可能”在集合中;如果任何一位为0,则元素肯定不在集合中。

    由于布隆过滤器使用了哈希函数和位数组,因此它具有以下特点:

    • 空间效率高:以牺牲一定的精度为代价,可以使用少量的存储空间来表示大规模的数据集合;
    • 查询效率高:查询一个元素的时间复杂度为O(k),其中k为哈希函数的个数;
    • 插入和删除操作不支持:由于每个元素的哈希值会影响其他元素的哈希值,因此无法进行删除操作,而插入操作需要重新计算哈希值并更新位数组。

    布隆过滤器的应用场景包括:缓存、防止网络爬虫等非法访问、垃圾邮件过滤、DNA序列分析等。

    • 如果全不是1,那么key不存在;
    • 如果都是1,也只是表示key可能存在。

    布隆过滤器存在误判的可能性,即会产生假阳性(false positives)错误,但不会产生假阴性(false negatives)错误。

    • 当查询一个没有被插入过的元素时,因为其他元素的哈希值影响,它可能的所有哈希位都已经被设置为1。这时,布隆过滤器错误地表示元素可能在集合中,产生一个假阳性错误。
    • 假阳性是布隆过滤器的固有缺陷,可以通过增加位数组的大小或使用更多的哈希函数来优化

    🌟缓存穿透,缓存击穿,缓存雪崩

    在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问Mysql等数据库。这样可以大大缓解数据库的压力。

    当缓存库出现时,必须要考虑如下问题:

    • 缓存穿透
    • 缓存穿击
    • 缓存雪崩
    • 缓存污染(或者满了)
    • 缓存和数据库一致性
      ![[Pasted image 20241003185756.png]]
    1. 缓存穿透
    • 问题来源 缓存和数据库都没有
      缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存 ,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
    • 解决方案
      • 缓存空值:对于查询结果为空的数据,将空值(如null)也缓存起来,并设置一个较短的过期时间。
      • 布隆过滤器:在应用层增加一个布隆过滤器,提前判断请求的键是否可能存在,以减少直接查询数据库的机会。
      • 参数校验:在查询数据库之前,对请求参数进行合法性校验,过滤掉明显非法的请求
    1. 缓存击穿
    • 问题来源 数据库有缓存没有(过期)
      缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
    • 解决方案
      • 设置热点数据永不过期:对于特别热点的数据,可以设置为永不过期,并定期刷新。
      • 加锁(Mutex)机制:在缓存失效后,使用分布式锁控制只有一个请求能查询数据库并更新缓存,其他请求等待缓存更新完成后再获取数据。
      • 预热缓存:在缓存即将过期时,提前异步更新缓存。比如后台设置一个守护线程定时更新缓存.
    1. 缓存雪崩
    • 问题来源 大量缓存击穿
      缓存雪崩是指缓存服务器因某种原因(如宕机或大量缓存数据同时过期)导致大面积缓存失效,大量请求直接打到数据库上,造成数据库压力过大甚至宕机。
    • 解决方案
      • 缓存高可用:使用Redis集群、主从复制等方式,确保缓存服务器的高可用性。
      • 设置多级缓存,第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。
      • 错开缓存过期时间(均匀过期):设置缓存数据的过期时间时,随机增加一些波动值,避免大量数据在同一时间过期。
      • 限流和降级:在缓存失效的情况下,对数据库请求进行限流或返回默认值,避免数据库崩溃。

    redis 如何应对热点数据?

    1. 热点数据预热:在系统启动或负载较低的时候,可以提前将热点数据加载到Redis中进行缓存预热。这样可以避免系统刚启动或负载突然增加时,大量请求涌入导致热点key未被缓存的情况。
    2. 热点数据不过期:可以设置热点数据的过期时间为永不过期,确保热点数据一直保持在缓存中,避免被淘汰。
    3. 热点数据分片:将热点数据分散存储在多个Redis实例上,通过数据分片的方式来分担单个Redis实例的负载压力。这样可以提高系统的并发处理能力。
    4. 多级缓存。例如使用 memcached 在本地缓存热点数据

    缓存大小设置,淘汰策略

    (类比页面置换算法)
    系统的设计选择是一个权衡的过程:大容量缓存是能带来性能加速的收益,但是成本也会更高,而小容量缓存不一定就起不到加速访问的效果。一般来说,建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销

    1. volatile-lru (Least Recently Used)
      仅对设置了过期时间的键进行 LRU 淘汰,即优先移除最近最少使用的数据。
    2. allkeys-lru
      对所有键进行 LRU 淘汰,无论是否设置了过期时间。
    3. volatile-lfu (Least Frequently Used)
      仅对设置了过期时间的键进行 LFU 淘汰,即优先移除使用频率最低的数据。
    4. allkeys-lfu
      对所有键进行 LFU 淘汰,无论是否设置了过期时间。
    5. volatile-ttl (Time to Live)
      仅对设置了过期时间的键进行淘汰,优先移除剩余生存时间(TTL)最短的键。
    6. noeviction
      当内存不足以容纳新写入数据时,新写入操作会报错,不会移除任何数据。这是 Redis 默认策略。
    7. volatile-random
      仅对设置了过期时间的键进行随机淘汰。
    8. allkeys-random
      对所有键进行随机淘汰,无论是否设置了过期时间。

    缓存如何保证与数据库的数据一致性?

    缓存更新策略 https://coolshell.cn/articles/17416.html

    1. 缓存更新策略
      Cache Aside (Lazy Loading) 最常用策略
    • 流程
      1. 应用从缓存读取数据,未命中时从数据库加载数据并将其写入缓存。
      2. 数据更新时,先更新数据库,然后删除缓存中的数据。
    • 优点:读取时缓存未命中只会带来一次数据库查询,更新操作更简单。
    • 缺点:在并发写操作中可能导致数据不一致,需加锁或其他方式保证一致性。
      Read Through
    • 流程
      1. 应用从缓存读取数据,未命中时由缓存系统从数据库加载数据并返回给应用。
    • 优点:缓存系统统一管理加载逻辑,简化应用代码。
    • 缺点:需定制缓存系统逻辑,更新策略需谨慎。
      Write Through
    • 流程
      1. 应用写入数据时,同时写入缓存和数据库。
    • 优点:写入操作后缓存和数据库数据一致。
    • 缺点:写操作较慢,适合读多写少的场景。
      Write Behind (Write Back)
    • 流程
      1. 应用先写入缓存,由缓存系统异步地将数据写入数据库。
    • 优点:写操作性能高,适合写频繁的场景。
    • 缺点:在系统故障时可能导致数据丢失,一致性较难保证。
    1. 数据更新策略
      直接更新缓存
    • 方法:数据更新时直接更新缓存和数据库。
    • 优点:数据实时一致。
    • 缺点:需要确保事务性,增加了实现复杂度。
      删除缓存
    • 方法:数据更新时,先更新数据库,然后删除缓存数据,让下一次读取时重新加载最新数据。
    • 优点:实现简单,适合大部分场景。
    • 缺点:在并发更新场景下需加锁或控制请求顺序,避免脏数据。
    1. 一致性策略
      分布式锁
    • 方法:在更新数据库和缓存时,使用分布式锁(如Redis的Redlock)确保操作的原子性。
    • 优点:保证并发操作的一致性。
    • 缺点:增加了系统复杂度,性能有所影响。
      事务
    • 方法:使用数据库事务或分布式事务(如TCC)保证操作的一致性。
    • 优点:数据一致性强。
    • 缺点:实现复杂度高,性能开销大。
      消息队列
    • 方法:使用消息队列(如Kafka)实现数据库和缓存的异步更新,确保顺序性。
    • 优点:解耦应用和数据库,保证顺序性和一致性。
    • 缺点:需要处理消息丢失和重复消费的问题。
      ![[Pasted image 20240802180733.png]]

    如何保证缓存和数据库数据的⼀致性?

    根据CAP理论,在保证可用性和分区容错性的前提下,无法保证一致性,所以缓存和数据库的绝对一致是不可能实现的,只能尽可能保存缓存和数据库的最终一致性。

    1. 选择合适的缓存更新策略
      1. 删除缓存而不是更新缓存
      2. 先更数据,后删缓存先更数据库还是先删缓存?
    2. 缓存不一致处理
      1. 缓存和数据库数据不一致常见的两种原因:
        • 缓存key删除失败
        • 并发导致写入了脏数据
      2. 消息队列保证key被删除可以引入消息队列,把要删除的key或者删除失败的key丢尽消息队列,利用消息队列的重试机制,重试删除对应的key。(对业务代码有一定的侵入性)
      3. 数据库订阅+消息队列保证key被删除可以用一个服务(比如阿里的 canal)去监听数据库的binlog,获取需要操作的数据。
      4. 延时双删防止脏数据,还有一种情况,是在缓存不存在的时候,写入了脏数据,这种情况在先删缓存,再更数据库的缓存更新策略下发生的比较多,解决方案是延时双删。就是在第一次删除缓存之后,过了一段时间之后,再次删除缓存。
      5. 设置缓存过期时间:给缓存设置一个合理的过期时间,即使发生了缓存数据不一致的问题,它也不会永远不一致下去,缓存过期的时候,自然又会恢复一致。

    如何保证本地缓存和分布式缓存的一致?

    是在两种不同的缓存系统系统间(多级缓存;Redis集群+Caffeine)

    • 采用Redis本身的Pub/Sub机制,分布式集群的所有节点订阅删除本地缓存频道,删除Redis缓存的节点,同事发布删除本地缓存消息,订阅者们订阅到消息后,删除对应的本地key。但是Redis的发布订阅不是可靠的,不能保证一定删除成功。
    • 引入专业的消息队列,比如RocketMQ,保证消息的可靠性,但是增加了系统的复杂度。
    • 设置适当的过期时间兜底,本地缓存可以设置相对短一些的过期时间。
      ![[Pasted image 20241004152546.png]]

    热点Key

    ⭐️若 QPS 达到十万级别,如何确保 Redis 正常工作?

    1. 高性能服务器:使用高性能的CPU和大容量内存的服务器。Redis主要依赖内存,因此需要确保有足够的内存来存储数据,同时高速的CPU有助于处理大量请求。
    2. Redis相关配置设置: 设置最大限制内存,淘汰策略,持久化策略等
    3. 分片:如果单个Redis实例无法处理这么大的负载,可以考虑将数据分片到多个Redis实例上。这样,每个实例只需要处理一部分请求。
    4. Redis集群:Redis集群是指将Redis数据分散存储到多个物理节点上,并且各个节点之间的数据和服务是共享的,从而实现了数据的高可用和自动化的负载均衡。Redis集群中的每个物理节点都是相同的,它们之间会进行数据复制和故障切换,从而保证节点之间的高可用性。(集群和分片的区别
    5. 热点数据缓存:对高频访问的数据进行专门的缓存管理,可以使用二级缓存机制(本地缓存+Redis);根据数据的访问频率和更新频率设置合适的TTL(Time To Live),避免缓存雪崩和缓存击穿。
    6. 连接优化: 使用连接池技术理Redis连接,减少连接建立和销毁的开销;尽量使用批量操作,减少网络往返次数,提高效率。
    7. 监控和报警: 时监控Redis的性能指标(如内存使用、命中率、QPS等),设置报警规则,当Redis的关键指标超过预设阈值时,及时报警
    8. 限流和降级: 使用限流算法(如令牌桶算法、漏桶算法)控制请求速率,避免Redis过载;在Redis负载过高或出现故障时,启用降级方案,如直接从数据库读取或返回默认值。

    优化代码和算法,减少冗余请求,选择合适的数据结构,选择高效的序列化方式、

    6.1.6 Redis场景题

    见场景题章节

    6.2 RabbitMQ

    消息队列的应用场景

    • 应用解耦:消息队列减少了服务之间的耦合性,不同的服务可以通过消息队列进行通信,而不用关心彼此的实现细节。
    • 异步处理:消息队列本身是异步的,它允许接收者在消息发送很长时间后再取回消息。
    • 流量削锋:当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的”载体”,在下游有能力处理的时候,再进行分发与处理。
    • 日志处理:日志处理是指将消息队列用在日志处理中,比如 Kafka 的应用,解决大量日志传输的问题。
    • 消息通讯:消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯,比如实现点对点消息队列,或者聊天室等。
    • 消息广播:如果没有消息队列,每当一个新的业务方接入,我们都要接入一次新接口。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情,无疑极大地减少了开发和联调的工作量。

    有了解过其他消息队列吗?如何做选型?

    1. RabbitMQ 特点
      • 消息可靠性高
        • 支持多种消息确认机制,如消息发送确认、消息接收确认等。通过这些机制,可以确保消息在生产者、队列、消费者之间可靠传递,减少消息丢失的风险。
      • 灵活的路由策略
        • 采用 Exchange(交换机)来实现消息的路由。Exchange 有多种类型,如 direct(直接路由)、topic(主题路由)、fanout(广播路由)等。这使得消息可以根据不同的规则被路由到不同的队列中,满足复杂的业务场景需求。
      • 支持多种消息协议
        • 除了支持 AMQP(Advanced Message Queuing Protocol)协议外,还支持 STOMP、MQTT 等协议,方便与不同类型的客户端进行集成。
      • 轻量级
        • 相对容易部署和管理,对资源的要求不是特别高,适合中小型企业和项目中的消息传递需求。
    2. RocketMQ 特点
      • 高吞吐与低延迟
        • 经过优化,能够处理大量的消息,在高并发场景下表现出色,同时还能保证较低的消息延迟,适用于对性能要求较高的大规模数据传输场景。
      • 分布式架构
        • 具有良好的分布式特性,支持集群部署,可以方便地进行水平扩展,提高系统的整体可用性和处理能力。
        • 支持主从复制,通过多个节点之间的数据复制来保证数据的安全性和可靠性。
      • 顺序消息支持
        • 对顺序消息有很好的支持,可以确保消息按照特定的顺序被消费,这在一些对消息顺序有严格要求的业务场景(如交易流水处理)中非常重要。
      • 消息回溯
        • 能够支持消息的回溯功能,即可以根据时间或者偏移量重新消费已经消费过的消息,方便进行数据的重新处理或者故障排查。
    3. Kafka 特点
      • 高吞吐率
        • 采用分区(Partition)机制和批量发送等技术,能够实现非常高的消息吞吐率,每秒可以处理数十万条消息,适合处理海量数据的场景,如日志收集、大数据分析等。
      • 可扩展性强
        • 基于分布式架构,通过增加节点可以轻松实现水平扩展。可以动态地添加或者删除主题(Topic)的分区,适应不断变化的业务需求。
      • 持久化存储
        • 消息可以持久化存储在磁盘上,保证消息的可靠性。并且通过优化的存储结构,即使在海量数据的情况下也能快速地查询和读取消息。
        • 支持按照时间或者偏移量(Offset)来查询消息,方便进行数据的回溯和分析。
      • 多副本机制
        • 为了提高数据的安全性和可用性,支持多副本存储。副本之间可以进行数据同步,当某个副本出现故障时,可以自动切换到其他副本继续提供服务。

    选型考虑

    • 消息可靠性要求
      • 如果对消息可靠性要求极高,例如金融交易类的消息传递,RabbitMQ 和 RocketMQ 都是不错的选择。RabbitMQ 通过多种确认机制保证消息可靠传递,RocketMQ 的主从复制和消息回溯功能也能确保消息的完整性。
    • 性能需求
      • 如果需要处理高并发、大量消息的场景,Kafka 和 RocketMQ 的高吞吐率和低延迟特性更有优势。Kafka 在处理海量数据时的性能表现尤为突出,而 RocketMQ 在低延迟方面也有很好的表现。
    • 消息顺序性要求
      • 如果业务场景对消息顺序有严格要求,如订单处理流程等,RocketMQ 的顺序消息支持会是一个重要的考虑因素。
    • 协议支持和集成便利性
      • 如果需要与多种类型的客户端集成,并且希望支持多种消息协议,RabbitMQ 可能更合适,因为它支持多种协议如 AMQP、STOMP、MQTT 等。
    • 系统复杂度和资源限制
      • 如果系统资源有限且对消息队列的管理复杂度要求较低,RabbitMQ 这种轻量级的消息队列可能更适合。而如果需要构建大规模的分布式消息系统,Kafka 或 RocketMQ 的分布式架构和可扩展性会更符合需求。

    简单介绍一些rabbitMQ的底层数据结构和实现方式

    首先,既然是消息队列,消息的实现数据结构一般就是队列,底层久通过链表来实现。

    简单来讲,rabbitMQ工作流程主要设计四个对象,分别是生产者、消费者、交换机和队列。
    生产者负责生产消息,通过信道,把消息发送给交换机(Exchange)。然后交换器可以将消息路由到一个或者多个队列中。消费者会监听RabbitMQ中的队列(Queue),并从队列中获取消息进行消费。需要注意的是,消息会一直保留在队列中,直到被消费者接收和处理。

    交换器的类型

    RabbitMQ 提供了5种不同类型的交换机,每种交换机都有其特定的路由逻辑:

    1) Direct Exchange
    Direct Exchange 根据消息的路由键(Routing Key)精确地将消息路由到队列。

    • 路由规则:消息被路由到路由键完全匹配的队列。
    • 使用场景:需要将消息发送到特定队列的情况。
      2)Fanout Exchange
      Fanout Exchange 将消息广播到绑定到该交换机的所有队列。
    • 路由规则:消息会被路由到所有与该交换机绑定的队列。
    • 使用场景:广播消息到多个队列的情况。
      3)Topic Exchange
      Topic Exchange 根据消息的路由键模式(通常是带点号的字符串)将消息路由到匹配的队列。
    • 路由规则:路由键和绑定键(Binding Key)是点号分隔的字符串。绑定键可以包含两个特殊字符:*(匹配一个单词)和 #(匹配零个或多个单词)。
      4)Headers Exchange
      Headers Exchange 根据消息的头属性(Headers)进行路由。与其他交换机不同,Headers Exchange 不使用路由键。
    • 路由规则:消息的头属性必须与绑定的头属性完全匹配,才能将消息路由到相应的队列。
    • 使用场景:需要基于消息的多个属性进行复杂路由的情况。
      5) Default Exchange
      Default Exchange 是 RabbitMQ 内置的一个隐式交换机,每个队列在创建时会自动绑定到这个交换机上,路由键为队列的名称。
    • 路由规则:消息的路由键必须与队列名称完全匹配。
    • 使用场景:直接发送消息到指定队列的情况,不需要显式声明交换机。

    消息队列有什么作用?

    1. 异步处理
      • 将耗时操作放入队列中,异步处理,提高系统响应速度。
    2. 解耦系统
      • 使用消息队列解耦系统中的组件,使系统更具弹性和可扩展性。
    3. 负载均衡
      • 消息可以分配到多个消费者,实现负载均衡和并发处理。
    4. 日志收集
      • 集中收集和处理日志信息,进行分析和监控。
    5. 事件驱动架构
      • 通过消息通知机制,实现事件驱动的系统架构。

    RabbitMQ如何保证消息的顺序一致性?

    在RabbitMQ中,保证消息的顺序性是一个相对复杂的问题,因为RabbitMQ本身并不保证消息的顺序。但是,可以通过以下策略来尽可能保证消息的顺序性:
    生产者方面:

    1. 单一队列:确保所有消息都发送到同一个队列中。在单一队列中,消息的顺序是由RabbitMQ保证的(FIFO,先进先出)。
    2. 消息顺序ID:在消息体中添加一个顺序ID字段,这样消费者可以检查消息的顺序是否正确。
    3. 事务消息:使用RabbitMQ的事务机制(通过channel.txSelect()channel.txCommit()方法),确保消息在发送过程中不会因为网络问题或其他原因导致顺序混乱。
    4. 发布确认:使用发布确认机制(通过将channel设置为确认模式,并处理返回的确认回调),确保消息被正确发送到队列。

    消费者方面:

    1. 单一消费者:确保队列只有一个消费者。如果有多个消费者从同一个队列中消费消息,那么消息的顺序可能会因为消费者处理消息的速度不同而变得混乱。
    2. 顺序消费:消费者在处理消息时,可以检查消息的顺序ID,如果发现顺序不正确,可以选择丢弃消息或者将消息重新入队。
    3. 预取计数(prefetch count):设置预取计数为1,这样消费者一次只能从队列中获取一条消息,处理完这条消息后再获取下一条消息。这样可以保证在单个消费者上的消息顺序性。

    如何保证消息可靠性?

    生产者角度

    • 事务机制
      • 生产者可以使用事务来确保消息的可靠传递。在事务模式下,生产者发送消息前先开启事务,然后发送消息,如果消息发送成功则提交事务,失败则回滚事务。不过事务机制会带来较大的性能开销。
    • 确认模式(Confirm)
      • 生产者开启 Confirm 模式后,每发送一条消息都会分配一个唯一的 ID。RabbitMQ 会在消息被成功接收并处理后,向生产者发送一个确认信号。如果生产者在一定时间内未收到确认信号,可以进行消息重发。
        队列和消息持久化
    • 队列持久化
      • 在声明队列时将其设置为持久化(durable = true),这样在 RabbitMQ 服务重启后,队列仍然存在。
    • 消息持久化
      • 发送消息时将消息的投递模式(delivery mode)设置为 2(持久化),确保消息在 RabbitMQ 服务器重启后不会丢失。不过消息持久化会影响性能。
        消费者角度
    • 手动确认
      • 消费者在消费完消息后,手动发送确认信号给 RabbitMQ。如果消费者在处理消息过程中崩溃,RabbitMQ 会认为该消息未被处理,从而将消息重新分发给其他消费者或者在该消费者恢复后重新发送。

    如何将消息持久化?

    队列持久化原理

    • 当在 RabbitMQ 中声明一个持久化队列(设置durable = true)时,RabbitMQ 会把队列的定义(包括队列名称、属性、绑定关系等信息)存储到磁盘上的一个持久化存储介质(通常是磁盘文件)中。这样,当 RabbitMQ 服务重启时,它会读取磁盘上存储的队列定义信息,然后重新创建这些队列,使得队列在重启后仍然可用。

    消息持久化原理

    • 消息存储机制
      • 当发送一条持久化消息(设置deliveryMode = 2)时,RabbitMQ 会将消息的内容写入到磁盘上的消息存储文件中。RabbitMQ 使用了一种高效的存储格式来保存消息,确保消息能够在磁盘上持久保存。
    • 刷盘策略
      • RabbitMQ 并不是每收到一条消息就立即将其刷写到磁盘,而是根据一定的刷盘策略来进行操作。例如,它可能会在一定时间间隔或者当消息积累到一定数量时,将内存中的消息批量刷写到磁盘上,这样既保证了消息的持久化,又兼顾了性能。
    • 恢复机制
      • 当 RabbitMQ 服务重启后,它会从磁盘上的消息存储文件中读取之前存储的持久化消息,并将这些消息重新加载到内存中,然后按照正常的消息分发流程将消息分发给消费者进行处理。

    RabbitMQ有什么特点?

    可靠性方面

    • 持久化:RabbitMQ 支持消息的持久化,确保在服务器重启或崩溃等情况下,消息不会丢失。它可以将队列、交换器和消息都设置为持久化的,把消息保存到磁盘上。
    • 确认机制:提供了发布确认(Publisher Confirm)和事务机制来保证消息的可靠传递。发布确认机制允许生产者在发送消息后收到来自 RabbitMQ 的确认,确保消息已被正确处理。
      灵活的路由方式
    • 多种交换器类型:RabbitMQ 有多种交换器类型,如直连交换器(Direct Exchange)、主题交换器(Topic Exchange)、扇形交换器(Fanout Exchange)等。直连交换器根据消息的路由键(Routing Key)将消息精确地发送到对应的队列;主题交换器可以根据路由键的模式匹配来进行消息分发,适用于复杂的消息路由场景;扇形交换器则会把消息广播到所有绑定的队列中。
      高可用性
    • 集群部署:支持集群模式,多个 RabbitMQ 服务器可以组成一个集群。在集群环境中,队列和消息可以在不同的节点之间进行复制和同步,当某个节点出现故障时,其他节点可以继续提供服务,保证系统的不间断运行。
      扩展性
    • 横向扩展:可以方便地进行横向扩展,通过增加更多的节点来提高系统的整体性能和处理能力。新的节点可以动态地加入到集群中,分担消息处理的负载。

    使用消息队列需要注意哪些问题?

    需要注意从生产者和消费者两个方面

    1. 消息可靠性
      消息发送确认:确保消息成功发送到消息队列。可以使用事务或者发送确认机制。在消费端也有消息确认机制,保证了消息被正确消费。
      消息持久化设置:对于重要的消息,要设置消息的持久化属性。
    2. 消息频率与流量控制:如果生产者生产消息的速度远大于消费者处理消息的速度,可能会导致消息积压。这可能会占用大量的存储空间,甚至影响系统的性能。可以通过限制消息生产的速率,增加消费者的数量、优化消费者的处理逻辑或者调整消息队列的参数来解决这个问题。
    3. 消息重复:在某些情况下,如网络故障、消费者处理失败等,可能会导致消息重复发送。消费者需要能够处理重复的消息,通常可以通过使用唯一标识符或者消息去重机制来解决这个问题。可以采用消息确认机制,或者在业务层面做幂等性设计
    4. 消息丢失:可能由于网络故障、服务器宕机等原因导致消息丢失。为了避免消息丢失,可以开启消息队列的持久化功能,将消息存储在磁盘上。同时,生产者和消费者也需要正确处理消息确认机制,确保消息被正确处理。
      生产者:开启事务或确认机制,生产者发送消息后会进入事务模式,等待消息被成功确认后才提交事务。如果消息发送失败,可以回滚事务并重新发送。不过事务会降低性能,因为它涉及到同步等待确认。
      消费者:
    5. 消息顺序
    6. 消息重复:在某些情况下,如网络故障、消费者处理失败等,可能会导致消息重复发送。消费者需要能够处理重复的消息,通常可以通过使用唯一标识符或者消息去重机制来解决这个问题。

    🌟讲讲RabbitMQ的消息确认机制

    如何解决消息堆积的?

    如何保证消息不被重复消费?

    业务层面

    • 幂等性设计
      • 使消费者的业务处理逻辑具备幂等性,即对同一操作的多次请求结果是相同的。例如,在数据库操作中,可以通过唯一键约束来避免重复插入数据;在处理支付业务时,保证同一笔支付请求无论被处理多少次,只会产生一次扣款效果。
    • 记录处理状态
      • 消费者在处理消息时,将处理过的消息的唯一标识(如消息 ID)记录到一个外部存储(如数据库、缓存等)中。每次处理消息前,先检查该消息是否已经被处理过,如果已经处理过则直接丢弃。

    RabbitMQ 机制层面

    • 消息确认机制优化
      • 消费者在手动确认消息时,确保在消息完全处理完成后再发送确认信号。如果在消息处理过程中发生异常,不要发送确认信号,这样 RabbitMQ 会重新将消息分发给其他消费者或者在当前消费者恢复后重新发送。
    • 设置消息 TTL(Time To Live)和死信队列
      • 为消息设置合理的 TTL,当消息在一定时间内没有被消费者正确处理(可能是因为重复消费导致处理时间过长),消息会过期进入死信队列。在死信队列中,可以根据业务需求对这些消息进行重新处理或者分析处理失败的原因。

    什么是死信队列?

    • 死信队列(Dead - Letter Queue,DLQ)是一种特殊的队列,用于存放那些无法被正常消费的消息。当消息在普通队列中出现特定情况导致不能被消费者成功处理时,这些消息就会被转移到死信队列中。

    产生死信的原因

    • 消息被拒绝:消费者明确拒绝(basicReject 或 basicNack 方法)接收某条消息,并且设置了不重新入队(requeue = false),那么该消息会被发送到死信队列。
    • 消息过期:当消息设置了有效期(TTL - Time To Live),如果在有效期内没有被消费,过期后就会被移到死信队列。
    • 队列达到最大长度:当普通队列设置了最大长度限制,并且队列已满,新进来的消息就会被转移到死信队列。

    RabbitMQ是如何实现死信队列的?

    • 首先需要定义两个队列,一个是普通的业务队列(用于正常消息的传递和消费),另一个是死信队列(用于存放死信)。例如,在 RabbitMQ 的管理界面或者通过代码来创建这两个队列。
      2. 设置普通队列的死信参数

    • x - dead - letter - exchange 参数:将普通队列与死信队列通过交换器关联起来。当消息在普通队列中变成死信后,会被发送到该参数指定的交换器。

    • x - dead - letter - routing - key 参数:指定消息从普通队列变成死信后,在发送到死信交换器时使用的路由键。如果不设置,默认使用原消息的路由键。
      3. 消息变成死信的情况触发

    • 消息被拒绝:在消费者端代码中,当使用basicRejectbasicNack方法拒绝消息,并且设置requeue参数为false时,消息就会进入死信队列。例如,在 Java 中可以这样写:

      1
      channel.basicReject(deliveryTag, false);
    • 消息过期:在发送消息时设置TTL(Time To Live)属性,当消息在队列中存活时间超过TTL规定的值时,就会成为死信。例如:

    1
    2
    3
    4
    AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
    .expiration("60000") // 设置消息过期时间为 60 秒
    .build();
    channel.basicPublish(exchange, routingKey, properties, message.getBytes());
    • 队列达到最大长度:当队列的长度达到设置的最大限制,并且新的消息进入队列时,最早的消息就会被推到死信队列(如果设置了死信参数)。
      4. 消费死信队列中的消息
    • 与消费普通队列中的消息类似,通过创建消费者来监听死信队列,当死信队列中有消息时,消费者就可以对这些消息进行处理,比如分析消息不能被正常消费的原因、进行消息备份等操作。

    6.3 Dubbo

    https://www.bilibili.com/video/BV1FU4y1K7C7/

    简单介绍一下Dubbo

    Dubbo 是一款高性能、轻量级的开源 WEB 和 RPC 框架。主要用于提供微服务架构中的分布式服务治理解决方案。Dubbo 提供了包括服务发现、负载均衡、远程调用、容错和服务监控等一系列功能,广泛应用于大规模分布式系统中。
    Dubbo 提供了六大核心能力

    1. 面向接口代理的高性能 RPC 调用。
    2. 智能容错和负载均衡。
    3. 服务自动注册和发现。
    4. 高度可扩展能力。
    5. 运行期流量调度。
    6. 可视化的服务治理与运维。
      ![[Pasted image 20240802204102.png]]

    Dubbo的工作原理是什么样的?

    1. 服务启动时,服务提供者和服务消费者根据配置信息会连接到注册中心, 分别向注册中心去订阅和注册服务
    2. 注册中心会根据订阅关系去返回服务提供者的信息到服务消费者,同时服务消费者 会把服务提供者的信息缓存到本地,如果信息发生变更,消费者会收到注册中心的一个推送去 更新本地的缓存
    3. 服务消费者会生成代理对象,同时根据负载均衡策略去选择一台目标服务提供者 并且定向向monitor记录接口的调用次数和时间信息。
    4. 拿到代理对象后,服务消费者通过代理对象发起接口的一个调用
    5. 服务提供者收到请求后会根据数据进行反序列化,然后通过代理调用具体的接口的 一个实现,这是整个dubbo的一个原理和实现过程。

    Dubbo有哪些负载均衡策略?

    1. 随机
    2. 轮询
    3. 加权轮询,加权随机
    4. 一致性哈希 相同参数的请求总是分发到同一提供者,通过一致性哈希算法保证请求的一致性。
    5. 最小活跃数 为每一个服务提供者设置一个活跃数标志,选择当前最少活跃调用数的提供者,活跃调用数指正在处理的请求数。

    Dubbo有哪些容错机制?

    1. 失败重试(Failover)
      在调用失败时自动切换到其他提供者进行重试。默认情况下,Dubbo 会进行2次重试,可以通过配置重试次数来调整策略。适用于读操作且对延迟不敏感的场景。
    2. 快速失败(Failfast)
      只发起一次调用,失败立即报错。适用于幂等性要求高的写操作,如新增记录。
    3. 失败安全(Failsafe)
      调用失败时忽略错误。适用于不重要的操作,如日志记录。
    4. 失败自动恢复(Failback)
      调用失败后记录失败请求,并在后台定时重发。适用于消息通知等需要保证最终一致性的场景
    5. 并行调用(Forking)
      并行调用多个提供者,只要有一个成功就返回。可以通过配置并行调用的提供者数量来控制并发度。适用于实时性要求高的场景,但需要多台提供者。
    6. 广播调用(Broadcast)
      广播调用所有提供者,一个一个调用,任意一台报错即返回错误。适用于更新配置等需要所有提供者都成功的场景。

    6.4 Etcd

    Etcd 如何保证数据一致性?

    http://play.etcd.io/play

    • 从表层来看,Etcd 支持事务操作,能够保证数据一致性,
    • 从底层来看,Etcd 使用 Raft 一致性算法来保证数据的一致性
      • Raft 是一种分布式一致性算法,它确保了分布式系统中的所有节点在任何时间点都能达成一致的数据视图。
      • 具体来说,Rat算法通过选举机制选举出一个领导者(eader)节点,领导者负责接收客户端的写请求,并将写操作复制到其他节点上。
      • 当客户端发送写请求时,领导者首先将写操(写入自己的日志中,并将写操作的日志条目分发给其他节点,其他节点收到日志后也将基写入自己的日志中。
      • 一旦大多数节点(即半数以上的节点)都将该日志条目成功写入到自己。的日志中,该日志条目就被视为已提交,领导者会向客户端发送成功响应,在领导者发送成功响应后,该写操作就被视为已提交,从而保证了数据的一致性。
      • 如果领导者节点宕机或失去联系,Rat 算法会在其他节点中 选举出新的领导者,从而保证系统的可用性和一致性,新的领导者会继续接收客户端的写请求,并负责将写操作复制到其他节点上,从而保持数据的一致性。

    6.5 Docker

    6.6 K8s

    讲一讲什么是k8s

    1. Kubernetes(k8s)的定义
      • Kubernetes 是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。它最初由 Google 开发,现在是云原生计算基金会(CNCF)的一部分。
      • 在容器化的环境中,Kubernetes 提供了一种集中式的管理方式,能够对容器进行有效的组织、调度和监控。
    2. Kubernetes 的用途
      • 自动化容器部署
        • 描述:Kubernetes 可以轻松地在集群中的多个节点上部署容器化应用。用户只需定义应用的期望状态(例如,运行的副本数量、资源需求等),Kubernetes 就会自动确保容器按照定义的状态进行部署。例如,一个包含多个微服务的应用,每个微服务都打包成容器,Kubernetes 可以一次性将这些容器部署到集群中的合适节点上。
        • 优势:大大简化了部署流程,减少了人工操作可能带来的错误,提高了部署效率。
      • 容器的可扩展性
        • 描述:它能够根据应用的负载情况自动调整容器的数量。例如,在电商促销活动期间,当网站流量突然增大时,Kubernetes 可以自动增加处理订单、商品展示等微服务容器的副本数量,以应对高负载。相反,当流量减少时,可以自动减少容器副本数量以节省资源。
        • 优势:确保应用能够处理不同级别的负载,提高了应用的可用性和资源利用率。
      • 容器的负载均衡
        • 描述:Kubernetes 内置了负载均衡机制。当有多个容器副本提供相同的服务时,它可以将外部请求均匀地分配到这些副本上。例如,对于一个 Web 应用的多个容器副本,Kubernetes 会将用户的 HTTP 请求均衡地分发到各个副本,避免某个副本负载过重而其他副本闲置的情况。
        • 优势:提高了应用的整体性能和可靠性,防止单点故障。
      • 容器的自我修复
        • 描述:如果某个容器出现故障(例如,由于程序错误或者节点故障),Kubernetes 会自动检测到并重新启动容器,或者将容器迁移到其他健康的节点上。例如,若一个运行在容器中的数据库服务突然崩溃,Kubernetes 会尝试重新启动该容器;如果是节点故障导致容器不可用,Kubernetes 会在其他可用节点上重新创建该容器。
        • 优势:提高了应用的容错能力,减少了因容器故障导致的服务中断时间。
      • 资源管理与优化
        • 描述:Kubernetes 可以对集群中的计算资源(如 CPU、内存等)进行有效的管理。它允许用户为每个容器或容器组定义资源需求(如需要多少 CPU 核心和内存),然后根据集群的资源总量合理分配资源。例如,在一个混合部署了多个应用的集群中,Kubernetes 会根据每个应用的资源请求和限制,确保每个应用都能获得所需资源,同时避免资源的过度占用。
        • 优势:提高了集群资源的整体利用率,使得在有限的资源下可以运行更多的应用。

    6.7 ElasticSearch

    简单讲讲什么是ES?

    ElasticSearch ,是一个实时的分布式搜索和分析引擎。能够快速地存储、搜索和分析大量的数据,擅长处理文本类型的数据。他提供了分布式式存储和倒排索引的功能,极大地提高了搜索速度。

    什么是倒排索引?

    • 倒排索引是将文档中的单词(或术语)映射到包含这些单词的文档的索引结构。与传统的正向索引(文档到单词的映射)相反,它是单词到文档的映射。
    • 主要包含了两个部分:词典(Vocabulary)倒排列表(Posting List) 。词汇表包含了文档集合中所有不重复的单词(术语);对于词汇表中的每个单词,都有一个对应的倒排列表,包含该单词的所有文档的标识(例如文档编号)以及在文档中的位置等其他相关信息。
    • 使用倒排索引的时候,首先对文档进行分词操作,将文档划分为一个个的单词或术语。收集所有文档分词后的单词,去除重复的单词,构建词汇表。遍历每个文档,对于文档中的每个单词,在词汇表中找到对应的单词项,然后将该文档的标识添加到这个单词的倒排列表中。如果需要记录单词在文档中的位置等更多信息,也一并进行记录。

    词典和倒排列表的底层数据结构是什么?
    字典树+

    为什么倒排索引不用B+树?

    1、创建时间长,文件大。分别以id建立索引和goods_name建立索引。通过B+的数据结构,能够发现,B+树存储索引信息了,当咱们以goods_name建立索引就会发现,索引文件会特大。当咱们的索引字段数据越多那索引文件就会越大。建立索引时间也就越长。

    6.8 Nginx

    什么是代理服务器,正向代理和反向代理的有什么区别

    代理服务器(Proxy Server)是一种计算机系统或应用程序,它作为客户端和服务器之间的中介,以实现网络请求的转发。使用代理服务器的目的是多方面的,包括提高安全性、提高访问速度、节省带宽、隐藏客户端真实IP等。

    • 正向代理是代表客户端向服务器发送请求的代理服务器。
    • 反向代理是代表服务器接收客户端的请求,然后将请求转发到相应的内部服务器。

    正向代理和反向代理的区别

    • 位置不同:正向代理位于客户端和互联网之间,而反向代理位于服务器和互联网之间。
    • 目的不同:正向代理主要是为了保护客户端的隐私和安全性,反向代理则是为了保护服务器和优化资源。
    • 代理的对象不同:正向代理代理的对象是客户端的请求,反向代理代理的对象是服务器的请求。
    • 匿名性:正向代理为客户端提供匿名性,而反向代理通常不会隐藏服务器的真实IP地址,因为它是为服务器服务的。

    6.8 其他中间件

    Memcached

    什么是负载均衡?

    负载均衡(Load Balancing 简称 LB)是一种技术策略,它通过将工作任务分摊到多个操作单元上执行,从而提高服务的响应速度和服务质量。

    二、三、四、七层负载是什么意思?

    1. 二层负载均衡(Data Link Layer): 二层负载均衡工作在OSI模型的第二层,即数据链路层。它主要基于MAC地址来转发网络流量。在二层负载均衡中,负载均衡器通常使用虚拟MAC地址接收流量,然后根据某种算法(如轮询、最小连接等)将流量转发到真实的服务器MAC地址上。这种负载均衡不涉及网络层以上的信息,因此不会修改IP地址。

    2. 三层负载均衡(Network Layer): 三层负载均衡工作在OSI模型的第三层,即网络层。它基于IP地址和端口来分发流量。在这种类型的负载均衡中,当数据包到达负载均衡器时,负载均衡器会根据IP地址和端口信息修改数据包的目的IP地址,将其转发到后端服务器。常见的三层负载均衡技术包括网络地址转换(NAT)和IP隧道。

    3. 四层负载均衡(Transport Layer): 四层负载均衡工作在OSI模型的第四层,即传输层。它主要基于TCP/IP协议的端口号来分配流量,如TCP或UDP端口。四层负载均衡器可以根据源IP、目的IP、源端口和目的端口等信息来决定如何将流量转发到后端服务器。常见的四层负载均衡技术包括TCP代理和IPVS(IP Virtual Server)。LVS

    4. 七层负载均衡(Application Layer): 七层负载均衡工作在OSI模型的第七层,即应用层。它不仅可以根据IP地址和端口号来分发流量,还可以基于HTTP头信息、cookie、内容类型等应用层信息来做出负载均衡的决策。七层负载均衡器能够更深入地理解应用层协议,因此可以进行更复杂的负载均衡策略,如基于HTTP请求内容进行分发。nginx

    6.9 前端

    vue中v-if, v-for, v-show,v-else, v-bind, v-on 的区别是什么

    一、v-if 和 v-show

    1. v-if
      • 根据表达式的真假值来有条件地渲染元素。当表达式为真时,元素被渲染;当表达式为假时,元素从 DOM 中完全移除。
      • 切换开销相对较大,因为涉及到元素的创建和销毁。
      • 例如:<div v-if="isVisible">这是一个根据条件显示或隐藏的元素</div>
    2. v-show
      • 通过设置元素的 CSS display属性来控制元素的显示和隐藏。当表达式为假时,元素仍然存在于 DOM 中,只是被隐藏起来。
      • 切换开销相对较小,因为只涉及到 CSS 属性的修改。
      • 例如:<div v-show="isVisible">这是一个通过 CSS 显示或隐藏的元素</div>
        二、v-for
    3. 用于遍历数组或对象,渲染多个相同的元素。
      • 对于数组,可以使用(item, index) in array的语法,其中item是数组中的元素,index是元素的索引。
      • 对于对象,可以使用(value, key, index) in object的语法,其中value是对象的值,key是对象的键,index是对象属性的索引。
      • 例如:<li v-for="item in items">{{ item }}</li>
        三、v-else
    4. v-ifv-show配合使用,提供一个 “否则” 的分支。
      • 必须紧跟在v-ifv-show元素之后。
      • 例如:<div v-if="isVisible">可见元素</div><div v-else>不可见时显示的元素</div>
        四、v-bind
    5. 用于动态绑定元素的属性。可以简写为:
      • 可以绑定各种属性,如classstylehref等。
      • 例如:<div v-bind:class="{ active: isActive }">动态绑定类名的元素</div>,也可以写成<div :class="{ active: isActive }">
        五、v-on
    6. 用于绑定元素的事件监听器。可以简写为@
      • 可以绑定各种事件,如clicksubmitinput等。
      • 例如:<button v-on:click="handleClick">点击我</button>,也可以写成<button @click="handleClick">点击我</button>

    总之,这些指令在 Vue 中各自承担着不同的功能,开发者可以根据具体的需求选择合适的指令来实现页面的动态渲染和交互

    Vue和React有什么主要区别?

    一、开发理念

    1. Vue
      • 采用自底向上增量开发的设计。
      • 旨在通过简洁的 API 和灵活的配置,让开发者能够快速构建用户界面。
      • 强调数据驱动视图,通过响应式数据绑定,当数据变化时自动更新视图。
    2. React
      • 基于函数式编程的理念。
      • 提倡以组件为单位进行开发,组件是纯函数,输入 props(属性),输出 UI。
      • 强调不可变数据和单向数据流,通过更新状态来触发视图的重新渲染。

    二、语法和模板

    1. Vue
      • 使用模板语法,类似于 HTML,但具有动态数据绑定和指令。
      • 可以使用单文件组件(.vue文件),将 HTML、CSS 和 JavaScript 代码放在一个文件中,方便管理。
      • 支持插值表达式{{}}和指令,如v-ifv-for等,易于理解和上手。
    2. React
      • 使用 JSX(JavaScript XML)语法,将 HTML 结构嵌入到 JavaScript 代码中。
      • 需要使用class组件或函数组件来定义组件,代码相对更加纯粹的 JavaScript。
      • JSX 语法需要一定的学习成本,但提供了更大的灵活性。

    三、状态管理

    1. Vue
      • 内置了状态管理工具 Vuex,用于集中管理应用的状态。
      • Vuex 提供了 mutations、actions 和 getters 等概念,方便管理状态的变化和异步操作。
      • 状态的变化可以直接触发视图的更新,无需手动操作 DOM。
    2. React
      • 通常使用第三方状态管理库,如 Redux 或 MobX。
      • Redux 强调单一数据源、纯函数和不可变数据,通过 dispatch actions 来触发状态的变化。
      • MobX 则采用响应式编程的方式,通过观察状态的变化自动更新视图。

    四、性能优化

    1. Vue
      • 采用虚拟 DOM 和 diff 算法来优化性能。
      • 可以通过v-once指令、事件修饰符等方式进行性能优化。
      • 对响应式数据进行了优化,只有当数据真正发生变化时才会触发视图的更新。
    2. React
      • 同样使用虚拟 DOM 和 diff 算法。
      • 可以通过使用shouldComponentUpdate生命周期方法、使用PureComponentReact.memo进行浅比较等方式来优化性能。
      • React 16 引入了 Fiber 架构,提高了渲染性能和可中断性。

    五、社区和生态

    1. Vue
      • 拥有活跃的中文社区,文档丰富,对初学者友好。
      • 有大量的第三方插件和库,可以满足各种开发需求。
      • 生态系统在不断发展壮大,尤其在国内应用广泛。
    2. React
      • 拥有庞大的全球社区,生态丰富。
      • 有很多成熟的第三方库和工具,如 React Router、Ant Design 等。
      • 在大型企业和复杂项目中应用广泛。

    讲讲Vue的双向数据绑定和React的单向数据流动

    在 Vue 和 React 这两个流行的前端框架中,数据绑定的方式有所不同。
    一、Vue 的双向数据绑定

    1. 原理
      • Vue 通过使用 ES5 的Object.defineProperty()方法来实现数据的双向绑定。这个方法可以在对象上定义一个新属性或者修改现有属性,并可以指定属性的 getter 和 setter 方法。
      • 当数据发生变化时,Vue 会自动更新绑定了该数据的视图。同时,当用户在视图上进行交互操作(如输入框输入内容)时,Vue 也会自动更新数据。
    2. 示例
      • 例如,在 Vue 中创建一个输入框,绑定一个数据属性message
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        <template>
        <div>
        <input v-model="message" />
        <p>{{ message }}</p>
        </div>
        </template>

        <script>
        export default {
        data() {
        return {
        message: '初始值',
        };
        },
        };
        </script>
    • 当用户在输入框中输入内容时,message属性会自动更新,同时页面上显示message<p>标签也会自动更新。
    1. 优势
      • 开发效率高:开发者不需要手动处理数据和视图的同步问题,极大地提高了开发效率。
      • 易于理解:对于有传统 Web 开发经验的开发者来说,双向数据绑定的概念比较容易理解和接受。
        二、React 的单向数据流动
    2. 原理
      • React 采用单向数据流动的方式,也称为单向数据流。数据从父组件流向子组件,通过 props(属性)传递。
      • 当数据发生变化时,父组件会重新渲染,并将新的数据通过 props 传递给子组件。子组件不能直接修改父组件传递过来的数据,而是通过触发父组件的回调函数来通知父组件进行数据更新。
    3. 示例
      • 例如,有一个父组件Parent和一个子组件Child
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        // 父组件 Parent
        import React from 'react';
        import Child from './Child';

        class Parent extends React.Component {
        state = {
        message: '初始值',
        };

        handleMessageChange = (newMessage) => {
        this.setState({ message: newMessage });
        };

        render() {
        return (
        <div>
        <Child message={this.state.message} onMessageChange={this.handleMessageChange} />
        </div>
        );
        }
        }

        export default Parent;

        // 子组件 Child
        import React from 'react';

        const Child = ({ message, onMessageChange }) => {
        return (
        <div>
        <input value={message} onChange={(e) => onMessageChange(e.target.value)} />
        <p>{message}</p>
        </div>
        );
        };

        export default Child;
    • 当用户在子组件的输入框中输入内容时,会触发onChange事件,调用父组件传递过来的回调函数onMessageChange,通知父组件更新数据。父组件更新数据后,会重新渲染子组件,并将新的数据通过 props 传递给子组件。
    1. 优势
      • 可预测性:单向数据流动使得数据的变化更加可预测,易于调试和维护。
      • 组件独立性:子组件不能直接修改父组件的数据,使得组件更加独立和可复用。
        总之,Vue 的双向数据绑定和 React 的单向数据流动各有优势,开发者可以根据项目的需求和个人喜好选择适合的框架。

    7 设计模式

    ![[Pasted image 20241008214313.png]]

    设计模式的分类

    • 创建型模式创建型模式

      • 创建型模式主要关注对象的创建过程,确保程序在创建对象时能够更加灵活,避免直接使用new关键字导致代码的耦合性增加。它们约束的是:

      • 对象的创建过程:如何创建对象,何时创建对象,以及由谁创建对象。

      • 类的实例化过程:允许子类决定实例化的类,隐藏创建逻辑,而不是直接使用new操作符。

      • 创建的复杂对象的表示:使得同样的创建过程可以创建不同的表示。

      • 单例模式:确保一个类只有一个实例,并提供全局访问点。

      • 抽象工厂模式:创建一组相关或相互依赖的对象。

      • 建造者模式:将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。

      • 工厂模式:定义一个用于创建对象的接口,由子类决定实例化哪个类。

      • 原型模式:通过复制现有对象来创建新对象。

    • 结构型模式

      • 结构型模式主要关注类和对象之间的组合,用于实现对象之间的关联,确保在系统结构上的灵活性和扩展性。它们约束的是:

      • 类和对象的组合:如何将类和对象组合在一起形成更大的结构,同时保持结构的灵活性和可扩展性。

      • 实现继承关系:如何使用继承关系在多个类之间共享代码。

      • 适配器模式:将一个类的接口转换成客户希望的另一个接口。

      • 桥接模式:将抽象部分与它的实现部分分离,使它们可以独立变化。

      • 装饰模式:动态地给一个对象添加一些额外的职责。

      • 组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构。

      • 外观模式:为子系统中的一组接口提供一个统一的接口。

      • 享元模式:运用共享技术有效地支持大量细粒度的对象。

      • 代理模式:为其他对象提供一种代理以控制对这个对象的访问。

    • 行为型模式
      行为型模式主要关注对象之间的通信,以及实现对象之间的责任划分,确保在运行时对象之间的交互能够更加灵活。它们约束的是:

      • 对象之间的通信:如何实现对象之间通信,以及如何分配职责。

      • 算法的封装:如何将算法封装在对象中,使得算法可互换。

      • 模板方法模式:定义一个操作中的算法的骨架,将一些步骤延迟到子类中实现。

      • 命令模式:将请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化。

      • 迭代器模式:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。

      • 观察者模式:定义对象之间的一对多依赖关系,使得一个对象的状态改变会通知其依赖者。

      • 中介者模式:用一个中介对象来封装一系列的对象交互。

      • 备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。

      • 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。

      • 状态模式:允许一个对象在其内部状态改变时改变它的行为。

      • 策略模式:定义一系列算法,将每个算法都封装起来,并使它们可以互换。

      • 责任链模式:将请求的发送者和接收者解耦,使多个对象都有机会处理这个请求。

      • 访问者模式:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

    单例模式及创建方式

    • 定义:确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。
    • 作用:对于一些全局共享的资源或者频繁创建和销毁会造成资源浪费的对象(如数据库连接池、线程池等),使用单例模式可以有效地控制资源的使用,保证整个系统中只有一个实例被创建。
    • 优点:单例模式在内存中只有一个实例,减少了内存开支,以避免对资源的多重占用,可以在系统设置全局的访问点
    • 缺点:单例模式一般没有接口,扩展很困难;不利于测试;与单一职责原则有冲突

    实际的单例模式例子
    资源管理方面

    • 数据库连接池
      • 在应用程序中,数据库连接的创建和销毁是比较耗费资源的操作。使用单例模式来管理数据库连接池,可以确保整个应用程序只有一个连接池实例。这样可以高效地管理数据库连接资源,避免频繁地创建和销毁连接,提高系统性能。
    • 线程池
      • 线程的创建和销毁同样需要消耗系统资源。单例模式的线程池可以在整个应用程序中提供统一的线程管理机制,控制线程的数量、分配任务,确保系统在多线程环境下高效稳定地运行。
        系统配置方面
    • 系统配置类
      • 对于应用程序的配置信息,如数据库配置、服务器配置等,通常在整个系统运行期间是固定不变的。通过单例模式创建系统配置类,可以保证系统在任何地方都能获取到一致的配置信息,而且只需要在第一次加载配置时进行初始化操作,提高系统效率。
        操作系统环境相关
    • 操作系统中的文件系统
      • 在操作系统中,文件系统通常被设计为单例。无论有多少个应用程序或者进程在访问文件系统,都是通过同一个文件系统实例来进行文件的读写、目录操作等,这样可以保证文件操作的一致性和稳定性。

    单例模式的实现

    饿汉模式:创建类时直接创建对象 (饥不择食,定义的时候直接初始化)

    • 优点:实现简单,线程安全。由于实例在类加载时就创建,所以在多线程环境下不会出现多个实例的情况。
    • 缺点:如果实例的创建过程比较耗时或者占用较多资源,并且这个实例可能不会被使用,那么就会造成资源的浪费。
      1
      2
      3
      4
      5
      6
      7
      public class Singleton {
      private static Singleton instance = new Singleton();
      private Singleton() {}
      public static Singleton getInstance(){
      retuen instance;
      }
      }
      懒汉模式:第一次调用时候创建
    • 优点:延迟实例化,只有在需要时才创建实例,避免了资源的过早占用。
    • 缺点:在多线程环境下不是线程安全的。如果多个线程同时进入if (instance == null)的判断,可能会创建多个实例。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class Singleton {
      private static Singleton instance;
      private Singleton() {}

      public static Singleton getInstance() {
      if (instance == null) {
      instance = new Singleton();
      }
      return instance;
      }
      }
      懒汉模式(线程安全):加锁, synchronized
    • 优点:线程安全,解决了多线程环境下的实例唯一性问题。
    • 缺点:每次调用getInstance方法都需要获取锁,性能开销较大,尤其是在高并发场景下。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      public class Singleton {
      private static Singleton instance;

      private Singleton() {}

      public static synchronized Singleton getInstance() {
      if (instance == null) {
      instance = new Singleton();
      }
      return instance;
      }
      }
      双重检查锁定(DCL)单例模式(线程安全)
    • 优点:在保证线程安全的前提下,提高了性能。通过两次if检查,只有在实例未创建时才获取锁创建实例。使用volatile关键字来确保在多线程环境下对instance变量的正确读写。
    • 缺点:实现相对复杂。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      public class Singleton {
      private static volatile Singleton instance;

      private Singleton() {}

      public static Singleton getInstance() {
      if (instance == null) {
      synchronized (Singleton.class) {
      if (instance == null) {
      instance = new Singleton();
      }
      }
      }
      return instance;
      }
      }
      静态内部类单例模式(线程安全)
      利用了类加载机制来保证线程安全和实例的唯一性;加载外部类时不会立即加载内部类,只有在调用getInstance方法时才会加载内部类并创建实例,实现了延迟加载,并且性能较好。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class Singleton {
      private Singleton() {}

      private static class SingletonHolder {
      private static final Singleton INSTANCE = new Singleton();
      }

      public static Singleton getInstance() {
      return SingletonHolder.INSTANCE;
      }
      }
      CAS实现
      这种CAS式的单例模式算是懒汉式直接加锁的一个变种,sychronized是一种悲观锁,而CAS是乐观锁,相比较,更轻量级。
      当然,这种写法也比较罕见,CAS存在忙等的问题,可能会造成CPU资源的浪费。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public class Singleton_6 {  
          private static final AtomicReference<Singleton_6> INSTANCE = new AtomicReference<Singleton_6>();
          private Singleton_6() {
          }
          public static final Singleton_6 getInstance() {
              //等待
              while (true) {
                  Singleton_6 instance = INSTANCE.get();
                  if (null == instance) {
                      INSTANCE.compareAndSet(nullnew Singleton_6());
                  }
                  return INSTANCE.get();
              }
          }
      }
      枚举类型
      1

    双锁单例模式,为什么要双重校验?

    1. 线程安全的懒汉模式,每次访问对象都会加锁,浪费资源。把synchronized加在了方法的内部,一般的访问是不加锁的,只有在instance==null的时候才加锁。
    2. 外层判断减少不必要的同步开销。外层判断if (instance == null)可以快速检查实例是否已经被创建,如果已经创建,直接返回实例,避免了进入同步块带来的性能开销。因为进入同步块需要获取锁,而获取锁是一个相对耗时的操作,尤其是在高并发场景下。
    3. 内层判断防止多个线程同时通过外层判断并创建多个实例。内层判断if (instance == null)在同步块内再次检查实例是否已经被创建,只有在实例还未被创建时才执行创建实例的操作,这样就保证了无论有多少个线程试图获取单例实例,最终只会创建一个实例。
    4. volatile 修饰 instance防止指令重排。对于创建对象这个操作,JVM底层分为散布来做:1. 分配内存空间;2. 初始化对象;3. 将对象指向刚分配的内存空间。有些编译器为为了性能优化,可能会把第二步和第三步进行重排序。如果不使用volatile防止指令重排,如果在初始化对象阶段,有请求该对象,就会导致返回一个初始化未完成的对象。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      public class Singleton {
      private static volatile Singleton instance;

      private Singleton() {}

      public static Singleton getInstance() {
      if (instance == null) {
      synchronized (Singleton.class) {
      if (instance == null) {
      instance = new Singleton();
      }
      }
      }
      return instance;
      }
      }

    1. 工厂模式

    • 定义:工厂方法模式提供了一个创建对象的接口,但是将具体的对象创建延迟到子类中。这样,客户端代码不需要知道要创建的具体对象的类,只需要通过工厂方法来创建对象。这使得客户端代码与具体对象的创建解耦,提高了代码的灵活性和可维护性。
      在工厂方法模式中,通常会定义一个抽象工厂类,其中包含一个创建对象的抽象方法,而具体的对象创建则由具体的子类实现。这样,每个具体的子类都可以根据需要创建不同类型的对象,而客户端代码只需要通过抽象工厂类来调用工厂方法,而不需要关心具体的对象创建细节。
    • 用途:用于将对象的创建过程与使用过程分离,使得代码更灵活,便于扩展和维护,特别是在需要大量创建同类对象的情况下。
    • 抽象工厂模式:创建一系列相关或相互依赖的对象,这些对象属于一组相关的产品族。同时,系统需要保证这些产品族之间的一致性。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      interface Product {
      void use();
      }

      class ConcreteProductA implements Product {
      public void use() { System.out.println("Using Product A"); }
      }

      class Factory {
      public Product createProduct(String type) {
      if ("A".equals(type)) return new ConcreteProductA();
      return null;
      }
      }

    2. 代理模式

    定义:代理模式为一个对象提供一个替代者或占位符,以控制对这个对象的访问。代理可以在访问之前或之后添加额外的逻辑。
    用途:用于实现懒加载、访问控制、日志记录和事务管理等场景,提供了对真实对象的间接访问。

    3. 适配器模式

    定义:有两个不兼容的接口(即类或对象),但需要它们能够一起工作时,适配器模式可以解决这个问题。通过将一个类的接口转换为客户端期望的另一种接口,使得原本不兼容的接口能够一起工作。
    用途:用于解决现有类与新类之间接口不匹配的问题,使得它们能够合作,增强系统的灵活性和可重用性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Adaptee {
    void specificRequest() { System.out.println("Specific Request"); }
    }

    interface Target {
    void request();
    }

    class Adapter implements Target {
    private Adaptee adaptee = new Adaptee();
    public void request() { adaptee.specificRequest(); }
    }

    4. 装饰器模式

    定义:在某些情况下,我们需要在不修改现有对象结构的情况下,动态地添加功能或责任。装饰器模式允许在不改变对象结构的情况下,动态地给对象添加额外的职责或功能。
    用途:用于扩展类的功能,能够灵活地在运行时组合不同的装饰效果,提供比子类继承更灵活的扩展方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    interface Component {
    void operation();
    }

    class ConcreteComponent implements Component {
    public void operation() { System.out.println("ConcreteComponent Operation"); }
    }

    class Decorator implements Component {
    private Component component;
    public Decorator(Component component) { this.component = component; }
    public void operation() { component.operation(); System.out.println("Decorator Operation"); } //添加功能
    }

    5. 观察者模式

    定义:一个对象(主题)的状态发生改变,而其他对象(观察者)需要在状态改变时得到通知并进行相应的更新。但是,如果直接在对象之间建立硬编码的依赖关系,会导致系统的耦合度增加,难以维护和扩展。观察者模式试图解决这个问题,允许主题和观察者之间的松耦合通信。
    观察者模式定义了一种一对多的依赖关系,使得一个对象的状态改变可以自动通知并更新多个观察者对象。
    用途:用于实现发布-订阅机制,广泛应用于事件驱动系统、消息通知、GUI事件处理等场景。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import java.util.ArrayList;
    import java.util.List;

    class Subject {
    private List<Observer> observers = new ArrayList<>();
    void attach(Observer observer) { observers.add(observer); }
    void notifyObservers() { observers.forEach(Observer::update); }
    }

    interface Observer {
    void update();
    }

    class ConcreteObserver implements Observer {
    public void update() { System.out.println("Observer Updated"); }
    }

    6. 策略模式

    定义:策略模式定义了一系列可互换的算法,将每一个算法封装起来,并使它们可以独立变化。
    用途:用于选择适当的算法或行为,提供一个灵活的方式来替换对象的行为,尤其是在多种算法可以互换时。

    7. 模板模式

    定义:模板模式定义了一个算法的骨架,并将某些步骤的实现延迟到子类中。
    用途:用于代码复用和算法的结构化设计,子类可以在不改变算法结构的情况下,重定义某些特定步骤。

    8 场景题

    https://zhuanlan.zhihu.com/p/687160447

    并发相关

    一个线程需要拿到a,b,c三个线程的结果才能执行,用Java如何保证?

    1. 使用CountDownLatch实现
      CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。在这里可以将计数器初始化为 3(因为有 a、b、c 三个线程),每个线程完成任务后调用countDown方法将计数器减 1,等待的线程在计数器变为 0 时继续执行。

    7.1 Redis相关

    🌟如何使用redis实现一个分布式锁?

    如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。
    如何采用Redis来实现,一个分布式锁,首先考虑数据结构,一个普通的String类型即可。主要分为两点,加锁和释放锁。

    1. 使用Redis的SET命令,并带上几个参数来确保原子性操作。通常的命令格式为:SET lock_key unique_value NX PX timeout。其中:
      • lock_key 是锁的键名。
      • unique_value 是一个确保锁不会被其他进程释放的唯一值,通常可以使用UUID或者当前时间戳加上机器标识。
      • NX 表示只有当lock_key不存在时才设置值,确保了锁的原子性获取。
      • PX 指定键的过期时间(单位为毫秒),防止锁永远不被释放。
      • timeout 是锁的过期时间。
    2. 释放锁:释放锁时需要确保只有锁的持有者才能释放。可以使用Lua脚本执行以下操作来保证原子性:
      • 检查lock_key的值是否与unique_value相同。
      • 如果相同,则使用DEL命令删除lock_key
        1
        2
        3
        4
        5
        if redis.call("get",KEYS[1]) == ARGV[1] then 
        return redis.call("del",KEYS[1])
        else
        return 0
        end

    Redis来实现一小时内只能发送10次短信验证码–Redis List

    List结构

    1. Redis Key设计
      • 为每个用户或电话号码创建一个唯一的Redis key。比如,使用"sms:<phone_number>"的形式来唯一标识一个用户的短信历史记录。
    2. 发送记录存储
      • 使用Redis的LIST数据结构来存储发送验证码的时间戳。
      • 每次用户请求发送验证码时,向这个列表中推入当前时间戳。
    3. 限速检查
      • 在用户请求发送验证码时,首先从Redis中获取该用户的所有发送时间戳。
      • 过滤掉一小时之前的时间戳,保留一小时内的记录。
      • 检查保留时间戳的数量。如果数量少于10次,则允许发送;如果达到或超过10次,则拒绝发送。
    4. 清理过期数据
      • 过滤时间戳时,只保留一小时内的数据,并将其重新写回Redis。这样可以避免列表无限增长。

    Redis延时操作

    一、使用 Redis 键的过期时间特性

    1. 订单创建时设置键值对和过期时间
      • 当订单创建时,在 Redis 中为该订单创建一个对应的键值对。例如,可以使用订单号作为键,订单相关的信息(如订单状态为 “未付款”)作为值。
      • 同时,设置这个键的过期时间为 30 分钟。在 Redis 中,可以使用SET命令结合EX(以秒为单位设置过期时间)或者PX(以毫秒为单位设置过期时间)选项来实现。例如:
        1
        SET order:12345 "unpaid" EX 1800
        这里假设订单号为 12345,设置键order:12345的值为 “unpaid”,并设置过期时间为 1800 秒(30 分钟)。
        2. 检查订单状态
    • 在系统中有一个定时任务或者相关的业务逻辑,定期(例如每隔几分钟)检查 Redis 中订单键是否还存在。如果键已经不存在(意味着过期了),则可以认为订单未在 30 分钟内付款,执行取消订单的操作。
    • 例如,可以使用GET命令来检查订单键的值,如果GET命令返回nil,则表示键已过期,订单未付款,可以进行取消订单的业务逻辑,如更新数据库中的订单状态为 “已取消”。

    二、使用 Redis 的有序集合(Sorted Set)

    1. 订单创建时添加到有序集合
      • 当订单创建时,将订单相关信息添加到一个 Redis 的有序集合中。以当前时间加上 30 分钟的时间戳作为有序集合的分数(score),订单号作为成员(member)。
      • 例如,假设当前时间戳为$now,30 分钟后的时间戳为$expire_time,使用以下命令将订单添加到有序集合unpaid_orders中:
        1
        ZADD unpaid_orders $expire_time order:12345
    2. 定期检查有序集合中的订单
      • 有一个后台进程定期(例如每分钟)检查有序集合unpaid_orders中分数最小的成员(因为按照时间戳排序,最早到期的订单分数最小)。
      • 如果当前时间大于等于有序集合中最小分数(即订单到期时间),则可以从有序集合中移除该订单(ZREM命令),并执行取消订单的操作,如更新数据库中的订单状态为 “已取消”。例如:
        1
        2
        3
        4
        5
        $min_score = ZSCORE unpaid_orders order:12345
        if ($now >= $min_score) {
        ZREM unpaid_orders order:12345
        // 执行取消订单操作
        }

    做一个排行榜 (sorted set)

    使用 Redis 实现排行榜主要可以利用 Redis 的有序集合(Sorted Set)数据结构,以下是具体步骤:
    一、数据结构设计

    1. 选择有序集合
      • 有序集合在 Redis 中是一个键值对的数据结构,其中每个成员(member)都关联着一个分数(score)。对于排行榜来说,成员可以是用户 ID 或者其他能够唯一标识参与者的标识,分数则对应着排行榜的排名依据,如用户的积分、得分等。
        二、数据添加与更新
    2. 添加成员到排行榜
      • 当有新的用户或项目需要加入排行榜时,使用ZADD命令将其添加到有序集合中。例如,如果要根据用户的得分创建一个排行榜,命令格式如下:
      • ZADD ranking_key score member,其中ranking_key是排行榜的键名,score是用户的得分,member是用户的标识(如用户 ID)。例如:ZADD game_ranking 1000 user123表示将用户user123添加到game_ranking排行榜中,其得分为 1000。
    3. 更新成员分数
      • 当用户的得分发生变化时,再次使用ZADD命令更新其分数。由于ZADD命令如果成员已经存在会更新其分数,所以不需要额外的判断操作。例如,如果用户user123的得分增加到 1500,执行ZADD game_ranking 1500 user123
        三、获取排行榜信息
    4. 获取排行榜前 N 名
      • 使用ZREVRANGE命令可以获取排行榜中分数最高的前 N 个成员。例如,要获取game_ranking排行榜中的前 10 名用户,可以使用命令:ZREVRANGE game_ranking 0 9。这个命令会按照分数从高到低(ZREVRANGE中的REV表示反向)返回排行榜中的前 10 个成员(索引从 0 开始,所以 0 到 9 表示前 10 个)。
    5. 获取某个成员的排名
      • 使用ZRANK(分数从低到高排名)或者ZREVRANK(分数从高到低排名)命令来获取某个成员在排行榜中的排名。例如,要获取用户user123game_ranking排行榜中的排名(按照分数从高到低),可以使用命令:ZREVRANK game_ranking user123
    6. 获取成员的分数
      • 使用ZSCORE命令来获取某个成员的分数。例如,要获取用户user123game_ranking中的分数,执行命令:ZSCORE game_ranking user123

    统计网站UV (HyperLogLog)

    订单在30min之内未支付则自动取消 –RabbitMQ 死信队列

    使用RabbitMQ实现订单在30分钟内未支付自动取消的功能,可以通过RabbitMQ的死信交换机(DLX, Dead Letter Exchange)和消息的TTL(Time To Live)特性来实现。下面是具体实现的步骤:

    1. 设置死信交换机(DLX)和死信队列(DLQ)
      • 创建一个死信交换机,用于处理过期的消息。
      • 创建一个死信队列,用于接收和处理死信。
    2. 创建业务队列
      • 创建一个业务队列,用于存放订单消息。
      • 设置该业务队列的死信交换机为步骤1中创建的死信交换机。
    3. 设置消息的TTL
      • 当订单创建时,将订单信息作为消息发送到业务队列中。
      • 设置消息的TTL为30分钟。
    4. 编写消费者监听死信队列
      • 编写一个消费者,监听死信队列。
      • 当有消息进入到死信队列时,即表示订单已经超过30分钟未支付。
      • 消费者接收到消息后,执行取消订单的逻辑。

    数据压缩存储 bitmap

    如何快速判断海量数据中是否存在某一个元素?(布隆过滤器)

    https://www.bilibili.com/video/BV1Mf421q7pD/

    • HashSet; BitSet;
      Bloom 布隆过滤器 多次哈希,误判率
      一般来说可以采用布隆过滤器来判断。布隆过滤器是一种空间效率很高的数据结构,可以用于判断一个元素是否在集合中。它使用多个哈希函数将元素映射到一个bitmap中,判断元素是否存在时,通过多个哈希函数检查对应的位是否全部为1。但是因为哈希冲突的存在,几个元素的多次哈希可能会凭空造出一个不存在元素。所以布隆过滤器可以判断哪个元素一定不存在,但是判断一定存在的话有误判率。如果精确度要求比较高的话可以选择HashSet或者BitMap。

    如何在海量文本文件查找某一个单词在哪个文档中?(倒排索引)

    倒排索引(Inverted Index)是一种高效的全文搜索索引结构,非常适合用于快速查找文本数据中的某个单词。它将单词映射到包含该单词的文档或位置列表,使得查找和检索速度非常快。

    • 构建阶段
      • 遍历所有文本数据,对每个单词进行分词处理。
      • 对于每个单词,记录它出现的文档(或段落、句子)及其位置。
      • 建立一个映射关系,即单词到包含该单词的文档列表的映射。
    • 查询阶段
      • 当需要查找某个单词时,直接在倒排索引中查找该单词。
      • 如果找到了,就可以快速获得包含该单词的所有文档。

    top-k问题

    1. 完全排序
      • 思路
        • 先对整个数据集进行排序,例如使用快速排序、归并排序等高效的排序算法。如果要找最大的 k 个元素,排序后取前 k 个元素即可;如果要找最小的 k 个元素,取排序后的前 k 个元素。
      • 时间复杂度
        • 时间复杂度取决于所使用的排序算法。例如,快速排序的平均时间复杂度为,归并排序的时间复杂度为,其中 n 是数据集的大小。这种方法在数据集较小的时候比较有效,但当 n 很大时,对整个数据集排序可能会比较耗时。
    2. 部分排序
      • 思路
        • 可以使用堆排序的思想,构建一个大小为 k 的堆(如果找最大的 k 个元素,构建小顶堆;找最小的 k 个元素,构建大顶堆)。然后遍历数据集,对于每个元素,如果它比堆顶元素更符合要求(更大或更小,取决于找最大还是最小的 k 个元素),则替换堆顶元素,并调整堆结构。
      • 时间复杂度
        • 构建初始堆的时间复杂度为,遍历数据集并调整堆的时间复杂度为,其中 n 是数据集的大小,k 是要找的元素个数。整体时间复杂度为,当 k 远小于 n 时,这种方法比完全排序更高效。

    维护热搜

    有⼏台机器存储着⼏亿搜索日志,在资源有限的情况下,怎么选出搜索热度最⾼的⼗个?

    在资源有限的情况下,可以采用以下方法从几台存储着几亿搜索日志的机器中选出搜索热度最高的十个:

    一、数据预处理阶段

    1. 数据采样
      • 思路:由于数据量巨大,全量处理可能不现实。可以从每台机器上随机抽取一定比例的搜索日志作为样本数据。这样可以在一定程度上代表整体数据的分布情况,同时减少后续处理的数据量。
      • 示例:比如从每台机器上随机抽取 1% 的搜索日志,将这些样本数据收集起来进行后续分析。
    2. 数据清洗
      • 思路:对采样得到的搜索日志进行清洗,去除无效数据和异常数据。例如,去除格式错误的日志记录、重复的搜索记录等。
      • 作用:确保后续分析的数据质量,提高结果的准确性。

    二、热度计算阶段

    1. 哈希分组
      • 思路:根据搜索关键词对样本数据进行哈希分组。将具有相同关键词的搜索日志分到同一组中。这样可以方便地统计每个关键词的搜索热度。
      • 示例:使用哈希函数将关键词映射到不同的组,比如根据关键词的首字母或者某种特定的编码方式进行哈希。
    2. 热度统计
      • 思路:对于每个哈希组,统计该组关键词的搜索热度。热度可以用搜索次数来衡量。可以在每台机器上分别进行部分热度统计,然后将结果汇总。
      • 示例:在每台机器上维护一个哈希表,键为关键词,值为该关键词在这台机器上的搜索次数。统计完后,将每台机器上的哈希表进行合并,得到总的搜索次数。

    三、筛选 top-10 阶段

    1. 构建小顶堆
      • 思路:首先从所有关键词中选取前 k(k 稍大于 10,比如取 20)个热度较高的关键词及其热度值,构建一个小顶堆。堆中的每个元素包含关键词和其热度值。
      • 作用:小顶堆的特性可以保证堆顶元素始终是热度最小的元素。这样在后续处理中,当遇到热度更高的关键词时,可以方便地替换堆顶元素,从而逐步筛选出热度最高的十个关键词。
    2. 遍历剩余关键词
      • 思路:遍历剩余的关键词,对于每个关键词,将其热度值与堆顶元素的热度值进行比较。如果该关键词的热度值大于堆顶元素的热度值,则替换堆顶元素,并调整小顶堆使其保持堆的性质。
      • 示例:假设当前堆顶元素的热度值为 1000,遇到一个新的关键词,其热度值为 1200,则将堆顶元素替换为这个新的关键词,并重新调整堆结构。
    3. 确定 top-10
      • 思路:遍历完所有关键词后,小顶堆中的元素就是热度最高的若干个关键词。从中选取热度最高的十个关键词作为最终结果。
      • 作用:通过这种方式,可以在资源有限的情况下,较为高效地筛选出搜索热度最高的十个关键词。

    如何基于 UDP 协议实现可靠传输?

    大文件查询

    一个文件里有一百亿条数据,该怎么处理和保存,使得能够快速找到想要的数据?

    哈希到不同的桶里,对于每个桶,可以进行排序。
    如果每个桶里的数据还是很大,那就分治,例如分成10块,对每一块分别排序。
    寻找阶段:哈希值得到桶 -> 对于每个块 顺序/二分查找(当前块没有就继续下一个块)

    两个大文件找重复行

    遍历第一个文件,对每一行进行哈希运算,将哈希值作为键,将行号(或行内容)作为值存入哈希表中。
    遍历第二个文件,对每一行进行哈希运算,查找哈希表中是否已存在该哈希值,如果存在,则说明这一行与第一个文件中的某一行重复,记录下重复的行号(或行内容)。
    当遍历完第二个文件后,所有重复的行号(或行内容)都已经记录下来了。
    需要注意的是,由于文件较大,不能一次性将整个文件读入内存中处理,可以采用分块读取的方式,将文件分成若干个块,每次读取一个块进行处理,以减小内存的使用量。此外,为了提高查找速度,可以使用一些哈希算法和哈希表实现方案,如Rabin-Karp哈希算法和布隆过滤器等,以提高查找的效率和减小哈希表的空间占用。

    系统设计

    秒杀系统设计

    瞬时高并发、页面静态化、秒杀按钮、读多写少、缓存问题、库存问题、分布式锁、mq异步处理、如何让限流

    1. 瞬时高并发问题
    • 负载均衡:将请求分发到多个服务器上,分担压力。例如,可以采用硬件负载均衡设备如 F5 等,或者使用软件负载均衡方案如 Nginx 等。

    • 页面资源加载:大多数的请求都是一些商品详情等图片资源,为了减少不必要的服务端请求,通常情况下,会对活动页面做静态化处理。用户浏览商品等常规操作,并不会请求到服务端。只有到了秒杀时间点,并且用户主动点了秒杀按钮才允许访问服务端。可是也可以使用CDN内容分发网络来分散服务器的压力。

    • 读多写少问题:在秒杀的过程中,系统一般会先查一下库存是否足够,如果足够才允许下单,写数据库。如果不够,则直接返回该商品已经抢完。这种典型的读多写少问题问题,就非常适合用缓存来分担压力。可以使用Redis + 集群的方式来解决。

    • 缓存问题
      缓存击穿:初始时商品不在缓存以及缓存key过期,导致访问压力直接落在数据库上。

      1. 缓存预热:即事先把所有的商品,同步到缓存中,这样商品基本都能直接从缓存中获取到,就不会出现缓存击穿的问题了。
      2. 设置热点key永不过期:
      3. 加锁: 当缓存失效时,第一个请求发现缓存中没有数据,去查询数据库。在这个请求查询数据库的同时,其他请求被阻塞等待。第一个请求从数据库中查询到数据后,将数据写入缓存,然后释放锁。其他被阻塞的请求此时可以从缓存中获取数据,而不需要再去查询数据库。
      4. 在设置缓存数据的时候,同时设置一个逻辑过期时间。当读取缓存数据时,判断逻辑过期时间是否过期。如果逻辑过期时间已过期,启动一个异步线程去更新缓存数据,同时返回旧的数据给客户端。这样可以避免在缓存数据过期的瞬间,大量请求直接访问数据库。

      缓存穿透问题:如果有大量的请求传入的商品id,在缓存中和数据库中都不存在,这些请求不就每次都会穿透过缓存,而直接访问数据库了。

      1. 布隆过滤器:
    • 库存问题

    曝光系统设计

    排行榜设计

    信息流系统

    站内信(私信,@)

    设计邀请码

    如何实现点赞(实时显示点赞数)?

    分布式系统

    CAP

    一、CAP 的具体含义

    1. 一致性(Consistency)
      • 在分布式系统中,一致性是指多个节点的数据在同一时刻是否完全相同。如果对某个数据进行了更新操作,那么在所有节点上查询这个数据时,都应该返回相同的最新值。
      • 例如,一个在线购物系统中,用户修改了自己的收货地址,那么在所有涉及该用户数据的服务器节点上,这个地址都应该同步更新为新地址,以保证一致性。
    2. 可用性(Availability)
      • 可用性意味着系统能够及时响应客户端的请求。无论系统处于何种状态,只要客户端发起请求,系统就应该在合理的时间内返回响应结果,而不会出现长时间的等待或错误提示。
      • 比如,一个电商网站在任何时候都应该能够让用户顺利地浏览商品、下单购买等,不能因为系统的某些问题而导致用户无法访问或操作。
    3. 分区容错性(Partition tolerance)
      • 分布式系统中,由于网络故障、硬件故障等原因,节点之间的通信可能会出现中断,从而形成网络分区。分区容错性就是指系统在出现网络分区的情况下,仍然能够正常运行。
      • 例如,两个数据中心之间的网络连接中断,但是每个数据中心内部的系统仍然能够独立地为用户提供服务。

    二、CAP 理论的限制
    在一个分布式系统中,CAP 三者不可兼得。这是因为:

    • 如果要保证一致性,那么在数据同步的过程中,可能需要暂停系统的服务,以确保所有节点的数据都一致,这就会影响系统的可用性。
    • 如果要保证可用性,那么在数据同步不及时的情况下,就可能出现不同节点的数据不一致,从而牺牲了一致性。
    • 如果要保证分区容错性,那么在网络分区的情况下,为了保证系统的正常运行,可能需要在一致性和可用性之间做出权衡。

    三、实际应用中的取舍

    在实际的分布式系统设计中,不同的系统会根据业务需求对 CAP 进行不同的取舍:

    • 一些对数据一致性要求非常高的系统,如银行交易系统,可能会优先保证一致性和分区容错性,在一定程度上牺牲可用性。在网络分区的情况下,可能会暂停部分服务,以确保数据的一致性。
    • 而一些对可用性要求极高的系统,如电商网站、社交媒体平台等,可能会优先保证可用性和分区容错性,在一定程度上牺牲一致性。这些系统可以允许在网络分区的情况下,不同节点的数据暂时不一致,但要确保用户能够正常访问和使用系统。

    总之,CAP 理论为分布式系统的设计提供了重要的指导原则,帮助开发者在一致性、可用性和分区容错性之间做出合理的取舍。

    9 智力题

    详见下面的链接:

    https://blog.csdn.net/qq_46588810/article/details/122088043

    https://blog.csdn.net/qq_29966203/article/details/124213450

    • 赛马找最快

    • 砝码称轻重

    • 药瓶毒白鼠

      • 二进制
    • 绳子两头烧

    • 犯人猜颜色

      • 奇偶
    • 猴子搬香蕉

      • 临界值。把所有香蕉搬走1米需要吃掉3根,
    • 高楼扔鸡蛋

      • 不完善但够用: x + h/x 的极值点

      • 最优法:(x+1)*x/2 = h

    • 轮流取石子

    • 蚂蚁走树枝【有多个变种,这里只收集了最简单的情况】

    • 海盗分金币

    • 三个火枪手

      • 博弈,从后往前推
    • 囚犯拿豆子

    • 学生猜生日

    • 水王问题

      • 出现超过一半的,直接抵消
      • 【进阶】出现超过 1/n,每次抵消不同的n个

    人工智能相关

    常见机器学习算法

    • 线性回归(Linear Regression)
      • 原理:通过建立自变量和因变量之间的线性关系模型,即(其中是因变量,是自变量,是系数,是误差项)。目标是找到一组系数,使得预测值与真实值之间的误差平方和最小。
      • 应用场景:预测数值型数据,如房价预测(根据房屋面积、房间数量等因素预测房价)、销售量预测等。
    • 逻辑回归(Logistic Regression)
      • 原理:用于处理二分类问题(也可扩展到多分类)。它基于线性回归模型,通过逻辑函数(如函数,其中)将线性结果映射到区间,表示属于某一类别的概率。
      • 应用场景:信用评分(判断是否违约)、疾病诊断(判断是否患病)等。
    • 决策树(Decision Tree)
      • 原理:通过对数据特征进行一系列的条件判断,构建出树状结构。决策树的每个内部节点是一个属性上的测试,分支是测试输出,叶节点是类别或值。例如,根据天气(晴、雨、阴)、温度(高、中、低)等特征来决定是否外出活动。
      • 应用场景:数据分类和回归任务。在医疗诊断(根据症状判断疾病类型)、客户细分等方面有应用。
    • 随机森林(Random Forest)
      • 原理:是一种集成学习算法,由多个决策树组成。通过对训练集进行有放回抽样(自助采样法)构建多个子样本集,然后在每个子样本集上构建决策树,最后综合多个决策树的结果进行预测(分类任务通常采用投票法,回归任务采用平均法)。
      • 应用场景:广泛应用于数据挖掘、数据分析等领域,如预测股票价格走势、图像分类等。
    • 支持向量机(Support Vector Machine,SVM)
      • 原理:对于二分类问题,在特征空间中寻找一个超平面将不同类别的数据点分开,并且使两类数据点到超平面的最小距离(间隔)最大。对于非线性可分的数据,可以通过核函数将数据映射到高维空间使其线性可分。
      • 应用场景:文本分类、图像识别等领域,在小样本数据上表现较好。
    • K - 近邻算法(K - Nearest Neighbors,KNN)
      • 原理:根据待分类样本周围的个最近邻样本的类别来确定该样本的类别(分类任务),或者根据个最近邻样本的值来预测该样本的值(回归任务)。距离度量方式可以采用欧几里得距离、曼哈顿距离等。
      • 应用场景:数据分类、推荐系统(根据用户的相似邻居的喜好推荐商品)等。
    • 朴素贝叶斯(Naive Bayes)
      • 原理:基于贝叶斯定理和特征条件独立假设。对于分类问题,计算在给定特征下各个类别的后验概率,选择概率最大的类别作为预测结果。例如,在文本分类中,假设单词之间相互独立,根据文档中单词出现的频率来判断文档所属的类别。
      • 应用场景:文本分类、垃圾邮件过滤等。

    常见的神经网络模型

    • 多层感知机(Multilayer Perceptron,MLP)
      • 原理:由输入层、若干隐藏层和输出层组成。神经元之间通过权重连接,信号在神经元之间传递时经过激活函数(如、等)的非线性变换。通过反向传播算法调整权重,以最小化预测误差。
      • 应用场景:可以用于回归和分类任务,如手写数字识别、时间序列预测等。
    • 卷积神经网络(Convolutional Neural Network,CNN)
      • 原理:专门为处理具有网格结构数据(如图像、音频)而设计。主要包含卷积层(通过卷积核提取数据的局部特征)、池化层(对特征进行下采样,减少数据量)和全连接层(用于分类或回归)。例如在图像分类中,卷积层可以提取图像中的边缘、纹理等特征。
      • 应用场景:图像识别、目标检测、语义分割等计算机视觉领域,也在自然语言处理的部分任务中有应用。
    • 循环神经网络(Recurrent Neural Network,RNN)
      • 原理:具有循环结构,能够处理序列数据。在处理序列中的每个元素时,会将当前的隐藏状态传递到下一个时间步,从而保留了序列中的历史信息。但是存在梯度消失或梯度爆炸问题。
      • 应用场景:自然语言处理(如机器翻译、文本生成)、语音识别、时间序列分析等。
    • 长短期记忆网络(Long - Short Term Memory,LSTM)
      • 原理:是一种特殊的 RNN,通过引入门控机制(输入门、遗忘门、输出门)来解决 RNN 的梯度消失和梯度爆炸问题,能够更好地处理长序列数据。
      • 应用场景:与 RNN 类似,尤其在处理较长的文本、语音等序列数据时表现更优,如语音翻译、长篇小说生成等。
    • 门控循环单元(Gated Recurrent Unit,GRU)
      • 原理:是另一种改进的 RNN,结构比 LSTM 更简单,通过更新门和重置门来控制信息的流动,同样能够有效处理长序列数据。
      • 应用场景:自然语言处理、时间序列预测等领域,在一些任务中可以替代 LSTM 以提高计算效率。

    简单介绍一下transformer

    Transformer是一种基于自注意力机制的深度学习模型,最初由Google的研究者在2017年的论文《Attention is All You Need》中提出。它是为了解决自然语言处理(NLP)中的序列到序列(sequence to sequence)转换任务而设计的,但后来也被广泛应用于其他领域,如计算机视觉和语音识别。

    以下是Transformer模型的一些关键特点:
    架构

    1. 编码器和解码器:Transformer模型由编码器(Encoder)和解码器(Decoder)两部分组成。编码器用于处理输入序列,解码器用于生成输出序列。
    2. 自注意力机制:Transformer的核心是自注意力(Self-Attention)机制,它允许模型在处理序列数据时,能够在不同位置之间建立关联,捕捉长距离依赖关系。
    3. 多头注意力:Transformer使用了多头注意力(Multi-Head Attention)机制,将输入分割成多个“头”,每个“头”有自己的权重矩阵,可以在不同的表示子空间中并行地学习信息。

    特点

    1. 并行计算:由于自注意力机制不依赖于序列中的顺序位置,Transformer可以并行处理序列中的所有元素,显著提高了训练效率。
    2. 长距离依赖:传统的循环神经网络(RNN)和长短时记忆网络(LSTM)在处理长序列时存在梯度消失或爆炸的问题,而Transformer通过自注意力机制能够更好地处理长距离依赖。
    3. 可扩展性: Transformer模型可以扩展到处理非常长的序列,因为它不像RNN那样需要在时间步上依次处理。

    有哪些常用损失函数?

    1. 回归任务中的损失函数
      • 均方误差(Mean Squared Error,MSE)
        • 公式:,其中是真实值,是预测值,是样本数量。
        • 特点:它对误差进行平方操作,这使得较大的误差在损失中占比更大,从而对模型的惩罚更严重。MSE 是回归任务中最常用的损失函数之一,优化 MSE 可以使模型的预测值尽可能接近真实值,常用于线性回归等模型的训练。
      • 平均绝对误差(Mean Absolute Error,MAE)
        • 公式:。
        • 特点:与 MSE 不同,MAE 计算的是预测值与真实值之间误差的绝对值之和。MAE 对异常值不像 MSE 那样敏感,因为它没有对误差进行平方放大。在一些对异常值比较敏感的回归问题中,MAE 可能是一个更好的选择。
      • 均方根误差(Root Mean Squared Error,RMSE)
        • 公式:。
        • 特点:RMSE 实际上是 MSE 的平方根。它与预测值和真实值具有相同的量纲,这使得在比较不同模型或者不同数据集上的结果时更直观,在评估回归模型性能方面也较为常用。
    2. 分类任务中的损失函数
      • 交叉熵损失(Cross - Entropy Loss)
        • 二分类情况
          • 对于二分类问题,假设,预测概率为,则交叉熵损失函数为。
          • 它衡量的是预测概率分布与真实标签分布之间的差异。在二分类任务中,如逻辑回归模型的训练,常使用交叉熵损失函数来优化模型,使得模型输出的预测概率尽可能接近真实标签。
        • 多分类情况
          • 对于多分类问题,假设类别数为,真实标签是一个 one - hot 向量(只有一个元素为 1,其余为 0,表示所属类别),预测概率向量为,则交叉熵损失函数为。在神经网络用于多分类任务(如图像分类)时,交叉熵损失函数被广泛应用。
      • 对数损失(Log Loss):本质上与交叉熵损失在二分类情况下是相同的概念,同样用于衡量预测概率与真实标签之间的差异。
      • 铰链损失(Hinge Loss)
        • 公式(二分类情况):,其中,是预测值。
        • 特点:主要用于支持向量机(SVM)。铰链损失函数鼓励正确分类且具有较大的间隔(margin),即不仅要分类正确,还要使分类的可信度足够高。在处理线性可分数据时,通过最小化铰链损失来找到最优的分类超平面。
      • KL 散度(Kullback - Leibler Divergence)
        • 公式:,其中是真实分布,是预测分布。
        • 特点:也称为相对熵,它衡量的是两个概率分布之间的差异。在一些生成模型(如变分自编码器)中会使用 KL 散度作为损失函数的一部分,用于使生成模型的输出分布尽可能接近真实分布。

    有哪些常用的激活函数?

    在深度学习中,激活函数是神经网络中不可或缺的部分,它们决定了神经元是否应该被激活,即它们在给定的输入下是否应该传递信号。以下是一些常用的激活函数:

    1. Sigmoid(S型函数)
      • 公式:
        $$f(x) = \frac{1}{1 + e^{-x}}
        $$
      • 范围:( (0, 1) )
      • 特点:可以将输入压缩到0和1之间,常用于二分类问题的输出层。
    2. Tanh(双曲正切函数)
      • 公式:
        $$
        f(x) = \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}
        $$
      • 范围:( (-1, 1) )
      • 特点:输出范围在-1到1之间,比Sigmoid函数的输出范围更广,且中心化效果更好。
    3. ReLU(修正线性单元)
      • 公式:
        $$
        f(x) = \max(0, x)
        $$
      • 范围:( [0, +\infty) )
      • 特点:在正数部分是线性的,在负数部分输出为0,计算简单,在深度学习中使用非常广泛。
    4. Leaky ReLU(带泄露的修正线性单元)
      • 公式:
        $$
        f(x) = \max(0.01x, x)
        $$
      • 范围:( (-\infty, +\infty) )
      • 特点:在负数部分有一个小的斜率(如0.01),解决了ReLU在负数区域梯度为0的问题。
    5. Parametric ReLU (PReLU)
      • 公式:
        $$
        f(x) = \max(\alpha x, x)
        $$
      • 范围:( (-\infty, +\infty) )
      • 特点:与Leaky ReLU类似,但负数部分的斜率α是一个可学习的参数。
    6. ELU (指数线性单元)
      • 公式:
        $$ f(x) = \begin{cases}
        x & \text{if } x > 0 \
        \alpha (e^x - 1) & \text{if } x \leq 0
        \end{cases}
        $$
      • 范围:( (-\alpha, +\infty) )
      • 特点:在负数部分使用指数函数,能够有负值,解决了ReLU的一些问题。
    7. Softmax
      • 公式:
        $$ f(x_i) = \frac{e^{x_i}}{\sum_j e^{x_j}}
        $$
      • 范围:( (0, 1) ) 且所有输出的和为1
      • 特点:常用于多分类问题的输出层,可以将输入转换为概率分布。
    8. Swish
      • 公式:
        $$ f(x) = x \cdot \sigma(x) $$
      • 范围:( (-\infty, +\infty) )
      • 特点:基于Sigmoid函数,表现通常比ReLU好,但计算成本较高。
    9. GELU (高斯误差线性单元)
      • 公式:
        $$ f(x) = x \cdot \Phi(x) $$
      • 范围:( (-\infty, +\infty) )
      • 特点:在最近的Transformer模型中非常流行,基于高斯分布。
        选择哪种激活函数通常取决于具体的应用场景和深度学习模型的需求。例如,ReLU和其变体在隐藏层中非常流行,而Softmax和Sigmoid通常用于输出层。随着研究的深入,可能会出现新的激活函数。