Skip to content

zhouxiaofu/Java-Interview-Guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 

Repository files navigation

Java基础

HashMap(Java8)

  1. 结构优化:在 Java 8 中,当一个 bucket(桶)中的元素过多时(默认超过 8 个),HashMap 会将这个 bucket 中的链表转换成红黑树,这大大提高了在高冲突情况下的查找效率。如果元素数量减少,红黑树又会退化成链表。
  2. Hash 函数:HashMap 使用内部的 hash 函数来分配元素的存储位置。在 Java 8 中,hash 函数被改进以减少碰撞。
  3. 并发改进:尽管 HashMap 本身不是线程安全的,但在 Java 8 中,它的并发行为得到了一定的改善,特别是在扩容过程中。然而,对于需要高并发的应用,通常建议使用 ConcurrentHashMap
  4. 空间效率和性能:Java 8 的 HashMap 实现在空间效率和性能方面都有所改进,特别是通过树化 bucket 和优化 hash 函数来减少碰撞。
  5. 默认容量和加载因子:默认的初始容量是 16,加载因子是 0.75。加载因子是一个衡量 HashMap 满的程度的指标,当 HashMap 的填充度超过加载因子时,将会进行扩容(即重哈希)。
  6. 计算模式:Java 8 引入了一些新的 API,如 computeIfAbsentcomputeIfPresent,这些方法结合了检查、获取和计算的步骤,使得一些常见的模式变得更加简洁。

为什么bucket数量需要是2的次方?

  1. 高效的索引计算:当桶的数量是 2 的 n 次方时,计算元素应该放入哪个桶的公式可以简化为使用位运算(hash & (length-1)),其中 length 是桶的总数。位运算比模运算(hash % length)要快,因为模运算在硬件层面上的执行效率相对较低。
  2. 均匀分布:哈希表的一个关键目标是尽量避免哈希冲突,将数据均匀分布到各个桶中。利用位运算可以更好地分散哈希值,尤其是当哈希函数较为均匀时,这种方法能够有效地利用哈希值的特性,实现数据的均匀分布。
  3. 扩容时的数据迁移效率:在 HashMap 扩容时(例如,当元素数量超过容量与加载因子的乘积),容量扩大到原来的两倍。由于容量始终是 2 的次方,所以在扩容过程中,元素的重新分布可以通过简单的位运算来完成,而不需要重新计算哈希值。这样可以大大提高扩容的效率。

AQS

  1. 核心概念:AQS 是一个提供了锁和其他同步机制的基础框架。它使用一个 int 类型的变量来表示同步状态,并通过一个 FIFO 队列来管理阻塞线程。
  2. 同步状态:AQS 使用一个内部的 volatile int 类型的变量来表示同步状态。子类可以通过改变这个状态来实现锁的获取和释放。
  3. 节点和等待队列:AQS 内部维护了一个等待队列,每个节点(Node)代表一个等待获取锁的线程。如果线程无法获取锁,则会被包装成节点加入到队列的尾部,并在必要时阻塞。
  4. 获取和释放锁:AQS 提供了方法来管理对同步状态的获取和释放。子类通过重写这些方法来实现具体的锁机制。
    • tryAcquire:尝试获取资源,成功则返回 true,失败则返回 false。
    • tryRelease:尝试释放资源,成功则返回 true,失败则返回 false。
  5. 独占模式和共享模式:AQS 支持两种同步模式。独占模式(如 ReentrantLock)一次只允许一个线程持有资源,而共享模式(如 Semaphore、CountDownLatch)则允许多个线程同时持有资源。
  6. 公平性和非公平性:AQS 可以支持创建公平锁和非公平锁。公平锁按照请求的顺序授予锁,而非公平锁则可能插队。
  7. 重要的子类:基于 AQS 实现的同步器包括 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等。

ReentrantLock和synchronized的区别

  1. 基本锁机制和工作原理
    • synchronized:内置于 Java 语言中,提供了一种基于监视器模式的同步机制。它可以用于方法(实例方法和静态方法)和代码块上,且是隐式的,不需要手动加锁和释放锁。
    • ReentrantLock:是 Java.util.concurrent.locks 包的一部分,提供了一种高级的同步机制。它需要显式地创建锁对象,手动进行加锁和解锁。
  2. 锁的公平性选项
    • synchronized:不支持公平性。不保证等待时间最长的线程会首先获取锁。
    • ReentrantLock:支持公平性和非公平性。可以在创建锁时指定,确保按照等待时间的顺序获取锁。
  3. 灵活性和控制能力
    • synchronized:较少的控制能力。不能尝试非阻塞地获取锁,也不能中断一个正在等待获取锁的线程。
    • ReentrantLock:更高的灵活性。提供了尝试非阻塞地获取锁(tryLock())、可中断的锁获取操作(lockInterruptibly())以及支持条件变量(Condition)。
  4. 对性能的影响
    • synchronized:在 JDK 1.6 之后,通过引入偏向锁和轻量级锁等机制,性能大幅提升,但在高竞争环境下可能仍然落后于 ReentrantLock
    • ReentrantLock:在高度竞争的环境下,通常提供比 synchronized 更好的性能,但在低竞争环境下,性能差异不大。
  5. 最佳使用场景
    • synchronized:适合简单的同步需求,以及锁竞争不是非常激烈的场景。因为其语法简洁,易于阅读和维护。
    • ReentrantLock:适合复杂的同步需求,或者需要更多同步控制,如公平锁、可中断锁、条件变量等。它提供了更大的灵活性,但需要更仔细的锁管理。

synchronized 锁的升级过程

  1. 偏向锁(Biased Locking)
    • 当一个线程首次获得锁时,锁会进入偏向模式,此时锁会偏向于第一个获取它的线程。
    • 如果在此后的执行过程中,没有其他线程尝试获取这个锁,那么持有偏向锁的线程可以无需同步操作直接进入同步块。
    • 偏向锁适用于只有一个线程访问同步块的情况。
  2. 轻量级锁(Lightweight Locking)
    • 当有另一个线程尝试获取已经被偏向的锁时,偏向锁会升级为轻量级锁。
    • 轻量级锁的工作机制是,线程会在对象的标记字中存储锁记录(Lock Record)的指针。
    • 如果尝试获取锁的线程发现对象已经被锁定(即存在有效的锁记录),它会在自己的栈帧中创建一个锁记录的副本(称为 Displaced Mark Word),然后尝试使用 CAS 操作将对象头的标记更新为指向该锁记录的指针。
    • 如果 CAS 操作成功,当前线程获取锁;如果失败,表明有其他线程竞争,锁会升级为重量级锁。
  3. 重量级锁(Heavyweight Locking)
    • 当多个线程同时竞争同一个锁时,轻量级锁会升级为重量级锁。
    • 重量级锁会使其他尝试获取锁的线程进入阻塞状态。
    • 这种锁的实现依赖于操作系统的 Mutex 机制,虽然增加了线程切换的开销,但在高度竞争的环境下可以保证线程安全。

各种for循环的效率和使用场景

  1. 传统的 for 循环(for (int i = 0; ...)
    • 效率:通常来说,在遍历数组时效率最高,因为它允许直接通过索引访问元素,减少了额外的方法调用。
    • 使用场景:适用于需要访问数组索引或需要修改遍历过程中的计数器值的场景。
  2. for-each 循环
    • 效率
      • 当遍历集合时,for-each 循环是基于 Iterator 实现的。其效率通常与传统 for 循环相近,但在某些情况下(尤其是在遍历大型集合时)可能会略低于传统 for 循环,因为它涉及到 Iterator 对象的创建和方法调用的额外开销。
      • 当遍历数组时,for-each 循环是基于索引的循环,其效率与传统的基于索引的 for 循环相当。
    • 使用场景
      • for-each 循环适用于不需要直接索引访问,只需遍历集合或数组中的每个元素的场景。
      • 它提供了一种更简洁、更易读的遍历方式,特别是在代码的可读性和简洁性更为重要时。
  3. Stream API 的 forEach
    • 效率:Stream forEach 在单线程情况下效率通常低于传统 for 循环和 for-each 循环,因为它涉及到更多的函数调用和中间操作。然而,Stream API 支持并行处理,这在处理大数据集时可能带来性能优势。
    • 使用场景:适用于更加函数式的编程风格,以及需要链式操作(如过滤、映射、排序等)的场景。当数据集非常大或者需要并行处理以提高性能时,使用并行 Stream (parallelStream().forEach(...)) 是一个好选择。

Mybatis

MyBatis 批量插入数据的效率比较

在使用 MyBatis 进行数据持久化时,批量插入是一个常见的需求,尤其是在处理大量数据时。了解不同批量插入技术的效率对于优化性能至关重要。以下是两种常用的批量插入方法及其效率比较:

1. 使用 foreach 标签

特点

  • foreach 标签用于生成一条包含多个插入操作的 SQL 语句。
  • 简化了代码,易于理解和实现。
  • 减少了与数据库的交互次数。

适用场景

  • 适合小到中等规模的数据量。
  • 当数据量非常大时,可能会遇到 SQL 语句长度限制或性能下降的问题。

2. 使用 JDBC 批处理(ExecutorType.BATCH

特点

  • 利用 JDBC 的批处理功能,允许一次性执行多条独立的 SQL 语句。
  • 减少网络往返次数,提高了大规模数据处理的效率。
  • 可以控制每次批处理的大小,避免超出数据库限制。

适用场景

  • 特别适用于大规模数据的插入。
  • 需要细致的错误处理机制,因为一次批处理中的某个操作失败可能会影响到整个批次的操作。

效率比较

  • 小到中等规模数据:使用 foreach 标签的方法更为简单和直接,通常效率足够高。
  • 大规模数据:使用 JDBC 批处理的方式效率更高,特别是在处理非常大量的数据时,它能提供更优的性能和资源利用率。

概念

CAS

在 Java 中,CAS(Compare And Swap,比较并交换)是一种无锁的原子操作,用于实现线程安全的操作。CAS 是并发编程中的一种重要技术,尤其在高并发环境下,CAS 可以实现较高的性能,因为它避免了传统的加锁机制。

CAS 的工作原理

CAS 机制主要通过三步操作来完成更新:

  1. 比较(Compare):判断内存中的值是否等于预期值。
  2. 交换(Swap):如果内存中的值与预期值相等,就将内存值更新为新值。
  3. 返回结果:如果更新成功,返回 true,否则返回 false

CAS 的优点

  1. 高效:CAS 是一种非阻塞算法,可以避免传统锁带来的线程阻塞和上下文切换开销,具有更高的性能。
  2. 线程安全:CAS 操作是原子性的,不会出现线程安全问题。
  3. 无锁设计:CAS 可以实现无锁设计,适合高并发环境。

CAS 的缺点

  1. ABA 问题
    • CAS 可能会遇到 ABA 问题。即某个线程在读取到的值为 A 后,另一个线程将值更改为 B,然后再改回 A。CAS 操作会认为没有发生变化,导致逻辑错误。
    • Java 中的 AtomicStampedReference 提供了解决方案,可以通过“版本号”来解决 ABA 问题。
  2. 自旋消耗 CPU
    • 如果 CAS 操作一直失败,线程会不断尝试更新(称为“自旋”),这会导致 CPU 资源浪费。
    • 自旋会在高并发下带来一定的性能开销。
  3. 只能保证一个共享变量的原子操作
    • CAS 仅能保证一个变量的原子操作。如果需要对多个变量进行原子操作,可以考虑使用锁,或者将多个变量封装在一个对象中,通过原子引用来更新。

乐观锁、悲观锁

乐观锁

乐观锁是一种假设不会有并发冲突的策略,即假设同一数据的多个线程或事务不会经常发生冲突。在更新数据之前,会检查数据是否已被其他线程或事务更改,如果未更改,则完成更新;如果已经被更改,则放弃操作或重新尝试。

优点

  • 无锁设计:不需要数据库加锁,性能高。
  • 适用于并发量小、冲突少的场景

缺点

  • 需要重试:如果冲突频繁,可能需要多次重试,影响性能。
  • 不适用于高冲突场景:如果冲突多,会导致多次失败和重试,降低效率。

悲观锁

悲观锁是一种假设会有并发冲突的策略,即假设同一数据的多个线程或事务会发生冲突。悲观锁会在对数据进行操作前,先加锁以确保其他线程或事务不能访问该数据,从而保证数据操作的排他性。

优点

  • 适用于高冲突场景:悲观锁会在操作数据前锁定数据,确保数据的一致性。
  • 数据一致性高:在高并发写操作下,能更好地保证数据的正确性和完整性。

缺点

  • 性能开销大:悲观锁在加锁期间会阻塞其他线程,可能导致系统性能下降。
  • 容易产生死锁:如果多个线程之间加锁顺序不当,可能会造成死锁。

JVM

jvm内存模型

  1. 堆(Heap)
    • JVM 中最大的内存区域。
    • 存储所有 Java 类实例和数组。
    • 堆内存在 JVM 启动时创建,是垃圾收集器管理的主要区域。
  2. 方法区(Method Area)
    • 存储已被虚拟机加载的类信息、常量、静态变量等数据。
    • 也称为“永久代”(Permanent Generation),在 Java 8 中被元空间(Metaspace)所替代。
  3. 栈(Stack)
    • 存储局部变量和部分方法执行过程中的信息。
    • 每个线程有自己的栈,随着方法调用而动态扩展和收缩。
  4. 程序计数器(Program Counter)
    • 当前线程所执行的字节码的行号指示器。
    • 每个线程都有自己的程序计数器。
  5. 本地方法栈(Native Method Stack)
    • 为 JVM 使用的 Native 方法服务。
    • 与 Java 栈类似,但服务于 Native 方法调用。

垃圾回收(GC)

JVM(Java 虚拟机)的垃圾回收(GC)是自动管理内存的过程。它的主要目的是识别并丢弃那些不再被应用程序使用的对象,以释放并重用内存。以下是 JVM 垃圾回收的关键概念:

  1. 垃圾回收根
    • GC 从所谓的“根”(如线程栈和静态字段)开始寻找,这些根是活动的引用。
  2. 可达性分析
    • 从根集开始,GC 进行可达性分析,识别出所有从根集可达的对象。无法从任何根到达的对象被认为是垃圾。
  3. 标记-清除算法
    • 最基本的垃圾回收算法。它分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,然后统一清除这些对象。
  4. 复制算法
    • 主要用在新生代。内存分为两块,每次只使用其中一块。当这一块的内存用完时,将还活着的对象复制到另一块内存上,然后清理掉这一块内存上的所有对象。
  5. 标记-整理算法
    • 用于老年代的一种方法。标记过程与标记-清除算法相同,但后续步骤是将所有存活的对象压缩到内存的一端,然后清理掉边界以外的内存。
  6. 分代收集理论
    • JVM 的堆分为新生代和老年代。新生代中对象生命周期短,老年代中对象生命周期长。根据这个特性,使用不同的收集算法来提高垃圾回收的效率。
  7. 垃圾回收器
    • 不同的 JVM 实现提供了不同的垃圾回收器,如 Serial、Parallel、CMS(Concurrent Mark-Sweep)、G1 等。每种回收器都有自己的特点,适用于不同类型的应用。

JVM内存管理:新生代与老年代

在 JVM(Java 虚拟机)中,堆内存被分为几个区域以优化垃场的回收,其中最重要的区域是新生代(Young Generation)和老年代(Old Generation)。以下是它们的详细说明:

  1. 新生代(Young Generation)
    • 新生代是堆内存的一个区域,用于存储新创建的对象。
    • 大多数情况下,新创建的对象首先被分配到新生代。
    • 新生代通常被进一步分为三个部分:一个 Eden 空间和两个幸存者空间(Survivor Spaces,通常称为 S0 和 S1)。
    • 新生代的特点是它经常进行垃圾回收,也称为 Minor GC。由于大多数新创建的对象很快就变得不可达(例如,局部变量),因此 Minor GC 通常很快且频繁地执行。
  2. 老年代(Old Generation)
    • 老年代用于存储生命周期较长的 Java 对象。
    • 当对象在新生代中存活了足够长的时间(经过了多次 Minor GC)后,就会被晋升到老年代。
    • 老年代的大小通常远大于新生代,且其垃圾回收频率较低,但每次回收的时间比新生代要长。在老年代进行的垃圾回收称为 Major GC 或 Full GC。
  3. 永久代/元空间(PermGen/Metaspace)
    • 在较早版本的 JVM 中,还有一个称为永久代(PermGen)的区域,用于存储类的元数据、方法对象等。
    • 从 Java 8 开始,永久代被移除,取而代之的是元空间(Metaspace)。元空间不在虚拟机内存中,而是使用本地内存。

这种内存区域的划分是为了优化垃圾回收效率。由于不同区域的对象生命周期不同,JVM 可以采用不同的策略来管理这些区域,从而提高整体的内存管理效率。

线上

如何处理线上的OOM错误

  1. 生成堆转储
    • 使用 jmap 工具生成堆转储文件:jmap -dump:live,format=b,file=/tmp/heapdump.hprof <PID>
    • <PID> 是发生 OOM 的 Java 进程 ID。
  2. 分析堆转储
    • 使用 Heap Analyzer Tool(MAT)或 VisualVM 等工具分析堆转储文件。
    • 寻找内存泄漏的对象、频繁分配的对象以及占用大量内存的对象。
  3. 检查 GC 日志
    • 分析 GC 日志了解垃圾回收行为。如果没有启用 GC 日志,考虑添加启动参数 -Xloggc:<file-path> 开启。
  4. 调整 JVM 参数
    • 根据分析结果,考虑调整 JVM 堆大小 -Xmx-Xms,或调整垃圾回收策略和参数。
  5. 代码优化
    • 对于内存泄露,查找并修复代码中的问题。
    • 对于过度消耗内存的场景,优化数据结构和算法,减少内存占用。
  6. 压力测试
    • 在进行调整后,通过压力测试验证改动的效果,确保问题得到解决。

CPU 占满排查

  1. 使用 top 或任务管理器确认哪个 Java 进程导致高 CPU/内存/磁盘 使用。
  2. 获取问题进程的线程堆栈:jstack <PID>
  3. 分析线程堆栈,寻找 RUNNABLE 状态的线程。
  4. 利用工具如 VisualVM 进行更深入分析。
  5. 检查应用日志寻找异常信息。

spring boot

Bean注册过程

  1. 资源定位:首先,Spring 通过配置文件或注解找到 Bean 的定义。
  2. 载入 Bean 定义:通过读取配置文件或注解,Spring 解析并将 Bean 定义加载到容器中。
  3. 注册 Bean:解析后的 Bean 定义被注册到 Spring 容器内。(解析一个注册一个)
  4. 依赖注入:容器分析 Bean 间的依赖关系,并完成相应的依赖注入。
  5. 初始化:创建 Bean 实例,并执行任何初始化逻辑。
  6. 后处理:应用如 AOP 的额外处理。

Bean相关的注解

  1. @Bean
  • 用于在 Spring 管理的类中声明一个 Bean。
  • 可以用于任何 Spring 管理的组件,但通常推荐在 @Configuration 类中使用。
  • 定义方法返回的对象将作为一个 Bean 被 Spring 容器管理。
  1. @Configuration
  • 用于定义配置类,这些类可以包含一个或多个 @Bean 注解的方法。
  • @Configuration 注解的类本身也会作为一个 Bean 被注册到 Spring 容器中。
  • @Configuration 类中的 @Bean 方法被特殊处理,以确保多次调用返回同一个实例。
  1. @Component
  • 基础注解,用于声明一个组件(Bean)。
  • 用于那些不易归类为服务(@Service)或存储库(@Repository)的 Bean。
  • @Repository@Service@Controller 都是这个注解的特化形式。
  1. @Service
  • 用于标注服务层组件。
  • 用于业务逻辑层(Service Layer),表明类是业务逻辑相关的 Bean。
  1. @Repository
  • 用于标注数据访问层组件。
  • 用于数据库访问操作,表明类是数据访问对象(DAO)。
  1. @Controller
  • 用于标注控制层组件。
  • 用于 Web 应用的 MVC 模式,处理 HTTP 请求。
  1. @RestController
  • 结合了 @Controller@ResponseBody 的功能。
  • 用于创建 RESTful Web 服务,处理 HTTP 请求并以 JSON 或 XML 形式返回数据。

@Service、@Repository、@Controller的区别

  1. @Service
    • 标注在服务层(业务逻辑层)的类上。
    • 主要用于标识业务逻辑服务组件。
    • Spring 框架可以对其进行一些特定的业务逻辑层面的处理,但在大多数情况下,@Service 注解仅作为一种标识,无额外的技术影响。
  2. @Repository
    • 标注在数据访问层(DAO)的类上。
    • 主要用于标识数据访问组件。
    • Spring 通过 @Repository 注解可以提供数据访问相关的异常转换功能。它可以将数据库异常转换为 Spring 的数据访问异常。
  3. @Controller
    • 标注在表现层(如 Web 控制器)的类上。
    • 主要用于标识处理 HTTP 请求的控制器组件。
    • @Controller 注解的类通常会配合 @RequestMapping 或其他相关注解来处理 HTTP 请求和返回相应的视图或数据。

AOP

  1. AOP 概念:AOP 是一种编程范式,用于将跨多个模块的关注点(如日志、安全、事务等)分离出来,以提高代码的模块化。
  2. 切面(Aspect):切面是 AOP 的核心,它将横切关注点封装成独立的模块。一个切面可以定义多个通知(Advice)和切点(Pointcut)。
  3. 通知(Advice):通知定义了切面要完成的工作以及何时执行这些工作。常见的通知类型有:前置(Before)、后置(After)、返回后(After-returning)、抛出异常后(After-throwing)和环绕(Around)。
  4. 切点(Pointcut):切点定义了通知应用的位置,即哪些方法或类会触发切面的执行。
  5. 织入(Weaving):将切面应用到目标对象并创建代理对象的过程称为织入。这可以在编译时(编译时织入)、类加载时(加载时织入)或运行时(运行时织入,Spring 使用这种方式)进行。
  6. 代理(Proxy):在 Spring AOP 中,AOP 实现通常通过动态代理完成。这意味着为目标对象动态地创建一个代理,代理会拦截对目标方法的调用,并根据切面的定义在调用前后执行相关逻辑。

class加载过程

  1. 加载(Loading)
    • 在这个阶段,JVM 通过类加载器(ClassLoader)读取二进制数据(通常是 .class 文件)并将其转换为一个 java.lang.Class 对象。这个过程涉及到查找字节码,并从文件系统、网络或其他源加载字节码。
  2. 链接(Linking)
    • 验证(Verification):确保加载的类符合 JVM 规范,没有安全问题。
    • 准备(Preparation):为类变量分配内存,并设置默认初始值。
    • 解析(Resolution):将类、接口、字段和方法的符号引用替换为直接引用。
  3. 初始化(Initialization)
    • 在这个阶段,JVM 执行类构造器 <clinit>() 方法的代码。这包括静态变量的初始化和静态代码块的执行。
  4. 使用(Using)
    • 类实例可以在程序中被使用,涉及到创建类的实例、调用类的方法、使用类的变量等。
  5. 卸载(Unloading)
    • 类卸载是由 JVM 的垃圾回收器处理的。当一个类的 ClassLoader 和该类的所有实例都不再被引用时,JVM 就会卸载这个类。

spring boot自动装配过程

  1. 启动类上的注解:Spring Boot 应用的入口类通常标有 @SpringBootApplication 注解,这个注解是一个方便的注解,结合了 @Configuration@EnableAutoConfiguration@ComponentScan
  2. 启用自动配置@EnableAutoConfiguration 注解是自动配置的关键,它告诉 Spring Boot 开始查看添加到类路径中的 jar,基于这些 jar 提供的类和配置来配置应用程序。
  3. 读取 spring.factories:Spring Boot 会加载类路径上所有 META-INF/spring.factories 配置文件,这些文件列出了所有可用的自动配置类。
  4. 条件评估:每个自动配置类可以使用条件注解(如 @ConditionalOnClass@ConditionalOnBean),这些注解确保只在满足特定条件时才启用自动配置。Spring Boot 会评估这些条件,并决定是否应用每个自动配置类。
  5. Bean 的创建和注册:满足条件的自动配置类将被处理,它们内部定义的 Beans 会根据条件创建并注册到 Spring 容器中。这包括对一些常用库的自动配置,如数据库连接、消息服务、Web MVC 设置等。
  6. 属性绑定:Spring Boot 会使用 application.propertiesapplication.yml 文件中的属性来覆盖自动配置提供的默认值。
  7. 覆盖和自定义:如果需要,开发者可以自定义额外的配置来覆盖自动配置提供的 Bean,或者通过创建自己的配置类来扩展自动装配。
  8. 应用准备就绪:完成上述步骤后,应用配置就绪,可以开始接受服务请求或执行业务逻辑。

spring boot事物传播级别

Spring Framework 提供了多种事务传播行为,这些行为定义了事务方法是如何在事务上下文中执行的。在 Spring Boot 中,这些传播级别可以通过 @Transactional 注解的 propagation 属性来设置。以下是常用的事务传播级别:

  1. REQUIRED(默认)
    • 如果当前存在事务,那么方法将在该事务中运行;否则,将创建一个新事务。
  2. SUPPORTS
    • 如果当前存在事务,方法将在该事务中运行;如果当前没有事务,那么方法将在非事务的环境中执行。
  3. MANDATORY
    • 如果当前存在事务,方法将在该事务中运行;如果没有事务,则抛出异常。
  4. REQUIRES_NEW
    • 总是创建一个新事务,如果当前存在事务,则暂停当前事务。
  5. NOT_SUPPORTED
    • 总是非事务性地执行,并暂停任何现有事务。
  6. NEVER
    • 总是非事务性地执行,如果存在一个活动事务,则抛出异常。
  7. NESTED
    • 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则其行为与 REQUIRED 一样。

这些传播行为允许你精细地控制事务的边界和方式,非常适合处理复杂的业务场景。正确选择合适的传播级别可以帮助避免一些常见的问题,比如脏读、不可重复读、幻读以及长时间占用数据库资源等。

分布式、微服务

分布式事物

  1. 两阶段提交(2PC, Two-Phase Commit)
    • 这是一种经典的分布式事务协议。它分为两个阶段:准备阶段和提交/回滚阶段。尽管两阶段提交可以保证事务的原子性和一致性,但它会锁定参与事务的所有资源,直到事务完成,这可能导致性能问题和资源瓶颈。
  2. 三阶段提交(3PC, Three-Phase Commit)
    • 三阶段提交是对两阶段提交的改进,增加了一个预提交阶段,以减少资源锁定的时间。它更复杂,但比两阶段提交更可靠和高效。
  3. TCC(Try-Confirm-Cancel)
    • TCC 是一种补偿性事务模型,分为三个阶段:尝试(Try)、确认(Confirm)和取消(Cancel)。每个阶段都有相应的业务逻辑。如果在确认阶段之前出现错误,系统将执行取消操作来回滚之前的操作。
  4. 本地消息表
    • 这种方案结合了数据库和消息队列。应用程序在执行业务操作的同时,将事件写入本地数据库的消息表中。然后,一个独立的进程或服务负责将这些事件发布到消息队列中,以触发后续的操作。
  5. Saga 模式
    • Saga 模式将长运行的事务分解为一系列更小的、局部的事务。每个局部事务提交后,Saga 会发布事件来触发下一个事务。如果某个事务失败,Saga 将执行一系列补偿事务来回滚之前的操作。
  6. 分布式事务框架
    • 如 Seata、XA 事务等。这些框架提供了对分布式事务的支持,包括资源锁定、事务协调等功能。它们旨在简化分布式事务的处理,但可能会增加系统的复杂性。

Seata 的 AT 模式

Seata AT 模式是一种自动化的分布式事务解决方案,用于简化微服务架构中事务的处理。它的核心特点和工作机制如下:

  1. 自动化分布式事务管理
    • AT 模式通过代理数据源和 JDBC 层,无需修改业务代码,即可实现分布式事务的自动管理。
  2. 两阶段提交
    • 第一阶段(准备阶段):各个微服务执行本地事务,记录数据变更的前后镜像,但不提交事务。
    • 第二阶段(提交/回滚阶段):Seata 服务器根据各服务的执行结果决定是提交还是回滚全局事务。
  3. 数据一致性保障
    • 通过两阶段提交机制,确保所有参与的微服务要么全部成功提交事务,要么全部回滚,从而维持全局数据的一致性。
  4. 优化资源锁定
    • 相比传统的 XA 协议,AT 模式减少了锁定资源的时间,提高了性能。
  5. 故障恢复机制
    • 如果事务失败或系统异常,Seata 通过记录的数据镜像自动回滚变更,确保数据的一致性。

Seata 的 AT 模式是处理微服务环境中分布式事务的有效方法,它平衡了性能和一致性,为微服务应用提供了强大的事务管理能力。

MySQL

MySQL索引失效

  1. 使用不等于 (!=<>) 运算符
  2. 使用 IS NULLIS NOT NULL
  3. 使用 OR 关键字
    • 当使用 OR 连接条件时,如果 OR 两侧的字段没有同时命中索引,MySQL 会放弃索引,进行全表扫描。
    • 解决方法是使用联合索引,或将 OR 拆分为多个查询。
  4. 在索引列上进行计算或函数操作
  5. 使用 LIKE 模糊查询以 % 开头
  6. 字符集不一致
  7. 数据类型不匹配,如果查询条件的类型与索引字段的类型不一致,MySQL 可能无法利用索引。
  8. 表中的数据太少

MySQL事物隔离级别

  1. 读未提交(Read Uncommitted)
    • 这是最低的隔离级别,在这个级别下,事务可以读取到其他未提交事务的更改。这意味着可能会出现“脏读”(Dirty Reads),即读取到其他事务未提交的数据。
  2. 读已提交(Read Committed)
    • 这个级别保证了一个事务不会读取到其他事务的未提交数据,避免了脏读。但是,它允许“不可重复读”,即在同一个事务内,连续两次读取同一数据可能会得到不同的结果,因为其他事务可能在这两次读取之间提交了更新。
  3. 可重复读(Repeatable Read)
    • 这是 MySQL 默认的隔离级别。在这个级别下,保证了在同一事务内多次读取同一数据的结果是一致的,即避免了不可重复读。但在某些情况下,可能会遇到幻读,即当事务读取某个范围的记录时,如果另一个事务插入了新的记录,那么在随后的查询中,第一个事务可能会“看到”新的记录。
  4. 串行化(Serializable)
    • 这是最高的隔离级别。它通过强制事务串行执行,完全避免了脏读、不可重复读和幻读问题。但这种级别的代价是可能导致大量的超时和锁争用问题。

MySQL锁

  1. 表锁(Table Locks)
    • 表锁是 MySQL 中最基本的锁策略。它会锁定整个表。这种锁的开销最小,但它的并发性最差,因为它会阻止其他用户对该表执行写操作。
  2. 行锁(Row Locks)
    • 行锁是最细粒度的锁,仅锁定被访问的数据行。InnoDB 存储引擎支持行锁。行锁可以最大程度地支持并发处理,但管理行锁的开销也最大。
  3. 页面锁(Page Locks)
    • 页面锁是介于表锁和行锁之间的一种锁。它锁定内存或磁盘上的数据页。这种锁比表锁的并发性高,但比行锁的开销小。MyISAM 存储引擎中不支持页面锁。
  4. 意向锁(Intention Locks)
    • 意向锁是 InnoDB 用于表明某个事务打算对表中的行进行哪种类型的锁定(共享或排他)。这种锁是表级的。
  5. 共享锁(Shared Locks)
    • 共享锁允许一个事务读取一行数据,同时其他事务也可以读取这行数据,但任何事务都不能修改它,直到共享锁被释放。
  6. 排他锁(Exclusive Locks)
    • 排他锁允许事务对一行数据进行读取和写入操作,并阻止其他事务对该行数据进行读取和写入操作。
  7. 间隙锁(Gap Locks)
    • 间隙锁是 InnoDB 特有的,用于锁定一个范围,但不包括记录本身。这主要用于事务隔离级别为可重复读(Repeatable Read)时,防止幻读现象。
  8. 临键锁(Next-Key Locks)
    • 临键锁是 InnoDB 特有的,它是行锁和间隙锁的组合。它锁定一个范围,并包括范围内的行,防止其他事务在该范围内插入新行。

MySQL索引

索引的基本概念

  • 索引是数据库表中一个或多个列的值存储在一个特定的物理结构中。索引的主要目的是加速查询操作,尽管它们也可以用于确保数据的唯一性或完整性。
  1. B-Tree 索引
    • 这是最常见的索引类型,用于大多数 MySQL 索引。B-Tree 索引适用于全键值、键值范围或键前缀查找。在 B-Tree 索引中,数据存储在树形结构中,以便快速读写。
  2. 哈希索引
    • 哈希索引基于哈希表实现,只有精确匹配索引中所有列的查询才能使用哈希索引。哈希索引非常快,但它们不支持部分列匹配、范围查询或排序操作。
  3. 全文索引
    • 全文索引专门用于全文搜索。在 MySQL 中,InnoDB 和 MyISAM 存储引擎支持全文索引。它们允许你对文本内容进行高效搜索。
  4. 空间索引
    • 用于空间数据类型,如地理空间信息。这类索引用于高效解决地理空间数据的查询。
  5. 复合索引
    • 复合索引是指在表的多个列上创建的索引。它们对于那些涉及多列的查询非常有用。在创建复合索引时,列的顺序很重要。
  6. 主键索引
    • 在 MySQL 中,表的主键自动成为一个唯一索引。主键用于唯一标识表中的每一行,并且要求主键列中的值必须唯一。
  7. 唯一索引
    • 唯一索引不仅加快查询速度,还保证索引列的每行数据的唯一性。与主键索引类似,但一个表可以有多个唯一索引。
  8. 外键索引
    • 在使用 InnoDB 存储引擎时,MySQL 会自动在外键上创建索引(如果尚未存在)。外键索引用于加速表之间的关联操作。

MySQL查询优化

  1. 优化 SQL 语句
  • 选择合适的字段:尽量只查询需要的字段,而不是使用 SELECT *
  • 避免在 WHERE 子句中使用函数或表达式:这会导致索引失效。
  • 使用连接(JOIN)替代子查询:在一些情况下,连接查询的性能会比子查询更好。
  • 分解复杂的查询:将一个复杂的查询分解成多个简单的查询,有时可以提高性能。
  1. 使用索引
  • 创建合适的索引:根据查询的特点(如经常出现在 WHEREJOINORDER BYGROUP BY 子句中的列)创建索引。
  • 避免过多索引:虽然索引可以加快查询速度,但过多索引会增加写操作的负担,并占用更多存储空间。
  • 使用前缀索引:对于文本字段,可以考虑使用前缀索引来减少索引大小和提高索引效率。
  1. 查询执行计划分析
  • 使用 EXPLAINEXPLAIN ANALYZE 命令来分析查询的执行计划,识别慢查询的原因,比如是否有全表扫描、索引是否被利用等。
  1. 优化数据访问
  • 减少请求的数据量:只返回必要的行和列,避免不必要的数据访问。
  • 缓存重复查询:对于重复执行且数据变动不频繁的查询,可以通过应用层缓存来提高性能。
  1. 数据库结构优化
  • 数据类型优化:为字段选择合适的数据类型,小的数据类型通常更快,因为它们占用的磁盘、内存和CPU周期更少。
  • 表分区:对于非常大的表,可以考虑分区来提高查询效率。
  1. 服务器配置
  • 根据数据库的负载特点调整 MySQL 服务器配置,如增加缓冲池大小(innodb_buffer_pool_size)、调整连接数等。
  1. 使用更快的存储硬件
  • 使用 SSD 替代传统硬盘可以显著提高 I/O 性能。

总结

MySQL 查询优化是一个涉及到 SQL 语句、索引设计、数据模型、服务器配置和硬件等多方面的复杂过程。理解和应用这些策略可以帮助提高查询效率,优化数据库性能。在具体的优化工作中,需要根据实际的应用场景和业务需求来制定优化方案。

MySQL索引优化

  1. 使用合适的索引类型:根据查询模式选择最合适的索引类型(比如,B-Tree、FULLTEXT、HASH 等)。

  2. 创建复合索引:对于经常一起查询的列,考虑创建复合索引,而不是为每个列单独创建索引。

  3. 避免冗余和重复的索引:定期检查索引,删除不必要或重复的索引以减少维护成本和提升写操作性能。

  4. 考虑前缀索引:对于长文本字段,使用前缀索引可以节省空间并提升效率。

    CREATE INDEX index_name ON table_name(column_name(length));
  5. 利用索引扫描顺序:尽量使索引的列顺序与查询条件和排序条件一致。

  6. 使用 EXPLAIN 分析查询:定期使用 EXPLAIN 命令分析查询计划,确保索引被正确使用。

中间件

docker和k8s的区别

  • Docker 是一个容器化工具,主要用来把应用打包进容器,让它们在任何环境中都能以相同的方式运行。它主要处理单个容器的创建、运行和管理。

  • Kubernetes,另一方面,是一个容器编排工具。它用来管理和调度多个容器(可能是用 Docker 创建的)在一个集群中的运行。Kubernetes 能做到自动化部署、扩展应用和保持应用的稳定运行。

  • 所以基本上,Docker 更多关注于单个容器层面,而 Kubernetes 则是在更高层面上管理这些容器,特别是在大规模和分布式系统中。

  1. 基本概念和作用
    • Docker:Docker 是一个容器化平台,它允许开发者将应用及其依赖打包到一个轻量级、可移植的容器中。容器在运行时在 Docker 引擎上相互隔离,共享同一操作系统内核。Docker 主要用于创建和管理单个容器。
    • Kubernetes:Kubernetes 是一个容器编排系统,用于自动化容器的部署、扩展和管理。它设计用来在集群环境中运行和协调多个容器化应用或服务。
  2. 功能和范围
    • Docker:提供了容器的生命周期管理,包括容器的构建、运行、停止和删除。Docker 通过 Dockerfile 简化了容器创建的过程。
    • Kubernetes:提供了更广泛的系统级功能,如集群管理、服务发现、负载均衡、自动扩展、滚动更新和自愈能力。
  3. 使用场景
    • Docker:适用于开发阶段的应用容器化、简单的应用部署和单机上的容器管理。
    • Kubernetes:适用于生产环境中的大规模容器管理,特别是当需要高可用性、扩展性和复杂的服务协调时。
  4. 架构
    • Docker:采用客户端-服务器架构。Docker 客户端与 Docker 守护进程(服务器)交互,守护进程负责构建、运行和分发容器。
    • Kubernetes:采用主从架构。Kubernetes 集群由一个或多个主节点(负责集群管理和调度)和多个工作节点(运行容器化应用)组成。
  5. 可伸缩性和高可用性
    • Docker:单独使用 Docker 时,并不直接提供多节点集群的高可用性和可伸缩性支持。
    • Kubernetes:天然支持多节点集群,能够自动管理和扩展应用,提供故障转移和负载均衡能力。

Redis

Redis String

Redis 的字符串(String)类型是最基本的数据类型之一,也是 Redis 中使用最为广泛的类型。它可以存储任何形式的字符串(包括二进制数据),最大能够存储的数据量为 512MB。在 Redis 中,字符串类型不仅用来存储文本或数值数据,还常用于实现更复杂的数据结构和功能,如计数器、锁等。

底层数据结构

Redis 的字符串类型底层实际上是由一个名为 simple dynamic string(简单动态字符串,SDS)的结构实现的。SDS 是 Redis 的私有实现,它比标准的 C 字符串提供了更多的优势:

  1. 长度可变
    • SDS 支持动态修改字符串内容而无需重新分配内存(直到超出当前分配的空间)。
    • SDS 结构内部维护了字符串的实际长度,使得字符串的长度修改操作更高效。
  2. 二进制安全
    • SDS 可以存储任何二进制数据,包括 \0 字符。
    • 这意味着它可以存储除了文本之外的任何类型的数据,如图片、音频等二进制数据。
  3. 获取字符串长度的时间复杂度为 O(1)
    • SDS 结构内部存储了字符串的长度,所以获取字符串长度不需要遍历整个字符串,性能较高。
  4. 空间预分配和惰性空间释放
    • 当对 SDS 进行扩展操作(如追加字符串)时,Redis 会预分配额外空间,以减少将来修改时所需的内存重新分配次数。
    • 同时,当字符串缩短时,Redis 不会立即释放多余的内存空间,而是使用惰性空间释放策略,以优化内存使用和减少内存碎片。

使用场景

  • 存储文本或二进制数据:作为最基础的数据类型,字符串可以存储各种形式的文本或二进制数据。
  • 计数器:使用 Redis 字符串存储数值数据,可以利用 INCRDECR 等命令实现原子性的计数功能。
  • 分布式锁:利用字符串类型和相关命令,可以实现分布式锁的功能。
  • 缓存:字符串类型常用于缓存用户信息、网页内容、数据库查询结果等。

Redis 的字符串类型因其简单、灵活而广泛应用于各种场景,是构建高性能、可扩展应用程序的重要基础。

Redis哨兵

Redis 哨兵(Sentinel)简介

Redis 哨兵是 Redis 的高可用解决方案,它主要负责三个核心任务:

  1. 监控:持续检测 Redis 主服务器和从服务器的运行状态。
  2. 自动故障转移:当主服务器出现故障时,自动将一个从服务器升级为新的主服务器,并重新配置其他从服务器,保持服务的可用性。
  3. 服务发现:客户端可以询问哨兵当前哪个是主服务器,以及获取最新的主从服务器配置。

工作原理

哨兵通过发送定期的 PING 命令来监控 Redis 实例。如果主服务器在配置的超时时间内未响应,哨兵会认为它可能下线,并通过多个哨兵之间的协商来确认主服务器是否真的下线。一旦确认,哨兵将执行故障转移,自动选择一个从服务器作为新的主服务器。

为什么需要哨兵

  • 高可用性:哨兵通过监控和自动故障转移确保了 Redis 服务的连续性和稳定性,即使在主服务器出现故障时也能保持服务的可用性。
  • 自动化操作:故障转移过程是自动进行的,减少了人工干预的需要,提高了系统的可维护性。

哨兵系统通过提供一种自动化的方式来处理 Redis 实例的故障,是构建高可用 Redis 系统的关键组件。

Redis与数据库数据数据一致性

  1. 缓存失效
  • 写操作时使缓存失效:在数据库写操作(更新、删除)后,立即删除相关缓存。这确保了后续的读操作会从数据库获取最新数据。
  1. 延迟双删策略
  • 在数据库更新前后各删除一次缓存,并在数据库更新后稍作延迟,然后再第二次删除缓存,以覆盖数据库写操作期间可能发生的缓存读取。
  1. 缓存过期时间
  • 为缓存数据设置过期时间,让老旧数据自动失效,减少数据不一致的机会。
  1. 消息队列
  • 使用消息队列异步更新缓存,确保数据库操作和缓存操作的一致性。

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published