Skip to content

Commit e60e7ff

Browse files
committed
Update Java Notes
1 parent 8418647 commit e60e7ff

File tree

2 files changed

+60
-39
lines changed

2 files changed

+60
-39
lines changed

DB.md

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5822,19 +5822,21 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一
58225822

58235823
#### 回滚日志
58245824

5825-
undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment
5825+
undo log 是采用段的方式来记录,Rollback Segement 称为回滚段,本质上就是一个类型是 Rollback Segement Header 的页面
58265826

5827-
Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot
5827+
每个回滚段中有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号,每个链表对应一个叫 undo log segment 的段
58285828

58295829
* 在以前老版本,只支持 1 个 Rollback Segement,只能记录 1024 个 undo log segment
58305830
* MySQL5.5 开始支持 128 个 Rollback Segement,支持 128*1024 个 undo 操作
58315831

58325832
工作流程:
58335833

58345834
* 事务执行前需要到系统表空间第 5 号页面中分配一个回滚段(页),获取一个 Rollback Segement Header 页面的地址
5835-
* 回滚段页面有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号。首先去回滚段的两个 cached 链表看是否有缓存的 slot,缓存中没有就在回滚段中找一个可用的 slot
5836-
* 缓存中获取的 slot 对应的 Undo Log Segment 已经分配了,需要重新分配,然后从 Undo Log Segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中
5837-
* 开始记录
5835+
* 回滚段页面有 1024 个 undo slot,首先去回滚段的两个 cached 链表获取缓存的 slot,缓存中没有就在回滚段页面中找一个可用的 undo slot 分配给当前事务
5836+
* 如果是缓存中获取的 slot,则该 slot 对应的 undo log segment 已经分配了,需要重新分配,然后从 undo log segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中
5837+
* 每个事务 undo 日志在记录的时候占用两个 undo 页面的组成链表,分别为 insert undo 链表和 update undo 链表,链表的头节点页面为 first undo page 会包含一些管理信息,其他页面为 normal undo page
5838+
5839+
说明:事务执行过程的临时表也需要两个 undo 链表,不和普通表共用,这些链表并不是事务开始就分配,而是按需分配
58385840

58395841

58405842

@@ -6114,17 +6116,28 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机
61146116

61156117
##### 日志缓冲
61166118

6117-
服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 log buffer 的大小,默认是 16MB
6118-
6119-
补充知识:MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR
6119+
服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 redo log buffer 的大小,默认是 16MB
61206120

61216121
log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成
61226122

61236123
* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作,写入 log buffer 的过程是顺序写入的(先写入前面的 block,写满后继续写下一个)
6124-
* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(碰撞指针)
6125-
* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,所以并不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer**,在进行数据恢复时也把这一组 redo log 当作一个不可分割的整体处理
6124+
* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(**碰撞指针**
6125+
6126+
MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR
6127+
6128+
* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,在进行数据恢复时也把一组 redo log 当作一个不可分割的整体处理
6129+
6130+
* 所以不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer**
6131+
6132+
6133+
6134+
***
61266135

6127-
redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快:
6136+
6137+
6138+
##### 日志刷盘
6139+
6140+
redo log 需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快,原因:
61286141

61296142
* 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO
61306143
* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO
@@ -6139,31 +6152,39 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到
61396152
* 服务器关闭时
61406153
* checkpoint 时(下小节详解)
61416154

6155+
redo 日志在磁盘中以文件组的形式存储,同一组中的每个文件大小一样格式一样,
61426156

6157+
* `innodb_log_group_home_dir` 代表磁盘存储 redo log 的文件目录,默认是当前数据目录
6158+
* `innodb_log_file_size` 代表文件大小,默认 48M,`innodb_log_files_in_group` 代表文件个数,默认 2 最大 100,所以日志的文件大小为 `innodb_log_file_size * innodb_log_files_in_group`
61436159

6144-
***
6160+
redo 日志文件也是由若干个 512 字节的 block 组成,日志文件的前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的用来存储 log buffer 中的 block 镜像
61456161

6162+
注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖于 MTR 的大小
61466163

6164+
服务器启动后 redo 磁盘空间不变,所以 redo 磁盘中的日志文件是被**循环使用**的,采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘
61476165

6148-
##### 磁盘文件
61496166

6150-
redo 存储在磁盘中的日志文件是被**循环使用**的,redo 日志文件组中每个文件大小一样格式一样,组成结构:前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的存储 log buffer 中的 block 镜像
61516167

6152-
注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖 MTR 的大小
6168+
***
61536169

6154-
磁盘存储 redo log 的文件目录通过 `innodb_log_group_home_dir` 指定,默认是当前数据目录,文件大小:
61556170

6156-
* 通过两个参数调节:`innodb_log_file_size` 文件大小默认 48M,`innodb_log_files_in_group` 文件个数默认 2 最大 100
6157-
* 服务器启动后磁盘空间不变,所以采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘
6171+
6172+
##### 日志序号
61586173

61596174
lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk_lsn 指刷新到磁盘中的 redo 日志量,两者都是**全局变量**,如果两者的值相同,说明 log buffer 中所有的 redo 日志都已经持久化到磁盘
61606175

6176+
工作过程:写入 log buffer 数据时,buf_free 会进行偏移,偏移量就会加到 lsn 上
6177+
61616178
MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中,链表中脏页是按照第一次修改的时间进行排序的(头插),控制块中有两个指针用来记录脏页被修改的时间:
61626179

61636180
* oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的
61646181
* newest_modification:每次修改页面,都将 MTR **结束时**对应的 lsn 值写入这个属性,所以是该页面最后一次修改后对应的 lsn 值
61656182

6166-
全局变量 checkpoint_lsn 表示当前系统中可以被覆盖的 redo 日志量,当 redo 日志对应的脏页已经被刷新到磁盘后就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint 并不是同一个线程
6183+
全局变量 checkpoint_lsn 表示当前系统可以被覆盖的 redo 日志总量,当 redo 日志对应的脏页已经被刷新到磁盘后,该文件空间就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint 并不是同一个线程
6184+
6185+
**checkpoint**:从 flush 链表尾部中找出还未刷脏的页面,该页面是当前系统中最早被修改的脏页,该页面之前产生的脏页都已经刷脏,然后将该页 oldest_modification 值赋值给 checkpoint_lsn,因为 lsn 小于该值时产生的 redo 日志都可以被覆盖了
6186+
6187+
checkpoint_lsn 是一个总量,随着 lsn 写入的增加,刷脏的继续进行,所以 checkpoint_lsn 值就会一直变大,该值的增量就代表磁盘文件中当前位置向后可以被覆盖的文件的量
61676188

61686189
在系统忙碌时,后台线程的刷脏操作不能将脏页快速刷出,导致系统无法及时执行 checkpoint,这时需要用户线程从 flush 链表中把最早修改的脏页刷新到磁盘中,然后执行 checkpoint
61696190

@@ -6192,7 +6213,7 @@ SHOW ENGINE INNODB STATUS\G
61926213

61936214
问题:系统崩溃前没有提交的事务的 redo log 可能已经刷盘,这些数据可能在重启后也会恢复
61946215

6195-
解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,定位到 128 个回滚段,遍历 slot,获取 undo 链表首节点页面的 Undo Segement Header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,如果是活跃的就全部回滚
6216+
解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,定位到 128 个回滚段,遍历 slot,获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,如果是活跃的就全部回滚
61966217

61976218

61986219

@@ -10454,7 +10475,7 @@ RDB三种启动方式对比:
1045410475
1045510476
#### 总结
1045610477
10457-
* RDB特殊启动形式的指令(客户端输入)
10478+
* RDB 特殊启动形式的指令(客户端输入)
1045810479
1045910480
* 服务器运行过程中重启
1046010481
@@ -10472,17 +10493,17 @@ RDB三种启动方式对比:
1047210493
1047310494
* 全量复制:主从复制部分详解
1047410495
10475-
* RDB优点
10496+
* RDB 优点
1047610497
- RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低
1047710498
- RDB 内部存储的是 redis 在某个时间点的数据快照,非常**适合用于数据备份,全量复制**等场景
1047810499
- RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复
10479-
- 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复
1048010500
10481-
* RDB缺点
10501+
* RDB 缺点
1048210502
1048310503
- bgsave 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能
1048410504
- RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有丢失数据的可能性,最后一次持久化后的数据可能丢失
1048510505
- Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容
10506+
* 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复
1048610507
1048710508
1048810509
@@ -10539,7 +10560,7 @@ AOF 持久化数据的三种策略(appendfsync):
1053910560
* 同步硬盘操作依赖于系统调度机制,比如缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失
1054010561
* fsync 针对单个文件操作(比如 AOF 文件)做强制硬盘同步,fsync 将阻塞到写入硬盘完成后返回,保证了数据持久化
1054110562
10542-
异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 redis,然后重新加载
10563+
异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 Redis,然后重新加载
1054310564
1054410565
1054510566
@@ -10678,7 +10699,7 @@ AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢
1067810699
- 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF
1067910700
- 如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB
1068010701
- 灾难恢复选用 RDB
10681-
- 双保险策略,同时开启 RDB和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量
10702+
- 双保险策略,同时开启 RDB 和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量
1068210703
- 不建议单独用 AOF,因为可能会出现 Bug,如果只是做纯内存缓存,可以都不用
1068310704
1068410705
@@ -10731,8 +10752,8 @@ fpid 的值在父子进程中不同:进程形成了链表,父进程的 fpid
1073110752
int main ()
1073210753
{
1073310754
pid_t fpid; // fpid表示fork函数返回的值
10734-
int count=0;
10735-
fpid=fork();
10755+
int count = 0;
10756+
fpid = fork();
1073610757
if (fpid < 0)
1073710758
printf("error in fork!");
1073810759
else if (fpid == 0) {
@@ -10761,16 +10782,16 @@ int main ()
1076110782
#include <stdio.h>
1076210783
int main(void)
1076310784
{
10764-
int i=0;
10785+
int i = 0;
1076510786
// ppid 指当前进程的父进程pid
1076610787
// pid 指当前进程的pid,
1076710788
// fpid 指fork返回给当前进程的值,在这可以表示子进程
10768-
for(i=0; i<2; i++){
10789+
for(i = 0; i < 2; i++){
1076910790
pid_t fpid = fork();
1077010791
if(fpid == 0)
10771-
printf("%d child %4d %4d %4d/n",i,getppid(),getpid(),fpid);
10792+
printf("%d child %4d %4d %4d/n",i, getppid(), getpid(), fpid);
1077210793
else
10773-
printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);
10794+
printf("%d parent %4d %4d %4d/n",i, getppid(), getpid(),fpid);
1077410795
}
1077510796
return 0;
1077610797
}

Frame.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2505,7 +2505,7 @@ Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如
25052505
+--------+-------------------------------------------------+----------------+
25062506
```
25072507

2508-
解决方法:通过调整系统的接受缓冲区的滑动窗口和 Netty 的接受缓冲区保证每条包只含有一条数据,滑动窗口大小,仅决定了 Netty 读取的**最小单位**,实际每次读取的一般是它的整数倍
2508+
解决方法:通过调整系统的接受缓冲区的滑动窗口和 Netty 的接受缓冲区保证每条包只含有一条数据,滑动窗口的大小仅决定了 Netty 读取的**最小单位**,实际每次读取的一般是它的整数倍
25092509

25102510

25112511

@@ -2540,13 +2540,13 @@ public class HelloWorldClient {
25402540

25412541
#### 固定长度
25422542

2543-
服务器端加入定长解码器,每一条消息采用固定长度,缺点浪费空间
2543+
服务器端加入定长解码器,每一条消息采用固定长度。如果是半包消息,会缓存半包消息并等待下个包到达之后进行拼包合并,直到读取一个完整的消息包;如果是粘包消息,空余的位置会进行补 0,会浪费空间
25442544

25452545
```java
25462546
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
25472547
@Override
25482548
protected void initChannel(SocketChannel ch) throws Exception {
2549-
ch.pipeline().addLast(new FixedLengthFrameDecoder(8));
2549+
ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
25502550
// LoggingHandler 用来打印消息
25512551
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
25522552
}
@@ -2692,7 +2692,7 @@ public class LengthFieldDecoderDemo {
26922692

26932693
#### HTTP协议
26942694

2695-
访问URLhttp://localhost:8080/
2695+
访问 URLhttp://localhost:8080/
26962696

26972697
```java
26982698
public class HttpDemo {
@@ -4537,9 +4537,9 @@ RocketMQ 的工作流程:
45374537
- 启动 NameServer 监听端口,等待 BrokerProducerConsumer 连上来,相当于一个路由控制中心
45384538
- Broker 启动,跟所有的 NameServer 保持长连接,每隔 30s 时间向 NameServer 上报 Topic 路由信息(心跳包)。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 TopicBroker 的映射关系
45394539
- 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic
4540-
- Producer 启动时先跟 NameServer 集群中的**其中一台**建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,同时 Producer 会默认每隔 30s 向 NameServer 拉取一次路由信息
4540+
- Producer 启动时先跟 NameServer 集群中的**其中一台**建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,同时 Producer 会默认每隔 30s 向 NameServer **定时拉取**一次路由信息
45414541
- Producer 发送消息时,根据消息的 Topic 从本地缓存的 TopicPublishInfoTable 获取路由信息,如果没有则会从 NameServer 上重新拉取并更新,轮询队列列表并选择一个队列 MessageQueue,然后与队列所在的 Broker 建立长连接,向 Broker 发消息
4542-
- ConsumerProducer 类似,跟其中一台 NameServer 建立长连接获取路由信息,根据当前订阅 Topic 存在哪些 Broker 上,直接跟 Broker 建立连接通道,在完成客户端的负载均衡后,选择其中的某一个或者某几个 MessageQueue 来拉取消息并进行消费
4542+
- ConsumerProducer 类似,跟其中一台 NameServer 建立长连接,定时获取路由信息,根据当前订阅 Topic 存在哪些 Broker 上,直接跟 Broker 建立连接通道,在完成客户端的负载均衡后,选择其中的某一个或者某几个 MessageQueue 来拉取消息并进行消费
45434543

45444544

45454545

@@ -4581,7 +4581,7 @@ RocketMQ 的工作流程:
45814581

45824582
#### 通信原理
45834583

4584-
==todo:后期学习了源码会进行扩充,现在暂时 copy 官方文档==
4584+
==todo:后期对 Netty 有了更深的认知后会进行扩充,现在暂时 copy 官方文档==
45854585

45864586
RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response
45874587

0 commit comments

Comments
 (0)