diff --git a/src/Java/eightpart/concurrency.md b/src/Java/eightpart/concurrency.md index ba2ca9ea..4b74b926 100644 --- a/src/Java/eightpart/concurrency.md +++ b/src/Java/eightpart/concurrency.md @@ -875,6 +875,82 @@ static class ThreadLocalMap { `ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。 +**ThreadLocal为什么不用强引用?** + +`ThreadLocal`在Java中是一种常用的线程隔离技术,它可以为每一个线程提供一个独立的变量副本,从而实现线程安全。但是,`ThreadLocal`并不使用强引用来存储这些变量副本,而是使用弱引用。这是出于以下几个原因: + +1. **内存泄漏问题**:如果`ThreadLocal`使用强引用来存储变量副本,那么只要线程本身不结束,这些副本就不会被垃圾回收机制回收。这可能会导致内存泄漏问题,尤其是在长时间运行的线程和大量使用`ThreadLocal`的情况下。 +2. **线程结束时的清理**:当线程结束时,所有由该线程持有的`ThreadLocal`变量副本应该被清理。如果`ThreadLocal`使用强引用,那么即使线程结束,这些副本仍然可能存在,除非显式地清理它们。而使用弱引用则可以确保线程结束时,这些副本会被自动清理。 +3. **灵活的生命周期管理**:弱引用允许更灵活的生命周期管理。`ThreadLocal`中的变量副本只要不再被线程引用,就可以被垃圾回收机制回收。这可以有效防止内存过载,并允许更高效的资源利用。 + +虽然`ThreadLocal`使用弱引用可以带来以上好处,但也有一些需要注意的地方。比如,你需要确保在线程生命周期内,持有到`ThreadLocal`对象的强引用,否则这个`ThreadLocal`对象可能会提前被回收。另外,即使`ThreadLocal`使用弱引用,也不能完全防止内存泄漏。如果线程结束后,没有清理`ThreadLocal`的值,那么这些值将会保留在线程的ThreadLocalMap中,导致内存泄漏。所以,使用`ThreadLocal`时,最好的做法是在不再需要使用变量副本时,显式地清理它。 + + + +`ThreadLocal`实现线程本地存储的核心在于每个`Thread`都维护了一个`ThreadLocalMap`,`ThreadLocalMap`实际上是一个特殊设计的哈希映射,其键为`ThreadLocal`对象(使用弱引用), 值为线程局部变量。 + +`ThreadLocalMap`的设计使得`ThreadLocal`对象可以在不被线程引用的情况下被垃圾回收器回收。这是因为`ThreadLocalMap`的键使用了`ThreadLocal`的弱引用。弱引用是一种弱度小于强引用但大于软引用和弱引用的引用,只要垃圾回收器发现了弱引用,不论系统内存是否足够,都会回收掉只被弱引用关联的对象。 + +当`ThreadLocal`对象被回收后,其对应的值在`ThreadLocalMap`中的实体(Entry)会变为null,这是一个空的引用。因此,`ThreadLocalMap`还需要提供一种机制来清理这些没有键的Entry,否则这些Entry的值可能会造成内存泄露。这个机制是在`ThreadLocal`的`get()`, `set()` 和 `remove()` 方法被调用时,通过调用`ThreadLocalMap`的`expungeStaleEntries()`方法来清理无效的Entry。(ps. 这里看源码调用链是这样的:get() -> map.getEntry(*this*) -> getEntryAfterMiss(key, i, e) -> expungeStaleEntry(i)) + +```java +// 清理过期的ThreadLocal变量 +private void expungeStaleEntries() { + Entry[] tab = table; + int len = tab.length; + // 遍历数组中的每一个元素 + for (int j = 0; j < len; j++) { + Entry e = tab[j]; + // 如果该元素存在,但其对应的ThreadLocal变量已经被回收,则进行清理 + if (e != null && e.get() == null) + expungeStaleEntry(j); + } +} + +// 清理staleSlot位置上的元素 +private int expungeStaleEntry(int staleSlot) { + Entry[] tab = table; + int len = tab.length; + + // 清除staleSlot位置上的元素 + tab[staleSlot].value = null; + tab[staleSlot] = null; + size--; + + // 重新哈希,直到遇到空元素 + Entry e; + int i; + for (i = nextIndex(staleSlot, len); + (e = tab[i]) != null; + i = nextIndex(i, len)) { + // 获取元素对应的ThreadLocal变量 + ThreadLocal k = e.get(); + if (k == null) { + // 如果ThreadLocal变量已经被回收,则清除该元素 + e.value = null; + tab[i] = null; + size--; + } else { + // 计算新的哈希值 + int h = k.threadLocalHashCode & (len - 1); + if (h != i) { + // 如果哈希值不等于当前位置,则将该元素拷贝到新位置 + tab[i] = null; + + // Unlike Knuth 6.4 Algorithm R, we must scan until + // null because multiple entries could have been stale. + while (tab[h] != null) + h = nextIndex(h, len); + tab[h] = e; + } + } + } + return i; +} +``` + +然而,尽管`ThreadLocalMap`提供了自动清理无效Entry的机制,但是如果我们不再使用`ThreadLocal`对象,但是又没有显式地调用`ThreadLocal`的`remove()`方法,那么`ThreadLocal`的值是不会被自动清理的,因为`expungeStaleEntries()`方法只有在`ThreadLocal`的`get()`, `set()` 和 `remove()` 方法被调用时才会执行。这就可能导致内存泄露,尤其是在长时间运行的线程中。因此,我们在使用`ThreadLocal`时,通常需要手动调用`ThreadLocal`的`remove()`方法来清理不再使用的值。 + ### 实现 Runnable 接口和 Callable 接口的区别 继承Thread类或者实现Runnable接口这两种方式来创建线程类,但是这两种方式有一个共同的缺陷:不能获取异步执行的结果。Callable接口类似于Runnable。不同的是,Runnable的唯一抽象方法run()没有返回值,也没有受检异常的异常声明。比较而言,Callable接口的call()有返回值,并且声明了受检异常,其功能更强大一些。 diff --git a/src/Java/eightpart/foundation.md b/src/Java/eightpart/foundation.md index 6cec27db..d5951ecf 100644 --- a/src/Java/eightpart/foundation.md +++ b/src/Java/eightpart/foundation.md @@ -1334,6 +1334,51 @@ http://softlab.sdut.edu.cn/blog/subaochen/2017/01/generics-type-erasure/ ![image-20220622211220694](./personal_images/image-20220622211220694.webp) +具体的: + +```java +public class Parent { + public T data; + + public void setData(T data) { + this.data = data; + } +} + +public class Child extends Parent { + @Override + public void setData(Integer data) { + super.setData(data); + } +} +``` + +在这个例子中,我们有一个泛型类 `Parent` 和一个扩展了 `Parent` 的非泛型类 `Child`。我们在 `Child` 中覆盖了 `Parent` 中的 `setData` 方法。这里要注意的是,`Child` 中的 `setData` 方法覆盖的是 `Parent` 中的 `setData` 方法,而非 `Parent` 类中的泛型 `setData` 方法。 + +为了解决这个问题,编译器会在 `Child` 类中自动生成一个桥方法,如下: + +```java +public void setData(Object data) { + setData((Integer) data); +} +``` + +这个方法的参数是 `Object` 类型,这样它就可以覆盖 `Parent` 类中的 `setData` 方法了。并且,这个方法内部会将参数转换为 `Integer` 类型,然后调用 `Child` 类中的 `setData(Integer)` 方法,也就是我们明确写出的方法。 + +所以,你并不需要手动写出桥方法,它是由编译器自动插入的。并且,桥方法只有在需要解决泛型类型擦除导致的方法覆盖问题时才会被生成。 + + + +小结一下: + +在Java中,因为泛型信息在编译后会被擦除,所以运行时所有的泛型类型都会变成它们的上界,对于没有明确指定上界的泛型参数,它的上界默认是 `Object`。 + +在你的例子中,`Parent` 类中的 `setData` 方法的参数类型 `T` 在运行时会变成 `Object`。因此,如果 `Child` 类想要覆盖这个方法,那么它的方法参数也必须是 `Object` 类型。但是,我们在 `Child` 类中定义的 `setData` 方法的参数类型是 `Integer`,因此它并没有真正覆盖 `Parent` 类中的 `setData` 方法。 + +为了解决这个问题,编译器会自动在 `Child` 类中生成一个桥方法,这个方法的参数类型是 `Object`,它覆盖了 `Parent` 类中的 `setData` 方法。这个桥方法内部会将参数强制转换为 `Integer` 类型,然后调用 `Child` 类中的 `setData(Integer)` 方法。这样,我们就可以通过 `Parent` 类的引用来调用 `Child` 类中的方法了,而不会出现类型不匹配的问题。 + + + #### 为了更深层次的理解类型擦除,选取了Stackoverflow的高赞回答 原帖:https://stackoverflow.com/questions/339699/java-generics-type-erasure-when-and-what-happens @@ -1552,7 +1597,7 @@ static int hash(int h) { #### JDK1.8 之后 -相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 +相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。如果是红黑树,当元素数量少于6时,会退化为链表。 ![image-20220616155623710](./personal_images/image-20220616155623710.webp) @@ -1581,6 +1626,39 @@ static final int hash(Object key) { 这样处理的主要目的是在不增加计算复杂度的前提下,尽可能减少哈希碰撞并更好地分布数据。因为HashMap中的bucket选择主要是通过哈希值的低位进行的,如果原始哈希值的高位和低位相似,那么进行这样的处理能够将高位的信息带入到低位,从而提高bucket的选择范围,减少碰撞。 +让我们看一个具体的例子: + +假设有一个`String`对象,其`hashCode()`方法返回的值为`123456789`。 + +在32位二进制表示中,这个数值看起来是这样的: + +```shell +0000 0111 0101 1011 1100 1101 0001 0101 +``` + +当我们将这个值右移16位(`>>> 16`)后,我们得到: + +```shell +0000 0000 0000 0000 0000 0111 0101 1011 +``` + +接下来,我们将这两个值进行异或(`^`)操作: + +```shell +0000 0111 0101 1011 1100 1101 0001 0101 (hashCode) +0000 0000 0000 0000 0000 0111 0101 1011 (hashCode >>> 16) +--------------------------------------- +0000 0111 0101 1011 1100 1101 0110 1110 (result) +``` + +最终结果就是新的哈希值。 + +现在,你可能会问为什么需要这样的操作。在HashMap中,我们`通常使用哈希值的低位来决定一个元素存放在哪个桶中,高位则不常用`。当我们将hashCode右移16位后,原来的高位被放到了低位。然后我们通过异或操作,使得新的低位同时受到原始的高位和低位的影响,这样就`使得原来不常使用的高位信息也能参与到桶的选择中`,从而使得元素在各个桶之间的分布更均匀。 + +这是一个非常巧妙的设计,既利用到了原始的hashCode信息,也改善了哈希分布。 + + + #### getNode() `get(Object key)`方法根据指定的 `key`值返回对应的 `value`,该方法调用了 `getEntry(Object key)`得到相应的 `entry`,然后返回 `entry.getValue()`。因此 `getEntry()`是算法的核心。 **算法思想**是首先通过 `hash()`函数得到对应 `bucket`的下标,然后依次遍历冲突链表,通过 `key.equals(k)`方法来判断是否是要找的那个 `entry`。 diff --git a/src/Java/eightpart/gaint2023.md b/src/Java/eightpart/gaint2023.md index 01f243cf..b681b7f6 100644 --- a/src/Java/eightpart/gaint2023.md +++ b/src/Java/eightpart/gaint2023.md @@ -462,6 +462,69 @@ unsafe.getAndAddInt() 方法实现了 CAS,它会获取当前值,加 1,并比较 所以 AtomicInteger 通过 CAS 无锁操作实现了线程安全的递增操作。 + + +### Future抛出的两个异常的对比,都有什么用?(2023 百度提前批) + +在Java中,`Future`接口在其方法执行过程中可能会抛出以下异常: + +1. **InterruptedException**:当线程在等待、睡眠或其他占用时间的操作中被中断时可能会抛出此异常。例如,`Future.get()`方法会在等待计算完成时抛出此异常,如果线程在等待过程中被中断。 + + ```java + Future future = executor.submit(callable); + try { + Integer result = future.get(); + } catch (InterruptedException e) { + // handle exception + } + ``` + +2. **ExecutionException**:如果`Future`的计算抛出异常,那么`Future.get()`方法会抛出这个异常。这个异常的原因可以通过`ExecutionException.getCause()`获得。 + + ```java + Future future = executor.submit(callable); + try { + Integer result = future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + // handle exception + } + ``` + +3. **TimeoutException**:当`Future.get(long timeout, TimeUnit unit)`在超时时间内没有得到结果时,会抛出此异常。 + + ```java + Future future = executor.submit(callable); + try { + Integer result = future.get(5, TimeUnit.SECONDS); + } catch (TimeoutException e) { + // handle exception + } + ``` + +4. **CancellationException**:当尝试取回已经取消的任务的结果时,会抛出此异常。 + + ```java + Future future = executor.submit(callable); + future.cancel(true); + try { + Integer result = future.get(); + } catch (CancellationException e) { + // handle exception + } + ``` + +请注意,这些异常都必须被捕获和处理,因为它们都是`java.lang.Exception`的子类,是已检查的异常。 + + + +> 参考: +> +> 1. https://stackoverflow.com/questions/2665569/in-what-cases-does-future-get-throw-executionexception-or-interruptedexception 在什么情况下Future. get()抛出ExecutionException或InterruptedException +> 2. https://stackoverflow.com/questions/2248131/handling-exceptions-from-java-executorservice-tasks 处理ExecutorService任务Java异常 +> 3. https://www.baeldung.com/java-future +> 4. https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html + ## 🍃 常用框架 ### MyBatis运用了哪些常见的设计模式?(2023美团) - **工厂模式**,工厂模式在 MyBatis 中的典型代表是 SqlSessionFactory @@ -492,7 +555,7 @@ MyBatis中的Mapper接口并不需要实现,它只是定义了一组方法签 5. MyBatis也能**更好地处理一对多、多对多**等复杂关系。 6. 最后,MyBatis提供了**一些JDBC无法提供的特性**,如延迟加载,这对于性能优化是非常有用的。 -### JDBC连接数据库的步骤吗?(2023美团) +### JDBC连接数据库的步骤(2023美团) 1. **加载数据库驱动程序**:首先,我们需要加载数据库驱动。这可以通过 Class.forName() 方法实现,例如 Class.forName("com.mysql.jdbc.Driver")。 2. **建立数据库连接**:使用DriverManager.getConnection()方法建立与数据库的连接,需要指定数据库的URL、用户名和密码,例如:Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/mydatabase", "username", "password"); 3. **创建Statement对象**:使用Connection对象的createStatement()方法创建一个Statement对象,用于执行SQL语句,例如:Statement stmt = conn.createStatement(); @@ -906,20 +969,49 @@ noslave-lazy-flush no >小红书的问法是:Redis中,如果单key过热怎么处理? 热key是指在Redis中被频繁访问的key。当大量的请求都集中在一小部分key上时,就会形成热key。这可能导致Redis服务器负载不均,甚至可能导致部分业务瘫痪。 -1. **热点key分拆**:将热点key拆分成多个小key,均匀分散到多个Redis实例上,从而避免单个Redis实例的瓶颈。 -2. **读写分离**:对于读多写少的热key,可以考虑使用Redis的主从复制功能,所有的读操作都发送到从服务器,写操作发送到主服务器,从而分散读取压力。 -3. **数据缓存**:对于计算复杂或者读取代价大的热key,可以考虑在业务端增加缓存处理,例如使用本地缓存,减少对Redis的直接访问。 -4. **使用第三方中间件**:例如使用Twemproxy等代理工具进行自动的分片和读写分离,避免热key问题。 -5. **使用限流/熔断机制**:在业务端加入限流/熔断机制,保证系统的稳定。 -6. **使用哈希算法分片热点键**:这种方法可以将热点数据分散到不同的节点上,降低单个节点的负载,从而解决热key问题。这是一种在数据层面解决问题的策略,需要结合具体的业务场景进行设计和实施。 -7. **设置LFU(最不经常使用)** 策略并运行redis-cli --hotkeys以确定哪些键更频繁地访问:Redis在4.0版本引入了LFU淘汰策略,这种策略可以根据键的使用频率来进行淘汰,比较适合处理热key问题。另外,通过运行redis-cli --hotkeys命令,可以找出访问频率最高的key,这对于分析和解决热key问题非常有帮助。 -总的来说,解决热key问题主要是通过分散热点、减少读取次数、增加读取的速度等方法,从而减轻Redis服务器的压力。 +#### 如何发现热key? + +- 凭借业务经验,预估热 Key 出现:根据业务系统上线的一些活动和功能,我们是可以在某些场景下提前预估热 `Key` 的出现的,比如业务需要进行一场商品秒杀活动,秒杀商品信息和数量一般都会缓存到 `Redis` 中,这种场景极有可能出现热 `Key` 问题的。 +- 客户端进行收集:一般我们在连接 `Redis` 服务器时都要使用专门的 SDK(比如:`Java` 的客户端工具 `Jedis`、`Redisson`),我们可以对客户端工具进行封装,在发送请求前进行收集采集,同时定时把收集到的数据上报到统一的服务进行聚合计算。 +- 在代理层进行收集:如果所有的 `Redis` 请求都经过 `Proxy`(代理)的话,可以考虑改动 `Proxy` 代码进行收集,思路与客户端基本类似。 +- hotkeys 参数:`Redis` 在 `4.0.3` 版本中添加了 [hotkeys](https://github.com/redis/redis/pull/4392) 查找特性,可以直接利用 `redis-cli --hotkeys` 获取当前 `keyspace` 的热点 `key`,实现上是通过 `scan + object freq` 完成的。 +- monitor 命令:`monitor` 命令可以实时抓取出 `Redis` 服务器接收到的命令,通过 `redis-cli monitor` 抓取数据,同时结合一些现成的分析工具,比如 [redis-faina](https://github.com/facebookarchive/redis-faina),统计出热 Key。 +- Redis 节点抓包分析:`Redis` 客户端使用 `TCP` 协议与服务端进行交互,通信协议采用的是 `RESP` 协议。自己写程序监听端口,按照 `RESP` 协议规则解析数据,进行分析。或者我们可以使用一些抓包工具,比如 `tcpdump` 工具,抓取一段时间内的流量进行解析。 + +#### 如何解决热key? + +常用方案: + +1. **【强力推荐】分片**:这是一种常见的解决方式,将数据分散到多个 Redis 实例中去,这样可以分散访问压力,避免单个 Redis 实例被过度访问。之所以出现热 `Key`,是因为有大量的对同一个 `Key` 的请求落到同一个 `Redis` 实例上,如果我们可以有办法将这些请求打散到不同的实例上,防止出现流量倾斜的情况,那么热 `Key` 问题也就不存在了。 + + 那么如何将对某个热 `Key` 的请求打散到不同实例上呢?我们就可以通过热 `Key` 备份的方式,基本的思路就是,我们可以给热 `Key` 加上前缀或者后缀,把一个热 `Key` 的数量变成 `Redis` 实例个数 `N` 的倍数 `M`,从而由访问一个 `Redis` `Key` 变成访问 `N * M` 个 `Redis` `Key`。 `N * M` 个 `Redis` `Key` 经过分片分布到不同的实例上,将访问量均摊到所有实例。 + +2. 二级缓存(本地缓存):当出现热 `Key` 以后,把热 `Key` 加载到系统的 `JVM` 中。后续针对这些热 `Key` 的请求,会直接从 `JVM` 中获取,而不会走到 `Redis` 层。这些本地缓存的工具很多,比如 `Ehcache`,或者 `Google Guava` 中 `Cache` 工具,或者直接使用 `HashMap` 作为本地缓存工具都是可以的。 + +其他解决方案: + +1. [MemCached](https://www.memcached.org/) / [Redission](https://redisson.org/) + Redis主从复制:这个方案也可以说是分片的具体实现了[2]。复制热点数据到多个节点,这样可以分摊读取压力。Redis 具有内建的主从复制功能,可以使用它来实现热点数据的复制。Memcached 不直接支持复制,但你可以在上层实现。同样,Redission 自身并不直接解决热点键(HotKeys)问题,但支持多种集群模式,包括分片,可以将数据更均匀地分布在多个节点上,从而降低热点键对单个节点的影响。 + +![](https://dz2cdn1.dzone.com/storage/temp/11309552-screen-shot-2019-02-19-at-62001-pm.png) + +2. 代理+读写分离+SLB(服务器负载均衡(Server Load Balancing)): + 1. 负载均衡在SLB层实现 + 2. 在代理层实现读写分离和自动路由 + 3. 写请求由主节点处理 + 4. 读请求由从节点处理 + 5. HA(High Availability,互联网黑话)在从节点和主节点上实现 + +![](https://dz2cdn1.dzone.com/storage/temp/11309553-screen-shot-2019-02-19-at-62011-pm.png) + + > 参考: > 1. https://dzone.com/articles/redis-hotspot-key-discovery-and-common-solutions -> 2. https://developer.redis.com/howtos/antipatterns/ -> 3. https://github.com/twitter/twemproxy +> 2. https://dongzl.github.io/2021/01/14/03-Redis-Hot-Key/index.html +> 3. https://abhivendrasingh.medium.com/understanding-and-solving-hotkey-and-bigkey-and-issues-in-redis-1198f98b17a5 +> 4. https://developerknow.com/what-is-the-hot-key-problem-in-redis-how-to-solve-the-hot-key-problem/ +> 5. https://www.programmersought.com/article/54911114988/ ### 讲下Redis的ZSet(2023 滴滴) Redis的ZSet(有序集合)是一种数据结构,它与普通的集合相比,在存储每个元素时关联了一个分数(score)。这个分数用于对集合中的元素进行排序,并且每个元素都是唯一的。当多个字符串具有相同分数时,这些字符串按字典顺序排列。一些有序集合的用途包括: @@ -1109,6 +1201,156 @@ Redis RDB的工作原理是: 所以,RDB持久化最大的问题就是可能导致数据丢失,虽然概率很小,但数据依然存在一定风险。 + + +### mysql中,主键索引和唯一索引的区别是什么?(2023 百度提前批) + +在MySQL中,**主键索引(Primary Key)**和**唯一索引(Unique Key)**都是两种非常重要的索引类型,它们之间的主要区别可以从以下几个方面来理解: + +1. **唯一性**:主键索引和唯一索引都保证了索引列的唯一性。换句话说,主键索引和唯一索引都要求索引列的值唯一,不能有重复的值。 +2. **非空性**:主键索引要求索引列的值必须是非空的,而唯一索引则允许索引列的值为NULL。也就是说,如果你在一列上定义了主键索引,那么你不能在这列上插入NULL值;而如果你在一列上定义了唯一索引,那么你可以在这列上插入NULL值。 +3. **数量**:在一个表中,主键索引只能有一个,而唯一索引可以有多个。 +4. **自动增长**:主键可以自动增长,但是唯一索引不能。 +5. **集群索引**:在InnoDB存储引擎中,主键索引是集群索引(Clustered Index),也就是说,数据记录是按照主键的顺序来存储的。如果一个表没有明确定义主键,InnoDB会选择一个唯一索引作为主键,如果没有唯一索引,InnoDB会自动生成一个6字节的ROWID作为主键。 + +这些区别让主键索引和唯一索引在数据库设计和优化中发挥不同的作用。 + + + +### 一个联合索引(a, b, c),查询(a, c),能用到索引吗?(2023 百度提前批) + +在MySQL中,使用联合索引(比如索引(a, b, c))查询时,需要按照索引的顺序使用列。这是由MySQL的索引策略决定的,称为最左前缀原则或最左前缀匹配原则。 + +对于查询(a, c),只有在查询条件中包含“a”字段,并且"b"字段也在查询条件中时,"c"才能用到索引。这是因为在联合索引(a, b, c)中,“c"是第三个字段,如果查询条件中不包含"b”,那么索引在"b"之后的部分都不能使用。 + +因此,如果你的查询条件是(a, c),那么MySQL只能使用到"a"的部分索引,不能利用到"c"的部分索引,即便"c"在索引中。 + +为了使查询更有效地使用索引,你可以考虑调整索引或查询的设计。例如,你可以创建一个新的联合索引(a, c),或者在查询条件中包含"b"字段。 + + + +### 事务原子性是怎么实现的?(2023 美团) + +MySQL的事务原子性主要通过Undo Log(撤销日志)来实现的。 + +当进行一次事务操作时,MySQL会首先在Undo Log中记录下事务操作前的数据状态。如果事务成功执行并提交,Undo Log中的记录就可以被删除。但如果在事务执行过程中出现错误,或者用户执行了ROLLBACK操作,MySQL就会利用Undo Log中的信息将数据恢复到事务开始前的状态,从而实现事务的原子性。 + +这就意味着,事务要么全部执行成功,要么如果部分执行失败,那么已经执行的部分也会被撤销,保证数据的一致性。 + + + +### 事务的隔离性怎么实现的?(2023 美团) + +MySQL的事务隔离性主要通过锁机制和多版本并发控制(MVCC)来实现。 + +1. 锁机制:包括行锁和表锁。行锁可以精确到数据库表中的某一行,而表锁则会锁定整个数据表。当一个事务在操作某个数据项时,会对其加锁,阻止其他事务对同一数据项的并发操作,从而实现隔离性。 +2. 多版本并发控制(MVCC):这是InnoDB存储引擎特有的一种机制,它可以在不加锁的情况下创建数据在某一时间点的快照。在读取数据时,MVCC会返回该时间点的数据版本,即使该数据后来被其他事务修改。这样,每个事务都有自己的数据视图,彼此之间不会互相影响,实现了隔离性。 + +此外,MySQL还提供了四种隔离级别(读未提交、读已提交、可重复读、串行化),可以根据需要选择不同的隔离级别,以在并发性和数据一致性之间取得平衡。 + + + +### 事务一致性怎么实现的?(2023 美团) + +MySQL实现事务一致性主要依赖于其InnoDB存储引擎的ACID属性,其中C代表一致性(Consistency)。具体来说,以下是MySQL如何实现事务一致性的一些方式: + +1. **使用锁机制**:InnoDB存储引擎支持行级锁和表级锁,通过锁机制来控制并发事务的访问冲突,确保每个事务都在一致性的状态下执行。 +2. **使用MVCC**:InnoDB存储引擎通过MVCC来实现读已提交和可重复读两个隔离级别,保证了事务的一致性视图,即在事务开始时生成一个快照,事务在执行过程中看到的数据都是这个快照中的数据。 +3. **使用Undo日志**:InnoDB存储引擎在修改数据前,会先将原始数据保存在Undo日志中,如果事务失败或者需要回滚,就可以利用Undo日志将数据恢复到原始状态,从而保证了数据的一致性。 +4. **使用Redo日志**:Redo日志用于保证事务的持久性,但也间接保证了一致性。因为在系统崩溃恢复时,可以通过Redo日志来重做已提交的事务,保证这些事务的修改能够持久保存。 + + + +### 事务的持久性怎么实现的?(通过2023 美团面试题思考补充) + +MySQL实现事务持久性主要依赖于其使用的存储引擎,比如InnoDB。以下是InnoDB如何实现事务持久性的一些方式: + +1. **重做日志(Redo Logs)**:InnoDB引擎有一组重做日志文件,事务提交前,会先将更改写入到重做日志中,并保证这些日志被写入到磁盘。这种机制就是所谓的预写式日志(Write-Ahead Logging,WAL)。每个重做日志都包含了事务的相关信息,以及这个事务所做的修改。 +2. **刷新缓冲池(Flush Buffer Pool)**:当事务提交时,InnoDB引擎只是将更改写入了重做日志,并没有立即写入到磁盘的数据文件中。实际的写入过程是在后台以一定的频率进行的,这就是所谓的刷新缓冲池。 +3. **崩溃恢复(Crash Recovery)**:如果数据库在事务执行过程中崩溃,InnoDB可以使用重做日志来恢复数据到一个一致的状态。在重启过程中,InnoDB会检查重做日志,并将所有未完成的事务所做的修改应用到数据文件中,同时取消所有未提交的事务。 + +这些机制共同确保了MySQL事务的持久性,即使在系统崩溃后,已提交的事务所做的修改也能被正确地恢复。 + +> 具体如何刷盘可以看:https://javaguide.cn/database/mysql/mysql-logs.html#%E5%88%B7%E7%9B%98%E6%97%B6%E6%9C%BA + + + +### MVCC的隔离机制介绍一下?(2023 美团) + +- Read View 中四个字段作用; +- 聚簇索引记录中两个跟事务有关的隐藏列; + +那 Read View 到底是个什么东西? + +![mvcc-1](./giant_images/mvcc-1.webp) + +Read View 有四个重要的字段: + +- m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的**事务 id 列表**,注意是一个列表,**“活跃事务”指的就是,启动了但还没提交的事务**。 +- min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 **id 最小的事务**,也就是 m_ids 的最小值。 +- max_trx_id :这个并不是 m_ids 的最大值,而是**创建 Read View 时当前数据库中应该给下一个事务的 id 值**,也就是全局事务中最大的事务 id 值 + 1; +- creator_trx_id :指的是**创建该 Read View 的事务的事务 id**。 + +知道了 Read View 的字段,我们还需要了解聚簇索引记录中的两个隐藏列。 + +假设在账户余额表插入一条小林余额为 100 万的记录,然后我把这两个隐藏列也画出来,该记录的整个示意图如下: + +![mvcc-2](./giant_images/mvcc-2.webp) + +对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列: + +- trx_id,当一个事务对某条聚簇索引记录进行改动时,就会**把该事务的事务 id 记录在 trx_id 隐藏列里**; +- roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后**这个隐藏列是个指针,指向每一个旧版本记录**,于是就可以通过它找到修改前的记录。 + +在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况: + +![mvcc-3](./giant_images/mvcc-3.webp) + +一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况: + +- 如果记录的 trx_id 值小于 Read View 中的 `min_trx_id` 值,表示这个版本的记录是在创建 Read View **前**已经提交的事务生成的,所以该版本的记录对当前事务**可见**。 + +- 如果记录的 trx_id 值大于等于 Read View 中的 `max_trx_id` 值,表示这个版本的记录是在创建 Read View **后**才启动的事务生成的,所以该版本的记录对当前事务**不可见**。 + +- 如果记录的 trx_id 值在 Read View 的 min_trx_id和max_trx_id之间,需要判断 trx_id 是否在 m_ids 列表中: + +- - 如果记录的 trx_id **在** `m_ids` 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务**不可见**。 + - 如果记录的 trx_id **不在** `m_ids`列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务**可见**。 + +**这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。** + + + +### 数据库的三大范式,可以反范式吗?(2023 美团) + +数据库的三大范式是数据库设计的基本原则,主要包括: + +1. **第一范式(1NF)** :数据表中的每一列都是不可分割的最小单元,也就是属性值是原子性的。 +2. **第二范式(2NF)**:在第一范式的基础上,要求数据表中的每一列都与主键相关,也就是说非主键列必须完全依赖于主键,不能只依赖主键的一部分(针对联合主键)。 +3. **第三范式(3NF)**:在第二范式的基础上,要求一个数据表中不包含已在其他表中已包含的非主键信息,也就是说,非主键列必须直接依赖于主键,不能存在传递依赖。 + +> 传递依赖进步一解释:在一个数据库表中,如果存在一个非主键列B,它不直接依赖于该表的主键列A,而是依赖于另一个非主键列C,而这个非主键列C又依赖于主键列A,那么就可以说,非主键列B对主键列A存在传递依赖。 +> +> 例如,假设我们有一个包含学生ID(主键)、学生姓名、班级名称和班主任姓名的表。在这个表中,学生姓名直接依赖于学生ID,班级名称也直接依赖于学生ID,但是班主任姓名并不直接依赖于学生ID,而是依赖于班级名称(因为一个班级对应一个班主任)。在这种情况下,我们就可以说,班主任姓名对学生ID存在传递依赖。 +> +> 为了满足第三范式(3NF),我们需要消除这种传递依赖。具体来说,我们可以将原表拆分为两个表,一个表包含学生ID、学生姓名和班级名称,另一个表包含班级名称和班主任姓名。这样,每个表中的非主键列都直接依赖于主键,不存在传递依赖,从而满足第三范式。 + +关于反范式,是的,数据库设计可以反范式。反范式设计是为了优化数据库性能,通过增加冗余数据或者组合数据,减少复杂的数据查询,提高数据读取性能。 + +> 反范式:是数据库设计中的一种策略,通过引入数据冗余或合并表来优化读取性能,但可能会增加数据管理的复杂性和数据一致性的风险。 +> +> 在理想的范式设计中,为了减少数据冗余和避免数据异常(如插入异常、更新异常和删除异常),一般会将数据分解到多个相关的表中。然而,这种设计可能会导致查询性能降低,因为需要执行多表连接操作以获取所需的数据。 +> +> 反范式设计通过以下方式优化读取性能: +> +> 1. **数据冗余**:在多个表中存储相同的数据,以减少多表连接操作。这可能会导致数据更新更复杂,因为需要在多个位置更新相同的数据。 +> 2. **数据预计算**:将经常需要计算的数据预先计算并存储起来,如总数、平均数等。 +> 3. **表合并**:将多个表合并为一个表,以减少多表连接操作。这可能会导致数据冗余,但可以提高查询性能。 +> +> 虽然反范式设计可以优化读取性能,但也会带来数据管理的复杂性和数据一致性的风险。因此,是否使用反范式设计需要根据具体的应用场景和性能需求进行权衡。 + + + ## ♻️ JVM ### 堆是如何管理内存的(2023 快手) @@ -1371,7 +1613,36 @@ Dubbo 的请求处理流程如下: 跨语言,可以选择 gRPC 或 Thrift。 ## 🌐 计算机网络 +### 服务端出现大量 close_wait 状态,可能的情况?(2023美团) + +`CLOSE_WAIT`状态通常意味着你的程序在关闭连接时有一些问题,或者说,它没有正确地关闭套接字连接。这通常发生在程序接收到了服务端的完成(FIN)信号,但是程序自身没有正确地关闭套接字,或者没有在适当的时间内关闭。当这种情况发生时,你会看到大量的连接处于`CLOSE_WAIT`状态。 + +导致大量CLOSE_WAIT状态的可能情况有很多,以下是一些可能的情况: + +1. **应用程序故障**:应用程序可能没有正确地关闭连接。例如,应用程序可能在完成数据交换后忘记关闭连接,或者应用程序可能由于错误而无法关闭连接。这是最常见的原因。 +2. **网络问题**:如果网络环境不稳定,可能导致部分TCP连接无法正常关闭,从而导致'close_wait'状态。网络故障**:网络连接问题可能会导致套接字无法正确关闭,从而导致大量的CLOSE_WAIT状态。例如,网络延迟可能导致FIN包被延迟发送,或者网络丢包可能导致FIN包丢失,这都可能导致连接无法正确关闭。 +3. **系统资源不足**:系统资源不足可能会导致应用程序无法创建新的套接字,或者无法为新的连接分配足够的内存,这都可能导致大量的`CLOSE_WAIT`状态。 +4. **服务端负载过高**:服务器负载过高可能会导致应用程序无法及时处理所有的连接,特别是在需要关闭的连接上,从而导致这些连接长时间处于`CLOSE_WAIT`状态。 +5. **服务端代码中有一个bug**: 服务器代码中的错误可能导致它意外关闭连接。 这也可能导致新请求处于等待状态。 +6. **服务端有硬件问题**: 硬件问题(如内存泄漏或CPU瓶颈)也可能导致服务器关闭连接。 这可能导致新请求处于等待状态。 +7. **客户端频繁断开连接**:如果客户端频繁地建立和断开连接,而服务端处理这些连接的速度不够快,也可能导致大量的'close_wait'连接。另外,如果客户端频繁地建立和断开连接,那么即使服务端可以及时处理所有的连接,也可能会因为网络延迟等问题,导致大量的`CLOSE_WAIT`状态。 +8. **TCP连接没有正确地被结束**:当一个TCP连接要结束时,通常需要双方都发送一个“FIN”数据包来表示他们不再发送数据,并等待对方的确认。关闭系统调用是应用程序向操作系统发出的指令,要求关闭套接字并释放资源。客户端的“FIN”数据包,但没有调用关闭系统调用,那么服务器就会一直保持在“close_wait”状态,直到应用程序关闭套接字 + +解决这种问题通常需要找出导致大量CLOSE_WAIT状态的原因,并进行相应的修复。例如,如果是应用程序没有正确关闭连接,那么可能需要修改应用程序的代码,确保它在完成数据交换后正确关闭连接。如果是网络问题,可能需要检查和修复网络连接。如果是系统资源不足,可能需要增加系统资源或优化应用程序以减少资源使用。如果是服务器负载过高,可能需要增加服务器资源或优化服务器配置来降低负载。 + +> 参考: +> +> 1. https://stackoverflow.com/questions/21033104/close-wait-state-in-server 服务器中的CLOSE_WAIT状态 +> 2. https://superuser.com/questions/173535/what-are-close-wait-and-time-wait-states 什么是CLOSE_WAITTIME_WAIT状态? +> 3. https://learn.microsoft.com/en-us/answers/questions/337518/tcp-connections-locked-in-close-wait-status-with-i +> 4. https://www.cnblogs.com/grey-wolf/p/10936657.html +> 5. https://juejin.cn/post/6844903734300901390#heading-6 +> 6. https://www.thegeekdiary.com/high-number-of-connections-is-close_wait-state-in-netstat-command-output/ 为什么"netstat"输出显示许多连接处于CLOSE_WAIT状态? + + + ### http协议的报文的格式有了解吗? + ![](./giant_images/640.webp) HTTP 的请求报文分为三个部分: @@ -1438,7 +1709,152 @@ TCP(传输控制协议)连接建立后,如果客户端下线或断开, >9. https://www.howtogeek.com/134132/how-to-use-traceroute-to-identify-network-problems/ >10. https://www.fortinet.com/resources/cyberglossary/traceroutes + + +### 如果同时有大量客户并发建立连接,服务器端有什么机制进行处理?(2023 字节提前批) + +服务器端处理大量并发连接的机制主要有以下几种: + +1. **多进程和多线程**:每当接收到新的客户端连接时,服务器都会分配一个新的进程或线程去处理这个连接。这可以确保服务器能够同时处理多个客户端连接。但是,如果连接数太多,过多的线程或进程可能会导致系统资源的过度使用,影响服务器的性能。 +2. **事件驱动(Event-driven)或异步I/O(Async I/O)**:在这种模型中,服务器一次接收多个连接请求,然后通过事件循环(Event Loop)异步地处理这些连接。这样可以避免为每个连接创建新的线程或进程,从而提高性能。 +3. **负载均衡(Load Balancing)**:如果服务器无法处理所有的并发连接,可以使用负载均衡器将连接请求分配到多个服务器上。这可以分散服务器的负载,提高整体的处理能力。 + +> 这里要区分出现了大量TIME_WAIT或者CLOSE_WAIT +> +> 大量的 `TIME_WAIT` 或 `CLOSE_WAIT`,它们是 TCP 连接关闭过程中的状态。`TIME_WAIT` 表示连接已被本地关闭,正在等待足够的时间以确保远程TCP接收到连接终止的通知。`CLOSE_WAIT` 则表示远程TCP连接关闭或接收到了连接终止的通知。 + + + +### HTTP1.1, 2.0区别(2023 百度提前批,2023 字节客户端) + +HTTP/1.1 和 HTTP/2 是两个不同的协议版本,用于在客户端和服务器之间进行通信。以下是它们之间的一些主要区别: + +1. **传输方式**:HTTP/1.1 使用序列化的文本数据来传输请求和响应,而 HTTP/2 使用二进制格式的数据帧来传输请求和响应。这种二进制格式能够更高效地传输数据,减少了数据传输的大小和延迟。 +2. **多路复用**:HTTP/1.1 在一个连接上只能处理一个请求和响应,而 HTTP/2 支持在同一个连接上同时处理多个请求和响应。这种多路复用的特性可以减少连接建立和关闭的开销,提高了性能和效率。 +3. **头部压缩**:HTTP/2 引入了头部压缩机制,使用 HPACK 算法对请求和响应的头部进行压缩。这种压缩机制可以减少数据传输的大小,提高网络传输效率。 +4. **服务器推送**:HTTP/2 支持服务器主动推送资源给客户端,以提高页面加载性能。服务器可以在发送请求的同时,将一些可能需要的资源主动推送给客户端,减少了客户端发送额外请求的需要。 +5. **流控制**:HTTP/2 引入了流控制机制,允许客户端和服务器控制数据流的速率,以避免数据传输过程中的拥塞和溢出。 总体而言,HTTP/2 相对于 HTTP/1.1 来说,在性能和效率上有了很大的提升。它的多路复用、头部压缩、服务器推送等特性使得网络传输更加高效,能够更快地加载网页和提供更好的用户体验。 +6. **流量控制**:HTTP/1.1 使用 TCP 层的流量控制机制,而 HTTP/2 在应用层实现了自己的流量控制机制。这使得 HTTP/2 可以更好地控制数据的传输速率,避免缓冲区溢出的问题。 + + + +总的来说,HTTP1.1和HTTP2.0是用于在web服务器和客户端之间传输数据的协议。HTTP2.0比HTTP1.1更快、更可靠,因为它使用二进制编码、多路复用和并发。HTTP1.1为每个TCP连接加载一个请求,这可能会导致网络延迟和页面加载速度减慢。HTTP2.0可以并行处理多个请求和响应,这可以提高web性能和用户体验。HTTP2.0与HTTP1.1语义(如方法和头)兼容,因此web应用程序仍然可以使用新协议正常运行。 + +> 参考: +> +> 1. https://www.cloudflare.com/zh-cn/learning/performance/http2-vs-http1.1/ HTTP/2 与 HTTP/1.1:它们如何影响 Web 性能? +> 2. https://www.baeldung.com/cs/http-versions HTTP: 1.0 vs. 1.1 vs 2.0 vs. 3.0 +> 3. https://www.digitalocean.com/community/tutorials/http-1-1-vs-http-2-what-s-the-difference 【推荐阅读】HTTP/1.1与HTTP/2:有什么区别? + + + +### tcp 是怎么实现可靠传输的?(2023 字节) + +- 序列号与确认应答:TCP将每个发送的数据包进行编号(序列号),接收方通过发送确认应答(ACK)来告知发送方已成功接收到数据。如果发送方在一定时间内未收到确认应答,会进行超时重传。 +- 数据校验:TCP使用校验和来验证数据的完整性。接收方会计算接收到的数据的校验和,并与发送方发送的校验和进行比较,以检测数据是否在传输过程中发生了错误。 +- 窗口控制:TCP使用滑动窗口机制来控制发送方和接收方之间的数据流量。发送方根据接收方的处理能力和网络状况来调整发送的数据量,接收方则通过窗口大小来告知发送方可以接收的数据量。 +- 重传机制:如果发送方未收到确认应答或接收方检测到数据错误,TCP会进行重传。发送方会根据超时时间或接收方的冗余确认来触发重传,以确保数据的可靠传输。 +- 拥塞控制:TCP使用拥塞控制算法来避免网络拥塞。通过动态调整发送速率和窗口大小,TCP可以根据网络的拥塞程度来进行适当的调整,以提高网络的利用率和稳定性。 + + + +### IP数据报的报头有哪些字段?(2023 字节) + +![](./giant_images/ipv4-head.webp) + + + +### IP 报文的TTL是什么意思?(2023 字节) + +指定数据报在网络中可以经过的最大路由器跳数。每当数据报经过一个路由器时,该字段的值会减少1。当TTL的值为0时,路由器将丢弃该数据报并发送ICMP的时间超过消息给源主机。 + +TTL的主要目的是防止数据报在网络中无限循环,避免由于路由环路或其他问题导致的数据报无法正常到达目的地。通过限制数据报的最大跳数,TTL可以确保数据报在有限的时间内能够到达目标主机或被丢弃,以避免网络资源的浪费和延迟。 + + + +### 如何实现一个可靠UDP?(2023 美团) + +可以通过以下方法实现一个可靠的UDP: + +1. 应用层协议设计:在应用层上设计一个自定义的协议,通过在UDP数据包中添加序列号、校验和、确认应答等字段来实现可靠性。发送方发送数据时,需要等待接收方的确认应答,如果没有收到确认应答或者收到了错误的确认应答,就进行重传。 +2. 超时重传:发送方在发送数据后设置一个超时时间,如果在超时时间内没有收到确认应答,就进行重传。接收方在接收到数据后发送确认应答,如果发送方没有收到确认应答,就进行重传。 +3. 数据校验:在发送方和接收方都进行数据校验,例如使用校验和算法(如CRC)来检测数据是否被篡改。如果校验失败,就进行重传。 +4. 应答机制:发送方发送数据后,接收方需要发送确认应答来告知发送方数据已经接收成功。如果发送方没有收到确认应答,就进行重传。 +5. 流量控制和拥塞控制:在发送方和接收方之间进行流量控制和拥塞控制,以防止数据包的丢失和网络拥塞。 + + + +### ping命令是什么协议?(2023 美团) + +icmp 协议 + +同个子网下的主机 A 和 主机 B,主机 A 执行`ping` 主机 B 后,我们来看看其间发送了什么? + +![ping-0](./giant_images/ping-0.webp) + +ping 命令执行的时候,源主机首先会构建一个 **ICMP 回送请求消息**数据包。 + +ICMP 数据包内包含多个字段,最重要的是两个: + +- 第一个是**类型**,对于回送请求消息而言该字段为 `8`; +- 另外一个是**序号**,主要用于区分连续 ping 的时候发出的多个数据包。 + +每发出一个请求数据包,序号会自动加 `1`。为了能够计算往返时间 `RTT`,它会在报文的数据部分插入发送时间。 + +![ping-1](./giant_images/ping-1.webp) + +然后,由 ICMP 协议将这个数据包连同地址 192.168.1.2 一起交给 IP 层。IP 层将以 192.168.1.2 作为**目的地址**,本机 IP 地址作为**源地址**,**协议**字段设置为 `1` 表示是 `ICMP`协议,再加上一些其他控制信息,构建一个 `IP` 数据包。 + +![ping-2](./giant_images/ping-2.webp) + +接下来,需要加入 `MAC` 头。如果在本地 ARP 映射表中查找出 IP 地址 192.168.1.2 所对应的 MAC 地址,则可以直接使用;如果没有,则需要发送 `ARP` 协议查询 MAC 地址,获得 MAC 地址后,由数据链路层构建一个数据帧,目的地址是 IP 层传过来的 MAC 地址,源地址则是本机的 MAC 地址;还要附加上一些控制信息,依据以太网的介质访问规则,将它们传送出去。 + +![ping-3](./giant_images/ping-3.webp) + +主机 `B` 收到这个数据帧后,先检查它的目的 MAC 地址,并和本机的 MAC 地址对比,如符合,则接收,否则就丢弃。 + +接收后检查该数据帧,将 IP 数据包从帧中提取出来,交给本机的 IP 层。同样,IP 层检查后,将有用的信息提取后交给 ICMP 协议。 + +主机 `B` 会构建一个 **ICMP 回送响应消息**数据包,回送响应数据包的**类型**字段为 `0`,**序号**为接收到的请求数据包中的序号,然后再发送出去给主机 A。 + +![ping-4](./giant_images/ping-4.webp) + +在规定的时候间内,源主机如果没有接到 ICMP 的应答包,则说明目标主机不可达;如果接收到了 ICMP 回送响应消息,则说明目标主机可达。 + +此时,源主机会检查,用当前时刻减去该数据包最初从源主机上发出的时刻,就是 ICMP 数据包的时间延迟。 + +针对上面发送的事情,总结成了如下图: + +![ping-5](./giant_images/ping-5.webp) + +当然这只是最简单的,同一个局域网里面的情况。如果跨网段的话,还会涉及网关的转发、路由器的转发等等。 + +但是对于 ICMP 的头来讲,是没什么影响的。会影响的是根据目标 IP 地址,选择路由的下一跳,还有每经过一个路由器到达一个新的局域网,需要换 MAC 头里面的 MAC 地址。 + +说了这么多,可以看出 ping 这个程序是**使用了 ICMP 里面的 ECHO REQUEST(类型为 8 ) 和 ECHO REPLY (类型为 0)**。 + + + +### 了解哪些网络错误码吗?(2023 美团) + +以下是其中一些常见的错误码及其含义: + +- 400 Bad Request:请求无效或不完整。 +- 401 Unauthorized:未经授权,需要身份验证。 +- 403 Forbidden:服务器拒绝请求,没有访问权限。 +- 404 Not Found:请求的资源不存在。 +- 500 Internal Server Error:服务器内部错误。 +- 502 Bad Gateway:网关错误,作为代理或网关的服务器从上游服务器接收到无效的响应。 +- 503 Service Unavailable:服务不可用,服务器暂时过载或维护中。 +- 504 Gateway Timeout:网关超时,作为代理或网关的服务器在等待上游服务器响应时超时。 +- 505 HTTP Version Not Supported:不支持的HTTP协议版本。 + + + + + ## 🖥️操作系统 + ### linux有几种IO模型(2023 阿里) > 参考:https://linyunwen.github.io/2022/01/02/linux-io-model/ @@ -1523,6 +1939,128 @@ brk和mmap系统调用,brk申请堆内存,mmap分配文件映射区和匿名 +### 死锁的必要条件是什么?(2023 百度提前批) + +死锁的必要条件通常被归结为四个基本条件,它们都必须同时满足才能发生死锁。这四个条件也被称为死锁的四个"Coffman"条件,得名于1971年由E.G. Coffman Jr.等人在其论文中首次提出。这四个条件包括: + +1. **互斥**:资源至少有一个是不能被共享的,即在一段时间内只能被一个进程使用。如果其他进程请求该资源,那么请求进程必须等待直到占有资源的进程已经释放。 +2. **占有并等待**:一个进程因请求占用资源而被阻塞时,对已获得的资源保持不放。 +3. **非抢占**:进程已获得的资源在未使用完前不能被其他进程强行剥夺,只能由拥有资源的进程主动释放。 +4. **循环等待**:在发生死锁时,必然存在一个进程—资源的环形链,即进程集合{P0, P1, …, Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已经被P0占用的资源。 + +只有在满足这四个条件的情况下,才可能出现死锁。 + + + +### top命令有哪些参数,说一下(2023 美团) + +top命令是一个用于实时监控系统资源和进程的命令,它可以显示当前运行的进程、CPU使用情况、内存使用情况等信息。以下是一些常用的top命令参数: + +1. -d <秒数>:指定top命令刷新的时间间隔,默认为3秒。 +2. -n <次数>:指定top命令执行的次数后自动退出。 +3. -p <进程ID>:指定要监控的进程ID。 +4. -u <用户名>:只显示指定用户名的进程。 +5. -s <排序字段>:按指定字段对进程进行排序,常见的字段有cpu(CPU使用率)、mem(内存使用率)等。 +6. -H:显示进程的层次关系。 +7. -i:只显示运行中的进程,不显示僵尸进程。 + + + +### 怎么显示线程?(2023 美团) + +在Linux中,可以使用以下命令来显示线程: + +- top命令:在top命令的默认显示中,可以看到每个进程的线程数(Threads列)。例如,执行top命令后,按下"Shift + H"键可以切换到线程视图,显示每个进程的线程信息。 +- ps命令:通过ps命令结合选项来显示线程。例如,使用"ps -eLf"命令可以显示系统中所有线程的详细信息。 + + + +### 虚拟地址是怎么转化到物理地址的?(2023 字节) + +![](./giant_images/virtual-addresses-1.webp) + +虚拟地址到物理地址的转换是通过操作系统中的内存管理单元(MMU,Memory Management Unit)来完成的。下面是一般的虚拟地址到物理地址转换过程: + +- 程序发出内存访问请求时,使用虚拟地址进行访问。 +- 虚拟地址被传递给MMU进行处理。 +- MMU中的地址映射表(页表)被用来将虚拟地址转换为物理地址。页表是一种数据结构,用于存储虚拟地址和物理地址之间的映射关系。 +- MMU根据页表中的映射关系,将虚拟地址转换为对应的物理地址。 +- 转换后的物理地址被传递给内存系统,用于实际的内存访问操作。 + + + +### 页表是怎么构成的?(2023 字节) + +页表是一种数据结构,用于存储虚拟地址和物理地址之间的映射关系。多级页表将页表分为多个层级,每个层级的页表项存储下一级页表的物理地址。通过多级索引,可以逐级查找,最终找到对应的物理页。 + +对于 64 位的系统,主要有四级目录,分别是: + +- 全局页目录项 PGD +- 上层页目录项 PUD +- 中间页目录项 PMD +- 页表项 PTE + +![](./giant_images/virtual-addresses-2.webp) + + +### 共享内存是怎么实现的?(2023 字节) + +共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。 + +![](./giant_images/virtual-addresses-3.webp) + + + +### 操作系统原子操作怎么实现的?(2023 字节) + +操作系统中的原子性操作是通过硬件和软件的支持来实现的。在多核处理器上,原子性操作需要保证在多个核心之间的并发执行中的正确性和一致性。 + +硬件层面上,现代处理器提供了一些特殊的指令或机制来支持原子性操作,例如原子交换(atomic exchange)、原子比较并交换(atomic compare-and-swap)等。这些指令能够在执行期间禁止中断或其他核心的干扰,确保操作的原子性。 + +软件层面上,操作系统提供了一些原子性操作的接口或函数,例如原子操作函数(atomic operation),它们使用了硬件提供的原子性指令来实现原子性操作。这些函数通常是在内核态下执行,可以保证在多个进程或线程之间的原子性。 + +操作系统还可以使用锁机制来实现原子性操作。例如,互斥锁(mutex)可以用来保护共享资源的访问,只有持有锁的进程或线程可以访问共享资源,其他进程或线程需要等待锁的释放。通过锁的机制,可以保证对共享资源的原子性操作。 + + + +## 数据结构 + +### 红黑树说一下,跳表说一下?(2023 美团) + +红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,它在插入和删除操作后能够通过旋转和重新着色来保持树的平衡。红黑树的特点如下: + +1. 每个节点都有一个颜色,红色或黑色。 +2. 根节点是黑色的。 +3. 每个叶子节点(NIL节点)都是黑色的。 +4. 如果一个节点是红色的,则它的两个子节点都是黑色的。 +5. 从根节点到叶子节点或空子节点的每条路径上,黑色节点的数量是相同的。 + +红黑树通过这些特性来保持树的平衡,确保最长路径不超过最短路径的两倍,从而保证了在最坏情况下的搜索、插入和删除操作的时间复杂度都为O(logN)。 + +![red-black-tree](./giant_images/red-black-tree.webp) + +跳表(Skip List)是一种基于链表的数据结构,它通过添加多层索引来加速搜索操作。 + +![sorted-sets-skiplist](./giant_images/sorted-sets-skiplist.webp) + +跳表的特点如下: + +1. 跳表中的数据是有序的。 +2. 跳表中的每个节点都包含一个指向下一层和右侧节点的指针。 + +跳表通过多层索引的方式来加速搜索操作。最底层是一个普通的有序链表,而上面的每一层都是前一层的子集,每个节点在上一层都有一个指针指向它在下一层的对应节点。这样,在搜索时可以通过跳过一些节点,直接进入目标区域,从而减少搜索的时间复杂度。 + +跳表的平均搜索、插入和删除操作的时间复杂度都为O(logN),与红黑树相比,跳表的实现更加简单,但空间复杂度稍高。跳表常用于需要高效搜索和插入操作的场景,如数据库、缓存等。 + + + +场景: + +- epoll 用了红黑树来保存监听的 socket +- redis 用了跳表来实现 zset + + + ## 🎨 设计模式 ### 适配器模式、装饰器模式、代理模式有什么区别?(2023小红书) - **适配器模式**:适配器模式就像是一个电源适配器,它允许两个不兼容的接口可以一起工作。例如,一个类的接口与客户端代码需要的接口不一致时,可以通过创建一个适配器类来转换接口,使得客户端代码能够利用现有的类。 @@ -1535,25 +2073,7 @@ brk和mmap系统调用,brk申请堆内存,mmap分配文件映射区和匿名 ## 🖼️场景题 -### 服务端出现大量 close_wait 状态,可能的情况?(2023美团) -`CLOSE_WAIT`状态通常意味着你的程序在关闭连接时有一些问题,或者说,它没有正确地关闭套接字连接。这通常发生在程序接收到了服务端的完成(FIN)信号,但是程序自身没有正确地关闭套接字,或者没有在适当的时间内关闭。当这种情况发生时,你会看到大量的连接处于`CLOSE_WAIT`状态。 - -导致大量CLOSE_WAIT状态的可能情况有很多,以下是一些可能的情况: -1. **应用程序故障**:应用程序可能没有正确地关闭连接。例如,应用程序可能在完成数据交换后忘记关闭连接,或者应用程序可能由于错误而无法关闭连接。这是最常见的原因。 -2. **网络故障**:网络连接问题可能会导致套接字无法正确关闭,从而导致大量的CLOSE_WAIT状态。 -3. **系统资源不足**:如果系统资源(如文件描述符)不足,可能会导致套接字无法关闭,从而产生大量的CLOSE_WAIT状态。 -4. **服务端负载过高**:如果服务器的负载过高,可能会导致套接字无法及时关闭,从而产生大量的CLOSE_WAIT状态。 -5. **服务端代码中有一个bug**: 服务器代码中的错误可能导致它意外关闭连接。 这也可能导致新请求处于等待状态。 -6. **服务端有硬件问题**: 硬件问题(如内存泄漏或CPU瓶颈)也可能导致服务器关闭连接。 这可能导致新请求处于等待状态。 - -解决这种问题通常需要找出导致大量CLOSE_WAIT状态的原因,并进行相应的修复。例如,如果是应用程序没有正确关闭连接,那么可能需要修改应用程序的代码,确保它在完成数据交换后正确关闭连接。如果是网络问题,可能需要检查和修复网络连接。如果是系统资源不足,可能需要增加系统资源或优化应用程序以减少资源使用。如果是服务器负载过高,可能需要增加服务器资源或优化服务器配置来降低负载。 -> 参考: -> 1. https://stackoverflow.com/questions/21033104/close-wait-state-in-server 服务器中的CLOSE_WAIT状态 -> 2. https://superuser.com/questions/173535/what-are-close-wait-and-time-wait-states 什么是CLOSE_WAITTIME_WAIT状态? -> 3. https://learn.microsoft.com/en-us/answers/questions/337518/tcp-connections-locked-in-close-wait-status-with-i -> 4. https://www.cnblogs.com/grey-wolf/p/10936657.html -> 5. https://juejin.cn/post/6844903734300901390#heading-6 ### Java 程序运行了一周,发现老年代内存溢出,分析一下?(2023美团) 老年代内存溢出表现为java.lang.OutOfMemoryError: Java heap space,通常是因为Java堆内存中长期存活的对象占用的空间过大,导致内存无法分配。下面从浅入深分析可能的原因和解决方法 @@ -1780,6 +2300,22 @@ jstack > thread_dump.txt 4. 关联操作系统线程:要确定哪些线程正在使用大量CPU资源,我们需要将Java线程与操作系统线程关联起来。在Linux上,可以使用`top -H -p `命令查看特定进程的线程CPU使用情况。在Windows上,可以使用Process Explorer或Process Monitor等工具查看线程CPU使用情况。将操作系统线程ID转换为16进制,然后与thread_dump.txt中的线程进行匹配。 5. 定位问题代码 + + +### 两个包含5亿URL的文件,如何找到两个文件的重复URL,内存只有4G(2023 百度提前批) + +处理两个包含5亿个URL的文件来找出重复URL,是一个内存和计算密集型的任务。在有限的内存下,我们需要考虑使用一些高效的数据处理技术,如外部排序和分布式计算。这里,我们将主要探讨使用基于硬盘的外部排序和哈希映射方法。 + +我们可以按以下步骤进行: + +**步骤1:** 预处理并切割文件 首先,由于文件的数据量太大,无法一次性装入内存,我们需要将每个文件切分成更小的分区。我们可以将URL按照一定的哈希函数分桶(如URL的哈希值对10000取余),将相同哈希值的URL写入到同一个文件中,这样可以保证相同的URL一定在同一个文件中。 + +**步骤2:** 逐一处理切割后的文件 将切割后的小文件依次加载进内存中,对每个文件使用HashSet去重,然后保存到一个临时文件。 + +**步骤3:** 两两对比找出重复的URL 最后,我们再把第一个文件和第二个文件相同哈希值的临时文件分别加载到内存,然后用一个HashSet保存第一个文件的URL,遍历第二个文件的URL,如果在HashSet中找到就说明是重复的URL。 + +以上步骤需要进行的IO操作相当多,会消耗较大的时间。这是因为我们是在内存不足的情况下处理大数据问题,所以要通过牺牲时间来换取空间。在实际的大数据处理中,我们通常会使用更高效的工具如Spark、Hadoop等分布式处理工具来进行处理。 + ## 其他 ### 讲一讲cms? 内容管理系统(英语:content management system,缩写为 CMS)是指在一个合作模式下,用于管理工作流程的一套制度。该系统可应用于手工操作中,也可以应用到电脑或网络里。作为一种中央储存器(central repository),内容管理系统可将相关内容集中储存并具有群组管理、版本控制等功能。版本控制是内容管理系统的一个主要优势。 @@ -1789,7 +2325,7 @@ jstack > thread_dump.txt > 参考: > https://zh.wikipedia.org/wiki/%E5%86%85%E5%AE%B9%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F -### DTS 了解过吗? +### DTS 了解过吗?(2023 小红书【数据库dts方向】) 数字剧院系统(DTS,Digital Theater Systems)由DTS公司(DTS Inc.,NASDAQ:DTSI)开发,为多声道音频格式中的一种,广泛应用于DVD音效上。其最普遍的格式为5.1声道。与杜比数字为主要竞争对手。要实现DTS音效输出,需在硬件上及软件上符合DTS的规格,多数会在产品上标示DTS的商标。 > 参考: @@ -1805,4 +2341,5 @@ OceanBase是一个开源的分布式关系型数据库,完全兼容MySQL。它 ## 💦 算法汇总 -1. [二叉树的公共祖先(2023 快手)](https://leetcode.cn/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/) \ No newline at end of file +1. [二叉树的公共祖先(2023 快手)](https://leetcode.cn/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/) +1. [字符串相乘(2023 美团)](https://leetcode.cn/problems/multiply-strings/) \ No newline at end of file diff --git a/src/Java/eightpart/giant_images/ipv4-head.webp b/src/Java/eightpart/giant_images/ipv4-head.webp new file mode 100644 index 00000000..2c52950d Binary files /dev/null and b/src/Java/eightpart/giant_images/ipv4-head.webp differ diff --git a/src/Java/eightpart/giant_images/mvcc-1.webp b/src/Java/eightpart/giant_images/mvcc-1.webp new file mode 100644 index 00000000..5828002a Binary files /dev/null and b/src/Java/eightpart/giant_images/mvcc-1.webp differ diff --git a/src/Java/eightpart/giant_images/mvcc-2.webp b/src/Java/eightpart/giant_images/mvcc-2.webp new file mode 100644 index 00000000..74802b84 Binary files /dev/null and b/src/Java/eightpart/giant_images/mvcc-2.webp differ diff --git a/src/Java/eightpart/giant_images/mvcc-3.webp b/src/Java/eightpart/giant_images/mvcc-3.webp new file mode 100644 index 00000000..259b0953 Binary files /dev/null and b/src/Java/eightpart/giant_images/mvcc-3.webp differ diff --git a/src/Java/eightpart/giant_images/ping-0.webp b/src/Java/eightpart/giant_images/ping-0.webp new file mode 100644 index 00000000..72000f28 Binary files /dev/null and b/src/Java/eightpart/giant_images/ping-0.webp differ diff --git a/src/Java/eightpart/giant_images/ping-1.webp b/src/Java/eightpart/giant_images/ping-1.webp new file mode 100644 index 00000000..d8ba70af Binary files /dev/null and b/src/Java/eightpart/giant_images/ping-1.webp differ diff --git a/src/Java/eightpart/giant_images/ping-2.webp b/src/Java/eightpart/giant_images/ping-2.webp new file mode 100644 index 00000000..3d515416 Binary files /dev/null and b/src/Java/eightpart/giant_images/ping-2.webp differ diff --git a/src/Java/eightpart/giant_images/ping-3.webp b/src/Java/eightpart/giant_images/ping-3.webp new file mode 100644 index 00000000..3bdd835c Binary files /dev/null and b/src/Java/eightpart/giant_images/ping-3.webp differ diff --git a/src/Java/eightpart/giant_images/ping-4.webp b/src/Java/eightpart/giant_images/ping-4.webp new file mode 100644 index 00000000..43edbec6 Binary files /dev/null and b/src/Java/eightpart/giant_images/ping-4.webp differ diff --git a/src/Java/eightpart/giant_images/ping-5.webp b/src/Java/eightpart/giant_images/ping-5.webp new file mode 100644 index 00000000..cd1c2af2 Binary files /dev/null and b/src/Java/eightpart/giant_images/ping-5.webp differ diff --git a/src/Java/eightpart/giant_images/red-black-tree.webp b/src/Java/eightpart/giant_images/red-black-tree.webp new file mode 100644 index 00000000..ecf1b6c4 Binary files /dev/null and b/src/Java/eightpart/giant_images/red-black-tree.webp differ diff --git a/src/Java/eightpart/giant_images/sorted-sets-skiplist.webp b/src/Java/eightpart/giant_images/sorted-sets-skiplist.webp new file mode 100644 index 00000000..719c1220 Binary files /dev/null and b/src/Java/eightpart/giant_images/sorted-sets-skiplist.webp differ diff --git a/src/Java/eightpart/giant_images/virtual-addresses-1.webp b/src/Java/eightpart/giant_images/virtual-addresses-1.webp new file mode 100644 index 00000000..dfe0602b Binary files /dev/null and b/src/Java/eightpart/giant_images/virtual-addresses-1.webp differ diff --git a/src/Java/eightpart/giant_images/virtual-addresses-2.webp b/src/Java/eightpart/giant_images/virtual-addresses-2.webp new file mode 100644 index 00000000..47207fe5 Binary files /dev/null and b/src/Java/eightpart/giant_images/virtual-addresses-2.webp differ diff --git a/src/Java/eightpart/giant_images/virtual-addresses-3.webp b/src/Java/eightpart/giant_images/virtual-addresses-3.webp new file mode 100644 index 00000000..2632cea4 Binary files /dev/null and b/src/Java/eightpart/giant_images/virtual-addresses-3.webp differ diff --git a/src/Java/eightpart/messageQueue.md b/src/Java/eightpart/messageQueue.md index 862dd307..6b1a8db3 100644 --- a/src/Java/eightpart/messageQueue.md +++ b/src/Java/eightpart/messageQueue.md @@ -403,5 +403,3 @@ public class Recv { > 别忘了在 `RocketMQ` 中,**一个队列只会被一个消费者消费** ,如果你仅仅是增加消费者实例就会出现我一开始给你画架构图的那种情况 ![image-20220617111103393](./personal_images/image-20220617111103393.webp) - -## \ No newline at end of file diff --git a/src/Java/eightpart/mysql.md b/src/Java/eightpart/mysql.md index 35af5ebe..653944fb 100644 --- a/src/Java/eightpart/mysql.md +++ b/src/Java/eightpart/mysql.md @@ -21,6 +21,8 @@ category: - **执行器:** 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。 - **插件式存储引擎** : 主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。 + + ### ⭐MyISAM 和 InnoDB 的区别是什么? 除了6,都是InnoDB支持,前者不支持 diff --git a/src/Java/eightpart/network.md b/src/Java/eightpart/network.md index 449ff625..a801ba54 100644 --- a/src/Java/eightpart/network.md +++ b/src/Java/eightpart/network.md @@ -1219,7 +1219,54 @@ https://stackoverflow.com/questions/46212623/why-tcp-termination-need-4-way-hand - 内核的全局hash表也可以保存连接信息,用于没有listen的情况下完成三次握手 - 所以,无论是否执行listen,连接信息总有地方保存,这使得连接得以建立 -### TIME_WAIT的作用! +### CLOSE_WAIT的作用 + +远程 TCP 对等方(另一端的计算机或服务器)已经发送了一个 FIN 包,要求关闭连接。这意味着对等方已经完成了数据的发送。本地应用程序一旦接收到这个信号,就会进入 `CLOSE_WAIT` 状态。 + +在此状态下,计算机正在等待本地应用程序关闭套接字连接。也就是说,这是一种“半关闭”的状态,其中远程对等方已经完成了数据的发送,但本地应用程序还未关闭连接。本地应用程序可能仍然需要发送一些数据,或者它可能只是需要一些时间来确认它已经接收到了所有的数据。 + +当本地应用程序完成所有的数据发送,并准备好关闭连接时,它会发送一个 FIN 包给远程对等方,并进入 `LAST_ACK` 状态。然后,它就会关闭连接,并从 `CLOSE_WAIT` 状态转移到 `CLOSED` 状态。 + + + +### 服务器出现大量 CLOSE_WAIT 状态的原因有哪些? + +CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。 + +所以,**当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接**。 + +那什么情况会导致服务端的程序没有调用 close 函数关闭连接?这时候通常需要排查代码。 + +我们先来分析一个普通的 TCP 服务端的流程: + +1. 创建服务端 socket,bind 绑定端口、listen 监听端口 +2. 将服务端 socket 注册到 epoll +3. epoll_wait 等待连接到来,连接到来时,调用 accpet 获取已连接的 socket +4. 将已连接的 socket 注册到 epoll +5. epoll_wait 等待事件发生 +6. 对方连接关闭时,我方调用 close + +可能导致服务端没有调用 close 函数的原因,如下。 + +**第一个原因**:第 2 步没有做,没有将服务端 socket 注册到 epoll,这样有新连接到来时,服务端没办法感知这个事件,也就无法获取到已连接的 socket,那服务端自然就没机会对 socket 调用 close 函数了。 + +不过这种原因发生的概率比较小,这种属于明显的代码逻辑 bug,在前期 read view 阶段就能发现的了。 + +**第二个原因**: 第 3 步没有做,有新连接到来时没有调用 accpet 获取该连接的 socket,导致当有大量的客户端主动断开了连接,而服务端没机会对这些 socket 调用 close 函数,从而导致服务端出现大量 CLOSE_WAIT 状态的连接。 + +发生这种情况可能是因为服务端在执行 accpet 函数之前,代码卡在某一个逻辑或者提前抛出了异常。 + +**第三个原因**:第 4 步没有做,通过 accpet 获取已连接的 socket 后,没有将其注册到 epoll,导致后续收到 FIN 报文的时候,服务端没办法感知这个事件,那服务端就没机会调用 close 函数了。 + +发生这种情况可能是因为服务端在将已连接的 socket 注册到 epoll 之前,代码卡在某一个逻辑或者提前抛出了异常。之前看到过别人解决 close_wait 问题的实践文章,感兴趣的可以看看:[一次 Netty 代码不健壮导致的大量 CLOSE_WAIT 连接原因分析(opens new window)](https://mp.weixin.qq.com/s?__biz=MzU3Njk0MTc3Ng==&mid=2247486020&idx=1&sn=f7cf41aec28e2e10a46228a64b1c0a5c&scene=21#wechat_redirect) + +**第四个原因**:第 6 步没有做,当发现客户端关闭连接后,服务端没有执行 close 函数,可能是因为代码漏处理,或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等。 + +可以发现,**当服务端出现大量 CLOSE_WAIT 状态的连接的时候,通常都是代码的问题,这时候我们需要针对具体的代码一步一步的进行排查和定位,主要分析的方向就是服务端为什么没有调用 close**。 + + + +### TIME_WAIT的作用 ![具体图](./personal_images/5e9a4324cfbf41ff8ca92c73ddb7621d.webp) TCP连接中的TIME_WAIT状态是一个重要的状态,它有几个作用: @@ -1228,7 +1275,7 @@ TCP连接中的TIME_WAIT状态是一个重要的状态,它有几个作用: 2. **让重复的FIN包消逝**:TIME_WAIT状态能持续足够长的时间(一般是4分钟),让网络中可能存在的重复的FIN包消逝。 3. **避免旧的数据包在新的连接中出现**:如果立即关闭后再打开一个相同的TCP连接,那么之前连接留下的数据可能会被误认为是新连接的数据,这会引发错误。TIME_WAIT状态能防止这种情况发生,因为在这个状态期间,相同的TCP连接不能被打开。 -这些作用都是为了保持TCP的可靠性,确保数据的正确传输。 +Or 观看[为什么需要 TIME_WAIT 状态](https://xiaolincoding.com/network/3_tcp/tcp_interview.html#%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-time-wait-%E7%8A%B6%E6%80%81) > 参考: > 1. https://www.baeldung.com/linux/close-socket-time_wait @@ -1245,6 +1292,8 @@ TCP连接中的TIME_WAIT状态是一个重要的状态,它有几个作用: - 启用TCP端口复用(SO_REUSEADDR和SO_REUSEPORT):SO_REUSEADDR可以让你在TIME_WAIT状态下绑定相同的端口,SO_REUSEPORT可以让你在已经打开的端口上启动多个监听。这两个选项都能在某种程度上解决TIME_WAIT导致的端口耗尽问题。 - 优化应用程序的连接管理:如果可能,避免频繁创建和关闭连接。例如,对于HTTP请求,可以使用长连接(Keep-Alive)代替短连接。对于数据库连接,可以使用连接池。 + + ### HTTP 与 HTTPS 有哪些区别? 1. HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。 @@ -1707,14 +1756,80 @@ TCP 连接是由四元组(源IP地址,源端口,目的IP地址,目的端 ***注意:实际上 HTTP/1.1 管道化技术不是默认开启,而且浏览器基本都没有支持,所以后面讨论HTTP/1.1 都是建立在没有使用管道化的前提。*** -### 交换机和路由器的区别? -交换机(Switch)和路由器(Router)都是网络设备,它们在网络中起到不同的作用。 +### 交换机和路由器的区别 + +> 数据包和数据帧的区别。 + +**数据包**和**数据帧**其实都是网络中传输数据的单位,但是它们在不同的网络层级上使用。 + +- **数据包**:在网络层(第三层)上,数据被封装为数据包(Packet)。数据包包含了源IP地址和目标IP地址,这是路由器决定如何转发数据包的关键信息。 +- **数据帧**:在数据链路层(第二层)上,数据被封装为数据帧(Frame)。数据帧包含了源MAC地址和目标MAC地址,这是交换机决定如何转发数据帧的关键信息。 + +可以将数据包和数据帧想象成邮件:数据包就像邮件的地址信息,告诉邮递员应该将邮件送到哪个城市(网络);而数据帧就像邮件的详细地址,告诉邮递员应该将邮件送到哪个房子(设备)。 + +> ARP(地址解析协议)的作用。 + +ARP主要用于将IP地址映射为MAC地址。当一个设备(比如你的电脑)想要向另一个设备(比如你的路由器)发送数据时,它首先需要知道目标设备的MAC地址。但是通常情况下,设备只知道目标的IP地址,所以它需要发送一个ARP请求,询问网络上谁拥有这个IP地址。收到ARP请求的设备如果发现这个IP地址就是自己的,就会回复一个ARP响应,告诉发送设备自己的MAC地址。这样,发送设备就知道了目标设备的MAC地址,就可以开始发送数据了。 + +所以,ARP主要在数据链路层(第二层)上使用,是实现IP地址到MAC地址映射的关键协议。而交换机就是利用了这个映射关系,根据数据帧的目标MAC地址,将数据帧转发到正确的端口。 + +> 当 MAC 地址表找不到指定的 MAC 地址会怎么样? + +地址表中找不到指定的 MAC 地址。这可能是因为具有该地址的设备还没有向交换机发送过包,或者这个设备一段时间没有工作导致地址被从地址表中删除了。 + +这种情况下,交换机无法判断应该把包转发到哪个端口,只能将包转发到除了源端口之外的所有端口上,无论该设备连接在哪个端口上都能收到这个包。 + +这样做不会产生什么问题,因为以太网的设计本来就是将包发送到整个网络的,然后**只有相应的接收者才接收包,而其他设备则会忽略这个包**。 + +有人会说:“这样做会发送多余的包,会不会造成网络拥塞呢?” + +其实完全不用过于担心,因为发送了包之后目标设备会作出响应,只要返回了响应包,交换机就可以将它的地址写入 MAC 地址表,下次也就不需要把包发到所有端口了。 + +局域网中每秒可以传输上千个包,多出一两个包并无大碍。 + +此外,如果接收方 MAC 地址是一个**广播地址**,那么交换机会将包发送到除源端口之外的所有端口。 + +以下两个属于广播地址: + +- MAC 地址中的 `FF:FF:FF:FF:FF:FF` +- IP 地址中的 `255.255.255.255` -**交换机**主要用于连接局域网中的设备,如计算机、打印机等。它工作在**OSI模型的第二层**,即数据链路层。交换机根据设备的MAC地址将信息发送到正确的设备。交换机的优势在于它能有效地管理网络流量,避免数据冲突,并提高网络的总体性能。 +> 交换机和路由器的区别 + +总的来说,路由器和交换机的工作原理如下: + +- 路由器在网络层(第三层)上工作,处理数据包,根据数据包的目标IP地址,将数据包转发到正确的网络。 +- 交换机在数据链路层(第二层)上工作,处理数据帧,根据数据帧的目标MAC地址,将数据帧转发到正确的端口。 + +### OSI 7层模型每一层都用到什么协议 + +1. **物理层**:负责在物理媒介上进行数据的传输,常用的标准和协议包括: + - Ethernet + - USB + - Bluetooth + - 802.11(Wi-Fi) +2. **数据链路层**:负责在两个网络设备之间建立和维护数据链路,常用的协议包括: + - [Ethernet](https://zh.wikipedia.org/wiki/%E4%BB%A5%E5%A4%AA%E7%BD%91) + - [PPP (Point-to-Point Protocol)](https://zh.wikipedia.org/wiki/%E7%82%B9%E5%AF%B9%E7%82%B9%E5%8D%8F%E8%AE%AE) + - [Frame Relay](https://zh.wikipedia.org/wiki/%E5%B8%A7%E4%B8%AD%E7%BB%A7) +3. **网络层**:负责将数据包从源网络转发到目标网络,常用的协议包括: + - [IP (Internet Protocol)](https://zh.wikipedia.org/wiki/IP%E5%9C%B0%E5%9D%80) + - [ICMP (Internet Control Message Protocol) 用于告知网络包传送过程中产生的错误以及各种控制信息](https://zh.wikipedia.org/wiki/%E4%BA%92%E8%81%94%E7%BD%91%E6%8E%A7%E5%88%B6%E6%B6%88%E6%81%AF%E5%8D%8F%E8%AE%AE) + - [IGMP (Internet Group Management Protocol)](https://zh.wikipedia.org/wiki/%E5%9B%A0%E7%89%B9%E7%BD%91%E7%BB%84%E7%AE%A1%E7%90%86%E5%8D%8F%E8%AE%AE) +4. **传输层**:负责在源端和目标端之间提供可靠或者不可靠的数据传输,常用的协议包括: + - [TCP (Transmission Control Protocol)](https://zh.wikipedia.org/wiki/TCP) + - [UDP (User Datagram Protocol)](https://zh.wikipedia.org/wiki/%E7%94%A8%E6%88%B7%E6%95%B0%E6%8D%AE%E6%8A%A5%E5%8D%8F%E8%AE%AE) + - [SCTP (Stream Control Transmission Protocol)](https://zh.wikipedia.org/wiki/%E6%B5%81%E6%8E%A7%E5%88%B6%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE) +5. **会话层**:负责在网络设备之间建立、管理和终止会话,它使用的协议不多,而且在现代网络中并不常用。 +6. **表示层**:负责数据的编码、解码、加密和解密,它使用的协议也不多,而且在现代网络中大部分功能已经被应用层的协议实现。 +7. **应用层**:负责为网络应用提供服务,常用的协议包括: + - [HTTP (Hypertext Transfer Protocol)](https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE) + - [FTP (File Transfer Protocol)](https://zh.wikipedia.org/wiki/%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE) + - [SMTP (Simple Mail Transfer Protocol)](https://zh.wikipedia.org/wiki/%E7%AE%80%E5%8D%95%E9%82%AE%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE) + - [DNS (Domain Name System)](https://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E7%B3%BB%E7%BB%9F) + - [SNMP (Simple Network Management Protocol)](https://zh.wikipedia.org/wiki/%E7%AE%80%E5%8D%95%E7%BD%91%E7%BB%9C%E7%AE%A1%E7%90%86%E5%8D%8F%E8%AE%AE) -**路由器**,相反,工作在**OSI模型的第三层**,即网络层。路由器的主要任务是在不同的网络之间转发数据包,例如在家庭网络和互联网之间。路由器通常具有更高级的功能,例如网络地址转换(NAT)和防火墙服务。路由器根据IP地址来转发数据包。 -总的来说,交换机主要用于在同一网络内部的通信,而路由器则用于在不同网络之间的通信。 ### 什么是粘包? diff --git a/src/Java/eightpart/personal_images/06657cb93ffa4a24b8fc5b3069cb29bf.webp b/src/Java/eightpart/personal_images/06657cb93ffa4a24b8fc5b3069cb29bf.webp new file mode 100644 index 00000000..ba7b8a34 Binary files /dev/null and b/src/Java/eightpart/personal_images/06657cb93ffa4a24b8fc5b3069cb29bf.webp differ diff --git a/src/Java/eightpart/personal_images/451024fe10374431aff6f93a8fed4638.webp b/src/Java/eightpart/personal_images/451024fe10374431aff6f93a8fed4638.webp new file mode 100644 index 00000000..1c4fff07 Binary files /dev/null and b/src/Java/eightpart/personal_images/451024fe10374431aff6f93a8fed4638.webp differ diff --git "a/src/Java/eightpart/personal_images/QQ\346\210\252\345\233\27620230623205725.webp" "b/src/Java/eightpart/personal_images/QQ\346\210\252\345\233\27620230623205725.webp" deleted file mode 100644 index 1e1c3d94..00000000 Binary files "a/src/Java/eightpart/personal_images/QQ\346\210\252\345\233\27620230623205725.webp" and /dev/null differ diff --git a/src/Java/eightpart/personal_images/c34a9d1f58d602ff1fe8601f7270baa7.webp b/src/Java/eightpart/personal_images/c34a9d1f58d602ff1fe8601f7270baa7.webp new file mode 100644 index 00000000..7f53fb15 Binary files /dev/null and b/src/Java/eightpart/personal_images/c34a9d1f58d602ff1fe8601f7270baa7.webp differ diff --git a/src/Java/eightpart/personal_images/cluster_slots.webp b/src/Java/eightpart/personal_images/cluster_slots.webp deleted file mode 100644 index 4f0941c2..00000000 Binary files a/src/Java/eightpart/personal_images/cluster_slots.webp and /dev/null differ diff --git a/src/Java/eightpart/personal_images/def7d5328829470c9f3cfd15bbcc6814.webp b/src/Java/eightpart/personal_images/def7d5328829470c9f3cfd15bbcc6814.webp new file mode 100644 index 00000000..417ae0ef Binary files /dev/null and b/src/Java/eightpart/personal_images/def7d5328829470c9f3cfd15bbcc6814.webp differ diff --git a/src/Java/eightpart/personal_images/ebd620db8a1af66fbeb8f4d4ef6adc68.webp b/src/Java/eightpart/personal_images/ebd620db8a1af66fbeb8f4d4ef6adc68.webp new file mode 100644 index 00000000..3df34c04 Binary files /dev/null and b/src/Java/eightpart/personal_images/ebd620db8a1af66fbeb8f4d4ef6adc68.webp differ diff --git a/src/Java/eightpart/personal_images/image-20220715143320253.webp b/src/Java/eightpart/personal_images/image-20220715143320253.webp deleted file mode 100644 index d83d0e36..00000000 Binary files a/src/Java/eightpart/personal_images/image-20220715143320253.webp and /dev/null differ diff --git a/src/Java/eightpart/personal_images/image-20220715143327361.webp b/src/Java/eightpart/personal_images/image-20220715143327361.webp deleted file mode 100644 index 5561d86c..00000000 Binary files a/src/Java/eightpart/personal_images/image-20220715143327361.webp and /dev/null differ diff --git a/src/Java/eightpart/personal_images/image-20220715143507698.webp b/src/Java/eightpart/personal_images/image-20220715143507698.webp deleted file mode 100644 index f6b5f1e6..00000000 Binary files a/src/Java/eightpart/personal_images/image-20220715143507698.webp and /dev/null differ diff --git a/src/Java/eightpart/personal_images/image-20220715143520445.webp b/src/Java/eightpart/personal_images/image-20220715143520445.webp deleted file mode 100644 index 28a2c040..00000000 Binary files a/src/Java/eightpart/personal_images/image-20220715143520445.webp and /dev/null differ diff --git a/src/Java/eightpart/personal_images/image-20220715143532988.webp b/src/Java/eightpart/personal_images/image-20220715143532988.webp deleted file mode 100644 index f9622f8f..00000000 Binary files a/src/Java/eightpart/personal_images/image-20220715143532988.webp and /dev/null differ diff --git a/src/Java/eightpart/personal_images/redis-cluster-1.webp b/src/Java/eightpart/personal_images/redis-cluster-1.webp new file mode 100644 index 00000000..de102bfe Binary files /dev/null and b/src/Java/eightpart/personal_images/redis-cluster-1.webp differ diff --git a/src/Java/eightpart/personal_images/redis-cluster-2.webp b/src/Java/eightpart/personal_images/redis-cluster-2.webp new file mode 100644 index 00000000..aa05cdf2 Binary files /dev/null and b/src/Java/eightpart/personal_images/redis-cluster-2.webp differ diff --git a/src/Java/eightpart/redis.md b/src/Java/eightpart/redis.md index 9cb2233f..95816196 100644 --- a/src/Java/eightpart/redis.md +++ b/src/Java/eightpart/redis.md @@ -174,6 +174,7 @@ Redis在实现有序集合时针对不同的情况采用了不同的数据结构 > 关于什么时候会用到跳表或者什么时候用到压缩列表官网有介绍:https://redis.io/docs/management/optimization/memory-optimization/ > - Redis中ziplist和zSkiplist有一个阈值,由两个配置参数决定:`zset-max-ziplist-entries`和`zset-max-ziplist-value` > - 官网给的都不一样,假设使用的Redis7.2,这两个参数的默认值分别为128和64,这意味着如果一个排序集的元素超过128个或任何元素长度超过64字节,它将被转换为跳表 +> - 在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。 随着 Redis 版本的更新,后面又支持了四种数据类型:BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增) @@ -252,7 +253,7 @@ io-threads-do-reads yes io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 ``` -### Redis 是如何判断数据是否过期的呢? +### Redis 是如何判断数据是否过期 每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个**过期字典**(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。 @@ -295,7 +296,7 @@ typedef struct redisDb { 个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间。 -### 过期的数据的删除策略了解么? +### 过期的数据的删除策略 常用的过期数据的删除策略就前两个(重要!自己造缓存轮子的时候需要格外考虑的东西): @@ -340,7 +341,49 @@ typedef struct redisDb { 在 Redis 的运行内存达到了某个阀值,就会触发**内存淘汰机制**,这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory。 -### Redis 内存淘汰机制了解么? +#### 如何设置过期时间? + +先说一下对 key 设置过期时间的命令。 设置 key 过期时间的命令一共有 4 个: + +- `expire `:设置 key 在 n 秒后过期,比如 expire key 100 表示设置 key 在 100 秒后过期; +- `pexpire `:设置 key 在 n 毫秒后过期,比如 pexpire key2 100000 表示设置 key2 在 100000 毫秒(100 秒)后过期。 +- `expireat `:设置 key 在某个时间戳(精确到秒)之后过期,比如 expireat key3 1655654400 表示 key3 在时间戳 1655654400 后过期(精确到秒); +- `pexpireat `:设置 key 在某个时间戳(精确到毫秒)之后过期,比如 pexpireat key4 1655654400000 表示 key4 在时间戳 1655654400000 后过期(精确到毫秒) + +当然,在设置字符串时,也可以同时对 key 设置过期时间,共有 3 种命令: + +- `set ex ` :设置键值对的时候,同时指定过期时间(精确到秒); +- `set px ` :设置键值对的时候,同时指定过期时间(精确到毫秒); +- `setex ` :设置键值对的时候,同时指定过期时间(精确到秒)。 + +如果你想查看某个 key 剩余的存活时间,可以使用 `TTL ` 命令。 + +```bash +# 设置键值对的时候,同时指定过期时间位 60 秒 +> setex key1 60 value1 +OK + +# 查看 key1 过期时间还剩多少 +> ttl key1 +(integer) 56 +> ttl key1 +(integer) 52 +``` + +如果突然反悔,取消 key 的过期时间,则可以使用 `PERSIST ` 命令。 + +```bash +# 取消 key1 的过期时间 +> persist key1 +(integer) 1 + +# 使用完 persist 命令之后, +# 查下 key1 的存活时间结果是 -1,表明 key1 永不过期 +> ttl key1 +(integer) -1 +``` + +### Redis 内存淘汰机制 Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略。 @@ -438,8 +481,60 @@ redis.conf 提供了两个配置项,用于调整 LFU 算法从而控制 logc - `lfu-decay-time` 用于调整 logc 的衰减速度,它是一个以分钟为单位的数值,默认值为1,lfu-decay-time 值越大,衰减越慢; - `lfu-log-factor` 用于调整 logc 的增长速度,lfu-log-factor 值越大,logc 增长越慢。 +### 如何判定 key 已过期了? + +每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个**过期字典**(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。 + +过期字典存储在 redisDb 结构中,如下: + +```c +typedef struct redisDb { + dict *dict; /* 数据库键空间,存放着所有的键值对 */ + dict *expires; /* 键的过期时间 */ + .... +} redisDb; +``` + +过期字典数据结构结构如下: + +- 过期字典的 key 是一个指针,指向某个键对象; +- 过期字典的 value 是一个 long long 类型的整数,这个整数保存了 key 的过期时间; + + + +字典实际上是哈希表,哈希表的最大好处就是让我们可以用 O(1) 的时间复杂度来快速查找。当我们查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中: + +- 如果不在,则正常读取键值; +- 如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。 + ### 跳跃表 +> 快速向面试官介绍: +> +> 跳跃表是一种可以进行快速查找、插入、删除操作的数据结构。它是一种扩展了有序链表的数据结构,通过在链表上增加多级索引层,以实现快速查找。 +> +> 我们可以这样想象跳跃表的构造:首先,我们有一个基础层,这就是一个**有序的链表**。然后,我们在这个基础层上添加一些索引层。每一层的索引节点都是从下一层中随机选择出来的。每一个节点都可能有一个或者多个向右的指针,这些指针指向的是该节点在下一层的节点。 +> +> 通过这种方式,我们就可以通过跳过一些不必要的节点,以达到更快的查找速度。在查找一个元素的时候,我们从最顶层的索引开始,如果下一个节点的值比我们要查找的值大,我们就降到下一层继续查找,直到找到我们想要的元素。 + +#### 概述 + +Redis的zset(sorted set)是一种将元素按照权重排序的数据类型,它能够将元素进行快速的插入、删除、查找和范围查询。zset内部使用的主要数据结构是跳跃表(Skip List)。 + +**跳跃表的基本原理** + +跳跃表是一种随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳跃表在原有的有序链表之上增加了多级索引层,通过这种方式,跳跃表对于插入、删除和查找等操作都能达到近乎对数级的时间复杂度,即`O(logN)`。 + +在跳跃表中,每一个节点包含了两部分内容,一是节点所存储的值,二是一个指向其他节点的指针数组。指针数组的长度是随机生成的,长度为`n`的指针数组就代表这个节点在`n`级索引中都有位置。 + +**跳跃表的操作** + +1. **查找**:查找过程从跳跃表的顶层开始,如果下一个节点的值大于查找的值,就转到当前层的下一个节点继续查找,如果下一个节点的值小于或等于查找的值,就转到下一层继续查找,直到找到相应的值或者查找失败。 +2. **插入**:插入过程首先进行查找,找到应该插入的位置,然后随机生成一个高度,根据这个高度更新节点的指针数组,更新过程需要维护跳跃表的有序性。 +3. **删除**:删除过程首先进行查找,找到要删除的节点,然后更新所有涉及该节点的指针数组,删除这个节点。 + + + #### 简介 跳跃表(skiplist)是一种随机化的数据结构,由 **William Pugh** 在论文《Skip lists: a probabilistic alternative to balanced trees》中提出,是一种可以与平衡树媲美的层次化链表结构——查找、删除、添加等操作都可以在对数期望时间下完成,以下是一个典型的跳跃表例子: @@ -807,7 +902,19 @@ unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) { } ``` -### ⭐怎么保证 Redis 挂掉之后再重启数据可以进行恢复?(持久化策略) +### Redis为什么选择跳表而不选择使用红黑树? + +Redis选择跳表(Skip Lists)而不是红黑树(Red-Black Trees)作为其排序数据结构的实现主要有以下原因: + +1. **易于实现**:跳表相比红黑树更容易实现和理解。红黑树的实现涉及到许多复杂的旋转和颜色变换操作,而跳表则只需要简单的指针操作。 +2. **高效的搜索和插入操作**:跳表的搜索和插入操作的平均时间复杂度都是O(log n),与红黑树相同。然而在实践中,跳表的插入操作往往比红黑树更快,因为跳表的结构更为简单,操作的代价更低。 +3. **内存使用优化**:虽然跳表的空间复杂度高于红黑树(因为每个节点可能有多个指向其的指针),但Redis的作者发现,在实际应用中,跳表的内存利用效率比红黑树更高。这主要是因为Redis的内存分配器可以更有效地处理跳表的节点分配。 +4. **有序性**:跳表是有序的数据结构,这使得在Redis中实现范围查询等操作非常方便。 +5. **并发控制**:跳表更适合进行并发控制。在并发环境下,跳表的结构允许多个线程同时对不同的部分进行操作,而红黑树的结构则需要更复杂的同步控制。 + + + +### ⭐ 持久化策略(RDB、AOF) / 怎么保证 Redis 挂掉之后再重启数据可以进行恢复 > 概述: > @@ -821,12 +928,18 @@ unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) { Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持两种不同的持久化操作。**Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)**。 -#### RDB + + +#### RDB 概述 Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。 -RDB快照有两种触发方式,其一为通过配置参数,例如在配置文件中写入如下配置: -1. 快照持久化是 Redis 默认采用的持久化方式,在 `redis.conf` 配置文件中默认有此下配置: +Redis 提供了两个命令来生成 RDB 文件,分别是 `save` 和 `bgsave`,他们的区别就在于是否在「主线程」里执行: + +- 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,**会阻塞主线程**; +- 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以**避免主线程的阻塞**; + +快照持久化是 Redis 默认采用的持久化方式,在 `redis.conf` 配置文件中默认有此下配置: ```shell save 60 1000 @@ -834,10 +947,47 @@ save 60 1000 则在60秒内如果有1000个key发生变化,就会触发一次RDB快照的执行 -2. 其二是通过在客户端执行`bgsave`命令显式触发一次RDB快照的执行: -![](./personal_images/QQ截图20230623205725.webp) -#### AOF日志 + +#### RDB 原理 + +Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程(执行 bgsave 命令的时候,会通过 `fork()` 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个)。子进程做数据持久化,不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。父进程则持续服务客户端请求,并对内存数据结构进行不间断的修改。这个时候就会使用操作系统的 COW(Copy-On-Write)机制来进行数据段页面的分离。 + +![](./personal_images/c34a9d1f58d602ff1fe8601f7270baa7.webp) + +具体来说,当父进程对数据段中的一个页面进行修改时,被共享的页面会复制一份分离出来,然后对这个复制的页面进行修改,而子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据。随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,**内存就会持续增长,但不会超过原有数据内存的 2 倍大小**。 + +![当父进程对数据段中的一个页面进行修改](./personal_images/ebd620db8a1af66fbeb8f4d4ef6adc68.webp) + +另外,Redis 实例里冷数据占的比例往往是比较高的,所以很少会出现所有的页面都被分离的情况,被分离的往往只有其中一部分页面。每个页面的大小只有 4KB,一个 Redis 实例里面一般都会有成千上万个页面。 + +子进程能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化叫“快照”的原因。接下来子进程就可以非常安心地遍历数据,进行序列化写磁盘了。 + +> ⚠️ 注意:bgsave 快照过程中,如果主线程修改了共享数据,**发生了写时复制后,RDB 快照保存的是原本的内存数据**,而主线程刚修改的数据,是没办法在这一时间写入 RDB 文件的,只能交由下一次的 bgsave 快照。 +> +> 所以 Redis 在使用 bgsave 快照过程中,如果主线程修改了内存数据,不管是否是共享的内存数据,RDB 快照都无法写入主线程刚修改的数据,因为此时主线程(父进程)的内存数据和子进程的内存数据已经分离了,子进程写入到 RDB 文件的内存数据只能是原本的内存数据。 +> +> 如果系统恰好在 RDB 快照文件创建完毕后崩溃了,那么 Redis 将会丢失主线程在快照期间修改的数据。 +> +> 另外,写时复制的时候会出现这么个极端的情况。 +> +> 在 Redis 执行 RDB 持久化期间,刚 fork 时,主进程和子进程共享同一物理内存,但是途中主进程处理了写操作,修改了共享内存,于是当前被修改的数据的物理内存就会被复制一份。 + +总结一下: + +1. 在 RDB 快照生成过程中,Redis 主进程会执行 bgsave 命令,创建一个子进程来生成 RDB 文件。这个子进程会将 Redis 内存中的数据写入到磁盘中,生成一个快照文件。在生成快照文件期间,主进程可以继续处理客户端的请求(被修改),因为子进程是独立于主进程的。 +2. 当主进程需要修改共享数据时,会使用写时复制技术COW,创建一个新的副本,以便主进程可以对副本进行修改,而不会影响子进程正在生成的快照文件中的原始数据。主进程对副本的修改不会影响子进程正在生成的快照文件中的原始数据,因为子进程只会将内存中的原始数据写入到磁盘中,而不会包括主进程对副本的修改。 + + + +> 参考: +> +> 1. https://xiaolincoding.com/redis/storage/rdb.html#%E5%BF%AB%E7%85%A7%E6%80%8E%E4%B9%88%E7%94%A8 RDB 快照是怎么实现的? +> 2. 《redis深度历险》- 第二章 持久化 + + + +#### AOF 日志 ![](./personal_images/6f0ab40396b7fc2c15e6f4487d3a0ad7.webp) 这种保存写操作命令到日志的持久化方式,就是 Redis 里的 AOF(Append Only File) 持久化功能,注意只会记录写操作命令,读操作命令是不会被记录的,因为没意义。 @@ -964,7 +1114,115 @@ aof-use-rdb-preamble yes > 1. Redis 5 设计与源码分析 > 2. https://xiaolincoding.com/redis/storage/aof.html#aof-持久化是怎么实现的 -### 缓存穿透 & 缓存雪崩 & 缓存击穿 + + +### Redis 大 Key 对持久化有什么影响? + +> 概述: +> +> Redis的持久化主要有两种方式:AOF日志和RDB快照。大Key会对这两种持久化方式产生不同的影响: +> +> 1. AOF日志使用Always策略时,每次写命令后会立即将数据fsync到磁盘。写入大Key时,fsync阻塞主线程,影响性能。 +> +> 2. AOF重写和RDB快照都需要通过fork创建子进程。在这个过程中,页表复制和写时复制(COW)都会随着Key大小和内存使用量的增加而变慢,导致主线程阻塞。 +> +> 3. 开启内存大页也会使写时复制的内存拷贝变慢,降低性能。 + +#### AOF 日志的影响 + +在使用 Always 策略的时候,主线程在执行完命令后,会把数据写入到 AOF 日志文件,然后会调用 fsync() 函数,将内核缓冲区的数据直接写入到硬盘,等到硬盘写操作完成后,该函数才会返回。 + +**当使用 Always 策略的时候,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的**。 + +当使用 Everysec 策略的时候,由于是异步执行 fsync() 函数,所以大 Key 持久化的过程(数据同步磁盘)不会影响主线程。 + +当使用 No 策略的时候,由于永不执行 fsync() 函数,所以大 Key 持久化的过程不会影响主线程。 + +#### AOF 重写和 RDB 的影响 + +当 AOF 日志写入了很多的大 Key,AOF 日志文件的大小会很大,那么很快就会触发 **AOF 重写机制**。 + +AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 `fork()` 函数创建一个子进程来处理任务。 + +在创建子进程的过程中,操作系统会把父进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。 + +![](./personal_images/06657cb93ffa4a24b8fc5b3069cb29bf.webp) + +这样一来,子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为**只读**。 + +随着 Redis 存在越来越多的大 Key,那么 Redis 就会占用很多内存,对应的页表就会越大。 + +在通过 `fork()` 函数创建子进程的时候,虽然不会复制父进程的物理内存,但是**内核会把父进程的页表复制一份给子进程,如果页表很大,那么这个复制过程是会很耗时的,那么在执行 fork 函数的时候就会发生阻塞现象**。 + +而且,fork 函数是由 Redis 主线程调用的,如果 fork 函数发生阻塞,那么意味着就会阻塞 Redis 主线程。由于 Redis 执行命令是在主线程处理的,所以当 Redis 主线程发生阻塞,就无法处理后续客户端发来的命令。 + +我们可以执行 `info` 命令获取到 latest_fork_usec 指标,表示 Redis 最近一次 fork 操作耗时。 + +```sql +# 最近一次 fork 操作耗时 +latest_fork_usec:315 +``` + +如果 fork 耗时很大,比如超过1秒,则需要做出优化调整: + +- 单个实例的内存占用控制在 10 GB 以下,这样 fork 函数就能很快返回。 +- 如果 Redis 只是当作纯缓存使用,不关心 Redis 数据安全性问题,可以考虑关闭 AOF 和 AOF 重写,这样就不会调用 fork 函数了。 +- 在主从架构中,要适当调大 repl-backlog-size,避免因为 repl_backlog_buffer 不够大,导致主节点频繁地使用全量同步的方式,全量同步的时候,是会创建 RDB 文件的,也就是会调用 fork 函数。 + +> 那什么时候会发生物理内存的复制呢? + +当父进程或者子进程在向共享内存发起写操作时,CPU 就会触发**写保护中断**,这个「写保护中断」是由于违反权限导致的,然后操作系统会在「写保护中断处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为「**写时复制(Copy On Write)**」。 + +![](./personal_images/451024fe10374431aff6f93a8fed4638.webp) + +写时复制顾名思义,在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。 + +如果创建完子进程后,**父进程对共享内存中的大 Key 进行了修改,那么内核就会发生写时复制,会把物理内存复制一份,由于大 Key 占用的物理内存是比较大的,那么在复制物理内存这一过程中,也是比较耗时的,于是父进程(主线程)就会发生阻塞**。 + +所以,有两个阶段会导致阻塞父进程: + +- 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长; +- 创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长; + +这里额外提一下, 如果 **Linux 开启了内存大页,会影响 Redis 的性能的**。 + +Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行的。 + +如果采用了内存大页,那么即使客户端请求只修改 100B 的数据,在发生写时复制后,Redis 也需要拷贝 2MB 的大页。相反,如果是常规内存页机制,只用拷贝 4KB。 + +两者相比,你可以看到,每次写命令引起的**复制内存页单位放大了 512 倍,会拖慢写操作的执行时间,最终导致 Redis 性能变慢**。 + +那该怎么办呢?很简单,关闭内存大页(默认是关闭的)。 + +禁用方法如下: + +```shell +echo never > /sys/kernel/mm/transparent_hugepage/enabled +``` + + + +当 AOF 写回策略配置了 Always 策略,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。 + +AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 `fork()` 函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程): + +- 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长; +- 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。 + +大 key 除了会影响持久化之外,还会有以下的影响。 + +- 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。 +- 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。 +- 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。 +- 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。 + +如何避免大 Key 呢? + +最好在设计阶段,就把大 key 拆分成一个一个小 key。或者,定时检查 Redis 是否存在大 key ,如果该大 key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主线程,而是用 unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞主线程。 + + + +### 🌟 缓存穿透 & 缓存雪崩 & 缓存击穿 > 概要:redis`缓存穿透`、缓存击穿和缓存雪崩都是Redis缓存中的问题。(面试问到直接这样答就完事了,如果问到更细节的问题可以看下面的内容) > @@ -1139,7 +1397,23 @@ aof-use-rdb-preamble yes ![image-20220714212532910](./personal_images/image-20220714212532910.webp) + + +> redis中缓存击穿的概念是扛着大量的并发突然失效导致并发打在数据库上,和雪崩有什么区别? +> +> 它们的定义和区别如下: +> +> - **缓存击穿(Cache penetration)**: 这种情况通常发生在大量并发请求针对一个已过期或不存在的缓存key时,这些请求会直接打到数据库上,可能会对数据库造成较大的压力。这种情况的“失效”通常指的是一个特定的key已经过期或者不存在。 +> - **缓存雪崩(Cache avalanche)**: 缓存雪崩是指在一个很短的时间内,大量的缓存项同时过期。这样,大量的请求将直接打到数据库上,可能会导致数据库过载甚至崩溃。这通常发生在大量缓存数据的过期时间设置得过于集中,或者因系统故障导致的全局缓存失效。 +> +> 两者的主要区别在于:缓存击穿通常是由一个特定的热点key引发的,而缓存雪崩则是由大量的key同时过期引发的。 +> +> 解决这两种问题的方法也有所不同。缓存击穿可以通过设置热点数据永不过期,或者使用互斥锁等方式控制并发访问来防止;而缓存雪崩则可以通过设置不同的过期时间,或者使用备份缓存等方式来防止。 + + + ### Redis 如何实现延迟队列? + 延迟队列是指把当前要做的事情,往后推迟一段时间再做。延迟队列的常见使用场景有以下几种: - 在淘宝、京东等购物平台上下单,超过一定时间未付款,订单会自动取消; @@ -1368,7 +1642,7 @@ repl-backlog-size 1mb 如果它配置的过小,主从服务器网络恢复时,可能发生「从服务器」想读的数据已经被覆盖了,那么这时就会导致主服务器采用全量复制的方式。所以为了避免这种情况的频繁发生,要调大这个参数的值,以降低主从服务器断开后全量同步的概率。 -### Sentinel(哨兵) 有什么作用? +### Sentinel(哨兵) > Redis Sentinel,即Redis哨兵,在Redis 2.8版本开始引入。哨兵的核心功能是主节点的自动故障转移。 @@ -1385,7 +1659,7 @@ repl-backlog-size 1mb 其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移;而配置提供者和通知功能,则需要在与客户端的交互中才能体现。 -### 为什么要哨兵? + 在 Redis 的主从架构中,由于主从模式是读写分离的,如果主节点(master)挂了,那么将没有主节点来服务客户端的写操作请求,也没有主节点给从节点(slave)进行数据同步了。 @@ -1581,169 +1855,148 @@ Redis 在 2.8 版本以后提供的**哨兵(\*Sentinel\*)机制**,它的 - 第三步:将新主节点的 IP 地址和信息,通过「发布者/订阅者机制」通知给客户端; - 第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点; -### Cluster(集群)的原理你能讲一下吗? - -哨兵模式解决了主从复制不能自动故障转移、达不到高可用的问题,但还是存在主节点的写能力、容量受限于单机配置的问题。而cluster模式实现了Redis的分布式存储,每个节点存储不同的内容,解决主节点的写能力、容量受限于单机配置的问题。 +### Cluster(集群)的原理 -Redis cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。 +#### 启动 -Redis cluster采用**虚拟槽分区**,所有的键根据哈希函数映射到0~16383个整数槽内,每个节点负责维护一部分槽以及槽所映射的键值数据。 +Redis服务器在启动时会根据`cluster-enabled`配置选项是否为yes来决定是否开启服务器的集群模式 -![img](./personal_images/cluster_slots.webp) -**工作原理:** -1. 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384 个槽位 -2. 每份数据分片会存储在多个互为主从的多节点上 -3. 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步) -4. 同一分片多个节点间的数据不保持一致性 -5. 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点 -6. 扩容时时需要需要把旧节点的数据迁移一部分到新节点 +#### 节点 -在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。 +一个Redis集群通常由多个节点(node)组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群。 -16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,`gossip` 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。 -**优点:** -- 无中心架构,**支持动态扩**容; -- 数据按照 `slot`存储分布在多个节点,节点间数据共享,**可动态调整数据分布**; -- **高可用性**。部分节点不可用时,集群仍可用。集群模式能够实现自动故障转移(failover),节点之间通过 `gossip`协议交换状态信息,用投票机制完成 `Slave`到 `Master`的角色转换。 +连接各个节点的工作可以使用CLUSTER MEET命令来完成 -**缺点:** +```bash +CIUSTER MEET <ip> <port> +``` -- **不支持批量操作**(pipeline)。 -- 数据通过异步复制,**不保证数据的强一致性**。 -- **事务操作支持有限**,只支持多 `key`在同一节点上的事务操作,当多个 `key`分布于不同的节点上时无法使用事务功能。 -- `key`作为数据分区的最小粒度,不能将一个很大的键值对象如 `hash`、`list`等映射到不同的节点。 -- **不支持多数据库空间**,单机下的Redis可以支持到16个数据库,集群模式下只能使用1个数据库空间。 -- 只能使用0号数据库。 +向一个节点node发送CLUSTER MEET命令,可以让node节点与ip和port所指定的节点进行握手(handshake),当握手成功时,node节点就会将ip和port所指定的节点添加到node节点当前所在的集群中。 -**哈希分区算法有哪些?** -节点取余分区。使用特定的数据,如Redis的键或用户ID,对节点数量N取余:hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上。 优点是简单性。扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况。 -一致性哈希分区。为系统中每个节点分配一个token,范围一般在0~232,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。 这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。 +例子: -虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。**Redis Cluser采用虚拟槽分区算法。** +现在有三个节点需要集群:127.0.0.1:8080,127.0.0.1:8081,127.0.0.1:8082 -> 详细说明 +![](./personal_images/redis-cluster-2.webp) -- 自动将数据进行分片,每个 master 上放一部分数据 -- 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的 +可以通过`CLUSTER NODES`查看当前节点的集群状况 -在 Redis cluster 架构下,每个 Redis 要放开两个端口号,比如一个是 6379,另外一个就是 加 1w 的端口号,比如 16379。 -16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议, `gossip` 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。 -#### 节点间的内部通信机制 +#### 节点连接原理 -##### 基本通信原理 +![](./personal_images/redis-cluster-1.webp) -集群元数据的维护有两种方式:集中式、Gossip 协议。Redis cluster 节点间采用 gossip 协议进行通信。 +1. 节点A会为节点B创建一个`clusterNode`结构,并将该结构添加到自己的`clusterState.nodes`字典里面。 +2. 之后,节点A将根据`CLUSTER MEET`命令给定的IP地址和端口号,向节点B发送一条MEET消息(message)。 +3. 如果一切顺利,节点B将接收到节点A发送的MEET消息,节点B会为节点A创建一个`clusterNode`结构,并将该结构添加到自己的`clusterState.nodes`字典里面。 +4. 之后,节点B将向节点A返回一条PONG消息。 +5. 如果一切顺利,节点A将接收到节点B返回的PONG消息,通过这条PONG消息节点A可以知道节点B已经成功地接收到了自己发送的MEET消息。 +6. 之后,节点A将向节点B返回一条PING消息。 +7. 如果一切顺利,节点B将接收到节点A返回的PING消息,通过这条PING消息节点B可以知道节点A已经成功地接收到了自己返回的PONG消息,握手完成。 -**集中式**是将集群元数据(节点信息、故障等等)集中存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的 `storm` 。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper(分布式协调的中间件)对所有元数据进行存储维护。 -![image-20220715143320253](./personal_images/image-20220715143320253.webp) -Redis 维护集群元数据采用另一个方式, `gossip` 协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。 +其中`clusterNode`的C语言代码如下: -![image-20220715143327361](./personal_images/image-20220715143327361.webp) +```c +struct clusterNode { + //创建节点的时间 + mstime_t ctime; + //节点的名字,由40个十六进制字符组成 + //例如68eef66df23420a5862208ef5b1a7005b806f2ff + char name[REDIS_CLUSTER_NAMELEN]; + //节点标识 + //使用各种不同的标识值记录节点的角色(比如主节点或者从节点), + //以及节点目前所处的状态(比如在线或者下线)。 + int flags; + //节点当前的配置纪元,用于实现故障转移 + uint64_t configEpoch; + //节点的IP地址 + char ip[REDIS_IP_STR_LEN]; + //节点的端口号 + int port; + //保存连接节点所需的有关信息 + clusterLink *link; + // ... +}; +``` -**集中式**的**好处**在于,元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到;**不好**在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。 +clusterState的C语言代码如下: -gossip 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;不好在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。 +```c +typedef struct clusterState { + //指向当前节点的指针 + clusterNode *myself; + //集群当前的配置纪元,用于实现故障转移 + uint64_t currentEpoch; + //集群当前的状态:是在线还是下线 + int state; + //集群中至少处理着一个槽的节点的数量 + int size; + //集群节点名单(包括myself节点) + //字典的键为节点的名字,字典的值为节点对应的clusterNode结构 + dict *nodes; + // ... +} clusterState; +``` -- 10000 端口:每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如 7001,那么用于节点间通信的就是 17001 端口。每个节点每隔一段时间都会往另外几个节点发送 `ping` 消息,同时其它几个节点接收到 `ping` 之后返回 `pong` 。 -- 交换的信息:信息包括故障信息,节点的增加和删除,hash slot 信息等等。 -##### gossip 协议 -gossip 协议包含多种消息,包含 `ping` , `pong` , `meet` , `fail` 等等。 +#### 插槽 -- meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。 +Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。算法实现如下: ```bash -Redis-trib.rb add-node +HASH_SLOT = CRC16(key) mod 16384 ``` -其实内部就是发送了一个 gossip meet 消息给新加入的节点,通知那个节点去加入我们的集群。 - -- ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。 -- pong:返回 ping 和 meet,包含自己的状态和其它信息,也用于信息广播和更新。 -- fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。 - -##### ping 消息深入 - -ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。 - -每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了 `cluster_node_timeout / 2` ,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 `cluster_node_timeout` 可以调节,如果调得比较大,那么会降低 ping 的频率。 - -每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 `3` 个其它节点的信息,最多包含 `总节点数减 2` 个其它节点的信息。 - -#### 分布式寻址算法 - -- hash 算法(大量缓存重建) -- 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡) -- Redis cluster 的 hash slot 算法 - -##### hash 算法 - -来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致**大部分的请求过来,全部无法拿到有效的缓存**,导致大量的流量涌入数据库。 - -![image-20220715143507698](./personal_images/image-20220715143507698.webp) - -##### 一致性 hash 算法 - -一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。 +计算出每个key所属的slot。客户端可以请求任意一个节点,每个节点中都会保存所有16384个slot对应到哪一个节点的信息。如果一个key所属的slot正好由被请求的节点提供服务,则直接处理并返回结果,否则返回MOVED重定向信息,如下: -来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环**顺时针“行走”**,遇到的第一个 master 节点就是 key 所在位置。 - -在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。 - -燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成**缓存热点**的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。 +```bash +GET key +-MOVED slot IP:PORT +``` -![image-20220715143520445](./personal_images/image-20220715143520445.webp) +由-MOVED开头,接着是该key计算出的slot,然后是该slot对应到的节点IP和Port。客户端应该处理该重定向信息,并且向拥有该key的节点发起请求。实际应用中,Redis客户端可以通过向集群请求slot和节点的映射关系并缓存,然后通过本地计算要操作的key所属的slot,查询映射关系,直接向正确的节点发起请求,这样可以获得几乎等价于单节点部署的性能。 -##### Redis cluster 的 hash slot 算法 -Redis cluster 有固定的 `16384` 个 hash slot,对每个 `key` 计算 `CRC16` 值,然后对 `16384` 取模,可以获取 key 对应的 hash slot。 -Redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 `hash tag` 来实现。 +我们使用`CLUSTER MEET`命令将8080、8081、8082三个节点连接到了同一个集群里面,不过这个集群目前仍然处于下线状态,因为集群中的三个节点都没有在处理任何槽: -任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。 -![image-20220715143532988](./personal_images/image-20220715143532988.webp) -#### Redis cluster 的高可用与主备切换原理 +通过向节点发送`CLUSTER ADDSLOTS`命令,我们可以将一个或多个槽指派(assign)给节点负责: -Redis cluster 的高可用的原理,几乎跟哨兵是类似的。 +```bash +CLUSTER ADDSLOTS <slot> [slot ...] +``` -##### 判断节点宕机 -如果一个节点认为另外一个节点宕机,那么就是 `pfail` ,**主观宕机**。如果多个节点都认为另外一个节点宕机了,那么就是 `fail` ,**客观宕机**,跟哨兵的原理几乎一样,sdown,odown。 -在 `cluster-node-timeout` 内,某个节点一直没有返回 `pong` ,那么就被认为 `pfail` 。 +@todo -如果一个节点认为某个节点 `pfail` 了,那么会在 `gossip ping` 消息中, `ping` 给其他节点,如果**超过半数**的节点都认为 `pfail` 了,那么就会变成 `fail` 。 -##### 从节点过滤 -对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。 +#### 参考书籍 -检查每个 slave node 与 master node 断开连接的时间,如果超过了 `cluster-node-timeout * cluster-slave-validity-factor` ,那么就**没有资格**切换成 `master` 。 +1. 《Redis 设计与实现》 +2. 《Redis5 设计与源码分析》 -##### 从节点选举 +#### 参考文档 -每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。 +1. https://redis.io/commands/cluster-meet/ +2. https://redis.io/commands/cluster-addslots/ -所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node `(N/2 + 1)` 都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。 -从节点执行主备切换,从节点切换为主节点。 -##### 与哨兵比较 -整个流程跟哨兵相比,非常类似,所以说,Redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。 ### 高并发场景下,到底先更新缓存还是先更新数据库 #### Cache aside @@ -1866,11 +2119,18 @@ Write behind简单理解就是延迟写入,Cache Provider 每隔一段时间 可以看到,**先删除缓存,再更新数据库,在「读 + 写」并发的时候,还是会出现缓存和数据库的数据不一致的问题**。 -##### 解决方案:延时双删 - -延时双删的方案的思路是,为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数据库之后,再sleep一段时间,然后再次删除缓存。 +> 核心理解: +> +> 用户A购买了一件商品,我们需要更新库存。这个过程如下: +> +> 1. 删除Redis中的库存缓存。 +> 2. 更新MySQL中的库存数据。 +> +> 问题是,在这两步操作之间,如果用户B查询了库存,他会发现Redis中没有缓存,因此他会从MySQL数据库读取库存数据(此时还未更新),并将旧的库存数据存入Redis。然后用户A的购买操作完成,MySQL的库存数据更新,但Redis中的数据仍是旧的,因此用户B看到的库存信息是错误的。 +> +> 使用分布式锁可以解决这个问题。当用户A购买商品时,他首先获取商品库存的锁,然后删除Redis的缓存,更新MySQL的数据,最后释放锁。在此期间,如果用户B查询库存,他需要等待用户A释放锁。这样用户B总是能获得最新的库存信息。 -sleep的时间要对业务读写缓存的时间做出评估,sleep时间大于读写缓存的时间即可。 +##### 解决方案:延时双删 流程如下: @@ -1881,6 +2141,21 @@ sleep的时间要对业务读写缓存的时间做出评估,sleep时间大于 ![image-20220715152925965](./personal_images/image-20220715152925965.webp) +> 核心理解: +> +> "延迟双删"是一种解决Redis缓存和MySQL数据库一致性问题的策略,这个策略的基本思想是在更新数据库之后和删除缓存之后都执行一个小的延迟操作。下面是"延迟双删"策略的具体步骤: +> +> 1. 删除Redis缓存。 +> 2. 更新MySQL数据库。 +> 3. 延迟一段时间(比如几十毫秒)。 +> 4. 再次删除Redis缓存。 +> +> 这种策略的关键在于,第3步的延迟时间需要足够长,以确保在这段时间内任何其他可能从MySQL读取旧数据并写入Redis缓存的操作都已经完成。这样,当执行第4步的时候,我们可以确保删除的是最新的缓存数据。 +> +> 在这种情况下,即使在步骤1和步骤2之间有其他请求从MySQL读取了旧数据并写入Redis,由于我们在步骤4中再次删除了缓存,所以这样的旧数据也会被删除,不会出现数据不一致的问题。 +> +> 需要注意的是,"延迟双删"策略并不能完全保证数据的一致性,比如在步骤3的延迟期间,如果有新的请求更新了数据库并更新了缓存,然后步骤4的删除操作可能会把这个新的缓存数据也删除了,导致数据不一致的问题。但在实际应用中,由于我们可以通过控制延迟时间来极大地减小这种情况发生的概率,所以"延迟双删"策略通常是一个有效的解决方案。 + #### 先更新数据库,再删除缓存 继续用「读 + 写」请求的并发的场景来分析。 @@ -1897,6 +2172,15 @@ sleep的时间要对业务读写缓存的时间做出评估,sleep时间大于 所以,**「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的**。 +> 核心理解: +> +> 用户A购买了一件商品,我们需要更新库存。这个过程如下: +> +> 1. 更新MySQL中的库存数据。 +> 2. 删除Redis中的库存缓存。 +> +> 问题是,在这两步操作之间,如果用户B查询了库存,他可能从Redis中读到了旧的库存数量(因为缓存还未删除)。然后即使MySQL的库存数据更新了,用户B看到的库存信息仍是旧的。 + ##### 解决方案1:消息队列 这是网上很多文章里都有写过的方案。但是这个方案的缺陷会更明显一点。 diff --git a/src/Java/eightpart/smallMedium.md b/src/Java/eightpart/smallMedium.md index 8cae1b92..84acfba7 100644 --- a/src/Java/eightpart/smallMedium.md +++ b/src/Java/eightpart/smallMedium.md @@ -53,6 +53,104 @@ public void setData(Object data) { +### try catch 应该在 for 循环里面还是外面? + +1. ##### try catch 在 for 循环 外面 + +```java +public static void tryOutside() { + try { + for (int count = 1; count <= 5; count++) { + if (count == 3) { + //故意制造一下异常 + int num = 1 / 0; + } else { + System.out.println("count:" + count + " 业务正常执行"); + } + } + } catch (Exception e) { + System.out.println("try catch 在for 外面的情形, 出现了异常,for循环显然被中断"); + } +} +``` + +try catch 在 for 循环 外面 的时候, 如果 for循环过程中出现了异常, 那么for循环会终止。 + + + +2. ##### try catch 在 for 循环 里面 + +```java +public static void tryInside() { + + for (int count = 1; count <= 5; count++) { + try { + if (count == 3) { + //故意制造一下异常 + int num = 1 / 0; + } else { + System.out.println("count:" + count + " 业务正常执行"); + } + } catch (Exception e) { + System.out.println("try catch 在for 里面的情形, 出现了异常,for循环显然继续执行"); + } + } +} +``` + +try catch 在 for 循环 里面 的时候, 如果 for循环过程中出现了异常,异常被catch抓掉,不影响for循环 继续执行。 + + + +总结: + +如果说代码没出错的话, try catch 在 for 里面 和 外面 ,都是几乎没区别的。 + +- 其实就是看业务。我需要出现异常就终止循环的,就放外边; + +- 不需要终止循环,就搞里边 + + + +### HashMap是否可以存null? + +是的,Java的HashMap允许空键和空值。您可以在HashMap中存储一个空键和任意数量的空值。这是一个例子: + +```java +import java.util.HashMap; + +public class Main { + public static void main(String[] args) { + HashMap map = new HashMap<>(); + + // Storing null key + map.put(null, "value for null key"); + + // Storing null value + map.put("key1", null); + map.put("key2", null); + + // Printing the HashMap + for(String key: map.keySet()){ + System.out.println("Key: " + key + ", Value: " + map.get(key)); + } + } +} + +``` + +输出: + +```java +Key: null, Value: value for null key +Key: key1, Value: null +Key: key2, Value: null +``` + +注意,当使用null键或值时,如果某些方法不是为处理null而设计的,它们可能会抛出NullPointerException。因此,在将null键或值与HashMap一起使用之前,最好先检查方法。 + + + ## ♻️JVM ### 元空间是起到什么作用? @@ -83,6 +181,29 @@ Java中的内置线程池创建方式(如Executors.newFixedThreadPool()、Exec +### 如何不使用锁来进行重用和保证线程安全 + +为了在Java中实现线程安全而不使用显式的锁,有几种替代方法: + +**不可变性**:第一种方法是创建不可变对象,这些对象在创建后不能被修改。不可变对象从创建到销毁始终保持不变,因此天然线程安全。Java中的String类就是一个不可变类的例子。要创建一个不可变类,需要: + +- 确保该类声明为final +- 确保所有字段都为final和private +- 不提供“setter”方法——修改字段或对象的方法 +- 如果有任何可变字段(例如,如果您的类具有数组或Collection对象),请确保独占访问。不要提供访问这些对象引用的方法。相反,您可以提供返回这些对象副本的方法。 + +**原子变量**:Java提供了java.util.concurrent.atomic包,其中包括AtomicInteger、AtomicLong等类。这些类使用高效的机器级指令来确保原子性,而不是使用锁。当执行简单的原子操作(如增加值)时,可以使用这些类。 + +**ThreadLocal变量**:ThreadLocal变量可以通过为每个线程提供变量的实例来提供线程安全,因此每个线程都将拥有其自己的变量副本。这是一种替代同步的方法,但应谨慎使用,因为如果不小心使用,可能会导致高内存消耗。 + +**Volatile变量**:Java中的volatile关键字用作指示器,告诉JVM访问变量的线程必须始终将其私有副本与内存中的主副本协调一致。虽然volatile不执行任何互斥,但它是从写线程到读线程的一种通信方式,表示变量的值已被修改。 + +**并发工具**:java.util.concurrent包提供了几个并发工具,如CountDownLatch、Semaphore、CyclicBarrier、Exchanger等,可以在不使用synchronized关键字的情况下实现线程安全。但是请注意,在底层,这些工具可能仍然使用锁或其他同步机制。 + +**非阻塞算法**:非阻塞算法旨在避免使用锁,而是使用低级原子机器指令,例如CAS。这种方法非常复杂,只有在绝对必要时才应使用,因为生成的代码可能非常难以理解和维护。 + + + ## 📑MySQL ### 主键索引和唯一索引可以存储NULL值吗? 在MySQL中,主键索引和唯一索引对于NULL值的处理是不同的。 @@ -602,7 +723,7 @@ MVC模式被广泛应用于Web开发、桌面应用程序以及移动应用程 - #{}:这是预编译的方式。将参数放入 #{} 中,MyBatis 会在 SQL 执行前将 #{} 替换成占位符 ?,并通过预编译的 SQL 语句进行数据库操作,参数通过 JDBC 驱动的 PreparedStatement 的参数设置方法动态设置进去。这种方式可以防止 SQL 注入攻击。 - ${}:这是字符串替换的方式。将参数放入 ${} 中,MyBatis 会直接将 SQL 语句中的 ${} 替换成参数值,然后执行 SQL 语句。这种方式可能会引起 SQL 注入攻击,因为参数值在 SQL 语句拼接后,不会再被 JDBC 驱动进行任何的安全检查,而是直接被执行。所以在处理字符串类型的参数时,特别要注意可能引发的 SQL 注入问题。 -### mybatis 的一级缓存和二级缓存能不能介绍下? +### 🌟 mybatis 的一级缓存和二级缓存能不能介绍下? **一级缓存**:一级缓存是 MyBatis 的默认缓存,当我们开启一个 SqlSession 并执行查询时,MyBatis 会将查询结果放到这个 SqlSession 关联的缓存中,这个缓存就是一级缓存。也就是说,一级缓存是 SqlSession 级别的缓存,只对当前 SqlSession 的多次查询有效。如果我们在同一个 SqlSession 中对同一个查询两次,第二次查询就可以直接从一级缓存中获取结果,而不需要再次访问数据库。但是,如果我们开启了另一个 SqlSession 或者清空了缓存,那么一级缓存就无法使用。 diff --git a/src/Java/eightpart/spring.md b/src/Java/eightpart/spring.md index 37e29f10..e5e654b0 100644 --- a/src/Java/eightpart/spring.md +++ b/src/Java/eightpart/spring.md @@ -508,12 +508,27 @@ AutoConfigurationEntry(Collection configurations, Collection exc } ``` + + **总结** -- 原理:Spring Boot 在启动时扫描类路径中的 spring.factories 文件,查找与 org.springframework.boot.autoconfigure.EnableAutoConfiguration 关联的自动配置类。 -- 条件装配:使用 @Conditional 注解及其派生注解(如 @ConditionalOnClass, @ConditionalOnBean 等)来控制自动配置类是否应用,以满足特定条件。 -- 自定义自动配置:开发者可以通过创建 spring.factories 文件并指定自己的自动配置类来实现自定义自动配置。 -- 排除自动配置:使用 @EnableAutoConfiguration 注解的 exclude 或 excludeName 属性,或在配置文件中设置 spring.autoconfigure.exclude 属性来排除不需要的自动配置类。 -- 事件监听:通过实现 AutoConfigurationImportListener 接口,开发者可以在自动配置类导入时执行自定义操作。 + +1. 启动main方法开始。 +2. **初始化配置**: + - **加载工厂配置文件**:使用`SpringFactoriesLoader`加载`META-INF/spring.factories`配置文件。 + - **创建SpringApplication对象**:解析`spring.factories`中的`SpringApplicationRunListener`,通知监听者应用程序启动开始。 + - **创建环境对象**:创建`ConfigurableEnvironment`环境对象,用于读取环境配置,如`application.properties`或`application.yml`。 +3. **创建应用程序上下文** (`ApplicationContext`): + - **决定上下文类型**:根据应用类型决定是创建`AnnotationConfigServletWebServerApplicationContext`、`AnnotationConfigReactiveWebServerApplicationContext`还是其他上下文。 + - **初始化Bean工厂**:初始化`BeanFactory`对象。 +4. **刷新上下文** (启动核心): + - **配置Bean工厂**:为`BeanFactory`设置类加载器、`BeanPostProcessor`等。 + - **处理配置类**:使用`BeanFactoryPostProcessor`对配置类进行处理,如`@Configuration`类会被`ConfigurationClassPostProcessor`处理。 + - **注册Bean处理器**:注册`BeanPostProcessor`,如`AutowiredAnnotationBeanPostProcessor`用于处理`@Autowired`。 + - **初始化特定bean**:初始化特定的bean,如内嵌的Tomcat服务器。 + - **实例化单例bean**:实例化其他单例bean,这些bean可能是应用程序中的组件、配置或者服务。 + - **启动Web服务器**:启动内嵌的Web服务器(如Tomcat)并通知`ContextRefreshedEvent`,表示上下文已经刷新。 +5. **通知监听者**: + - **通知应用启动完成**:使用`SpringApplicationRunListener`通知所有监听者,表明应用启动完成。 ### 🌟 Spring Bean生命周期 diff --git a/src/Java/eightpart/system.md b/src/Java/eightpart/system.md index 9fa1af60..dd3bb519 100644 --- a/src/Java/eightpart/system.md +++ b/src/Java/eightpart/system.md @@ -99,10 +99,6 @@ public class VirtualThreadExample { ### 进程和线程的区别 -👨‍💻**面试官**: 好的!我明白了!那你再说一下: **进程和线程的区别**。 - -🙋 **我:** 好的! 下图是 Java 内存区域,我们从 JVM 的角度来说一下线程和进程之间的关系吧! - ![image-20220626201137158](./personal_images/image-20220626201137158.webp) 从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)\**资源,但是每个线程有自己的\**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 @@ -194,10 +190,6 @@ public class VirtualThreadExample { ### 进程有哪几种状态? -👨‍💻**面试官** : 那你再说说**进程有哪几种状态?** - -🙋 **我** :我们一般把进程大致分为 5 种状态,这一点和线程很像! - - **创建状态(new)** :进程正在被创建,尚未到就绪状态。 - **就绪状态(ready)** :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。 - **运行状态(running)** :进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。 @@ -206,10 +198,6 @@ public class VirtualThreadExample { ### 进程间的通信方式 -👨‍💻**面试官** :**进程间的通信常见的的有哪几种方式呢?** - -🙋 **我** :大概有 7 种常见的进程间的通信方式。 - > 下面这部分总结参考了:[《进程间通信 IPC (InterProcess Communication)》](https://www.jianshu.com/p/c1015f5ffa74)这篇文章,推荐阅读,总结的非常不错。 1. **管道/匿名管道(Pipes)** :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。 @@ -334,19 +322,13 @@ public class PipeExample { ### 线程间的同步的方式 -👨‍💻**面试官** :**那线程间的同步的方式有哪些呢?** - -🙋 **我** :线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。操作系统一般有下面三种线程同步的方式: - 1. **互斥量(Mutex)**:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。 2. **信号量(Semaphore)** :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。 3. **事件(Event)** :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作 -### 进程的调度算法 -👨‍💻**面试官** :**你知道操作系统中进程的调度算法有哪些吗?** -🙋 **我** :嗯嗯!这个我们大学的时候学过,是一个很重要的知识点! +### 进程的调度算法 为了确定首先执行哪个进程以及最后执行哪个进程以实现最大 CPU 利用率,计算机科学家已经定义了一些算法,它们是: @@ -356,24 +338,34 @@ public class PipeExample { - **多级反馈队列调度算法** :前面介绍的几种进程调度的算法都有一定的局限性。如**短进程优先的调度算法,仅照顾了短进程而忽略了长进程** 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前**被公认的一种较好的进程调度算法**,UNIX 操作系统采取的便是这种调度算法。 - **优先级调度** : 为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。 + + ### 什么是死锁 -👨‍💻**面试官** :**你知道什么是死锁吗?** +死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。 + +> 注:外力作用是指打破死锁的一种机制,例如进程主动放弃已获得的资源、通知其他进程释放资源等。 + + + +> 参考: +> +> 1. https://stackoverflow.com/questions/34512/what-is-a-deadlock +> 2. https://www.geeksforgeeks.org/introduction-of-deadlock-in-operating-system/ +> 3. [https://baike.baidu.com/item/%E6%AD%BB%E9%94%81/2196938](https://baike.baidu.com/item/死锁/2196938) + -🙋 **我** :死锁描述的是这样一种情况:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。 -### 死锁的四个条件 +### 🌟 死锁的四个条件 -👨‍💻**面试官** :**产生死锁的四个必要条件是什么?** +- **互斥**:这个条件意味着一次只有一个进程(或线程)可以使用特定的资源。如果其他进程想要使用同一个资源,它必须等待,直到当前占用该资源的进程完成并释放该资源。比如,打印机通常就是一个互斥资源,你不能同时打印两份文件。 +- **占有并等待**:这个条件描述的是一个进程在占有至少一个资源的同时,还在等待获取其他资源。这些其他资源可能正被其他进程所占有。比如,一个进程可能已经占有了访问数据库的权限,但它还需要访问网络才能完成其任务。 +- **非抢占**:这是指一旦一个进程获取了一个资源,其他进程就不能强行从它那里取走。这个资源只能被占有它的进程主动释放。比如,一个进程在使用CPU进行计算,这时候其他进程不能强行打断它,只能等到这个进程完成计算后,CPU才能被其他进程使用。 +- **循环等待**:这个条件形成了一种循环链,其中每一个进程都在等待下一个进程所占有的资源。最后一个进程在等待第一个进程所占有的资源,从而形成了一个等待循环。比如,进程A等待被进程B占有的资源,进程B等待被进程C占有的资源,进程C等待被进程A占有的资源,形成了一个循环等待。 -🙋 **我** :如果系统中以下四个条件同时成立,那么就能引起死锁: -- **互斥**:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。 -- **占有并等待**:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。 -- **非抢占**:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。 -- **循环等待**:有一组等待进程 `{P0, P1,..., Pn}`, `P0` 等待的资源被 `P1` 占有,`P1` 等待的资源被 `P2` 占有,......,`Pn-1` 等待的资源被 `Pn` 占有,`Pn` 等待的资源被 `P0` 占有。 -### 死锁的解决方式(中小厂可能会问到) +### 死锁的解决方式 > 官方说法: > 解决死锁的方法可以从多个角度去分析,一般的情况下,有预防,避免,检测和解除四种。 diff --git a/src/Java/eightpart/virtualMachine.md b/src/Java/eightpart/virtualMachine.md index 9bafe374..ed9caea2 100644 --- a/src/Java/eightpart/virtualMachine.md +++ b/src/Java/eightpart/virtualMachine.md @@ -211,6 +211,18 @@ Java 堆既可以被实现成固定大小的,也可以是可扩展的,当前 ![image-20220628160739969](./personal_images/image-20220628160739969.webp) + + +### 为什么程序计数器是在JVM运行时数据区唯一一个不会出现 OutOfMemoryError 的内存区域 + +1. **简单的结构与功能**:程序计数器是一个非常小且简单的内存结构。它的作用是记录线程正在执行的字节码指令的地址或者说是指令的行号。对于正在执行的方法,程序计数器记录的是当前线程正在执行的字节码指令地址;如果是调用的本地方法,则计数器的值为空(undefined)。因此,它的内存需求是非常有限且固定的。 +2. **不共享**:每个线程在创建时都会创建一个属于自己的程序计数器,它们之间不会相互影响,也不需要进行线程间的同步,所以在这个区域是不可能出现内存溢出的情况。 +3. **无需动态扩展**:程序计数器的生命周期与线程相同,且在创建时即确定了大小,不需要在运行时动态扩展,因此不存在内存溢出的情况。 + +因此,程序计数器是 JVM 中唯一一个不会出现 `OutOfMemoryError` 的内存区域。 + + + ### JDK 中有哪些默认的类加载器 > 这里参考了[JVM 底层原理最全知识总结](https://doocs.github.io/jvm/)