拉巴力的纸皮箱


  • 首页

  • 标签

  • 归档

  • 关于

  • 搜索

轻量级微服务公共库设计

发表于 2022-03-22

目的

  • 减少拷贝代码,抽象公共业务组件和复用,快速支持产品需求

  • 提升开发效率,增强排查能力

  • 统一维护,提升代码质量,减少重复错误,提高服务可控性

  • 公共嵌入式的sdk,其实有点类似一个轻量级的网关,sidecar,localproxy

  • 为什么不使用API网关?
    1. 有一定的机器和维护成本
    2. 跟组织架构有一定关系

关于轻量级分布式事件通知的思考

发表于 2022-03-12
  • 有些业务场景,我们需要发出一个事件,通知到每一个进程。
    比如数据变更,通知每个进程更新本地缓存。

  • 使用MQ的话,以NSQ为例,将channel名设置为ip,那么每个进程都会收到这个事件;但是目前现在是k8s的时代,使用k8s部署进程,那么会导致某些旧ip的channel仍然存在,从而造成事件堆积。

  • redis虽然支持事件,但是微服务时代,不太建议每个业务的微服务都连同一个redis(同业务内的通知可以考虑);使用一个公共的zk获取是一个可以考虑的方案,各服务启动时watch相应的节点即可。

经典排序算法

发表于 2022-02-28
  • 十大经典排序算法(动图演示)

排序算法分类

(一)

  • 内部排序和外部排序

(二)

  1. 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。(非线性时间)
  2. 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。 (线性时间)

(三)

  1. 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。如:快速排序、归并排序、堆排序、冒泡排序等。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。
  2. 线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。如:计数排序、基数排序、桶排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr之前有多少个元素,则唯一确定了arr在排序后数组中的位置。

(四)

  1. 比较类排序
    • 交换排序 (swap)
      • 冒泡排序 (bubble Sort) - 双重遍历,相邻元素两两比较交换
      • 快速排序 (quick Sort) - 较复杂
    • 插入排序 (insertion)
      • 简单插入排序 (insertion sort) - 分左右两批,遍历有序列表,将无序的元素插入有序的列表
      • 希尔排序 (shell sort) (缩小增量排序) - 较复杂
    • 选择排序 (selection)
      • 简单选择排序 (selection) - 双重遍历,每次找到最小的元素,最后和该趟遍历的第一个元素进行交换
      • 堆排序 (heap sort)
    • 归并排序 (merge sort) - 即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。 (递归)
      • 二路归并排序
      • 多路归并排序
  2. 非比较类排序
    • 计数排序 (counting sort)
    • 桶排序 (bucket sort)
    • 基数排序 (radix sort)(分配排序)

(五)

  • 算法复杂度
排序方法 时间复杂度(平均) 时间复杂度(最坏) 时间复杂度(最好) 空间复杂度 稳定性
冒泡排序 O(n^2) O(n^2) O(n) O(1) 稳定
快速排序 O(nlog2n) O(n^2) O(nlog2n) O(nlog2n) 不稳定
简单插入排序 O(n^2) O(n^2) O(n) O(1) 稳定
希尔排序 O(n1.3) O(n^2) O(n) O(1) 不稳定
简单选择排序 O(n^2) O(n^2) O(n^2) O(1) 不稳定
堆排序 O(nlog2n) O(nlog2n) O(nlog2n) O(1) 不稳定
归并排序 O(nlog2n) O(nlog2n) O(nlog2n) O(n) 稳定
计数排序 O(n+k) O(n+k) O(n+k) O(n+k) 稳定
桶排序 O(n+k) O(n^2) O(n) O(n+k) 稳定
基数排序 O(n*k) O(n*k) O(n*k) O(n+k) 稳定
  • 相关概念:

1、时间复杂度

  • 时间复杂度可以认为是对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
  • 常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n), 线性对数阶O(nlog2n),平方阶O(n2)
  • 时间复杂度O(1):算法中语句执行次数为一个常数,则时间复杂度为O(1),

2、空间复杂度

  • 空间复杂度是指算法在计算机内执行时所需存储空间的度量,它也是问题规模n的函数
  • 空间复杂度O(1):当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1)
  • 空间复杂度O(log2N):当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为O(log2n), ax=N,则x=logaN,
  • 空间复杂度O(n):当一个算法的空间复杂度与n成线性比例关系时,可表示为0(n).

复杂度分析

  • 常见排序算法分类(排序算法,是算法的基石,你掌握多少?)
  • 大O时间复杂度表示法:表示代码执行时间随数据规模增长的变化趋势,又称渐进时间复杂度
  • 大O空间复杂度表示法:表示代码执行所占的内存空间随数据规模增长的变化趋势,又称渐进空间复杂度
  • 复杂度分析法则
    • 单段代码,看循环的次数。
    • 多段代码,看代码循环量级。
    • 嵌套代码求乘积,比如递归和多重循环。
    • 多个规模求加法,方法中并行的两个循环。
  • 常用的复杂度级别
    • 多项式阶:随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长,包括,O(1)(常数阶)、O(logn)(对数阶)、O(n)(线性阶)、O(nlogn)(线性对数阶)、O(n2)(平方阶)、O(n3)(立方阶)。
    • 非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这列算法性能极差。包括,O(2^n)(指数阶)、O(n!)(阶乘阶)

(六)

  • 稳定性
  • 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,A1=A2,且A1在A2之前,而在排序后的序列中,A1仍在A2之前,则称这种排序算法是稳定的;否则称为不稳定的。
  • 稳定也可以理解为一切皆在掌握中,元素的位置处在你在控制中.而不稳定算法有时就有点碰运气,随机的成分.当两元素相等时它们的位置在排序后可能仍然相同.但也可能不同.是未可知的.
  • 不稳定排序算法
    • 快选希堆(快速排序,选择排序,希尔排序,堆排序)

(七)

  • 算法总结
  • 所需辅助空间最多:归并排序;
    所需辅助空间最少:堆排序;
    平均速度最快:快速排序;
    不稳定:快速排序,希尔排序,堆排序。

冒泡排序

  • 1、冒泡排序是一种用时间换空间的排序方法,n小时好
  • 2、最坏情况是把顺序的排列变成逆序,或者把逆序的数列变成顺序,最差时间复杂度O(N^2)只是表示其操作次数的数量级
  • 3、最好的情况是数据本来就有序,复杂度为O(n)

快速排序

  • 1、n大时好,快速排序比较占用内存,内存随n的增大而增大,但却是效率高不稳定的排序算法。
  • 2、划分之后一边是一个,一边是n-1个,
    这种极端情况的时间复杂度就是O(N^2)
  • 3、最好的情况是每次都能均匀的划分序列,O(N*log2N)

归并排序

  • 1、n大时好,归并比较占用内存,内存随n的增大而增大,但却是效率高且稳定的排序算法。

Reference

  • 十大经典排序算法(动图演示)
  • Go语言经典排序算法实现
  • [算法总结] 十大排序算法
  • 排序算法的稳定性
  • 各种排序算法时间复杂度
  • 常见排序算法分类(排序算法,是算法的基石,你掌握多少?) !!!

理解状态机原理及实践

发表于 2022-02-23

基本概念

  • 有限状态机FSM

  • 描述事物的有限状态机模型的元素由以下组成:

    1. 状态(State):事物的状态,包括初始状态和所有事件触发后的状态
    2. 事件(Event):触发状态变化或者保持原状态的事件
    3. 行为或转换(Action/Transition):执行状态转换的过程
    4. 检测器(Guard):检测某种状态要转换成另一种状态的条件是否满足
  • https://www.jianshu.com/p/37281543f506

  • 深入浅出理解有限状态机

  • 状态机的要素

  • 状态机可归纳为4个要素,即现态、条件、动作、次态。“现态”和“条件”是因,“动作”和“次态”是果。详解如下:
    1. 现态:是指当前所处的状态。
    2. 条件:又称为“事件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
    3. 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
    4. 次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。

代码实现

  1. 定义状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public enum ProcessState {

    INIT(0, "未开始"),
    READY_ONE_SIDE(1, "一方准备完成"),
    READY_ALL(2, "双方准备完成"),
    CHAT(3, "发言完成"),
    END(4, "结束"),
    ;

    private int val;
    private String desc;

    ProcessState(int val, String desc) {
    this.val = val;
    this.desc = desc;
    }

    public int getVal() {
    return val;
    }
    }
  2. 定义事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public enum ProcessEvent {

    READY_ONE_SIDE(1, "一方准备"),
    READY_ALL(2, "另一方准备"),
    CHAT(3, "发言"),
    FORBID_ALL(4, "全员禁言"),
    ;

    private Integer code;
    private String desc;

    ProcessEvent(Integer code, String desc) {
    this.code = code;
    this.desc = desc;
    }

    public Integer getCode() {
    return code;
    }
    }
  3. 定义状态机

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class ProcessEventConfig {

    private ProcessEvent event;
    private ProcessState fromState;
    private ProcessState toState;

    public String desc(){
    return fromState.getDesc() + "_" + event.getDesc() + "_" + toState.getDesc();
    }
    }


    List<ProcessEventConfig> configList = new ArrayList<>(16);
    configList.add(new ProcessEventConfig(ProcessEvent.READY_ONE_SIDE, ProcessState.INIT, ProcessState.READY_ONE_SIDE));
    configList.add(new ProcessEventConfig(ProcessEvent.READY_ALL, ProcessState.READY_ONE_SIDE, ProcessState.READY_ALL));
    configList.add(new ProcessEventConfig(ProcessEvent.CHAT, ProcessState.READY_ALL, ProcessState.CHAT));
    configList.add(new ProcessEventConfig(ProcessEvent.FORBID_ALL, ProcessState.CHAT, ProcessState.END));

    Map<ProcessEvent, ProcessEventConfig> eventResultStateConfigMap = new EnumMap<>(ProcessEvent.class);

    configList.forEach(eventConfig -> eventResultStateConfigMap.put(eventConfig.getEvent(), eventConfig));

  4. 触发状态变化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public boolean fire(ProcessEvent event, Process process) {

    ProcessEventConfig config = Optional.ofNullable(eventResultStateConfigMap.get(event)).orElseThrow(() -> ExceptionCodeEnum.AGRS_INVALID.newException("不存在该事件"));

    if (process.getStatus() != config.getFromState().getVal()) {
    return false;
    }
    process.setRemark(config.desc());
    process.setStatus(config.getToState().getVal());

    //DB改变状态
    /*update table set status=${status} WHERE id={id} AND status=${beforeStatus}*/
    boolean suc = processManager.updateStatus(process, config.getFromState().getVal());
    if (suc) {
    //变更成功,业务逻辑
    }
    return suc;
    }

nba选秀抽签具体是如何操作的?

发表于 2022-02-20

nba选秀抽签具体是如何操作的?

  • 14个乒乓球分别贴上1-14数字,随机滚出4个,加起来是1001可能,其中11、12、13、14这个组合不算,剩下1000种可能。

  • 闲来无事,使用Go粗暴模拟了一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
package main

import (
"fmt"
"math/rand"
"sort"
"time"
)

/**
https://www.zhihu.com/question/52895544
*/
func main() {
n := 14
m := 4
nn := 1
mm := 1
nm := 1
for i := 1; i <= n; i++ {
nn = nn * i
}
for i := 1; i <= m; i++ {
mm = mm * i
}
for i := 1; i <= (n - m); i++ {
nm = nm * i
}

all_result := make([]string, 1000)
all_result_map := make(map[string]int)

count := 0
for i := 1; i <= 11; i++ {
for j := i + 1; j <= 12; j++ {
for k := j + 1; k <= 13; k++ {
for l := k + 1; l <= 14; l++ {
//fmt.Printf("%d -> %d -> %d -> %d\n", i, j, k, l)
if i == 11 && j == 12 && k == 13 && l == 14 {
continue
}
all_result[count] = fmt.Sprintf("%d-%d-%d-%d", i, j, k, l)
count++
all_result_map[fmt.Sprintf("%d-%d-%d-%d", i, j, k, l)] = count
}
}
}
}

weight_arr := [14]int{250, 199, 156, 119, 88, 63, 43, 28, 17, 11, 8, 7, 6, 5}
all_weight := 0
source_map := make(map[int]int)

for num, weight := range weight_arr {

for i := 0; i < weight; i++ {
source_map[i+1+all_weight] = num + 1
}
all_weight = all_weight + weight
}

/**
模拟前三顺位
*/
all_hit_result := []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
all_hit_result_second_round := []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
all_hit_result_third_round := []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
r := rand.New(rand.NewSource(time.Now().Unix()))
for xx := 0; xx < 1000000; xx++ {

hit_map := make(map[int]int)
for round := 0; round < 3; round++ {
all_ball := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}

chosen_ball := make([]int, 4)
for i := 0; i < 4; i++ {

hit := r.Intn(14 - i)
chosen_ball[i] = all_ball[hit]
index := hit
all_ball = append(all_ball[:index], all_ball[index+1:]...)

}

sort.Ints(chosen_ball)
result := fmt.Sprintf("%d-%d-%d-%d", chosen_ball[0], chosen_ball[1], chosen_ball[2], chosen_ball[3])

if result == "11-12-13-14" {
round--
continue
}

result_rank := source_map[all_result_map[result]]

value := hit_map[result_rank]
if value == 1 {
round--
continue
}

if round == 0 {
all_hit_result[result_rank-1]++
}
if round == 1 {
all_hit_result_second_round[result_rank-1]++
}
if round == 2 {
all_hit_result_third_round[result_rank-1]++
}

hit_map[result_rank] = 1
}

}
fmt.Println("第一轮")
for i, resut_hit := range all_hit_result {
fmt.Printf("%d -> %d\n", i+1, resut_hit)
}
fmt.Println("第二轮")
for i, resut_hit := range all_hit_result_second_round {
fmt.Printf("%d -> %d\n", i+1, resut_hit)
}
fmt.Println("第三轮")
for i, resut_hit := range all_hit_result_third_round {
fmt.Printf("%d -> %d\n", i+1, resut_hit)
}

/**
模拟100万次
第一轮
1 -> 249512
2 -> 198836
3 -> 155714
4 -> 118849
5 -> 88395
6 -> 63352
7 -> 43285
8 -> 28016
9 -> 16892
10 -> 11093
11 -> 7894
12 -> 7004
13 -> 6100
14 -> 5058
第二轮
1 -> 215647
2 -> 188267
3 -> 157704
4 -> 125474
5 -> 96543
6 -> 71033
7 -> 49195
8 -> 32360
9 -> 19803
10 -> 13185
11 -> 9521
12 -> 8252
13 -> 7031
14 -> 5985
第三轮
1 -> 177455
2 -> 171603
3 -> 155736
4 -> 133323
5 -> 106458
6 -> 81193
7 -> 58185
8 -> 38863
9 -> 24146
10 -> 15550
11 -> 11508
12 -> 10064
13 -> 8634
14 -> 7282
*/

}

Java11基础原理和实践

发表于 2022-02-08

一、为什么选择java11

Java 11 是 Java 8 之后的首个 LTS 版本(Long-term support 长期支持版本),所以有不少开发者表示会选择升级至 Java 11。

1、目前相对来说,Java 8 太旧,Java 17 太新,Java11 刚刚好;
2、支持以类路径方式运行,适合过渡阶段升级;
3、其他原因(下文会说明)

二、模块系统

模块系统是Java 9的旗舰功能,使用java9,有必要了解一下。

java9之前存在哪些痛点

1、没有封装

类一旦公开,意味着没有封装。

如果将一个类设置为protected,那么就可以防止其他类访问该类,除非这些类与该类位于相同的包中。但这样做会产生一个有趣的问题:如果想从组件的另一个包中访问该类,同时仍然防止其他类使用该类,那么应该怎么做呢?事实是无法做到。

让类公开,意味着对系统中的所有类型都是公开的,也就意味着没有封装。

Java在过去经历过相当多的安全漏洞。这些漏洞都有一个共同的特点:不知何故,攻击者可以绕过JVM的安全沙盒并访问JDK中的敏感类。从安全的角度来看,在JDK中对危险的内部类进行强封装是一个很大的改进。同时,减少运行时中可用类的数量会降低攻击面。在应用程序运行时中保留大量暂时不使用的类是一种不恰当的做法。而通过使用模块化JDK,可以确定应用程序所需的模块。

2、编译时无法感知依赖缺失或类重复

Java确实使用了显式的import语句。但不幸的是,从严格意义上讲,这些导入是编译时结构,一旦将代码打包到JAR中,就无法确定哪些JAR包含当前JAR运行所需的类型。

在Java 9出现之前,JAR文件似乎是最接近模块的,它们拥有名称、对相关代码进行了分组并且提供了定义良好的公共接口。

(1)依赖缺失
所有类按照-classpath参数定义的顺序加载类。由于类会延迟加载,JVM无法在应用程序启动时有效地验证类路径的完整性,即无法预先知道类路径是否是完整的,或者是否应该添加另一个JAR。

(2)类重复
当类路径上有重复类时,则会出现更为隐蔽的问题。出现相同库的两个版本(如Guava 19和Guava 18)是非常常见的,这两个库JAR以一种未定义的顺序压缩到类路径中。库类的任一版本都可能会被首先加载。此外,有些类还可能会使用来自(可能不兼容的)其他版本的类。此时就会导致运行时异常。

关于类重复

目前存在以下方法缓解或解决

1、通过插件检查类重复

在编译时,检测类是否重复,比如使用插件 maven enforcer plugin,但本质上是通过解压全部jar包,检查所有文件是否存在重复类来判断的,效率可想而知。

2、通过构建工具保证只存在一个版本的jar包

构建工具冲突失败策略设置如gradle中

1
2
3
configurations.all {
    resolutionStrategy {  failOnVersionConflict() }
}

这种设置仅仅是针对jar包冲突,不能保证不同名的jar包是否存在相同的类。

处理jar包冲突有两种策略:

1)强制指定版本:版本冲突太多,指定版本的情况,后续有新的版本间接依赖也无法直接发现。
2)取最高版本:同样有可能引起高版本导致不兼容的问题。

要彻底解决,只有项目依赖完全模块化。

构建工具

Maven、Gradle等构建工具,在运行时没有任何作用。Maven构建了想要运行的工件,但是最后仍然需要配置Java运行时,以便运行正确的模块路径和类路径。虽然手动配置模块路径比类路径要容易得多,但它仍然是重复的工作,因为相关信息已经在pom.xml文件中了。

模块化的好处

模块化 可以 在编译时和运行时获得所有这些信息所带来的优势。这可以防止对来自其他非引用模块的代码的意外依赖。通过检查(传递)依赖关系,工具链可以知道运行模块需要哪些附加模块并进行优化。

Java平台模块系统带来了如下最重要的好处:

1.可靠的配置在编译或运行代码之前,模块系统会检查给定的模块组合是否满足所有依赖关系,从而导致更少的运行时错误。
2.强封装型模块显式地选择了向其他模块公开的内容,从而防止对内部实现细节的意外依赖。
3.可扩展开发显式边界能够让开发团队并行工作,同时可创建可维护的代码库。只有显式导出的公共类型是共享的,这创建了由模块系统自动执行的边界。
4.安全性在JVM的最深层次上执行强封装,从而减少Java运行时的攻击面,同时无法获得对敏感内部类的反射访问。
5.优化由于模块系统知道哪些模块是在一起的,包括平台模块,因此在JVM启动期间不需要考虑其他代码。同时,其也为创建模块分发的最小配置提供了可能性。此外,还可以在一组模块上应用整个程序的优化。

在模块出现之前,这样做是非常困难的,因为没有可用的显式依赖信息,一个类可以引用类路径中任何其他类。

此外还有:
(1)JDK9的模块化可以减少Java程序打包的体积,同时拥有更好的隔离线与封装性。
在JDK9之前,JVM的基础类以前都是在rt.jar这个包里,这个包也是JRE运行的基石。这不仅是违反了单一职责原则,同样程序在编译的时候会将很多无用的类也一并打包,造成臃肿。在JDK9中,整个JDK都基于模块化进行构建,以前的rt.jar, tool.jar被拆分成数十个模块,编译的时候只编译实际用到的模块,同时各个类加载器各司其职,只加载自己负责的模块。
(2)经过破坏后的双亲委派模型更加高效,减少了很多类加载器之间不必要的委派操作

模块系统的基础

本质上就是一个jar包切成多模块(更细的jar包)来引用

1、module-info.java描述文件 关键字说明

requires代表依赖的模块,只有依赖的模块存在才能通过编译并运行.需要注意的是,所有模块均自动隐式依赖java.base模块,不需要显示声明
exports指出需要暴露的包,如果某个包没有被exports,那么其他模块是无法访问的。

Readability:指的是必须exports的包才可被其他模块访问
Accessibility:指的是即使是exports的包,其中的类的可访问下也要基于java的访问修饰符,仅有public修饰的才可被其他模块访问

Implied Readability(隐式Readability, requires transitive):
Readability默认情况下是不会被传递的
requires transitive,传递性依赖生效

由于requires transitive的存在,就可以支持聚合模块。有些聚合模块可以没有任何代码,就一个module-info.java描述文件,比如java.se, java.se.ee模块
不建议直接引用java.se模块,因为它就相当于java9以前版本的rt.jar的内容。

Qualified Exports(有限制的exports)
比如我只想exports某个包给部分模块,而不是所有模块

2、模块化基础

(1)模块拥有一个名称,并对相关的代码以及可能的其他资源进行分组,同时使用一个模块描述符进行描述。模块描述符保存在一个名为module-info.java的文件中。
(2)模块描述符还可以包含exports语句。强封装性是模块的默认特性。只有当显式地导出一个包时(比如示例中的java.util.prefs),才可以从其他模块中访问该包
可访问性和可读性的结合可以确保在模块系统中实现强封装性。
(3)其他模块无法使用未导出包中的任何类型——即使包中的类型是公共的。这是对Java语言可访问性规则的根本变化。
(4)Java 9出现之后,public意味着仅对模块中的其他包公开。只有当导出包包含了公开类型时,其他模块才可以使用这些类型。这就是强封装的意义所在。

在模块出现之前,强封装实现类的唯一方法是将这些类放置到单个包中,并标记为私有。这种做法使得包变得非常笨重,实际上,将类公开只是为了实现不同包之间的访问。通过使用模块,可以以任何方式构建包,并仅导出模块使用者真正必须访问的包。如果愿意的话,还可以将导出的包构成模块的API。

(5)模块提供了导出包的显式信息,从而能够高效地对模块路径进行索引。当从给定的包中查找类型时,Java运行时和编译器可以准确地知道从模块路径中解析哪个模块。而在以前,对整个类路径进行扫描是找到任意类型的唯一方法。

在解析过程中还会完成一些额外的检查。例如,具有相同名称的两个模块在启动时(而不是在运行过程出现类加载失败时)会产生错误。此外,还会检查导出包的唯一性。

模块解析过程以及额外的检查确保了应用程序在一个可靠的环境中运行,降低了运行时失败的可能性。

(6)通过使用未命名模块,尚未模块化的代码可以继续在JDK 9上运行。
当将代码放在类路径上时,会自动使用未命名模块。这也意味着需要构建一个正确的类路径。可一旦使用了未命名模块,前面讨论的模块系统所带来的保障和好处也就没有了。

当在Java 9中使用类路径时,还需要注意两件事情。首先,由于平台是模块化的,因此对内部实现类进行了强封装。

(7)模块系统执行的另一个检查是循环依赖。
在编译时,模块之间的可读性关系必须是非循环的。而在模块中,仍然可以在类之间创建循环关系

(8)通过使用ServiceLoader API可在模块描述符和代码中表示服务
使用这个功能可以对代码的实现进行良好的封装

3、以非模块化方式开发和运行应用

在java9中也是允许你以非模块化方式开发和运行应用的(也就是说,模块化开发是可选的),如果你的应用中没有module-info.java,那么这就是一个unnamed module. java9对于unnamed module的处理方式就是所有的jdk模块均直接可用(模块图中是以java.se模块作为root模块的,也意味着单独处于java.se.ee下的一些包,比如JAXB API是无法访问到的)。

但是需要注意的是,在java8以及之前的版本中,我们可以访问jdk中的一些不推荐访问的内部类,比如com.sun.image.codec.jpeg,但在java9模块化之后被强封装了,所以在java9中无法使用这些内部类,也就是说无法通过编译,但是java9为了保持兼容性,允许之前引用这些内部类的已有的jar或已编译的类正确运行。换言之,就是java9不允许源码中引用这些类,无法通过编译,但是之前版本中引用这些类的已编译class文件是允许正常运行的。

三、迁移

(1)为了便于将基于类路径的应用程序迁移到Java 9,在对平台模块中的类应用深度反射时,或者使用反射来访问非导出包中的类型时,JVM默认显示警告

(2)那些在JDK 8和更早的版本上运行没有任何问题的代码现在会在控制台上显示一个醒目的警告——即使是在生产环境中也是如此。这表明严重破坏了强封装。除了这个警告之外,应用程序仍然照常运行。如警告消息所示,在下一个Java版本中行为将发生变化。将来,即使是类路径上的代码,JDK也会强制执行平台模块的强封装。

(3)可以使用–add-opens标志授予对模块中特定包的类路径深度反射访问。同样,当类路径上的代码尝试访问非导出包中的类型时,可以使用–add-exports来强制导出包。

(4)为了使逐步迁移成为可能,可以混合使用类路径和模块路径。但这不是一种理想的情况,因为只能部分受益于Java模块系统的优点。但是,“小步”迁移是非常有帮助的。

自动模块

Java模块系统提供了一个有用的功能来处理非模块的代码:自动模块。只需将现有的JAR文件从类路径移动到模块路径,而不改变其内容,就可以创建一个自动模块。这样一来,JAR就转换为一个模块,同时模块系统动态生成模块描述符。相比之下,显式模块始终有一个用户自定义的模块描述符。

自动模块的行为不同于显式模块。自动模块具有以下特征:

(1)不包含module-info.class;
(2)它有一个在META-INF/MANIFEST.MF中指定或者来自其文件名的模块名称。
(3)通过requires transitive请求所有其他已解析模块。
(4)导出所有包。
(5)读取路径(或者更准确地讲,读取前面所讨论的未命名模块)。
(6)它不能与其他模块拆分包。

自动模块并不是一个设计良好的模块。虽然请求所有的模块并导出所有的包听起来不像是正确的模块化,但至少是可用的。

自动模块中仍然没有明确的信息来告诉模块系统真正需要哪些模块,这意味着JVM在启动时不会警告自动模块的依赖项丢失。

作为开发人员,负责确保模块路径(或类路径)包含所有必需的依赖项。这与使用类路径没有太大区别。

模块图中的所有模块都需要通过自动模块传递。这实际上意味着,如果请求一个自动模块,那么就可以“免费”获得所有其他模块的隐式可读性。

当使用自动模块时,也会遇到拆分包。在大型应用程序中,由于依赖关系管理不善,通常会发现拆分包。拆分包始终是一个错误,因为它们在类路径上不能可靠地工作。

未命名模块

模块路径上的非模块化JAR变成了自动模块。而类路径变成了未命名模块。

类路径上的所有代码都是未命名模块的一部分。

存在一个很大的限制:未命名模块本身只能通过自动模块读取(只有自动模块可以读取类路径)

当读取未命名模块时自动模块和显式模块之间的区别。显式模块只能读取其他显式模块和自动模块。而自动模块可读取所有模块,包括未命名模块。

未命名模块的可读性只是一种在混合类路径/模块路径迁移方案中有助于自动模块的机制。

JVM为什么没有那么“聪明”呢?它有权访问自动模块中的所有代码,那么为什么不分析依赖关系呢?如果想要分析这些代码是否调用其他模块,JVM需要对所有代码执行字节码分析。虽然这不难实现,但却是一个昂贵的操作,可能会大量增加大型应用程序的启动时间。而且,这样的分析不会发现通过反射产生的依赖关系。由于存在这些限制,JVM不可能也永远不会这样做。相反,JDK附带了另一个工具jdeps,它可以执行字节码分析。

现代构建工具通常有一个“在重复依赖项上失败”的设置,这使得依赖关系管理问题变得更加清晰,从而迫使尽早解决这些问题。强烈建议使用这个设置。关于该问题,Java模块系统比类路径要严格得多。当它检测到一个包从模块路径上的两个模块导出时,就会拒绝启动。相比于以前使用类路径时所遇到的不可靠情况,这种快速失败(fail-fast)机制要好得多。在开发过程中失败好过在生产过程中失败,尤其是当一些不幸的用户碰到一个由于模糊的类路径问题而被破坏的代码路径时。但这也意味着我们必须处理这些问题。盲目地将所有JAR从类路径移至模块路径可能导致在生成的自动模块之间出现拆分包。而这些拆分包将被模块系统所拒绝。

为了使迁移变得容易一些,当涉及自动模块和未命名模块时,上述规则存在一个例外,即承认很多类路径是不正确的,并且包含拆分包。当(自动)模块和未命名模块都包含相同的包时,将使用来自(自动)模块的包,而未命名模块中的包被忽略。

如果在迁移到Java 9时遇到了拆分包问题,那么是无法绕过的。即使从用户角度来看基于类路径的应用程序可以正确工作,你也必须处理这些问题。

拆分包会导致无法以模块系统的方式启动服务

未命名模块(unnamed module)和自动模块(automatic module)总结

(1)一个未经模块化改造的 jar 文件是转为未命名模块还是自动模块,取决于这个 jar 文件出现的路径,如果是类路径,那么就会转为未命名模块,如果是模块路径,那么就会转为自动模块。注意,自动模块也属于命名模块的范畴,其名称是模块系统基于 jar 文件名自动推导得出的
(2)两者还有一个关键区别,分裂包规则适用于自动模块,但对未命名模块无效,也即多个未命名模块可以导出同一个包,但自动模块不允许。
(3) 未命名模块和自动模块存在的意义在于,无论传入的 jar 文件是否一个合法的模块(包含 module descriptor),Java 内部都可以统一的以模块的方式进行处理,这也是 Java 9 兼容老版本应用的架构原理。运行老版本应用时,所有 jar 文件都出现在类路径下,也就是转为未命名模块,对于未命名模块而言,默认导出所有包并且依赖所有模块,因此应用可以正常运行。
(4)基于未命名模块和自动模块,相应的就产生了两种老版本应用的迁移策略,或者说模块化策略。
(5)等所有 jar 包都完成模块化改造,应用改为 -m 方式启动,这也标志着应用已经迁移为真正的 Java 9 应用
(6)-cp 和 -m 可以同时存在 并运行??可以(类路径和模块系统可以混合运行)

java9 自动模块 的意义

(1)个人理解,只是为了让开发者能像模块一样使用起来,引入。实际上封装性方面和类路径没任何区别。
(2)未命名模块和自动模块存在的意义在于,无论传入的 jar 文件是否一个合法的模块(包含 module descriptor),Java 内部都可以统一的以模块的方式进行处理,这也是 Java 9 兼容老版本应用的架构原理。

其他

(1)如何用最快的速度判别它是不是一个模块?它又是如何定义的?
试试看 jar -d -f 。
(2)通过JDK11内置jdeps工具查找过期以及废弃API以及对应的替换
./jdk-11.0.10.jdk/Contents/Home/bin/jdeps --jdk-internals ./build/libs/service.jar
./jdk-11.0.10.jdk/Contents/Home/bin/jdeps --jdk-internals -R --class-path ./build/deploy/* ./build/libs/*
jdeps --jdk-internals -R --class-path 'libs/*' $project
libs是你的所有依赖的目录,$project是你的项目jar包
(3)我们可以在线上使用OpenJDK,开发时,使用任意的JDK。
https://zhuanlan.zhihu.com/p/87157172

库迁移

迁移库和迁移应用程序之间最大的区别在于库被许多应用程序使用。这些应用程序可能运行在不同版本的Java上,所以库通常需要在各种Java版本上工作。期望库的用户在你迁移库的同时切换到Java 9是不现实的。

库的迁移过程由以下步骤组成:

1)确保库可以作为自动模块在Java 9上运行。
2)使用Java 9编译器编译库(主要使用满足需求的最低Java版本),而不使用新的Java 9功能。
3)添加一个模块描述符,并将库转换为显式模块。
4)重构库的结构,以便增加封装性,识别API,并尽可能分割成多个模块(可选)。
5)开始使用库中的Java 9功能,同时保持向后兼容Java 9的早期版本。

库管理需要使用java8编译,模块描述需要用java9编译,且不能使用新特性,否则java8编译不过

项目模块化后,很多第三库(基于反射的),深度反射项目代码,或jdk,可能不能用,要进行特殊处理

历史解决jar包冲突问题得改造成模块化

四、模块系统实践例子

1、相同模块不同版本,新版废弃方法

结论:运行时才会报错

module_2-1.0  依赖 module_1-1.0 printInfo.print2()方法,项目依赖 module_1-2.0 printInfo.print3();
build的时候并不能识别到错误,只有运行时才会发现错误 java.lang.NoSuchMethodError: com.kxw.module1.info.PrintInfo.print2()V

2、不同模块含重复类

结论:不同模块可以存在相同类,在引入时指定模块,可以正确加载到对应类

(1)如果同时引入两个不同模块(不管是直接还是间接),编译时会检测引入的模块是否包含可读的相同路径
编译报错:错误: 未命名的模块同时从 com.kxw.module.four 和 com.kxw.module.three 读取程序包 com.kxw.module.config

(2)如果模块存在相同类,但是却没有通过export声明,仅内部使用,那么编译时将无法检测,但运行时报 (两个模块都没有export的情况,结果一样)
Error occurred during initialization of boot layer
java.lang.LayerInstantiationException: Package com.kxw.module.config in both module com.kxw.module.three and

没声明(export)的情况下,运行时怎么检测有相同的package?
报:Checks for split packages between modules defined to the built-in class loaders.
 ModuleBootstrap#checkSplitPackages 会检查所有module的package是否重复,通过String other = packageToModule.putIfAbsent(p, name);来实现

结论:只要是一个模块,且被引入,有相同包名都可以在启动时被检测出来并报错 (为什么编译时不做的呢?main方法不明确?还是要指定主模块?)

注意:类路径加载的模块都是未命名模块,且不会因为包冲突而报错 

五、classloader的变化

1、类加载器变化概览

双亲委派机制

(1)任意一个 ClassLoader 在尝试加载一个类的时候,都会先尝试调用其父类的相关方法去加载类,如果其父类不能加载该类,则交由子类去完成。 这样的好处:对于任意使用者自定义的 ClassLoader,都会先去尝试让 jvm 的 Bootstrap ClassLoader 去尝试加载(自定义的 ClassLoader 都继承了它们)。那么就能保证 jvm 的类会被优先加载,限制了使用者对 jvm 系统的影响。

(2)jdk9中的双亲委派机制仍然存在,只是在委派之前会优先寻找模块所属的加载器进行加载。

jdk9之前

Boostrap -> Extension -> Application

(1)bootstrap classloader加载rt.jar,jre/lib/endorsed (用来加载 jvm 自身需要的类,c++ 实现,用来加载 rt.jar)
(2)ext classloader加载jre/lib/ext ( 在 jdk9 中已经被移除。)
(3)application classloader加载-cp指定的类 (负责加载ClassPath路径下的类)

jdk9及之后

Application -> Platform -> Boostrap

(1)bootstrap classloader加载lib/modules 中的核心模块 (主要用来加载 java.base 中的核心系统类)
(2)ext classloader被废弃,新增platform classloader,加载lib/modules的非核心模块 (用来加载 jdk 中的非核心模块类)
(3)application classloader加载-cp,-mp指定的类 (用来加载一般的应用类)

注意

JDK9开始,AppClassLoader父类不再是 URLClassLoader;而是BuiltinClassLoader

之前对于动态加载的类,我们总是通过将这个类通过反射调用URLClassLoader加到classpath里面进行加载。这么加载在JDK11中已经无法实现,并且这样加载的类不能卸载。 对于动态加载的类,我们在OpenJDK11中只能自定义类加载器去加载,而不是通过获取APPClassLoader去加载。同时,这么做也有助于你随时能将动态加载的类卸载,因为并没有加载到APPClassLoader。

建议使用自定义的类加载器继承SecureClassLoader去加载类:java.security.SecureClassLoader

BuiltinClassLoader

BuiltinClassLoader 是 jdk9 中代替 URLClassLoader 的加载器,是 PlatformClassLoader 与 AppClassLoader 的父类。其继承了 SecureClassLoader,其核心的方法主要是 loadClassOrNull(…) 方法

如何自定义加载器

(1)java9之前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ArthasClassloader extends URLClassLoader {
public ArthasClassloader(URL[] urls) {
super(urls, ClassLoader.getSystemClassLoader().getParent());
}

@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
final Class<?> loadedClass = findLoadedClass(name);
if (loadedClass != null) {
return loadedClass;
}

// 优先从parent(SystemClassLoader)里加载系统类,避免抛出ClassNotFoundException
if (name != null && (name.startsWith("sun.") || name.startsWith("java."))) {
return super.loadClass(name, resolve);
}
try {
Class<?> aClass = findClass(name);
if (resolve) {
resolveClass(aClass);
}
return aClass;
} catch (Exception e) {
// ignore
}
return super.loadClass(name, resolve);
}
}

(2)java9之后

TODO

2、扩展机制

(1)版本9之前的Java SE允许扩展机制,可以通过将JAR放置在系统属性java.ext.dirs指定的目录中来扩展运行时映像。 如果未设置此系统属性,则使用jre\lib\ext目录作为其默认值。 该机制通过扩展类加载器(这是引导类加载器的子类)和系统类加载器的父级加载了该目录中的所有JAR。 它加载所有应用程序类。 这些JAR的内容对于在此运行时映像上编译或运行的所有应用程序都可见。

(2)Java SE 9不支持扩展机制。 如果需要类似的功能,可以将这些JAR放在类路径的前面。 使用名为JAVA_HOME\lib\ext的目录或设置名为java.ext.dirs的系统属性会导致JDK 9中的错误。

(3)在JDK 9之前,扩展类加载器和应用程序类加载器是java.net.URLClassLoader类的一个实例。 在JDK 9中,平台类加载器(以前的扩展类加载器)和应用程序类加载器是内部JDK类的实例。 如果你的代码依赖于URLClassLoader类的特定方法,代码可能会在JDK 9中崩溃。

3、类加载器的加载流程

在JDK 9之前

a. JDK使用三个类加载器来加载类
b. JDK类加载器以分层方式工作 —— 引导类加载器位于层次结构的顶部。 类加载器将类加载请求委托给上层类加载器。

例如,如果应用程序类加载器需要加载一个类,它将请求委托给扩展类加载器,扩展类加载器又将请求委托给引导类加载器。 如果引导类加载器无法加载类,扩展类加载器将尝试加载它。 如果扩展类加载器无法加载类,则应用程序类加载器尝试加载它。 如果应用程序类加载器无法加载它,则抛出ClassNotFoundException异常。

c. 引导类加载器是扩展类加载器的父类。 扩展类加载器是应用程序类加载器的父类。 引导类加载器没有父类。 默认情况下,应用程序类加载器将是你创建的其他类加载器的父类。

在JDK 9之前的类加载机制

(1)【Boostrap】引导类加载器加载由Java平台组成的引导类,包括JAVA_HOME\lib\rt.jar中的类和其他几个运行时JAR。 它完全在虚拟机中实现。 可以使用-Xbootclasspath/p和-Xbootclasspath/a命令行选项来附加引导目录。 可以使用-Xbootclasspath选项指定引导类路径,该选项将替换默认的引导类路径。 在运行时,sun.boot.class.path系统属性包含引导类路径的只读值。 JDK通过null表示这个类加载器。 也就是说,你不能得到它的引用。 例如,Object类由引导类加载器加载,并且Object.class.getClassLoader()表达式将返回null。

(2)【Extension】扩展类加载器用于通过java.ext.dirs系统属性指定的目录中的位于JAR中的扩展机制加载可用的类。要获得扩展类加载器的引用,需要获取应用程序类加载器的引用,并在该引用上使用getParent()方法。

(3)【Application】应用程序类加载器从由CLASSPATH环境变量指定的应用程序类路径或命令行选项-cp或-classpath加载类。应用程序类加载器也称为系统类加载器,这是一种误称,它暗示它加载系统类。可以使用ClassLoader类的静态方法getSystemClassLoader()获取对应用程序类加载器的引用。

在JDK 9及之后

a. JDK9保持三级分层类加载器架构以实现向后兼容。但是,从模块系统加载类的方式有一些变化。
b. 在JDK 9中,应用程序类加载器【Application】可以委托给平台类加载器【Platform】以及引导类加载器【Boostrap】;平台类加载器【Platform】可以委托给引导类加载器【Boostrap】和应用程序类加载器【Application】。

(1)在JDK 9中,引导类加载器是由类库和代码在虚拟机中实现的(不再是c++实现)。 为了向后兼容,它在程序中仍然由null表示。 例如,Object.class.getClassLoader()仍然返回null。 但是,并不是所有的Java SE平台和JDK模块都由引导类加载器加载。 举几个例子,引导类加载器加载的模块是java.base,java.logging,java.prefs和java.desktop。 其他Java SE平台和JDK模块由平台类加载器和应用程序类加载器加载,这在下面介绍。 JDK 9中不再支持用于指定引导类路径,-Xbootclasspath和-Xbootclasspath/p选项以及系统属性sun.boot.class.path。-Xbootclasspath/a选项仍然受支持,其值存储在jdk.boot.class.path.append的系统属性中。

(2)JDK 9不再支持扩展机制。 但是,它将扩展类加载器保留在名为平台类加载器的新名称下。 ClassLoader类包含一个名为getPlatformClassLoader()的静态方法,该方法返回对平台类加载器的引用。 下表包含平台类加载器加载的模块列表。 平台类加载器用于另一目的。 默认情况下,由引导类加载器加载的类将被授予所有权限。 但是,几个类不需要所有权限。 这些类在JDK 9中已经被取消了特权,并且它们被平台类加载器加载以提高安全性。
下面是JDK 9中由平台加载器加载的模块列表。

(3)应用程序类加载器加载在模块路径上找到的应用程序模块和一些提供工具或导出工具API的JDK模块,如下表所示。 仍然可以使用ClassLoader类的getSystemClassLoader()的静态方法来获取应用程序类加载器的引用。

在JDK 9及之后的的类加载机制

三个内置的类加载器一起协作来加载类。

(1)当应用程序类加载器需要加载类时,它将搜索定义到所有类加载器的模块。 如果有合适的模块定义在这些类加载器中,则该类加载器将加载类,这意味着应用程序类加载器现在可以委托给引导类加载器和平台类加载器。 如果在为这些类加载器定义的命名模块中找不到类,则应用程序类加载器将委托给其父类,即平台类加载器。 如果类尚未加载,则应用程序类加载器将搜索类路径。 如果它在类路径中找到类,它将作为其未命名模块的成员加载该类。 如果在类路径中找不到类,则抛出ClassNotFoundException异常。

(2)当平台类加载器需要加载类时,它将搜索定义到所有类加载器的模块。 如果一个合适的模块被定义为这些类加载器中,则该类加载器加载该类。 这意味着平台类加载器可以委托给引导类加载器以及应用程序类加载器。 如果在为这些类加载器定义的命名模块中找不到一个类,那么平台类加载器将委托给它的父类,即引导类加载器。

(3)当引导类加载器需要加载一个类时,它会搜索自己的命名模块列表。 如果找不到类,它将通过命令行选项-Xbootclasspath/a指定的文件和目录列表进行搜索。 如果它在引导类路径上找到一个类,它将作为其未命名模块的成员加载该类。

JDK 9包含一个名为-Xlog::modules的选项,用于在虚拟机加载时记录调试或跟踪消息,可以看到类加载器及其加载的模块和类。其格式如下:

-Xlog:modules=<debug|trace>
java -Xlog:modules=trace --module-path lib --module com.jdojo.prime.client/com.jdojo.prime.client.Main > test.txt

JDK 9各类加载器对应加载的模块

(1)bootstrap classloader加载lib/modules

1
2
3
4
5
6
7
8
9
10
java.base java.security.sasl
java.datatransfer java.xml
java.desktop jdk.httpserver
java.instrument jdk.internal.vm.ci
java.logging jdk.management
java.management jdk.management.agent
java.management.rmi jdk.naming.rmi
java.naming jdk.net
java.prefs jdk.sctp
java.rmi jdk.unsupported

(2)platform classloader,加载lib/modules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java.activation* jdk.accessibility
java.compiler* jdk.charsets
java.corba* jdk.crypto.cryptoki
java.scripting jdk.crypto.ec
java.se jdk.dynalink
java.se.ee jdk.incubator.httpclient
java.security.jgss jdk.internal.vm.compiler*
java.smartcardio jdk.jsobject
java.sql jdk.localedata
java.sql.rowset jdk.naming.dns
java.transaction* jdk.scripting.nashorn
java.xml.bind* jdk.security.auth
java.xml.crypto jdk.security.jgss
java.xml.ws* jdk.xml.dom
java.xml.ws.annotation* jdk.zipfs

(3)application classloader加载-cp,-mp指定的类

1
2
3
4
5
6
7
8
9
10
11
12
13
jdk.aot jdk.jdeps
jdk.attach jdk.jdi
jdk.compiler jdk.jdwp.agent
jdk.editpad jdk.jlink
jdk.hotspot.agent jdk.jshell
jdk.internal.ed jdk.jstatd
jdk.internal.jvmstat jdk.pack
jdk.internal.le jdk.policytool
jdk.internal.opt jdk.rmic
jdk.jartool jdk.scripting.nashorn.shell
jdk.javadoc jdk.xml.bind*
jdk.jcmd jdk.xml.ws*
jdk.jconsole

4、其他

(1)如果你想访问classpath下的内容,你可以读取环境变量:

1
2
3
4
5
String pathSeparator = System
.getProperty("path.separator");
String[] classPathEntries = System
.getProperty("java.class.path")
.split(pathSeparator);

(2)设置虚拟机参数为”-XX:+TraceClassLoading”来获取类加载信息
(3)如何判断类是有未命名模块加载,还是正常模块加载的 :clazz.getModule()

六、垃圾回收器

JDK 11 默认使用G1

G1会比CMS有更多的内存消耗,多了两个内存结构:

Remembered Sets:每个区块Region都有一个与之对应的Remembered Set来记录不同区块间对象引用(其他收集器只是记录新生代、老年代间互相引用)。用来避免全堆扫描
Collection Sets:将要被回收区块对象的集合,GC时收集器将这些区块对象复制到其他区块中

ZGC

ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延迟垃圾回收器,它的设计目标包括:

停顿时间不超过10ms;
停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
支持8MB~4TB级别的堆(未来支持16TB)。

从设计目标来看,我们知道ZGC适用于大内存低延迟服务的内存管理和回收。

对吞吐量优先的场景,ZGC可能并不适合。
ZGC作为下一代垃圾回收器,性能非常优秀。ZGC垃圾回收过程几乎全部是并发,实际STW停顿时间极短,不到10ms。这得益于其采用的着色指针和读屏障技术。

后续考虑在生产环境使用ZGC。

七、新特性

1、HttpClient API

1
2
3
4
5
6
7
8
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://openjdk.java.net/"))
.build();
client.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();

2、使用Stream dropWhile 简化代码

获取列表从lastId开始到结尾的子集

java8

1
2
3
4
5
6
7
8
9
10
11
int index = 0;
for (UserDynamicSortSetMemberWithScoreBO bo : sortSetKeyList) {
if (bo.getId() == lastId) {
break;
}
index++;
}
return sortSetKeyList.stream()
.skip(index + 1)
.limit(DynamicConfig.feedListPageSize())
.collect(Collectors.toList());

java9

1
2
3
4
5
return sortSetKeyList.stream()
.dropWhile(v -> !v.getId().equals(lastId))
.skip(1)
.limit(DynamicConfig.feedListPageSize())
.collect(Collectors.toList());

3、Optional.ifPresentOrElse

1
2
optional.ifPresentOrElse( x -> System.out.println("Value: " + x),() ->
System.out.println("Not Present."));

八、迁移实践

目标1:可运行

由于项目历史沉重(代码不兼容或依赖未模块化),先使用旧方式运行(类路径而不是模块系统)

1、目标:用最简单快速的方式使用java11编译和运行代码
2、收益:使用属于Java 9-11 新的API、工具和性能改进。
3、未使用Java 9的旗舰功能——模块系统,仍使用类路径方式运行服务

目标2:使用模块系统运行

模块化改造,在现有的项目中如何发挥模块化的作用

自动模块会exports所有包,导致分包问题,不同模块包含相同包

解决的方法:
(1)改代码,去除这种不规范(难,第三方包很多这种)
(2)把这种包放在未命名模块

由于完全模块化分包的问题无法解决,所以要混合两种方式运行项目

gradle对混合两种(类路径和模块系统)的支持不足
直接命令行使用混合两种运行是可以的:

1
./jdk-11.0.10.jdk/Contents/Home/bin/java --add-opens=java.base/jdk.internal.loader=ALL-UNNAMED -cp "build/deploy/*" -p "build/module/*" --add-modules "com.xx.xx.xx" com.xx.xx.xx.Application

目标3:使用java17

1、Gradle 7.3 is the first version fully supporting Java 17

2、Spring Boot 2.5.5是Spring Boot 第一个支持Java 17的版本

其他

  • 新版本jdk现在基本都是规定时间内免费,比如半年或三年,一般情况下直接用openjdk就足够了。
  • OpenJDK和Oracle JDK有什么区别和联系
  • openjdk与Oraclejdk的区别
    • 在2006年11月13日的JavaOne大会上,Sun公司(当时还没被收购)宣布计划要把Java开源,在随后的一年多时间内,它陆续地将JDK的各个部分在GPL v2(GNU General Public License v2)协议下公开了源码,并建立了OpenJDK组织对这些源码进行独立管理。除了极少量的产权代码(Encumbered Code,这部分代码所有权不属于Sun公司,Sun本身也无权进行开源处理)外,OpenJDK几乎拥有了当时SunJDK 的全部代码。
    • 但是随着JDK版本的不断发布,Oracle失去了维护OpenJDK的耐心,因为不赚钱啊。RedHat从Oracle手上接过OpenJDK的管理权利和维护职责。
  • https://www.zhihu.com/question/19646618
  • https://www.zhihu.com/question/353325963
    • OpenJDK 实际上不适合拿来和 OracleJDK 进行对比,OpenJDK 不提供 LTS 服务,而 OracleJDK 每三年都会推出一个 LTS 版进行长期支持。
  • 从 11+ 版本开始 -XX:+UseContainerSupport 已经自动开启, 可以自适应内存pod的内存限制,不用通过-Xms -Xmx 来设置

Reference

  • [Java 9模块化开发_核心原则与实践]
  • 模块化加载_Java9模块化的类加载机制实现剖析
  • 【JDK 11】关于 Java 模块系统,看这一篇就够了
  • Java 9 揭秘(8. JDK 9重大改变)
  • 从JDK8升级到JDK11,看这篇就足够了
  • Load class from exploded module using custom classloader
  • Class Loaders in Java
  • How to load JAR files dynamically at Runtime?
  • Is it possible to load and unload jdk and custom modules dynamically in Java 9?
  • 聊聊java9的classloader
  • JVM_类加载机制详解
  • 新一代垃圾回收器ZGC的探索与实践

NIO总结笔记

发表于 2022-01-17

Java I/O

I/O NIO
面向流 面向缓冲
阻塞IO 非阻塞IO
无 选择器
  • I/O 与 NIO 一个比较重要的区别是我们使用 I/O 的时候往往会引入多线程,每个连接使用一个单独的线程,而 NIO 则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。而由于 NIO 的非阻塞需要一直轮询,比较消耗系统资源,所以异步非阻塞模式 AIO 就诞生了。

5 种 I/O模型

  1. blocking I/O
  2. nonblocking I/O
  3. I/O multiplexing (select and poll)
  4. signal driven I/O (SIGIO)
  5. asynchronous I/O (the POSIX aio_functions)。
  • 不同的操作系统对上述模型支持不同,UNIX 支持 IO 多路复用。不同系统叫法不同,freebsd 里面叫 kqueue,Linux 叫 epoll。而 Windows2000 的时候就诞生了 IOCP 用以支持 asynchronous I/O。
  • Java 是一种跨平台语言,为了支持异步 I/O,诞生了 NIO,Java1.4 引入的 NIO1.0 是基于 I/O 复用的,它在各个平台上会选择不同的复用方式。Linux 用的 epoll,BSD 上用 kqueue,Windows 上是重叠 I/O。
  • IO多路复用,Java NIO的核心类库多路复用器Selector就是基于epoll的多路复用技术实现。
  • IO复用的系统调用方式:select,pselect,poll,epoll(IO复用属于同步IO)
  • 同步阻塞BIO,同步非阻塞NIO,异步非阻塞AIO
    同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞!
    阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回!

Java I/O 的相关方法

  1. 同步并阻塞 (I/O 方法):服务器实现模式为一个连接启动一个线程,每个线程亲自处理 I/O 并且一直等待 I/O 直到完成,即客户端有连接请求时服务器端就需要启动一个线程进行处理。但是如果这个连接不做任何事情就会造成不必要的线程开销,当然可以通过线程池机制改善这个缺点。I/O 的局限是它是面向流的、阻塞式的、串行的一个过程。对每一个客户端的 Socket 连接 I/O 都需要一个线程来处理,而且在此期间,这个线程一直被占用,直到 Socket 关闭。在这期间,TCP 的连接、数据的读取、数据的返回都是被阻塞的。也就是说这期间大量浪费了 CPU 的时间片和线程占用的内存资源。此外,每建立一个 Socket 连接时,同时创建一个新线程对该 Socket 进行单独通信 (采用阻塞的方式通信)。这种方式具有很快的响应速度,并且控制起来也很简单。在连接数较少的时候非常有效,但是如果对每一个连接都产生一个线程无疑是对系统资源的一种浪费,如果连接数较多将会出现资源不足的情况;
  2. 同步非阻塞 (NIO 方法):服务器实现模式为一个请求启动一个线程,每个线程亲自处理 I/O,但是另外的线程轮询检查是否 I/O 准备完毕,不必等待 I/O 完成,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。NIO 则是面向缓冲区,非阻塞式的,基于选择器的,用一个线程来轮询监控多个数据传输通道,哪个通道准备好了 (即有一组可以处理的数据) 就处理哪个通道。服务器端保存一个 Socket 连接列表,然后对这个列表进行轮询,如果发现某个 Socket 端口上有数据可读时,则调用该 Socket 连接的相应读操作;如果发现某个 Socket 端口上有数据可写时,则调用该 Socket 连接的相应写操作;如果某个端口的 Socket 连接已经中断,则调用相应的析构方法关闭该端口。这样能充分利用服务器资源,效率得到大幅度提高;
  3. 异步非阻塞 (AIO 方法,JDK7 发布):服务器实现模式为一个有效请求启动一个线程,客户端的 I/O 请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,每个线程不必亲自处理 I/O,而是委派操作系统来处理,并且也不需要等待 I/O 完成,如果完成了操作系统会另行通知的。该模式采用了 Linux 的 epoll 模型。
  • 在连接数不多的情况下,传统 I/O 模式编写较为容易,使用上也较为简单。但是随着连接数的不断增多,传统 I/O 处理每个连接都需要消耗一个线程,而程序的效率,当线程数不多时是随着线程数的增加而增加,但是到一定的数量之后,是随着线程数的增加而减少的。所以传统阻塞式 I/O 的瓶颈在于不能处理过多的连接。非阻塞式 I/O 出现的目的就是为了解决这个瓶颈。非阻塞 IO 处理连接的线程数和连接数没有联系,例如系统处理 10000 个连接,非阻塞 I/O 不需要启动 10000 个线程,你可以用 1000 个,也可以用 2000 个线程来处理。因为非阻塞 IO 处理连接是异步的,当某个连接发送请求到服务器,服务器把这个连接请求当作一个请求“事件”,并把这个“事件”分配给相应的函数处理。我们可以把这个处理函数放到线程中去执行,执行完就把线程归还,这样一个线程就可以异步的处理多个事件。而阻塞式 I/O 的线程的大部分时间都被浪费在等待请求上了。

AIO 相关的类和接口

  • java.nio.channels.AsynchronousChannel:标记一个 Channel 支持异步 IO 操作;
  • java.nio.channels.AsynchronousServerSocketChannel:ServerSocket 的 AIO 版本,创建 TCP 服务端,绑定地址,监听端口等;
  • java.nio.channels.AsynchronousSocketChannel:面向流的异步 Socket Channel,表示一个连接;
  • java.nio.channels.AsynchronousChannelGroup:异步 Channel 的分组管理,目的是为了资源共享。一个 AsynchronousChannelGroup 绑定一个线程池,这个线程池执行两个任务:处理 IO 事件和派发 CompletionHandler。AsynchronousServerSocketChannel 创建的时候可以传入一个 AsynchronousChannelGroup,那么通过 AsynchronousServerSocketChannel 创建的 AsynchronousSocketChannel 将同属于一个组,共享资源;
  • java.nio.channels.CompletionHandler:异步 IO 操作结果的回调接口,用于定义在 IO 操作完成后所作的回调工作。AIO 的 API 允许两种方式来处理异步操作的结果:返回的 Future 模式或者注册 CompletionHandler,推荐用 CompletionHandler 的方式,这些 handler 的调用是由 AsynchronousChannelGroup 的线程池派发的。这里线程池的大小是性能的关键因素。

Reactor线程模型

常用的Reactor线程模型有三种,分别如下:

  1. Reactor单线程模型;
  2. Reactor多线程模型;
  3. 主从Reactor多线程模型
    • Netty的线程模型并非固定不变,通过在启动辅助类中创建不同的EventLoopGroup实例并通过适当的参数配置,就可以支持上述三种Reactor线程模型。

Netty线程模型

  • http://www.infoq.com/cn/articles/netty-threading-model
  • 主从Reactor线程模型
  • Netty线程开发最佳实践
    • 2.4.1. 时间可控的简单业务直接在IO线程上处理
      如果业务非常简单,执行时间非常短,不需要与外部网元交互、访问数据库和磁盘,不需要等待其它资源,则建议直接在业务ChannelHandler中执行,不需要再启业务的线程或者线程池。避免线程上下文切换,也不存在线程并发问题。
    • 2.4.2. 复杂和时间不可控业务建议投递到后端业务线程池统一处理
      对于此类业务,不建议直接在业务ChannelHandler中启动线程或者线程池处理,建议将不同的业务统一封装成Task,统一投递到后端的业务线程池中进行处理。
      过多的业务ChannelHandler会带来开发效率和可维护性问题,不要把Netty当作业务容器,对于大多数复杂的业务产品,仍然需要集成或者开发自己的业务容器,做好和Netty的架构分层。
    • 2.4.3. 业务线程避免直接操作ChannelHandler
      对于ChannelHandler,IO线程和业务线程都可能会操作,因为业务通常是多线程模型,这样就会存在多线程操作ChannelHandler。为了尽量避免多线程并发问题,建议按照Netty自身的做法,通过将操作封装成独立的Task由NioEventLoop统一执行,而不是业务线程直接操作
      ctx.executor().execute(new Runnable(){})
      如果你确认并发访问的数据或者并发操作是安全的,则无需多此一举,这个需要根据具体的业务场景进行判断,灵活处理。

Netty的“零拷贝”

  • Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。
  • Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
  • Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。

Netty - 灵活的TCP参数配置能力

  • SO_RCVBUF和SO_SNDBUF:通常建议值为128K或者256K;
  • SO_TCPNODELAY:NAGLE算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法;
  • 软中断:如果Linux内核版本支持RPS(2.6.35以上版本),开启RPS后可以实现软中断,提升网络吞吐量。RPS根据数据包的源地址,目的地址以及目的和源端口,计算出一个hash值,然后根据这个hash值来选择软中断运行的cpu,从上层来看,也就是说将每个连接和cpu绑定,并通过这个hash值,来均衡软中断在多个cpu上,提升网络并行处理性能。

心跳实现

  • 使用TCP协议层的Keeplive机制,但是该机制默认的心跳时间是2小时,依赖操作系统实现不够灵活
  • 应用层实现自定义心跳机制,比如Netty实现心跳机制
    • 服务端添加IdleStateHandler心跳检测处理器,并添加自定义处理Handler类实现userEventTriggered()方法作为超时事件的逻辑处理

Netty中比较常用的帧解码器

  1. 固定长度帧解码器 - FixedLengthFrameDecoder
    • 适用场景:每个上层数据包的长度,都是固定的,比如 100。在这种场景下,只需要把这个解码器加到 pipeline 中,Netty 会把底层帧,拆分成一个个长度为 100 的数据包 (ByteBuf),发送到下一个 channelHandler入站处理器。
  2. 行分割帧解码器 - LineBasedFrameDecoder
    • 适用场景:每个上层数据包,使用换行符或者回车换行符做为边界分割符。发送端发送的时候,每个数据包之间以换行符/回车换行符作为分隔。在这种场景下,只需要把这个解码器加到 pipeline 中,Netty 会使用换行分隔符,把底层帧分割成一个一个完整的应用层数据包,发送到下一站。前面的例子,已经对这个解码器进行了演示。
  3. 自定义分隔符帧解码器 - DelimiterBasedFrameDecoder
    • DelimiterBasedFrameDecoder 是LineBasedFrameDecoder的通用版本。不同之处在于,这个解码器,可以自定义分隔符,而不是局限于换行符。如果使用这个解码器,在发送的时候,末尾必须带上对应的分隔符。
  4. 自定义长度帧解码器 - LengthFieldBasedFrameDecoder
    • 这是一种基于灵活长度的解码器。在数据包中,加了一个长度字段(长度域),保存上层包的长度。解码的时候,会按照这个长度,进行上层ByteBuf应用包的提取。
    • LengthFieldPrepender(编码):如果协议中的第一个字段为长度字段,netty提供了LengthFieldPrepender编码器,它可以计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中

Netty API

JDK ByteBuffer VS Netty ByteBuf

  • https://blog.csdn.net/u013828625/article/details/79845512
  • Netty中的ByteBuf则完全对JDK中的ByteBuffer的缺点进行了改进
  • 网络数据的基本单位永远是 byte(字节)。Java NIO 提供 ByteBuffer 作为字节的容器,但该类过于复杂,有点难用。ByteBuf是Netty当中的最重要的工具类,它与JDK的ByteBuffer原理基本上相同,也分为堆内与堆外俩种类型,但是ByteBuf做了极大的优化,具有更简单的API,更多的工具方法和优秀的内存池设计。
  • ByteBuf 维护俩不同索引:一个用于读取,一个用于写入:从 ByteBuf 读取时,其 readerIndex 将会被递增已经被读取的字节数;当写入 ByteBuf 时,writerIndex 也会被递增

SimpleChannelInboundHandler

  • SimpleChannelInboundHandler与ChannelInboundHandlerAdapter
  • Netty随记之ChannelInboundHandlerAdapter、SimpleChannelInboundHandler
  • Netty——高级发送和接收数据handler处理器
  • 每一个Handler都一定会处理出站或者入站(也可能两者都处理)数据,例如对于入站的Handler可能会继承SimpleChannelInboundHandler或者ChannelInboundHandlerAdapter,而SimpleChannelInboundHandler又是继承于ChannelInboundHandlerAdapter,最大的区别在于SimpleChannelInboundHandler会对没有外界引用的资源进行一定的清理,并且入站的消息可以通过泛型来规定。
  • 对于两者关系:
    public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter
  • public void channelRead(ChannelHandlerContext ctx, Object msg) , msg 是ByteBuf类型(未decode转化类型的情况),使用 SimpleChannelInboundHandler 会被自动释放
  1. ChannelInboundHandlerAdapter
    • ChannelInboundHandlerAdapter是ChannelInboundHandler的一个简单实现,默认情况下不会做任何处理,只是简单的将操作通过fire*方法传递到ChannelPipeline中的下一个ChannelHandler中让链中的下一个ChannelHandler去处理。
    • 需要注意的是信息经过channelRead方法处理之后不会自动释放(因为信息不会被自动释放所以能将消息传递给下一个ChannelHandler处理)。
  2. SimpleChannelInboundHandler
    • SimpleChannelInboundHandler支持泛型的消息处理,默认情况下消息处理完将会被自动释放,无法提供fire*方法传递给ChannelPipeline中的下一个ChannelHandler,如果想要传递给下一个ChannelHandler需要调用ReferenceCountUtil#retain方法。
    • channelRead0方法在将来将会重命名为messageReceived

channelRead()和channelReadComplete()

  • channelRead()和channelReadComplete() 方法的区别是什么?
  • channelRead表示接收消息,可以看到msg转换成了ByteBuf,然后打印,也就是把Client传过来的消息打印了一下,你会发现每次打印完后,channelReadComplete也会调用,如果你试着传一个超长的字符串过来,超过1024个字母长度,你会发现channelRead会调用多次,而channelReadComplete只调用一次。

ctx.write() vd ctx.channel().write()

  • Any difference between ctx.write() and ctx.channel().write() in netty?:https://stackoverflow.com/questions/20366418/any-difference-between-ctx-write-and-ctx-channel-write-in-netty
  • Yes there is… Channel.write(..) always start from the tail of the ChannelPipeline and so pass through all the ChannelOutboundHandlers. ChannelHandlerContext.write(…) starts from the current position of the ChannelHandler which is bound to the ChannelHandlerContext and so only pass those ChannelOutboundHandlers that are in front of it.

自定义消息协议

  • len : 表示消息的长度,通常用4个字节保存

  • head : 消息头部

  • body : 消息内容

  • 在实际的项目中,消息格式可能会增加一些标志,例如,开始标记,结束标志,消息序列号,消息的协议类型(json或者二进制等)

Netty 没用JDK1.7的AIO

  • 为什么Netty不用AIO而用NIO?
According to the book the main reasons were:
1. Not faster than NIO (epoll) on unix systems (which is true)
There is no daragram suppport
2. Unnecessary threading model (too much abstraction without usage)
3. I agree that AIO will not easily replace NIO, but it is useful for windows developers nonetheless.

We obviously did not consider Windows as a serious platform so far, and that's why we were neglecting NIO.2 AIO API which was implemented using IOCP on Windows. (On Linux, it wasn't any faster because it was using the same OS facility - epoll.)

Netty-Epoll

  • Selector 实现原理:http://www.jianshu.com/p/2b71ea919d49
  • epoll的两种工作模式:
    • LT:level-trigger,水平触发模式,只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket。
      当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
    • ET:edge-trigger,边缘触发模式,只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。
      当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
      ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
  • 在Linux系统中JDK NIO使用的是 LT ,而Netty epoll使用的是 ET。

其他

  • JDK中有自带的ByteBuffer类,但是netty中的 ByteBuf 算是对Byte Buffer的重新实现。他们没有关联关系。
  • netty推荐使用io.netty.buffer.Unpooled来进行Buff的创建工作。Unpooled是一个工具类,可以为ByteBuf分配空间、拷贝或者封装操作
  • DirectBuffer:使用 DirectBuffer 是一种更加接近系统底层的方法,所以,它的速度比普通的 ByteBuffer 更快。DirectBuffer 相对于 ByteBuffer 而言,读写访问速度快很多,但是创建和销毁 DirectBuffer 的花费却比 ByteBuffer 高。
  • Netty的并发处理能力主要体现在两个方面:
    1. 利用Java语言自身的多线程机制实现消息的并行处理;
    2. 利用Java NIO类库的Selector实现多路复用,一个NIO线程可以同时并发处理成百上千个通信链路,实现海量客户端的并发接入和处理。
  • netty.pipeline执行顺序:在给定的示例配置中,当事件进入到入站时,处理程序计算顺序为(代码行:从上到下)。当一个事件出站时,顺序是(代码行:从下到上)。

Reference

  • Netty系列之Netty高性能之道
  • Netty实战-自定义解码器处理半包消息
  • TODO nio-demo/docs
  • TODO nio_demo/netty-definitive-guide/netty-definitive-guide-notes.md

如何做数据库迁移

发表于 2022-01-14

先抛出以下问题:
把MHA-1集群的Database-C 迁到 MHA-2 集群中,有没有简单高效的方案?


  • 方案1、在两个MHA集群,在建一套MHA3。MHA2的主作为MHA1的从?这样MH3切换的时候,应用服务就可以直接感知切到新库了;
  • 方案2、先把C同步到2集群,然后做otter同步

  • 方案1: 存在的问题:
    • 中间再搞一套MHA,不是运维麻烦,而是多了一套中间库,可能会导致MHA的切换逻辑紊乱。
  • 方案2:
    • 业务方的服务要自行选择数据源进行切换 (流量低且数据一致性要求不高,考虑通过开关切换;否则需考虑停服等其他方案)
    • otter双向同步,业务最好同时只写一边(要注意旧库是否有自增主键;同步冲突策略设置,默认会冲突会同步中断)

raft学习笔记

发表于 2021-10-27
  • Raft将共识问题分解三个子问题:

    1. Leader election 领导选举:有且仅有一个leader节点,如果leader宕机,通过选举机制选出新的leader;
    2. Log replication 日志复制:leader从客户端接收数据更新/删除请求,然后日志复制到follower节点,从而保证集群数据的一致性;
    3. Safety 安全性:通过安全性原则来处理一些特殊case,保证Raft算法的完备性;
  • 所以,Raft算法核心流程可以归纳为:

    • 首先选出leader,leader节点负责接收外部的数据更新/删除请求;
    • 然后日志复制到其他follower节点,同时通过安全性的准则来保证整个日志复制的一致性;
    • 如果遇到leader故障,followers会重新发起选举出新的leader;
  • Raft规定:只有拥有最新提交日志的follower节点才有资格成为leader节点。具体做法:candidate竞选投票时会携带最新提交日志,follower会用自己的日志和candidate做比较。

  • 因为日志提交需要超过半数的节点同意,所以针对日志同步落后的follower(还未同步完全部日志,导致落后于其他节点)在竞选leader的时候,肯定拿不到超过半数的票,也只有那些完成同步的才有可能获取超过半数的票成为leader。

  • 日志更新判断方式是比较日志项的term和index:

    • 如果TermId不同,选择TermId最大的;
    • 如果TermId相同,选择Index最大的;
  • Raft对日志提交有额外安全机制:leader只能提交当前任期Term的日志,旧任期Term(以前的数据)只能通过当前任期Term的数据提交来间接完成提交。简单的说,日志提交有两个条件需要满足:

    • 当前任期;
    • 复制结点超过半数;
  • Raft在日志项提交上增加了限制:只有当前任期且复制超过半数的日志才可以提交。

Reference

  • 分布式一致性算法Raft
  • Raft 一致性算法论文中文译文

CQRS笔记

发表于 2021-10-12
  • 后端开发实践系列——简单可用的CQRS编码实践

个人认为:

  1. 清除消息业务:写时只记录一个”清除时间”,读时只读取”清除时间”之后的数据,这是一种CQRS (避免写时操作太多数据,QPS不高,但单个请求需要操作多条数据的情况)
  2. 消息通知业务:写时只写在redis,定时任务从redis批量获取数据逐步写入的数据库(QPS高,但单个请求却不多,可以批量操作的情况)
<1…91011…17>

166 日志
191 标签
RSS
© 2025 Kingson Wu
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4