# Java **Repository Path**: itic/java ## Basic Information - **Project Name**: Java - **Description**: 我一定会成为人类历史上最伟大的Java工程师。 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 0 - **Created**: 2025-08-08 - **Last Updated**: 2026-01-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: Java ## README # JAVA ## Java基础 ### String类能被继承吗,为什么不可变 1. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。 2. 保存字符串的数组被 final 修饰且为私有的,并且String类没有提供/暴露修改这个字符串的方法。 ### String 字面量 vs new String()有什么区别 使用字符串字面量(如 "Hello")更高效,因为它利用了`字符串常量池`来减少内存使用。 使用 new String() 每次都创建一个新的对象,导致不必要的内存消耗。 除非有特殊需求,否则推荐使用字符串字面量的方式。 ```java String s1 = "Hello"; String s2 = "Hello"; // s1 和 s2 都指向常量池中的同一个 "Hello" 对象 String s3 = new String("Hello"); // 在堆上创建一个新的 "Hello" 对象 String s4 = new String("Hello"); // 再次在堆上创建另一个新的 "Hello" 对象 System.out.println(s1 == s2); // true, 因为它们引用的是常量池中的同一个对象 System.out.println(s1 == s3); // false, s1 引用的是常量池中的对象,而 s3 引用的是堆上的新对象 System.out.println(s3 == s4); // false, s3 和 s4 各自引用了堆上的不同对象 ``` ### Hashtable和HashMap区别 > 它们的核心区别主要体现在数据结构、线程安全性、性能以及对 null值的处理上。 | 特性 | HashMap (JDK 1.8+) | HashTable | | :--- | :--- | :--- | | **底层结构** | 数组 + 链表 + 红黑树 | 数组 + 链表 | | **线程安全** | 否(需用 `Collections.synchronizedMap` 或 `ConcurrentHashMap`) | 是(方法用 `synchronized` 修饰) | | **性能** | 高(无锁开销,有树化优化) | 低(全局锁,高并发下性能差) | | **null键/值** | 允许(key 和 value 都可以为 null) | 不允许(会抛出异常) | | **迭代器** | `Iterator` 是 fail-fast 的 | `Enumerator` 不是 fail-fast 的 | | **继承类** | 继承自 `AbstractMap` | 继承自 `Dictionary` | | **初始容量** | 16 | 11 | | **扩容** | `oldCap << 1` (2倍) | `oldCap * 2 + 1` | | **哈希计算** | 二次哈希(扰动函数)降低冲突 | 直接使用 `hashCode()`,冲突概率较高 | ### HashMap在1.8为什麽改为尾插法 > 最核心、最直接的原因是为了彻底解决在多线程环境下使用 HashMap 时可能出现的“死循环”问题。 | 特性 | 头插法 (JDK 1.7) | 尾插法 (JDK 1.8) | | :--- | :--- | :--- | | **顺序** | 反转链表顺序 | 保持链表原顺序 | | **并发扩容** | 极有可能产生循环引用(死循环),导致 CPU 爆满 | 不会产生循环引用,但会有数据覆盖等线程安全问题 | | **初衷** | 认为新插入的数据更可能被访问,放在头部可以提升缓存命中率(但收益微乎其微) | 根本性解决并发死循环问题,同时为引入红黑树做铺垫(需要保持相对顺序) | ### HashMap扩容为什麽容量是2的幂 计算索引时可以用 位运算 代替取模运算:`index = hash % capacity → index = hash & (capacity - 1)` 因为: - 分布均匀 - 减少了碰撞的几率 `capacity = 16 → capacity - 1 = 15 = 01111 hash & 15 相当于取 hash 的低 4 位 → 就是 hash % 16` ### HashMap的扩容机制 1. 触发条件: HashMap 在元素数量超过扩容阈值(容量 × 负载因子,默认 0.75)时会触发扩容。 - 容量 (Capacity):哈希表中桶(bucket)的数量,默认初始容量是 16。 - 负载因子 (Load Factor):一个衡量哈希表填满程度的指标,默认值是 0.75。 2. 扩容过程 - 创建新数组:创建一个容量为原数组两倍的新数组(例如,从 16 扩容到 32)。 - 重新分配元素 (Rehash):将原数组中的所有键值对重新计算其在新数组中的位置,并放入新数组。 - 更新引用:将 HashMap 内部的数组引用指向新数组,旧数组等待垃圾回收。 3. JDK 1.8 的关键优化:高效再哈希 - 在 JDK 1.8 之前,每次扩容都需要对每个元素的 key 重新调用 hash() 函数,然后通过 (n-1) & hash 计算其在新数组中的索引。这是一个相对耗时的操作。 - JDK 1.8 引入了一个巧妙的优化,避免了对每个元素进行完整的 rehash 计算,大大提升了扩容性能 ```markdown 1. 核心原理: HashMap 的容量始终是 2 的幂(如 16, 32, 64...)。这意味着容量的二进制表示只有一位是 1(例如,16 是 10000,32 是 100000)。 元素在数组中的索引是通过 hash & (capacity - 1) 计算得出的。capacity - 1 的二进制表示是低位连续的 1(例如,15 是 1111,31 是 11111)。 当容量从 n 扩容到 2n 时,2n - 1 比 n - 1 多了一位高位 1。 关键点:一个元素在扩容后的新索引,只可能有两个位置: 原索引位置 (low position) 原索引位置 + 旧容量 (high position) 2. 判断方法: 只需要检查元素 key 的 hash 值在新增的那一位(即 hash & 旧容量)是否为 1。 如果 (hash & 旧容量) == 0,则该元素在新数组中的索引等于其在旧数组中的索引。 如果 (hash & 旧容量) != 0,则该元素在新数组中的索引等于其在旧数组中的索引 + 旧容量。 3. 为什么? 旧容量 对应的二进制位是 100...0。 hash & 旧容量 的结果,就是 hash 值在 旧容量 对应的那个二进制位上的值(0 或 1)。 这个位恰好是决定元素在新数组中是留在低位桶(原位置)还是迁移到高位桶(原位置 + 旧容量)的关键。 ``` ### ConcurrentHashMap​ 在JDK7和JDK8中的区别?为什么它线程安全? | 特性 | JDK 7 | JDK 8 | | :--- | :--- | :--- | | **核心设计** | 分段锁 (Segment) | CAS + `synchronized` | | **锁粒度** | 中等(锁住整个 `Segment`) | **非常细**(锁住链表/红黑树的头节点) | | **数据结构** | `Segment` 数组 + `HashEntry` 数组(链表) | `Node` 数组 + 链表 + **红黑树** | | **线程安全机制** | `ReentrantLock` 锁 `Segment` | `volatile` 变量 + CAS + `synchronized` 锁桶(`Node` 头节点) | | **并发度** | 由 `concurrencyLevel` 决定(默认 16),初始化后不可变 | 理论上可达到数组长度,并发度更高 | | **性能** | 读操作无锁,写操作在 `Segment` 级别加锁,并发性受限 | 读操作无锁(`volatile` 读),写操作锁粒度极小,性能显著提升 | | **初始化** | `Segment` 数组在构造时初始化 | `Node` 数组(`table`)延迟初始化,首次 `put` 时才创建 | ### ConcurrentHashMap在什么场景下使用 CAS,什么场景下使用 synchronized > CAS 用于“无中生有”或“轻量更新”(如空桶插入、初始化、计数),而 synchronized 用于“已有内容”的复杂操作(如链表插入、删除、树化) > | 场景 | 使用机制 | 原因 | | :--- | :--- | :--- | | **初始化 table** | CAS | 避免多个线程重复初始化,保证只有一个线程能成功创建数组。 | | **向空桶插入第一个节点** | CAS | 无哈希冲突,使用乐观锁直接写入,性能高,避免加锁开销。 | | **更新 size 计数** | CAS + `CounterCell` | 高并发下 `size()` 统计,使用类似 `LongAdder` 的分段计数思想,减少 CAS 竞争,提升性能。 | | **扩容协调** | CAS 操作 `sizeCtl` | 通过 `sizeCtl` 状态变量协调多个线程参与扩容 (`transfer`),避免重复或冲突。 | | **桶不为空,插入/删除/修改** | `synchronized` 锁头节点 | 保证对链表或红黑树的遍历、插入、删除等复杂操作的原子性,防止并发修改导致结构损坏。 | | **链表转红黑树** | `synchronized` 块内执行 | 转换过程复杂,必须在同步块内进行,确保转换期间桶的状态稳定,防止并发修改导致结构不一致或损坏。 | ### ConcurrentHashMap的put流程 ``` 开始 put(key, value) ↓ 检查 key/value 是否 null → 是?抛异常 ↓ 计算 hash ↓ 定位桶 index = (n-1) & hash ↓ 查看 table[index] 是否为空? ├─ 是 → 使用 CAS 插入新节点(乐观尝试) │ ├─ 成功:结束 │ └─ 失败:说明有竞争 → 执行for循环后发现已有节点后进入 synchronized 块 │ └─ 否 → 当前桶已有节点 ↓ synchronized (头节点) { 遍历链表/树,插入或更新 } ``` ### volatile作用 | 特性 | `volatile` | `synchronized` | |--------------|-------------------------------------------|-----------------------------------------| | 作用范围 | 变量级别 | 代码块或方法级别 | | 主要功能 | 保证可见性 + 禁止指令重排序 | 保证原子性、可见性、有序性 (能保证有序性,但只到“块边界”为止) | | 原子性保证 | ❌ 不能(如 `i++` 不安全) | ✅ 可以 | | 锁机制 | 无锁(基于内存屏障) | 有锁(可重入) | | 性能开销 | 低 | 较高(涉及操作系统调度) | | 使用场景 | 状态标志位、双重检查锁定(DCL) | 复合操作、临界区保护 | ### 是否可以只用 `synchronized` 而不用 `volatile`? - 所有共享变量的访问都受synchronized保护 - 不涉及指令重排 - 性能要求不高 | 情况 | 是否可以只用 `synchronized` | |------|----------------------------| | 所有读写都走同步方法/块 | ✅ 可以 | | 高频读、低频写的标志位 | ❌ 建议用 `volatile` | | 需要禁止指令重排(如 DCL) | ❌ 必须用 `volatile` | | 复合操作(如 i++) | ✅ 必须用 `synchronized`(`volatile` 不够) | ### CAS是什么 CAS(Compare-And-Swap)是一种原子操作,它包含三个数:内存位置、预期值、新值。 如果内存位置的值等于预期值,就把它更新为新值,整个过程是原子的。 ### CAS有什么缺点 1. ABA 问题: - 值从 A → B → A,CAS 会误以为没变过。 - 解决:加版本号,比如用 AtomicStampedReference。 2. 自旋消耗 CPU: - 竞争激烈时,线程不断重试 CAS,空转浪费 CPU。 - 解决:限制重试、使用 LongAdder 等优化结构。 ``` 问题场景:AtomicLong 在高并发 increment() 时,所有线程都竞争同一个 value 变量,导致 CAS 失败率极高,自旋严重。 LongAdder 的设计思想: 分段 (Striped):内部维护一个 Cell 数组(类似于分段锁的分段思想)。 分散竞争:每个线程根据自己的 ThreadLocal 或哈希值,尝试更新数组中的不同槽位(Cell)。这极大地降低了线程间的直接竞争。 减少自旋:因为竞争分散了,单个 Cell 上的 CAS 失败率大大降低,自旋次数和 CPU 消耗也随之减少。 最终聚合:sum() 方法会将 base 值和所有 Cell 的值相加,得到最终结果。sum() 操作相对较慢,但 increment() 非常快。 适用场景:高并发、读少写多的计数场景(如统计请求数、耗时等)。如果读操作(sum())非常频繁,则 AtomicLong 可能更优。 ``` 3. 只能保证单个变量的原子性: - 无法原子更新多个变量。 - 解决:封装成对象,或用锁。 ### CAS算法的ABA问题 > ABA 问题是指:一个变量 V 的值从 A 变为 B,又变回 A。此时,CAS 操作会误认为“值未被修改”,从而成功执行,但实际上中间已经发生了变化。 版本号(Version/Stamp)机制 —— AtomicStampedReference Java 提供了 AtomicStampedReference,它不仅记录值,还记录一个版本号(stamp),每次修改版本号递增。 ```java AtomicStampedReference asr = new AtomicStampedReference<>(100, 0); // T1 读取时记录值和版本号 Integer expectedValue = asr.getReference(); int stamp = asr.getStamp(); // T2 修改:值变 80,版本号 +1 asr.compareAndSet(100, 80, stamp, stamp + 1); // stamp: 0 → 1 // T2 恢复:值变 100,版本号 +1 asr.compareAndSet(80, 100, stamp + 1, stamp + 2); // stamp: 1 → 2 // T1 执行 CAS boolean success = asr.compareAndSet(expectedValue, 120, stamp, stamp + 1); // 失败!因为当前版本号是 2,而 T1 期望的是 0 ``` ```java public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); } ``` ### AQS 是什么?原理? 抽象队列同步器 AQS(AbstractQueuedSynchronizer)是 Java 并发包 java.util.concurrent 的核心基础框架之一,它是实现锁和同步器(如 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等)的底层支撑。 AQS 通过一个 volatile 的 state 变量和一个 FIFO 等待队列,利用 CAS 实现原子修改,将获取不到锁的线程放入队列并阻塞,释放时唤醒后继线程,从而实现高效的线程同步。 ### 遇到过死锁吗?怎么排查和解决的? 1. 生产环境中的典型表现 | 表现 | 说明 | | :--- | :--- | | **🚫 某些请求一直不返回** | 用户或调用方看到接口超时、页面卡住,无法获得响应。 | | **🐢 系统响应变慢甚至停滞** | 死锁线程被永久阻塞,依赖这些线程的业务流程完全卡住,系统部分或整体失去响应。 | | **📈 CPU 使用率不高** | 死锁的线程处于 `BLOCKED` 状态,不执行任何指令,因此不会消耗 CPU 资源。这是与“活锁”或“CPU 密集型”问题的关键区别。 | | **🧱 线程池耗尽** | 多个线程陷入死锁而无法释放,导致线程池中可用线程数减少,新来的请求因无法获取线程而排队或被拒绝。 | | **🔁 日志停在某个位置** | 发生死锁的线程在打印完获取到的最后一个锁的日志后,就无法继续执行并打印后续日志,导致日志流在特定位置停滞。 | | **🔄 监控显示线程状态为 BLOCKED** | 通过 `jstack`、`jvisualvm` 或 APM 监控工具,可以清晰地看到大量线程的堆栈信息中包含 `java.lang.Thread.State: BLOCKED (on object monitor)`,并指明在等待哪个对象的监视器。 | 2. 如何通过工具发现死锁 - jstack (最常用), 如果检测到死锁,jstack 会明确输出:`Found one Java-level deadlock` - jconsole 或 jvisualvm ``` 实时查看线程状态 点击“检测死锁”按钮,自动提示死锁线程 ``` 3. 预防和解决 | 措施 | 说明 | |------|------| | ✅ 代码规范 | 避免嵌套锁,统一加锁顺序 | | ✅ 使用 `tryLock(timeout)` | 设置超时,避免无限等待 | | ✅ 监控线程状态 | 在线程池监控中加入 `BLOCKED` 线程告警 | | ✅ 定期 `jstack` 巡检 | 尤其在高负载时 | | ✅ 压测环境模拟死锁 | 提前发现潜在问题 | ### 如何跨线程传递 ThreadLocal 的值? 1. 使用 InheritableThreadLocal(基础方案) > InheritableThreadLocal 是 ThreadLocal 的子类,它支持**父线程创建子线程时**传递值。但是只支持父子线程,不支持线程池(因为线程池中的线程是复用的,不是“新创建”的子线程); 另外一旦子线程启动后,父线程再修改 InheritableThreadLocal,子线程不会感知。 ```java public class InheritableExample { private static final InheritableThreadLocal userContext = new InheritableThreadLocal<>(); public static void main(String[] args) { userContext.set("main-thread-user"); // 子线程可以继承父线程的值 Thread child = new Thread(() -> { System.out.println("Child thread: " + userContext.get()); }); child.start(); } } ``` ```java // 源码 // Thread 的构造方法会调用 init() 方法 private void init(/* ... */) { // 1、获取父线程 Thread parent = currentThread(); // 2、将父线程的 inheritableThreadLocals 赋值给子线程 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); } ``` 2. 使用 TransmittableThreadLocal(推荐方案) > TransmittableThreadLocal(TTL)是阿里巴巴开源的工具,专门解决 ThreadLocal 在线程池中传递的问题。它通过**任务包装 + 上下文快照 + 自动清理的机制**,完美解决了 ThreadLocal 在异步和线程池环境下的传递问题 ```java public class TTLExample { private static final TransmittableThreadLocal userContext = new TransmittableThreadLocal<>(); public static void main(String[] args) { // 创建线程池(需使用 TTL 提供的包装) ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2)); userContext.set("main-user"); // 提交任务 executorService.submit(() -> { System.out.println("Task in thread pool: " + userContext.get()); // 输出: main-user }); executorService.shutdown(); } } ``` ```java // TransmittableThreadLocal工作流程 主线程 (Main Thread) | | userContext.set("main-user") | | | V | executorService.submit(task) <--- TtlExecutors 包装的 submit | | | | 1. 捕获快照: {userContext: "main-user"} | | 2. 创建 TtlRunnable 包装 task | | 3. 将 TtlRunnable 提交到底层线程池 | V 底层线程池 (Worker Thread 1) | | 取出 TtlRunnable | | | V | TtlRunnable.run() | | | | 4. 从快照恢复: userContext.set("main-user") | | 5. 执行原始 task.run() | | | | | V | | System.out.println(userContext.get()) --> "main-user" | | | | | V | | 6. 清理: userContext.remove() (或恢复旧值) | V | TtlRunnable.run() 结束 | V Worker Thread 1 回到线程池 (干净状态) ``` ### 线程池常见参数有哪些 1. ThreadPoolExecutor 3 个最重要的参数: - corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量。 - maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 - workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 1. ThreadPoolExecutor其他常见参数 : - keepAliveTime:当线程池中的线程数量大于 corePoolSize ,即有非核心线程(线程池中核心线程以外的线程)时,这些非核心线程空闲后不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁。 - unit : keepAliveTime 参数的时间单位。 - threadFactory :executor 创建新线程的时候会用到。 - handler :拒绝策略 ### 线程池处理任务的流程 1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。 1. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。 1. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。 1. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用RejectedExecutionHandler.rejectedExecution()方法。 ### 线程池有哪些拒绝策略 | 策略 | 行为 | 是否丢弃 | 是否抛异常 | 适用场景 | | :--- | :--- | :--- | :--- | :--- | | `AbortPolicy` | 抛出异常 | 否 | 是 | **默认**,需明确反馈 | | `CallerRunsPolicy` | 调用者线程执行 | 否 | 否 | 不丢任务,可接受延迟 | | `DiscardPolicy` | 静默丢弃 | 是 | 否 | 任务不重要 | | `DiscardOldestPolicy` | 丢最旧任务,重试新任务 | 是 | 否 | 新任务更重要 | ### 线程池如果不允许丢弃任务,应该选择哪个拒绝策略? `CallerRunsPolicy `。 不过,如果走到CallerRunsPolicy的任务是个非常耗时的任务,且处理提交任务的线程是主线程,可能会导致主线程阻塞,影响程序的正常运行。 可以参考任务持久化的思路。包括但不限于: mysql, redis, 消息队列。 ```markdown 以mysql为例。 1. 实现RejectedExecutionHandler接口自定义拒绝策略,自定义拒绝策略负责将线程池暂时无法处理(此时阻塞队列已满)的任务入库(保存到 MySQL 中)。。 2. 继承BlockingQueue实现一个混合式阻塞队列,该队列包含 JDK 自带的ArrayBlockingQueue。另外,该混合式阻塞队列需要修改取任务处理的逻辑,也就是重写take()方法,取任务时优先从数据库中读取最早的任务,数据库中无任务时再从 ArrayBlockingQueue中去取任务。 ``` ### notify和notifyAll有什么区别 ```markdown notify()和notifyAll()都属于`Object类的方法`,用于实现线程间的通信。 notify()方法用于唤醒在当前对象上等待的单个线程。如果有多个线程同时在某个对象上等待(通过调用该对象的wait()方法),则只会唤醒其中一个线程,并使其从等待状态变为可运行状态。具体是哪个线程被唤醒是不确定的。 notifyAll()方法用于唤醒在当前对象上等待的所有线程。如果有多个线程在某个对象上等待,调用notifyAll()方法后,所有等待的线程都会被唤醒并竞争该对象的锁。其中一个线程获得锁后继续执行,其他线程则继续等待。 需要注意的是,notify()和notifyAll()方法只能在 **同步代码块或同步方法** 内部调用,并且必须拥有与该对象关联的锁。否则会抛出IllegalMonitorStateException异常。 ``` ### Java反射getDeclaredXXXX vs getXXXX 区别 > 记忆口诀: > > Declared = "声明的" → 只关心这个类自己声明了什么,不管继承,不管访问修饰符。 > > get = "获取的" → 遵循 Java 的访问规则,只获取能公开访问的成员(包括从父类继承来的 public 成员)。 | 特性 | `getDeclaredXxx()` | `getXxx()` | | :--- | :--- | :--- | | **范围** | 仅限当前类 (`this.class`) | 当前类 + 父类链 | | **访问级别** | 所有 (`public`, `protected`, `package`, `private`) | 仅 `public` | | **能否获取继承成员** | ❌ 不能 | ✅ 能(仅限 `public` 继承成员) | | **典型用途** | 查看类的“完整”内部结构(如框架扫描所有字段/方法)。 | 获取类的公共 API(符合 Java 的访问规则)。 | ### 四种引用的区别 | 引用类型 | 回收时机 | 使用场景 | 是否可 `get()` | |----------|----------|----------|----------------| | **强引用** | 永不回收(只要存在) | 正常对象引用 | ✅ 是 | | **软引用** | 内存不足时回收 | 内存敏感缓存(如图片) | ✅ 是 | | **弱引用** | 每次 GC 都回收 | 缓存、监听器、去重 | ✅ 是(可能 `null`) | | **虚引用** | 随时可回收,需配合队列 | 堆外内存释放、资源清理 | ❌ 永远 `null` | ### OSI 七层模型 > 物联网传回表情 物理层 -> 数据链路层 -> 网络层 -> 传输层 -> 会话层 -> 表示层 -> 应用层 ```markdown - 每一层只与相邻的上下层通信,通过接口传递数据。 - 封装与解封装:数据从上层向下层传递时,每层都会添加自己的头部信息(有时是尾部),这个过程叫封装。在接收端,数据从下层向上传递时,每层会剥去对应的头部信息,这个过程叫解封装 ``` | 层级 | 名称 | 主要作用 | | :--- | :--- | :--- | | **7** | **应用层 (Application Layer)** | **直接为用户应用程序提供网络服务**。它是用户与网络的接口。定义了应用程序用于交换数据的协议和接口。
**常见协议/技术**:HTTP, HTTPS, FTP, SMTP, POP3, IMAP, DNS, Telnet, SSH。 | | **6** | **表示层 (Presentation Layer)** | **处理数据的表示、格式化和编码**。确保一个系统应用层发送的数据能被另一个系统的应用层正确理解。
**核心功能**:数据的加密/解密、压缩/解压缩、格式转换(如 ASCII 到 EBCDIC)、序列化/反序列化。
**常见技术**:SSL/TLS (加密), JPEG, GIF, MPEG, ASCII, Unicode。 | | **5** | **会话层 (Session Layer)** | **建立、管理和终止应用程序之间的通信会话**。负责在设备之间建立、同步、保持和结束会话连接。
**核心功能**:会话的建立与拆除、会话同步(检查点和恢复)、对话控制(单工、半双工、全双工)。
**常见协议/技术**:RPC, NetBIOS, PPTP, L2TP, NFS (部分功能)。 | | **4** | **传输层 (Transport Layer)** | **提供端到端(进程到进程)的可靠或不可靠的数据传输服务**。负责数据的分段、传输、错误检测、流量控制和拥塞控制。
**核心功能**:
- **可靠传输**(如 TCP):确保数据完整、有序、无差错地到达。
- **不可靠传输**(如 UDP):提供“尽力而为”的服务,不保证可靠性。
- **多路复用与多路分解**:通过端口号区分同一主机上的不同应用。
**常见协议**:TCP, UDP, SCTP。 | | **3** | **网络层 (Network Layer)** | **负责数据包(Packet)的路由和转发,实现不同网络之间的互联**。确定数据从源主机到目的主机的“最佳路径”。
**核心功能**:逻辑寻址(IP 地址)、路由选择、数据包转发、拥塞控制(部分)。
**常见协议/技术**:IP (IPv4, IPv6), ICMP, IGMP, ARP (常被视为链路层,但功能相关), 路由协议 (OSPF, BGP, RIP)。 | | **2** | **数据链路层 (Data Link Layer)** | **负责在**(通常指)**物理链路上**(如一根网线、一个局域网段)**提供可靠的数据传输**。将原始的物理比特流组织成“帧”(Frame)。
**核心功能**:
- **成帧**:定义帧的开始和结束。
- **物理寻址**(MAC 地址):识别同一网络内的设备。
- **错误检测**(如 CRC):检测传输中的比特错误。
- **流量控制**(链路级):防止接收方被淹没。
- **介质访问控制**(MAC):决定谁可以在共享介质(如以太网)上发送数据。
**常见协议/技术**:以太网 (Ethernet), Wi-Fi (802.11), PPP, HDLC, MAC 地址, VLAN, 交换机 (工作在此层)。 | | **1** | **物理层 (Physical Layer)** | **定义了网络通信的物理媒介和电气/光学规范**。负责在物理介质上传输原始的比特流(0 和 1)。
**核心功能**:定义电缆类型、连接器、电压电平、信号速率、比特同步、物理拓扑(星型、总线型等)。
**常见技术**:网线 (Cat5e, Cat6), 光纤, 无线电波 (Wi-Fi, 蓝牙), 集线器 (Hub), 中继器 (Repeater), 网卡 (NIC) 的物理接口。 | ### OSI 七层模型,哪层用 IP?哪层用 MAC?哪层用端口? * IP → 网络层 * MAC → 数据链路层 * 端口 → 传输层 ### http的三次握手, 为什麽是三次 > 为了确保双方都具备“发送”和“接收”数据的能力。 ``` 三次握手, 建立连接过程: 第一次:客户端 → 服务端 SYN=1, seq=x → “我想连你,我的序号是 x” 第二次:服务端 → 客户端 SYN=1, ACK=1, seq=y, ack=x+1 → “我知道你要连,我也想连你,我序号是 y” 第三次:客户端 → 服务端 ACK=1, seq=x+1, ack=y+1 → “收到,咱们开始传数据吧” ``` 三次之后,双方都确认了:我能发,你能收;你能发,我能收。 ### http的四次握手, 为什麽是四次 > 因为服务端收到 FIN 后,不能立刻关闭自己的发送通道,可能还有数据要发,所以 ACK 和 FIN 要分开发,必须四次。 ``` 四次挥手过程: 第一次:客户端 → 服务端 FIN=1, seq=u → “我没数据要发了” 第二次:服务端 → 客户端 ACK=1, seq=v, ack=u+1 → “我知道你发完了,但我可能还有数据要发” 第三次:服务端 → 客户端 FIN=1, seq=w, ack=u+1 → “我也发完了” 第四次:客户端 → 服务端 ACK=1, seq=u+1, ack=w+1 → “收到,连接关闭” ``` ### 从输入 URL 到页面展示到底发生了什么 1. DNS 解析:浏览器将域名(如 www.example.com)通过 DNS 查询(先查缓存,再逐级查询)转换成对应的 IP 地址。 1. 建立连接: 与服务器的 IP 地址建立 TCP 连接(通过三次握手)。 如果是 HTTPS,紧接着进行 TLS/SSL 握手,协商加密密钥,建立安全连接。 1. 发送请求:浏览器通过已建立的连接,向服务器发送一个 HTTP(S) 请求(包含方法、路径、头信息等)。 1. 服务器响应:服务器处理请求,生成并返回一个 HTTP 响应(包含状态码、头信息和 HTML 内容)。 1. 渲染页面:浏览器接收 HTML,解析生成 DOM 树,同时请求并解析 CSS 生成 CSSOM 树,两者结合形成 渲染树,然后进行布局(计算位置大小)和绘制(将像素显示在屏幕上)。 > 简单来说,就是:解析域名 -> 建立连接 -> 发送请求 -> 获取响应 -> 解析渲染。 ## 设计模式 ### 策略模式 > 解决多if else 的问题,客户端通过字符串编码在运行时动态选择,新增方式无需改动原有代码,符合“开闭原则”。 ```java public interface PaymentStrategy { /** 返回任意支付方式编码,调用方负责写对 */ String getPayWay(); void pay(int amount); } ``` ```java public class AliPayStrategy implements PaymentStrategy { @Override public String getPayWay() { return "ALI"; } // 自己定 @Override public void pay(int amount) { System.out.println("支付宝付款:" + amount + " 元"); } } public class WeChatPayStrategy implements PaymentStrategy { @Override public String getPayWay() { return "WECHAT"; } @Override public void pay(int amount) { System.out.println("微信付款:" + amount + " 元"); } } public class CreditCardPayStrategy implements PaymentStrategy { @Override public String getPayWay() { return "CREDIT"; } @Override public void pay(int amount) { System.out.println("信用卡付款:" + amount + " 元"); } } ``` ```java public class Order { private final Map repo; public Order(List strategies) { repo = strategies.stream() .collect(Collectors.toMap(PaymentStrategy::getPayWay, Function.identity())); } public void pay(String payWayCode, int amount) { PaymentStrategy strategy = repo.get(payWayCode); if (strategy == null) { throw new IllegalArgumentException("不支持的支付方式:" + payWayCode); } strategy.pay(amount); } } ``` ## 数据库 ### Mysql #### MySQL 的存储引擎有哪些?InnoDB 和 MyISAM 的区别? | 特性 | InnoDB | MyISAM | |------|--------|--------| | **事务** | 支持 (ACID) | 不支持 | | **锁粒度** | 行级锁 | 表级锁 | | **外键** | 支持 | 不支持 | | **崩溃恢复** | 支持 (通过 redo log) | 不支持 | | **MVCC** | 支持 (多版本并发控制) | 不支持 | | **全文索引** | MySQL 5.6+ 支持 | 支持 | | **索引结构** | B+ 树| B+ 树 | | **主键索引** | 聚簇索引 (数据与主键索引在一起) | 非聚簇索引 (索引与数据分离) | | **COUNT(*) 性能** | 需要扫描 (无精确计数) | 直接读取存储的行数 (快) | | **适用场景** | 高并发、需要事务和数据安全的 OLTP | 只读或读多写少、不需要事务的场景 | > 重要纠正: MyISAM 的所有索引(包括辅助索引)的叶子节点都存储“数据行的物理地址”,不是主键值。 而 InnoDB 的辅助索引叶子节点存储的是“主键值”,再通过主键去聚集索引中查找数据(回表)。 #### MySQL的InnoDB有B+和HASH索引,为什么选择B+树 > MySQL的InnoDB存储引擎支持B+树索引还有哈希索引。 如果选用哈希索引会有以下的问题: 1. 哈希索引只能匹配是否相等, 不支持范围查询; 1. 哈希索引没办法利用索引进行order by, group by; 1. 哈希索引无法使用复合索引 (a, b, c); 1. 当数据量很大时,哈希索引也可能发生哈希冲突 B+树: 1. 平衡多叉树 - 平衡:从根节点到任何一个叶子节点的**路径长度都相同** 2. 叶节点**顺序存储且双向链表连接**, 高效的范围查询, 排序, 分组 - 顺序存储:同一个叶子节点内的关键字是有序的。 - 双向链表连接:所有叶子节点通过指针(prev 和 next)连接成一个双向链表。 3. **所有数据存储在叶子节点** - 关键区别:这是 B+ 树与 B 树最核心的区别。 - 内部节点 (非叶子节点)只存储索引信息(关键字和指向子节点的指针),不存储实际数据; 叶子节点存储完整的数据记录(或指向数据记录的指针)。 - 减少 I/O:查询最终都要落到叶子节点。内部节点作为“导航图”,可以更快地定位到目标叶子节点,减少了不必要的磁盘读取。 4. 支持**复合索引** - 复合索引是指在多个列上建立的索引,例如 INDEX idx_name_age (name, age)。 - 排序规则先按第一个列排序,第一个列相同时再按第二个列排序,以此类推。 #### MySQL的索引为什么不选择其他数据结构? **总结** | 数据结构 | 是否适合 MySQL 索引 | 原因 | | :--- | :--- | :--- | | **B+ 树** | ✅ **是 (首选)** | 扇出大,I/O 少;支持范围查询;查询稳定;缓存友好;支持高并发。完美契合磁盘存储和数据库查询需求。 | | **哈希表** | ❌ **否 (仅作补充)** | 不支持范围查询和排序;哈希冲突;不适合磁盘。 | | **B 树** | ❌ **否** | 扇出小,树更高;范围查询效率低;缓存效率低。B+ 树是其优化版。 | | **二叉树/红黑树** | ❌ **否** | 树太高,I/O 次数多;不适合磁盘。 | 1. 为什么不选哈希表 * 优点: * 在**精确查找**(`=`)上,哈希表的平均时间复杂度是 `O(1)`,理论上比B+树的 `O(log n)` 更快。 * 缺点: * **不支持范围查询**:哈希表无法高效地进行 `BETWEEN`, `>`, `<`, `ORDER BY` 等操作。这是数据库查询的常见需求。 * **不支持排序**:哈希表是无序的。 * **哈希冲突**:需要处理冲突(如链地址法、开放寻址),在极端情况下性能会退化。 * **动态扩容开销大**:当哈希表需要扩容时,需要重新哈希所有数据,开销巨大。 * **不适合磁盘存储**:哈希表的随机访问模式对磁盘 I/O 不友好。 * 结论:虽然哈希表在精确查找上快,但牺牲了数据库最重要的功能,范围查询和排序。因此,它**只适合做辅助索引**(如 MySQL 的 Memory 引擎支持哈希索引,或 InnoDB 的自适应哈希索引),不能作为主索引结构。 2. 为什么不选 B 树 (B-Tree)? * 优点: * B 树也是一种平衡多路搜索树,同样能减少 I/O 次数。 * 缺点: * **非叶子节点存储数据**:导致每个节点能存储的键/指针数量减少,**扇出变小,树的高度变高**,增加了 I/O 次数。 * **范围查询效率低**:叶子节点之间没有指针连接,进行范围查询时需要进行中序遍历,访问路径不连续,I/O 效率低。 * **缓存效率低**:非叶子节点存储了数据,体积更大,更难被缓存。 * 结论:B+ 树是 B 树的优化版本,专门针对数据库场景解决了 B 树的上述缺点。**B+ 树在数据库场景下全面优于 B 树**。 3. 为什么不选二叉搜索树 (BST) / 红黑树 (Red-Black Tree) / AVL 树? * **共同缺点**: * **树太高**:它们是二叉树,每个节点最多两个子节点,**扇出极小**。对于海量数据,树的高度会非常高(`O(log₂ n)`,但常数因子大)。查找一条记录可能需要几十甚至上百次磁盘 I/O,性能无法接受。 * **不适合磁盘 I/O**:设计初衷是基于内存操作的,无法有效利用磁盘块(Page)的特性。 * 结论:这些数据结构在内存中性能优异,但在需要频繁磁盘 I/O 的数据库系统中,性能会急剧下降,**完全不适用**。 #### 事务的隔离级别有哪些?分别解决了什么问题? | 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) | InnoDB 实现方式 | |--------|-------------------|-------------------------------|--------------------|----------------| | 读未提交 (Read Uncommitted) | ✅ 可能发生 | ✅ 可能发生 | ✅ 可能发生 | - | | 读已提交 (Read Committed) | ❌ 解决 | ✅ 可能发生 | ✅ 可能发生 | 锁 + MVCC | | 可重复读 (Repeatable Read) | ❌ 解决 | ❌ 解决 | ❌ 解决 (InnoDB 特有) | MVCC + 间隙锁 (Gap Lock) | | 串行化 (Serializable) | ❌ 解决 | ❌ 解决 | ❌ 解决 | 表级读写锁 | #### 什么是 MVCC?它是如何实现的? - **MVCC (Multi-Version Concurrency Control)**:多版本并发控制。它通过维护数据的多个版本来实现非阻塞的读操作。读操作可以读取一个旧版本的数据,而写操作则创建一个新版本,从而避免了读写冲突,提高了并发性能。 - **InnoDB 中的实现**: - **隐藏字段**:InnoDB 为每行数据自动添加几个隐藏字段: - `DB_TRX_ID`:记录最后一次修改(INSERT/UPDATE)该行数据的事务 ID。 - `DB_ROLL_PTR`:回滚指针,指向 undo log 中的一条记录,该记录包含了该行数据之前的版本。 - `DB_ROW_ID`:行 ID(如果表没有主键或唯一非空索引,InnoDB 会创建)。 - **Undo Log**:记录数据修改前的旧版本。通过 `DB_ROLL_PTR` 可以找到这些旧版本,形成一个版本链。 - **Read View**:事务在读取数据时,会创建一个 Read View,它包含: - m_low_limit_id:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见 - m_up_limit_id:活跃事务列表 m_ids 中最小的事务 ID,如果 m_ids 为空,则 m_up_limit_id 为 m_low_limit_id。小于这个 ID 的数据版本均可见 - m_ids:Read View 创建时其他未提交的活跃事务 ID 列表。创建 Read View时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids 不包括当前事务自己和已提交的事务(正在内存中) - m_creator_trx_id:创建该 Read View 的事务 ID #### MySQL的优化可以从哪些方面考虑 > 架构调优 > mysql调优 > 硬件调优 1. 索引优化 ``` 常用的索引包括主键唯一索引、普通索引、全文索引等。 同时,要避免过多的索引,因为每个索引都需要占用存储空间,会影响写入性能。 ``` 2. 查询优化 ``` 尽可能使用索引,避免全表扫描。 要避免使用子查询,尽可能使用连接查询; 避免在查询中使用“%”通配符; 避免多余的字段等等。 ``` 3. 数据库表结构优化 ``` 选择合适字段类型, 应该避免使用大字段,如TEXT、BLOB等。因为这些字段会占用大量的存储空间。同时,应该避免冗余字段,避免更新和维护时的复杂性。 ``` 4. 缓存优化 ``` 使用缓存可以大大减轻MySQL数据库的压力,提高查询效率。常用的缓存技术包括Memcached和Redis等。 ``` 5. 分区优化 ``` 对于数据量较大的表,可以使用分区技术将表分成多个部分。这样可以提高查询效率,同时降低了单个表的存储空间和索引大小。 ``` 6. 配置优化 ``` MySQL的参数配置会影响MySQL的性能。需要根据实际情况进行调整,包括缓冲区、连接数、线程数、查询缓存等等。 ``` 7. 硬件优化 ``` 硬件设备也会影响MySQL的性能。要选择更快速的硬件设备,如更快的磁盘、更快的CPU和更多的内存等等。同时,要根据实际情况来决定使用RAID、SSD等技术。 ``` ``` >> 架构调优 首先需要充分考虑业务的实际情况,是否可以把不适合数据库做的事情放到搜索引擎或者缓存中去做; 然后考虑写的并发量有多大,是否需要采用分布式; 最后考虑读的压力是否很大,是否需要读写分离。 对于核心应用或者金融类的应用,需要额外考虑数据安全因素,数据是否不允许丢失。 >> MySQL调优 需要确认业务表结构设计是否合理,SQL语句优化是否足够,该添加的索引是否都添加了,是否可以剔除多余的索引等等 >> 硬件和OS调优 需要对硬件和OS有着非常深刻的了解,仅仅就磁盘一项来说,一般非DBA能想到的调整就是SSD盘比用机械硬盘更好。DBA级别考虑的至少包括了,使用什么样的磁盘阵列(RAID)级别、是否可以分散磁盘10、是否使用裸设备存放数据,使用哪种文件系统(目前比较推荐的是XFS),操作系统的磁盘调度算法选择,是否需要调整操作系统文件管理方面比如atime属性等等。 ``` #### 什么是慢查询,如何避免 ``` 核心定义: SQL 语句的执行时间超过了预设的“慢查询阈值”。 这个阈值是可配置的。例如,在 MySQL 中,可以通过 long_query_time 参数设置(默认通常是 10 秒)。 慢查询的常见表现: 应用页面加载缓慢或超时。 数据库 CPU 使用率或 I/O 等待时间飙升。 数据库连接池被占满。 SHOW PROCESSLIST 或性能监控工具中看到大量长时间运行的 SQL。 根本原因: 查询效率低下:最常见的是全表扫描。 资源消耗巨大:涉及大量数据的排序(ORDER BY)、分组(GROUP BY)、连接(JOIN)或子查询,且没有有效索引支持。 锁竞争:长时间持有锁,阻塞其他查询 ``` ```markdown 避免慢查询: 1. 启用并分析慢查询日志 MySQL开启 slow_query_log=ON,设置 long_query_time(例如 0.5 秒),并指定日志文件 slow_query_log_file。 分析工具mysqldumpslow(MySQL 自带的简单分析工具), pt-query-digest (Percona Toolkit)。 2. 合理使用索引 创建合适索引: 为 WHERE、ORDER BY、GROUP BY 子句中的列创建合适索引(例如复合索引,并遵循最左前缀原则); 避免索引失效: 不在索引列上进行函数运算或表达式计算; 避免在索引列上使用 != 或 NOT IN,通常会导致全表扫描; 避免在索引列上使用 LIKE 以通配符%开头; 注意类型转换; 定期审查和优化索引: 删除长期未使用或重复的索引(索引维护有成本)。 使用 EXPLAIN 或 EXPLAIN ANALYZE 分析查询执行计划。 3. 优化 SQL 只查询需要的列: 避免使用 SELECT *,只选择真正需要的字段,减少 I/O 和网络传输。 优化 JOIN 操作: 确保 JOIN 的关联字段都有索引; 尽量减少 JOIN 的表数量; 考虑是否可以用子查询或应用层逻辑替代复杂的 JOIN; 谨慎使用子查询: 某些子查询可以改写为 JOIN,性能可能更好。 合理使用分页: 避免 LIMIT 1000000, 10 这种深度分页,它需要扫描前 1000000 行。可以考虑使用游标分页(基于上一页最后一条记录的 ID 或时间戳进行查询)。 避免在 WHERE 子句中对字段进行 NULL 值判断: WHERE column = NULL 应该用 IS NULL,但大量 IS NULL 查询可能效率不高,需结合业务考虑。 批量操作: 将多次 INSERT 或 UPDATE 合并为一条批量语句。 4. 数据库和表结构设计 选择合适的数据类型: 使用能存储所需值的最小数据类型(例如,用 TINYINT 代替 INT 存储状态码),更小的数据类型通常更快,占用更少的磁盘、内存和 CPU 缓存。 避免过度规范化或反规范化: 在查询性能和数据一致性之间找到平衡。 考虑分区: 对于超大表,可以按时间、范围等进行分区,查询时可以只扫描相关分区,极大提升速度。 5. 利用缓存 应用层缓存: 使用 Redis、Memcached 等缓存热点数据,避免频繁查询数据库。 6. 监控与持续优化 建立性能监控体系: 持续监控数据库的 QPS、TPS、慢查询数量、连接数、I/O 等指标。 定期进行性能审计: 主动分析慢查询日志,优化 TOP SQL。 压力测试: 在上线新功能或大促前进行压力测试,提前发现潜在的慢查询。 ``` #### 如何优化MySQL的表结构 1. 索引列的类型尽量小 2. 索引的选择性 (选择离散度高的) 3. 前缀索引 (长字符串可选择前缀索引) 4. 只为用于搜索、排序或分组的列创建索引 5. 多列索引的优化 (遵循最左匹配原则) #### MySQL如何监控和分析死锁 1. 查看最近一次死锁 `SHOW ENGINE INNODB STATUS`, 在输出的 `LATEST DETECTED DEADLOCK` 部分可以详细看到最近一次死锁的两个事务、它们持有的锁、等待的锁以及执行的 SQL 语句 2. 开启死锁日志(生产建议) ```sql -- 查看当前设置 SHOW VARIABLES LIKE 'innodb_print_all_deadlocks'; -- 临时启用(重启失效) SET GLOBAL innodb_print_all_deadlocks = ON; -- 推荐:永久启用(写入配置文件 my.cnf 或 my.ini) [mysqld] innodb_print_all_deadlocks = ON -- 查看错误日志文件路径 SHOW VARIABLES LIKE 'log_error'; ``` #### MySQL如何避免死锁 ``` MySQL 死锁无法完全避免,但可以通过以下方式减少: 统一加锁顺序:所有事务按相同顺序访问表和行; 减少事务大小:避免长事务,及时提交; 使用索引:避免全表扫描导致大量锁; 设置合理的超时时间:对于被锁定的资源,设置合理的超时时间,避免长时间等待导致死锁。 同时,通过 SHOW ENGINE INNODB STATUS 可以分析死锁原因,帮助优化。 ``` #### 如何优化大量数据插入的性能 1. 批量插入 ```sql -- 批量插入 (快) INSERT INTO users (name, email) VALUES ('Alice', 'alice@email.com'), ('Bob', 'bob@email.com'), ('Charlie', 'charlie@email.com'); ``` 2. 使用数据库专用的批量加载工具 ```sql -- MySQL: LOAD DATA INFILE -- LOAD DATA INFILE 快,是因为它绕过了 SQL 解析、逐条事务提交和客户端网络开销,直接在服务器端批量、高效、低开销地加载数据。 LOAD DATA INFILE '/path/to/data.csv' INTO TABLE users FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n' (name, email); ``` 3. 并行化插入 ``` 将数据分片(Shard),由多个线程/进程并行插入不同的分片 ``` #### COUNT(*) vs COUNT(1) vs COUNT(列名) | 写法 | 是否推荐 | 性能 | 说明 | |------------------|----------------|------------|----------------------------------------| | `COUNT(*)` | ✅ 强烈推荐 | ⭐️ 最优 | 标准、清晰、数据库优化最好 | | `COUNT(1)` | ❌ 不推荐 | ⭐️ 相同 | 性能一样,但非标准,易引发误解 | | `COUNT(列名)` | ✅ 用于非空统计 | ⚠️ 稍慢 | 只统计非 NULL 值 | #### COUNT(*)很慢,具体如何提升性能 1. 如果业务需要的统计结果不需要特别精确, 查看执行计划估算行数 ```sql -- 返回的 rows 字段是优化器的估算值,几乎瞬间返回。 EXPLAIN SELECT COUNT(*) FROM users; ``` 2. 使用数据库内置的统计信息 ```sql -- 这是 InnoDB 的估算值,不精确但很快。。 SELECT table_rows FROM information_schema.tables WHERE table_schema = 'your_db' AND table_name = 'users'; ``` 3. explain分析sql看是否有优化的余地,最好能走主键索引。 ```sql SELECT COUNT(*) FROM orders WHERE id > 0; -- 强制走主键索引 ``` 4. 分批查询或者数据分片汇总统计 ```text 如果查询的结果集很大,可以考虑将查询分批进行,每次查询一部分数据,然后累加结果。这样可以减少单次查询的数据量,提高查询速度。 询的数据量分散到多个节点上,提高查询性能。 ``` 5. 使用缓存或者直接新建统计表 #### 分库分表后,查询一定会变快吗 1. 什么时候查询会变快 - 带分片键的查询 2. 什么时候查询会变慢?(分库分表的陷阱) - ​非分片键查询(最经典的坑)​ ``` ​原理​:系统无法确定数据在哪,必须向所有分库和分表​(比如 16 个库 * 16 个表 = 256 个表)都发送一次查询请求(称为广播查询或扫全表)。 ​结果​:原本 1 次的查询变成 256 次查询,然后在内存中聚合所有结果,再进行排序、分页等操作。​网络开销和内存开销巨大,速度极慢。 ​例子​:订单表按 user_id分表,管理员想查询 WHERE product_id = 888的所有订单,就会引发此问题。 解决方案: 建立异构索引(最常用), 可以使用 MySQL 自建索引表,或使用 Elasticsearch (ES)、Redis 等作为索引服务。ES 特别适合复杂的非分片键查询(如模糊搜索、多条件组合) ``` - 分页查询 ``` ​场景​:LIMIT 10000, 20。 ​原理​:同样需要到所有分片查询,每个分片都要取出 10000 + 20条数据,然后在内存中排序聚合,最后才能取出正确的 20 条。​效率低下,偏移量越大越慢。 解决方案: 1. 基于游标的分页, 无法直接跳转到第 N 页,只能“下一页”、“上一页”。 2. 二次查询法(大数据量), 先在所有分片上查询出符合条件的 id,进行聚合排序,再根据最终的 id 列表去主表查询完整数据。网络和内存开销依然存在。适用于偏移量不是特别大的情况。 3. 在业务上限制用户只能查看前几百页的数据,超过则提示“查看更多”或提供搜索功能。前端或后端校验 OFFSET 不能超过某个阈值(如 1000)。 ``` - ​聚合查询 ``` 场景​:COUNT(), SUM(), GROUP BY, ORDER BY。 ​原理​:需要在每个分片上执行一次聚合,然后将中间结果传输到一个节点进行二次计算。​网络传输和数据计算压力都很大。 解决方案: 1. 预计算 + 异步更新 2. 应用层聚合 3. 将业务库的数据同步到专门的 数据仓库(如 Hive, ClickHouse, Doris)或 OLAP 引擎中进行复杂的聚合分析。 ``` - 跨库关联查询 ``` ​场景​:需要连接(JOIN)位于不同分库的表。 ​原理​:数据库本身无法执行跨物理实例的 JOIN。必须在业务代码里模拟,先查 A 表,再根据结果查 B 表,然后手动关联数据。​复杂度高,性能差。 解决方案: 1. 冗余字段(反范式化) 2. 应用层 JOIN 3. 将业务库的数据同步到专门的 数据仓库(如 Hive, ClickHouse, Doris)或 OLAP 引擎中进行复杂的聚合分析。 ``` #### binlog日志如何恢复数据 * 恢复前提条件 1. 必须开启了binlog功能(查看show variables like 'log_bin') 2. 知道需要恢复的时间点或位置点 3. 有完整的binlog文件 * 基本恢复步骤 1. 查看当前binlog文件, `SHOW BINARY LOGS;` 2. 确定恢复的时间点或位置点, `SHOW BINLOG EVENTS IN 'mysql-bin.000001';` 3. 使用mysqlbinlog工具恢复 ```bash # 恢复到指定时间点 mysqlbinlog --start-datetime="2023-01-01 00:00:00" --stop-datetime="2023-01-02 00:00:00" mysql-bin.000001 | mysql -u root -p # 恢复到指定位置点 mysqlbinlog --start-position=107 --stop-position=1000 mysql-bin.000001 | mysql -u root -p ​--start-position=XXX​ 表示 ​从该位置开始(包含 XXX)​​ ​--stop-position=YYY​ 表示 ​在该位置之前结束(不包含 YYY) ``` 4. 恢复单个数据库, `mysqlbinlog -d database_name mysql-bin.000001 | mysql -u root -p` * 高级恢复技巧 1. 先导出为SQL文件再恢复 ```bash mysqlbinlog mysql-bin.000001 > recovery.sql # 编辑recovery.sql文件,删除不需要的语句 mysql -u root -p < recovery.sql ``` 2. 跳过错误继续执行 `mysqlbinlog mysql-bin.000001 | mysql -u root -p --force` 3. 从多个binlog文件恢复 `mysqlbinlog mysql-bin.000001 mysql-bin.000002 | mysql -u root -p` ### Redis #### 启动bat ```bat @echo off :: 启动 Redis 服务器 echo 正在启动 Redis 服务... cd /d C:\Users\Administrator\Desktop\Redis-x64-3.2.100 redis-server.exe redis.windows.conf echo Redis 服务已启动。 pause ``` ``` @echo off 的作用是关闭命令回显 默认情况下,CMD 执行批处理时会把每一条命令本身先打印到屏幕上,然后再执行。加上 @echo off 后,不再显示这些命令本身,只保留程序输出的内容,界面更整洁表示本条命令也不回显,所以连echo off 本身也看不到 在 Windows 批处理里,切换盘符并进入目录 必须用 /d 参数 ``` #### Redis为什么这么快 1. 基于内存操作 2. 使用单线程事件驱动的非阻塞IO模型 3. 单线程模型避免上下文切换,锁竞争的开销 #### Redis的内存淘汰策略 - noeviction (默认策略) : 当内存达到 maxmemory 时,新写入操作会返回错误(如 OOM command not allowed when used memory > 'maxmemory') - allkeys-lru (Least Recently Used - 最近最少使用) :从所有键(keys) 中,淘汰最近最少使用的键 - volatile-lru : 仅从设置了过期时间(TTL)的键中,淘汰最近最少使用的键。 - allkeys-lfu (Least Frequently Used - 最不经常使用) : 从所有键中,淘汰访问频率最低的键 - volatile-lfu : 仅从设置了过期时间的键中,淘汰访问频率最低的键 - allkeys-random : 从所有键中,随机淘汰一个键 - volatile-random : 仅从设置了过期时间的键中,随机淘汰一个键 - volatile-ttl : 仅从设置了过期时间的键中,淘汰剩余生存时间(TTL)最短的键。 **如何选择淘汰策略** | 业务需求 | 推荐策略 | | :--- | :--- | | 通用缓存,希望保留热点数据 | `allkeys-lru` | | 有永久数据,不希望其被淘汰 | `volatile-lru` 或 `volatile-lfu` | | 访问模式不均,长期热点明显 | `allkeys-lfu` | | 对缓存命中率要求不高,或模式随机 | `allkeys-random` | | 希望优先淘汰快过期的数据 | `volatile-ttl` | | 不允许写入失败,必须释放内存 | 选择除 `noeviction` 外的任何策略 | | 不允许数据被淘汰,由应用控制 | `noeviction` | #### lru 与 lfu 区别 > 核心区别在于淘汰的依据不同:一个是基于最近使用时间(LRU),另一个是基于访问频率(LFU)。 如果你的缓存数据“喜新厌旧”,选 lru;如果数据“历久弥新”,选 lfu | 特性 | lru | lfu | | :--- | :--- | :--- | | **关注点** | 时间维度:你“上次”是什么时候用的它? | 频率维度:你“总共”用了它多少次? | | **算法特点** | 关注访问的时效性。一个曾经热门但最近没用的键会被淘汰。 | 关注访问的累积热度。一个长期高频访问的键即使最近几天没用,也可能因为历史热度高而不被淘汰。 | | **内存开销** | 相对较低(存储一个近似 LRU 的时间戳或计数器)。 | 相对较高(需要存储一个能反映频率和衰减的计数器)。 | #### Redis的大Key问题 > String 类型:Value 值超过 10KB 就算较大,超过 100KB 或 1MB 就是典型的大 Key。 > > 集合类型(Hash, List, Set, ZSet):元素个数超过 几千 就算较多,超过 几万 或 几十万 就是大 Key。 1. 危害 * 阻塞主线程,导致 Redis 变慢甚至卡顿 * Redis 是单线程处理命令的(网络 I/O 和命令执行在同一个主线程)。 * 操作一个大 Key(如 HGETALL, LRANGE 0 -1, DEL)会消耗大量 CPU 时间进行序列化、网络传输或内存释放。 * 在此期间,其他所有命令都会被阻塞,导致 Redis 响应延迟飙升,客户端超时。 * 网络带宽耗尽 * 读取或写入一个大 Key 需要传输大量数据,可能瞬间占满服务器的网络带宽,影响其他正常请求 * 内存分配与释放压力 * 创建大 Key 时需要一次性分配大块连续内存,可能引发内存碎片或分配失败。 * 删除大 Key(DEL)时,释放大块内存是同步操作,同样会阻塞主线程。虽然 UNLINK 命令可以异步删除,但仍有代价。 * 主从复制延迟 * 主节点复制大 Key 到从节点需要更长时间,导致主从数据不一致窗口增大,从节点延迟(lag)升高 * 集群迁移困难 * 在 Redis Cluster 中,迁移一个包含大 Key 的 Slot 会非常慢,影响集群的伸缩和维护 * 内存不均 * 大 Key 可能导致某些 Redis 实例或 Slot 内存使用远高于其他,造成负载不均衡 2. 如何发现 * 使用 redis-cli --bigkeys 命令 ```markdown -- 该命令会遍历数据库,在生产环境高峰时段慎用,可能影响性能 redis-cli -h host -p port --bigkeys ``` * 使用 SCAN 命令 + MEMORY USAGE ```markdown 编写脚本,用 SCAN 遍历 Key,对每个 Key 使用 MEMORY USAGE 命令获取其内存占用 可以定期扫描并记录超过阈值的 Key ``` * 使用 Redis 监控系统(如 Redis 自带的 INFO 命令、Prometheus + Redis Exporter、云服务商的监控平台)观察 ```config used_memory / used_memory_rss:整体内存。 instantaneous_ops_per_sec:OPS 突然下降可能意味着阻塞。 latest_fork_usec:BGSAVE/BGREWRITEAOF 耗时变长,可能与大 Key 有关。 master_repl_offset - slave_repl_offset:主从延迟。 ``` * 慢查询日志 (Slow Log) ```config 配置 slowlog-log-slower-than(如 10ms),通过 SLOWLOG GET 查看执行时间长的命令。如果频繁出现 HGETALL, LRANGE, DEL 等操作,且参数是某个特定 Key,那它很可能就是大 Key ``` 3. 如何解决 * 拆分 * 数据压缩 * 在存入 Redis 前,对 Value 进行压缩(如 Gzip, Snappy)。读取后解压 * 适用于可压缩的文本数据(JSON, XML, HTML)。 * 使用合适的数据结构 * 例如,如果只需要判断元素是否存在,用 Set 比 List 更高效。 * 如果需要范围查询,ZSet 比 List 更合适。 * 异步删除 * 删除大 Key 时,使用 UNLINK 命令代替 DEL。UNLINK 会将删除操作放入后台线程异步执行,避免阻塞主线程 * 优化访问模式 * 避免使用 HGETALL, LRANGE 0 -1 等全量操作。尽量使用 HGET, LRANGE start end 获取所需的部分数据。 * 对于排行榜,可以只缓存 Top N,而不是全量数据 * 考虑是否真的需要缓存 * 非常大的数据(如整张图片、大文件)可能不适合放在 Redis。考虑使用专门的对象存储(如 S3, MinIO)或 CDN,Redis 只存其 URL 或元数据。 * 设置合理的过期时间 (TTL) * 确保大 Key 不会长期占用内存 #### Redis的热Key问题 > 热 Key 问题与“大 Key 问题”不同: > > 大 Key:指单个 Key 的 Value 值过大,操作它会消耗大量 CPU 和带宽。 > 热 Key:指单个 Key 的 访问频率极高(QPS 非常高),导致请求集中。 * 危害 * 单点过载:一个热 Key 的所有请求都会打到同一个节点上,造成该节点成为性能瓶颈 * CPU 和网络瓶颈:处理超高 QPS 的请求会耗尽节点的 CPU 资源和网络带宽 * 响应延迟增加:节点处理不过来,导致请求排队,客户端响应时间变长。 * 服务雪崩风险:节点过载可能导致超时、连接失败,进而影响上游应用,甚至引发连锁故障。 * 缓存击穿风险:如果热 Key 恰好在此时过期或被淘汰,所有请求将直接穿透到后端数据库,可能导致数据库崩溃 * 发现 * 使用 redis-cli --hotkeys 命令 ``` -- 此命令依赖于 LFU 计数器,且会扫描数据库,生产环境慎用 redis-cli -h host -p port --hotkeys ``` * 监控工具分析 * Redis 监控:观察单个节点的 instantaneous_ops_per_sec(OPS)。如果某个节点的 OPS 远高于其他节点,且其处理的 Key 比较集中,则可能存在热 Key。 * 网络监控:检查节点的网络流入/流出带宽是否达到瓶颈。 * 慢查询日志 (Slow Log):虽然热 Key 本身操作可能很快,但如果因过载导致其他命令变慢,慢日志会有体现。 * 客户端埋点: * 在应用代码中,对访问频率高的 Key 进行统计和上报。 * 使用 APM(应用性能监控)工具(如 SkyWalking, Zipkin)追踪调用链,识别高频访问的 Key * 解决 * 本地缓存 * 使用 Caffeine, Guava Cache 等内存缓存库 * 多级缓存 (Multi-Level Cache) * 结合本地缓存和分布式缓存(Redis),形成多级缓存体系,`应用 -> 本地缓存 (L1) -> Redis (L2) -> DB` * 使用读写锁或限流 * 读写锁:对于“缓存击穿”场景(热 Key 失效瞬间),可以使用分布式锁(如 Redisson)或本地锁,让一个线程去数据库加载数据,其他线程等待或返回旧数据。 * 限流:在应用层或网关层对访问特定 Key 的请求进行限流,防止流量洪峰。 * 利用 Redis 的复制能力 * 将大部分读请求路由到从节点,实现读写分离,分散读压力 > 核心策略:本地缓存 + 读写分离 是解决热 Key 问题的黄金组合。在绝大多数场景下,优先考虑使用本地缓存,因为它能最直接、最高效地将流量从 Redis 层卸载下来。同时,要建立完善的监控体系,及时发现和处理热 Key #### Redis分布式锁原理 > Redis 分布式锁原理:利用 SET NX PX 原子命令获取锁,用 unique_value + Lua 脚本原子性地释放锁,通过过期时间防死锁,通过Watchdog防业务超时。 > > Redisson:是一个功能强大的 Java Redis 客户端,它不仅实现了上述原理,还提供了可重入、自动续期(Watchdog)、公平锁、Redlock等高级特性,极大地简化了分布式锁的开发,是生产环境的首选方案。。 1. Redis 分布式锁的核心原理 * 基础实现(SET 命令), `SET lock_key unique_value NX PX 30000` * 安全释放锁(Lua 脚本), 不能简单地用 DEL lock_key 释放锁,否则可能发生误删 (***客户端 A 拿到锁 -> 处理时间过长导致锁过期 -> 客户端 B 拿到锁 -> 客户端 A 恢复并执行 DEL -> 错误删除了客户端 B 的锁。***) ```Lua -- 释放锁的 Lua 脚本 -- 原理:脚本先检查 lock_key 的值是否等于当前客户端的 unique_value。只有匹配才删除,否则不操作。 -- 原子性:整个 Lua 脚本在 Redis 内部是原子执行的,杜绝了误删。 if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end ``` * 问题 * 锁过期问题:业务执行时间 > 锁过期时间 * 可重入性:同一个线程能否多次获取同一把锁 2. Redisson:Redis 分布式锁的终极解决方案 * 可重入锁 * Redisson 在 Redis 中使用 **Hash** 结构存储锁信息:`key -> Hash Field (unique_value) -> Hash Value (重入计数)`。 * 每次 lock(),计数+1;每次 unlock(),计数-1;计数为0时才真正删除 key。 * 自动续期 (Watchdog) * Watchdog 是一个后台线程,它会在锁过期时间过去 1/3 时(默认锁超时30秒,则10秒后),自动向 Redis 发送命令延长锁的过期时间(例如再续30秒) * 公平锁 * Redisson 支持公平锁 `RLock fairLock = redisson.getFairLock("fair_lock")`; * 实现原理:使用 Redis 的 List 或 Sorted Set 来维护一个等待队列 * 联锁 (MultiLock) * Redisson 支持联锁 `RLock multiLock = redisson.getMultiLock(lock1, lock2, ...);`; 可以将多个 RLock 对象视为一个锁。调用 multiLock.lock() 会同时获取所有锁,unlock() 会释放所有锁 ```java // 1. 获取锁对象 RLock lock = redisson.getLock("lock:order:1001"); // 2. 加锁 (默认使用可重入、公平/非公平、带Watchdog的锁) lock.lock(); // 阻塞直到获取锁 // 或 lock.lock(10, TimeUnit.SECONDS); // 尝试获取,10秒内拿不到则放弃 // 3. 执行业务逻辑 try { // ... 你的业务代码 } finally { // 4. 释放锁 lock.unlock(); } ``` #### Redission的分布式锁Hash的Field中存储的是什么 > Redisson 在 Hash 的 Field 中存储的是“线程唯一标识”(包括线程ID和Redisson实例ID),而不仅仅是“线程ID”。 为什么需要“实例ID + 线程ID”?这是为了解决分布式环境下多个 JVM 实例中线程ID重复的问题。 ```py KEY: "my_lock" TYPE: hash HASH FIELD: "{unique_id}:{thread_id}" HASH VALUE: 重入计数(1, 2, 3...) 举个例子: 机器 A 上的 JVM 实例1:线程ID=50 机器 B 上的 JVM 实例2:线程ID=50(Java 线程ID在不同JVM中可能重复) 如果只用 thread_id 作为标识: 实例1的线程50加锁 实例2的线程50也能“重入” —— ❌ 严重错误! ``` #### Redis的AOF(Append Only File)文件 ``` AOF是Redis 的持久化机制之一,它的作用是: 记录所有写操作命令,用于故障恢复(重启时重放 AOF 文件恢复数据)。 提供比RDB更强的数据安全性(可配置每秒或每次写操作同步)。 Redis宕机重启时数据恢复顺序: 如果`appendonly yes`且AOF文件存在 → 优先使用 AOF。 否则,如果 RDB 文件存在 → 使用 RDB 恢复。 否则,空启动。 ``` #### 为什么推荐同时开启 AOF 和 RDB? > AOF:记录了从 Redis 启动到现在的所有写命令历史(经过重写后是精简的,但仍是所有变更)。 > > RDB:只保存了某个时间点的数据快照。 | 特性 | RDB (快照) | AOF (日志) | 同时开启的优势 | | :--- | :--- | :--- | :--- | | **数据安全性** | 较低。如果在两次快照之间宕机,会丢失这段时间的数据。 | 高。根据 `appendfsync` 策略,最多丢失 1 秒(`everysec`)或 0 次(`always`)写操作。 | ✅ 以 AOF 为主,保障最高数据安全。 | | **恢复速度** | 非常快。RDB 是二进制压缩快照,加载速度快。 | 较慢。需要逐条重放写命令。 | ✅ 虽然恢复用 AOF,但 RDB 可作为备份或迁移使用。 | | **文件大小** | 小。是压缩的二进制快照。 | 大。记录所有写命令,可能包含冗余操作(如多次修改同一 key)。 | ✅ AOF 可通过 `BGREWRITEAOF` 压缩,RDB 文件小,便于备份。 | | **性能影响** | `SAVE` 会阻塞主线程,`BGSAVE` 由子进程完成,对主线程影响小。 | `appendfsync` 策略影响大。`always` 严重降低 QPS,`everysec` 影响较小。 | ✅ `everysec` + `BGSAVE`,性能影响可控。 | | **灾难恢复** | 适合做定期备份(如每小时/每天一个 RDB 文件)。 | 适合做实时或准实时的增量备份。 | ✅ RDB + AOF = 完整的备份策略(全量 + 增量)。 | #### 为什么“直接复制 AOF 文件”在实践中不推荐作为主要迁移手段? 1. 恢复速度极慢(核心缺点) * RDB:加载一个 1GB 的 RDB 文件可能只需要几秒到十几秒,因为它是一个压缩的二进制快照,直接加载到内存即可。 * AOF:重放一个 1GB 的 AOF 文件可能需要几分钟甚至几十分钟,因为它需要逐条解析和执行其中的文本命令。 * 影响:这意味着你的目标 Redis 在启动后会经历一个漫长的“加载中”状态,无法提供服务,停机时间大大延长。 2. AOF 文件可能非常大且冗余 * 即使启用了 auto-aof-rewrite,AOF 文件也会包含大量历史操作。 * 例如,对同一个 key 执行了 SET key 1, SET key 2, SET key 3,AOF 会记录这三条命令,而 RDB 只保存最终的 key=3。 * 迁移一个巨大的 AOF 文件,网络传输时间长,磁盘占用高。 3. AOF 文件格式和配置依赖性强 * AOF 文件是纯文本,其格式依赖于 Redis 版本。 * 某些命令(如 Lua 脚本)在不同版本的 Redis 中序列化方式可能有差异,直接复制可能导致加载失败。 * 需要确保目标 Redis 的 appendonly 配置、appendfilename 等设置完全匹配。 4. “直接复制”无法解决增量问题 * 即使你复制了 AOF 文件,如果源 Redis 在复制过程中还在写入,你仍然会丢失最后几秒的数据。 * 这和 RDB 面临的“数据窗口丢失”问题是一样的。 ```markdown 在线迁移(接近零停机) 1. 使用 BGSAVE 将 RDB 文件复制到目标服务器(全量同步)。 2. 使用专门的工具(如 redis-shake 或 redis-port): - 这些工具会连接到源 Redis 的 AOF 文件或通过 PSYNC 增量复制协议。 - 它们能持续地将源 Redis 的增量写操作实时同步到目标 Redis。 3. 当增量同步的延迟降到极低时,短暂停止源 Redis 写入。 4. 等待增量同步完成。 5. 切流到目标 Redis。 > 这是生产环境大库迁移的常用方案,核心是RDB 做全量 + AOF/PSYNC 做增量。 ``` #### Redis主从同步的过程 > 主从同步的核心:RDB 快照 + 增量命令流 **Redis的主从复制过程分为两个阶段:** 1. 全量同步, 使用RDB ``` 当一个从节点(slave)第一次连接主节点(master),或者从节点断线太久无法进行部分同步时,就会触发全量同步。 过程: 主节点执行 bgsave 命令,生成一个 RDB 快照文件。 主节点将这个RDB文件发送给从节点。 从节点接收RDB文件后,清空自己的数据库,然后加载这个RDB文件,将数据恢复到主节点当时的快照状态。 ``` 2. 增量同步, 使用复制积压缓冲区 ``` 在全量同步完成后,主节点会继续将后续的所有写命令(如 SET, DEL, HSET 等)异步发送给从节点。 这些命令是以原始命令流的形式发送的,不是AOF格式,也不是RDB。 主节点内部维护一个环形缓冲区(复制积压缓冲区),用于缓存最近执行的写命令。 如果从节点短暂断线后重连,它会发送上次复制的偏移量(offset),主节点判断是否还能在缓冲区中找到对应命令: 能找到 → 进行增量同步(只发缺失的部分命令)。 找不到 → 重新触发全量同步(再次生成 RDB)。 ``` #### Redis的哨兵机制 > Redis Sentinel, 一个独立的进程(或一组进程),用于监控 Redis 主从集群。实现自动故障发现和故障转移,保证 Redis 服务的高可用。 - 哨兵不能单点部署,必须是多个哨兵组成的集群(通常至少 3 个)。 ``` 为什么不能只部署2个Sentinel?当主节点宕机时,Sentinel能够自动发现故障,并从从节点中选举出新的主节点,完成故障转移。这个过程依赖于 “多数派投票机制”来做出决策 (2个容错能力为 0) ,防止出现“脑裂”问题。 ``` - 当主节点宕机,哨兵如何自动处理? 1. 阶段1:主观下线 ``` 每个哨兵会定期(默认1秒)向主节点发送PING命令。 如果主节点在 down-after-milliseconds(如 30 秒)内未响应,该哨兵认为主节点“主观下线”。 ``` 2. 阶段2:客观下线 ``` 哨兵 A 认为主节点下线后,会向其他哨兵询问:“你也认为它挂了吗?” 如果超过半数的哨兵都认为主节点下线,则判定为“客观下线”。 此时,故障转移流程正式启动。 ``` 3. 阶段3:选举领导者哨兵 ``` 哨兵集群通过Raft算法(通过epoch(先到先得)和多数派投票选出) 选出一个“领导者哨兵”。 只有这个领导者有权执行故障转移。 ``` 4. 阶段4:故障转移 ``` 领导者哨兵执行以下操作: 选择一个从节点作为新主: 优先级(slave-priority)高的, 数值越小,优先级越高(slave-priority 0,它永远不会被选为新主)。 复制偏移量(offset)最大的(数据最完整)。 runid 最小的(字典序)。 向选中的从节点发送 SLAVEOF NO ONE,将其提升为新主。 通知其他从节点,让它们执行 SLAVEOF new_master,开始复制新主。 将旧主节点标记为“已下线”,待其恢复后自动变为从节点。 ``` 5. 阶段 5:通知客户端 ``` 哨兵会通知客户端:“主节点变了,新地址是 XXX”。 客户端(如 Jedis、Lettuce)通过监听哨兵,自动更新连接。 ``` #### Redis Cluster故障转移机制 > 它不依赖外部的 Sentinel 哨兵,而是将故障检测和转移的功能内置到了每一个主节点中 ``` 一、故障检测阶段 1. 主观下线(PFAIL) •每个节点通过 Gossip 协议与其他节点保持心跳 •当节点 M2 向主节点 M1 发送 PING 后,在 cluster-node-timeout(默认15秒)内未收到 PONG 响应 •M2 会在本地将 M1 标记为 PFAIL(主观下线)状态 2. 客观下线(FAIL) •M2 通过 Gossip 消息将 M1 的 PFAIL 状态传播给其他节点 •集群中所有主节点(M2、M3)开始收集对 M1 的状态判断 •当超过半数主节点(N/2+1)都认为 M1 处于 PFAIL 状态时 •任一节点(如 M2)会将 M1 的状态升级为 FAIL(客观下线)并向集群广播 FAIL 消息 二、选举触发阶段 1. 资格检查 •M1 的从节点 S1 检测到主节点进入 FAIL 状态 •S1 首先自检与主节点的断开时长,确保数据新鲜度符合要求 2. 延迟竞选 •从节点根据复制偏移量排名计算竞选延迟: 延迟 = 500ms + 随机延迟(0-500ms) + 排名 × 1000ms •数据最完整的从节点(复制偏移量最大)排名为0,延迟最短 •此机制确保数据最完整的从节点优先发起竞选 3. 投票表决 •S1 向所有主节点(M2、M3)发送 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST投票请求 •每个主节点在一个选举周期内只能投一票 •S1 获得超过半数主节点的投票后赢得选举 三、故障转移阶段 1. 角色晋升 •获胜的从节点 S1 执行 SLAVEOF no one命令 •S1 正式晋升为新主节点(记为 M1-new) 2. 槽位接管 •M1-new 自动接管原主节点 M1 负责的所有哈希槽 •集群数据访问路由无缝切换到新主节点 3. 集群同步 •M1-new 广播 PONG 消息,宣告身份变更和槽位信息 •集群内所有节点更新本地路由表: •主节点 M2、M3 更新槽位映射关系 •其他从节点 S2、S3 同步新拓扑信息 4. 原主节点处理 •当原主节点 M1 恢复后: •通过接收到的 PONG 消息发现槽位已被接管 •自动降级为新主节点 M1-new 的从节点 •开始从 M1-new 进行数据同步 ``` #### 哨兵 vs Cluster | 对比项 | 哨兵(Sentinel) | Redis Cluster | |----------------|---------------------------------------------|------------------------------------------------| | 数据分片 | ❌ 不支持(单主单从或一主多从) | ✅ 支持(分片存储,16384 个哈希槽) | | 高可用 | ✅ 支持自动故障转移 | ✅ 支持自动故障转移 | | 写扩展 | ❌ 只有一个主节点,写操作无法扩展 | ✅ 多主节点,写操作可分布在不同分片上 | | 复杂度 | 简单(部署和运维相对容易) | 复杂(需管理分片、集群拓扑,客户端需支持 Cluster) | | 适用场景 | 数据量不大、要求高可用性和简单架构的场景 | 数据量大、高并发、需要水平扩展和高性能的场景 | #### Redis Sentinel会发生脑裂吗 > 在分布式系统中,​脑裂指由于网络分区(Network Partition)导致集群中部分节点误认为主节点宕机,从而选举出多个主节点,造成数据不一致。 >> 典型场景​: >> >> • 主节点(Master)和部分哨兵(Sentinel)之间网络中断。 >> >> • 哨兵误判主节点宕机,重新选举新主节点。 >> >> • 原主节点未真正宕机,仍在接受写请求。 >> >> • 结果:​两个主节点同时存在,数据分叉(不一致)​。 * Redis Sentinel 通过以下机制降低脑裂概率 1. 多数派投票 * 哨兵集群需要多数哨兵(>N/2)​​同意才能执行主节点切换(failover)。 * 例如:3 个哨兵至少需要 2 个哨兵达成共识。 * ​作用​:网络分区时,只有一侧能获得多数投票,避免双主。 2. 主节点客观下线 * 哨兵通过 SENTINEL is-master-down-by-addr命令与其他哨兵确认主节点状态。 * 只有多个哨兵都认为主节点不可达,才会触发故障转移。 3. 旧主节点隔离 * 新主节点选举后,哨兵会向旧主节点发送 SLAVEOF命令,强制其降级为从节点。 * 如果旧主节点仍接受写请求,这些写入会在它恢复后因冲突被丢弃。 4. 最小从节点数要求(min-slaves-to-write)​​ * 通过配置 min-slaves-to-write,主节点仅在至少 N 个从节点同步时才会接受写入。 * 如果从节点不足,主节点会自动拒绝写入,避免数据丢失。 * 哨兵模式下仍可能发生脑裂的场景 * 场景 1:哨兵集群网络分区​ * 假设 5 个哨兵,分裂为 2+3 两组。 * 如果 3 个哨兵误判主节点宕机并选举新主,而另外 2 个哨兵仍认为旧主存活。 * ​结果​:短暂出现双主(直到网络恢复)。 * ​场景 2:主节点假死(进程阻塞但未崩溃)​​ * 主节点因 CPU 飙高、磁盘 IO 阻塞等导致无法响应哨兵心跳。 * 哨兵误判主节点宕机并选举新主,但旧主节点仍在处理客户端请求。 * ​结果​:两个主节点同时写入,数据不一致。 * ​场景 3:客户端未更新主节点信息​ * 客户端仍向旧主节点写入,而哨兵已切换至新主节点。 * ​结果​:旧主节点的写入可能丢失(恢复后会被覆盖)。 * 如何进一步避免脑裂? 1. 合理配置哨兵集群​ * ​至少部署 3 个哨兵​(推荐 5 个),确保多数派投票有效。 * 哨兵分散在不同物理机/可用区,减少网络分区影响。 2. 设置 `min-slaves-to-write`, 如果从节点丢失或延迟过高,主节点会拒绝写入,避免数据不一致。 ```conf # 主节点在没有足够且同步良好的从节点时,会拒绝接受任何新的写请求 min-slaves-to-write 1 # 主节点至少要有 1 个从节点才能写入 min-slaves-max-lag 10 # 从节点复制延迟不能超过 10 秒 ``` 3. 监控哨兵状态​ `redis-cli info sentinel`, 关注 sentinel_masters、sentinel_tilt等指标。 4. 使用 Redis Cluster * Redis Cluster 采用分片(Sharding)+ Gossip 协议,脑裂风险更低。 * 但复杂度更高,适合大规模集群。 #### 主从为什么不适合做分布式锁 在 Redis 主从架构 + 哨兵(Sentinel) 的场景中,当发生主从故障转移时,可能出现 分布式锁失效或多个客户端同时持有同一把锁的情况。 | 原因 | 说明 | |------|------| | **1. Redis 主从复制是异步的** | 主节点写入后不等待从节点确认,failover 时新主可能丢失部分数据(包括锁状态) | | **2. 故障转移是自动的** | Sentinel 无法保证旧主上的所有写操作都已同步到新主 | | **3. 客户端无感知切换** | 客户端可能还在使用旧连接(或未及时收到主节点变更通知) | | **4. SETNX 锁不具备强一致性** | 没有考虑故障转移、网络分区等分布式系统异常 | #### Redis Cluster为什么比主从更适合做锁 > Redis Cluster 是“快锁”,ZooKeeper 是“稳锁”——选哪个取决于业务是“要速度”还是“要安全”。 Redis Cluster的锁比主从架构更健壮,降低了因单点故障导致锁失效的概率。 虽然 Redis Cluster 本身也是最终一致性,但可以通过限制锁只在一个分片上使用,并结合 WAIT 命令强制同步复制(牺牲性能)来提高安全性, 但 WAIT 只是尽力而为,不能完全保证(如从节点宕机)。 ```java // 推荐方案:使用 Redisson + WAIT 命令(简单且有效) RLock lock = redisson.getLock("myLock"); // 加锁,同时要求至少 1 个从节点同步成功 lock.lock(); // 或者带超时 lock.lock(10, TimeUnit.SECONDS); // Redisson 在底层会自动执行: // SET myLock NX PX 30000 // WAIT 1 1000 # 等待至少 1 个从节点确认同步,最多等 1 秒 ``` ```java // 配置示例(在 Config 中): Config config = new Config(); config.useSentinelServers() .addSentinelAddress("redis://127.0.0.1:26389") .setMasterName("mymaster") // 设置 WAIT 的副本数和超时 .setSlaveConnectionMinimumIdleSize(1) .setReadMode(ReadMode.SLAVE); // Redisson 内部会自动使用 WAIT ``` #### Redis集群方案什么情况下会导致整个集群不可用 ``` 1. 超过半数的主节点宕机或失联。 Redis 集群需要多数主节点在线才能正常工作,否则无法选举、无法写入,整个集群就挂了。 2. 只要有一个slot哈希槽没分配,集群状态就会变成 fail,所有请求都会被拒绝。 ``` #### Redis集群会有写操作丢失吗? 为什么? 写操作是可能丢失的,Redis 集群优先保证 可用性(AP) 而不是强一致性 1. 异步复制: * 发生过程:​​ 1. 客户端向主节点(Master)M1 写入一条数据 2. M1 成功处理并回复客户端 "OK" 3. ​但在 M1 将这条数据异步同步给它的从节点 S1 之前,M1 突然宕机了​ 4. 哨兵或集群触发故障转移,将 S1 提升为新的主节点(M1-new) 5. ​结果​:客户端已经收到成功的写入确认,但这条数据因为未同步到 S1,随着原 M1 的宕机而永久丢失。 * ​核心原因​:Redis 默认采用异步复制​(Asynchronous Replication)以追求高性能。主节点在回复客户端后,才会在后台将数据同步给从节点。 2. 网络分区(脑裂): * 发生过程 1. 主节点 M1 和它的从节点 S1 之间发生网络分区,导致它们无法通信。 2. 哨兵/集群检测到 M1 失联,误认为其已宕机,于是将 S1 提升为新的主节点(M1-new)。 3. 此时集群中存在两个"主节点"(原 M1 和 新 M1-new),各自接收不同客户端的写请求。 4. 当网络分区恢复后,原 M1 会被降级为 S1 的从节点,并丢弃自己分区期间的数据,从新的 M1-new 全量同步数据。 5. ​结果​:所有在分区期间写入原 M1 的数据都会被丢弃,造成数据丢失。 3. 如何规避数据丢失(增强数据安全) ``` 如果需要更强的数据安全性,可以配合以下配置降低风险; 但即使这样,也不能完全避免丢失,只是降低了概率。 min-replicas-to-write 1:限制主节点至少有 1 个从节点才允许写。 min-replicas-max-lag 10:从节点延迟不能超过 10 秒。 ``` #### Redis常见性能问题和解决方案有哪些 1. 高延迟(响应变慢)​​ - ​可能原因​: - ​慢查询​:执行时间过长的命令(如 KEYS *、大范围 SCAN、复杂 Lua 脚本) - ​大 Key(BigKey)​​:单个 Key 存储的数据过大(如 1MB 以上的 String、百万元素的 Hash) - ​内存不足​:频繁触发 OOM 或 SWAP 导致性能下降 - ​持久化阻塞​:RDB 快照或 AOF 重写占用过多资源 - ​网络问题​:带宽不足、连接数过多、跨机房访问 - ​CPU 瓶颈​:单线程模型下 CPU 成为瓶颈 - 解决方案​: - ​优化慢查询​: ``` 使用 SLOWLOG 分析慢查询:SLOWLOG GET 10 避免 KEYS *,改用 SCAN 分批扫描 优化复杂 Lua 脚本,减少执行时间 ``` - ​处理大 Key​: ``` 拆分大 Key(如 HGETALL 改为 HSCAN) 使用 redis-cli --bigkeys 找出大 Key 并优化 ``` - ​内存优化​: ``` 设置合理的 maxmemory 和淘汰策略(allkeys-lru / volatile-lru) ``` - ​持久化优化​: ``` 避免 AOF 的 always 模式(改用 everysec) 在低峰期执行 BGSAVE 或 BGREWRITEAOF ``` - ​网络优化​: ``` 使用 pipeline 或批量命令减少网络往返 避免跨机房访问,优化网络带宽 ``` - ​CPU 优化​: ``` 监控 CPU 使用率,避免单机过载 考虑分片(Cluster / Codis)分散 CPU 压力 ``` 2. 内存占用过高(OOM)​​ - ​可能原因​: - 数据量过大,超出 maxmemory 限制 - 内存碎片率高(频繁增删 Key) - 未设置合理的淘汰策略 - ​解决方案​: - ​合理设置淘汰策略​ `配置 maxmemory-policy 为 volatile-lru` - ​减少内存碎片​: `开启 activedefrag(Redis 4.0+): config set activedefrag yes` - 定期重启(低峰期) - ​优化数据结构​: `使用 Hash 代替多个 String(节省 Key 的元数据) ; 使用 ziplist 压缩小数据` - ​监控内存​: ` INFO memory 查看内存使用情况; MEMORY USAGE key 查看单个 Key 占用` 3. 连接数过多 - 可能原因​: - 客户端未正确关闭连接(连接泄漏) - 短连接频繁创建/销毁(如 HTTP 短连接访问 Redis) - maxclients 设置过低 - 解决方案​: - ​优化客户端连接管理 `使用连接池(如 Jedis、Lettuce); 避免短连接,改用长连接` - 调整 maxclients `config set maxclients 10000 # 根据机器配置调整` - ​监控连接数 ``` INFO clients 查看活跃连接 CLIENT LIST 查看所有连接 ``` 4. 主从复制延迟 - 可能原因​: - 主节点写入量过大,从节点同步跟不上 - 网络带宽不足 - 从节点执行 BGSAVE 或 AOF 重写导致阻塞 - ​解决方案​: - ​优化主从同步 ``` 使用 PSYNC2(Redis 4.0+)减少全量同步 增加从节点带宽 ``` - ​减少主节点压力​: ``` 使用读写分离(从节点处理读请求) 避免大 Key 同步阻塞 ``` - ​监控复制延迟​: ``` INFO replication # 查看 slave_repl_offset 和 master_repl_offset ``` 6. AOF 重写 / RDB 快照导致阻塞 - 可能原因​: - fork() 操作耗时(数据量越大,阻塞越久) - 磁盘 IO 性能差 - ​解决方案​: - ​优化持久化策略​ ``` 低峰期执行 BGSAVE / BGREWRITEAOF 使用 AOF + RDB 混合模式(Redis 4.0+) ``` - ​提升磁盘性能​ ``` 使用 SSD 避免和其他高 IO 服务混部 ``` - ​监控 fork 耗时​: `INFO stats # 查看 latest_fork_usec(上次 fork 耗时)` #### Redis的订阅模式跟其他对比 | 模式 | 特点 | 代表技术 | 适用场景 | | :--- | :--- | :--- | :--- | | **即时广播 (Fire-and-Forget)** | 低延迟,消息不持久,允许丢失 | **Redis Pub/Sub** | 实时通知、状态同步、配置推送 | | **可靠队列 (Reliable Queue)** | 高可靠,持久化,解耦,支持堆积 | **RocketMQ, RabbitMQ, Kafka** | 订单处理、支付通知、异步任务 | | **流式处理 (Streaming)** | 高吞吐,持久化,可重放,流计算 | **Kafka, Pulsar** | 日志分析、用户行为、事件溯源 | | **长连接推送 (Long-Polling/WS)** | 极低延迟,双向通信,服务端主动推 | **WebSockets, gRPC Streams** | 实时聊天、在线游戏、协同编辑 | | **轮询 (Polling)** | 实现简单,兼容性好,延迟高 | **HTTP Polling** | 简单场景,或作为长连接的降级方案 | ```python 选型决策树 选择哪种订阅模式,可以按以下思路决策: 需要“即时”通知,且能容忍丢失? → Redis Pub/Sub 需要高可靠、不丢消息、异步解耦? - 吞吐量要求极高,流式数据? → Kafka / Pulsar - 需要复杂路由? → RabbitMQ - 平衡可靠、性能、功能? → RocketMQ 需要服务端主动、极低延迟地推送给前端? → WebSockets / gRPC Streams 需要处理海量日志或事件流? → Kafka / Pulsar 系统简单,已有 Redis,且需求不复杂? → Redis Pub/Sub 核心原则:没有“最好”的技术,只有“最合适”的方案。通常,一个大型系统会组合使用多种模式,例如: 用 Kafka 收集用户行为日志。 用 RocketMQ 处理订单和支付。 用 Redis Pub/Sub 推送配置更新和实时状态。 用 WebSockets 实现客服聊天功能。 ``` ## 常用框架 ### Spring #### Spring中IOC的理解、原理、实现 - 理解 ``` 控制反转:把整个对象交给spring来帮我们进行管理。 容器:存储对象,使用map结构来存储,在spring中一般存在三级缓存,singletonObjeacts存放完整的bean对象,整个bean的生命周期,从创建到使用到销毁的过程全部都是由容器来管理的(bean的生命周期) ``` - 原理 ``` 1.先通过createBeanFactory 创建一个Bean工厂(DefaultListableBeanFactory) 2.开始循环创建对象,因为容器中的bean默认都是单例的,所以优先通过getBean,doGetBean从容器中查找 3.找不到的话通过createBean,doCreateBean方法,以反射的方式创建对象,一般情况下使用的是无参的构造器(getDeclaredConstructor(), newinstance) 4.进行对象的属性填充populateBean 5.进行其他的初始化操作(initializingBean) ``` #### Spring中Bean容器的生命周期是什么样的 1. 实例化:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。 2. 属性设置:在Bean实例化后,Spring容器会通过populateBean进行属性填充。 ```java protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) { // 1. 处理@Autowired/@Value等注解的注入 // 2. 处理XML/配置文件中定义的显式属性值 // 3. 按需选择Setter注入或字段直接注入 } ``` 循环依赖的问题(三级缓存) 3. 初始化 ```markdown - 调用aware(是一系列以 Aware 结尾的接口,它们是 Spring 提供的“回调”机制,让 Bean 可以获取到容器内部的一些基础设施对象)接口相关的方法:invokeAwareMethod - 调用BeanPostProcessor中的前置处理方法 - 判断是否实现了InitializingBean接口, 是否配置initmethod方法 - 调用BeanPostProcessor的后置处理方法:spring的aop就是在此处实现的,AbstractAutoProxyCreator ``` 4. 使用:通过getBean的方式来进行对象的获取。 5. 销毁:①判断是否实现了DispoableBean接口 ②调用destroyMethod方法。 ```java # 总结流程图 createBeanInstance (反射创建) ↓ populateBean (属性填充) ↓ initializeBean (初始化) ├──> BeanPostProcessor.beforeInitialization ├──> @PostConstruct ├──> InitializingBean.afterPropertiesSet ├──> init-method └──> BeanPostProcessor.afterInitialization (AOP代理常在此创建) ↓ Bean 准备就绪,放入单例池 ``` #### Spring是如何解决Bean的循环依赖 Spring 使用三个 Map(通常称为三级缓存)来管理单例 Bean,它们是解决循环依赖的关键: | 缓存级别 | 名称 | 存储内容 | 目的 | |----------|----------------------|-------------------------------------------------|----------------------------------------------------------------------| | 一级缓存 | singletonObjects | 完全初始化好的单例 Bean(最终成品) | 存放可以被直接使用的 Bean。 | | 二级缓存 | earlySingletonObjects| 提前暴露的原始 Bean 实例(半成品) | 存放为解决循环依赖而提前暴露的、尚未初始化的 Bean 实例。 | | 三级缓存 | singletonFactories | ObjectFactory(能创建早期 Bean 实例的工厂) | 存放一个工厂,这个工厂能在需要时生成早期实例,并有机会进行 AOP 代理。 | #### 二级缓存能不能解决循环依赖 > 二级缓存可以解决普通循环依赖(非代理场景),但二级缓存不能完美解决AOP动态代理的循环依赖,主要是因为代理对象的创建时机和单例一致性问题。 1. 代理对象的创建时机问题​ * 在Spring AOP中,代理对象通常是在Bean初始化完成之后(postProcessAfterInitialization阶段)创建的。但在循环依赖的场景下,​一个Bean可能需要在初始化完成之前就被其他Bean引用​(比如A依赖B,B又依赖A)。这时候就会出现矛盾: * ​如果使用二级缓存​: * 当A依赖B时,B可能还没有完成初始化,但B又需要引用A。 * 如果直接从二级缓存获取A的早期引用,此时A可能还没有被AOP代理(因为代理通常是在初始化之后创建的)。 * 这样会导致B注入的是A的原始对象,而不是代理对象,**​破坏了AOP的预期行为**。 * ​三级缓存的解决方案​: * 三级缓存存储的是ObjectFactory,它可以动态决定是否返回原始对象或代理对象。 * 当B需要A时,会调用ObjectFactory.getObject(),此时Spring会检查A是否需要代理: * 如果需要代理,就提前创建代理对象并放入二级缓存。 * 如果不需要代理,就直接返回原始对象。 * 这样能确保B注入的是正确的代理对象(如果A需要AOP的话)。 2. 单例一致性问题​ * Spring的单例Bean必须保证全局唯一性,即所有依赖注入的应该是同一个实例。如果只用二级缓存: * ​假设A需要AOP代理​: * A在初始化之前被B引用,此时如果直接从二级缓存获取A的原始对象,B会持有A的原始引用。 * 但后续A初始化完成后,Spring会生成A的代理对象(A$$Enhancer),此时A的最终版本是代理对象。 * 这样会导致: * B持有的是A的原始对象(不是代理) * 其他Bean可能持有的是A的代理对象 * ​破坏了单例一致性​(同一个Bean有不同的版本) * ​三级缓存的解决方案​: * 通过ObjectFactory,Spring可以在第一次暴露Bean时就决定是否生成代理。 * 这样所有依赖方拿到的都是同一个代理对象(如果该Bean需要AOP的话),确保单例一致性。 3. 为什么二级缓存不能动态判断AOP?​​ * 二级缓存存储的是已经实例化但未初始化的Bean​(原始对象),它无法动态决定是否返回代理对象。而三级缓存的ObjectFactory可以在真正被依赖时​(调用getObject()时)才决定是否创建代理,这样更加灵活。 **总结** | 缓存级别 | 存储内容 | 能否处理AOP循环依赖 | 原因 | |------------|------------------------------|---------------------|----------------------------------------------------------------------| | 二级缓存 | 原始对象(半成品Bean) | ❌ 不能 | 无法动态决定是否返回代理对象,可能导致注入原始对象而非代理 | | 三级缓存 | ObjectFactory(可动态生成代理) | ✅ 能 | 在依赖注入时动态判断是否需要代理,保证单例一致性 | #### 为什么需要三级缓存 1. ​解决代理时机问题​ * AOP代理需要在Bean被引用时动态决定是否创建(而不是在初始化完成后),三级缓存的ObjectFactory提供了回调入口。 2. ​保证单例一致性​ * 确保所有依赖注入的Bean是同一个对象(无论是原始对象还是代理对象)。 3. ​避免重复代理​ * 防止同一个Bean被多次生成不同的代理实例。 #### Spring 能不能解决多例 Bean 的循环依赖 不能。 多例不会使用缓存进行存储(多例Bean每次使用都需要重新创建)。 不缓存早期对象就无法解决循环。 #### Spring有没有解决构造函数参数Bean的循环依赖 > Spring ​无法直接解决构造函数参数引起的循环依赖,但可以通过 @Lazy注解实现间接解决。 1. 为什么构造函数循环依赖无法直接解决?​​ * 当两个Bean的构造函数互相依赖时(例如 A(B b)和 B(A a)),Spring会直接抛出 BeanCurrentlyInCreationException,因为: * ​实例化顺序矛盾​: * 建A时需要先有B,但创建B时又需要先有A,形成死锁。 * ​三级缓存机制失效​: * 级缓存要求至少先完成实例化​(调用构造函数),但构造函数参数依赖导致连实例化都无法完成。 2. @Lazy的解决方案 * 通过 @Lazy注解让Spring延迟初始化依赖的Bean,打破死锁: ```java @Component public class A { private final B b; public A(@Lazy B b) { // 关键点:延迟加载B this.b = b; } } @Component public class B { private final A a; public B(A a) { this.a = a; } } ``` 3. @Lazy的工作原理​ * ​代理介入​: * Spring会为被 @Lazy标记的依赖生成一个代理对象(如 B$$EnhancerBySpringCGLIB),而非立即初始化真实Bean。 * ​延迟触发​: * 只有当代码首次调用代理对象的方法时,才会触发真实Bean的创建: #### Bean Factory与FactoryBean有什么区别 ``` 相同点:都是用来创建bean对象的 不同点: 使用BeanFactory创建对象的时候,必须要遵循严格的生命周期流程; 如果想要简单的自定义某个对象的创建,同时创建完成的对象想交给spring来管理,那么就需要实现FactoryBean接口 isSingleton:是否是单例对象 getObjectType:获取返回对象的类型 getObject:自定义创建对象的过程(new,反射,动态代理) ``` #### Spring的AOP的底层实现原理 bean的创建过程中有一个步骤可以对bean进行扩展实现,aop本身就是一个扩展功能,所以在 BeanPostProcessor的后置处理方法中来进行实现。 - 核心机制:运行时动态代理。 - 选择策略: * 有接口 -> JDK 动态代理 (优先)。 * 无接口 -> CGLIB 动态代理。 - 实现方式: * JDK:实现接口 + InvocationHandler。 * CGLIB:继承目标类 + 方法重写。 - 关键时机:BeanPostProcessor.postProcessAfterInitialization 阶段创建代理。 - 最终结果:容器中管理的 Bean 实例通常是代理对象,调用其方法时会触发 AOP 的通知逻辑。 ### SpringBoot #### SpringBoot为什么默认使用CGLIB 1. 无需接口 - JDK 动态代理要求目标类必须实现至少一个接口。如果没有接口,就无法使用 JDK 代理。 2. AOP支持(尤其是注解获取) - 在 AOP 切面中,经常需要通过反射获取目标方法上的注解(比如 @Transactional, @Cacheable)。JDK 动态代理在某些情况下会导致注解丢失或无法正确获取。 3. 可以代理本类方法 - 因为 AopContext.currentProxy() 返回的是当前代理对象(CGLIB 子类实例)。你可以把它强转为**当前被代理类类型**,然后调用方法,从而走代理逻辑。 - JDK 代理虽然也能暴露,但它是 Proxy 类型,不能强转为 **当前被代理类类型**(除非它实现了接口)。 4. 方法调用性能,CGLIB 更快 | 代理方式 | 实现机制 | 方法调用路径 | 性能表现 | | :--- | :--- | :--- | :--- | | JDK 动态代理 | `InvocationHandler.invoke()` | 接口方法 → 反射调用目标方法 | 中等(有反射开销) | | CGLIB | 继承 + 方法重写 | 子类方法 → 直接调用 `super.method()` | 更快(无反射) | #### SpringBoot如何自定义Starter 1. 创建项目结构: 创建一个Maven项目,确保项目结构符合标准的约定。通常,项目结构包括src/main/java用于存放Java代码和src/main/resources用于存放资源文件。 2. 编写自动配置类: 创建一个自动配置类,该类负责配置自定义Starter的功能。在自动配置类上使用@Configuration注解,并通过其他注解如`@ConditionalOnClass、@ConditionalOnProperty`等来定义条件,以确保只有在满足特定条件时才会应用配置。 3. 提供属性配置:如果您的Starter需要配置属性,可以在src/main/resources/application.properties或src/main/resources/application.yml中定义属性。这些属性可以在自动配置类中使用@Value注解注入。 4. 创建META-INF/spring.factories文件: 在项目的资源目录中创建META-INF/spring.factories文件。在这个文件中,注册您的自动配置类,以便Spring Boot能够自动识别和加载它。 5. 定义Starter依赖: 在自定义Starter的pom.xml文件中,定义Spring Boot的核心依赖以及您的Starter所依赖的其他库。 6. 测试和文档: 编写单元测试和集成测试,以确保自定义Starter的功能和配置正确。同时,提供详细的文档和示例,以便用户能够正确配置和使用您的Starter。 7. 发布到仓库: 将自定义Starter打包,并发布到Maven中央仓库或私有仓库,以便其他项目可以引入和使用。 #### SpringBoot可以同时处理多少请求 ```yml 在SpringBoot中处理请求数量相关的参数有四个: ● server.tomcat.threads.min-spare:最少的工作线程数,默认大小是10。该参数相当于长期工,如果并发请求的数量达不到10,就会依次使用这几个线程去处理请求。 ● server.tomcat.threads.max:最多的工作线程数,默认大小是200。该参数相当于临时工,如果并发请求的数量在10到200之间,就会使用这些临时工线程进行处理。 ● server.tomcat.max-connections:最大连接数,默认大小是8192。表示Tomcat可以处理的最大请求数量,超过8192的请求就会被放入到等待队列。 ● server.tomcat.accept-count:等待队列的长度,默认大小是100。 SpringBoot同时所能处理的最大请求数量是max-connections+accept-count ``` #### SpringBoot官方建议使用构造方法注入,如果出现循环依赖,那么如果使用三级缓存的话,能否解决此处的循环依赖 结论:**不能解决** | 问题 | 回答 | |------|------| | **Spring Boot 是否推荐构造器注入?** | ✅ 是,从 2.0+ 开始强烈推荐 | | **构造器注入能否产生循环依赖?** | ✅ 能,且非常常见 | | **三级缓存能否解决构造器循环依赖?** | ❌ 不能!三级缓存只能解决 setter/字段注入的循环依赖 | | **为什么不能?** | 构造器依赖必须在实例化时提供,而此时对象还未创建,无法暴露“早期引用” | | **如何解决?** | 重构代码、使用 `@Lazy`、或降级为字段注入(不推荐) | #### Springboot推荐属性注入原因 | 优势 | 说明 | |:--- |:---| | 不可变性 | 依赖通过构造函数传入,可用 final 修饰,保证不可变| | 非空保证 | 构造时就必须提供依赖,避免 NullPointerException| | 易于测试 | 可以在单元测试中直接 new 对象并传入 mock 依赖| | 避免 @Autowired 的滥用 | 减少对 Spring 容器的耦合| #### 为什么SpringBoot的jar可以直接运行 | 特性 | 说明 | | ----------------------------------- | ------------------------- | | **Fat JAR** | 包含项目代码和所有依赖,无需额外配置类路径。 | | **启动器(JarLauncher)** | 负责初始化类加载器并调用应用主类。 | | **自定义类加载器(LaunchedURLClassLoader)** | 支持加载嵌套的 jar 包,打破双亲委派模型。 | | **内嵌服务器** | 默认内嵌 Tomcat/Jetty,无需外部部署。 | ``` Spring Boot 使用 spring-boot-maven-plugin 插件,将项目代码和所有依赖打包成一个 可执行的 Fat JAR spring-boot-app.jar ├── BOOT-INF │ ├── classes/ # 项目编译后的类文件和配置文件 │ └── lib/ # 项目依赖的所有第三方 jar 包 ├── META-INF │ └── MANIFEST.MF # 定义 JAR 启动入口 └── org/springframework/boot/loader/ ├── JarLauncher.class # 启动器类 ├── LaunchedURLClassLoader.class # 自定义类加载器 └── ... ``` #### @Import注解的三种用法 1. 导入普通的配置类 ```java @Configuration public class DatabaseConfig { @Bean public DataSource dataSource() { return new HikariDataSource(); } } @Configuration public class SecurityConfig { @Bean public UserDetailsService userDetailsService() { return new InMemoryUserDetailsManager(); } } // 主配置类 @Configuration @Import({DatabaseConfig.class, SecurityConfig.class}) public class MainConfig { } ``` 2. 导入 @Component 或普通类(自动注册为 Bean) ```java // 普通类,没有任何 Spring 注解 public class EmailService { public void send(String to, String content) { System.out.println("Email sent to " + to); } } @Configuration @Import(EmailService.class) // 直接导入,自动注册为 Bean public class AppConfig { } ``` 3. 导入 ImportSelector 或 ImportBeanDefinitionRegistrar ```java public class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { // 可以根据条件动态选择导入哪些类 return new String[]{ "com.example.config.DatabaseConfig", "com.example.config.CacheConfig" }; } } @Configuration @Import(MyImportSelector.class) public class AppConfig { } ``` ```java public class FeignClientRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // 手动注册一个 Bean BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(EmailService.class); builder.setScope("singleton"); registry.registerBeanDefinition("myEmailService", builder.getBeanDefinition()); } } @Configuration @Import(FeignClientRegistrar.class) public class AppConfig { } ``` ## 微服务 ### Spring Cloud Alibaba #### 微服务组件描述 1. ​注册与配置中心​: ​Nacos​ 2. ​服务调用​: ​OpenFeign​ (HTTP) 或 ​Dubbo​ (RPC) 3. ​服务容错​: ​Sentinel​ 4. ​分布式事务​: ​Seata​ 5. ​消息队列​: ​RocketMQ​ 6. ​API 网关​: ​Spring Cloud Gateway​ (集成 Sentinel 和 Nacos) 7. ​认证授权​: ​Spring Security OAuth2​ 8. ​监控​: ​Spring Boot Admin​ + ​Prometheus​ + ​Grafana | 功能 | Spring Cloud Alibaba 组件 | Spring Cloud Netflix 组件 | 说明 | | :------------------- | :----------------------------- | :-------------------------- | :----------------------------------------------------------- | | 服务注册/发现 | **Nacos** | Eureka | Nacos 功能更强大,还集成了配置中心 | | 配置中心 | **Nacos** | Spring Cloud Config | Nacos 配置动态推送更高效,集成更简单 | | 服务调用 | **Dubbo** / OpenFeign | OpenFeign / Ribbon | Dubbo 提供高性能 RPC 选项 | | 熔断降级 | **Sentinel** | Hystrix | Sentinel 功能更丰富,控制台更友好,规则可配置 | | 分布式事务 | **Seata** | 无 | Spring Cloud Netflix 缺乏官方分布式事务方案 | | 消息驱动 | **RocketMQ** | 无 | 提供企业级消息队列支持 | | 客户端负载均衡 | Spring Cloud LoadBalancer | Ribbon | Spring Cloud Alibaba 默认使用 Spring Cloud 官方的 LoadBalancer | | API 网关 | (集成 Spring Cloud Gateway) | Zuul | 推荐使用 Spring Cloud 官方的 Gateway | ### OpenFeign #### OpenFeign与Dubbo的区别 | 特性 | Dubbo | OpenFeign | | :--- | :--- | :--- | | **通信协议** | RPC (Dubbo 协议,基于 TCP) | HTTP (基于 RESTful) | | **性能** | ⚡️ 极高(二进制、长连接、Netty) | 🐢 较低(文本、短连接、HTTP 开销) | | **调用方式** | 接口调用(像本地方法) | 声明式 REST 调用(注解描述 HTTP 请求) | | **服务治理** | 🌟 功能强大(负载均衡、路由、降级、参数校验等) | 🔧 较弱(依赖 Ribbon/Hystrix,已过时) | | **序列化** | 支持多种(Hessian2, JSON, Protobuf 等) | 主要是 JSON | | **依赖** | 通常需要注册中心(Nacos/ZooKeeper) | 可与 Eureka/Nacos 集成,也可独立使用 | | **适用场景** | 高并发、高性能、复杂治理需求 | 快速开发、简单调用、跨语言(HTTP 通用) | #### OpenFeign底层原理 > OpenFeign 通过声明式接口 + 动态代理 + 注解解析 + 编解码 + 可插拔客户端 + 负载均衡集成,极大地简化了微服务间的 HTTP 调用,让开发者像调用本地方法一样完成远程服务调用。 1. ​动态代理(核心机制)​ * Spring在启动时,会扫描所有被@FeignClient注解标记的接口。 * 然后为每一个接口动态地生成一个代理对象​(JDK动态代理),并注册到Spring的IOC容器中。 2. ​请求构造(注解解析)​ * 代理对象在接收到方法调用时,会解析方法上的注解(如@GetMapping, @PathVariable, @RequestParam)。 * OpenFeign会将方法名、参数、注解信息转换成一个HTTP请求模板(RequestTemplate)​。这个模板里包含了最终HTTP请求需要的所有元素:URL、请求方法、请求头、请求参数、请求体等。 3. ​客户端执行与负载均衡​ * 构造好请求模板后,需要由底层的HTTP客户端来真正地执行这个请求。 * OpenFeign使用了可插拔的HTTP客户端设计,默认使用JDK的HttpURLConnection,但更常用的是集成Apache HttpClient或OkHttp,因为它们有连接池等性能优化特性。 * 如果整合了Ribbon或Spring Cloud LoadBalancer,​在这一步还会进行负载均衡。客户端会向服务注册中心(如Nacos)查询服务名对应的所有实例列表,然后根据负载均衡策略(如轮询)选择一个具体的服务实例地址,再将请求发送过去。 4. ​编码与解码(Encoder & Decoder)​​ * 在发送请求前,​编码器(Encoder)​​ 会负责将@RequestBody等参数对象序列化成JSON等格式,放到请求体中。 * 在接收到响应后,​解码器(Decoder)​​ 会负责将HTTP响应体(JSON/XML等)​反序列化成我们在接口方法中定义的返回值类型(如User对象)。 * 默认使用Spring的HttpMessageConverter,通常集成了Jackson来做JSON处理。 5. ​容错与集成​ * OpenFeign可以非常方便地与Sentinel或Hystrix集成,实现服务的熔断降级。只需简单配置,就可以在请求失败时(超时、异常等)执行预设的降级方法,防止雪崩效应。 ### Nacos #### 启动bat ```bat @echo off :: 启动 Nacos 单机模式 echo 正在启动 Nacos 单机模式... cd /d C:\Users\Administrator\Desktop\nacos\bin call startup.cmd -m standalone echo Nacos 启动命令已执行。 pause ``` #### Nacos数据模型 > Nacos 的数据模型主要围绕两个核心概念:​服务 (Service)​​ 和 ​配置 (Configuration)​。它们分别对应 Nacos 的两大功能:​服务发现​ 和 ​配置管理。 > > 其数据模型的层次结构可以概括为以下关系: > > Namespace-> Group-> Service/Data ID-> Cluster-> Instance * 服务发现 (Naming Service) 数据模型 1. 命名空间 (Namespace) * 用于进行环境隔离和租户隔离。例如,你可以将开发、测试、生产环境的服务完全隔离开,互不干扰 2. 分组 (Group) * 用于对服务进行分组。在同一个命名空间内,可以通过 Group 来进一步划分不同的服务组。例如,你可以将有依赖关系的服务分到同一个组(app-group),或者将同一个项目的数据源服务分到 db-group * Group通常和 Service Name一起作为服务的标识,即 Group + ServiceName才是服务的唯一标识(在同一个 Namespace 下) 3. 服务 (Service) * 一个具体的微服务或应用。例如 user-service, order-service。 4. 集群 (Cluster) * 对服务实例进行逻辑分组。通常是基于物理结构来划分,比如同一个机房的实例分配到一个集群(cluster-shanghai,cluster-beijing),或者根据业务逻辑划分(cluster-a, cluster-b用于灰度发布)。 * 服务消费者可以优先调用同集群的服务实例,实现“就近访问”,降低网络延迟 5. 实例 (Instance) * 一个服务的具体提供者,是一个可用的网络端点(IP + Port)。一个服务包含多个实例(集群部署) * 配置管理 (Configuration) 数据模型 1. 命名空间 (Namespace) 2. 分组 (Group) 3. 数据 ID (Data ID) * 配置集的唯一标识,通常对应一个配置文件的名称。 * ​命名规则​:通常是有意义的字符串,例如 ${prefix}-${spring.profile.active}.${file-extension}。 * 在 Spring Cloud 中,一个典型的 Data ID 是 user-service-dev.yaml。 * user-service:应用名。 * dev: profile(环境)。 * yaml:配置文件格式。 * 一个 Namespace下可以有多个 Group,一个 Group下可以有多个 Data ID,每个 Data ID对应一份 配置内容。 ```text ### 完整的内存数据结构图示 +---------------------+ (Namespace ID: "public") | serviceMap.get(...) | +----------+----------+ | v +-------------------------------------------------+ | groupedServiceMap (Map) | | | | Key: "DEFAULT_GROUP@@user-service" +--------+ | | Value: --------------------------> | Service| | | +---+----+ | | | | | Key: "ORDER_GROUP@@order-service" +---v----+ | | Value: --------------------------> |Service | | | +--------+ | +-------------------------------------------------+ | | Service.clusterMap (Map) v +------------------+ | clusterMap.get(...) | +---------+----------+ | v +-----------------------+ | instanceMap (Map) | | | | Key: "192.168.1.100#8080#..." +-----+ | | Value: -------------------> |Inst.| | | +-----+ | | | | Key: "192.168.1.101#8080#..." +-----+ | | Value: -------------------> |Inst.| | | +-----+ | +-----------------------+ ``` #### Nacos底层原理 > Nacos 的核心是服务发现和配置管理,其底层机制也围绕这两大功能展开 * 服务发现工作原理 1. 服务注册 * 当服务提供者(如 UserService)启动时,会向 Nacos Server 发送注册请求。 * ​客户端​:服务提供者内置的 Nacos Client 会通过 OpenAPI(通常是 RESTful API)向 Nacos Server 注册自身信息,包括: * ip, port * 所属的 namespaceId, serviceName, clusterName * metadata(元数据,如版本、权重) * ​服务端​: * Nacos Server 接收到注册请求后,会将这个服务实例 (Instance)​​ 信息存储在一个内存注册表里(一个巨大的双层 Map结构)。 * 同时,Server 会启动一个健康检查任务来定期检查这个实例的健康状态。 * 启动健康检查任务 (Active):这是关键!Nacos Server 会启动一个独立的、全局的定时任务(通常称为 HealthCheckTask 或类似名称)。 * 执行频率:这个任务会每隔很短的时间(例如几百毫秒到 1 秒)执行一次。 * 任务内容: * 扫描内存中所有的临时实例 (Ephemeral Instances)。 * 对于每个实例,计算 currentTime - lastBeat。 * 如果 currentTime - lastBeat > healthyThreshold (默认 15秒),则将该实例的 healthy 标记为 false。 * 如果 currentTime - lastBeat > ipDeleteTimeout (默认 30秒),则将该实例从注册表中彻底删除。 2. 服务健康检查 * 这是保证注册列表可用的核心机制。Nacos 支持两种模式: * 客户端心跳模式 (默认)​​:​AP 模式的体现。 * 服务实例会定期(如每5秒)向 Nacos Server 发送一次心跳​(一个简单的 ping 请求)。 * Server 如果在一段时间内​(如15秒)没有收到某个实例的心跳,则会将该实例标记为不健康。 * 如果超过更长时间​(如30秒)仍未收到心跳,则 Server 会直接删除这个实例的注册信息。 * 优点:性能开销小,吞吐量高。 * ​服务端主动探活模式​:​CP 模式的体现。 * 由 Nacos Server 主动发送请求(如 HTTP、MySQL 连接检查)到服务实例的健康检查端点。 * 如果连续失败多次,则标记该实例为不健康。 * 优点:准确性更高,但 Server 端压力大。通常用于临时实例较少的场景。 3. 服务发现与订阅 * 当服务消费者(如 OrderService)需要调用 UserService 时: * ​拉取列表​:消费者首先会从 Nacos Server ​查询​(pull)UserService 的可用实例列表,并缓存到本地。 * ​订阅监听​:为了防止调用到已下线的实例,消费者还会订阅​(subscribe)UserService 的变更。 * ​服务端推送​:当 UserService 的实例列表发生变化(如实例上线、下线、权重变更),Nacos Server 会基于 ​UDP​ 协议向所有订阅的消费者推送一条变更消息。 * ​注意​:UDP 是不可靠协议,可能存在推送失败的情况。因此,Nacos Client 内置了一个失败重试和定时轮询​(每10秒一次)的补偿机制,双重保证客户端数据的最终一致性。 4. 集群数据同步 (Distro Protocol) * 对于服务发现(AP模式),Nacos 集群节点间的数据同步采用自研的 ​Distro 协议。它是一个 ​AP 优先的最终一致性协议,流程如下: * 某个 Nacos Server 节点接收到写请求(如注册实例)。 * 该节点先在本地持久化数据,并立即返回成功。 * 然后异步地将数据变更同步给集群内的其他节点。 * 在网络分区的情况下,节点间可能暂时数据不一致,但每个节点仍能独立提供读写服务,保证了高可用。网络恢复后,数据会最终一致。 * 配置变更推送 1. 客户端发起一个长轮询请求到 Server,询问:“我监听的配置有没有变化?” 2. Server 端接收到请求后,会挂起这个请求,而不是立即返回。 3. 有两种情况会释放这个请求: * ​超时​:挂起一段时间(如 30 秒)后,如果配置一直无变化,则 Server 返回“无变更”。客户端收到后,会立即发起一个新的长轮询请求,如此反复。 * ​配置变更​:在挂起期间,如果有用户修改了配置,Server 会立即响应挂起的请求,返回“配置已变更”和对应的 Data ID。 4. 客户端收到“配置变更”响应后,会主动从 Server ​拉取最新的配置内容,并更新本地缓存。同时,触发 Spring Cloud 的 ​Refresh机制,动态更新应用中的 @Value和 @ConfigurationProperties等注解的值。 * 集群数据同步 (Raft Protocol) * 对于配置管理(CP模式),Nacos 集群节点间的数据同步采用标准的 ​Raft 协议。它是一个强一致性协议,流程如下: 1. 所有的写请求(配置的增删改)必须由 ​Leader​ 节点处理。 2. Leader 会将写操作复制给超过半数的 ​Follower​ 节点。 3. 一旦大多数节点持久化成功,Leader 才提交这个操作,并返回客户端成功。 4. 这样保证了在任何时候,集群中所有节点看到的配置数据都是强一致的,但牺牲了一定的可用性(如果大多数节点不可用,则无法写入)。 **Nacos 核心机制对比** | 特性 | 服务发现 (Naming Service) | 配置管理 (Config Service) | |--------------------|------------------------------------------------------|-----------------------------------| | **CAP模式** | AP 优先 (默认),支持切换到 CP | CP 强一致 | | **健康检查** | 客户端心跳 (默认) + 服务端主动探测 | 不适用 | | **数据同步协议** | Distro (自研,AP,最终一致) | Raft (强一致) | | **数据存储** | 内存注册表 (快) + 异步持久化到磁盘 | 数据库 (MySQL,可靠) | | **变更推送机制** | UDP 推送 + 客户端定时拉取补偿 | 长轮询 (Long Polling) | | **数据一致性** | 最终一致性 | 强一致性 | #### Nacos支持AP和CP,为什麽默认AP * 为什么服务发现场景更需要 AP? 1. “发现”比“绝对一致”更重要 * CP 模式下:如果发生网络分区,导致 Nacos 集群无法选举出 Leader 或无法达成多数派共识,那么整个注册中心将拒绝写操作(服务注册/注销)和读操作(服务发现)。消费者将无法获取任何服务列表,导致服务调用完全中断。 * AP 模式下:即使发生网络分区,只要有一个 Nacos 节点存活,它就能继续提供服务发现功能。消费者仍然可以获取到一个(可能是稍旧的)服务列表,并尝试进行调用。 2. 服务实例的“临时性”与“健康检查” * Nacos 中的服务实例默认是临时实例 (Ephemeral Instance),它们通过心跳来维持自己的存活状态。 * 即使服务列表同步有短暂延迟(AP 的最终一致性),Nacos 的健康检查机制(心跳超时、TCP 检测、HTTP 检测)也能快速地将不健康的实例从可用列表中剔除。 * 这意味着,数据的“不一致”窗口期很短,并且有兜底机制。短暂的不一致带来的风险是可控的。 3. 性能与延迟 * AP 模式 (Distro 协议):数据写入本地节点后立即返回成功,再异步同步到其他节点。写入延迟极低,性能高。 * CP 模式 (Raft 协议):写操作必须经过 Leader,并复制到多数节点确认后才能返回成功。写入延迟较高,受网络和最慢节点影响。 * 服务注册/注销是相对低频操作,但高性能的写入对系统整体响应能力有积极影响。 4. 符合“最终一致性”模型 * Nacos 的 AP 模式提供的最终一致性,与微服务架构的整体哲学是契合的。我们追求的是系统整体的稳定和可用,而不是某个组件的绝对数据一致。 * CP 模式存在的意义 * 配置中心 1. 关键性:数据库连接串、开关、限流规则等配置一旦不一致,可能导致数据错误、服务异常、资损等严重后果。 2. 一致性要求高:所有实例必须在某个时间点后,看到完全相同的配置值。例如,一个“支付开关”必须全局一致地开启或关闭。 3. 写少读多:配置更新相对低频,可以接受稍高的写入延迟来换取强一致性。 | 维度 | AP 模式 (默认) | CP 模式 (可选) | | :--- | :--- | :--- | | **核心目标** | **高可用性 (A)** | **强一致性 (C)** | | **服务发现场景** | ✅ **优先保证“能发现服务”**,即使列表稍旧。网络分区时,部分节点仍可提供服务。 | ❌ 网络分区时,可能完全不可用,导致服务调用链路中断。 | | **数据一致性** | **最终一致性**:短暂不一致可接受,由健康检查兜底。 | **强一致性**:所有节点数据实时一致。 | | **性能** | ✅ **高**:写操作本地完成,异步同步。 | ⚠️ **较低**:写操作需多数节点确认。 | | **典型应用** | **服务注册与发现** (临时实例 `ephemeral=true`) | **配置中心** (持久化实例 `ephemeral=false`) | #### Distro协议原理 | 特点 | 描述 | 优点 | |----------------|--------------------------------------------|----------------------------------| | **去中心化** | 没有固定的主节点,所有节点对等。 | 架构简单,无单点故障。 | | **分片机制** | 每个节点只负责部分数据的写入。 | 写入压力分散,易于水平扩展。 | | **读写分离** | 写操作由责任节点处理,读操作任何节点都可处理。 | 读性能极高,延迟低。 | | **最终一致性** | 数据通过异步机制同步。 | 保证了高可用和分区容错性 (AP)。 | | **双重同步** | 增量同步 + 定时全量校验。 | 既保证了同步效率,又保证了数据的可靠性。 | ### Zookeeper #### Zookeeper分布式锁 具体来说,在Zookeeper中实现分布式锁,通常会执行以下步骤: 1. 选择一个锁路径:首先确定一个Zookeeper中的路径作为锁节点的位置,比如 /lock。这个路径是所有竞争锁的客户端都知道的一个固定位置。 1. 创建临时顺序节点:每个希望获取锁的客户端都会在这个路径下创建一个临时且顺序的节点(例如,使用 create() 方法并指定为 EPHEMERAL_SEQUENTIAL 类型)。这一步非常重要,因为: 临时性确保了如果某个客户端崩溃或断开连接,它所创建的节点会被自动删除,从而不会导致死锁。 顺序性保证了所有节点可以按照创建的顺序进行排序,这样就可以根据节点顺序来决定哪个客户端获得锁。 1. 判断是否获得锁:客户端通过比较自己创建的节点的序列号与同一父路径下的其他节点的序列号来判断自己是否获得了锁。最小序号的节点对应的客户端视为获得锁的客户端。 1. 监听机制:如果一个客户端发现自己不是最小的节点,则需要对它前面的那个节点设置一个监听器(Watcher),以便在前一个节点被删除时得到通知,然后再次检查是否可以获得锁。 #### Zookeeper是如何解决脑裂问题的 **Zookeeper的解决方案是通过过半机制来避免脑裂问题的发生。** | 集群节点数 | 过半数要求 | 最多允许宕机数 | 是否防脑裂 | |----------|---------|------------|--------| | 3 | 2 | 1 | ✅ | | 5 | 3 | 2 | ✅ | | 7 | 4 | 3 | ✅ | ``` Zookeeper 如何防止这种“双主”脑裂? 关键机制如下: 1. Leader 必须获得“过半”节点支持才能生效 当 A 被隔离时,它虽然还“认为”自己是 Leader,但它无法与过半节点通信(比如只剩自己 1 台,1 < 3),所以它不会处理任何写请求,也不会对外提供写服务。 它进入“孤立”状态,只能读本地数据(如果允许),但不能写。 Zookeeper 的 Leader 在无法连接到过半节点时,会自动退位(或停止服务)。 2. 新 Leader 的选举和数据同步 B、C、D、E 组成多数派(4 > 3),可以正常选举出新 Leader(B)。 B 成为新 Leader 后,所有写操作都必须经过 B,并且需要 过半节点确认 才能提交。 3. 旧 Leader(A)重新加入后会发生什么? A 恢复网络后,会收到来自 B 的心跳或消息。 A 发现 B 的 epoch(任期号)比自己新,就会立即意识到: “我已经不是 Leader 了” “B 是新的合法 Leader” A 会主动放弃 Leader 身份(如果它还坚持的话),并作为 Follower 重新加入集群,从 B 那里同步最新的数据。 ``` #### 为什么Zookeeper集群的数目一般为奇数个 1. 避免脑裂:奇数节点在网络分区时,能确保只有一个子集获得“多数”(Quorum),防止多个子集同时选主,保证数据一致性。 1. 选举高效:奇数节点天然避免投票平局,确保选举过程快速、确定,提升系统稳定性。 1. 成本最优:相比偶数节点,奇数节点在相同容错能力下,所需节点更少,资源利用率更高 ### Sentinel #### Sentinel是什么, 它是如何工作的 ```md Sentinel 的主要功能有: 1. 流量控制:Sentinel 可以控制每个服务或接口的并发请求数量,避免因为并发请求过多导致服务崩溃。 2. 熔断降级:当某个服务或接口不可用时,Sentinel 可以自动触发熔断机制,避免因单个服务或接口故障导致整个系统的瘫痪。 3. 系统负载保护:Sentinel 通过控制系统的整体负载,避免因系统过载导致服务性能下降甚至崩溃。 Sentinel 的工作原理主要分为三个步骤: 1. 数据采集:Sentinel 通过代理模式将流量数据采集到自身,并进行数据清洗和整合。 2. 策略计算:根据预先设定的规则和算法,Sentinel 计算并判断是否需要控制流量、熔断降级或保护系统负载。 3. 结果执行:根据计算结果,Sentinel 对流量进行控制、熔断降级或保护系统负载等操作,以保障服务的稳定性。 ``` #### Sentinel_与Hystrix的区别是什么 | 场景 | 推荐工具 | 原因 | |---------------------|------------|------------------------------------------------------------------------------------------| | 高并发限流 | Sentinel | 支持漏桶/令牌桶算法、热点参数限流,适合秒杀、网关限流等场景。 | | 长尾请求保护 | Sentinel | 基于响应时间的熔断策略可防止慢调用拖垮系统,而 Hystrix 无法自动降级慢请求。 | | 系统级自适应保护 | Sentinel | BBR 算法动态调整负载阈值,避免 CPU/内存过载,Hystrix 无此能力。 | | 遗留系统迁移 | Hystrix | 若已有 Hystrix 实现且无法快速迁移,可继续使用(不推荐长期依赖)。 | | 跨语言/非 Java 系统 | Hystrix | Hystrix 无语言绑定,但 Sentinel 主要面向 Java 生态(可通过适配层支持其他语言)。 | ### Seata > Seata是一款开源的分布式事务解决方案。提供了多种事务模式,包括AT、TCC、Saga和XA事务模式,以适应不同的业务场景。 **强一致选 XA,最终一致且低侵入选 AT,高性能选 TCC,长流程选 Saga** - 优先选择 AT 模式: 适用于大多数基于关系型数据库的场景,开发成本低,性能优于 XA。 例如:电商订单、库存管理、会员积分等。 - 选择 TCC 模式: 需要高性能且涉及非关系型系统(如 Redis、消息队列)。 例如:高并发支付场景、账户余额操作。 - 选择 Saga 模式: 业务流程长且复杂,需跨系统协作(如订单→支付→物流)。 例如:供应链管理、跨平台订单流程。 - 选择 XA 模式: 对一致性要求极高,且并发量较低。 例如:银行核心交易系统、证券清算。 | 模式 | 优点 | 缺点 | |------|------|------| | **XA** | 强一致,开发透明 | 性能差,依赖数据库 | | **AT** | 无侵入,性能较好 | 最终一致,依赖数据库 | | **TCC** | 高性能,灵活控制资源 | 代码侵入性强 | | **Saga** | 支持长流程,性能高 | 补偿逻辑复杂,无隔离 | #### 简述Seata的AT模式两阶段过程 1. 第一阶段(分支事务执行) 1. 开启本地事务。 2. 查询前镜像:SELECT ... WHERE id = 1。 3. 执行业务 SQL:UPDATE account SET balance = ... WHERE id = 1。 此时数据库会给 id=1 这行加 排他锁(X Lock)。 4. 查询后镜像。 5. 生成 undo_log 并插入到数据库(同事务)。 6. 向 TC(事务协调器)申请全局锁: ``` 请求:对 table=account, key=1 这行数据加全局锁。 TC 会维护一个全局锁表,检查是否有其他全局事务已经持有该行的全局锁。 如果能获取到全局锁 → 继续。 如果获取失败(被其他全局事务持有)→ 当前分支事务回滚本地事务,整个流程失败。 ``` 7. 提交本地事务: ``` 本地事务提交 → 数据库行锁被释放。 但 全局锁仍然被当前分支事务持有(直到第二阶段结束)。 向 TC 汇报“分支注册成功”。 第一阶段结束后状态 数据库行锁:已释放(其他非 Seata 事务可以修改!但 Seata 会尽量避免这种情况) 全局锁:仍被当前分支事务持有(由 TC 管理) ``` 2. 第二阶段 1. 情况一:全局提交 ``` TC 通知所有分支事务。 分支事务异步删除 undo_log。 分支事务向 TC 释放全局锁。 ``` 2. 情况二:全局回滚 ``` TC 通知所有分支事务回滚。 分支事务尝试获取该行的全局锁(防止回滚时被其他事务干扰)。 获取到后,用“前镜像”执行反向 SQL 恢复数据。 删除 undo_log。 释放全局锁。 ``` ### SkyWalking #### SkyWalking底层原理 ### ElasticSearch #### ES为什麽这麽快 * 倒排索引:ES 底层基于 Lucene,采用倒排索引来存储数据。 * 词典 → FST 是首选实现。 * 作用:存储所有被索引的“词项”(Term),比如 apple, search, fast。 * 为什么用 FST? * 空间效率极高:共享前缀/后缀,10亿词项可能只占几百MB内存。 * 查询速度快:O(词长),与词典大小无关。 * 支持高级查询:前缀匹配(app*)、模糊查询(~)、正则表达式等。 * 倒排列表 → 丰富信息 + 高压缩 * 倒排列表中存储了包含某个关键词的所有文档ID,可能非常长。Elasticsearch/Lucene 使用了多种压缩技术,例如: * ​差值编码(Delta Encoding)​​:存储文档ID的差值而非原始ID。 * ​Roaring Bitmap​:对文档ID集合进行高效压缩,同时支持超快的集合运算(如求交集,用于 AND 查询)。 * 列式存储:Doc Values 加速聚合与排序 * 虽然 ES 是文档存储,但它内部用了 Doc Values(列式存储)来做聚合、排序。 * 传统行存:查平均值要扫整行。 * Doc Values:按列存,聚合时只读需要的字段,IO更少,速度更快。 ```text Doc Values 就像是一个为分析操作(排序、聚合)优化的“列式数据库”。它将每个字段的值按文档 ID 顺序组织成一个紧凑的、经过压缩编码的数组。当你需要“列出所有文档的 price 值并求平均”时,ES 不需要去解析每个完整的 JSON 文档,而是直接高效地遍历 price 字段的 Doc Values 数组即可,这正是其速度之源。它是 ES 区别于传统数据库,成为强大分析引擎的关键技术之一。 ``` * 分布式架构与数据分片 * 数据分片:数据会被分成多个分片 (Shard),这些分片可以分布在集群中的不同节点上。 * 并行处理:当执行查询时,请求会发送到所有相关的分片上,各个分片并行地执行搜索任务。 * 结果聚合:协调节点收集所有分片返回的(通常是 Top N)结果,进行合并、排序,然后返回给客户端。 * 缓存机制多,重复查询极快 * ​文件系统缓存(Filesystem Cache)​​:Lucene 的倒排索引段常驻内存,查询时直接从内存读取,速度极快。 * ​查询结果缓存(Query Cache)​​:缓存某些过滤条件的结果。 * ​字段数据缓存(Fielddata Cache)​​:用于聚合和排序的字段数据会被加载到内存。 #### 倒排索引 > ​词项(Term) → 包含该词项的文档ID列表(以及可能的其他信息,如词频、位置等)​​ * Elasticsearch 中的倒排索引 * 当你为一个文本字段建立索引时,ES 会先对该字段的内容进行**​分词**​,然后为每个​词项(Term)​​ 建立倒排索引。 * 倒排索引不仅记录了哪些文档包含该词,还可能包括: * ​词频(TF, Term Frequency)​​:该词在该文档中出现了多少次 * ​位置(Position)​​:该词在文档中的出现位置(用于短语查询) * ​偏移量(Offset)​​:词的开始和结束位置(用于高亮显示) * 倒排索引的优化与扩展 * Elasticsearch 在 Lucene 的倒排索引基础上,做了很多优化,使其更高效、功能更强大 1. 词项字典(Term Dictionary)优化 * Lucene 使用 ​FST 数据结构来存储词项字典,既节省空间,又支持快速前缀查找、自动补全等 2. 倒排列表(Posting List)优化 * 存储包含某个词项的文档ID列表时,Lucene 使用了诸如 ​跳跃表(Skip List)、FOR(Frame of Reference)、RLE(Run Length Encoding)​​ 等压缩技术,来减少磁盘占用并提高查询速度。 3. 支持复杂查询 * 倒排索引不仅支持简单的词项查询,还支持: * ​布尔查询(AND/OR/NOT)​​ * ​短语查询(Phrase Query,利用位置信息)​​ * ​模糊查询(Fuzzy)​​ * ​通配符查询(Wildcard)​​ * ​高亮(Highlighting)​​ * ​聚合(部分基于倒排索引或 doc values)​ * 倒排索引 vs 正排索引(Doc Values) * 除了倒排索引,ES 还使用另一种数据结构叫 ​Doc Values​(正排索引的一种形式),与倒排索引(为搜索优化)不同,Doc Values 是为 ​字段值→文档​ 的方向优化的,通常是 ​列式存储,更适用于批量读取某个字段的所有值。主要用于: * ​排序​ * ​聚合 * 脚本计算 #### ES全文检索的过程 > 分析 -> 倒排索引 -> 查找 -> 评分 -> 排序 ```markdown 1. ​用户提交查询请求​ 2. ​查询文本分析与解析​(分词、生成查询结构) 3. ​倒排索引查找​(找到包含查询词项的文档) 4. 相关性评分计算​(计算每个文档的匹配程度 _score) 5. 排序并返回结果​(按得分排序,返回最相关的文档) ``` * 查询解析与分析 1. 查询文本分析(针对 text 类型字段): 如果你查询的是一个 ​text 类型字段,ES 会使用该字段对应的 ​分析器(Analyzer)​​ 对查询文本进行分词、小写化等处理。 2. 生成查询树 * 原始的 match 查询会被 ES 内部转换为一系列 ​Lucene 查询对象,比如 BooleanQuery+ TermQuery。 * 对于 "elasticsearch 全文检索",可能会生成多个词项的布尔组合(默认是 should,相当于 OR)。 3. 倒排索引查找(倒排索引发挥作用) * ES 会在对应字段的 ​倒排索引(Inverted Index)​​ 中,查找每个词项(Term)所匹配的 ​文档ID列表(Posting List)​。接下来,ES 会根据布尔逻辑(比如 should = OR),对这些 posting list 进行 ​合并,找出 ​至少包含其中一个词项的文档,得到一个初步的 ​候选文档集合。 4. 相关性评分(Scoring - 核心!) * 对每一个候选文档,ES 会根据 BM25 模型​ 计算它与查询的相关性得分(_score),用来衡量:​这条文档和你的搜索词到底有多相关?​​ 5. 结果排序与返回 * 所有命中文档会根据 _score ​从高到低排序。 * ES 默认返回得分最高的若干条结果(比如 10 条)。 * 最终返回 JSON 格式的搜索结果,包括文档内容、得分、高亮等信息。 #### ES写入原理 > 一句话总结:​先写内存,再写日志,异步落盘,主分片写入成功后再写副本 1. 客户端发送写入请求 * 请求可以发到 ​任意节点(Node)​,该节点充当 ​协调节点 * 协调节点根据 ​文档 ID(_id)和索引路由规则,计算出该文档应该写入哪个 ​主分片 * 然后,协调节点将请求 ​转发给对应的主分片所在节点 ``` 路由规则(默认):​​ shard = hash(_routing) % number_of_primary_shards 如果没指定 _routing,默认使用 _id来计算。 ``` 2. 写入主分片 1. 写入 ​内存缓冲区 2. 同时写入 ​Translog(事务日志)​,默认每隔 5 秒或 Translog 达到一定大小会 fsync 到磁盘。 * 为了保证数据不丢,ES 在写入内存的同时,也会将这次操作 ​追加写入到 Translog(事务日志,基于 Lucene 的 Write-Ahead Log)​。 * Translog 是 ​持久化存储​(写入磁盘文件),即使服务崩溃,也能通过它恢复数据。 3. 定时刷新(Refresh)——让数据可被搜索 * ​每隔 1 秒钟(默认)​,ES 会执行一次 ​Refresh 操作​: * 将内存缓冲区中的数据生成一个新的 ​Lucene Segment(倒排索引段, 每个 Segment 本质上就是一个小型的、完整的倒排索引)​,并放入 ​文件系统缓存(Filesystem Cache)​。 * 此时,新数据就可以被 ​搜索到了(近实时,NRT, Near Real-Time)​。 * 但该 Segment ​尚未刷盘(fsync),如果宕机仍可能丢失。 4. 定时刷盘(Flush)——真正落盘 * 每隔一段时间(默认 30 分钟)​,或者 ​Translog 达到一定大小,ES 会触发 ​Flush 操作​: * 将文件系统缓存中的 Segment ​fsync 到磁盘,变成真正的 Lucene 持久化 Segment。 * 清空内存缓冲区。 * ​将 Translog 中的内容清空(或滚动新的 Translog)​,因为数据已经安全落盘。 5. 写入副本分片 * ​ES 默认保证数据写入主分片 + 所有副本分片都成功,才认为写入成功(强一致性)​ 6. 返回响应给客户端 * 如果你对一致性要求没那么高,也可以设置 wait_for_active_shards或使用 ?refresh=false等参数控制行为。 ``` 客户端 ↓ 协调节点(Coordinating Node) —— 根据路由找到主分片位置 ↓ 主分片节点(Primary Shard) ├─ 1. 写入内存缓冲区(Memory Buffer) ├─ 2. 同步写入 Translog(磁盘日志,防崩溃) ├─ 3. 定时 Refresh(1s)→ 生成 Segment,可被搜索 ├─ 4. 定时 Flush(30min)→ Segment fsync 到磁盘,清空 Buffer └─ 5. 并行写入所有 Replica Shards(副本分片) (同样走内存 → Translog → Refresh 流程) ↓ 所有分片写入成功后,返回成功响应给客户端 ``` #### ES写入调优 1. 调整 Refresh Interval, 增大 refresh 间隔,减少 segment 生成频率,提高写入吞吐。 ```json PUT /my_index/_settings { "index.refresh_interval": "30s" } ``` 2. 手动控制 Refresh / Flush, 比如大批量导入时,可以先关闭 refresh,导入完再手动触发 ```json PUT /my_index/_settings { "index.refresh_interval": "-1" } ``` 3. 使用 Bulk API 批量写入, 单条写入性能差,推荐使用 _bulk接口批量提交,减少网络和 IO 开销。 4. 副本数写入期间临时调低。 数据导入阶段可以将副本数设为 0,导入完成后再调回,提高写入速度 ```json PUT /my_index/_settings { "index.number_of_replicas": 0 } ``` #### ES搜索调优 #### Elasticsearch(ES) 中,keyword 和 text的区别 | 特性 | keyword | text | |------|--------|------| | 用途 | 精确值匹配(如 ID、状态、标签) | 全文搜索(如文章内容、标题) | | 是否分词 | ❌ 不分词(原样存储) | ✅ 分词(拆成词项) | | 典型查询 | `term`, `terms`, `match_all` | `match`, `multi_match`, `query_string` | | 聚合分析 | ✅ 推荐(精确) | ⚠️ 不推荐(已分词) | | 排序 | ✅ 支持 | ❌ 不建议(分词后无意义) | | 存储空间 | 小 | 相对较大(因倒排索引) | #### 了解ElasticSearch深翻页的问题及解决吗 ```text 1. Search After (推荐) 这是官方推荐的用于深翻页的方案,适用于需要按特定顺序获取大量数据的场景(如导出数据、后台处理)。 原理: 不使用 from,而是使用上一页返回的排序值(sort 字段的值)作为下一页查询的起点。 每次查询都从“某个排序值之后”开始获取 size 条数据。 需要一个全局唯一的、稳定的排序字段(通常结合 @timestamp 和 _id)。 2. Scroll API (已过时,不推荐用于实时查询) 原理: 创建一个快照(scroll),并在一段时间内保持这个快照。 通过 scroll_id 分批获取数据。 适用于需要遍历大量数据的场景(如数据迁移、报表生成)。 需要实时、精确的深翻页,且能接受顺序翻页:使用 search_after。 需要遍历大量数据用于离线处理,不要求实时性:可以考虑 scroll (尽管已过时,但在特定场景仍有用) 或 search_after。 ``` ## 分布式 ### 分布式幂等性设计 > 幂等性:指某操作无论执行多少次,结果都一样。 - 唯一索引:当表中存在唯一索引或唯一组合索引,保证数据唯一性。 - token机制:前端在数据提交前申请token, 并将token放置redis中设置有效时间。提交后后台验证token, 同时删除token (redis要用删除操作来判断token,删除成功代表token校验通过,如果用select + delete来校验token,存在并发问题)。 - 悲观锁:悲观锁(select for update)一般和事务一起使用,数据锁定时间可能会很长,根据实际情况选用(另外还要考虑id是否为主键,如果id不是主键或者不是 InnoDB 存储引擎,那么就会出现锁全表)。 - 乐观锁:给数据库表增加一个version字段,可以通过这个字段来判断是否已经被修改 - 分布式锁:分布式锁(Redis、Zookeeper), 推荐Redission, 看门狗。 ## 消息队列 ### RocketMq #### 广播消息和集群消息 |**特性**|**广播模式**|**集群模式**| |--------|-----------|------------| | **offset 管理** | 本地管理,依赖消费者存储| Broker 集中管理(如 RocketMQ/Kafka)| | **消息丢失风险** | 高(依赖本地存储可靠性) | 低(依赖 Broker 持久化机制) | | **消息重试机制** | 通常无 | 支持(如 Kafka 的 `retries` 配置) | | **死信队列支持** | 通常无 | 支持(如 RocketMQ 的 DLQ) | | **适用场景** | 日志广播、通知等非关键业务 | 订单处理、库存管理等关键业务 | #### RocketMq广播模式为什麽消息丢失风险高 > 在 RocketMQ 的广播模式下,消息的消费进度是存储在消费者客户端的本地文件系统中,而不是像集群模式那样存储在 Broker 端。 消费进度默认存储在消费者客户端机器上的如下路径:**`${user.home}/.rocketmq_offsets/${clientId}/${group}/offsets.json`** * ${user.home}:是运行消费者进程的操作系统用户的主目录,例如: * Linux/Unix: /home/username * Windows: C:\Users\username * ${clientId}:是消费者的客户端 ID,通常是 IP@PID(IP地址@进程ID)或自定义* 的 clientId。 * ${group}:是消费者组(Consumer Group)名称。 1. 本地文件易丢失: * 如果消费者机器宕机、磁盘损坏或目录被误删除,offsets.json 文件会丢失。 * 一旦消费位点丢失,消费者重启后将无法知道上次消费到哪一条消息。 2. 重新消费全部消息: * 当位点丢失或消费者是首次启动时,RocketMQ 会根据配置的 ConsumeFromWhere 策略决定从何处开始消费。 * 在广播模式下,通常会从 队列的最开始 或 最后 开始消费,这可能导致: * 重复消费:从头开始消费已处理过的消息。 * 消息丢失:如果配置为从最新位置开始(CONSUME_FROM_LAST_OFFSET),则会跳过中间未消费的消息。 3. 不支持高可用与故障转移: * 广播模式下,每个消费者实例独立存储自己的位点。 * 即使你有多个消费者实例,也无法像集群模式那样自动将消费任务转移到其他节点,因为位点不共享。 #### RocketMq死信队列 > 当一条消息被消费者消费失败,并且**达到最大重试次数后仍然失败**,RocketMQ 不会直接丢弃这条消息,而是将其转发到一个特殊的队列中——这个队列就是**死信队列**。这些被转移的消息称为**死信消息**。 * 消息进入死信队列的条件 * 消息进入死信队列的主要原因是消费失败且重试无效,具体包括: * 消费者处理消息时抛出异常(如业务逻辑错误、网络超时等)。 * 消费者返回 ConsumeConcurrentlyStatus.RECONSUME_LATER 或消费超时。 * 达到最大重试次数(默认 16 次) * 对于 集群模式(Clustering) 消费者,消息重试次数达到 maxReconsumeTimes(默认 16 次)后仍未成功,将被投递至 DLQ。 * 对于 广播模式(Broadcasting),消息消费失败不会重试,也不会进入死信队列。 * 死信队列的核心特性 | 特性 | 说明 | |------|------| | 一个消费组对应一个死信队列 | 死信队列与 Consumer Group(GID) 绑定,不是与单个消费者或 Topic 绑定。 | | 自动创建 | 当某个 Group 第一次产生死信消息时,RocketMQ 自动创建对应的死信队列。 | | 跨 Topic 聚合 | 同一个 Group 消费的多个 Topic 的死信消息,都会进入同一个死信队列。 | | 不可被正常消费 | 死信队列中的消息不会被普通消费者自动拉取消费,必须手动处理。 | | 保留时间有限 |默认保留 72 小时(3 天),超时后消息会被自动删除。 | * 如何处理死信消息? * 排查原因: * 查看 reconsume_times(重试次数)和 body(消息内容)。 * 检查消费者日志,定位是代码 bug、依赖服务异常还是数据格式错误。 * 修复问题: * 修复代码逻辑、重启依赖服务、更新数据格式等。 * 重新处理: * `重放只是“复制”并重新发送消息,死信队列中的原消息依然存在,需等待自动过期或手动清理。` * 在控制台重新投递消息,让其重新进入消费流程。 * 或将消息导出后通过脚本重新发送。 * 监控告警: * 设置死信队列消息数的监控,一旦堆积立即告警。 #### RocketMq事务消息是如何实现的 ```java import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.client.producer.TransactionMQProducer; import org.apache.rocketmq.client.producer.TransactionSendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; public class OrderService { public static void main(String[] args) throws MQClientException { // 初始化事务消息生产者 TransactionMQProducer producer = new TransactionMQProducer("order_transaction_group"); producer.setNamesrvAddr("localhost:9876"); // 设置事务监听器 TransactionListener transactionListener = new OrderTransactionListener(); producer.setTransactionListener(transactionListener); producer.start(); try { // 构建减库存半消息 Message msg = new Message("OrderTopic", "TagA", "KEY1", ("OrderID:12345, SKU:67890, Quantity:1").getBytes()); // 发送事务消息 TransactionSendResult sendResult = producer.sendMessageInTransaction(msg, null); System.out.printf("SendResult: %s%n", sendResult); } catch (MQClientException e) { e.printStackTrace(); } Runtime.getRuntime().addShutdownHook(new Thread(producer::shutdown)); } } class OrderTransactionListener implements TransactionListener { @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { try { // 执行本地事务:创建订单 boolean isOrderCreated = createOrder(msg); if (isOrderCreated) { return LocalTransactionState.COMMIT_MESSAGE; // 提交库存扣减消息 } else { return LocalTransactionState.ROLLBACK_MESSAGE; // 回滚库存扣减消息 } } catch (Exception e) { e.printStackTrace(); return LocalTransactionState.UNKNOW; // 未知状态 } } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { // 回查本地事务状态 boolean isOrderConfirmed = checkOrderStatus(msg.getKeys()); return isOrderConfirmed ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE; } private boolean createOrder(Message msg) { // 模拟订单创建逻辑(通常为数据库操作) System.out.println("Executing local transaction to create order: " + new String(msg.getBody())); // 这里应该包括数据库的insert操作和相关的业务逻辑 return true; // 返回订单创建成功状态 } private boolean checkOrderStatus(String orderKey) { // 模拟查询订单状态 System.out.println("Checking order status for key: " + orderKey); // 这里通常涉及查询数据库以确认订单的最终状态 return true; // 假设订单已确认 } } ``` #### RocketMQ如何保证消息不丢失 > 为了实现“消息不丢失”,需要在 生产 → 存储 → 消费 全链路每个环节都做好可靠性保障。 1. 生产者保证 - 同步发送`(send())` - 生产者发送消息后,同步等待 Broker 的响应,只有收到 SEND_OK 才认为发送成功。 ```java DefaultMQProducer producer = new DefaultMQProducer("ProducerGroup"); producer.start(); Message msg = new Message("TopicTest", "TagA", "Hello RocketMQ".getBytes()); try { SendResult sendResult = producer.send(msg); // 同步阻塞 if (sendResult.getSendStatus() == SendStatus.SEND_OK) { System.out.println("消息发送成功"); } else { // 处理失败(可重试或记录日志) } } catch (Exception e) { e.printStackTrace(); // 记录失败,可持久化到 DB 或重试 } ``` - 异步发送 + 重试机制 `(sendAsync())` - 异步发送,通过回调函数处理成功/失败,失败时可自动重试。 ```java producer.setRetryTimesWhenSendAsyncFailed(3); // 设置异步重试 3 次 producer.send(msg, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { System.out.println("发送成功: " + sendResult.getMsgId()); } @Override public void onException(Throwable e) { System.out.println("所有重试都失败了,最终异常: " + e.getMessage()); // ❌ 不要在这里再手动重试! // 因为这个回调是在 1 + 3 次尝试都失败后才触发的 } }, 3000); ``` - 事务消息(保证“本地事务”与“消息发送”一致性) - 如“扣款成功 → 发送订单消息”,必须两者一致。 2. Broker保证 - 同步刷盘 - 消息写入 CommitLog 后,必须同步刷写到磁盘才返回成功,即使 Broker 宕机也不丢。 ```conf # 配置(broker.conf), 默认是 ASYNC_FLUSH(异步刷盘),可能丢失最后几秒数据。 flushDiskType=SYNC_FLUSH ``` - 主从架构,设置同步复制 - 采用 主从架构(Master-Slave),消息同步复制到 Slave,Master 宕机后 Slave 可接管。 ```conf # brokerRole=SYNC_MASTER(主节点) # brokerRole=SLAVE(从节点) # flushDiskType=SYNC_FLUSH(建议主从都开启) # Master 配置 brokerName=broker-a brokerId=0 brokerRole=SYNC_MASTER flushDiskType=SYNC_FLUSH # Slave 配置 brokerName=broker-a brokerId=1 brokerRole=SLAVE ``` 3. 消费者保证 - 手动提交消费位移(避免自动提交导致“假消费”) - 如果使用自动提交(AUTO),消费者拉取消息后立即提交 offset,但处理过程中宕机,消息就“丢失”了(未处理但 offset 已提交)。 ```java DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroup"); consumer.setMessageModel(MessageModel.CLUSTERING); consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); // 关闭自动提交 consumer.setConsumeMessageBatchMaxSize(1); // 单条处理 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { try { // 1. 处理业务逻辑(如保存到 DB) processOrder(msgs.get(0)); // 2. 只有成功才返回 CONSUME_SUCCESS return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } catch (Exception e) { // 处理失败,返回 RECONSUME_LATER,触发重试 return ConsumeConcurrentlyStatus.RECONSUME_LATER; } } }); ``` - 消费重试 - 集群模式:失败后自动重试 16 次,间隔逐渐拉长。 - 广播模式:不重试,直接丢弃(需自己保证可靠性) - 幂等性消费(保证不重复消费) - 数据库唯一键:如 message_id 作为唯一索引。 - Redis 去重 ```java String msgId = message.getMsgId(); // 只有当 key 不存在时才设置,等价于 Redis 命令:SET key value NX EX seconds Boolean isExist = redisTemplate.opsForValue().setIfAbsent("msg:consumed:" + msgId, "1", Duration.ofDays(7)); if (!isExist) { // 已处理,跳过 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } // 处理业务 ``` #### RocketMQ如何保证消息顺序 > RocketMQ 通过 “单队列有序写入 + 消费者串行处理” 的机制来保证消息的顺序性。根据业务需求,分为 全局顺序消息 和 分区顺序消息 两种模式。 1. 全局顺序消息 ***(提交消息直接选择队列; 消费者使用顺序消费模式(MessageListenerOrderly)*** 1. 生产者:强制发送到同一个队列 ```java // 发送消息时,手动指定队列(比如 Queue 0) Message message = new Message("BankTransactionTopic", "TagA", "用户A转账100元".getBytes()); SendResult sendResult = producer.send(message, new MessageQueueSelector() { @Override public MessageQueue select(List mqs, Message msg, Object arg) { // 强制选择第0个队列(全局唯一队列) return mqs.get(0); } }, null); ``` 2. 消费者:使用顺序消费模式 ```java DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("BankConsumerGroup"); consumer.subscribe("BankTransactionTopic", "*"); // 使用 MessageListenerOrderly 实现顺序消费 consumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { try { for (MessageExt msg : msgs) { System.out.println("按序处理: " + new String(msg.getBody())); // 处理业务:写数据库、更新账目等 } return ConsumeOrderlyStatus.SUCCESS; } catch (Exception e) { // 出现异常,稍后重试(顺序消费会阻塞) return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; } } }); consumer.start(); ``` 2. 分区顺序消息 ***(具有相同分区键发送到同一队列; 消费者使用顺序消费模式(MessageListenerOrderly)*** 1. 生产者:根据“分区键”选择队列 ```java // 发送订单消息,使用订单ID作为分区键 String orderId = "ORDER_001"; // 分区键 Message message = new Message("OrderStatusTopic", "TagA", ("订单" + orderId + "支付成功").getBytes()); SendResult sendResult = producer.send(message, new MessageQueueSelector() { @Override public MessageQueue select(List mqs, Message msg, Object arg) { String orderId = (String) arg; // 根据 orderId 的 hash 值选择队列,保证同一个订单进入同一个队列 int index = Math.abs(orderId.hashCode()) % mqs.size(); return mqs.get(index); } }, orderId); // 传入 orderId 作为分区键 ``` 2. 消费者:使用顺序消费模式 ```java @Override public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { try { for (MessageExt msg : msgs) { String bizId = new String(msg.getBody()); // 假设是订单ID String msgId = msg.getMsgId(); // 或使用 msgId 作为幂等键 // 1. 检查是否已处理(使用数据库唯一索引) boolean isProcessed = orderService.isMessageProcessed(msgId); if (isProcessed) { System.out.println("消息已处理,跳过: " + msgId); continue; } // 2. 处理业务逻辑 System.out.println("顺序处理订单消息: " + bizId); orderService.updateOrderStatus(bizId); // 3. 标记消息已处理 orderService.markMessageAsProcessed(msgId); } return ConsumeOrderlyStatus.SUCCESS; } catch (Exception e) { return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; } } ``` #### RocketMQ延迟消息 1. 延迟级别:RocketMQ不支持任意时间的延迟,而是提供了18个固定的延迟级别,从1s,5s,10s,30s,1m,2m,3m到2h不等。不同延迟级别 → 不同的 Message Queue(队列)。 | 延迟级别 | 延迟时间 | |---------|---------| | 1 | 1s | | 2 | 5s | | 3 | 10s | | 4 | 30s | | 5 | 1m | | 6 | 2m | | ... | ... | | 18 | 2h | 2. 特殊主题:所有延迟消息都会先发送到一个特殊的内部主题 SCHEDULE_TOPIC_XXXX。 3. 定时任务:Broker 会为每个延迟级别(每个 Queue)维护一个最小堆或时间轮结构(实际是基于 Timer + DelayQueue 的机制),并启动一个后台线程定期检查: * 每个队列中的消息按 投递时间(storeTimestamp + delayTime) 排序 * 定时任务只检查每个队列中最早到期的那条消息 * 如果它到期了,就转移;然后检查下一条 * 如果没到期,就跳过这个队列,等下次扫描 4. 消息转移:当定时任务发现消息已到期后,会将消息从 SCHEDULE_TOPIC_XXXX 转移到目标主题。 5. 消费者消费:消息被转移到目标主题后,消费者就可以正常消费这条消息了。 #### RocketMQ有哪些息过滤机制 1. Tag过滤:简单高效,适合多订阅者需接受不同子类型消息的场景。 1. 生产者发送消息(指定 Tag) ``` java DefaultMQProducer producer = new DefaultMQProducer("OrderProducerGroup"); producer.start(); // 发送“支付成功”消息 Message payMsg = new Message("OrderTopic", "PAY_SUCCESS", "订单123支付成功".getBytes()); producer.send(payMsg); // 发送“发货通知”消息 Message deliverMsg = new Message("OrderTopic", "DELIVER", "订单123已发货".getBytes()); producer.send(deliverMsg); // 发送“订单取消”消息 Message cancelMsg = new Message("OrderTopic", "CANCEL", "订单456已取消".getBytes()); producer.send(cancelMsg); ``` 2. 消费者订阅(按 Tag 过滤) ```java DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("PayConsumerGroup"); // 只订阅 "PAY_SUCCESS" 标签的消息 consumer.subscribe("OrderTopic", "PAY_SUCCESS"); consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { for (MessageExt msg : msgs) { System.out.println("收到支付成功消息: " + new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); consumer.start(); ``` 2. SQL92 表达式过滤(属性过滤):灵活强大,适合需要复杂条件过滤的场景,但是需要对性能加以考虑,并且需要在Broker端配置以支持属性过滤。 * 消息可以携带自定义属性(Message Properties); * 消费者使用 SQL92 表达式 对属性进行过滤,如:age > 18 AND city = 'Beijing'; * Broker 端解析 SQL 表达式,匹配后才投递给消费者; * 需要 在 Broker 配置中开启属性过滤支持。默认不开启,需在 broker.conf 中设置:`enablePropertyFilter=true` ```java 1. 生产者发送消息(添加属性) DefaultMQProducer producer = new DefaultMQProducer("UserActionProducerGroup"); producer.start(); Message msg = new Message("UserActionTopic", "ACTION", "用户行为数据".getBytes()); // 添加自定义属性 msg.putUserProperty("userId", "U123"); msg.putUserProperty("age", "30"); msg.putUserProperty("city", "Beijing"); msg.putUserProperty("isVip", "true"); msg.putUserProperty("action", "ADD_TO_CART"); msg.putUserProperty("isNewUser", "false"); producer.send(msg); 2. 消费者订阅(使用 SQL92 表达式过滤) DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("BeijingConsumerGroup"); // 使用 SQL 表达式:年龄 > 25 且来自北京 consumer.subscribe("UserActionTopic", MessageSelector.bySql("age > 25 AND city = 'Beijing'")); consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { for (MessageExt msg : msgs) { System.out.println("北京高龄用户行为: " + new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); consumer.start(); ``` #### RocketMQ消息是如何存储的 > RocketMQ 采用 “混合存储 + 索引分离” 的设计,所有消息顺序写入 CommitLog,再通过 ConsumeQueue 和 IndexFile 构建逻辑索引,实现高性能写入与高效查询。 > 1. CommitLog文件 - 所有的消息(包括消息体、主题、队列ID等。)都**顺序写入CommitLog文件** 2. ConsumeQueue文件:消费者队列的逻辑索引(快速定位消息) - 每个主题的每个队列都有独立的ConsumeQueue文件,文件路径 `store/consumequeue/{topic}/{queueId}`。它不存储消息内容,只存储 消息在 CommitLog 中的物理偏移量。通过ConsumeQueue,消费者无需扫描整个CommitLog即可快速找到消息的位置。 - 工作流程: 1. 消费者拉取消息,指定 Topic 和 QueueId; 2. Broker 根据 QueueId 找到对应的 ConsumeQueue 文件; 3. 读取 ConsumeQueue 中的 commitLogOffset; 4. 根据 offset 从 CommitLog 中读取消息内容; 5. 返回给消费者。 3. IndexFile文件:基于 Key 的快速查询索引(按业务 ID 查消息) - 若消息带有key(如业务ID),则将其哈希和偏移量存入IndexFile。 这样,可以通过该key快速查找消息。 - 使用 哈希索引 + 链表 解决哈希冲突。 | 文件 | 作用 | 存储内容 | 访问方式 | 性能特点 | |-------------|--------------------|--------------------------------------------|--------------|------------------| | CommitLog | 物理存储所有消息 | 消息完整内容(Body、Topic、Tags、Keys...) | 顺序写 | ⚡️ 写入极高 | | ConsumeQueue| 消费者队列索引 | `commitLogOffset`, `size`, `tagsCode` | 随机读 | 🔍 快速定位 | | IndexFile | 业务 Key 索引 | `keyHash`, `offset`, `timestamp` | 哈希查找 | 🔎 按 key 查询 | #### RocketMQ消息积压问题如何解决 > 消息积压是消息中间件中常见的问题,主要由消费速度跟不上生产速度导致 1. 增加消费者线程数量 ***(最直接的方法,通过增加消费者线程数来提高消费能力)*** ```java DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroup"); // 设置消费线程池大小(默认 20) // 线程数过多可能导致系统资源耗尽,需结合 CPU、数据库连接等资源评估。 consumer.setConsumeThreadMin(50); consumer.setConsumeThreadMax(100); ``` 2. 消息业务提交到线程池中,开启后台线程异步处理 ```java ExecutorService businessThreadPool = Executors.newFixedThreadPool(20); consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { for (MessageExt msg : msgs) { // 提交到业务线程池异步处理 // 风险:异步处理可能丢失消息,建议搭配状态回写或失败重试机制 businessThreadPool.submit(() -> { try { processBusinessLogic(msg); // 耗时操作 } catch (Exception e) { log.error("业务处理失败", e); } }); } // 立即返回 SUCCESS(注意:需确保异步任务可靠) return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); ``` 3. 调整消费者的消费模式 ***(将MessageListenerOrderly改为MessageListenerConcurrently)*** ```java // 改为并发消费,提升吞吐量 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { for (MessageExt msg : msgs) { processMessage(msg); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); ``` 4. 使用消息过滤,只消费重要的消息 `consumer.subscribe("TopicTest", "tag1 || tag2 || tag3");` 5. 临时扩容 ***(消费者组内消费者数量 ≤ 队列总数。若消费者数量超过队列总数,则多余的消费者实例会处于空闲状态(无法分配到队列))*** * RocketMQ 消费者组的并行度受限于 Topic 的队列总数; * 消费者实例数 ≤ 队列数,才能充分利用并发; * 若消费者实例数 > 队列数,多余实例将空闲。 6. 调整生产者发送策略 ```java // 1. 降低发送频率 Thread.sleep(100); // 测试用,生产环境使用限流框架 // 2. 关闭非核心消息发送 if (isCoreMessage(msg)) { producer.send(msg); } // 非核心消息暂不发送 // 3. 批量发送(减少网络开销,但增加单次处理压力) List batch = buildBatch(); producer.send(batch); ``` | 方案 | 优点 | 缺点 | 适用场景 | |----------------|--------------------|------------------|----------------| | 增加消费线程 | 简单直接 | 受限于 CPU/IO | 消费逻辑轻量 | | 异步处理 | 提升吞吐 | 可靠性风险 | 耗时业务 | | 改为并发消费 | 性能翻倍 | 失去顺序性 | 无需顺序 | | 消息过滤 | 快速减负 | 部分消息延迟 | 紧急降级 | | 临时扩容 | 最大化消费能力 | 成本高 | 突发流量 | | 调整生产者 | 源头控制 | 影响上游 | 严重积压 | #### RocketMQ消费消息是推模式还是拉模式 > RocketMQ实际上同时支持推模式和拉模式来消费消息。虽然RocketMQ提供了所谓的"推模式"消费者,但在底层实现上,它仍然是基于拉模式的。 #### RocketMQ生产环境优化 1. 生产者优化 - 批量发送消息 - 异步发送 - 合理设置发送超时时间 2. 消费者优化 - 通过增加消费线程数来提高消费能力 ```java consumer.setConsumeThreadMin(20); consumer.setConsumeThreadMax(64); ``` - 批量消费 配置批量消费可以减少网络交互,提高效率 ```java // 设置批量消费,一次最多消费50条 consumer.setConsumeMessageBatchMaxSize(50); ``` - 合理设置消费重试次数 ***(如果不通过代码或配置显式设置 setMaxReconsumeTimes,RocketMQ 会使用 16 次 作为默认的最大重试次数。 )*** ```java consumer.setMaxReconsumeTimes(3); // 设置最大重试次数为3次 ``` - 消费设置长轮询模式 ```java consumer.setPullInterval(0); // 设置拉取间隔为0,启用长轮询 ``` 3. Broker配置优化 ```yml brokerClusterName = DefaultCluster brokerName = broker-a brokerId = 0 brokerRole = ASYNC_MASTER #主从异步复制消息 flushDiskType = ASYNC_FLUSH #开启异步刷盘 ``` 4. 合理设置Topic的队列数 ```java producer.setDefaultTopicQueueNums(8); // 设置默认的队列数 ``` 6. JVM优化 - 设置合适的堆内存大小 `-Xms8g -Xmx8g -Xmn2g` - 使用G1垃圾收集器 `-XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30` 8. 监控和告警 9. 消息压缩 ```java Message msg = new Message("TopicTest", "TagA", "Hello World".getBytes(StandardCharsets.UTF_8)); msg.setCompressed(true); ``` #### | 模式 | 高可用性 | 数据可靠性 | 性能 | 适用场景 | |-------------------------------|----------|------------|------|----------------------------------| | 单 Master | 低 | 低 | 高 | 测试/开发环境 | | 多 Master | 中 | 低 | 高 | 日志收集、低可靠性要求的业务 | | 多 Master + Slave 异步 | 中 | 中 | 中 | 允许少量数据丢失的高可用场景 | | 多 Master + Slave 同步 | 高 | 高 | 低 | 金融交易、核心业务系统 | | Dledger 集群(Raft) | 极高 | 极高 | 中 | 自动故障转移、强一致性要求场景 | > 选择建议 生产环境优先选择: 多 Master + Slave 同步双写 或 Dledger 集群模式,确保数据零丢失和高可用性。 性能优先: 多 Master 模式,但需容忍少量数据丢失风险。 自动故障转移需求: Dledger 集群模式(RocketMQ 4.5+),通过 Raft 协议实现自动选举和容灾。 #### #### #### #### ## 网络 ### WebSocket ## Jekins > [jekins中文官网](https://www.jenkins.io/zh/doc/) **以下是 jekins 说明** ## 内网穿透 ### natApp 1. 下载: [netapp官网](https://natapp.cn/) 2. 登录账号,新用户购买免费隧道,配置我的隧道 3. 执行`natapp.exe -authtoken=b52ad8e836b66844` | 参数 | 说明 | |------|------| | `-authtoken=...` | 身份验证令牌(必须) | | `-port=8080` | 指定本地服务端口 | | `-log=stdout` | 将日志输出到控制台,便于调试(可选但推荐) | | `-subdomain=myapp` | 指定自定义子域名(如 `myapp.natapp.cc`,可选) | | `-http=80` | 显式指定协议和端口(可选) | 4. 改为bat一键启动 ```bat @echo off cd /d D:\ natapp.exe -authtoken=b52ad8e836b66844 -port=80 -log=stdout pause ``` ## 互联网 ### 一文分清OA、CRM、ERP、MES、HRM、SCM、WMS、KMS等 - 办公协作类 - OA(办公自动化系统) - 定义:Office Automation System - 核心功能: ✓ 流程审批(请假/报销/采购) ✓ 文档协同(在线编辑/版本管理) ✓ 通知公告/日程会议管理 - 典型模块:钉钉、飞书、企业微信的协同功能 - 场景案例:10分钟完成跨部门合同审批 - 客户管理类 - CRM(客户关系管理系统) - 定义:Customer Relationship Management - 核心功能: ✓ 客户信息库(联系人/商机/历史交互) ✓ 销售漏斗管理(线索→商机→成交) ✓ 客户分析(RFM模型/生命周期预测) - 典型模块:Salesforce、纷享销客 - 场景案例:自动识别高价值客户并触发专属服务策略 - 资源管理类 - ERP(企业资源计划系统) - 定义:Enterprise Resource Planning - 核心功能: ✓ 业财一体化(销售-生产-采购-财务联动) ✓ 库存管理(安全库存预警/MRP运算) ✓ 成本核算(标准成本/实际成本对比) - 典型模块:SAP、金蝶、用友 - 场景案例:自动生成采购计划避免停工待料 - 生产制造类 - MES(制造执行系统) - 定义:Manufacturing Execution System - 核心功能: ✓ 生产调度(工单排程/设备联机) ✓ 过程监控(良品率/OEE设备效率) ✓ 质量追溯(批次追踪/SPC统计分析) - 典型模块:西门子MES、蓝卓supOS - 场景案例:实时预警生产线异常并自动触发维修工单 - 人力管理类 - HRM(人力资源管理系统) - 定义:Human Resource Management - 核心功能: ✓ 组织架构管理(岗位/职级/编制) ✓ 全周期员工管理(招聘→入职→离职) ✓ 薪酬绩效(个税计算/KPI自动关联) - 典型模块:北森、SAP SuccessFactors - 场景案例:自动生成人力成本分析报告 - 供应链类 - SCM(供应链管理系统) - 定义:Supply Chain Management - 核心功能: ✓ 供应商管理(准入/评估/分级) ✓ 需求预测(机器学习预测销量) ✓ 物流协同(运输路线优化/在途跟踪) - 典型模块:SAP SCM、京东物流系统 - 场景案例:疫情期动态调整全球采购策略 - 仓储物流类 - WMS(仓储管理系统) - 定义:Warehouse Management System - 核心功能: ✓ 库位管理(三维建模/智能推荐储位) ✓ 作业优化(波次拣货/AGV调度) ✓ 库存可视化(实时库龄/效期预警) - 典型模块:富勒FLUX、通天晓 - 场景案例:双十一期间库内作业效率提升200% - 知识管理类 - KMS(知识管理系统) - 定义:Knowledge Management System - 核心功能: ✓ 知识库建设(文档/视频/FAQ管理) ✓ 智能检索(语义搜索/关联推荐) ✓ 知识图谱(构建业务关系网络) - 典型模块:Confluence、语雀 - 场景案例:新人通过知识图谱快速掌握项目全貌 企业信息系统类型及核心特征 | 系统类型 | 核心对象 | 数据特征 | 价值焦点 | |----------|----------------|------------------------|--------------------| | **OA** | 企业员工 | 流程/协作数据 | 效率提升 | | **CRM** | 客户 | 商机/交互数据 | 收入增长 | | **ERP** | 企业资源 | 业财一体化数据 | 成本控制 | | **MES** | 生产现场 | 过程质量数据 | 良率提升 | | **SCM** | 供应链节点 | 物流/库存数据 | 供需平衡 | | **WMS** | 仓库 | 库位/作业数据 | 周转率优化 | | **KMS** | 知识资产 | 文档/经验数据 | 组织能力沉淀 | | **HRM** | 员工 | 人事/绩效数据 | 人才管理与发展 | | **PMS** | 项目 | 项目进度/资源数据 | 项目成功率 | | **DMS** | 分销商/经销商 | 渠道销售/订单数据 | 渠道管理与拓展 | | **BI** | 决策者 | 数据分析/报表 | 决策支持 | | **LMS** | 学员/学习者 | 学习记录/评估数据 | 学习效果提升 | | **TMS** | 运输/物流 | 运输计划/执行数据 | 运输效率与成本控制 | | **QMS** | 质量管理 | 质量检测/改进数据 | 质量保证与提升 | | **EMS** | 环境监测 | 环境参数/合规数据 | 环保合规与可持续发展| | **FMS** | 财务 | 财务报表/预算数据 | 财务透明度与管控 | | **CMS** | 内容 | 内容创建/发布数据 | 内容管理和分发效率 | | **AMS** | 资产 | 资产登记/维护数据 | 资产利用率与维护 | | **EAM** | 设备/设施 | 设备运行/维护数据 | 设备可靠性与维护 | | **PLM** | 产品生命周期 | 产品设计/变更数据 | 创新与产品上市速度 | | **ITSM** | IT服务 | 事件/问题/变更数据 | IT服务质量与响应 | | **GRC** | 合规/风险 | 风险评估/合规数据 | 合规性与风险管理 |