Skip to content

Commit bda5ab0

Browse files
committed
update: redis 手写跳表代码优化&mysql redo log 刷盘时间完善
1 parent da114b0 commit bda5ab0

File tree

3 files changed

+54
-111
lines changed

3 files changed

+54
-111
lines changed

docs/database/mysql/mysql-logs.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@ MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一
4141
4242
### 刷盘时机
4343

44-
InnoDB 刷新重做日志的时机有几种情况
45-
46-
InnoDB 将 redo log 刷到磁盘上有几种情况:
47-
48-
1. 事务提交:当事务提交时,log buffer 里的 redo log 会被刷新到磁盘(可以通过`innodb_flush_log_at_trx_commit`参数控制,后文会提到)
49-
2. log buffer 空间不足时:log buffer 中缓存的 redo log 已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上
50-
3. 事务日志缓冲区满:InnoDB 使用一个事务日志缓冲区(transaction log buffer)来暂时存储事务的重做日志条目。当缓冲区满时,会触发日志的刷新,将日志写入磁盘
51-
4. Checkpoint(检查点):InnoDB 定期会执行检查点操作,将内存中的脏数据(已修改但尚未写入磁盘的数据)刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性
52-
5. 后台刷新线程:InnoDB 启动了一个后台线程,负责周期性(每隔 1 秒)地将脏页(已修改但尚未写入磁盘的数据页)刷新到磁盘,并将相关的重做日志一同刷新
53-
6. 正常关闭服务器:MySQL 关闭的时候,redo log 都会刷入到磁盘里去
44+
InnoDB 存储引擎中,**redo log buffer**(重做日志缓冲区)是一块用于暂存 redo log 的内存区域。为了确保事务的持久性和数据的一致性,InnoDB 会在特定时机将这块缓冲区中的日志数据刷新到磁盘上的 redo log 文件中。这些时机可以归纳为以下六种
45+
46+
1. **事务提交时(最核心)**:当事务提交时,log buffer 里的 redo log 会被刷新到磁盘(可以通过`innodb_flush_log_at_trx_commit`参数控制,后文会提到)。
47+
2. **redo log buffer 空间不足时**:这是 InnoDB 的一种主动容量管理策略,旨在避免因缓冲区写满而导致用户线程阻塞。
48+
-redo log buffer 的已用空间超过其总容量的**一半 (50%)** 时,后台线程会**主动**将这部分日志刷新到磁盘,为后续的日志写入腾出空间,这是一种“未雨绸缪”的优化
49+
- 如果因为大事务或 I/O 繁忙导致 buffer **完全写满**,那么所有试图写入新日志的用户线程都会被**阻塞**,并强制进行一次同步刷盘,直到有可用空间为止。这种情况会影响数据库性能,应尽量避免
50+
3. **触发检查点 (Checkpoint) 时**:Checkpoint 是 InnoDB 为了缩短崩溃恢复时间而设计的核心机制。当 Checkpoint 被触发时,InnoDB 需要将在此检查点之前的所有脏页刷写到磁盘。根据 **Write-Ahead Logging (WAL)** 原则,数据页写入磁盘前,其对应的 redo log 必须先落盘。因此,执行 Checkpoint 操作必然会确保相关的 redo log 也已经被刷新到了磁盘
51+
4. **后台线程周期性刷新**:InnoDB 有一个后台的 master thread,它会大约每秒执行一次例行任务,其中就包括将 redo log buffer 中的日志刷新到磁盘。这个机制是 `innodb_flush_log_at_trx_commit` 设置为 0 或 2 时的主要持久化保障
52+
5. **正常关闭服务器**:在 MySQL 服务器正常关闭的过程中,为了确保所有已提交事务的数据都被完整保存,InnoDB 会执行一次最终的刷盘操作,将 redo log buffer 中剩余的全部日志都清空并写入磁盘文件
53+
6. **binlog 切换时**:当开启 binlog 后,在 MySQL 采用 `innodb_flush_log_at_trx_commit=1``sync_binlog=1` 的 双一配置下,为了保证 redo log 和 binlog 之间状态的一致性(用于崩溃恢复或主从复制),在 binlog 文件写满或者手动执行 flush logs 进行切换时,会触发 redo log 的刷盘动作
5454

5555
总之,InnoDB 在多种情况下会刷新重做日志,以保证数据的持久性和一致性。
5656

docs/database/redis/redis-skiplist.md

Lines changed: 41 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -241,41 +241,40 @@ private int levelCount = 1;
241241
private Node h = new Node();
242242

243243
public void add(int value) {
244-
245-
//随机生成高度
246-
int level = randomLevel();
244+
int level = randomLevel(); // 新节点的随机高度
247245

248246
Node newNode = new Node();
249247
newNode.data = value;
250248
newNode.maxLevel = level;
251249

252-
//创建一个node数组,用于记录小于当前value的最大值
253-
Node[] maxOfMinArr = new Node[level];
254-
//默认情况下指向头节点
250+
// 用于记录每层前驱节点的数组
251+
Node[] update = new Node[level];
255252
for (int i = 0; i < level; i++) {
256-
maxOfMinArr[i] = h;
253+
update[i] = h;
257254
}
258255

259-
//基于上述结果拿到当前节点的后继节点
260256
Node p = h;
261-
for (int i = level - 1; i >= 0; i--) {
257+
// 关键修正:从跳表的当前最高层开始查找
258+
for (int i = levelCount - 1; i >= 0; i--) {
262259
while (p.forwards[i] != null && p.forwards[i].data < value) {
263260
p = p.forwards[i];
264261
}
265-
maxOfMinArr[i] = p;
262+
// 只记录需要更新的层的前驱节点
263+
if (i < level) {
264+
update[i] = p;
265+
}
266266
}
267267

268-
//更新前驱节点的后继节点为当前节点newNode
268+
// 插入新节点
269269
for (int i = 0; i < level; i++) {
270-
newNode.forwards[i] = maxOfMinArr[i].forwards[i];
271-
maxOfMinArr[i].forwards[i] = newNode;
270+
newNode.forwards[i] = update[i].forwards[i];
271+
update[i].forwards[i] = newNode;
272272
}
273273

274-
//如果当前newNode高度大于跳表最高高度则更新levelCount
274+
// 更新跳表的总高度
275275
if (levelCount < level) {
276276
levelCount = level;
277277
}
278-
279278
}
280279
```
281280

@@ -380,7 +379,7 @@ public class SkipList {
380379
/**
381380
* 每个节点添加一层索引高度的概率为二分之一
382381
*/
383-
private static final float PROB = 0.5 f;
382+
private static final float PROB = 0.5f;
384383

385384
/**
386385
* 默认情况下的高度为1,即只有自己一个节点
@@ -392,9 +391,11 @@ public class SkipList {
392391
*/
393392
private Node h = new Node();
394393

395-
public SkipList() {}
394+
public SkipList() {
395+
}
396396

397397
public class Node {
398+
398399
private int data = -1;
399400
/**
400401
*
@@ -404,58 +405,55 @@ public class SkipList {
404405

405406
@Override
406407
public String toString() {
407-
return "Node{" +
408-
"data=" + data +
409-
", maxLevel=" + maxLevel +
410-
'}';
408+
return "Node{"
409+
+ "data=" + data
410+
+ ", maxLevel=" + maxLevel
411+
+ '}';
411412
}
412413
}
413414

414415
public void add(int value) {
415-
416-
//随机生成高度
417-
int level = randomLevel();
416+
int level = randomLevel(); // 新节点的随机高度
418417

419418
Node newNode = new Node();
420419
newNode.data = value;
421420
newNode.maxLevel = level;
422421

423-
//创建一个node数组,用于记录小于当前value的最大值
424-
Node[] maxOfMinArr = new Node[level];
425-
//默认情况下指向头节点
422+
// 用于记录每层前驱节点的数组
423+
Node[] update = new Node[level];
426424
for (int i = 0; i < level; i++) {
427-
maxOfMinArr[i] = h;
425+
update[i] = h;
428426
}
429427

430-
//基于上述结果拿到当前节点的后继节点
431428
Node p = h;
432-
for (int i = level - 1; i >= 0; i--) {
429+
// 关键修正:从跳表的当前最高层开始查找
430+
for (int i = levelCount - 1; i >= 0; i--) {
433431
while (p.forwards[i] != null && p.forwards[i].data < value) {
434432
p = p.forwards[i];
435433
}
436-
maxOfMinArr[i] = p;
434+
// 只记录需要更新的层的前驱节点
435+
if (i < level) {
436+
update[i] = p;
437+
}
437438
}
438439

439-
//更新前驱节点的后继节点为当前节点newNode
440+
// 插入新节点
440441
for (int i = 0; i < level; i++) {
441-
newNode.forwards[i] = maxOfMinArr[i].forwards[i];
442-
maxOfMinArr[i].forwards[i] = newNode;
442+
newNode.forwards[i] = update[i].forwards[i];
443+
update[i].forwards[i] = newNode;
443444
}
444445

445-
//如果当前newNode高度大于跳表最高高度则更新levelCount
446+
// 更新跳表的总高度
446447
if (levelCount < level) {
447448
levelCount = level;
448449
}
449-
450450
}
451451

452452
/**
453453
* 理论来讲,一级索引中元素个数应该占原始数据的 50%,二级索引中元素个数占 25%,三级索引12.5% ,一直到最顶层。
454-
* 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。
455-
* 该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 :
456-
* 50%的概率返回 1
457-
* 25%的概率返回 2
458-
* 12.5%的概率返回 3 ...
454+
* 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。 该 randomLevel
455+
* 方法会随机生成 1~MAX_LEVEL 之间的数,且 : 50%的概率返回 1 25%的概率返回 2 12.5%的概率返回 3 ...
456+
*
459457
* @return
460458
*/
461459
private int randomLevel() {
@@ -523,11 +521,11 @@ public class SkipList {
523521
}
524522

525523
}
526-
527524
}
525+
528526
```
529527

530-
对应测试代码和输出结果如下
528+
测试代码
531529

532530
```java
533531
public static void main(String[] args) {
@@ -550,61 +548,6 @@ public static void main(String[] args) {
550548
}
551549
```
552550

553-
输出结果:
554-
555-
```bash
556-
**********输出添加结果**********
557-
Node{data=0, maxLevel=2}
558-
Node{data=1, maxLevel=3}
559-
Node{data=2, maxLevel=1}
560-
Node{data=3, maxLevel=1}
561-
Node{data=4, maxLevel=2}
562-
Node{data=5, maxLevel=2}
563-
Node{data=6, maxLevel=2}
564-
Node{data=7, maxLevel=2}
565-
Node{data=8, maxLevel=4}
566-
Node{data=9, maxLevel=1}
567-
Node{data=10, maxLevel=1}
568-
Node{data=11, maxLevel=1}
569-
Node{data=12, maxLevel=1}
570-
Node{data=13, maxLevel=1}
571-
Node{data=14, maxLevel=1}
572-
Node{data=15, maxLevel=3}
573-
Node{data=16, maxLevel=4}
574-
Node{data=17, maxLevel=2}
575-
Node{data=18, maxLevel=1}
576-
Node{data=19, maxLevel=1}
577-
Node{data=20, maxLevel=1}
578-
Node{data=21, maxLevel=3}
579-
Node{data=22, maxLevel=1}
580-
Node{data=23, maxLevel=1}
581-
**********查询结果:Node{data=22, maxLevel=1} **********
582-
**********删除结果**********
583-
Node{data=0, maxLevel=2}
584-
Node{data=1, maxLevel=3}
585-
Node{data=2, maxLevel=1}
586-
Node{data=3, maxLevel=1}
587-
Node{data=4, maxLevel=2}
588-
Node{data=5, maxLevel=2}
589-
Node{data=6, maxLevel=2}
590-
Node{data=7, maxLevel=2}
591-
Node{data=8, maxLevel=4}
592-
Node{data=9, maxLevel=1}
593-
Node{data=10, maxLevel=1}
594-
Node{data=11, maxLevel=1}
595-
Node{data=12, maxLevel=1}
596-
Node{data=13, maxLevel=1}
597-
Node{data=14, maxLevel=1}
598-
Node{data=15, maxLevel=3}
599-
Node{data=16, maxLevel=4}
600-
Node{data=17, maxLevel=2}
601-
Node{data=18, maxLevel=1}
602-
Node{data=19, maxLevel=1}
603-
Node{data=20, maxLevel=1}
604-
Node{data=21, maxLevel=3}
605-
Node{data=23, maxLevel=1}
606-
```
607-
608551
**Redis 跳表的特点**
609552

610553
1. 采用**双向链表**,不同于上面的示例,存在一个回退指针。主要用于简化操作,例如删除某个元素时,还需要找到该元素的前驱节点,使用回退指针会非常方便。

docs/java/basis/java-basic-questions-02.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -694,10 +694,10 @@ System.out.println(s);
694694
**字符串常量池**JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
695695
696696
```java
697-
// 在字符串常量池中创建字符串对象 ”ab“
698-
// 将字符串对象 ”ab“ 的引用赋值给 aa
697+
// 1.在字符串常量池中查询字符串对象 "ab",如果没有则创建"ab"并放入字符串常量池
698+
// 2.将字符串对象 "ab" 的引用赋值给 aa
699699
String aa = "ab";
700-
// 直接返回字符串常量池中字符串对象 ”ab“,赋值给引用 bb
700+
// 直接返回字符串常量池中字符串对象 "ab",赋值给引用 bb
701701
String bb = "ab";
702702
System.out.println(aa==bb); // true
703703
```

0 commit comments

Comments
 (0)