- 结构优化:在 Java 8 中,当一个 bucket(桶)中的元素过多时(默认超过 8 个),HashMap 会将这个 bucket 中的链表转换成红黑树,这大大提高了在高冲突情况下的查找效率。如果元素数量减少,红黑树又会退化成链表。
- Hash 函数:HashMap 使用内部的 hash 函数来分配元素的存储位置。在 Java 8 中,hash 函数被改进以减少碰撞。
- 并发改进:尽管
HashMap
本身不是线程安全的,但在 Java 8 中,它的并发行为得到了一定的改善,特别是在扩容过程中。然而,对于需要高并发的应用,通常建议使用ConcurrentHashMap
。 - 空间效率和性能:Java 8 的
HashMap
实现在空间效率和性能方面都有所改进,特别是通过树化 bucket 和优化 hash 函数来减少碰撞。 - 默认容量和加载因子:默认的初始容量是 16,加载因子是 0.75。加载因子是一个衡量 HashMap 满的程度的指标,当 HashMap 的填充度超过加载因子时,将会进行扩容(即重哈希)。
- 计算模式:Java 8 引入了一些新的 API,如
computeIfAbsent
和computeIfPresent
,这些方法结合了检查、获取和计算的步骤,使得一些常见的模式变得更加简洁。
- 高效的索引计算:当桶的数量是 2 的 n 次方时,计算元素应该放入哪个桶的公式可以简化为使用位运算(
hash & (length-1)
),其中length
是桶的总数。位运算比模运算(hash % length
)要快,因为模运算在硬件层面上的执行效率相对较低。 - 均匀分布:哈希表的一个关键目标是尽量避免哈希冲突,将数据均匀分布到各个桶中。利用位运算可以更好地分散哈希值,尤其是当哈希函数较为均匀时,这种方法能够有效地利用哈希值的特性,实现数据的均匀分布。
- 扩容时的数据迁移效率:在 HashMap 扩容时(例如,当元素数量超过容量与加载因子的乘积),容量扩大到原来的两倍。由于容量始终是 2 的次方,所以在扩容过程中,元素的重新分布可以通过简单的位运算来完成,而不需要重新计算哈希值。这样可以大大提高扩容的效率。
- 核心概念:AQS 是一个提供了锁和其他同步机制的基础框架。它使用一个 int 类型的变量来表示同步状态,并通过一个 FIFO 队列来管理阻塞线程。
- 同步状态:AQS 使用一个内部的 volatile int 类型的变量来表示同步状态。子类可以通过改变这个状态来实现锁的获取和释放。
- 节点和等待队列:AQS 内部维护了一个等待队列,每个节点(Node)代表一个等待获取锁的线程。如果线程无法获取锁,则会被包装成节点加入到队列的尾部,并在必要时阻塞。
- 获取和释放锁:AQS 提供了方法来管理对同步状态的获取和释放。子类通过重写这些方法来实现具体的锁机制。
tryAcquire
:尝试获取资源,成功则返回 true,失败则返回 false。tryRelease
:尝试释放资源,成功则返回 true,失败则返回 false。
- 独占模式和共享模式:AQS 支持两种同步模式。独占模式(如 ReentrantLock)一次只允许一个线程持有资源,而共享模式(如 Semaphore、CountDownLatch)则允许多个线程同时持有资源。
- 公平性和非公平性:AQS 可以支持创建公平锁和非公平锁。公平锁按照请求的顺序授予锁,而非公平锁则可能插队。
- 重要的子类:基于 AQS 实现的同步器包括 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等。
- 基本锁机制和工作原理:
synchronized
:内置于 Java 语言中,提供了一种基于监视器模式的同步机制。它可以用于方法(实例方法和静态方法)和代码块上,且是隐式的,不需要手动加锁和释放锁。ReentrantLock
:是 Java.util.concurrent.locks 包的一部分,提供了一种高级的同步机制。它需要显式地创建锁对象,手动进行加锁和解锁。
- 锁的公平性选项:
synchronized
:不支持公平性。不保证等待时间最长的线程会首先获取锁。ReentrantLock
:支持公平性和非公平性。可以在创建锁时指定,确保按照等待时间的顺序获取锁。
- 灵活性和控制能力:
synchronized
:较少的控制能力。不能尝试非阻塞地获取锁,也不能中断一个正在等待获取锁的线程。ReentrantLock
:更高的灵活性。提供了尝试非阻塞地获取锁(tryLock()
)、可中断的锁获取操作(lockInterruptibly()
)以及支持条件变量(Condition
)。
- 对性能的影响:
synchronized
:在 JDK 1.6 之后,通过引入偏向锁和轻量级锁等机制,性能大幅提升,但在高竞争环境下可能仍然落后于ReentrantLock
。ReentrantLock
:在高度竞争的环境下,通常提供比synchronized
更好的性能,但在低竞争环境下,性能差异不大。
- 最佳使用场景:
synchronized
:适合简单的同步需求,以及锁竞争不是非常激烈的场景。因为其语法简洁,易于阅读和维护。ReentrantLock
:适合复杂的同步需求,或者需要更多同步控制,如公平锁、可中断锁、条件变量等。它提供了更大的灵活性,但需要更仔细的锁管理。
- 偏向锁(Biased Locking):
- 当一个线程首次获得锁时,锁会进入偏向模式,此时锁会偏向于第一个获取它的线程。
- 如果在此后的执行过程中,没有其他线程尝试获取这个锁,那么持有偏向锁的线程可以无需同步操作直接进入同步块。
- 偏向锁适用于只有一个线程访问同步块的情况。
- 轻量级锁(Lightweight Locking):
- 当有另一个线程尝试获取已经被偏向的锁时,偏向锁会升级为轻量级锁。
- 轻量级锁的工作机制是,线程会在对象的标记字中存储锁记录(Lock Record)的指针。
- 如果尝试获取锁的线程发现对象已经被锁定(即存在有效的锁记录),它会在自己的栈帧中创建一个锁记录的副本(称为 Displaced Mark Word),然后尝试使用 CAS 操作将对象头的标记更新为指向该锁记录的指针。
- 如果 CAS 操作成功,当前线程获取锁;如果失败,表明有其他线程竞争,锁会升级为重量级锁。
- 重量级锁(Heavyweight Locking):
- 当多个线程同时竞争同一个锁时,轻量级锁会升级为重量级锁。
- 重量级锁会使其他尝试获取锁的线程进入阻塞状态。
- 这种锁的实现依赖于操作系统的 Mutex 机制,虽然增加了线程切换的开销,但在高度竞争的环境下可以保证线程安全。
- 传统的
for
循环(for (int i = 0; ...)
):- 效率:通常来说,在遍历数组时效率最高,因为它允许直接通过索引访问元素,减少了额外的方法调用。
- 使用场景:适用于需要访问数组索引或需要修改遍历过程中的计数器值的场景。
for-each
循环:- 效率:
- 当遍历集合时,
for-each
循环是基于Iterator
实现的。其效率通常与传统for
循环相近,但在某些情况下(尤其是在遍历大型集合时)可能会略低于传统for
循环,因为它涉及到Iterator
对象的创建和方法调用的额外开销。 - 当遍历数组时,
for-each
循环是基于索引的循环,其效率与传统的基于索引的for
循环相当。
- 当遍历集合时,
- 使用场景:
for-each
循环适用于不需要直接索引访问,只需遍历集合或数组中的每个元素的场景。- 它提供了一种更简洁、更易读的遍历方式,特别是在代码的可读性和简洁性更为重要时。
- 效率:
- Stream API 的
forEach
:- 效率:Stream
forEach
在单线程情况下效率通常低于传统for
循环和for-each
循环,因为它涉及到更多的函数调用和中间操作。然而,Stream API 支持并行处理,这在处理大数据集时可能带来性能优势。 - 使用场景:适用于更加函数式的编程风格,以及需要链式操作(如过滤、映射、排序等)的场景。当数据集非常大或者需要并行处理以提高性能时,使用并行 Stream (
parallelStream().forEach(...)
) 是一个好选择。
- 效率:Stream
在使用 MyBatis 进行数据持久化时,批量插入是一个常见的需求,尤其是在处理大量数据时。了解不同批量插入技术的效率对于优化性能至关重要。以下是两种常用的批量插入方法及其效率比较:
特点:
foreach
标签用于生成一条包含多个插入操作的 SQL 语句。- 简化了代码,易于理解和实现。
- 减少了与数据库的交互次数。
适用场景:
- 适合小到中等规模的数据量。
- 当数据量非常大时,可能会遇到 SQL 语句长度限制或性能下降的问题。
特点:
- 利用 JDBC 的批处理功能,允许一次性执行多条独立的 SQL 语句。
- 减少网络往返次数,提高了大规模数据处理的效率。
- 可以控制每次批处理的大小,避免超出数据库限制。
适用场景:
- 特别适用于大规模数据的插入。
- 需要细致的错误处理机制,因为一次批处理中的某个操作失败可能会影响到整个批次的操作。
- 小到中等规模数据:使用
foreach
标签的方法更为简单和直接,通常效率足够高。 - 大规模数据:使用 JDBC 批处理的方式效率更高,特别是在处理非常大量的数据时,它能提供更优的性能和资源利用率。
在 Java 中,CAS(Compare And Swap,比较并交换)是一种无锁的原子操作,用于实现线程安全的操作。CAS 是并发编程中的一种重要技术,尤其在高并发环境下,CAS 可以实现较高的性能,因为它避免了传统的加锁机制。
CAS 机制主要通过三步操作来完成更新:
- 比较(Compare):判断内存中的值是否等于预期值。
- 交换(Swap):如果内存中的值与预期值相等,就将内存值更新为新值。
- 返回结果:如果更新成功,返回
true
,否则返回false
。
- 高效:CAS 是一种非阻塞算法,可以避免传统锁带来的线程阻塞和上下文切换开销,具有更高的性能。
- 线程安全:CAS 操作是原子性的,不会出现线程安全问题。
- 无锁设计:CAS 可以实现无锁设计,适合高并发环境。
- ABA 问题:
- CAS 可能会遇到 ABA 问题。即某个线程在读取到的值为 A 后,另一个线程将值更改为 B,然后再改回 A。CAS 操作会认为没有发生变化,导致逻辑错误。
- Java 中的
AtomicStampedReference
提供了解决方案,可以通过“版本号”来解决 ABA 问题。
- 自旋消耗 CPU:
- 如果 CAS 操作一直失败,线程会不断尝试更新(称为“自旋”),这会导致 CPU 资源浪费。
- 自旋会在高并发下带来一定的性能开销。
- 只能保证一个共享变量的原子操作:
- CAS 仅能保证一个变量的原子操作。如果需要对多个变量进行原子操作,可以考虑使用锁,或者将多个变量封装在一个对象中,通过原子引用来更新。
乐观锁是一种假设不会有并发冲突的策略,即假设同一数据的多个线程或事务不会经常发生冲突。在更新数据之前,会检查数据是否已被其他线程或事务更改,如果未更改,则完成更新;如果已经被更改,则放弃操作或重新尝试。
- 无锁设计:不需要数据库加锁,性能高。
- 适用于并发量小、冲突少的场景。
- 需要重试:如果冲突频繁,可能需要多次重试,影响性能。
- 不适用于高冲突场景:如果冲突多,会导致多次失败和重试,降低效率。
悲观锁是一种假设会有并发冲突的策略,即假设同一数据的多个线程或事务会发生冲突。悲观锁会在对数据进行操作前,先加锁以确保其他线程或事务不能访问该数据,从而保证数据操作的排他性。
- 适用于高冲突场景:悲观锁会在操作数据前锁定数据,确保数据的一致性。
- 数据一致性高:在高并发写操作下,能更好地保证数据的正确性和完整性。
- 性能开销大:悲观锁在加锁期间会阻塞其他线程,可能导致系统性能下降。
- 容易产生死锁:如果多个线程之间加锁顺序不当,可能会造成死锁。
- 堆(Heap):
- JVM 中最大的内存区域。
- 存储所有 Java 类实例和数组。
- 堆内存在 JVM 启动时创建,是垃圾收集器管理的主要区域。
- 方法区(Method Area):
- 存储已被虚拟机加载的类信息、常量、静态变量等数据。
- 也称为“永久代”(Permanent Generation),在 Java 8 中被元空间(Metaspace)所替代。
- 栈(Stack):
- 存储局部变量和部分方法执行过程中的信息。
- 每个线程有自己的栈,随着方法调用而动态扩展和收缩。
- 程序计数器(Program Counter):
- 当前线程所执行的字节码的行号指示器。
- 每个线程都有自己的程序计数器。
- 本地方法栈(Native Method Stack):
- 为 JVM 使用的 Native 方法服务。
- 与 Java 栈类似,但服务于 Native 方法调用。
JVM(Java 虚拟机)的垃圾回收(GC)是自动管理内存的过程。它的主要目的是识别并丢弃那些不再被应用程序使用的对象,以释放并重用内存。以下是 JVM 垃圾回收的关键概念:
- 垃圾回收根:
- GC 从所谓的“根”(如线程栈和静态字段)开始寻找,这些根是活动的引用。
- 可达性分析:
- 从根集开始,GC 进行可达性分析,识别出所有从根集可达的对象。无法从任何根到达的对象被认为是垃圾。
- 标记-清除算法:
- 最基本的垃圾回收算法。它分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,然后统一清除这些对象。
- 复制算法:
- 主要用在新生代。内存分为两块,每次只使用其中一块。当这一块的内存用完时,将还活着的对象复制到另一块内存上,然后清理掉这一块内存上的所有对象。
- 标记-整理算法:
- 用于老年代的一种方法。标记过程与标记-清除算法相同,但后续步骤是将所有存活的对象压缩到内存的一端,然后清理掉边界以外的内存。
- 分代收集理论:
- JVM 的堆分为新生代和老年代。新生代中对象生命周期短,老年代中对象生命周期长。根据这个特性,使用不同的收集算法来提高垃圾回收的效率。
- 垃圾回收器:
- 不同的 JVM 实现提供了不同的垃圾回收器,如 Serial、Parallel、CMS(Concurrent Mark-Sweep)、G1 等。每种回收器都有自己的特点,适用于不同类型的应用。
在 JVM(Java 虚拟机)中,堆内存被分为几个区域以优化垃场的回收,其中最重要的区域是新生代(Young Generation)和老年代(Old Generation)。以下是它们的详细说明:
- 新生代(Young Generation):
- 新生代是堆内存的一个区域,用于存储新创建的对象。
- 大多数情况下,新创建的对象首先被分配到新生代。
- 新生代通常被进一步分为三个部分:一个 Eden 空间和两个幸存者空间(Survivor Spaces,通常称为 S0 和 S1)。
- 新生代的特点是它经常进行垃圾回收,也称为 Minor GC。由于大多数新创建的对象很快就变得不可达(例如,局部变量),因此 Minor GC 通常很快且频繁地执行。
- 老年代(Old Generation):
- 老年代用于存储生命周期较长的 Java 对象。
- 当对象在新生代中存活了足够长的时间(经过了多次 Minor GC)后,就会被晋升到老年代。
- 老年代的大小通常远大于新生代,且其垃圾回收频率较低,但每次回收的时间比新生代要长。在老年代进行的垃圾回收称为 Major GC 或 Full GC。
- 永久代/元空间(PermGen/Metaspace):
- 在较早版本的 JVM 中,还有一个称为永久代(PermGen)的区域,用于存储类的元数据、方法对象等。
- 从 Java 8 开始,永久代被移除,取而代之的是元空间(Metaspace)。元空间不在虚拟机内存中,而是使用本地内存。
这种内存区域的划分是为了优化垃圾回收效率。由于不同区域的对象生命周期不同,JVM 可以采用不同的策略来管理这些区域,从而提高整体的内存管理效率。
- 生成堆转储:
- 使用
jmap
工具生成堆转储文件:jmap -dump:live,format=b,file=/tmp/heapdump.hprof <PID>
。 <PID>
是发生 OOM 的 Java 进程 ID。
- 使用
- 分析堆转储:
- 使用 Heap Analyzer Tool(MAT)或 VisualVM 等工具分析堆转储文件。
- 寻找内存泄漏的对象、频繁分配的对象以及占用大量内存的对象。
- 检查 GC 日志:
- 分析 GC 日志了解垃圾回收行为。如果没有启用 GC 日志,考虑添加启动参数
-Xloggc:<file-path>
开启。
- 分析 GC 日志了解垃圾回收行为。如果没有启用 GC 日志,考虑添加启动参数
- 调整 JVM 参数:
- 根据分析结果,考虑调整 JVM 堆大小
-Xmx
和-Xms
,或调整垃圾回收策略和参数。
- 根据分析结果,考虑调整 JVM 堆大小
- 代码优化:
- 对于内存泄露,查找并修复代码中的问题。
- 对于过度消耗内存的场景,优化数据结构和算法,减少内存占用。
- 压力测试:
- 在进行调整后,通过压力测试验证改动的效果,确保问题得到解决。
- 使用
top
或任务管理器确认哪个 Java 进程导致高 CPU/内存/磁盘 使用。 - 获取问题进程的线程堆栈:
jstack <PID>
。 - 分析线程堆栈,寻找
RUNNABLE
状态的线程。 - 利用工具如 VisualVM 进行更深入分析。
- 检查应用日志寻找异常信息。
- 资源定位:首先,Spring 通过配置文件或注解找到 Bean 的定义。
- 载入 Bean 定义:通过读取配置文件或注解,Spring 解析并将 Bean 定义加载到容器中。
- 注册 Bean:解析后的 Bean 定义被注册到 Spring 容器内。(解析一个注册一个)
- 依赖注入:容器分析 Bean 间的依赖关系,并完成相应的依赖注入。
- 初始化:创建 Bean 实例,并执行任何初始化逻辑。
- 后处理:应用如 AOP 的额外处理。
@Bean
- 用于在 Spring 管理的类中声明一个 Bean。
- 可以用于任何 Spring 管理的组件,但通常推荐在
@Configuration
类中使用。 - 定义方法返回的对象将作为一个 Bean 被 Spring 容器管理。
@Configuration
- 用于定义配置类,这些类可以包含一个或多个
@Bean
注解的方法。 - 被
@Configuration
注解的类本身也会作为一个 Bean 被注册到 Spring 容器中。 @Configuration
类中的@Bean
方法被特殊处理,以确保多次调用返回同一个实例。
@Component
- 基础注解,用于声明一个组件(Bean)。
- 用于那些不易归类为服务(
@Service
)或存储库(@Repository
)的 Bean。 @Repository
、@Service
和@Controller
都是这个注解的特化形式。
@Service
- 用于标注服务层组件。
- 用于业务逻辑层(Service Layer),表明类是业务逻辑相关的 Bean。
@Repository
- 用于标注数据访问层组件。
- 用于数据库访问操作,表明类是数据访问对象(DAO)。
@Controller
- 用于标注控制层组件。
- 用于 Web 应用的 MVC 模式,处理 HTTP 请求。
@RestController
- 结合了
@Controller
和@ResponseBody
的功能。 - 用于创建 RESTful Web 服务,处理 HTTP 请求并以 JSON 或 XML 形式返回数据。
- @Service:
- 标注在服务层(业务逻辑层)的类上。
- 主要用于标识业务逻辑服务组件。
- Spring 框架可以对其进行一些特定的业务逻辑层面的处理,但在大多数情况下,
@Service
注解仅作为一种标识,无额外的技术影响。
- @Repository:
- 标注在数据访问层(DAO)的类上。
- 主要用于标识数据访问组件。
- Spring 通过
@Repository
注解可以提供数据访问相关的异常转换功能。它可以将数据库异常转换为 Spring 的数据访问异常。
- @Controller:
- 标注在表现层(如 Web 控制器)的类上。
- 主要用于标识处理 HTTP 请求的控制器组件。
@Controller
注解的类通常会配合@RequestMapping
或其他相关注解来处理 HTTP 请求和返回相应的视图或数据。
- AOP 概念:AOP 是一种编程范式,用于将跨多个模块的关注点(如日志、安全、事务等)分离出来,以提高代码的模块化。
- 切面(Aspect):切面是 AOP 的核心,它将横切关注点封装成独立的模块。一个切面可以定义多个通知(Advice)和切点(Pointcut)。
- 通知(Advice):通知定义了切面要完成的工作以及何时执行这些工作。常见的通知类型有:前置(Before)、后置(After)、返回后(After-returning)、抛出异常后(After-throwing)和环绕(Around)。
- 切点(Pointcut):切点定义了通知应用的位置,即哪些方法或类会触发切面的执行。
- 织入(Weaving):将切面应用到目标对象并创建代理对象的过程称为织入。这可以在编译时(编译时织入)、类加载时(加载时织入)或运行时(运行时织入,Spring 使用这种方式)进行。
- 代理(Proxy):在 Spring AOP 中,AOP 实现通常通过动态代理完成。这意味着为目标对象动态地创建一个代理,代理会拦截对目标方法的调用,并根据切面的定义在调用前后执行相关逻辑。
- 加载(Loading):
- 在这个阶段,JVM 通过类加载器(ClassLoader)读取二进制数据(通常是
.class
文件)并将其转换为一个java.lang.Class
对象。这个过程涉及到查找字节码,并从文件系统、网络或其他源加载字节码。
- 在这个阶段,JVM 通过类加载器(ClassLoader)读取二进制数据(通常是
- 链接(Linking):
- 验证(Verification):确保加载的类符合 JVM 规范,没有安全问题。
- 准备(Preparation):为类变量分配内存,并设置默认初始值。
- 解析(Resolution):将类、接口、字段和方法的符号引用替换为直接引用。
- 初始化(Initialization):
- 在这个阶段,JVM 执行类构造器
<clinit>()
方法的代码。这包括静态变量的初始化和静态代码块的执行。
- 在这个阶段,JVM 执行类构造器
- 使用(Using):
- 类实例可以在程序中被使用,涉及到创建类的实例、调用类的方法、使用类的变量等。
- 卸载(Unloading):
- 类卸载是由 JVM 的垃圾回收器处理的。当一个类的 ClassLoader 和该类的所有实例都不再被引用时,JVM 就会卸载这个类。
- 启动类上的注解:Spring Boot 应用的入口类通常标有
@SpringBootApplication
注解,这个注解是一个方便的注解,结合了@Configuration
、@EnableAutoConfiguration
和@ComponentScan
。 - 启用自动配置:
@EnableAutoConfiguration
注解是自动配置的关键,它告诉 Spring Boot 开始查看添加到类路径中的 jar,基于这些 jar 提供的类和配置来配置应用程序。 - 读取
spring.factories
:Spring Boot 会加载类路径上所有META-INF/spring.factories
配置文件,这些文件列出了所有可用的自动配置类。 - 条件评估:每个自动配置类可以使用条件注解(如
@ConditionalOnClass
、@ConditionalOnBean
),这些注解确保只在满足特定条件时才启用自动配置。Spring Boot 会评估这些条件,并决定是否应用每个自动配置类。 - Bean 的创建和注册:满足条件的自动配置类将被处理,它们内部定义的 Beans 会根据条件创建并注册到 Spring 容器中。这包括对一些常用库的自动配置,如数据库连接、消息服务、Web MVC 设置等。
- 属性绑定:Spring Boot 会使用
application.properties
或application.yml
文件中的属性来覆盖自动配置提供的默认值。 - 覆盖和自定义:如果需要,开发者可以自定义额外的配置来覆盖自动配置提供的 Bean,或者通过创建自己的配置类来扩展自动装配。
- 应用准备就绪:完成上述步骤后,应用配置就绪,可以开始接受服务请求或执行业务逻辑。
Spring Framework 提供了多种事务传播行为,这些行为定义了事务方法是如何在事务上下文中执行的。在 Spring Boot 中,这些传播级别可以通过 @Transactional
注解的 propagation
属性来设置。以下是常用的事务传播级别:
- REQUIRED(默认):
- 如果当前存在事务,那么方法将在该事务中运行;否则,将创建一个新事务。
- SUPPORTS:
- 如果当前存在事务,方法将在该事务中运行;如果当前没有事务,那么方法将在非事务的环境中执行。
- MANDATORY:
- 如果当前存在事务,方法将在该事务中运行;如果没有事务,则抛出异常。
- REQUIRES_NEW:
- 总是创建一个新事务,如果当前存在事务,则暂停当前事务。
- NOT_SUPPORTED:
- 总是非事务性地执行,并暂停任何现有事务。
- NEVER:
- 总是非事务性地执行,如果存在一个活动事务,则抛出异常。
- NESTED:
- 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则其行为与
REQUIRED
一样。
- 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则其行为与
这些传播行为允许你精细地控制事务的边界和方式,非常适合处理复杂的业务场景。正确选择合适的传播级别可以帮助避免一些常见的问题,比如脏读、不可重复读、幻读以及长时间占用数据库资源等。
- 两阶段提交(2PC, Two-Phase Commit):
- 这是一种经典的分布式事务协议。它分为两个阶段:准备阶段和提交/回滚阶段。尽管两阶段提交可以保证事务的原子性和一致性,但它会锁定参与事务的所有资源,直到事务完成,这可能导致性能问题和资源瓶颈。
- 三阶段提交(3PC, Three-Phase Commit):
- 三阶段提交是对两阶段提交的改进,增加了一个预提交阶段,以减少资源锁定的时间。它更复杂,但比两阶段提交更可靠和高效。
- TCC(Try-Confirm-Cancel):
- TCC 是一种补偿性事务模型,分为三个阶段:尝试(Try)、确认(Confirm)和取消(Cancel)。每个阶段都有相应的业务逻辑。如果在确认阶段之前出现错误,系统将执行取消操作来回滚之前的操作。
- 本地消息表:
- 这种方案结合了数据库和消息队列。应用程序在执行业务操作的同时,将事件写入本地数据库的消息表中。然后,一个独立的进程或服务负责将这些事件发布到消息队列中,以触发后续的操作。
- Saga 模式:
- Saga 模式将长运行的事务分解为一系列更小的、局部的事务。每个局部事务提交后,Saga 会发布事件来触发下一个事务。如果某个事务失败,Saga 将执行一系列补偿事务来回滚之前的操作。
- 分布式事务框架:
- 如 Seata、XA 事务等。这些框架提供了对分布式事务的支持,包括资源锁定、事务协调等功能。它们旨在简化分布式事务的处理,但可能会增加系统的复杂性。
Seata AT 模式是一种自动化的分布式事务解决方案,用于简化微服务架构中事务的处理。它的核心特点和工作机制如下:
- 自动化分布式事务管理:
- AT 模式通过代理数据源和 JDBC 层,无需修改业务代码,即可实现分布式事务的自动管理。
- 两阶段提交:
- 第一阶段(准备阶段):各个微服务执行本地事务,记录数据变更的前后镜像,但不提交事务。
- 第二阶段(提交/回滚阶段):Seata 服务器根据各服务的执行结果决定是提交还是回滚全局事务。
- 数据一致性保障:
- 通过两阶段提交机制,确保所有参与的微服务要么全部成功提交事务,要么全部回滚,从而维持全局数据的一致性。
- 优化资源锁定:
- 相比传统的 XA 协议,AT 模式减少了锁定资源的时间,提高了性能。
- 故障恢复机制:
- 如果事务失败或系统异常,Seata 通过记录的数据镜像自动回滚变更,确保数据的一致性。
Seata 的 AT 模式是处理微服务环境中分布式事务的有效方法,它平衡了性能和一致性,为微服务应用提供了强大的事务管理能力。
- 使用不等于 (
!=
或<>
) 运算符 - 使用
IS NULL
或IS NOT NULL
- 使用
OR
关键字- 当使用
OR
连接条件时,如果OR
两侧的字段没有同时命中索引,MySQL 会放弃索引,进行全表扫描。 - 解决方法是使用联合索引,或将
OR
拆分为多个查询。
- 当使用
- 在索引列上进行计算或函数操作
- 使用
LIKE
模糊查询以%
开头 - 字符集不一致
- 数据类型不匹配,如果查询条件的类型与索引字段的类型不一致,MySQL 可能无法利用索引。
- 表中的数据太少
- 读未提交(Read Uncommitted):
- 这是最低的隔离级别,在这个级别下,事务可以读取到其他未提交事务的更改。这意味着可能会出现“脏读”(Dirty Reads),即读取到其他事务未提交的数据。
- 读已提交(Read Committed):
- 这个级别保证了一个事务不会读取到其他事务的未提交数据,避免了脏读。但是,它允许“不可重复读”,即在同一个事务内,连续两次读取同一数据可能会得到不同的结果,因为其他事务可能在这两次读取之间提交了更新。
- 可重复读(Repeatable Read):
- 这是 MySQL 默认的隔离级别。在这个级别下,保证了在同一事务内多次读取同一数据的结果是一致的,即避免了不可重复读。但在某些情况下,可能会遇到幻读,即当事务读取某个范围的记录时,如果另一个事务插入了新的记录,那么在随后的查询中,第一个事务可能会“看到”新的记录。
- 串行化(Serializable):
- 这是最高的隔离级别。它通过强制事务串行执行,完全避免了脏读、不可重复读和幻读问题。但这种级别的代价是可能导致大量的超时和锁争用问题。
- 表锁(Table Locks):
- 表锁是 MySQL 中最基本的锁策略。它会锁定整个表。这种锁的开销最小,但它的并发性最差,因为它会阻止其他用户对该表执行写操作。
- 行锁(Row Locks):
- 行锁是最细粒度的锁,仅锁定被访问的数据行。InnoDB 存储引擎支持行锁。行锁可以最大程度地支持并发处理,但管理行锁的开销也最大。
- 页面锁(Page Locks):
- 页面锁是介于表锁和行锁之间的一种锁。它锁定内存或磁盘上的数据页。这种锁比表锁的并发性高,但比行锁的开销小。MyISAM 存储引擎中不支持页面锁。
- 意向锁(Intention Locks):
- 意向锁是 InnoDB 用于表明某个事务打算对表中的行进行哪种类型的锁定(共享或排他)。这种锁是表级的。
- 共享锁(Shared Locks):
- 共享锁允许一个事务读取一行数据,同时其他事务也可以读取这行数据,但任何事务都不能修改它,直到共享锁被释放。
- 排他锁(Exclusive Locks):
- 排他锁允许事务对一行数据进行读取和写入操作,并阻止其他事务对该行数据进行读取和写入操作。
- 间隙锁(Gap Locks):
- 间隙锁是 InnoDB 特有的,用于锁定一个范围,但不包括记录本身。这主要用于事务隔离级别为可重复读(Repeatable Read)时,防止幻读现象。
- 临键锁(Next-Key Locks):
- 临键锁是 InnoDB 特有的,它是行锁和间隙锁的组合。它锁定一个范围,并包括范围内的行,防止其他事务在该范围内插入新行。
索引的基本概念:
- 索引是数据库表中一个或多个列的值存储在一个特定的物理结构中。索引的主要目的是加速查询操作,尽管它们也可以用于确保数据的唯一性或完整性。
- B-Tree 索引:
- 这是最常见的索引类型,用于大多数 MySQL 索引。B-Tree 索引适用于全键值、键值范围或键前缀查找。在 B-Tree 索引中,数据存储在树形结构中,以便快速读写。
- 哈希索引:
- 哈希索引基于哈希表实现,只有精确匹配索引中所有列的查询才能使用哈希索引。哈希索引非常快,但它们不支持部分列匹配、范围查询或排序操作。
- 全文索引:
- 全文索引专门用于全文搜索。在 MySQL 中,InnoDB 和 MyISAM 存储引擎支持全文索引。它们允许你对文本内容进行高效搜索。
- 空间索引:
- 用于空间数据类型,如地理空间信息。这类索引用于高效解决地理空间数据的查询。
- 复合索引:
- 复合索引是指在表的多个列上创建的索引。它们对于那些涉及多列的查询非常有用。在创建复合索引时,列的顺序很重要。
- 主键索引:
- 在 MySQL 中,表的主键自动成为一个唯一索引。主键用于唯一标识表中的每一行,并且要求主键列中的值必须唯一。
- 唯一索引:
- 唯一索引不仅加快查询速度,还保证索引列的每行数据的唯一性。与主键索引类似,但一个表可以有多个唯一索引。
- 外键索引:
- 在使用 InnoDB 存储引擎时,MySQL 会自动在外键上创建索引(如果尚未存在)。外键索引用于加速表之间的关联操作。
- 优化 SQL 语句
- 选择合适的字段:尽量只查询需要的字段,而不是使用
SELECT *
。 - 避免在
WHERE
子句中使用函数或表达式:这会导致索引失效。 - 使用连接(JOIN)替代子查询:在一些情况下,连接查询的性能会比子查询更好。
- 分解复杂的查询:将一个复杂的查询分解成多个简单的查询,有时可以提高性能。
- 使用索引
- 创建合适的索引:根据查询的特点(如经常出现在
WHERE
、JOIN
、ORDER BY
、GROUP BY
子句中的列)创建索引。 - 避免过多索引:虽然索引可以加快查询速度,但过多索引会增加写操作的负担,并占用更多存储空间。
- 使用前缀索引:对于文本字段,可以考虑使用前缀索引来减少索引大小和提高索引效率。
- 查询执行计划分析
- 使用
EXPLAIN
或EXPLAIN ANALYZE
命令来分析查询的执行计划,识别慢查询的原因,比如是否有全表扫描、索引是否被利用等。
- 优化数据访问
- 减少请求的数据量:只返回必要的行和列,避免不必要的数据访问。
- 缓存重复查询:对于重复执行且数据变动不频繁的查询,可以通过应用层缓存来提高性能。
- 数据库结构优化
- 数据类型优化:为字段选择合适的数据类型,小的数据类型通常更快,因为它们占用的磁盘、内存和CPU周期更少。
- 表分区:对于非常大的表,可以考虑分区来提高查询效率。
- 服务器配置
- 根据数据库的负载特点调整 MySQL 服务器配置,如增加缓冲池大小(
innodb_buffer_pool_size
)、调整连接数等。
- 使用更快的存储硬件
- 使用 SSD 替代传统硬盘可以显著提高 I/O 性能。
总结
MySQL 查询优化是一个涉及到 SQL 语句、索引设计、数据模型、服务器配置和硬件等多方面的复杂过程。理解和应用这些策略可以帮助提高查询效率,优化数据库性能。在具体的优化工作中,需要根据实际的应用场景和业务需求来制定优化方案。
-
使用合适的索引类型:根据查询模式选择最合适的索引类型(比如,B-Tree、FULLTEXT、HASH 等)。
-
创建复合索引:对于经常一起查询的列,考虑创建复合索引,而不是为每个列单独创建索引。
-
避免冗余和重复的索引:定期检查索引,删除不必要或重复的索引以减少维护成本和提升写操作性能。
-
考虑前缀索引:对于长文本字段,使用前缀索引可以节省空间并提升效率。
CREATE INDEX index_name ON table_name(column_name(length));
-
利用索引扫描顺序:尽量使索引的列顺序与查询条件和排序条件一致。
-
使用
EXPLAIN
分析查询:定期使用EXPLAIN
命令分析查询计划,确保索引被正确使用。
-
Docker 是一个容器化工具,主要用来把应用打包进容器,让它们在任何环境中都能以相同的方式运行。它主要处理单个容器的创建、运行和管理。
-
Kubernetes,另一方面,是一个容器编排工具。它用来管理和调度多个容器(可能是用 Docker 创建的)在一个集群中的运行。Kubernetes 能做到自动化部署、扩展应用和保持应用的稳定运行。
-
所以基本上,Docker 更多关注于单个容器层面,而 Kubernetes 则是在更高层面上管理这些容器,特别是在大规模和分布式系统中。
- 基本概念和作用:
- Docker:Docker 是一个容器化平台,它允许开发者将应用及其依赖打包到一个轻量级、可移植的容器中。容器在运行时在 Docker 引擎上相互隔离,共享同一操作系统内核。Docker 主要用于创建和管理单个容器。
- Kubernetes:Kubernetes 是一个容器编排系统,用于自动化容器的部署、扩展和管理。它设计用来在集群环境中运行和协调多个容器化应用或服务。
- 功能和范围:
- Docker:提供了容器的生命周期管理,包括容器的构建、运行、停止和删除。Docker 通过 Dockerfile 简化了容器创建的过程。
- Kubernetes:提供了更广泛的系统级功能,如集群管理、服务发现、负载均衡、自动扩展、滚动更新和自愈能力。
- 使用场景:
- Docker:适用于开发阶段的应用容器化、简单的应用部署和单机上的容器管理。
- Kubernetes:适用于生产环境中的大规模容器管理,特别是当需要高可用性、扩展性和复杂的服务协调时。
- 架构:
- Docker:采用客户端-服务器架构。Docker 客户端与 Docker 守护进程(服务器)交互,守护进程负责构建、运行和分发容器。
- Kubernetes:采用主从架构。Kubernetes 集群由一个或多个主节点(负责集群管理和调度)和多个工作节点(运行容器化应用)组成。
- 可伸缩性和高可用性:
- Docker:单独使用 Docker 时,并不直接提供多节点集群的高可用性和可伸缩性支持。
- Kubernetes:天然支持多节点集群,能够自动管理和扩展应用,提供故障转移和负载均衡能力。
Redis 的字符串(String)类型是最基本的数据类型之一,也是 Redis 中使用最为广泛的类型。它可以存储任何形式的字符串(包括二进制数据),最大能够存储的数据量为 512MB。在 Redis 中,字符串类型不仅用来存储文本或数值数据,还常用于实现更复杂的数据结构和功能,如计数器、锁等。
Redis 的字符串类型底层实际上是由一个名为 simple dynamic string
(简单动态字符串,SDS)的结构实现的。SDS 是 Redis 的私有实现,它比标准的 C 字符串提供了更多的优势:
- 长度可变:
- SDS 支持动态修改字符串内容而无需重新分配内存(直到超出当前分配的空间)。
- SDS 结构内部维护了字符串的实际长度,使得字符串的长度修改操作更高效。
- 二进制安全:
- SDS 可以存储任何二进制数据,包括
\0
字符。 - 这意味着它可以存储除了文本之外的任何类型的数据,如图片、音频等二进制数据。
- SDS 可以存储任何二进制数据,包括
- 获取字符串长度的时间复杂度为 O(1):
- SDS 结构内部存储了字符串的长度,所以获取字符串长度不需要遍历整个字符串,性能较高。
- 空间预分配和惰性空间释放:
- 当对 SDS 进行扩展操作(如追加字符串)时,Redis 会预分配额外空间,以减少将来修改时所需的内存重新分配次数。
- 同时,当字符串缩短时,Redis 不会立即释放多余的内存空间,而是使用惰性空间释放策略,以优化内存使用和减少内存碎片。
- 存储文本或二进制数据:作为最基础的数据类型,字符串可以存储各种形式的文本或二进制数据。
- 计数器:使用 Redis 字符串存储数值数据,可以利用
INCR
、DECR
等命令实现原子性的计数功能。 - 分布式锁:利用字符串类型和相关命令,可以实现分布式锁的功能。
- 缓存:字符串类型常用于缓存用户信息、网页内容、数据库查询结果等。
Redis 的字符串类型因其简单、灵活而广泛应用于各种场景,是构建高性能、可扩展应用程序的重要基础。
Redis 哨兵是 Redis 的高可用解决方案,它主要负责三个核心任务:
- 监控:持续检测 Redis 主服务器和从服务器的运行状态。
- 自动故障转移:当主服务器出现故障时,自动将一个从服务器升级为新的主服务器,并重新配置其他从服务器,保持服务的可用性。
- 服务发现:客户端可以询问哨兵当前哪个是主服务器,以及获取最新的主从服务器配置。
哨兵通过发送定期的 PING 命令来监控 Redis 实例。如果主服务器在配置的超时时间内未响应,哨兵会认为它可能下线,并通过多个哨兵之间的协商来确认主服务器是否真的下线。一旦确认,哨兵将执行故障转移,自动选择一个从服务器作为新的主服务器。
- 高可用性:哨兵通过监控和自动故障转移确保了 Redis 服务的连续性和稳定性,即使在主服务器出现故障时也能保持服务的可用性。
- 自动化操作:故障转移过程是自动进行的,减少了人工干预的需要,提高了系统的可维护性。
哨兵系统通过提供一种自动化的方式来处理 Redis 实例的故障,是构建高可用 Redis 系统的关键组件。
- 缓存失效
- 写操作时使缓存失效:在数据库写操作(更新、删除)后,立即删除相关缓存。这确保了后续的读操作会从数据库获取最新数据。
- 延迟双删策略
- 在数据库更新前后各删除一次缓存,并在数据库更新后稍作延迟,然后再第二次删除缓存,以覆盖数据库写操作期间可能发生的缓存读取。
- 缓存过期时间
- 为缓存数据设置过期时间,让老旧数据自动失效,减少数据不一致的机会。
- 消息队列
- 使用消息队列异步更新缓存,确保数据库操作和缓存操作的一致性。