LOADING

加载过慢请开启缓存 浏览器默认开启

IceOfSummerの博客

还是自己搭的博客靠谱

后面一辈子的博客都在这了!

Netty面试题

2023/4/10

1. 为什么要用Netty

Netty是一个基于Java NIO封装的高性能网络通信框架。它主要有以下优势:

  1. Netty提供了比NIO更简单的API
    • 很容易地实现Reactor模型
  2. Netty在NIO的基础上做出了很多优化
    • 内存池
    • 零拷贝
  3. Netty内置了多种通信协议

用官方的总结就是:Netty 成功地找到了一种在不妥协可维护性和性能的情况下实现易于开发,性能,稳定性和灵活性的方法。

2. Netty零拷贝

Netty零拷贝主要在五个方面:

  1. Netty默认情况下使用直接内存,避免了从JVM堆内存拷贝到直接内存这一次拷贝,而是直接从直接使用直接内存进行Socket读写
  2. Netty的文件传输调用了FileRegion包装的transferTo方法,可以直接将文件从缓冲区发送到目标Channel
  3. Netty提供了CompositeByteBuf类,可以将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了多个ByteBuf的拷贝。
  4. 通过ByteBuf.wrap方法,可以将byte[]数组、ByteBuffer包装成一个ByteBuf,从而避免了拷贝
  5. ByteBuf支持slice操作,可以将ByteBuf分解为多个共享同一存储区域的ByteBuf,避免了内存的拷贝

3. Netty内存管理

为了减少频繁向操作系统申请内存的情况,Netty会一次性申请一块较大的内存(由ChunkSize决定,默认为16M),这块内存被称为PoolChunk

而在一个Chunk下,又分为了一个一个页,叫做Page,默认为8K,即默认情况下一个Chunk有2048个页。

超详细图文详解神秘的 Netty 高性能内存管理 - 知乎 (zhihu.com)

3.1 PoolChunk如何管理Page

PoolChunk通过一个完全二叉树来管理Page,这颗二叉树的深度为12(2^11 = 2048)。

PoolChunk会维护一个memeoryMap数组,这个数组对应着每个节点,它的值代表这个节点之下的第几层还存在未分配的节点。

  • 比如说第9层的memeoryMap值为9,代表这个节点下面的子节点都未被分配
  • 若第9层的memeoryMap为10,代表它本身不可被分配,但第10层有子节点可以被分配
  • 若第9层的memeoryMap为12(树的高度),代表当前节点下的所有子节点都不可分配

那么我们怎么分配呢?

比如我们要15KB的空间,这里会先向上取8的整数,也就是16K,也就是2^1 * 8,拿到指数1,通过depth - 1 = 12 - 1得到11,那么我们只需要去找memeoryMap为11的节点即可。在分配后,父节点的memeoryMap等于两个子节点的最小值。

3.2 Page的管理

一个Page有8K,一般我们的应用程序是用不了这么多的,因此每个Page下会再次分隔。但这次分隔并不是以完全二叉树的形式,因为太占空间了,而是将这8K划分为等长的n份,一般会由PoolSubpage管理,一般分为两类:

  • tiny:用于分配小于512字节的内存,一般大小为16B,32B,…,496B,每次增长为16的倍数,共32个。
  • small:用于分配大于等于512字节的内存,一般大小为512B、1K、2K,4K。

对于每个块,会有一个bitMap去判断是否使用,可以理解为Java中的BitSet

3.3 Chunk的管理

每个PoolChunk通过PoolArena类来管理,这些Chunk被封装在PoolChunkList类中,这是一个双向链表。

PoolArena有6个PoolChunkList

  • qInit:存储内存利用率 0-25% 的 chunk
  • q000:存储内存利用率 1-50% 的 chunk
  • q025:存储内存利用率 25-75% 的 chunk
  • q050:存储内存利用率 50-100% 的 chunk
  • q075:存储内存利用率 75-100%的 chunk
  • q100:存储内存利用率 100%的 chunk

PoolArena分配内存的顺序是:q050、q025、q000、qInit、q075

这样分配的好处是可以提高内存的利用率,以及减少链表的遍历次数。

3.4 PoolThreadCache

PoolThreadCache利用了ThreadLocal,每次线程在申请内存时都会优先从这里面获取。

  • 在释放已分配的内存块时,不放回到 Chunk 中,而是缓存到 ThreadCache 中
  • 在分配内存块时,优先从 ThreadCache 获取。若无法获取到,再从 Chunk 中分配
  • 通过这样的方式,既能提高分配效率,又尽可能的避免多线程的同步和竞争

4. 直接内存回收原理

每个ByteBuf都实现了一个ReferenceCounted接口,netty也是直接采用了引用计数法来进行内存回收。

5. 怎么判断ByteBuffer是否处于写模式或读模式

ByteBuffer有三个重要参数:positionlimitcapacity,而平常我们说的读模式或写模式只是用来方便我们理解的东西,真正在ByteBuffer的实现里并不存在什么读模式和写模式,也就是说你在”读模式下”仍然可以写。

例如下面的代码:

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

byte[] hello = "hello".getBytes(StandardCharsets.UTF_8);
System.out.println(Arrays.toString(hello));
// "write mode"
byteBuffer.put(hello);
// "read mode"
byteBuffer.flip();
// write again
byteBuffer.put("h".getBytes(StandardCharsets.UTF_8));

while (byteBuffer.hasRemaining()) {
    System.out.print(byteBuffer.get() + " ");
}

在”读模式”下去写的时候,并不会报错,由于切换到了”读模式”,此时position = 0,limit = 写模式的offset,因此在写的时候,会从索引0处开始写,写完后,position变为1,我们再读的话也就只能从索引1读到4了。

如果硬要判断是不是”读模式”或”写模式”,可以根据positionlimit的值进行判断:

  • limit = capacity,表示当前可能为写模式
阅读全文

MySql面试

2023/4/2

1. B树和B+树之间的区别

B树有些博客上会写成B-树,部分博客甚至读成了B减树,其实这个减号只是一个连接符,没有任何意义

B-Tree Visualization (usfca.edu)

B+ Tree Visualization (usfca.edu)

B树和B+树的区别:

  • B+树只会在叶子节点存储数据,而B树每个节点上都会有数据
  • B+树每个叶子节点之间有一个指针乡相连

2. 高度为3的B+树能存多少条数据

MySQL系列(4)— InnoDB数据页结构 - 掘金 (juejin.cn)

在InnoDB中,索引默认使用的数据结构为B+树,而B+树里的每个节点都是一个页,默认的页大小为16KB

page

3. 简单说一下InnoDB事务实现原理

事务ACID:

名称 别名 说明
Atomicity 原子性 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生
Consistency 一致性 事务前后数据的完整性必须保持一致
Isolation 隔离性 事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离
Durability 持久性 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

一文了解InnoDB事务实现原理 - 知乎 (zhihu.com)

上面那个比较深入,下面这个比较好理解一些:

图解InnoDB事务实现原理|Redo Log&Undo Log - 掘金 (juejin.cn)

4.MySql的三大日志是哪些

MySQL三大日志(binlog,redolog,undolog)详解 - 掘金 (juejin.cn)

聊聊MVCC和Next-key Locks - 掘金 (juejin.cn)

5. MySql当前读和快照度

mysql快照读原理实现 - 掘金 (juejin.cn)

MySQL 的可重复读到底是怎么实现的?图解 ReadView 机制 - 知乎 (zhihu.com)

这里其实有个问题,如果我只是单独的一条查询语句,没有开启事务,那么怎么去快照读呢?

这个我自己查了一下,众所周知,MySql里有一个autocommit属性,对于单条SQL,这个值一定是true,那么是不是说明我们每条SQL都会被认作是一个事务呢?

然后我在官方文档里查了一下:

每条SQL都是一个单独的事务

MySQL :: MySQL 5.7 Reference Manual :: 14.7.2.2 autocommit, Commit, and Rollback

MySQL :: MySQL 8.0 Reference Manual :: 15.7.2.2 autocommit, Commit, and Rollback

不管是8.0还是5.7,都是这样写的,那么就可以说的通了。每次执行单条SQL都会拿到一个事务id,然后再去进行快照读。

那么什么是当前读呢,使用下面的sql语句就是当前读:

# 加共享锁
SELECT ... LOCK IN SHARE MODE 
# 加排它锁
SELECT ... FROM UPDATE

这两条语句的原理就是给对应的行加上共享锁(读锁)或排它锁(写锁),当有事务进行增删改时也会加排它锁,对于共享锁,允许多个事务持有(即允许多读),对于排它锁,则只允许一个事务持有(即只能一个人写,且除了自己其它人都不能读)。

在排它锁和共享锁下读的的数据就是当前读,这份数据永远是最新的(此时若有其它事务想要修改相关的行,都会被阻塞)。其它状况则就是快照读了,通过MVCC创建ReadView进行数据的读取。

6. MySql的MVCC

MVCC(Multiversion Concurrency Control)多版本并发控制。

首先在在MVCC下,每个表都会多出几个隐藏的列,分别为隐藏主键(row_id)、事务id(trx_id)、回滚指针(roll_pointer)。

MVCC还有两个重要的组成:undo log(回滚日志)、ReadView。

更详细的就不说了,因为上面的链接里面都有,主要是下面这四个关系:

(1)当【版本链中记录的 trx_id 等于当前事务id(trx_id = creator_trx_id)】时,说明版本链中的这个版本是当前事务修改的,所以该快照记录对当前事务可见。

(2)当【版本链中记录的 trx_id 小于活跃事务的最小id(trx_id < min_trx_id)】时,说明版本链中的这条记录已经提交了,所以该快照记录对当前事务可见。

(3)当【版本链中记录的 trx_id 大于下一个要分配的事务id(trx_id > max_trx_id)】时,该快照记录对当前事务不可见。

(4)当【版本链中记录的 trx_id 大于等于最小活跃事务id】且【版本链中记录的trx_id小于下一个要分配的事务id】(min_trx_id<= trx_id < max_trx_id)时,如果版本链中记录的 trx_id 在活跃事务id列表 m_ids 中,说明生成 ReadView 时,修改记录的事务还没提交,所以该快照记录对当前事务不可见;否则该快照记录对当前事务可见。

6.1 RepeatableRead是怎么实现的

我们都知道,RepeatableRead相比ReadCommited能够避免不可重复读的问题(实际也能够避免幻读,是通过加间隙锁实现的)。

首先我们来看ReadCommitted,使用mysql执行如下指令(假如我们叫它事务A)

set session transaction isolation level read committed;
begin;
update test set xid = 2 where id = 1;
# 等一会再提交
commit;

然后再开一个mysql执行如下指令(假如我们叫它事务B):

set session transaction isolation level read committed;
begin;
select * from test where id = 1;
# 提交上面那个指令后再执行下面这条
select * from test where id = 1;

这里就不放图了,大家都知道第二次读取会不一样。

这回我们再将隔离级别设置为RepeatableRead,并同样执行上面的指令。

这次执行后,发现两次查询的结果都是一样的,而且在事务A执行更新后且没有提交时,B再去读,并没有发生阻塞,因为在修改数据的时候会加排它锁,在读的时候要么是当前读要么是快照读,如果是当前读,那么读操作会堵塞,说明在B这里是快照读,是创建了ReadView的,通过ReadView有效地避免了不可重复读。

我们再用同样的方式去验证ReadCommited级别的读,发现同样是快照读,那么凭什么RepeatableRead不会读到新值,而ReadCommited会呢?

这里我画了一个流程图方便理解:

流程图

图画的可能不太好,不过应该能看懂

网上大部分人讲的都是以ReadCommited级别为例子的,即m_ids里的事务提交后可读,但其实在RepeatableRead隔离级别下是读不了的,只能走undo_log进行回滚。


这里可能有点错误,在ReadCommited下可以读已经提交的事务,所以如果trx_id大于等于mid_id,只需要判断对应的事务是否已经提交(或者trx_id指向自己)就能读

7. RepeatableRead真的不能避免幻读吗?

美团三面:一直追问我, MySQL 幻读被彻底解决了吗?_肥肥技术宅的博客-CSDN博客

8. 为什么bin_log不能用作崩溃后的恢复

mysql 为什么不能用binlog来做数据恢复? - 知乎 (zhihu.com)

阅读全文

redis面试

2023/3/28

1. Redis字典

深入理解Redis 数据结构—字典 - 知乎 (zhihu.com)

可以这样理解:Redis的字典就是java7的HashMap,即哈希表+链表

Redis字典使用的哈希表结构如下:redis/dict.h at 2.6 · redis/redis (github.com)

typedef struct dictht {
     // table 数组
    dictEntry **table;
    // 哈希表的大小
    unsigned long size;
    // 等于size-1,用于计算索引值, 这里说明size肯定是2的幂
    unsigned long sizemask;
    // 已有的键值对数量
    unsigned long used;
} dictht;

dictEntry就是哈希节点了:redis/dict.h at 2.6 · redis/redis (github.com)

typedef struct dictEntry {
    // 键
    void *key;
    // 值   
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下一个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

Redis中的字典则由dict组成:redis/dict.h at 2.6 · redis/redis (github.com)

typedef struct dict {
    // 类型特定的函数,提供增删改查等功能 
    dictType *type;
   // 私有函数
    void *privdata;
    // 哈希表, 这里的二维是后面用来扩容的
    dictht ht[2];
    // rehash 索引,记录了当前扩容的进度
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 用来记录当前运行的安全迭代器数,当不为0的时候表示有安全迭代器正在执行,这时候就会暂停rehash操作
    int iterators; /* number of iterators currently running */
} dict;

总览

2. Redis扩容与缩容

我们用ht[0].used/ht[0].size表示负载因子

2.1 扩容

  • 如果没有fork子进程在执行RDB或者AOF的持久化,一旦满足负载因子大于等于1,此时触发扩容;

  • 如果有fork子进程在执行RDB或者AOF的持久化时,则需要满足负载因子大于5,此时触发扩容。

Redis在扩容时使用的是渐进式哈希,即每次值移动一部分的数据到新的哈希表中。

在字典dict中,dict.ht[0]代表旧的哈希表,dict.ht[1]代表新的哈希表,每次扩容时会将容量乘2,同时dict.rehashidx代表rehash的进度,表示dict.ht[0]中,小于该索引的值都已经被移动到dict.ht[1]中了,此时需要在dict.ht[1]中进行相关的增删改查操作,反之则在dict.ht[0]中进行。

在扩容期间,每次进行增删改查都会将dict.rehashidx加一,并进行相关的rehash操作。

在扩容完毕后,将dict.ht[0]指向dict.ht[1],并删除旧的哈希表。

2.2 缩容

当负载因子小于0.1时,Redis就会对哈希表进行收缩操作。

相关操作和扩容一样,在dict.ht[1]处创建新的哈希表,之后再渐进式rehash。

2.3 其它问题

假如在rehash扩容的时候,我们一直插入,会不会导致再次扩容呢?

假设此时哈希表容量为n,元素数量为n,在扩容哈希表容量后变为2n,而对于Redis来说,完成rehash需要2n - n = n次操作,所以我们最多进行n次插入,插入完后元素数量也变为2n,再次触发扩容。

对于负载因子为5的时候,假设此时哈希表容量为n,元素数量为5n + 1,扩容后哈希表容量为2n,同样我们可以插入n个元素,此时元素数量变为6n + 1,负载因子为(6n + 1) / 2n约等于3,此时不会触发扩容。

3. 字典遍历

3.1 全遍历

使用如下指令就会执行全遍历,返回所有的key:

keys *

优点:

  • 返回的key不会重复

缺点:

  • 在遍历完前会阻塞服务器

迭代器结构:

typedef struct dictIterator {
    dict *d; //迭代的字典
    int index; //当前迭代到Hash表中哪个索引值
    int table, safe; //table用于表示当前正在迭代的Hash表,即ht[0]与ht[1],safe用于表示当前创建的是否为安全迭代器
    dictEntry *entry, *nextEntry;//当前节点,下一个节点
    /* unsafe iterator fingerprint for misuse detection. */
    long long fingerprint;//字典的指纹,当字典未发生改变时,该值不变,发生改变时则值也随着改变
} dictIterator;

3.3.1 安全迭代器和非安全迭代器

Redis源码学习——安全迭代器和非安全迭代器(一)_damanchen的博客-CSDN博客

Redis源码学习——安全迭代器和非安全迭代器(二)_damanchen的博客-CSDN博客

跋山涉水 —— 深入 Redis 字典遍历 - 知乎 (zhihu.com)

安全迭代器:

  • 迭代的时候不能rehash,可以进行过期键的删除

非安全迭代器:

  • 迭代的时候可以rehash,但是不能进行删除等操作(字典只读),通过fingerprint字段来判断字典是否发生变动

3.2 间接遍历

使用scan命令可以间接遍历,这个命令每次会返回一个下一个需要遍历的索引值:

redis 127.0.0.1:6379&gt; scan 0
1) "17"
2)  1) "key:12"
    2) "key:8"
    3) "key:4"
    4) "key:14"
    5) "key:16"
    6) "key:17"
    7) "key:15"
    8) "key:10"
    9) "key:3"
   10) "key:7"
   11) "key:1"
redis 127.0.0.1:6379&gt; scan 17
1) "0"
2) 1) "key:5"
   2) "key:18"
   3) "key:0"
   4) "key:2"
   5) "key:19"
   6) "key:13"
   7) "key:6"
   8) "key:9"
   9) "key:11"

Redis SCAN 命令

优点:

  • 一次只返回部分内容,响应较快,不会较长时间阻塞服务器

缺点:

  • 可能会返回重复的值

redis scan 命令底层原理(为什么会重复扫描?)_redis scan命令原理_柏油的博客-CSDN博客

这里第一次看可能会有这个疑问,我们打个比方:

遍历顺序:00 -> 10 -> 01 -> 11

若正好遍历到10时扩容完毕了,则新顺序为:

000 -> 100 -> 010 -> 110 -> 001 -> 101 -> 011 -> 111

此时我们在第三个位置,即010那里。

这时候可能就有疑问了:100那里不就遍历不到了吗?这不是丢数据了吗?

但这样其实是想多了,我们来看000和100,假如哈希表长度为4时,这两个索引下的元素会落到哪个哈希表下?

很明显,这两个位置的元素都会落到00这个索引的下面,因为哈希表长度为4时,索引位置的取法就是和0x11做与操作,而000和100低两位相同,所以它们俩在之前就在00处,以链表的形式组合在了一起,当遍历到10时,100也肯定被遍历了。


总结一下就是scan命令会在哈希表缩容的时候造成数据重复,在rehash的期间也会造成重复。

在rehash期间调用scan,Redis会先扫小表,假如最终索引为v,然后会接着在大表中从v开始扫。

4. 五大基本数据类型

在看数据类型前,我们再回顾一下entry的结构:

typedef struct dictEntry {
    // 键
    void *key;
    // 值   
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下一个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

有没有发现一个问题:这个v代表值,那么这个值是个什么东西??

这里其实是C语言的union,可以让多个变量使用同一个内存空间:C/C++ union 使用教程 (常见操作与缺陷) - 知乎 (zhihu.com)

你可以这样理解:这里的v即有三种类型,即void*uint64_t(64位无符号整数)、int64_t(64位有符号整数)。

对应void*你可以理解为Java中的Object类型,用它做参数的话就可以传入任意对象,更详细的信息可以看这篇博客:void(指针)的类型转换-专讲_void转换_NeverLate_gogogo的博客-CSDN博客


一般情况下void*都是指向redisObject redis/README.md at cb1717865804fdb0561e728d2f3a0a1138099d9d · redis/redis (github.com)

struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
};
  • type:没啥好说的,每种数据结构的标识符

  • encoding:编码

    • 以string来说,就有三种:int , embstr , raw

      127.0.0.1:6379&gt; SET counter 1
      OK
      127.0.0.1:6379&gt; OBJECT ENCODING counter
      "int"
      127.0.0.1:6379&gt; SET name "Tom"
      OK
      127.0.0.1:6379&gt; OBJECT ENCODING name
      "embstr"
      127.0.0.1:6379&gt; SETBIT bits 1 1
      (integer) 0
      127.0.0.1:6379&gt; OBJECT ENCODING bits
      "raw"
      
  • lru:给Redis做内存淘汰用

  • refcount:引用计数,这个值为0的时候这个对象会被清除

  • ptr:指向对象的实际表示,可能有多个指向同一个对象,一般还要配合encoding判断

4.1 String

Redis 的字符串是动态字符串,是可以修改的字符串,可以勉强理解为Java里的StringBuilder

当字符串需要扩容时,有如下两种情况:

  • 当字符串长度小于 1M 时,扩容都是加倍现有的空间
  • 超过 1M,扩容时一次只会多扩 1M 的空间

字符串最大长度为512MB。
数据结构:redis/sds.h at cb1717865804fdb0561e728d2f3a0a1138099d9d · redis/redis (github.com)

#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
typedef char *sds;

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

我们可以发现,字符串结构体基本由len(已使用的长度)、alloc(最大长度/分配的长度)、flags(标志信息)、buf(字符串内容)组成。

字符串拼接:redis/sds.c at cb1717865804fdb0561e728d2f3a0a1138099d9d · redis/redis · GitHub

sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);
    s = sdsMakeRoomFor(s,len);
    // 内存不足
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len);
    sdssetlen(s, curlen+len);
    s[curlen+len] = '\0';
    return s;
}

要懂redis,首先得看懂sds(全网最细节的sds讲解) - 知乎 (zhihu.com)

4.2 Hash

Hash类型有两种实现方式:

  • ziplist 编码的哈希对象使用压缩列表作为底层实现
  • hashtable 编码的哈希对象使用字典作为底层实现

Redis之Hash数据结构底层原理_redis hash原理_不要迷恋发哥的博客-CSDN博客

4.2.1 ziplist

ziplist的运作方式类似于一个队列,当有一对键值时,先将值入队,再将键入队。

这种设计完全不符合哈希表的设计,所以只会在数据量较少时使用。

当发生如下情况时,ziplist会被转换为真正的哈希表(字典):

  • 当hash中的数据项的数目超过512的时候,也就是ziplist数据项超过1024的时候
  • 当hash中插入的任意一个value的长度超过了64的时候
area        |&lt;---- ziplist header ----&gt;|&lt;----------- entries -------------&gt;|&lt;-end-&gt;|

size          4 bytes  4 bytes  2 bytes    ?        ?        ?        ?     1 byte
            +---------+--------+-------+--------+--------+--------+--------+-------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | entryN | zlend |
            +---------+--------+-------+--------+--------+--------+--------+-------+
                                       ^                          ^        ^
address                                |                          |        |
                                ZIPLIST_ENTRY_HEAD                |   ZIPLIST_ENTRY_END
                                                                  |
                                                         ZIPLIST_ENTRY_TAIL
字段 类型 说明
zlbytes uint32_t 整个ziplist占用的内存字节数。
zltail uint32_t 到达ziplist表尾节点的偏移量。
zllen uint16_t ziplist中节点的数量。
entryX ziplist的各个节点。
zlend uint8_t 常量0b111111,用于标记ziplist末尾。

每个entry的结构是这样的:

+-----------------------+----------+---------+
| previous_entry_length | encoding | content |
+-----------------------+----------+---------+

previous_entry_length:前面一个节点的长度(字节)。若前面一个节点的长度小于254字节,则该属性占1个字节的宽度,反正则是占5个字节的宽度,第一个字节是常量0xFE(254)

encoding:记录content的类型

content:保存节点的值

我们可以发现ziplist不能从头开始遍历,因为每个节点的长度都是不一样的,在遍历的时候需要根据zltail的值从尾部开始向前遍历

4.2.2 hashtable

hashtable就和Redis最外层的字典是差不多的了。

4.3 List

对于List同样也有两种编码:

  • ziplist:压缩列表
  • linkedlist:双向链表

满足如下条件时,压缩列表会被转换为双向链表:

  • 试图往列表新添加一个字符串值,且这个字符串的长度超过 server.list_max_ziplist_value (默认值为 64 )
  • ziplist 包含的节点超过 server.list_max_ziplist_entries (默认值为 512 )

Redis列表list 底层原理 - 知乎 (zhihu.com)

4.4 Set

Set拥有两种编码:

  • intset:使用数组维护set,数组是有序的
  • hashtable:直接使用哈希表维护set

满足如下条件时intset将会被转换成hashtable:

  • 保存了非整型的值
  • 元素数量超过了512个

4.4.1 intset

数据结构:

typedef struct intset {
    // 这个编码用来决定contents的大小
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

redis/intset.h at 971b177fa338fe06cb67a930c6e54467d29ec44f · redis/redis (github.com)

面试官:说说Redis中Set数据类型的底层实现 - 掘金 (juejin.cn)

4.5 zset(Sorted Set)

zset有两种实现:

  • zipList:压缩列表
  • skipList:跳表

满足如下条件时zipList会转换成skipList:

  • 节点数量大于等于128(server.zset_max_ziplist_entries)
  • 节点的长度大于等64(server.zset_max_ziplist_value)

Redis 跳跃表skiplist(深入理解,面试再也不用怕)_redis skiplist_妖四灵.Shuen的博客-CSDN博客

跳表

  • header:指向跳跃表的表头节点
  • tail:指向跳跃表的表尾节点
  • level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
  • length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)

节点数据结构:

typedef struct zskiplistNode {
    // 当前保存的值
    sds ele;
    // 分值,用于排序
    double score;
    // 前一个节点
    struct zskiplistNode *backward;
    // 当前层节点
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        // 跳表的跨度
        unsigned long span;
    } level[];
} zskiplistNode;

redis/server.h at 971b177fa338fe06cb67a930c6e54467d29ec44f · redis/redis (github.com)

// 跳表
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    // 跳表的长度
    unsigned long length;
    // 最高层数
    int level;
} zskiplist;

// zset数据结构
typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

5. Redis持久化方式

5.1 RDB

RDB即Redis Database,它会将Redis某一时刻的数据以文件的形式全量备份到磁盘。

Redis提供了两个指令来生成RDB,一个是SAVE,这个命令会阻塞主线程,直到RDB生成完毕。

另外一个则是BGSAVE,这时会fork一个子进程去专门负责写入RDB。

在读取数据时用到了写时复制(COW)技术,fork创建出的子进程,与父进程共享内存空间。也就是说,如果子进程不对内存空间进行写入操作的话,内存空间中的数据并不会复制给子进程,这样创建子进程的速度就很快了!

LInux fork的写时复制(copy on write)_fork写时复制_富士康质检员张全蛋的博客-CSDN博客

5.2 AOF

AOF即Append Only File,它会记录在redis服务器上执行过的命令来实现持久化的目的。

Redis详解(七)—— AOF 持久化 - YSOcean - 博客园 (cnblogs.com)

Redis默认每秒执行一次AOF的写入。

5.2.1 AOF重写

为了防止AOF文件过大,当AOF的文件大小超过设定的阈值后,Redis就会启动AOF的文件压缩。

压缩前:

sadd animals "cat"
sadd animals "dog"
sadd animals "cat"
sadd animals "lion"

压缩后:

sadd animals "cat" "dog" "lion"

6. 内存

6.1 内存过期策略

Redis对于过期的key有两种删除策略:

  • 定期删除
  • 惰性删除

6.1.1 定期删除

Redis会将每个设置了过期时间的key放到一个独立的字典中,以后会定期按照某种算法来遍历里面的key。

默认每秒进行10次扫描,每次会随机选取一些key,然后删除其中过期的key,若某次删除的数量超过了选取的1/4,则会重复这一步骤。

6.1.2 惰性删除

在客户端访问某个key时,若这个key过期,则会将其删除。

6.2 内存淘汰策略

当没有可以被删除的key,且Redis内存不足时,此时会根据内存淘汰策略删除一些没有过期的key。

  1. noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键

  2. allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键

  3. volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键

  4. allkeys-random:加入键的时候如果过限,从所有key随机删除

  5. volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐

  6. volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键

  7. volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键

  8. allkeys-lfu:从所有键中驱逐使用频率最少的键

关于LRU和LFU,在redisObject里会保存相关的参数:

struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
};

redis/README.md at cb1717865804fdb0561e728d2f3a0a1138099d9d · redis/redis (github.com)

  • 当使用LRU算法时,lru整个值都用来表示相关访问时间。
  • 当使用LFU算法时,低8位表示访问频率,高16位表示上一次访问时间

6.2.1 LRU

LRU即Least Recently Used-最近最少使用算法。

LRU会维护一个双向链表,对于新加入的数据或者最近被访问的数据,它们会被存/移动到链表头部,当内存不足的时候删除链表尾部的数据。

优点:

  • 好实现

缺点:

  • 冷数据可能会把热数据顶掉

6.2.2 LFU

LFU即Least Frequently Used-最不经常使用。其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。

LFU 缓存 - 提交记录 - 力扣(LeetCode)

Redis的LFU和上面的LFU有一些不一样,一般的LFU有如下缺点:

  • 新增的缓存容易被删除

而Redis在此基础上会将每个数据的访问次数设置为5,每次访问会根据其上次访问时间扣除一定的访问次数,然后再根据生成的一个随机数,决定是否对访问次数字段加一:

uint8_t LFULogIncr(uint8_t counter) {
    if (counter == 255) return 255;
    double r = (double)rand()/RAND_MAX;
    double baseval = counter - LFU_INIT_VAL;
    if (baseval &lt; 0) baseval = 0;
    // 访问次数越多,加一的概率越小
    double p = 1.0/(baseval*server.lfu_log_factor+1);
    if (r &lt; p) counter++;
    return counter;
}

其中server.lfu_log_factor默认为10。

因为访问计数器的长度为8位,最大为255,如果每次访问都加一,很可能会导致溢出。

16 | LFU算法和其他算法相比有优势吗? (geekbang.org)

7. Redis集群

一文搞懂 Redis 的三种集群方案 - 腾讯云开发者社区-腾讯云 (tencent.com)

Redis支持三种集群方案:

  • 主从复制
  • 哨兵模式
  • Cluster模式

7.1 主从复制

主从复制主要由一个主数据库与一个或多个从数据库实例组成。

客户端可对主数据库进行读写操作,对从数据库进行读操作,主数据库写入的数据会实时自动同步给从数据库。

具体工作机制为:

  1. slave启动后,向master发送SYNC命令,master接收到SYNC命令后通过bgsave保存快照,并使用缓冲区记录保存快照这段时间内执行的写命令
  2. master将保存的快照文件发送给slave,并继续记录执行的写命令
  3. slave接收到快照文件后,加载快照文件,载入数据
  4. master快照发送完后开始向slave发送缓冲区的写命令,slave接收命令并执行,完成复制初始化
  5. 此后master每次执行一个写命令都会同步发送给slave,保持master与slave之间数据的一致性

优点:

  • master能自动将数据同步到slave,可以进行读写分离,分担master的读压力
  • master、slave之间的同步是以非阻塞的方式进行的,同步期间,客户端仍然可以提交查询或更新请求

缺点:

  • 难以支持在线扩容,Redis的容量受限于单机配置

  • master宕机,如果宕机前数据没有同步完,则切换IP后会存在数据不一致的问题

  • 不具备自动容错与恢复功能,master或slave的宕机都可能导致客户端请求失败,需要等待机器重启或手动切换客户端IP才能恢复

7.2 哨兵模式

哨兵模式基于主从复制模式,只是引入了哨兵来监控与自动处理故障。

哨兵顾名思义,就是来为Redis集群站哨的,一旦发现问题能做出相应的应对处理。其功能包括:

  1. 监控master、slave是否正常运行
  2. 当master出现故障时,能自动将一个slave转换为master(大哥挂了,选一个小弟上位)
  3. 多个哨兵可以监控同一个Redis,哨兵之间也会自动监控

Redis哨兵(Sentinel)模式 - 简书 (jianshu.com)

优点:

  1. 哨兵模式基于主从复制模式,所以主从复制模式有的优点,哨兵模式也有
  2. 哨兵模式下,master挂掉可以自动进行切换,系统可用性更高

缺点:

  1. 同样也继承了主从模式难以在线扩容的缺点,Redis的容量受限于单机配置
  2. 需要额外的资源来启动sentinel进程,实现相对复杂一点,同时slave节点作为备份节点不提供服务

7.3 Cluster模式

Cluster采用无中心结构,它的特点如下:

  1. 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽
  2. 节点的fail是通过集群中超过半数的节点检测失效时才生效
  3. 客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可

Cluster模式的具体工作机制:

  1. 在Redis的每个节点上,都有一个插槽(slot),取值范围为0-16383
  2. 当我们存取key的时候,Redis会根据CRC16的算法得出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作
  3. 为了保证高可用,Cluster模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点
  4. 当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点都宕机了,那么该集群就无法再提供服务了

Cluster模式集群节点最小配置6个节点(3主3从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。

8. 分布式锁

面试必问的分布式锁,你懂了吗?_几年经验会问分布式锁_程序员囧辉的博客-CSDN博客

阅读全文

springboot源码

2023/3/23

1. 基础

通过AnnotationConfigApplicationContext可以创建一个Spring容器:

public class MySpringApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

        UserService userService = (UserService) applicationContext.getBean("userService");
        userService.test();
    }

}

@ComponentScan("pers.xds.springboot")
public class AppConfig {
}

1.1 生命周期

可以在AbstractAutowireCapableBeanFactory#doCreateBean中看到完整的bean生成流程。

大致分为如下过程:

1.对Bean进行实例化

2.依赖注入

3.如果Bean实现了BeanNameAware接口,Spring将调用setBeanName(),设置 Bean的 id(xml文件中bean标签的id)

4.如果Bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()

5.如果Bean实现了ApplicationContextAware接口,Spring容器将调用setApplicationContext()

6.如果存在BeanPostProcessor,Spring将调用它们的postProcessBeforeInitialization(预初始化)方法,在Bean初始化前对其进行处理

7.如果Bean实现了InitializingBean接口,Spring将调用它的afterPropertiesSet方法,然后调用xml定义的 init-method方法(初始化),两个方法作用类似,都是在初始化 bean 的时候执行

8.如果存在BeanPostProcessor,Spring将调用它们的postProcessAfterInitialization(后初始化)方法,在Bean初始化后对其进行处理

9.Bean初始化完成,供应用使用,直到应用被销毁

10.如果Bean实现了DisposableBean接口,Spring将调用它的destory方法,然后调用在xml中定义的 destory-method方法,这两个方法作用类似,都是在Bean实例销毁前执行。

1.2 BeanFactory和FactoryBean的区别

BeanFactory:管理Bean的容器,Spring中生成的Bean都是由这个接口的实现来管理的。

FactoryBean:让开发者以编程的方式来创建一个bean,一般用于创建比较复杂的bean。

1.3 Bean注入容器有哪些方式

1、使用@Configuration@Bean注解

2、使用@Controller@Service@Repository@Component 注解标注该类,然后启用@ComponentScan自动扫描

3、使用@Import 方法,使用@Import注解把bean导入到当前容器中。

1.4 Bean的作用域

1、singleton:单例,Spring中的bean默认都是单例的。

2、prototype:每次请求都会创建一个新的bean实例。

3、request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。

4、session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。

5、application:限定一个Bean的作用域为ServletContext的生命周期。该作用域仅适用于web的Spring WebApplicationContext环境。

1.5 自动装配的方式

@Autowired注解会优先根据类型来注入,当有多个bean时,会尝试根据变量名来注入(byname),如果没有找到就抛出异常。

可以通过@Qualifier来指定要注入的bean的名称。

@Resource注解会优先byname,找不到再byType。

1.6 @Bean和@Component的区别

@Bean只能作用于方法上,表示这个方法会返回一个Bean,一般需要配合@Configuration使用。

@Component只能作用于类型上,表示这个类会作为组件类,并告诉Spring要为这个类创建bean。

1.6.1 @Bean必须在@Configuration里使用吗?

Spring: @Bean can still work without @Configuration - Stack Overflow

@Bean@Configuration表示的类里使用时,Spring会为其自动创建一个动态代理对象,在同一个配置类中可以直接调用方法来获取Bean:

@Configuration
public class ExampleConfiguration {
    
    @Bean
    public Datasource datasource() {
        BasicDatasource datasource = new BasicDatasource();
        // ...
        return datasource;
    }
    
    public PlatformTransactionManager transactionManager() {
          // 注意这里是直接调用了方法,每次调用都会返回同一个bean,并不会多次创建
        return new DataSourceTransactionManager(datasource());
    }
    
}

而在非@Configuration下定义的@Bean会以Lite Mode运作,在该模式下调用其它@Bean方法时,则是普通的方法调用(没有代理对象去拦截调用)。

1.7 Spring怎么解决循环依赖问题

对于构造器注入的循环依赖:Spring处理不了,直接抛出BeanCurrentlylnCreationException异常。

非单例循环依赖:无法处理。

单例模式下属性注入的循环依赖会通过三级缓存处理循环依赖:

singletonObjects:完成了初始化的单例对象map,bean name –> bean instance

earlySingletonObjects:完成实例化未初始化的单例对象map,bean name –> bean instance

singletonFactories: 单例对象工厂map,bean name –> ObjectFactory,存放 bean 工厂对象

具体的执行逻辑可以在DefaultSingletonBeanRegistry中看到,这里贴出核心方法:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            synchronized(this.singletonObjects) {
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        ObjectFactory&lt;?&gt; singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }

    return singletonObject;
}

1.8 Spring的单例Bean是否有线程安全问题

当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑,如果业务逻辑有对单例状态的修改(体现为此单例的成员属性),则必须考虑线程安全问题。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,那么不会有线程安全问题;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

无状态bean和有状态bean

  • 有实例变量的bean,可以保存数据,是非线程安全的。
  • 没有实例变量的对象。不能保存数据,是线程安全的。

在Spring中无状态的Bean适合用单例模式,这样可以共享实例提高性能。有状态的Bean在多线程环境下不安全,一般用Prototype模式或者使用ThreadLocal解决线程安全问题。

1.9 Spring容器的启动过程

阿里面试真题:Spring容器启动流程_spring启动流程面试题_敖 丙的博客-CSDN博客

2. AOP

常见的动态代理有两种:

  • JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才生成代理对象。

  • CGLIB动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。

JDK Proxy的优势:

​ 最小化依赖关系、代码实现简单、简化开发和维护、JDK原生支持,比CGLIB更加可靠,随JDK版本平滑升级。而字节码类库通常需要进行更新以保证在新版Java上能够使用。

CGLIB的优势:

​ 无需实现接口,达到代理类无侵入,只操作关心的类,而不必为其他相关类增加工作量。高性能。

Java动态代理之一CGLIB详解 - 知乎 (zhihu.com)

3. Spring事务

可能是最漂亮的Spring事务管理详解 - 掘金 (juejin.cn)

4. MyBatis工作原理

一文搞懂Mybatis架构与工作原理 - 掘金 (juejin.cn)

  1. 加载映射文件(通过动态代理和xml为接口生成对应的代理类)

  2. 构造会话工程(SqlSessionFactory)

  3. 创建会话对象(SqlSession)

    try (SqlSession session = sqlSessionFactory.openSession()) {
      BlogMapper mapper = session.getMapper(BlogMapper.class);
      Blog blog = mapper.selectBlog(101);
    }
    
  4. Executor执行器

    Mybatis会通过Executor去执行SQL语句。一般这里面会有缓存的实现。

  5. MappedStatement对象

    对映射信息的封装,它存储了一个SQL对应的所有信息。Mybatis 通过解析 XML 和 mapper 接口上的注解,生成 sql 对应的 MappedStatement 实例

  6. 输入参数映射

  7. 输出参数映射

​ 将数据库输出转换为 MapList或自定义的类型

阅读全文

计网常用知识总结

2023/3/22

1. 网际协议IP

1.1 IPv4

IP报文格式

  • 版本:IPv4为4,IPv6即为6
  • 首部长度:单位为4字节,因此头部最长为15个4字节,即60字节
  • 区分服务(旧版叫服务类型):QoS分类和标记 - 知乎 (zhihu.com)
  • 总长度:总长度包括首部和数据的长度。
  • 标识:用于路由器分片重组。同一个分片的标志值相同,不同的分片的标识值不同。
  • 标志:占3位,但只两位可以使用
    • 最低位(可以理解为最右边):记为MF(More Fragment)。MF = 1标识后面还有分片
    • 中间位:记为DF(Don’t Fragment),意为不能分片,只有当DF = 0时才允许分片。
  • 片偏移:较长的分组在分片后,某片在原分组中的相对位置,单位是8字节。
    • 例如有一个数据有3800字节,被分为了1400字节、1400字节、1000字节(忽略了首部),则它们的片偏移分别为:0/8 = 01400 / 8 = 1752800 / 8 = 350
  • 生存时间:一般称为TTL(Time To Live),标明数据报在网络中的寿命。最初的设计是以作为TTL的单位,每经过一个路由器就减掉响应的时间,到零时则会被丢弃。然而随着技术的进步,路由器处理数据报所需的时间不断在缩短,一般都远远小于1秒,后来就把TTL字段的功能改为“跳数限制”,即每经过一个路由器,就将该值减一,到零时就删除掉,最大值为255
  • 协议:见下表
协议名 ICMP IGMP IP TCP EGP IGP UDP IPv6 ESP ICMP-IPv6
字段值 1 2 4 6 8 9 17 41 50 58
  • 首部校验和:这个字段只检验数据报的首部,但不包括数据部分。具体运算方式是把首部按16字节划分,然后按照反码算术相加得到。
  • 源地址:发送IP数据报主机的IP地址
  • 目的地址:占32位。接收IP数据报主机的IP地址
  • 选项:由于选项很难背用到,而且也会增加路由器的开销,因此在IPv6中,IP数据报首部长度被改为固定值了

1.2 IPv6

IPv6

  • 版本:IPv6肯定就是6了

  • 通信量类(Traffic Class):这是为了区分不同的IPv6数据报的类别或优先级,和IPv4的区分服务字段的作用相似。

  • 流标号(Flow Label):IPv6的一个新的机制是支持资源预分配,并且允许路由器把每个数据报与一个给定的资源分配相联系。

  • 有效荷载长度:它指明IPv6数据报除基本首部以为的字节数(所有扩展首部都算在有效荷载之内)。

  • 下一个首部:它相当于IPv4的协议字段或可选字段。

  • 跳数限制:用来防止数据报在网络中无期限的存在。

2. TCP

2.1 三次握手

深入浅出TCP三次握手 (多图详解) - 知乎 (zhihu.com)

主要是要记住客户端和服务端的状态:

  • 客户端:CLOSED -> SYN-SENT -> ESTABLISHED
  • 服务端:CLOSED -> SYN-RCVD -> ESTABLISHED

之后在每次通信时,下次消息的seq为对方上次消息的ack,下次消息的ack为对方上次消息的seq + 总数据长度(同一个seq可能会有多个消息)

在握手时,虽然数据长度为0,在理解时需要将其”当做”长度为1。

2.2 四次挥手

TCP四次挥手详解_‍oOoOoOooOO的博客-CSDN博客

客户端和服务端的状态:

  • 客户端(主动关闭的那一方):ESTABLISHED -> FIN_WAIT -> TIME_WAIT(等待2ms) -> CLOSE
  • 服务端(响应关闭的那一方):ESTABLISHED -> CLOSE_WAIT -> LAST_ACK -> CLOSE

这里在主动关闭的那一方需要等待2MSL,原因如下:

  • 首先占用该端口,因为IP报文在网络中的生存时间是有限的,让旧的报文全部在网络中被丢弃

  • 若主动关闭的那一方的ACK没有被服务端收到,此时服务端再次返回一个FIN报文,会被客户端错误的以为是一个错误报文而导致状态重置(发送RST报文)

TCP的TIME_WAIT状态 - 知乎 (zhihu.com)

2.3 TCP报文格式

TCP报文格式解析 (biancheng.net)

其中可能比较难记的就是8个标志位的前两个了,其实只需要记住全称就不容易忘了:

  • CWR:Congestion Window Reduce
  • ECE:ECN Echo,全称Explicit Congestin Notification Echo

里面还有个校验和,这里需要知道是怎么算的。在计算机网络第八版P218中介绍了UDP的检验和计算:

  • 首先在数据报前添加一个伪首部
  • 将校验和的位置置零
  • 以16位2个字节为单位,如果由于数据的长度导致无法满足该条件,则在数据部分后补零即可。之后将所有数据相加,如果溢出,则添加到低位(二进制反码运算求和)
  • 将结果取反添加到校验和的位置

在验证时同样使用上面的方法,但不用把校验位置零,若计算结果最终为全1,则表示数据无误

TCP/UDP伪头部详解_tcp 伪头部_M、k的博客-CSDN博客

需要注意的是IP报文的校验不包含数据。

3. 路由器和交换机

如果让你来设计网络 (qq.com)

交换机是工作在数据链路层的,它根据每台设备的MAC地址来转发数据。

而路由器是工作在网络层的,它负责IP报文的转发。

阅读全文

面试题记录2

2023/3/19
阅读全文
1 2 3 4
avatar
IceOfSummer

这个人很懒,没有个人简介