## 疑问
数据存储在硬盘,InnoDB如何高效读写?
页 + 行格式
数据存储在硬盘如何高效的利用空间?
行格式
## InnoDB页简介
InnoDB需要把数据存储到硬盘(持久性),但是真正处理数据的过程是发生在内存中的(内存速度快)。所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上。磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,`InnoDB`存储引擎不能一条一条的把去磁盘读取记录,需要更高效的方式。
`InnoDB`采取的方式是:将数据分为若干页,以 **页** 作为磁盘和内存之间交互的最小单位。InnoDB中页的大小一般为 16 KB。也就是一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
## InnoDB行格式
我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为`行格式`或者`记录格式`。设计`InnoDB`存储引擎的大叔们到现在为止设计了4种不同类型的`行格式`,分别是`Compact`、`Redundant`、`Dynamic`和`Compressed`行格式,随着时间的推移,他们可能会设计出更多的行格式,但是不管怎么变,在原理上大体都是相同的。
Compact 行格式是MySQL5.1后的默认格式。但在 MySQL 5.7.9 及以后版本,默认行格式是innodb_default_row_format变量决定,**默认值是 Dynamic**
### 指定行格式的语法
我们可以在创建或修改表的语句中指定`行格式`:
```
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
```
### COMPACT行格式
![](https://img.kancloud.cn/b3/b2/b3b24fdcb365c4ecef531bf79f70d0d2_783x250.png)
一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分
#### 记录的额外信息
这部分信息是服务器为了描述这条记录而不得不额外添加的一些信息,这些额外信息分为3类,分别是**变长字段长度列表、NULL值列表和记录头信息**。
##### 变长字段长度列表
变长字段占用的存储空间分为两部分:
1. 真正的数据内容
2. 占用的字节数
在`Compact`行格式中,所有的变长字段数据占用的字节长度都放在记录的开头部位, 形成一个变长字段长度列表, 各变长字段数据占用的字节数按照列的顺序**逆序存放**。变长字段长度列表中只存储值为 非NULL 的列内容占用的长度,值为 NULL 的列的长度是不储存的 。
对于***CHAR(M)***类型的列来说,当列采用的是定长字符集时,该列占用的字节数不会被加到变长字段长度列表,而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表。
### 行溢出数据
#### VARCHAR(M)最多能存储的数据
`VARCHAR(M)`类型的列最多可以占用`65535`个字节。其中的`M`代表该类型最多存储的字符数量,`MySQL`对一条记录占用的最大存储空间是有限制的,除了`BLOB`或者`TEXT`类型的列之外,**其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过`65535`个字节。**
存储一个`VARCHAR(M)`类型的列,其实需要占用3部分存储空间:
* 真实数据
* 真实数据占用字节的长度
* `NULL`值标识,如果该列有`NOT NULL`属性则可以没有这部分存储空间
如果该`VARCHAR`类型的列没有`NOT NULL`属性,那最多只能存储`65532`个字节的数据, 有则存储`65533`个字节的数据。
utf8字符集表示一个字符最多需要3个字节,那在该字符集下,M的最大取值就是21844(也就是:65532/3)个字符。
`MySQL`是以`页`为基本单位来管理存储空间的,页能存储16kb数据,也就是`16384`字节,而一个`VARCHAR(M)`类型的列就最多可以存储`65532`个字节,这样就可能造成一个页存放不了一条记录的尴尬情况。这种就会在`记录的真实数据`处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中,然后`记录的真实数据`处用20个字节存储指向这些页的地址。
### Dynamic和Compressed行格式
如果某一列中的数据非常多的话,在本记录的真实数据处只会存储该列的前`768`个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中,这个过程也叫做`行溢出`,存储超出`768`字节的那些页面也被称为`溢出页`。
`Dynamic`和`Compressed`行格式,我现在使用的`MySQL`版本是`5.7`,它的默认行格式就是`Dynamic`,这俩行格式和`Compact`行格式挺像,只不过在处理`行溢出`数据时有点儿分歧,它们不会在记录的真实数据处存储字段真实数据的前`768`个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址
`Compressed`行格式和`Dynamic`不同的一点是,`Compressed`行格式会采用压缩算法对页面进行压缩,以节省空间。
## InnoDB数据页结构
| 名称 | 中文名 | 占用空间大小 | 简单描述 |
| :-: | :-: | :-: | :-: |
| `File Header` | 文件头部 | `38`字节 | 页的一些通用信息 |
| `Page Header` | 页面头部 | `56`字节 | 数据页专有的一些信息 |
| `Infimum + Supremum` | 最小记录和最大记录 | `26`字节 | 两个虚拟的行记录 |
| `User Records` | 用户记录 | 不确定 | 实际存储的行记录内容 |
| `Free Space` | 空闲空间 | 不确定 | 页中尚未使用的空间 |
| `Page Directory` | 页面目录 | 不确定 | 页中的某些记录的相对位置 |
| `File Trailer` | 文件尾部 | `8`字节 | 校验页是否完整 |
## 记录在页中的存储
一开始生成页的时候,其实并没有`User Records`这个部分,每当我们插入一条记录,都会从`Free Space`部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到`User Records`部分,当`Free Space`部分的空间全部被`User Records`部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。
### 记录头信息
| 名称 | 大小(单位:bit) | 描述 |
| :-: | :-: | :-: |
| `预留位1` | `1` | 没有使用 |
| `预留位2` | `1` | 没有使用 |
| `delete_mask` | `1` | 标记该记录是否被删除 |
| `min_rec_mask` | `1` | B+树的每层非叶子节点中的最小记录都会添加该标记 |
| `n_owned` | `4` | 表示当前记录拥有的记录数 |
| `heap_no` | `13` | 表示当前记录在记录堆的位置信息 |
| `record_type` | `3` | 表示当前记录的类型,`0`表示普通记录,`1`表示B+树非叶节点记录(目录项记录),`2`表示最小记录,`3`表示最大记录 |
| `next_record` | `16` | 表示下一条记录的相对位置 |
* delete_mask
这个属性标记着当前记录是否被删除,占用1个二进制位,值为`0`的时候代表记录并没有被删除,为`1`的时候代表记录被删除掉了。这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗,所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
* min_rec_mask
B+树的每层非叶子节点中的最小记录都会添加该标记
* n_owned
这个分组中的记录数
* heap_no
这个属性表示当前记录在本页中的位置,值从 2 开始, 0和1分别被自动插入的最小记录和最大记录占用了,也就是 Infimum + Supremum 的部分
* record_type
当前记录的类型,一共有4种类型的记录,`0`表示普通记录,`1`表示B+树非叶节点记录,`2`表示最小记录,`3`表示最大记录。
* next_record
非常重要,它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量
## Page Directory(页目录)
![](https://img.kancloud.cn/37/93/3793d641bb414997bc18fafe604aba28_1011x540.png)
从这个图中我们需要注意这么几点:
* 现在`页目录`部分中有两个槽,也就意味着我们的记录被分成了两个组,`槽1`中的值是`112`,代表最大记录的地址偏移量(就是从页面的0字节开始数,数112个字节);`槽0`中的值是`99`,代表最小记录的地址偏移量。
* 注意最小和最大记录的头信息中的`n_owned`属性
* 最小记录的`n_owned`值为`1`,这就代表着以最小记录结尾的这个分组中只有`1`条记录,也就是最小记录本身。
* 最大记录的`n_owned`值为`5`,这就代表着以最大记录结尾的这个分组中只有`5`条记录,包括最大记录本身还有我们自己插入的`4`条记录。
每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有***1***条记录,最大记录所在的分组拥有的记录条数只能在***1~8***条之间,剩下的分组中记录的条数范围只能在是***4~8***条之间。
## Page Header(页面头部)
比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等
| 名称 | 占用空间大小 | 描述 |
| :-: | :-: | :-: |
| `PAGE_N_DIR_SLOTS` | `2`字节 | 在页目录中的槽数量 |
| `PAGE_HEAP_TOP` | `2`字节 | 还未使用的空间最小地址,也就是说从该地址之后就是`Free Space` |
| `PAGE_N_HEAP` | `2`字节 | 本页中的记录的数量(包括最小和最大记录以及标记为删除的记录) |
| `PAGE_FREE` | `2`字节 | 第一个已经标记为删除的记录地址(各个已删除的记录通过`next_record`也会组成一个单链表,这个单链表中的记录可以被重新利用) |
| `PAGE_GARBAGE` | `2`字节 | 已删除记录占用的字节数 |
| `PAGE_LAST_INSERT` | `2`字节 | 最后插入记录的位置 |
| `PAGE_DIRECTION` | `2`字节 | 记录插入的方向 |
| `PAGE_N_DIRECTION` | `2`字节 | 一个方向连续插入的记录数量 |
| `PAGE_N_RECS` | `2`字节 | 该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录) |
| `PAGE_MAX_TRX_ID` | `8`字节 | 修改当前页的最大事务ID,该值仅在二级索引中定义 |
| `PAGE_LEVEL` | `2`字节 | 当前页在B+树中所处的层级 |
| `PAGE_INDEX_ID` | `8`字节 | 索引ID,表示当前页属于哪个索引 |
| `PAGE_BTR_SEG_LEAF` | `10`字节 | B+树叶子段的头部信息,仅在B+树的Root页定义 |
| `PAGE_BTR_SEG_TOP` | `10`字节 | B+树非叶子段的头部信息,仅在B+树的Root页定义 |
* `PAGE_DIRECTION`
假如新插入的一条记录的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右边,反之则是左边。用来表示最后一条记录插入方向的状态就是`PAGE_DIRECTION`。
* `PAGE_N_DIRECTION`
假设连续几次插入新记录的方向都是一致的,`InnoDB`会把沿着同一个方向插入记录的条数记下来,这个条数就用`PAGE_N_DIRECTION`这个状态表示。当然,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重新统计。
## File Header(文件头部)
`File Header`针对各种类型的页都通用,也就是说不同类型的页都会以`File Header`作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁
| 名称 | 占用空间大小 | 描述 |
| :-: | :-: | :-: |
| `FIL_PAGE_SPACE_OR_CHKSUM` | `4`字节 | 页的校验和(checksum值) |
| `FIL_PAGE_OFFSET` | `4`字节 | 页号 |
| `FIL_PAGE_PREV` | `4`字节 | 上一个页的页号 |
| `FIL_PAGE_NEXT` | `4`字节 | 下一个页的页号 |
| `FIL_PAGE_LSN` | `8`字节 | 页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number) |
| `FIL_PAGE_TYPE` | `2`字节 | 该页的类型 |
| `FIL_PAGE_FILE_FLUSH_LSN` | `8`字节 | 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值 |
| `FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID` | `4`字节 | 页属于哪个表空间 |
* `FIL_PAGE_SPACE_OR_CHKSUM`
这个代表当前页面的校验和(checksum)。啥是个校验和?就是对于一个很长很长的字节串来说,我们会通过某种算法来计算一个比较短的值来代表这个很长的字节串,这个比较短的值就称为`校验和`。这样在比较两个很长的字节串之前先比较这两个长字节串的校验和,如果校验和都不一样两个长字节串肯定是不同的,所以省去了直接比较两个比较长的字节串的时间损耗。
* `FIL_PAGE_OFFSET`
每一个`页`都有一个单独的页号,就跟你的身份证号码一样,`InnoDB`通过页号来可以唯一定位一个`页`。
* `FIL_PAGE_TYPE`
这个代表当前`页`的类型,我们前边说过,`InnoDB`为了不同的目的而把页分为不同的类型,我们上边介绍的其实都是存储记录的`数据页`,其实还有很多别的类型的页
| 类型名称 | 十六进制 | 描述 |
| :-: | :-: | :-: |
| `FIL_PAGE_TYPE_ALLOCATED` | 0x0000 | 最新分配,还没使用 |
| `FIL_PAGE_UNDO_LOG` | 0x0002 | Undo日志页 |
| `FIL_PAGE_INODE` | 0x0003 | 段信息节点 |
| `FIL_PAGE_IBUF_FREE_LIST` | 0x0004 | Insert Buffer空闲列表 |
| `FIL_PAGE_IBUF_BITMAP` | 0x0005 | Insert Buffer位图 |
| `FIL_PAGE_TYPE_SYS` | 0x0006 | 系统页 |
| `FIL_PAGE_TYPE_TRX_SYS` | 0x0007 | 事务系统数据 |
| `FIL_PAGE_TYPE_FSP_HDR` | 0x0008 | 表空间头部信息 |
| `FIL_PAGE_TYPE_XDES` | 0x0009 | 扩展描述页 |
| `FIL_PAGE_TYPE_BLOB` | 0x000A | 溢出页 |
| `FIL_PAGE_INDEX` | 0x45BF | 索引页,也就是我们所说的`数据页` |
我们存放记录的数据页的类型其实是`FIL_PAGE_INDEX`,也就是所谓的`索引页`。
* `FIL_PAGE_PREV`和`FIL_PAGE_NEXT`
`InnoDB`可能不可以一次性为这么多数据分配一个非常大的存储空间,如果分散到多个不连续的页中存储的话需要把这些页关联起来,`FIL_PAGE_PREV`和`FIL_PAGE_NEXT`就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了,而无需这些页在物理上真正连着。
## File Trailer
每个页的尾部都加了一个`File Trailer`部分,这个部分由`8`个字节组成,校验页是否完整
* 前4个字节代表页的校验和
这个部分是和`File Header`中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为`File Header`在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。二者不同则意味着同步中间出了错。
* 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
这个`File Trailer`与`File Header`类似,都是所有类型的页通用的。
## 总结
1. InnoDB为了不同的目的而设计了不同类型的页,我们把用于存放记录的页叫做`数据页`。
2. 一个数据页可以被大致划分为7个部分,分别是
* `File Header`,表示页的一些通用信息,占固定的38字节。
* `Page Header`,表示数据页专有的一些信息,占固定的56个字节。
* `Infimum + Supremum`,两个虚拟的伪记录,分别表示页中的最小和最大记录,占固定的`26`个字节。
* `User Records`:真实存储我们插入的记录的部分,大小不固定。
* `Free Space`:页中尚未使用的部分,大小不确定。
* `Page Directory`:页中的某些记录相对位置,也就是各个槽在页面中的地址偏移量,大小不固定,插入的记录越多,这个部分占用的空间越多。
* `File Trailer`:用于检验页是否完整的部分,占用固定的8个字节。
3. 每个记录的头信息中都有一个`next_record`属性,从而使页中的所有记录串联成一个`单链表`。
4. `InnoDB`会把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个`槽`,存放在`Page Directory`中,所以在一个页中根据主键查找记录是非常快的,分为两步:
* 通过二分法确定该记录所在的槽。
* 通过记录的next\_record属性遍历该槽所在的组中的各个记录。
5. 每个数据页的`File Header`部分都有上一个和下一个页的编号,所以所有的数据页会组成一个`双链表`。
6. 为保证从内存中同步到磁盘的页的完整性,在页的首部和尾部都会存储页中数据的校验和和页面最后修改时对应的`LSN`值,如果首部和尾部的校验和和`LSN`值校验不成功的话,就说明同步过程出现了问题。
- 学习地址
- MySQL
- 查询优化
- SQL优化
- 关于or、in、not in、!=等走不走索引的说明
- 千万级数据查询优化
- MySQL 深度分页问题
- 嵌套循环 Block Nested Loop 导致索引查询慢
- MySQL增加日志统计表优化各种日志表的统计功能
- MySQL单机读写QPS(性能)优化
- sqlMode 置 select 的值可以比 group 里的多
- drop、delete、truncate的区别
- 尚硅谷MySQL数据库高级学习笔记
- MySQL架构
- 事务部分
- MySQL知识点
- mysql索引
- Linux docker安装 mysql 8.0.25
- docker 安装mysql 5.7
- mysql Field ‘xxx’ doesn’t have a default value
- mysql多实例
- docker中的sql文件导入
- mysql进阶知识
- mysql字符集
- 连接的原理
- redo日志
- InnoDB存储引擎
- InnoDB的数据存储结构
- B+树索引
- 文件系统-表空间
- Buffer Pool
- 亿级数据导入到es
- MySQL数据复制
- MySQL缺少主键的表数据
- mysql update 其中更新的字段根据另一个更新字段作为条件去更新
- MySQL指定字段值排序(将指定值排在前面)
- 设置MySQL连接数、时区
- Navicat15右键删除数据刷新就又恢复了
- MySQL替换字段部分内容
- Java和MySQL统计本周本月本季和年
- 分页时order by 排序数据重复,丢失
- mysql同一张表根据某个字段删除重复数据
- mysqldump定时全量热备
- 专题总结
- 事务
- MySQL事务
- spring事务
- spring事务本类调用
- spring事务传播行为
- spring事务失效问题
- 锁和Transactional注解一块使用的问题
- 数据安全
- 敏感数据
- SQL注入
- 数据源
- XSS
- 接口设计
- 缓存设计
- 限流
- 自定义注解实现根据用户做QPS限流
- 架构
- 高可用
- Java
- Unsatisfied dependency expressed through field ‘baseMapper‘
- mybatisplus多数据源
- 单个字母前缀的java变量
- spring
- spring循环依赖解决
- 事务@Transactional
- yml 文件配置信息绑定到java工具类的静态变量上
- @Configuration @Component 区别
- springboot启动yml文件报错
- spring方法重试注解Retryable
- spring读取yml集合数据
- spring自定义注解
- 获取resource下的图片资源
- 手机号和电话号的正则验证
- 获取字符串中的数字
- mybatis
- mybatis多参数添加数据并返回主键
- 统一异常处理
- 分组校验
- Java读取Python json.dumps 函数保存的redis数据
- springboot整合springCache
- 若依mybatis值为null的字段没有返回
- 若依
- 接口白名单
- @JsonFormat时区问题
- RequestParam.value() was empty on parameter 0
- jdk8和hutool请求第三方的https报错
- springMVC
- springMVC与vue使用post传数组
- elementUI 时间组件报错问题
- vue具名插槽slot
- springboot配置maven的profiles(配置微服务多环境切换打包)
- resources 配置文件读取顺序
- Windows的cmd部署jar注意事项
- Java基础
- JUC(锁-并发-线程池)
- CAS
- Java 锁简介
- synchronized和Logk有什么区别?用新的ock有什么好处
- synchronized锁介绍
- CompletableFuture
- 多线程
- 线程池
- 集合类
- map见过的小问题
- 退出双层循环
- StringBuilder和StringBuffer核心区别
- 日志打印
- 打印log日志
- log日志文件生成配置
- 日期时间
- 时间戳转为时间
- 并发工具
- 连接池
- http调用
- 内网访问天地图
- 判等问题
- 数值计算
- null问题
- 异常处理
- 文件IO
- 序列化
- 内存溢出OOM
- 子线程的错误, 全局异常处理捕获不到
- vue同一个项目访问多个不同ip地址接口
- Autowired注解导入为null
- shiro
- UnavailableSecurityManagerException错误
- Windows服务器80端口被占用
- java图片增加水印
- springcloud
- Feign方法配置错误导致jar包启动失败
- feign调用超时
- 定时任务quartz
- JavaPOI导出Excel
- 合并行和列
- 设置样式
- 设置背景色
- docker
- Linux 安装
- docker命令
- docker网络
- docker数据卷
- dockerfile
- docker安装ping命令
- docker-compose
- docker-compose文件内容介绍
- Linux关闭docker开机启动
- jar打包为镜像
- 迁移docker容器存储位置
- Nginx
- Linux在线安装Nginx
- nginx.conf 核心配置文件
- vue 和 nginx 刷新页面会报404
- nginx 转发给三个集群的tomcat
- ServerName匹配规则
- Nginx负载均衡策略
- location 匹配规则
- Nginx 搭建前端调用后台接口的集群
- alias与root
- nginx 拦截 post 请求, 带参数转发到前端页面
- 防盗链配置
- Nginx的缓存
- 通用Nginx配置
- nginx配置文件服务器
- 后台jar包得不到正确ip,nginx代理时要处理
- 升级使用websocket协议
- 设置IP黑/白名单
- Redis
- 缓存数据一致性
- 内存淘汰策略
- Redis数据类型
- gmt6
- Linux安装GMT6
- GMT6配置中文
- GMT文件修改Windows版本到Linux版本
- 注意GMT不同字体导致符号不同的问题
- GMT绘制南海诸岛小图
- GMT生成中文图例
- elasticsearch
- 安装配置
- Linux安装配置elasticsearch7.6.2
- Linux 安装 kibana 7.6.2
- 安装7.6.2中文分词器
- docker 安装elasticsearch7.6.2
- 安装Logback7.6.2
- springboot使用
- 0. elasticsearch账号密码模式访问
- 1. 配置连接
- 2. 索引
- 3. 批量保存更新
- Result window is too large 10000
- elasticsearch 分词的字段做排序 fielddata, 设置fielddata=true 无效果
- elasticsearch 完全匹配查询(精确查询)
- 模糊搜索
- 日期区间查询
- 6.x基础知识
- 自定义词库
- elasticsearch集群
- 搜索推荐Suggester
- 查询es保存的数组
- 亿级mysql数据导入到es
- es 报错 ORBIDDEN/12/index read-only
- es核心概念
- es的分布式架构原理
- 优化大数据量时的ES查询性能
- canal
- 1. mysql的Binlog
- 2. Canal 的工作原理
- 3. canal同步es
- JVM
- 1 类的字节码
- 2. 类的加载
- JVM知识点
- Maven
- 依赖冲突
- xxl-job
- docker 安装配置 xxl-job
- idea
- springboot启动报错命令过长
- services统一启动微服务各模块
- 云服务器安装宝塔面板
- 突然出现启动或者运行特别慢
- 有导入依赖但是显示红色同时点击进去也有依赖
- Linux
- sh文件执行报错: command not found
- 使用vagrant安装虚拟机
- Linux 开启端口
- 开放端口
- 复制文件夹及其文件到另一个文件夹
- 两个服务器之间映射端口
- TCP协议
- 分层模型
- TCP概述
- 支撑 TCP 协议的基石 —— 首部字段
- 数据包大小对网络的影响 —— MTU 与 MSS 的奥秘
- 端口号
- 三次握手
- TCP 自连接
- 四次挥手
- TCP 头部时间戳
- 分布式
- 分布式脑裂问题
- 分布式事务
- 基础知识
- 实现分布式事务的方案
- 阿里分布式事务中间件seata
- 幂等性问题
- 其他工具
- webstorm git提交代码后project目录树不显示
- 消息队列
- 如何保证消费的顺序
- 数据结构
- 漫画算法:小灰的算法之旅