2024年8月

作者:来自 vivo 互联网存储研发团队- Xia Qianyong

本文首先介绍MySQL 5.7 DDL以及GH-OST的原理,然后从效率、空间占用、锁阻塞、binlog日志产生量、主备延时等方面,对比GH-OST和MySQL5.7 DDL的差异。

一、背景介绍

在 MySQL 数据库中,DDL(数据定义语言)操作包括对表结构、索引、触发器等进行修改、创建和删除等操作。由于 MySQL 自带的 DDL 操作可能会阻塞 DML(数据操作语言)写语句的执行,大表变更容易产生主备延时,DDL 变更的速度也不能控制,因此在进行表结构变更时需要非常谨慎。

为了解决这个问题,可以使用 GitHub 开源的工具 GH-OST。GH-OST 是一个可靠的在线表结构变更工具,可以实现零宕机、低延迟、自动化、可撤销的表结构变更。相比于 MySQL 自带的 DDL 操作,GH-OST 可以在不影响正常业务运行的情况下进行表结构变更,避免了 DDL 操作可能带来的风险和影响。

通过使用 GH-OST工具,可以对 MySQL 数据库中的表进行在线结构变更,而不会对业务造成太大的影响。同时,GH-OST 工具还提供了多种高级特性,如安全性检测、自动化流程等,可以帮助用户更加高效地进行表结构变更。

二、MySQL5.7几种DDL介绍

2.1 copy

  • server层触发创建临时表

  • server层对源表加MDL锁,阻塞DML写、不阻塞DML读

  • server层从源表中逐行读取数据,写入到临时表

  • 数据拷贝完成后,升级字典锁,禁止读写

  • 删除源表,把临时表重命名为源表

MySQL copy方式的DDL变更,数据表的重建(主键、二级索引重建),server层作为中转把从innodb读取数据表,在把数据写到innodb层临时表。简单示意图如下:

图片

2.2 inplace

(1)rebuild table

需要根据DDL语句创建新的表结构,根据源表的数据和变更期间增量日志,重建新表的主键索引和所有的二级索引。

Prepare阶段

  • 创建新的临时frm文件

  • 持有EXCLUSIVE-MDL锁,禁止读写

  • 根据alter类型,确定执行方式(copy,online-rebuild,online-norebuild)假如是Add Index,则选择online-norebuild

  • 更新数据字典的内存对象

  • 分配row_log对象记录增量

  • 生成新的临时ibd文件

ddl执行阶段
:

  • 降级EXCLUSIVE-MDL锁,允许读写

  • 扫描old_table的聚集索引每一条记录rec

  • 遍历新表的聚集索引和二级索引,逐一处理各个索引

  • 根据rec构造对应的索引项

  • 将构造索引项插入sort_buffer块排序

  • 将sort_buffer块更新到新表的索引上

  • 记录ddl执行过程中产生的增量(记录主键和索引字段)

  • 重放row_log中的操作到新表索引商

  • 重放row_log间产生dml操作append到row_log最后一个Block

commit阶段
:

  • 当前Block为row_log最后一个时,禁止读写,升级到EXCLUSIVE-MDL锁

  • 重做row_log中最后一部分增量

  • 更新innodb的数据字典表

  • rename临时idb文件,frm文件

  • 增量完成

MySQL rebuild table方式的DDL,数据不需要通过sever层中转,innodb层自己完成数据表的重建。简单示意图如下:

图片

(2)build-index

需要根据DDL语句创建新的表结构,根据源表的数据和变更期间增量日志,创建新的索引。

Prepare阶段
:

  • 持有EXCLUSIVE-MDL锁,禁止读写

  • 根据alter类型,确定执行方式(copy,online-rebuild,online-norebuild)

  • 假如是Add Index,则选择online-norebuild

  • 更新数据字典的内存对象

  • 分配row_log对象记录增量

ddl执行阶段
:

  • 降级EXCLUSIVE-MDL锁,允许读写

  • 扫描old_table的聚集索引每一条记录rec

  • 遍历新表的聚集索引,根据rec构造新的索引数据

  • 将构造索引项插入sort_buffer块排序

  • 将sort_buffer块更新到新表的索引上

  • 记录ddl执行过程中产生的增量(仅记录主键和新索引字段)

  • 重放row_log中的操作到新表索引上

  • 重放row_log间产生dml操作append到row_log最后一个Block

commit阶段
:

  • 当前Block为row_log最后一个时,禁止读写,升级到EXCLUSIVE-MDL锁

  • 重做row_log中最后一部分增量

  • 更新innodb的数据字典表

  • 增量完成

MySQL rebuild index方式的DDL,数据不需要通过sever层中转,innodb层只需要完成变更二级索引的创建。简单示意图如下:

图片

(3)only modify metadata

只修改元数据(.frm文件和数据字典),不需要拷贝表的数据。

图片

三、GH-OST

在GH-OST端,根据DDL语句创建新的表结构,根据源表的数据和增量期间增量日志,重建新表的主键索引和所有的二级索引,最终完成DDL增量。

主要流程如下:

  • 根据DDL语句和源表创建新的表结构

  • 根据唯一索引(主键索引或者其它唯一索引)

- 优先应用新增量的binlog到新的表中,需要经过GH-OST把binlog日志转换为sql,然后回放到影子表

- 其次拷贝源表中的数据到新的表中,表数据拷贝通过sql语句 insert ignore into (select .. from)直接在MySQL实例上执行,无需经过GH-OST中转

  • 数据拷贝完成并应用完binlog后,通过lock table write 锁住源表

  • 应用数据完成-获取到锁期间产生的增量binlog

  • delete源表,rename影子表为源表,完成数据增量

GH-OST 进行DDL变更,GH-OST服务通知server层,server层作为中转把从innodb读取数据表,在把数据写到innodb层影子表。并且GH-OST作为中转读取DDL变更期间增量binlog解析成SQL写语句回放到影子表。简单示意图如下:

图片

四、对比分析

DDL变更执行时长、对磁盘的额外占用(临时数据表+binlog)、锁阻塞时长、主备延时都是执行DDL变更人员比较关心的问题,本章将从从执行效率、占用表空间、锁阻塞、产生binlog日志量、主备延时等方面对MySQL原生的DDL和GH-OST进行对比分析。

4.1 执行效率

(1)only modify metadata(正常小于1S)

(2)build-index: 数据条目越多、新索引字段越大耗时越多

  • 增量日志超过innodb_online_alter_log_max_size造成DDL失败

(3)rebuild table: 数据条目越多、所有索引字段之和越大耗时越多

  • 增量日志超过innodb_online_alter_log_max_size造成DDL失败

(4)copy:数据条目越多,所有索引字段之和越大耗时越多,相对于rebuild table,数据需要从server层中转,所以比rebuild table耗时多

(5)GH-OST :数据条目越多,所有索引字段之和越大耗时越多,

  • 相对于copy,增量日志数据需要从GH-OST中转,所以比copy耗时多

  • 有各种限流,(主备延时,threads超限延时…),增加耗时

  • 增量期间应用binlog速度如果跟不上业务产生binlog日志的速度,将无法完成增量

  • critical 参数还会导致主动退出,例如thread_running

耗时:only modify metadata < build-index < build < copy < GH-OST

4.2 占用表空间

  • 【only modify metadata】:忽略

  • 【build-index】:额外需要,新增索引字段占用的空间

  • 【rebuild-table】:额外需要约两倍的表空间

  • 【copy】:额外需要约两倍的表空间

  • 【GH-OST】 :临时表占用约两倍的表空间,另外生成影子表会产生大量的binlog日志会占用表空间

占用表空间: only modify metadata < build-index < build = copy < GH-OST

4.3 锁阻塞

(1)only modify metadata

  • DDL prepare阶段短暂的MDL排他锁,阻塞读写

(2)build-index table

  • DDL prepare阶段短暂的MDL排他锁,阻塞读写

  • 执行阶段(主要耗时阶段),MDL SHARED_UPGRADABLE锁,不阻塞读写

  • 执行阶段的最后会回放增量日志row_log,两个block间隙和最后block,持有源表索引的数据结构锁,会阻塞写

  • 提交阶段,MDL锁升级为排他锁

  • 回放剩余的row_log(执行完成致MDL锁升级期间新增的row_log,持有源表索引的数据结构锁,阻塞读写)

(3)rebuild-table: 和build-index table一致

(4)copy

  • DDL prepare阶段短暂的MDL排他锁,阻塞读写

  • 执行阶段(主要耗时阶段),阻塞写,不阻塞读

(5)GH-OST

  • 等待锁的时间也会阻塞业务

  • 进入rename到拿表写锁的间隙有少量的新增binlog,后续需要持锁回放这部分日志

  • rename表本身的耗时通常1s以内左右

锁阻塞时间:

only modify metadata=GH-OST < build-index table = rebuild-table  < copy(整个DDL期间都会阻塞业务的写)

锁阻塞分析:

MySQL DDL在获取MDL排它锁和GH-OST获取表的的写锁,在获取锁的等待期间都会阻塞业务的读写

  • MySQL等待锁的超时时间为MySQL参数innodb_lock_wait_timeout。等待超时则失败

  • GH-OST等待锁的时间,等待超时时间可配(默认6秒),等待超时次数可配

4.4 产生binlog日志量

【MySQL5.7 DDL】: 在DDL执行结束时仅向binlog中写入一条DDL语句,日志量较小。

【GH-OST】: 影子表在全量数据拷贝和增量数据应用过程中产生大量的binlog日志(row模式),对于大表日志量非常大。

产生binlog日志量:MySQL5.7 DDL < GH-OST

4.5 主备延时分析

(1)MySQL5.7 DDL:MySQL集群主备环境

  • Master上DDL执行完成,binlog提交后,slave才开始进行DDL。

  • slave串行复制、group复制模式,需要等前面的DDL回放完成后才会进行后续binlog回放,主备延时至少是DDL回放的时间。

图片

(2)GH-OST:主备复制延时基本可以忽略

  • GH-OST在master上创建一个影子表,在执行数据拷贝和binlog应用阶段,GHO表的binlog会实时同步到备。

  • 影子表(_GHO表)应用完成后,通过rename实现新表切换,这个rename动作也会通过binlog传到salve执行完成DDL。

图片

延时时间:GH-OST < MySQL DDL

备库执行DDL期间主库异常,主备切换。备库升级为主过程中,要回放完relaylog中的DDL和dml,才能对外服务,否则会出现数据丢失,这将造成业务较长时间的阻塞。

4.6 总结

图片

GH-OST 工具和 MySQL 原生 DDL 工具的适用场景不同,具体使用哪种工具需要根据实际需求进行选择。

  • 变更人员无法判断本次DDL是否会造成DML阻塞、锁阻塞等,建议使用GH-OST工具。

  • 如果需要进行在线表结构变更,并且需要减少锁阻塞时间、减少主备延时等问题,建议使用 GH-OST 工具。

  • 变更只涉及到元数据的修改,建议使用mysql原生DDL。

  • 如果表结构变更较小,对锁阻塞时间和主备延时要求不高,建议使用 MySQL 原生 DDL 工具。

参考资料:

导读

还是要先看
官方手册
.

学过DMA的同志可能比较好理解,一句话,
释放CPU总线
:
如果把应用程序执行的整个过程进行进一步分析,可以看到,当程序访问 I/O 外设或睡眠时,其实是不需要占用处理器的,于是我们可以把应用程序在不同时间段的执行过程分为两类,占用处理器执行有效任务的计算阶段和不必占用处理器的等待阶段。这些阶段就形成了一个我们熟悉的“暂停-继续…”组合的控制流或执行历史。从应用程序开始执行到结束的整个控制流就是应用程序的整个执行过程。
本节的重点是操作系统的核心机制——
任务切换
,在内核中这种机制是在
__switch
函数中实现的。 任务切换支持的场景是:一个应用在运行途中便会主动或被动交出 CPU 的使用权,此时它只能暂停执行,等到内核重新给它分配处理器资源之后才能恢复并继续执行。

任务概念的形成

这里直接看
官方手册
.

这里主要是提到了一些概念,我把它们摘抄出来:

  1. 执行片段称为 “
    计算任务片
  2. 空闲片段称为“
    空闲任务片
  3. 需要保存与恢复的资源称为“
    任务上下文

不同类型的上下文与切换

这部分之前第二章复习第一章的知识的时候我们就重复过关于第一章的
函数调用栈
和第二章的
内核栈/用户栈
的类比和区别.

这里直接看
官方手册
回顾一下就行,应该是为
任务切换
打基础.

在我的脑海里,任务切换是一个直接用
sp
指针进行操作的过程,按照我们上一章学到的知识,只需要在切换之前把
上下文
保存到用户栈就行.可能会增加的功能:

  1. 给用户栈增加入栈功能
  2. 增加切换APP的函数,可能需要调用汇编代码,有点类似于
    __restore
    ,但是不需要触发
    Trap
    .

任务切换的设计与实现

官方手册
中提到的异同和我自己脑子里总结的异同还是非常不同的:

  • 与 Trap 切换不同,它不涉及特权级切换;
  • 与 Trap 切换不同,它的一部分是由
    编译器帮忙
    完成的;
  • 与 Trap 切换相同,它对应用是
    透明
    的。

这个
编译器帮忙

对应用透明
是需要在后续学习过程中注意的.

任务切换的流程:

  1. 某个应用Trap到
    S模式
    的操作系统内核中.
  2. Trap控制流调用
    __switch
    .
  3. Trap 控制流
    A
    会先被暂停并被切换出去.
  4. CPU 转而运行另一个应用在内核中的 Trap 控制流
    B
  5. 然后在某个合适的时机,原 Trap 控制流
    A
    才会从某一条 Trap 控制流
    C
    (很有可能不是它之前切换到的
    B
    )切换回来继续执行并最终返回

问题
:既然不需要特权级切换,那它为什么还要进入Trap呢?是怎么进行的Trap吗?还是通过
ecall
吗?

从实现的角度讲,
__switch
函数和一个普通的函数之间的核心差别仅仅是它会
换栈

说起栈的上下文切换,我们不得不想到上一章我们保存的包含
CSR

X0~X31
的上下文,那么同样地,在
任务切换
的过程中也有任务的上下文:

认真看这个图,左侧写得是
运行
状态的一个任务,它的内核栈里保存了两部分的东西:

  1. 上一章我们学到的
    TrapContext
  2. 那么当
    Trap
    之后把
    sp
    指针指向内核栈,函数调用的一些上下文也会保存在内核里,除了
    TrapContext
    内核栈里还保存着
    TrapHandler
    函数的
    调用栈信息
    .

右侧写得是
准备
状态的一个任务(可以看到一个细节
sp
寄存器
没有指向
这个栈).

为了保证
sp
重新指向右侧的内核栈的时候能够
恢复现场
, 因此一定有一些东西是需要保存的,那么它就是任务上下文.

这里定义
任务上下文
: CPU 当前的某些寄存器.

可以看到左侧和右侧的图的下面都有一个
TASK_MANAGER
,它是一个类似于我们上一章实现的
APP_MANAGER
的东西,是一个结构体
TaskManager
的一个
全局实例
.

可以看到它保存了
sp
,
ra
,
s0~s11
等寄存器.
为什么
这些寄存器要保存才能
保证
任务能够继续运行,是我们接下去学习的重点.

对于
TaskManager
的具体实现
官方手册
提供了思路和细节,

  1. 实现一个
    TaskControlBlock
    结构体,用于储存任务上下文
    TaskContext
    .

  2. TaskManager
    实现一个
    TaskControlBlock
    数组,用于储存多个上下文.

对于当前正在执行的任务的 Trap 控制流,我们用一个名为
current_task_cx_ptr
的变量来保存放置当前任务上下文的地址;而用
next_task_cx_ptr
的变量来保存放置下一个要执行任务的上下文的地址.

这里直接看示意图,可以看到实现了一个以
current_task_cx_ptr

next_task_cx_ptr
为参数的
swtich
函数用以切换上下文.

这里也说明了一件事,就是控制流本身在进行切换之前就可以感知到:

  1. 当前执行的是哪个任务
  2. 接下去要执行的是哪个任务

官方手册
为我们描述了任务切换的四个阶段:

  • 阶段 [1]:在 Trap 控制流 A 调用
    __switch
    之前,A 的内核栈上只有 Trap 上下文和 Trap 处理函数的调用栈信息,而 B 是之前被切换出去的;
  • 阶段 [2]:A 在 A 任务上下文空间在里面保存 CPU 当前的寄存器快照;
  • 阶段 [3]:这一步极为关键,读取
    next_task_cx_ptr
    指向的 B 任务上下文,根据 B 任务上下文保存的内容来恢复
    ra
    寄存器、
    s0~s11
    寄存器以及
    sp
    寄存器。只有这一步做完后,
    __switch
    才能做到一个函数跨两条控制流执行,即
    通过换栈也就实现了控制流的切换
  • 阶段 [4]:上一步寄存器恢复完成后,可以看到通过恢复
    sp
    寄存器换到了任务 B 的内核栈上,进而实现了控制流的切换。这就是为什么
    __switch
    能做到一个函数跨两条控制流执行。此后,当 CPU 执行
    ret
    汇编伪指令完成
    __switch
    函数返回后,任务 B 可以从调用
    __switch
    的位置继续向下执行。

这里我们可以直接看
__switch
的具体实现:

# os/src/task/switch.S

.altmacro
.macro SAVE_SN n
    sd s\n, (\n+2)*8(a0)
.endm
.macro LOAD_SN n
    ld s\n, (\n+2)*8(a1)
.endm
    .section .text
    .globl __switch
__switch:
    # 阶段 [1]
    # __switch(
    #     current_task_cx_ptr: *mut TaskContext,
    #     next_task_cx_ptr: *const TaskContext
    # )
    # 阶段 [2]
    # save kernel stack of current task
    sd sp, 8(a0)
    # save ra & s0~s11 of current execution
    sd ra, 0(a0)
    .set n, 0
    .rept 12
        SAVE_SN %n
        .set n, n + 1
    .endr
    # 阶段 [3]
    # restore ra & s0~s11 of next execution
    ld ra, 0(a1)
    .set n, 0
    .rept 12
        LOAD_SN %n
        .set n, n + 1
    .endr
    # restore kernel stack of next task
    ld sp, 8(a1)
    # 阶段 [4]
    ret

这里应该没什么看不懂的部分,我画了一张图来表述
TaskContext
的内存情况:

对应
rust
的代码:

// os/src/task/context.rs

pub struct TaskContext {
    ra: usize,
    sp: usize,
    s: [usize; 12],
}

这里提一下:

  1. 在RISC-V架构中,
    ra
    寄存器(Return Address Register)是一个特殊的通用寄存器,编号为
    x1
    。这个寄存器主要用于存储返回地址,即函数调用之后应该返回的指令地址。当一个函数被调用时,调用者(caller)通常会将返回地址存入
    ra寄存器
    ,以便在函数执行完毕后能够正确返回到调用点。

    __swtich
    执行结束后使用
    ret
    返回到
    ra
    的位置,我们修改了
    ra
    为下一个要执行的任务上下文的
    ra
    自然会继续执行到执行的任务上次保存上下文时调用
    __swtich
    的位置

    .
  2. Rust/C 编译器会在函数的起始位置自动生成代码来保存
    s0~s11
    这些被调用者保存的寄存器。但
    __switch
    是一个用汇编代码写的特殊函数,它不会被 Rust/C 编译器处理,所以我们需要在
    __switch
    中手动编写保存
    s0~s11
    的汇编代码.
  3. 不用保存其它寄存器是因为:其它寄存器中,属于调用者保存的寄存器是由编译器在
    高级语言
    编写的调用函数中
    自动生成
    的代码来完成保存的;还有一些寄存器属于临时寄存器,不需要保存和恢复。

对应
第三点
,我们应该理解,要使用
Rust
调用才能使得编译器自动帮我们
保存/恢复调用者保存寄存器
:

// os/src/task/switch.rs

global_asm!(include_str!("switch.S"));

use super::TaskContext;

extern "C" {
    pub fn __switch(
        current_task_cx_ptr: *mut TaskContext,
        next_task_cx_ptr: *const TaskContext
    );
}

这一章主要分享一下使用 Konva 遇到的性能优化问题,并且介绍一下 UI 美化的思路。

至少有 2 位小伙伴积极反馈,发现本示例有明显的性能问题,一是内存溢出问题,二是卡顿的问题,在这里感谢大家的提醒。

请大家动动小手,给我一个免费的 Star 吧~

大家如果发现了 Bug,欢迎来提 Issue 哟~

github源码

gitee源码

示例地址

性能优化

内存溢出

根据官方文档
Konva Class: Node
的说明:

remove()
: remove a node from parent, but don't destroy. You can reuse the node later.
destroy()
: remove and destroy a node. Kill it and delete forever! You should not reuse node after destroy().
If the node is a container (Group, Stage or Layer) it will destroy all children too.

在本示例之前的版本中,只使用 remove() 是不正确的,只使用 remove,每次 redraw 都产生巨量的实例没有被清除,也就是内存溢出了,导致 JS heap size 随随便便干到几个 GB。

【简单判断内存溢出】
前往:Chrome -> Console 面板 -> 左侧更多 -> Performance monitor -> JS heap size
如果内存只升不降,基本可以认为内存溢出了。

在本示例中,大部分图形实例都是用完即弃的,所以大部分的 remove 都替换为 destory 后,JS heap size 将基本维持在几十上百 MB(根据内容复杂度)。

这里提个醒,除了使用 remove 的时候要注意,还有个容易忽略的 API 要注意,就是 Stage、Layer、Group 的 removeChildren(),如果子节点不再有用,建议先遍历子节点分别 destroy 一下。

初始状态,如下:

image

卡顿

在本示例之前的版本中,只要画面需要变化,都是重新 redraw 所有图形,这导致加载的素材过多的时候,交互会产生明显的卡顿,尤其是加载 gif 的时候,每一帧都会 redraw 一次。

因此,redraw 必须是可以选择性 draw 每一层 layer 的,主要调整如下:

// 重绘(可选择)
  redraw(drawNames?: string[]) {
    const all = [
      Draws.BgDraw.name, // 更新背景
      Draws.LinkDraw.name, // 更新连线
      Draws.AttractDraw.name, // 更新磁贴
      Draws.RulerDraw.name, // 更新比例尺
      Draws.RefLineDraw.name, // 更新参考线
      Draws.PreviewDraw.name, // 更新预览
      Draws.ContextmenuDraw.name // 更新右键菜单
    ]

    if (Array.isArray(drawNames) && !this.debug) {
      // 选择性 draw 也要保持顺序
      for (const name of all) {
        if (drawNames.includes(name)) {
          this.draws[name].draw()
        }
      }
    } else {
      for (const name of all) {
        this.draws[name].draw()
      }
    }
  }

这里有几点细节考虑:
1、传哪些 drawNames 就 redraw 哪些 draw 的 group,除非当时是调试模式。
2、不传 drawNames 就全 redraw。
3、redraw 要按 all 的顺序执行。

举例:

  • 拖动画布的时候:
this.render.redraw([Draws.BgDraw.name, Draws.RulerDraw.name, Draws.PreviewDraw.name])

因为这个交互只影响了 背景、比例尺、预览的 draw。

  • 放大缩小的时候:
            this.render.redraw([
              Draws.BgDraw.name,
              Draws.LinkDraw.name,
              Draws.RulerDraw.name,
              Draws.RefLineDraw.name,
              Draws.PreviewDraw.name
            ])

此时影响的 draw 就比较多了。

根据不同交互的特点,做必要的 redraw 处理,就可以很好的提高交互性能,减少卡顿。

UI 美化

之前的重心都放在画布的交互上,界面得过且过就行了。

现在基础架构基本稳定了,是应该美化一下丑陋的 UI 了,简单美化后:

image

Naive UI

为了快速美化,这里用 Naive UI,比较清爽。

主要美化了一下 头部 和 素材 栏:

  • src/components/main-header
  • src/components/asset-bar

这里就不贴具体代码了,比较简单。

mitt - Emitter

之前是通过配置式,传入一些 方法 当作事件的 handler,没法动态订阅,太不方便了。

这里改造了一下 Render,使用 mitt 给它赋予 Emitter 能力:

// 略
import mitt, { type Emitter } from 'mitt'
// 略
export class Render {
  // 略
  protected emitter: Emitter<Types.RenderEvents> = mitt()
  on: Emitter<Types.RenderEvents>['on']
  off: Emitter<Types.RenderEvents>['off']
  emit: Emitter<Types.RenderEvents>['emit']
  // 略
  constructor(stageEle: HTMLDivElement, config: Types.RenderConfig) {
    // 略
    this.on = this.emitter.on.bind(this.emitter)
    this.off = this.emitter.off.bind(this.emitter)
    this.emit = this.emitter.emit.bind(this.emitter)
    // 略
  }
}

在外面的组件里,通过 render 实例,就可以方便订阅事件,例如:

        props.render?.on('selection-change', (nodes: Konva.Node[]) => {
            selection.value = nodes
        })

Thanks watching~

More Stars please!勾勾手指~

源码

gitee源码

示例地址

JDK版本升级,通常引入新的语言特性、新的API,废弃一些API,从而导致一些不兼容的现象。
因此在升级产品使用的JDK版本时,通常有如下考虑:

  • 新产品包括新建设的项目以及刚启动不久的项目,使用高版本的JDK。
  • 对于生命周期中间的产品,依据人力预算和团队的技能,决策是否升级。
  • 对于进入生命周期尾部的产品,则建议维持现有的JDK版本,不进行升级工作。

升级JDK前,完整梳理涉及的变更项,有助于准确评估升级所需的工作量。
升级JDK时,提前就如下事项,进行评估:
升级JDK版本的目的
明确升级的目的,比如提升开发效率、改善应用运行时的效率、提升团队技能准备度、客户的明确要求等,有助于缩小备选的版本集合,快速做出选型的决策。
就升级目的和收益,和客户、管理团队、研发团队统一认识,达成一致。

开发环境

  • IDE的选型,当前在用的低版本的IDE可能不支持高版本的JDK,因此需要同步更新版本。
  • 构建工具,比如项目当前使用maven来构建产品,部分插件可能依赖JDK的版本,因此需要同步升级。
  • 静态检查工具,低版本的检查工具可能无法识别高版本JDK引入的新语法,因此需要升级或者重新选型。
  • 调试工具,比如内存检查和分析工具、一些Profile的工具,可能需要同步升级或者重新选型。
  • UT用例,比如Mock框架,可能需要同步升级或者重新选型,这涉及到已有UT代码的整改。

构建流水线

  • 流水线软件,比如Jenkins,统一JDK版本,有助于简化环境的配置,同时复用升级JDK带来的红利。
  • 静态检查工具,和开发环境中使用的代码静态工具的版本和规则需要匹配,二者同步升级。
  • 自动化测试用例

产品源码

  • 源码中涉及语言、API的兼容性修改
  • 开源软件,选用兼容高版本JDK的版本,或者重新选型
  • 脚本,比如安装、卸载、启动、停止脚本等

验证工作

  • 功能验证
    • 安装/卸载/运行/停止/升级/回退
    • 业务功能
  • 性能测试
    • 资源消耗,观察CPU、内存
    • 业务表现,比如吞吐量、时延、并发度
    • JVM表现,比如GC次数、GC时延、GC日志
  • 长稳测试

周边配套系统
运营、运维等系统,和产品一起完成整改。

生产变更
提前演练,准备好应对预案。

完成上述梳理,算是好的开始,后续仍需制订执行计划,有序开展工作,跟踪各任务的进度,在预期时间内完成升级的工作,达成升级的目标。

AI的答案

如下是百度生成的答案,质量还不错,文笔、措辞的质量不错。

‌JDK(‌Java Development Kit)的升级是一个涉及多个方面的过程,包括版本选择、依赖更新、代码兼容性检查、测试以及查阅官方文档和社区支持。以下是一些关键的步骤和注意事项:
版本选择

  • 推荐升级到最新稳定版本,如‌JDK 17,以利用最新的性能改进和安全更新。
  • 考虑项目的兼容性和依赖库的支持情况,确保所选版本与项目的需求相匹配。

依赖更新

  • 检查项目依赖的库是否支持新版本的JDK,并更新到兼容的版本。特别注意那些有强制依赖的库,如Zookeeper和Curator,它们可能需要特定版本的JDK才能正常工作。
  • 对于使用Maven或Gradle等构建工具的项目,更新项目的构建配置文件(如pom.xml或build.gradle),指定新的JDK版本。

代码兼容性检查

  • 检查代码中是否有使用已弃用或不再支持的API,并进行相应的修改。
  • 利用IDE的提示功能检查潜在的兼容性问题,并进行修复。

测试

  • 在升级后进行全面的测试,包括单元测试、集成测试和系统测试,确保功能不受影响。
  • 特别注意那些与JDK版本紧密相关的功能模块,如网络编程、并发处理等。

文档和社区支持

  • 查阅JDK官方文档和社区讨论,获取更多关于升级指导和常见问题的解决方案。
  • 利用社区论坛和问答网站(如Stack Overflow)寻求帮助和建议。

通过遵循上述步骤和建议,可以更顺利地完成JDK的升级过程,同时最小化潜在的风险和问题。‌

参考资料

Oracle JDK

OpenJDK

开源软件

网友帖子

在快节奏的现代办公环境中,一款高效、集成化的操作系统无疑是提升工作效率的利器。今天,我们要为您隆重介绍 ——GodoOS,一款专为内网办公环境设计的全能操作系统。它不仅仅是一个工具,更是您团队协作与文件管理的得力助手,将彻底改变您的工作方式,带来前所未有的便捷体验!

【全能办公套件,一应俱全】
GodoOS 内置了 Word、Excel、PPT、PDF 阅读器等常用办公软件,满足您日常文档处理的需求。同时,它还加入了内网聊天、白板、思维导图等工具,让团队协作变得更加高效。无需在多个软件间频繁切换,一个平台,全部搞定,大大提升您的工作效率。

【极简设计,Windows 风格】
界面设计上,GodoOS 精心模仿了 Windows 风格,操作直观简便。即使您是初次使用的用户,也能迅速上手,无需额外培训。同时,它保持了极低的资源消耗,确保高性能运行。即使是老旧设备,也能流畅使用 GodoOS,享受出色的办公体验。

【零配置,即下即用】
GodoOS 实现了真正的零配置安装,无需繁琐的注册过程。下载后即可直接使用,省去了繁琐的设置步骤。更重要的是,它纯净无插件依赖,确保您的办公环境干净、安全,远离恶意软件的侵扰。

【轻巧便携,功能强大】
别看 GodoOS 体积小,打包后仅
65M,却蕴含着强大的办公能量。无论是日常文档处理还是团队协作,它都能轻松应对。而且,GodoOS 还支持多平台运行,无论是
Windows、Linux 还是 MacOS,都能享受到一致且出色的办公体验。这意味着您可以在不同设备间无缝切换,随时随地进行办公。

【无限扩展,定制专属应用】
GodoOS 的应用商店体系是其另一大亮点。它灵活高配置,只需简单学习,即可开发出符合您团队特定需求的应用。这意味着您可以根据实际需求,定制专属的办公应用,进一步提升工作效率。

【开源免费,畅享未来办公】
最值得一提的是,GodoOS 是一款完全开源的操作系统。这意味着您可以自由地使用、修改并分享它,无需担心任何版权问题。同时,它也支持离线使用,保障您的数据安全无忧。您可以放心地使用 GodoOS 进行内网办公,享受高效、便捷的办公体验。

【即时通讯与文件共享,团队协作无界限】
GodoOS 还具备即时通讯和文件共享功能,让团队协作变得更加紧密。无需注册即可自动连接内网用户,实现即时沟通和文件共享。这大大提高了团队协作的效率,让工作变得更加顺畅。

综上所述,GodoOS
以其全面的办公功能、极简的操作体验、跨平台的兼容性、高度的可定制性以及开源免费的特性,正逐步成为内网办公领域的一股清新力量。

godoos开源地址:https://gitee.com/ruitao_admin/godoos