开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

王者并发课-青铜8:分工协作-从本质认知线程的状态和动作方法

发表于 2021-05-31

欢迎来到《王者并发课》,本文是该系列文章中的第8篇。

在本篇文章中,我将从多线程的本质出发,为你介绍线程相关的状态和它们的变迁方式,并帮助你掌握这块知识点。

一、多线程的本质是分工协作

如果你是王者的玩家,那么你一定知道王者中的众多英雄分为主要分为几类,比如法师、战士、坦克、辅助等等。一些玩家对这些分类可能并不了解,甚至会觉得,干嘛要搞得这么复杂,干不完了嘛。这…当然不可以!

抱此想法的如果不是青铜玩家,想必就是战场上的那些个人英雄主义玩家,在他们眼里没有团队。然而,只有王者知道,比赛胜利的关键,在于团队的分工协作。各自为战必将一团乱麻、溃不成军,正所谓单丝不成线,独木难成林。

分工协作无处不在,峡谷中需要分工协作,现实中我们的工作更是社会化分工的结果,因为社会的本质就是分工协作。

而我要告诉你的是,在并发编程里,多线程的本质也是分工协作,每个线程恰似一个英雄,有着自己的职责、状态和技能(动作方法)。所谓线程的状态、方法实现不过都是为了完成线程间的分工协作。换句话说,线程状态的存在不是目的,而是实现分工协作的方式。所以,理解线程的线程状态和驱动方法,首先要理解它们为什么而存在。

二、从协作认知线程的状态

线程的状态是线程在协作过程中的瞬时特征。根据协作的需要,线程总共有六种状态,分别是NEW、RUNNABLE、WAITING、TIMED_WAITING、BLOCKED和TERMINATED等。比如,我们创建一个英雄哪吒的线程neZhaPlayer:

1
java复制代码Thread neZhaPlayer = new Thread(new NeZhaRunnable());

那么,线程创建之后,接下来它将在下图所示的六种状态中变迁。刚创建的线程处于NEW的状态,而如果我们调用neZhaPlayer.start(),那它将会进入RUNNABLE状态。

六种不同状态的含义是这样的:

  • NEW:线程新建但尚未启动时所处的状态,比如上面的neZhaPlayer;
  • RUNNABLE:在 Java 虚拟机中执行的线程所处状态。需要注意的是,虽然线程当前正在被执行,但可能正在等待其他线程释放资源;
  • WAITING:无限期等待另一个线程执行特定操作来解除自己的等待状态;
  • TIMED_WAITING:限时等待另一个线程执行或自我解除等待状态;
  • BLOCKED:被阻塞等待其他线程释放Monitor Lock;
  • TERMINATED:线程执行结束。

在任意特定时刻,一个线程都只能处于上述六种状态中的一种。需要你注意的是RUNNABLE这个状态,它有些特殊。确切地说,它包含READY和RUNNING两个细分状态,下一章节的图示中有明确标示。

另外,前面我们已经介绍过Thread类,对于线程各状态的表述,你可以直接阅读JDK中的Thread.State枚举,并可以通过Thread.getState()查看当前线程的瞬时状态。

三、从线程状态变迁看背后的方法驱动

和人类的交流类似,在多线程的协作时,它们也需要交流。所以,线程状态的变迁就需要不同的方法来实现交流,比如刚创建的线程需要通过调用start()将线程状态由NEW变迁为RUNNABLE。

下图所展示的正是线程间的状态变迁以及相关的驱动方法,你可以先大概浏览一遍,随后再结合下文的各关键方法的表述深入理解。

需要注意的是,本文不会详细介绍线程状态相关的所有方法,这既不现实也毫无必要。上面这幅宝藏图示是理解本文所述知识的核心,下面所介绍的几个主要方法也并非为了你记忆,而是为了让你更好理解上面这幅图。

在你理解了这幅宝图之后,你便可以完全自行去了解其他更多的方法。

1. start:对战开始,敌军还有5秒到达战场

1
2
3
4
5
6
7
8
java复制代码public class NeZhaRunnable implements Runnable {
public void run() {
System.out.println("我是哪吒,我去上路");
}
}

Thread neZhaPlayer = new Thread(new NeZhaRunnable());
neZhaPlayer.start();

start()方法主要将完成线程状态从NEW到RUNNABLE的变迁,这里有两个点:

  • 创建新的线程;
  • 由新的线程执行其中的run()方法。

需要注意的是,你不可以重复调用start()方法,否则会抛出IllegalThreadStateException异常。

2. wait和notify:我在等你,好了请告诉我

哪吒每次在使用完大招后,都需要经历几十秒的冷却时间才可以再次使用,接下来我们通过代码片段来模拟这个过程。

我们先定义一个Player类,这个类中包含了fight()和refreshSkills()两个方法,分别用于进攻和技能刷新,代码片段如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Java复制代码public class Player {
public void fight() {
System.out.println("大招未就绪,冷却中...");
synchronized (this) {
try {
this.wait();
System.out.println("大招已就绪,发起进攻!");
} catch (InterruptedException e) {
System.out.println("大招冷却被中断!");
}
}
}
public void refreshSkills() {
System.out.println("技能刷新中...");
synchronized (this) {
this.notifyAll();
System.out.println("技能已刷新!");
}
}
}

随后,我们写一段main()方法使用刚才创建的Player。注意,这里我们创建了两个线程分别调用Player中的不同方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java复制代码public static void main(String[] args) throws InterruptedException {
final Player neZha = new Player();
Thread neZhaFightThread = new Thread() {
public void run() {
neZha.fight();
}
};
Thread skillRefreshThread = new Thread() {
public void run() {
neZha.refreshSkills();
}
};
neZhaFightThread.start();
skillRefreshThread.start();
}

代码运行结果如下:

1
2
3
4
5
6
vbnet复制代码大招未就绪,冷却中...
技能刷新中...
技能已刷新!
大招已就绪,发起进攻!

Process finished with exit code 0

从运行的结果看,符合预期。相信你已经看到了,在上面的代码中我们使用了wait()和notify()两个函数。这两个线程是如何协作的呢?往下看。

首先,neZhaAttachThread调用了neZha.fight()这个方法。可是,当哪吒想发起进攻的时候,竟然大招还没有冷却!于是,这个线程不得不通过wait()方法进入等待队列。

紧接着,skillRefreshThread调用了neZha.refreshSkills()这个方法。并且,在执行结束后又调用了notify()方法。有趣的事情发生了,前面处于等待队列中的neZhaAttachThread竟然又“复活”了,并且大喊了一声:大招已经就绪,发起进攻!

这是怎么回事?理解这块逻辑,你需要了解以下几个知识点:

  • wait():看到wait()时,你可以简单粗暴地认为每个对象都有一个类似于休息室的等待队列,而wait()正是把当前线程送进了等待队列并暂停继续执行;
  • notify():如果说wait()是把当前线程送进了等待队列,那么notify()则是从等待队列中取出线程。此外,和notify()具有相似功能的还有个notifyAll()。与notify()不同的是,notifyAll()会取出等待队列中的所有线程;

看到这,你是不是觉得wait()和notify()简直是完美的一对?其实不然。真相不仅不完美,还很不靠谱!

wait()和notify()在执行时都必须先获得锁,这也是你在代码中看到synchronized的原因。notify()在释放锁的时候,会从等待队列中取出线程,此时的线程必须获得锁之后才能继续运行。那么,问题来了。如果队列中有多个线程时,notify()能取出指定的线程吗?答案是不能!

换句话说,如果队列中有多个线程,你将无法预料后续的执行结果!notifyAll()虽然可以取出所有的线程,但最终也只能有一个线程能获得锁。

是不是有点懵?懵就对了。所以你看,wait()和notify()是不是很不靠谱?因此,如果你需要在项目代码中使用它们,请务必要小心谨慎!

此外,如果你阅读过《Effective Java》,可以看到在这本书里作者Josh Bloch也是强烈建议不要随便使用这对组合。因为它们就像Java中的“汇编语言”,确实复杂且不容易控制,如果有相似的并发场景需要处理,可以考虑使用Java中的其他高级的并发工具。

3. interrupt:做完这一单,我就退隐江湖

在王者的游戏中,如果英雄血量没了,可以回城补血。回城大概需要5秒左右,如果在回城的过程中,突然被攻击或需要移位,那么回城就会中断。接下来,下面我们看看怎么模拟回城中的中断。

现在Player中定义backHome()方法用于回城。

1
2
3
4
5
6
7
8
9
10
11
Java复制代码 public void backHome() {
System.out.println("回城中...");
synchronized (this) {
try {
this.wait();
System.out.println("已回城");
} catch (InterruptedException e) {
System.out.println("回城被中断!");
}
}
}

接下来启动新的线程调用backHome()回城补血。

1
2
3
4
5
6
7
8
9
10
Java复制代码public static void main(String[] args) throws InterruptedException {
final Player neZha = new Player();
Thread neZhaBackHomeThread = new Thread() {
public void run() {
neZha.backHome();
}
};
neZhaBackHomeThread.start();
neZhaBackHomeThread.interrupt();
}

运行结果如下:

1
2
3
4
vbnet复制代码回城中...
回城被中断!

Process finished with exit code 0

可以看到,回城被中断了,因为我们调用了interrupt()方法!那么,在线程中的中断是怎么回事?往下看。

在Thread中,我们可以通过interrupt()中断线程。然而,如果你细心的话,还会发现Thread中除了interrupt()方法之外,竟然还有两个长相酷似的方法:interrupted()和isInterrupted()。这就要小心了。

  • interrupt():将线程设置为中断状态;
  • interrupted():取消线程的中断状态;
  • isInterrupted():判断线程是否处于中断状态,而不会变更线程状态。

不得不说,interrupt()和interrupted()这两个方法的命名实在糟糕,你在编码时可不要学习它,方法的名字应该清晰明了表达出其意图。

那么,当我们调用interrupt()时,所调用对象的线程会立即抛出InterruptedException异常吗?其实不然,这里容易产生误解。

interrupt()方法只是改变了线程中的中断状态而已,并不会直接抛出中断异常。中断异常的抛出必须是当前线程在执行wait()、sleep()、join()时才会抛出。换句话说,如果当前线程正在处理其他的逻辑运算,不会被中断,直到下次运行wait()、sleep()、join()时!

4. join:稍等,等我结束你再开始

在前面的示例中,哪吒发起进攻和技能刷新两个线程是同时开始的。然而,我们在前面已经说了wait()和notify()并不靠谱,所以我们想在技能刷新结束后再执行后续动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java复制代码public static void main(String[] args) throws InterruptedException {
final Player neZha = new Player();
Thread neZhaFightThread = new Thread() {
public void run() {
neZha.fight();
}
};
Thread skillRefreshThread = new Thread() {
public void run() {
neZha.refreshSkills();
}
};

skillRefreshThread.start();
skillRefreshThread.join(); //这里是重点
neZhaFightThread.start();
}

主线程调用join()时,会阻塞当前线程继续运行,直到目标线程中的任务执行完毕。此外,在调用join()方法时,也可以设置超时时间。

小结

以上就是关于线程状态及变迁的全部内容。在本文中,我们介绍了多线程的本质是协作,而状态和动作方法是实现协作的方式。无论是面试还是其他的资料中,线程的状态和方法都是重点。然而,我希望你明白了的是,对于本文知识点的掌握,不要从静态的角度死记硬背,而是要动静结合,从动态的方法认知静态的状态。

正文到此结束,恭喜你又上了一颗星✨

夫子的试炼

在本文中,我们并没有提到yield()、Thread.sleep()和Thread.current()等方法。不过,如果你感兴趣的话,不妨检索资料:

  • 了解yield()并对比它和join()的不同;
  • 了解wait()并对比它和Thread.sleep()的不同;
  • 了解Thread.current()的主要用法和它的实现。

延伸阅读

  • Life Cycle of a Thread in Java
  • Enum Thread.State
    *《王者并发课》专栏文集下载:github.com/ThoughtsBet…

关于作者

专注高并发领域创作。姊妹篇小册《高并发秒杀的设计精要与实现》作者,关注公众号【MetaThoughts】,及时获取文章更新和文稿。


如果本文对你有帮助,欢迎点赞、关注、监督,我们一起从青铜到王者。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java项目性能分析工具(神器)--阿里Arthas

发表于 2021-05-31

前言

在使用 Arthas 之前,当遇到 Java 线上问题时,如 CPU 飙升、负载突高、内存溢出等问题,你需要查命令,查网络,然后 jps、jstack、jmap、jhat、jstat、hprof 等一通操作。最终焦头烂额,还不一定能查出问题所在。而现在,大多数的常见问题你都可以使用 Arthas 轻松定位,迅速解决,及时止损,准时下班。

1
2
3
4
5
scss复制代码相对JDK自带的监控工具Arthas优势
1、个人感觉Arthas增加更丰富的监控命令,比如针对性能的trace可以监控响应耗时。
2、提供了web console可以通过浏览器直接输入及执行命令。
3、方便远程监控,是个单独的监控工具,不需要增加jvm配置也重启jvm(当然如果
生成环境是封闭的效果就不太明显了)。

1、Arthas 介绍

Arthas 是 Alibaba 在 2018 年 9 月开源的 Java 诊断工具。支持 JDK6+, 采用命令行交互模式,提供 Tab 自动不全,可以方便的定位和诊断线上程序运行问题。截至本篇文章编写时,已经收获 Star 17000+。

Arthas 官方文档十分详细,本文也参考了官方文档内容,同时在开源在的 Github 的项目里的 Issues 里不仅有问题反馈,更有大量的使用案例,也可以进行学习参考。

开源地址:github.com/alibaba/art…

官方文档:arthas.aliyun.com/doc/

2、Arthas 使用场景

得益于 Arthas 强大且丰富的功能,让 Arthas 能做的事情超乎想象。下面仅仅列举几项常见的使用情况,更多的使用场景可以在熟悉了 Arthas 之后自行探索。

  1. 是否有一个全局视角来查看系统的运行状况?
  2. 为什么 CPU 又升高了,到底是哪里占用了 CPU ?
  3. 运行的多线程有死锁吗?有阻塞吗?
  4. 程序运行耗时很长,是哪里耗时比较长呢?如何监测呢?
  5. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  6. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  7. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  8. 有什么办法可以监控到 JVM 的实时运行状态?

3、Arthas 怎么用

前文已经提到,Arthas 是一款命令行交互模式的 Java 诊断工具,由于是 Java 编写,所以可以直接下载相应 的 jar 包运行。

3.1 安装

可以在官方 Github 上进行下载,如果速度较慢,可以尝试国内的码云 Gitee 下载。

1
2
3
4
5
6
7
8
bash复制代码# 从阿里云下载jar包
curl -O https://arthas.aliyun.com/arthas-boot.jar
# github下载
wget https://alibaba.github.io/arthas/arthas-boot.jar
# 或者 Gitee 下载
wget https://arthas.gitee.io/arthas-boot.jar
# 打印帮助信息
java -jar arthas-boot.jar -h

3.2 运行

Arthas 只是一个 java 程序,所以可以直接用 java -jar 运行。运行时或者运行之后要选择要监测的 Java 进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码# 运行方式1,先运行,在选择 Java 进程 PID
java -jar arthas-boot.jar
# 选择进程(输入[]内编号(不是PID)回车)
eternity@01ak31301447966 project % java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.4.0
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 769
[2]: 58579 org.jeecg.JeecgApplication
[3]: 58580 org.jetbrains.jps.cmdline.Launcher
[4]: 27300 org.jetbrains.jps.cmdline.Launcher
[5]: 32197 myssh.App

# 运行方式2,运行时选择 Java 进程 PID
java -jar arthas-boot.jar [PID]

查看 PID 的方式可以通过 ps 命令,也可以通过 JDK 提供的 jps命令。

注意:命令窗口要在电脑或服务器环境下,不能进入arthas环境下

1
2
3
4
ruby复制代码# 查看运行的 java 进程信息
$ jps -mlvV
# 筛选 java 进程信息
$ jps -mlvV | grep [xxx]

jps 筛选想要的进程方式。

1
2
perl复制代码eternity@01ak31301447966 ~ % jps -mlvV|grep arthas
67250 arthas-boot.jar

在出现 Arthas Logo 之后就可以使用命令进行问题诊断了。下面会详细介绍。

更多的启动方式可以参考 help 帮助命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lua复制代码# 其他用法
EXAMPLES:
java -jar arthas-boot.jar <pid>
java -jar arthas-boot.jar --target-ip 0.0.0.0
java -jar arthas-boot.jar --telnet-port 9999 --http-port -1
java -jar arthas-boot.jar --tunnel-server 'ws://192.168.10.11:7777/ws'
java -jar arthas-boot.jar --tunnel-server 'ws://192.168.10.11:7777/ws'
--agent-id bvDOe8XbTM2pQWjF4cfw
java -jar arthas-boot.jar --stat-url 'http://192.168.10.11:8080/api/stat'
java -jar arthas-boot.jar -c 'sysprop; thread' <pid>
java -jar arthas-boot.jar -f batch.as <pid>
java -jar arthas-boot.jar --use-version 3.1.4
java -jar arthas-boot.jar --versions
java -jar arthas-boot.jar --session-timeout 3600
java -jar arthas-boot.jar --attach-only
java -jar arthas-boot.jar --repo-mirror aliyun --use-http

3.3 web console

Arthas 目前支持 Web Console,在成功启动连接进程之后就已经自动启动,可以直接访问 http://127.0.0.1:8563/ 访问,页面上的操作模式和控制台完全一样。

3.4 常用命令

下面列举一些 Arthas 的常用命令,看到这里你可能还不知道怎么使用,别急,后面会一一介绍。

命令

介绍

dashboard

命令

介绍

thread

命令

介绍

watch

命令

介绍

trace

命令

介绍

stack

命令

介绍

tt

命令

介绍

monitor

命令

介绍

jvm

命令

介绍

vmoption

命令

介绍

sc

命令

介绍

sm

命令

介绍

jad

命令

介绍

classloader

命令

介绍

heapdump

命令

介绍

3.5 退出

使用 shutdown 退出时 Arthas 同时自动重置所有增强过的类 。

4、Arthas 常用操作

上面已经了解了什么是 Arthas,以及 Arthas 的启动方式,下面会依据一些情况,详细说一说 Arthas 的使用方式。在使用命令的过程中如果有问题,每个命令都可以是 -h 查看帮助信息。

首先编写一个有各种情况的测试类运行起来,再使用 Arthas 进行问题定位,

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
scss复制代码import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;

/**
* <p>
* Arthas Demo
* 公众号:未读代码
*
* @Author niujinpeng
*/
@Slf4j
public class Arthas {

private static HashSet hashSet = new HashSet();
/** 线程池,大小1*/
private static ExecutorService executorService = Executors.newFixedThreadPool(1);

public static void main(String[] args) {
// 模拟 CPU 过高,这里注释掉了,测试时可以打开
// cpu();
// 模拟线程阻塞
thread();
// 模拟线程死锁
deadThread();
// 不断的向 hashSet 集合增加数据
addHashSetThread();
}

/**
* 不断的向 hashSet 集合添加数据
*/
public static void addHashSetThread() {
// 初始化常量
new Thread(() -> {
int count = 0;
while (true) {
try {
hashSet.add("count" + count);
Thread.sleep(10000);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}

public static void cpu() {
cpuHigh();
cpuNormal();
}

/**
* 极度消耗CPU的线程
*/
private static void cpuHigh() {
Thread thread = new Thread(() -> {
while (true) {
log.info("cpu start 100");
}
});
// 添加到线程
executorService.submit(thread);
}

/**
* 普通消耗CPU的线程
*/
private static void cpuNormal() {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
while (true) {
log.info("cpu start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}

/**
* 模拟线程阻塞,向已经满了的线程池提交线程
*/
private static void thread() {
Thread thread = new Thread(() -> {
while (true) {
log.debug("thread start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 添加到线程
executorService.submit(thread);
}

/**
* 死锁
*/
private static void deadThread() {
/** 创建资源 */
Object resourceA = new Object();
Object resourceB = new Object();
// 创建线程
Thread threadA = new Thread(() -> {
synchronized (resourceA) {
log.info(Thread.currentThread() + " get ResourceA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread() + "waiting get resourceB");
synchronized (resourceB) {
log.info(Thread.currentThread() + " get resourceB");
}
}
});

Thread threadB = new Thread(() -> {
synchronized (resourceB) {
log.info(Thread.currentThread() + " get ResourceB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread() + "waiting get resourceA");
synchronized (resourceA) {
log.info(Thread.currentThread() + " get resourceA");
}
}
});
threadA.start();
threadB.start();
}
}

4.1 全局监控

使用 dashboard 命令可以概览程序的 线程、内存、GC、运行环境信息。

4.2 CPU 为什么起飞了

上面的代码例子有一个 CPU 空转的死循环,非常的消耗 CPU性能,那么怎么找出来呢?

使用 thread查看所有线程信息,同时会列出每个线程的 CPU 使用率,可以看到图里 ID 为12 的线程 CPU 使用100%。

使用命令 thread 12 查看 CPU 消耗较高的 12 号线程信息,可以看到 CPU 使用较高的方法和行数(这里的行数可能和上面代码里的行数有区别,因为上面的代码在我写文章时候重新排过版了)。

上面是先通过观察总体的线程信息,然后查看具体的线程运行情况。如果只是为了寻找 CPU 使用较高的线程,可以直接使用命令 thread -n [显示的线程个数] ,就可以排列出 CPU 使用率 Top N 的线程。

定位到的 CPU 使用最高的方法。

4.3 线程池线程状态

定位线程问题之前,先回顾一下线程的几种常见状态:

  • RUNNABLE 运行中
  • TIMED_WAITIN调用了以下方法的线程会进入TIMED_WAITING
  1. Thread#sleep()
  2. Object#wait() 并加了超时参数
  3. Thread#join() 并加了超时参数
  4. LockSupport#parkNanos()
  5. LockSupport#parkUntil()
  • WAITING当线程调用以下方法时会进入WAITING状态:
  1. Object#wait() 而且不加超时参数
  2. Thread#join() 而且不加超时参数
  3. LockSupport#park()
  • BLOCKED 阻塞,等待锁

上面的模拟代码里,定义了线程池大小为1 的线程池,然后在 cpuHigh 方法里提交了一个线程,在 thread方法再次提交了一个线程,后面的这个线程因为线程池已满,会阻塞下来。

使用 thread | grep pool 命令查看线程池里线程信息。

可以看到线程池有 WAITING 的线程。

4.4 线程死锁

上面的模拟代码里 deadThread方法实现了一个死锁,使用 thread -b 命令查看直接定位到死锁信息。

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
scss复制代码/**
* 死锁
*/
private static void deadThread() {
/** 创建资源 */
Object resourceA = new Object();
Object resourceB = new Object();
// 创建线程
Thread threadA = new Thread(() -> {
synchronized (resourceA) {
log.info(Thread.currentThread() + " get ResourceA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread() + "waiting get resourceB");
synchronized (resourceB) {
log.info(Thread.currentThread() + " get resourceB");
}
}
});

Thread threadB = new Thread(() -> {
synchronized (resourceB) {
log.info(Thread.currentThread() + " get ResourceB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread() + "waiting get resourceA");
synchronized (resourceA) {
log.info(Thread.currentThread() + " get resourceA");
}
}
});
threadA.start();
threadB.start();
}

检查到的死锁信息。

注意, 目前只支持找出synchronized关键字阻塞住的线程, 如果是java.util.concurrent.Lock, 目前还不支持。

4.5 反编译

上面的代码放到了包 com下,假设这是一个线程环境,当怀疑当前运行的代码不是自己想要的代码时,可以直接反编译出代码,也可以选择性的查看类的字段或方法信息。

如果怀疑不是自己的代码,可以使用 jad 命令直接反编译 class。

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
ini复制代码# 自己定义的类
[arthas@20451]$ jad com.fridge.service.impl.CardActivateAppServiceImpl

ClassLoader:
+-sun.misc.Launcher$AppClassLoader@18b4aac2
+-sun.misc.Launcher$ExtClassLoader@4278a03f

Location:
/Users/admin/project/uhome/Doc_01_Uhome/mythicalanimals/target/classes/

/*
* Decompiled with CFR.
*
* Could not load the following classes:
* com.fridge.common.base.enumeration.HttpCodeEnum
* com.fridge.common.base.exception.CommonException
* com.fridge.common.bus.inputVO.CardActivateAppInputVO
* com.fridge.common.bus.inputVO.CardActivateEditStatusInputVO
* com.fridge.common.bus.inputVO.CardActivateQueryInputVO
* com.fridge.common.bus.outVO.CardActivateAppOutputVO
* com.fridge.common.bus.outVO.CardActivateQueryOutPutVO
* com.fridge.dao.sl.CardAfterRecordMapper
* com.fridge.dao.sl.CardConsumeDetailMapper
* com.fridge.example.CardAfterRecordExample
* com.fridge.example.CardAfterRecordExample$Criteria
* com.fridge.model.CardActivate
* com.fridge.model.CardDevice
* com.fridge.model.CardRules
* com.fridge.model.CardStore
* com.fridge.model.CardUser
* com.fridge.repository.CardActivateAppRepository
* com.fridge.repository.CardUserRepository
* com.fridge.service.CardActivateAppService
* com.fridge.util.BeanUtils
*/
package com.fridge.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class CardActivateAppServiceImpl
implements CardActivateAppService {
private static final Logger log = LoggerFactory.getLogger(CardActivateAppServiceImpl.class);
@Autowired
private CardActivateAppRepository cardActivateAppRepository;
@Autowired
private CardUserRepository cardUserRepository;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private CardAfterRecordMapper cardAfterRecordMapper;
@Autowired
private CardConsumeDetailMapper cardConsumeDetailMapper;

public CardActivateAppOutputVO queryCardActivateStatus(CardActivateAppInputVO inputVO) {
this.checkParams(inputVO);
CardActivateAppOutputVO outputVO = new CardActivateAppOutputVO();
String deviceMac = inputVO.getDeviceMac();
String userId = inputVO.getUserId();
List devices = this.cardActivateAppRepository.queryCardDeviceByMac(deviceMac);
List cardActivates = this.cardActivateAppRepository.queryCardActivateByUserId(userId);
String[] arr = new String[]{"0", "1", "2"};
List cardActivatesMac = this.cardActivateAppRepository.queryCardActivateByMac(deviceMac, Arrays.asList(arr));
if (CollUtil.isEmpty(cardActivatesMac)) {
if (CollUtil.isEmpty(devices)) {
if (CollUtil.isEmpty(cardActivates)) {
this.getCardActivateOutputVOInvalid(outputVO);
return outputVO;
}
outputVO.setStatus(Integer.valueOf(2));
return outputVO;
}
outputVO = this.getCardActivateOutputVOvalid(devices, userId, deviceMac);
return outputVO;
}
......

jad 命令还提供了一些其他参数:

1
2
3
4
bash复制代码# 反编译只显示源码
jad --source-only com.Arthas
# 反编译某个类的某个方法
jad --source-only com.Arthas mysql

4.6 查看字段信息

使用 **sc -d -f ** 命令查看类的字段信息。

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
ruby复制代码[arthas@20252]$ sc -d -f com.Arthas
sc -d -f com.Arthas
class-info com.Arthas
code-source /C:/Users/Niu/Desktop/arthas/target/classes/
name com.Arthas
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name Arthas
modifier public
annotation
interfaces
super-class +-java.lang.Object
class-loader +-sun.misc.Launcher$AppClassLoader@18b4aac2
+-sun.misc.Launcher$ExtClassLoader@2ef1e4fa
classLoaderHash 18b4aac2
fields modifierfinal,private,static
type org.slf4j.Logger
name log
value Logger[com.Arthas]

modifierprivate,static
type java.util.HashSet
name hashSet
value [count1, count2]

modifierprivate,static
type java.util.concurrent.ExecutorService
name executorService
value java.util.concurrent.ThreadPoolExecutor@71c03156[Ru
nning, pool size = 1, active threads = 1, queued ta
sks = 0, completed tasks = 0]


Affect(row-cnt:1) cost in 9 ms.


# 解释
class-info com.fridge.service.impl.CardActivateAppServiceImpl
code-source /Users/admin/project/uhome/Doc_01_Uhome/mythicalanimals/targ
et/classes/
name com.fridge.service.impl.CardActivateAppServiceImpl
# 是否是接口
isInterface false
# 是否是注解
isAnnotation false
# 是否是枚举
isEnum false
# 是否是匿名类
isAnonymousClass false
# 是否是数组
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
# 类名
simple-name CardActivateAppServiceImpl
# 修饰符
modifier public
# 使用的注解
annotation org.springframework.stereotype.Service
# 实现的接口
interfaces com.fridge.service.CardActivateAppService
# 父类
super-class +-java.lang.Object
# 类加载器
class-loader +-sun.misc.Launcher$AppClassLoader@18b4aac2
+-sun.misc.Launcher$ExtClassLoader@4278a03f

4.7 查看方法信息

使用 sm 命令查看类的方法信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码[arthas@22180]$ sm com.Arthas
com.Arthas <init>()V
com.Arthas start()V
com.Arthas thread()V
com.Arthas deadThread()V
com.Arthas lambda$cpuHigh$1()V
com.Arthas cpuHigh()V
com.Arthas lambda$thread$3()V
com.Arthas addHashSetThread()V
com.Arthas cpuNormal()V
com.Arthas cpu()V
com.Arthas lambda$addHashSetThread$0()V
com.Arthas lambda$deadThread$4(Ljava/lang/Object;Ljava/lang/Object;)V
com.Arthas lambda$deadThread$5(Ljava/lang/Object;Ljava/lang/Object;)V
com.Arthas lambda$cpuNormal$2()V
Affect(row-cnt:16) cost in 6 ms.

4.8 对变量的值很是好奇

使用 ognl 命令,ognl 表达式可以轻松操作想要的信息。

代码还是上面的示例代码,我们查看变量 hashSet 中的数据:

查看静态变量 hashSet 信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ruby复制代码[arthas@19856]$ ognl '@com.Arthas@hashSet'
@HashSet[
@String[count1],
@String[count2],
@String[count29],
@String[count28],
@String[count0],
@String[count27],
@String[count5],
@String[count26],
@String[count6],
@String[count25],
@String[count3],
@String[count24],

查看静态变量 hashSet 大小。

1
2
ruby复制代码[arthas@19856]$ ognl '@com.Arthas@hashSet.size()'
@Integer[57]

甚至可以进行操作。

1
2
3
4
5
6
7
ruby复制代码[arthas@19856]$ ognl  '@com.Arthas@hashSet.add("test")'
@Boolean[true]
[arthas@19856]$
# 查看添加的字符
[arthas@19856]$ ognl '@com.Arthas@hashSet' | grep test
@String[test],
[arthas@19856]$

ognl 可以做很多事情,可以参考 ognl 表达式特殊用法( https://github.com/alibaba/arthas/issues/71 )。

4.9 程序有没有问题

4.9.1 运行较慢、耗时较长

使用 trace 命令可以跟踪统计方法耗时

这次换一个模拟代码。一个最基础的 Springboot 项目(当然,不想 Springboot 的话,你也可以直接在 UserController 里 main 方法启动)控制层 getUser 方法调用了 userService.get(uid);,这个方法中分别进行check、service、redis、mysql操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
less复制代码@RestController
@Slf4j
public class UserController {

@Autowired
private UserServiceImpl userService;

@GetMapping(value = "/user")
public HashMap<String, Object> getUser(Integer uid) throws Exception {
// 模拟用户查询
userService.get(uid);
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("uid", uid);
hashMap.put("name", "name" + uid);
return hashMap;
}
}

模拟代码 Service:

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
java复制代码@Service
@Slf4j
public class UserServiceImpl {

public void get(Integer uid) throws Exception {
check(uid);
service(uid);
redis(uid);
mysql(uid);
}

public void service(Integer uid) throws Exception {
int count = 0;
for (int i = 0; i < 10; i++) {
count += i;
}
log.info("service end {}", count);
}

public void redis(Integer uid) throws Exception {
int count = 0;
for (int i = 0; i < 10000; i++) {
count += i;
}
log.info("redis end {}", count);
}

public void mysql(Integer uid) throws Exception {
long count = 0;
for (int i = 0; i < 10000000; i++) {
count += i;
}
log.info("mysql end {}", count);
}

public boolean check(Integer uid) throws Exception {
if (uid == null || uid < 0) {
log.error("uid不正确,uid:{}", uid);
throw new Exception("uid不正确");
}
return true;
}
}

运行 Springboot 之后,使用 **trace== ** 命令开始检测耗时情况。

1
ruby复制代码[arthas@6592]$ trace com.UserController getUser

访问接口 /getUser ,可以看到耗时信息,看到 com.UserServiceImpl:get()方法耗时较高。

继续跟踪耗时高的方法,然后再次访问。

1
csharp复制代码[arthas@6592]$ trace com.UserServiceImpl get

很清楚的看到是 com.UserServiceImpl的 mysql方法耗时是最高的。

1
2
3
4
5
6
7
ini复制代码Affect(class-cnt:1 , method-cnt:1) cost in 31 ms.
`---ts=2019-10-16 14:40:10;thread_name=http-nio-8080-exec-8;id=1f;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@23a918c7
`---[6.792201ms] com.UserServiceImpl:get()
+---[0.008ms] com.UserServiceImpl:check() #17
+---[0.076ms] com.UserServiceImpl:service() #18
+---[0.1089ms] com.UserServiceImpl:redis() #19
`---[6.528899ms] com.UserServiceImpl:mysql() #20

4.9.2 统计方法耗时

使用 monitor 命令监控统计方法的执行情况。

每5秒统计一次 com.UserServiceImpl 类的 get 方法执行情况。

1
2
arduino复制代码monitor -c 5 com.UserServiceImpl get
1

4.10 想观察方法信息

下面的示例用到了文章的前两个模拟代码。

4.10.1 观察方法的入参出参信息

使用 watch 命令轻松查看输入输出参数以及异常等信息。

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
ini复制代码USAGE:
watch [-b] [-e] [-x <value>] [-f] [-h] [-n <value>] [-E] [-M <value>] [-s] class-pattern method-pattern express [condition-express]

SUMMARY:
Display the input/output parameter, return object, and thrown exception of specified method invocation
The express may be one of the following expression (evaluated dynamically):
target : the object
clazz : the object's class
method : the constructor or method
params : the parameters array of method
params[0..n] : the element of parameters array
returnObj : the returned object of method
throwExp : the throw exception of method
isReturn : the method ended by return
isThrow : the method ended by throwing exception
#cost : the execution time in ms of method invocation
Examples:
watch -b org.apache.commons.lang.StringUtils isBlank params
watch -f org.apache.commons.lang.StringUtils isBlank returnObj
watch org.apache.commons.lang.StringUtils isBlank '{params, target, returnObj}' -x 2
watch -bf *StringUtils isBlank params
watch *StringUtils isBlank params[0]
watch *StringUtils isBlank params[0] params[0].length==1
watch *StringUtils isBlank params '#cost>100'
watch -E -b org\.apache\.commons\.lang\.StringUtils isBlank params[0]

WIKI:
https://alibaba.github.io/arthas/watch

常用操作:

1
2
3
4
5
6
7
8
9
shell复制代码# 查看入参和出参
$ watch com.Arthas addHashSet '{params[0],returnObj}'
# 查看入参和出参大小
$ watch com.Arthas addHashSet '{params[0],returnObj.size}'
# 查看入参和出参中是否包含 'count10'
$ watch com.Arthas addHashSet '{params[0],returnObj.contains("count10")}'
# 查看入参和出参,出参 toString
$ watch com.Arthas addHashSet '{params[0],returnObj.toString()}'
12345678

查看入参出参。

查看返回的异常信息。

4.10.2 观察方法的调用路径

使用 stack命令查看方法的调用信息。

1
2
3
arduino复制代码# 观察 类com.UserServiceImpl的 mysql 方法调用路径
stack com.UserServiceImpl mysql
12

4.10.3 方法调用时空隧道

使用 tt 命令记录方法执行的详细情况。

tt 命令方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测 。

常用操作:

开始记录方法调用信息:tt -t com.UserServiceImpl check

可以看到记录中 INDEX=1001 的记录的 IS-EXP = true ,说明这次调用出现异常。

查看记录的方法调用信息: tt -l

查看调用记录的详细信息(-i 指定 INDEX): tt -i 1001

可以看到 INDEX=1001 的记录的异常信息。

重新发起调用,使用指定记录,使用 -p 重新调用。

1
css复制代码tt -i 1001 -p

转载Arthas线上监控及问题定位 - 未月廿三 - 博客园转载

Arthas线上监控及问题定位 - 未月廿三 - 博客园​

www.cnblogs.com

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数仓的元数据管理和上下游管理

发表于 2021-05-31

数仓元数据管理

一、元数据定义

元数据(Meta Data)狭义的来说可以理解为描述数据的数据,广义的来看是除了业务逻辑直接读写处理外的业务数据,所有用来维持整个系统运转所需的信息及数据都可以叫作元数据。

如何理解上面这句话呢?我告诉你小头儿子的信息,如果你没见过动画片你肯定是不能理解的,但是如果按照下面的文字进行描述呢?

小头儿子是一个高150cm的男孩子,重100斤,出生在2008年,现在13岁,爸爸的名字是小头爸爸,现在在上中学。

上文在描述小头儿子,那么是怎样描述的?从身高,体重,性别,家庭关系,年龄等。这些描述的信息可以理解为元数据。

同理,现在我告诉你一个数字175,看到这个你除了可以确定他具备一定的度量意义外,还是有很多疑问,无法立即,比如度量的什么,谁度量的等等问题,但是如果按照下面这些信息度量呢?

数据类型 数据内容
数据值 175
单位 cm
指标 平均身高
统计时间 2020年
区域范围 全国
人群范围 成年男性
范围 80-260
数据库类型 Mysql
数据库链接 jdbc:mysql://127.0.0.1:3306/info
表名 human_stat
字段 height_avg
创建人 李四
创建时间 2020-10-01
修改时间 2020-10-01
数据权限 公开
安全等级 安全
质量等级 极高

这样是不是就很清楚了?175 的意思是:2020 年统计的全国成年男性平均身高,该值的合理范围是 80-260cm,数据目前存在 MySQL中,访问连接是 jdbc:mysql://127.0.0.1:3306/info,由国家统计局的李四在 2020 年 10 月 1 日创建,数据目前是公开的,很安全,质量经过多重确认无误的。

上表在描述 175 这个数据,用了哪些描述项呢?单位、指标、统计时间、统计范围、合理范围、数据库、表、字段、创建人、创建时间、数据权限、质量等级等等。这些都是在描述 175 这个数据。我们把描述 175 这个数据的其他数据称之为“元数据”。

二、元数据类型

元数据可分为技术元数据、业务元数据和管理过程元数据等几类。

  1. 技术元数据
    为开发和管理数据仓库的 IT 人员使用,它描述了与数据仓库开发、管理和维护相关的数据,包括数据源信息、数据转换描述、数据仓库模型、数据清洗与更新规则、数据映射和访问权限等;如表结构、文件路径/格式。
  2. 业务元数据
    为管理层和业务分析人员服务,从业务角度描述数据,包括商务术语、数据仓库中有什么数据、数据的位置和数据的可用性等,帮助业务人员更好地理解数据仓库中哪些数据是可用的以及如何使用;如责任人、归属的业务、血缘关系。
  3. 管理过程元数据
    指描述管理领域相关的概念、关系和规则的数据,主要包括管理流程、人员组织、角色职责等信息;如表每天的行数、大小、更新时间。

针对元数据的类型上更容易理解,对上述例子中的元数据类型,进行归类划分:

业务元数据

数据类型 数据内容
数据值 175
单位 cm
指标 平均身高
统计时间 2020年
区域范围 全国
人群范围 成年男性
范围 80-260

技术元数据

数据类型 数据内容
数据库类型 Mysql
数据库链接 jdbc:mysql://127.0.0.1:3306/info
表名 human_stat
字段 height_avg

管理过程元数据

数据类型 数据内容
创建人 李四
创建时间 2020-10-01
修改时间 2020-10-01
数据权限 公开
安全等级 安全
质量等级 极高

三、元数据功能

  1. 血缘分析

“表”是元数据系统的后台逻辑核心,数据仓库是构建在 Hive 之上,而 Hive 的原始数据往往来自于生产系统,也可能会把计算结果导出到外部存储,所以我们认为 Hive 表、mysql 表、hbase 表、BI 报表都是“表”,这些“表”间关系是一个 DAG,也就是血缘关系。

血缘关系案例

有了血缘关系,优化可视化展示,可以让用户清楚看到一张表的上下游,更方便地查找表。基于血缘关系可以做很多事情,例如:

  • 结合表的更新时间,还可以找到调度 DAG 的关键路径,协助定位性能瓶颈;
  • 当表出现变更时,可以通知下游责任人,以及自动对下游任务做 SQL 的静态检查;
  • 辅助生命周期管理,找到没有被使用的表/字段;
  • 辅助维护字段的一致性,如注释、校验规则复用。
  1. 影响分析
    向下追溯元数据对象对下游的影响。影响分析可以让您轻松应对变更可能产生的影响,自动识别与其相关的依赖项和潜在的影响还可以跟踪所有对象及其依赖关系,最后我们还提供数据全生命周期的可视化显示。例如,如果您的某一信息系统中准备将“销售额”从包含税费更改为不包括税费,则SE-DWA将自动显示所有使用了“销售金额”字段,以便您可以确定有哪些工作需要完成,并且建议您在更改前完成该工作。
  2. 同步检查
    检查源表到目标表的数据结构是否发生变更。
  3. 指标一致性分析
    定期分析指标定义是否和实际情况一致。
  4. 实体关联查询
    事实表与维度表的代理键自动关联

四、元数据应用

  1. ETL自动化管理:使用元数据信息自动生成物理模型,ETL程序脚本,任务依赖关系和调度程序。
  2. 数据质量管理:使用数据质量规则元数据进行数据质量测量。数据质量根据设定的规则帮助您过滤出有问题的数据,并智能分析数据质量缺陷。
  3. 数据安全管理:的安全性会更进一步,可以限制特定的组成员只可以访问表中特定的数据。
  4. 数据标准管理:使用元数据信息生成标准的维度模型。
  5. 数据接口管理:使用元数据信息进行接口统一管理。多种数据源接入,并提供多种插件对接最流行的源系统。应该可以简单方便获取数据。
  6. 项目文档管理:使用元数据可以自动、方便的生成的健壮全面的项目文档,其以帮助您应对各种对于数据合规性要求。读取元数据模型,并生成pdf格式的描述文件。生成文档您查看每个对象的名称、设置、描述和代码。
  7. 数据语义管理:业务用户在自助服务分析中面临的挑战他们不了解数据仓库从而无法正确解释数据,使用元数据可以语义层建模,使用易于业务用户理解的描述来转换数据。

五、总结

由上可见,元数据(Meta Data),不仅记录数据仓库中模型的定义、各层级间的映射关系、监控数据仓库的数据状态及 ETL 的任务运行状态。元数据把数据仓库系统中各个松散的组件联系起来,组成了一个整体数据仓库解决方案。

构建数据仓库的主要步骤之一是 ETL。此时元数据将发挥重要的作用,它定义了源数据系统到数据仓库的映射、数据转换的规则、数据仓库的逻辑结构、数据更新的规则、数据导入历史记录以及装载周期等相关内容。数据抽取和转换的专家以及数据仓库管理员正是通过元数据高效地构建数据仓库。

用户在使用数据仓库时,通过元数据访问数据,明确数据项的含义以及定制报表。数据仓库的规模及其复杂性离不开正确的元数据管理,包括增加或移除外部数据源,改变数据清洗方法,控制出错的查询以及安排备份等。

数据仓库上下游以及上下游约定

基于数据仓库的特性和定位,数仓强依赖于上游的业务系统,下游的报表、可视化平台又强依赖于数仓。

一、数仓的上游约定

数据仓库最重要的数据源主要来源于业务系统,故而数据需要不断的从业务系统导入数仓中。由此上游业务系统的变动,将会非常直接的影响下游。所以制定严谨的上游约定是极其重要的。

常见约定形式如下:

  • 表结构变更
  • 表字段的枚举值定义变化
  • create_time和update_time,业务数据与数仓系统的数据同步,对新增和修改的触发tongue,一般依托于这两个字段
  • is_delete 和 is_valid的定义规约

二、数仓的下游约定

对数仓来说,下游系统一般为接口,报表、可视化平台等使用。所以针对数仓平台的优化改动,都需要及时沟通同步。

如果你认为作者写的不错!可以关注作者公众号 “白程序员的自习室”获取更多内容。写作不易,也可以点赞、关注、评论给一个鼓励,哈哈哈。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

springboot中优雅的使用websocket通信(分布

发表于 2021-05-31

关于什么是websocket以及为什么要使用websocket我这里就不去描述了,可以看一下知乎上的这个回答
WebSocket是什么原理

好,我们直接进入主题:在Springboot中优雅的使用websocket通信

一、理解需求

例如我们现在有一个需求:站内消息自动刷新
1、消息是谁来发送?
在我们的系统里面,每一个用户的某一些操作都会给对应的处理人发送消息。
2、谁来处理消息?
对应的处理人有可能是消息发送者也有可能不是,那么这时候就需要系统主动告诉对应的消息处理人有事情要处理了,而不是每个处理人自己去一直刷新界面。

二、实现构思设计

在上面的需求里面我们可以知道,实现的方式很简单,我们大可以在用户操作的那段代码后面加上一段发送websocket通知的代码,但是这样真的好吗,如果只有一两处还好,但是如果我们有几十个地方需要给前端推送消息呢?
有人说,可以写成一个工具方法,让业务代码去调用。没错确实可以这样,但是我觉得还是不够优雅,为什么呢?因为这样会造成我们的业务代码被入侵,破坏了方法的单一职责。
这时候我们就会想,既然是业务代码调用之后需要发送消息通知,那么AOP,没错,就是AOP。我们可以定义一个切面,然后在切面里面去对我们的业务方法进行增强就好了。
废话不多说,设计图献上:
在这里插入图片描述
关于如何在Springboot里面使用WebSocket 请移步springboot里使用websocket
在上图里面,我们先关注左侧WebSocketServer—->AbstractWebSocketServer—–>NoticeNumber这条线
这条线实现了服务器向客户端推送消息的功能
然后再看右边,我们生命了一个注解,一个切面,我们只需要在业务方法上面加上这个注解,业务方法就会被切面增强
现在我们需要把左右两边连接起来。
也就是右边如何通知左边向客户端推送消息呢?
这里我们使用redis的一个广播消息队列,可参考这篇文章用Redis实现RabbitMq消息广播
我们切面里面会调用一个消息发送的方法来通知左边的websocket进行消息推送

三、代码实现

websocketServer接口:
在这里插入图片描述
CusstomSession
在这里插入图片描述

AbstractWebsocketServer:
在这里插入图片描述

实现子类:
在这里插入图片描述
切面类的切面方法:
在这里插入图片描述

四、如何使用
1、后端调用
无需侵入业务代码,仅仅加上一个注解就可以实现消息通知
在这里插入图片描述
2、前端调用:
在这里插入图片描述

五、结语

在这个实现里面由于使用了redis实现消息广播,所以它是适用于分布式系统的,在分布式场景里面,客户端只需要注册任意一个节点即可。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

CentOS Docker基本操作

发表于 2021-05-31

一、docker下载

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
bash复制代码***推荐方式***
#更新最新的软件包
yum update
#安装依赖
yum install -y yum-utils device-mapper-persistent-data lvm2
#安装docker-yum源
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
#查看docker版本
yum list docker-ce --showduplicates | sort -r
#安装指定版本
yum install docker-ce-20.10.7

#配置daemon.json
vim /etc/docker/daemon.json

{
"registry-mirrors": [
"https://hub-mirror.c.163.com",
"https://mirror.baidubce.com"
]
}

#如果改动了daemon则需要重新加载daemon
systemctl daemon-reload

#启动docker
systemctl start docker

***其他方式***
yum -y install docker

查看docker版本

docker version

启动docker服务

docker service start

查看本地镜像

docker images

如果没有本地镜像,可以下载一个centos7的镜像来作为基础运行环境

docker pull centos

再次查看本地镜像

docker images

安装 docker-compose
yum -y install -y epel-release #安装pip需要先安装epel-release包

yum install -y python-pip #安装pip

pip install --upgrade pip #升级pip

pip install docker-compose #安装docker-compose

docker-compose -v #查看docker-compose的版本

第二种方式安装

sudo curl -L "https://github.com/docker/compose/releases/download/1.28.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

sudo chmod +x /usr/local/bin/docker-compose

sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

docker-compose --version

第三种方式安装

下载对应的版本
https://github.com/docker/compose/releases
然后把文件传到该文件夹下/usr/local/bin/
# 添加可执行权限
sudo chmod +x /usr/local/bin/docker-compose
# 将文件copy到 /usr/bin/目录下
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
# 查看版本
docker-compose --version

二、centos镜像下载

1
2
bash复制代码#下载稳定版本centos镜像
docker pull centos

三、基础操作

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
bash复制代码#删除tat为none的镜像
docker images|grep none|awk '{print $3}'|xargs docker rmi

#进入镜像centos进行相关操作
docker run -it centos /bin/bash
安装软件
yum install vim*
备注:此时需要记住此时的容器ID
退出容器
exit
保存镜像--就像git操作一样
docker commit -m "Added vim_" -a "Docker Newbee" 92896fb7e091 centos
//其中:
-m 来指定提交的说明信息,跟我们使用的版本控制工具一样;
-a 可以指定更新的用户信息;
92896fb7e091 是用来创建镜像的容器的 ID;
centos_vim_lrzsz是指定目标镜像的仓库名(centos_vim_lrzsz)
创建成功后会返回新镜像的 ID:

再次进入新容器进行相关操作
docker run -i -t -v /home/huzhihui/huzhihui:/home/huzhihui 89796a /bin/bash
//其中:
-i 表示以"交互模式"运行容器
-t 表示容器启动后会进入其命令行
-v 表示需要将本地哪个目录挂载到容器中,格式:-v <宿主机绝对目录>:<容器绝对目录>
89796a 即2中新容器的ID,也可以用"容器名:TAG"唯一即可
/bin/bash 即进入容器后的bash shell命令行对应-t

一切就绪,开始运行tomcat服务
docker run -d -p 58080:8080 -v /home/huzhihui/:/home/huzhihui/ --name test_tomcat_1 test_tomcat:1.0 /root/run.sh
//其中:
-v 略,如前所述说
-d 表示以"守护模式"执行/root/run.sh脚本,此时 Tomcat 控制台不会出现在输出终端上
-p 表示宿主机与容器的端口映射,此时将容器内部的8080端口映射为宿主机的58080端口,这样就向外界暴露了58080端口,可通过Docker网桥来访问容器内部的8080端口了
--name 表示容器名称,用一个你觉得有意义的名称命名即可
test_tomcat:1.0 即新容器名:TAG
/root/run.sh 即需要执行的脚本

当然你可能会问容器中的tomcat正常启动了吗?日志怎么看?
docker logs e25a7d9f4506

进去启动的docker容器
docker exec -it db3 /bin/sh 或者 docker exec -it d48b21a7e439 /bin/sh

给镜像设置tag

docker tag huzhihui/frps huzhihui/frps:0.28

上传镜像到docker hub

docker push huzhihui/frps:1.0

查看docker容器与宿主机器共享的文件夹命令
docker inspect sms | grep Mounts -A 20 #sms是容器的名称
查看docker容器与书主机映射的端口
docker container port sms #sms是容器名称

如果防火墙是iptables则运行
1.iptables -nL -t nat 查看nat规则是否存在

2.iptables-save 检查当前 IPTable 表是否空

3.如果IPtable 表不为空。 先备份IPtable表

cp /etc/sysconfig/iptables /etc/sysconfig/iptables_bak_1

4.iptables-save > /etc/sysconfig/iptables 将所有IPtable 规则重新保存

这时候,重启防火墙也不会影响 容器使用了

解决防火墙重启导致docker ip混乱问题
采用docker network解决该问题
#查看配置的网络列表
docker network ls
#配置自己的docker网络
docker network create --driver=bridge --subnet=192.168.0.1/16 my_network
#这样添加的网络可能不能访问宿主机,还需要自己添加桥接
#编辑iptables
-A INPUT -s 192.168.0.0/24 -p tcp -m state --state NEW -m tcp -j ACCEPT
-A INPUT -s 192.168.0.0/24 -p udp -m state --state NEW -m udp -j ACCEPT
#使用自己的网络创建运行docker容器

docker run -d -p 58002:8080 -p 59002:8500 -v /usr/local/docker/discovery/logs:/usr/local/tomcat/logs -v /usr/local/docker/discovery/webapps:/usr/local/tomcat/webapps --name discovery --network ycl_docker_network --ip 172.20.0.4 ycl/tomcat:jdk8-tomcat8.5

五、第一个Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bash复制代码FROM centos
MAINTAINER huzhihui_c@qq.com
WORKDIR /usr/local
RUN mkdir jdk
RUN mkdir tomcat

ADD jdk1.8 /usr/local/jdk/

ADD tomcat8.5 /usr/local/tomcat/

ENV JAVA_HOME=/usr/local/jdk
ENV JRE_HOME=$JAVA_HOME/jre
ENV CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH
ENV PATH=/sbin:$JAVA_HOME/bin:$PATH

EXPOSE 8080

ENTRYPOINT ["/usr/local/tomcat/bin/catalina.sh","run"]

#保存后执行
docker build -t huzhihui/tomcat .

#运行镜像
docker run -d -p 8080:8080 --name hmk_tomcat huzhihui/tomcat:latest

生产环境Dockerfile

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
bash复制代码#创建Dockerfile文件,写入如下内容
FROM centos:7
MAINTAINER huzhihui_c@qq.com

ENV LANG=en_US.UTF-8

RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

WORKDIR /usr/local
RUN mkdir jdk
RUN mkdir tomcat

ADD jdk1.8 /usr/local/jdk/
ADD tomcat8.5 /usr/local/tomcat/
ADD run.sh /root

RUN ln -sf /dev/stdout /usr/local/tomcat/logs/catalina.out

ENV JAVA_HOME=/usr/local/jdk
ENV JRE_HOME=$JAVA_HOME/jre
ENV CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH
ENV PATH=/sbin:$JAVA_HOME/bin:$PATH

EXPOSE 8080
EXPOSE 8500

ENTRYPOINT ["/root/run.sh"]

#该文件夹下面存在jdk1.8、tomcat8.5以及run.sh文件

#run.sh文件就是你想如何运行你的容器
#!/bin/sh

#如果APP文件夹不存在,则复制默认的APP文件夹过去
if [[ ! -d "/usr/local/tomcat/webapps/app/" ]] ; then
cp -rf /usr/local/tomcat/webapps_back/app/ /usr/local/tomcat/webapps/app/
fi
if [[ ! -d "/usr/local/tomcat/webapps/docs/" ]] ; then
cp -rf /usr/local/tomcat/webapps_back/docs/ /usr/local/tomcat/webapps/docs/
fi
if [[ ! -d "/usr/local/tomcat/webapps/examples/" ]] ; then
cp -rf /usr/local/tomcat/webapps_back/examples/ /usr/local/tomcat/webapps/examples/
fi
if [[ ! -d "/usr/local/tomcat/webapps/host-manager/" ]] ; then
cp -rf /usr/local/tomcat/webapps_back/host-manager/ /usr/local/tomcat/webapps/host-manager/
fi
if [[ ! -d "/usr/local/tomcat/webapps/manager/" ]] ; then
cp -rf /usr/local/tomcat/webapps_back/manager/ /usr/local/tomcat/webapps/manager/
fi

bash /usr/local/tomcat/bin/startup.sh
tail -f /usr/local/tomcat/logs/catalina.out

#然后执行下面命令即可
docker build huzhihui/tomcat:8.5 .

六、开放dockerAPI并TLS加密

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
bash复制代码#docker开启 api TLS

#我们利用脚本自动生成,这样非常便捷,脚本(auto-tls-certs.sh)如下:
#!/bin/bash
#
# -------------------------------------------------------------
# 自动创建 Docker TLS 证书
# -------------------------------------------------------------

# 以下是配置信息
# --[BEGIN]------------------------------

CODE="dp"
IP="docker服务器ip"
PASSWORD="证书密码"
COUNTRY="CN"
STATE="BEIJING"
CITY="BEIJING"
ORGANIZATION="公司"
ORGANIZATIONAL_UNIT="Dev"
COMMON_NAME="$IP"
EMAIL="邮箱"

# --[END]--

# Generate CA key
openssl genrsa -aes256 -passout "pass:$PASSWORD" -out "ca-key-$CODE.pem" 4096
# Generate CA
openssl req -new -x509 -days 365 -key "ca-key-$CODE.pem" -sha256 -out "ca-$CODE.pem" -passin "pass:$PASSWORD" -subj "/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORGANIZATION/OU=$ORGANIZATIONAL_UNIT/CN=$COMMON_NAME/emailAddress=$EMAIL"
# Generate Server key
openssl genrsa -out "server-key-$CODE.pem" 4096

# Generate Server Certs.
openssl req -subj "/CN=$COMMON_NAME" -sha256 -new -key "server-key-$CODE.pem" -out server.csr

echo "subjectAltName = IP:$IP,IP:127.0.0.1" >> extfile.cnf
echo "extendedKeyUsage = serverAuth" >> extfile.cnf

openssl x509 -req -days 365 -sha256 -in server.csr -passin "pass:$PASSWORD" -CA "ca-$CODE.pem" -CAkey "ca-key-$CODE.pem" -CAcreateserial -out "server-cert-$CODE.pem" -extfile extfile.cnf


# Generate Client Certs.
rm -f extfile.cnf

openssl genrsa -out "key-$CODE.pem" 4096
openssl req -subj '/CN=client' -new -key "key-$CODE.pem" -out client.csr
echo extendedKeyUsage = clientAuth >> extfile.cnf
openssl x509 -req -days 365 -sha256 -in client.csr -passin "pass:$PASSWORD" -CA "ca-$CODE.pem" -CAkey "ca-key-$CODE.pem" -CAcreateserial -out "cert-$CODE.pem" -extfile extfile.cnf

rm -vf client.csr server.csr

chmod -v 0400 "ca-key-$CODE.pem" "key-$CODE.pem" "server-key-$CODE.pem"
chmod -v 0444 "ca-$CODE.pem" "server-cert-$CODE.pem" "cert-$CODE.pem"

# 打包客户端证书
mkdir -p "tls-client-certs-$CODE"
cp -f "ca-$CODE.pem" "cert-$CODE.pem" "key-$CODE.pem" "tls-client-certs-$CODE/"
cd "tls-client-certs-$CODE"
tar zcf "tls-client-certs-$CODE.tar.gz" *
mv "tls-client-certs-$CODE.tar.gz" ../
cd ..
rm -rf "tls-client-certs-$CODE"

# 拷贝服务端证书
mkdir -p /etc/docker/certs.d
cp "ca-$CODE.pem" "server-cert-$CODE.pem" "server-key-$CODE.pem" /etc/docker/certs.d/

对脚本中的变量进行修改后运行,自动会创建好tls证书,服务器的证书在/etc/docker/certs.d/目录下:

客户端的证书在运行脚本的目录下,同时还自动打好了一个.tar.gz的包,很方便。

#配置dockerdaemon配置
cat /etc/docker/daemon.json

{
"hosts": ["tcp://0.0.0.0:2375","unix:///var/run/docker.sock"],
"tlsverify":true,
"tlscacert":"/root/tls/pem/ca.pem",
"tlscert":"/root/tls/pem/server-cert.pem",
"tlskey":"/root/tls/pem/server-key.pem"
}

#并且编辑vim /lib/systemd/system/docker.service
#下面这行 -H fd:// 一定要去掉,不然会和daemon.json配置冲突
ExecStart=/usr/bin/dockerd

#然后重启docker api已经有了认证了

七、portainer工具管理docker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yaml复制代码# docker-compose.yml

version: '3'
volumes:
portainer_data:

services:
portainer:
networks:
default:
ipv4_address: 172.18.0.2
image: portainer/portainer
container_name: portainer
ports:
- "9000:9000"
command: -H unix:///var/run/docker.sock
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/data
networks:
default:
external:
name: huzhihui

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

计科专业的大一新生,如何提高编程能力?

发表于 2021-05-31

大家应该都知道欧阳修的《卖油翁》的寓言小故事,油翁说的那句“无他,唯手熟尔”,不仅适用于康肃公的射箭、油翁的倒油,也适合新手编程。

我上大学的时候由于老师水平太菜的原因,编程其实学得一塌糊涂。这里多说一句,当时捧着老师推荐的《Java 编程思想》视为圣经,前前后后看了好多遍,差点被劝退。当然了,AWT 和 Swing 可真没少学,当时为了做一个类似 QQ 的聊天界面,真的是差点崩溃,用的是 MyEclipse,不像 NetBeans 能拖拽组件。

嗯,我说老师水平菜这件事,会不会被说不尊重老师?真没有哈,纯粹说一个事实。我上大学那会编程水平也菜的不行不行的。

勉强找到一份外企的实习工作后,终于下定决心要奋发图强,原因很简单,因为如果能力和公司要求不匹配的话,是会被无情辞退的。观察期两个月,和我同去的一波新人当中就被辞退了七八个。

怎么提高编程能力呢?我的做法很简单,就是到编程网站上找例子做,有些是原封不动地抄到本地,但因为环境的不同,很多都不能正常运行,为了解决错误,又不好意思为前辈,就只能问搜索引擎,当时谷歌还能正常访问,如果还找不到答案,就去问度娘。

实在是解决不了,只能放弃,继续去抄下一个例子。就这样持续了半年的时间,水平竟然得到了很大程度上的提高,想起来真的是觉得不可思议。重要的是,领导安排的任务也都能独立地完成了。

说到这,给大家推荐一个我觉得不错的 Java 在线学习网站。

风格贼舒服!还能做任务和打游戏,学习起来也更轻松自在一些。网址我贴一下:

codegym.cc/zh/quests/t…

记住一点,编程是一门手艺活,不动手是学不会的。尽量把网站上的代码都敲一遍,你就会发现自己在技术层面突飞猛进。

清华大学的孙家广院士曾说过三句话,我觉得非常妙:

  • learning by doing
  • do not give up
  • get things done

我之前一直推荐的 vhr 和 mall,都是 GitHub 上很优质的练手项目,文档特别全面,如何部署,如何上手每一个技术栈,不管是松哥还是宏哥,要么提供了视频演示,要么写了详细的文档,新手完全不用担心看不懂、跑不起来的问题。

把其中的任意一个 down 下来,用 Intellij IDEA 打开,然后运行它,加 log,试着修改一些数据和代码,看看有什么变化,用不了仨月,你就会感觉自己的编程能力在突飞猛进。

这个过程中,你既可以通过实践去检验学过的知识,也可以通过拆解优质的项目学习作者的编程经验,可谓是一举两得。

还有,像《C 程序设计语言》这本书,里面的代码都写得非常的优雅,在学习的时候不仅要看懂里面的代码,还要能把里面的代码在本机上敲出来跑起来,一遍没记住,就多敲几遍,直到自己可以自主的敲完一个例子。

有很多读者向我反馈,说敲完代码总是忘记,其实问题很简单,就是敲得少啊,就像一篇文言文,你得背会啊,不然考试的时候肯定答不出来啊,就是因为还没有形成记忆,没有刻意练习。

我一开始学 Java 的时候,也总是忘记,需要不停地查 API。为了方便,我桌面上就备了一份 Java SE 1.6 的中文帮助文档(又暴露了什么),想不起来就去查,查久了,敲久了,经常用的 API 算是彻底记到脑子里了。

如果真想学习 Java 基础的话,可以先看看我整理的这份 GitHub 上星标 115k+ 的 Java 教程,里面涵盖了 Java 所有的知识点,包括 Java 语法、Java 集合框架、Java IO、Java 并发编程和 Java 虚拟机,内容不多,只讲重点。

GitHub 星标 115k+的 Java 教程,超级硬核!

其实不只是学编程语言,像 Linux 命令啊,也得经常敲才能形成肌肉记忆。像常用的 cd mkdir cp rm 等等,如果不敲上个几十遍,后面该跟什么参数就会很生疏。记得我最经常跑的一条命令是 ps -ef | grep java,用来查看正在运行的 Java 进程,好 kill 它。

编程是一门手艺活,不动手是学不会的。


有读者说,掘金上有个博主在骂我,不仅标题抄袭我,内容也抄袭我,还说自己是反讽。然后一直在评论区带节奏,我觉得很没有必要,大家都是博主,各说各的,就可以了。

这里简单回应一下。

博主在自己的文章里留言这样说:

我本来不打算说的,但这博主一直在评论区带节奏,我就留言了一句。

不知道动了谁的奶酪,可怕。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数据结构基础知识

发表于 2021-05-30

概述

数据结构是为实现对计算机数据有效使用的各种数据组织形式,服务于各类计算机操作。不同的数据结构具有各自对应的适用场景,旨在降低各种算法计算的时间与空间复杂度,达到最佳的任务执行效率。

分类

线性数据结构(物理结构)

数组(Array)、链表(Linked List)、栈(Stack)、队列(Queue)

非线性数据结构(逻辑结构)

树(Tree)、堆(Heap)、散列表(Hashing)、图(Graph)

数组

数组是将相同类型的元素存储于连续内存空间的数据结构,其长度不可变。

如下图所示,构建此数组需要在初始化时给定长度,并对数组每个索引元素赋值,代码如下:

1
2
3
4
5
6
7
8
java复制代码// 初始化一个长度为 5 的数组 array
int[] array = new int[5];
// 元素赋值
array[0] = 2;
array[1] = 3;
array[2] = 1;
array[3] = 0;
array[4] = 2;

或者可以使用直接赋值的初始化方式,代码如下:

1
java复制代码int[] array = {2, 3, 1, 0, 2};

可变数组

可变数组是经常使用的数据结构,其基于数组和扩容机制实现,相比普通数组更加灵活。常用操作有:访问元素、添加元素、删除元素。

1
2
3
4
5
6
7
8
9
java复制代码// 初始化可变数组
List<Integer> array = new ArrayList<>();

// 向尾部添加元素
array.add(2);
array.add(3);
array.add(1);
array.add(0);
array.add(2);

链表

链表以节点为单位,每个元素都是一个独立对象,在内存空间的存储是非连续的。链表的节点对象具有两个成员变量:「值 val」,「后继节点引用 next」。

1
2
3
4
5
java复制代码class ListNode {
int val; // 节点值
ListNode next; // 后继节点引用
ListNode(int x) { val = x; }
}

如下图所示,建立此链表需要实例化每个节点,并构建各节点的引用指向。

1
2
3
4
5
6
7
8
java复制代码// 实例化节点
ListNode n1 = new ListNode(4); // 节点 head
ListNode n2 = new ListNode(5);
ListNode n3 = new ListNode(1);

// 构建引用指向
n1.next = n2;
n2.next = n3;

栈

栈是一种具有 「先入后出」 特点的抽象数据结构,可使用数组或链表实现。

1
java复制代码Stack<Integer> stack = new Stack<>();

如下图所示,通过常用操作 「入栈 push()」,「出栈 pop()」,展示了栈的先入后出特性。

1
2
3
4
java复制代码stack.push(1); // 元素 1 入栈
stack.push(2); // 元素 2 入栈
stack.pop(); // 出栈 -> 元素 2
stack.pop(); // 出栈 -> 元素 1

注意:通常情况下,不推荐使用 Java 的 Vector 以及其子类 Stack ,而一般将 LinkedList 作为栈来使用。

1
2
3
4
5
6
java复制代码LinkedList<Integer> stack = new LinkedList<>();

stack.addLast(1); // 元素 1 入栈
stack.addLast(2); // 元素 2 入栈
stack.removeLast(); // 出栈 -> 元素 2
stack.removeLast(); // 出栈 -> 元素 1

队列

队列是一种具有 「先入先出」 特点的抽象数据结构,可使用链表实现。

1
java复制代码Queue<Integer> queue = new LinkedList<>();

如下图所示,通过常用操作 「入队 push()」,「出队 pop()」,展示了队列的先入先出特性。

1
2
3
4
java复制代码queue.offer(1); // 元素 1 入队
queue.offer(2); // 元素 2 入队
queue.poll(); // 出队 -> 元素 1
queue.poll(); // 出队 -> 元素 2

树

树是一种非线性数据结构,根据子节点数量可分为 「二叉树」 和 「多叉树」,最顶层的节点称为 「根节点 root」。以二叉树为例,每个节点包含三个成员变量:「值 val」、「左子节点 left」、「右子节点 right」 。

1
2
3
4
5
6
java复制代码class TreeNode {
int val; // 节点值
TreeNode left; // 左子节点
TreeNode right; // 右子节点
TreeNode(int x) { val = x; }
}

如下图所示,建立此二叉树需要实例化每个节点,并构建各节点的引用指向。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 初始化节点
TreeNode n1 = new TreeNode(3); // 根节点 root
TreeNode n2 = new TreeNode(4);
TreeNode n3 = new TreeNode(5);
TreeNode n4 = new TreeNode(1);
TreeNode n5 = new TreeNode(2);

// 构建引用指向
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;

图

图是一种非线性数据结构,由 「节点(顶点)vertex」 和 「边 edge」 组成,每条边连接一对顶点。根据边的方向有无,图可分为 「有向图」 和 「无向图」 。

如下图所示,此无向图的 顶点 和 边 集合分别为:

  • 顶点集合: vertices = {1, 2, 3, 4, 5}
  • 边集合: edges = {(1, 2), (1, 3), (1, 4), (1, 5), (2, 4), (3, 5), (4, 5)}

表示图的方法通常有两种:

1、邻接矩阵: 使用数组 verticesvertices 存储顶点,邻接矩阵 edgesedges 存储边;edges[i][j] 代表节点 i + 1和节点 j + 1之间是否有边。

1
2
3
4
5
6
java复制代码int[] vertices = {1, 2, 3, 4, 5};
int[][] edges = {{0, 1, 1, 1, 1},
{1, 0, 0, 1, 0},
{1, 0, 0, 0, 1},
{1, 1, 0, 0, 1},
{1, 0, 1, 1, 0}};

2、邻接表: 使用数组vertices存储顶点,邻接表edges存储边。 edges为一个二维容器,第一维 i 代表顶点索引,第二维 edges[i] 存储此顶点对应的边集和;例如 edges[0] = [1, 2, 3, 4]代表 vertices[0]的边集合为 [1, 2, 3, 4]。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码int[] vertices = {1, 2, 3, 4, 5};
List<List<Integer>> edges = new ArrayList<>();

List<Integer> edge_1 = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
List<Integer> edge_2 = new ArrayList<>(Arrays.asList(0, 3));
List<Integer> edge_3 = new ArrayList<>(Arrays.asList(0, 4));
List<Integer> edge_4 = new ArrayList<>(Arrays.asList(0, 1, 4));
List<Integer> edge_5 = new ArrayList<>(Arrays.asList(0, 2, 3));
edges.add(edge_1);
edges.add(edge_2);
edges.add(edge_3);
edges.add(edge_4);
edges.add(edge_5);

邻接矩阵 VS 邻接表 :

邻接矩阵的大小只与节点数量有关,即N^2,其中N为节点数量。因此,当边数量明显少于节点数量时,使用邻接矩阵存储图会造成较大的内存浪费。因此,邻接表 适合存储稀疏图(顶点较多、边较少);邻接矩阵适合存储稠密图(顶点较少、边较多)。

散列表

散列表是一种非线性数据结构,通过利用 Hash 函数将指定的 「键 key」 映射至对应的 「值 value」 ,以实现高效的元素查找。

设想一个简单场景:小力、小特、小扣的学号分别为 10001, 10002, 10003 。现需求从「姓名」查找「学号」。

则可通过建立姓名为 key ,学号为 value 的散列表实现此需求,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 初始化散列表
Map<String, Integer> dic = new HashMap<>();

// 添加 key -> value 键值对
dic.put("小力", 10001);
dic.put("小特", 10002);
dic.put("小扣", 10003);

// 从姓名查找学号
dic.get("小力"); // -> 10001
dic.get("小特"); // -> 10002
dic.get("小扣"); // -> 10003

自行设计 Hash 函数:

假设需求:从「学号」查找「姓名」。

将三人的姓名存储至以下数组中,则各姓名在数组中的索引分别为 0, 1, 2。

1
java复制代码String[] names = { "小力", "小特", "小扣" };

此时,我们构造一个简单的Hash函数(%为取余符号),公式和封装函数如下所示:

hash(key) = (key - 1) % 10000

1
2
3
4
java复制代码int hash(int id) {
int index = (id - 1) % 10000;
return index;
}

则我们构建了以学号为 key 、姓名对应的数组索引为 value 的散列表。利用此 Hash 函数,则可在 O(1) 时间复杂度下通过学号查找到对应姓名,即:

1
2
3
java复制代码names[hash(10001)] // 小力
names[hash(10002)] // 小特
names[hash(10003)] // 小扣

以上设计只适用于此示例,实际的 Hash 函数需保证低碰撞率、
高鲁棒性(健壮性)等,以适用于各类数据和场景。

堆

堆是一种基于 「完全二叉树」 的数据结构,可使用数组实现。以堆为原理的排序算法称为 「堆排序」 ,基于堆实现的数据结构为 「优先队列」 。堆分为 「大顶堆」 和 「小顶堆」 ,大(小)顶堆:任意节点的值不大于(小于)其父节点的值。

完全二叉树定义: 设二叉树深度为 k ,若二叉树除第 k 层外的其它各层(第 1 至 k−1 层)的节点达到最大个数,且处于第 k 层的节点都连续集中在最左边,则称此二叉树为完全二叉树。

如下图所示,为包含 1, 4, 2, 6, 8 元素的小顶堆。将堆(完全二叉树)中的结点按层编号,即可映射到右边的数组存储形式。

通过使用「优先队列」的「压入 push()」和「弹出pop()」操作,即可完成堆排序,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// 初始化小顶堆
Queue<Integer> heap = new PriorityQueue<>();

// 元素入堆
heap.add(1);
heap.add(4);
heap.add(2);
heap.add(6);
heap.add(8);

// 元素出堆(从小到大)
heap.poll(); // -> 1
heap.poll(); // -> 2
heap.poll(); // -> 4
heap.poll(); // -> 6
heap.poll(); // -> 8

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

dubbo对SPI扩展实现|周末学习

发表于 2021-05-30

本文已参与周末学习计划,点击链接查看详情:juejin.cn/post/696572…

关于SPI

可以看之前写的博文,点击链接查看详情:juejin.cn/post/696771…

JDK中的SPI缺点

  1. jdk中spi是一次性把扩展点的实现全部实例化,如果扩展点的实现很多加载很耗时间
  2. 异常无法准确捕捉提示,当扩展点的的某个实现依赖的第三方库不存在,会导致类加载失败,报的错误是找不到扩展点,而不是扩展点加载失败,以及真正的原因

Dubbo对SPI的改进

  1. 增加了扩展点的默认实现
  2. 增加了AOP的实现
  3. 增加了缓存机制,提高了性能
  4. 配置文件内容改成为key=value形式,这样配置是为了改进上面所说的SPI的第二点缺点,为了将异常信息和配置对应起来

Dubbo中的SPI实现

  1. 在需要扩展点接口使用@SPI注解标识,以前使用的@Extension注解,不过后来因为含义广泛废弃,改用SPI
  2. dubbo使用ExtensionLoader.getExtensionLoader(Class type)获取扩展点实例,下面是具体实现
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
typescript复制代码public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {

//如果扩展点类型为空,抛出异常
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
}
//扩展点不是接口,抛出异常
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
//扩展点没有使用spi注解
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}

//获取扩展点,EXTENSION_LOADERS 是一个map,key是扩展点接口类型,value是一个ExtensionLoader对象
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}


private ExtensionLoader(Class<?> type) {
this.type = type;

//扩展点类型如果是ExtensionLoader,返回null,否则返回扩展装饰类
objectFactory = (type == ExtensionFactory.class ? null
: ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
  1. 返回指定名字的扩展点对象
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
scss复制代码public T getExtension(String name) {
if (name == null || name.length() == 0) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
return getDefaultExtension();
}

//从缓存中获取扩展点对象
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
  1. cachedInstances 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object>>();

public class Holder<T> {

/**
* value线程可见性
*/
private volatile T value;

public void set(T value) {
this.value = value;
}

public T get() {
return value;
}

}

Dubbo中SPI具体使用

  1. 以protocol为例,缺省使用dubbo协议,dubbo支持默认spi扩展点
1
2
3
4
5
kotlin复制代码@SPI("dubbo")
public interface Protocol {

//.....省略代码
}
  1. SPI对应的配置文件
1
2
ini复制代码//com.alibaba.dubbo.rpc.Protocol文件里面的配置
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
  1. DubboProtocol
1
2
3
4
5
6
7
8
9
csharp复制代码/*
*jdk内部的SPI机制需要通过循环判断才能获取到扩展点实例,而dubbo只需要通过通过key就可以获取扩展点实例
*/
public static DubboProtocol getDubboProtocol() {
if (INSTANCE == null) {
ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(DubboProtocol.NAME); // load
}
return INSTANCE;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JavaFX 系列教程【2】-第一个 JavaFX 程序及窗

发表于 2021-05-30

先让东西显示出来

我们首先创建一个窗体程序,程序必须继承自 javafx.application.Application 类,并实现相关接口:

1
2
3
4
5
6
7
8
9
10
11
java复制代码package com.codelideliwan;

import javafx.application.Application;
import javafx.stage.Stage;

public class FirstApp extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.show(); // 让窗体显示出来
}
}

然后,我们在 main 方法中启动:

1
2
3
4
5
6
7
java复制代码package com.codelideliwan;

public class Main {
public static void main(String[] args) {
FirstApp.launch(FirstApp.class, args);
}
}

运行 main 方法即可看见弹出的窗口了。只不过这个窗口是一个空白窗口,里面什么也没有。

需要注意的是,Stage 即是窗口类,表示一个窗口,我们可以使用方法内提供的窗口,也可以自己 new 一个窗口。

概念介绍

这里介绍几个概念:

  • Stage:窗口,表示显示出来的窗口,Stage 在调用 show 方法的时候才会显示出来。
  • Scene:场景,如果你想在窗口上显示任何内容,这个内容都必须放在 Scene 上面。
  • Node:节点,所有依附于 Scene 显示出来的内容,都是节点,所有的控件、布局等也都是节点。
  • Controls:控件,控件是用来进行操作的节点,比如按钮、复选框等都属于空间。
  • Layouts:布局,布局是用来约束控件等显示的方式、位置等的节点,比如垂直布局、水平布局等。
  • 其他:诸如图表、图形等,都是一种节点。

下面我们就需要基于以上的介绍给窗口加点料了。

基本组件的使用

我们需要给窗口添加一个按钮,并添加一行文字,文字在按钮的下方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码package com.codelideliwan;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class FirstApp extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("Hello Stage"); // 设置窗口显示的名称

VBox vBox = new VBox(); // 添加一个垂直布局
vBox.getChildren().add(new Button("I am a button")); // 添加一个按钮
vBox.getChildren().add(new Label("I am a label")); // 添加一行文字

Scene scene = new Scene(vBox); // 创建一个 scene,并显示垂直布局
primaryStage.setScene(scene); // 让窗体显示对应的 scene
primaryStage.show(); // 让窗体显示出来
}
}

再次运行,即可发现显示出来了一个按钮和一个文字。

现在发现,点击按钮后没有任何反应,我想点击按钮的时候,下面给我计数点击了多少次,该怎么办呢?

为按钮添加点击事件

可以给按钮添加相应的点击事件,比如点击了以后下面显示点击了多少次:

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
java复制代码package com.codelideliwan;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.util.concurrent.atomic.AtomicInteger;

public class FirstApp extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("Hello Stage"); // 设置窗口显示的名称

VBox vBox = new VBox(); // 添加一个垂直布局
Button button = new Button("I am a button");
Label label = new Label("I am a label");
vBox.getChildren().addAll(button, label);

AtomicInteger count = new AtomicInteger(0); // 这里使用一个 AtomicInteger 来计数

// 给按钮添加点击事件
button.setOnMouseClicked(event -> {
label.setText("you clicked " + count.incrementAndGet() + " times.");
});

Scene scene = new Scene(vBox); // 创建一个 scene,并显示垂直布局
primaryStage.setScene(scene); // 让窗体显示对应的 scene
primaryStage.show(); // 让窗体显示出来
}
}

现在,点击按钮就能看到对应的文字变化啦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试必备:4种经典限流算法讲解

发表于 2021-05-30

前言

最近我们系统引入了Guava的RateLimiter限流组件,它是基于令牌桶算法实现的,而令牌桶是非常经典的限流算法。本文将跟大家一起学习几种经典的限流算法。

  • 公众号:捡田螺的小男孩
  • github地址

限流是什么?

维基百科的概念如下:

1
2
3
vbnet复制代码In computer networks, rate limiting is used to control the rate of requests sent or
received by a network interface controller. It can be used to prevent DoS attacks
and limit web scraping

简单翻译一下:在计算机网络中,限流就是控制网络接口发送或接收请求的速率,它可防止DoS攻击和限制Web爬虫。

限流,也称流量控制。是指系统在面临高并发,或者大流量请求的情况下,限制新的请求对系统的访问,从而保证系统的稳定性。限流会导致部分用户请求处理不及时或者被拒,这就影响了用户体验。所以一般需要在系统稳定和用户体验之间平衡一下。举个生活的例子:

一些热门的旅游景区,一般会对每日的旅游参观人数有限制的。每天只会卖出固定数目的门票,比如5000张。假设在五一、国庆假期,你去晚了,可能当天的票就已经卖完了,就无法进去游玩了。即使你进去了,排队也能排到你怀疑人生。

常见的限流算法

固定窗口限流算法

首先维护一个计数器,将单位时间段当做一个窗口,计数器记录这个窗口接收请求的次数。

  • 当次数少于限流阀值,就允许访问,并且计数器+1
  • 当次数大于限流阀值,就拒绝访问。
  • 当前的时间窗口过去之后,计数器清零。

假设单位时间是1秒,限流阀值为3。在单位时间1秒内,每来一个请求,计数器就加1,如果计数器累加的次数超过限流阀值3,后续的请求全部拒绝。等到1s结束后,计数器清0,重新开始计数。如下图:

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arduino复制代码    /**
* 固定窗口时间算法
* @return
*/
boolean fixedWindowsTryAcquire() {
long currentTime = System.currentTimeMillis(); //获取系统当前时间
if (currentTime - lastRequestTime > windowUnit) { //检查是否在时间窗口内
counter = 0; // 计数器清0
lastRequestTime = currentTime; //开启新的时间窗口
}
if (counter < threshold) { // 小于阀值
counter++; //计数器加1
return true;
}

return false;
}

但是,这种算法有一个很明显的临界问题:假设限流阀值为5个请求,单位时间窗口是1s,如果我们在单位时间内的前0.8-1s和1-1.2s,分别并发5个请求。虽然都没有超过阀值,但是如果算0.8-1.2s,则并发数高达10,已经超过单位时间1s不超过5阀值的定义啦。

滑动窗口限流算法

滑动窗口限流解决固定窗口临界值的问题。它将单位时间周期分为n个小周期,分别记录每个小周期内接口的访问次数,并且根据时间滑动删除过期的小周期。

一张图解释滑动窗口算法,如下:

假设单位时间还是1s,滑动窗口算法把它划分为5个小周期,也就是滑动窗口(单位时间)被划分为5个小格子。每格表示0.2s。每过0.2s,时间窗口就会往右滑动一格。然后呢,每个小周期,都有自己独立的计数器,如果请求是0.83s到达的,0.8~1.0s对应的计数器就会加1。

我们来看下滑动窗口是如何解决临界问题的?

假设我们1s内的限流阀值还是5个请求,0.8~1.0s内(比如0.9s的时候)来了5个请求,落在黄色格子里。时间过了1.0s这个点之后,又来5个请求,落在紫色格子里。如果是固定窗口算法,是不会被限流的,但是滑动窗口的话,每过一个小周期,它会右移一个小格。过了1.0s这个点后,会右移一小格,当前的单位时间段是0.2~1.2s,这个区域的请求已经超过限定的5了,已触发限流啦,实际上,紫色格子的请求都被拒绝啦。

TIPS: 当滑动窗口的格子周期划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

滑动窗口算法伪代码实现如下:

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
csharp复制代码 /**
* 单位时间划分的小周期(单位时间是1分钟,10s一个小格子窗口,一共6个格子)
*/
private int SUB_CYCLE = 10;

/**
* 每分钟限流请求数
*/
private int thresholdPerMin = 100;

/**
* 计数器, k-为当前窗口的开始时间值秒,value为当前窗口的计数
*/
private final TreeMap<Long, Integer> counters = new TreeMap<>();

/**
* 滑动窗口时间算法实现
*/
boolean slidingWindowsTryAcquire() {
long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口
int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数

//超过阀值限流
if (currentWindowNum >= thresholdPerMin) {
return false;
}

//计数器+1
counters.get(currentWindowTime)++;
return true;
}

/**
* 统计当前窗口的请求数
*/
private int countCurrentWindow(long currentWindowTime) {
//计算窗口开始位置
long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
int count = 0;

//遍历存储的计数器
Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, Integer> entry = iterator.next();
// 删除无效过期的子窗口计数器
if (entry.getKey() < startTime) {
iterator.remove();
} else {
//累加当前窗口的所有计数器之和
count =count + entry.getValue();
}
}
return count;
}

滑动窗口算法虽然解决了固定窗口的临界问题,但是一旦到达限流后,请求都会直接暴力被拒绝。酱紫我们会损失一部分请求,这其实对于产品来说,并不太友好。

漏桶算法

漏桶算法面对限流,就更加的柔性,不存在直接的粗暴拒绝。

它的原理很简单,可以认为就是注水漏水的过程。往漏桶中以任意速率流入水,以固定的速率流出水。当水超过桶的容量时,会被溢出,也就是被丢弃。因为桶容量是不变的,保证了整体的速率。

  • 流入的水滴,可以看作是访问系统的请求,这个流入速率是不确定的。
  • 桶的容量一般表示系统所能处理的请求数。
  • 如果桶的容量满了,就达到限流的阀值,就会丢弃水滴(拒绝请求)
  • 流出的水滴,是恒定过滤的,对应服务按照固定的速率处理请求。

漏桶算法伪代码实现如下:

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
arduino复制代码
/**
* 每秒处理数(出水率)
*/
private long rate;

/**
* 当前剩余水量
*/
private long currentWater;

/**
* 最后刷新时间
*/
private long refreshTime;

/**
* 桶容量
*/
private long capacity;

/**
* 漏桶算法
* @return
*/
boolean leakybucketLimitTryAcquire() {
long currentTime = System.currentTimeMillis(); //获取系统当前时间
long outWater = (currentTime - refreshTime) / 1000 * rate; //流出的水量 =(当前时间-上次刷新时间)* 出水率
long currentWater = Math.max(0, currentWater - outWater); // 当前水量 = 之前的桶内水量-流出的水量
refreshTime = currentTime; // 刷新时间

// 当前剩余水量还是小于桶的容量,则请求放行
if (currentWater < capacity) {
currentWater++;
return true;
}

// 当前剩余水量大于等于桶的容量,限流
return false;
}

在正常流量的时候,系统按照固定的速率处理请求,是我们想要的。但是面对突发流量的时候,漏桶算法还是循规蹈矩地处理请求,这就不是我们想看到的啦。流量变突发时,我们肯定希望系统尽量快点处理请求,提升用户体验嘛。

令牌桶算法

面对突发流量的时候,我们可以使用令牌桶算法限流。

令牌桶算法原理:

  • 有一个令牌管理员,根据限流大小,定速往令牌桶里放令牌。
  • 如果令牌数量满了,超过令牌桶容量的限制,那就丢弃。
  • 系统在接受到一个用户请求时,都会先去令牌桶要一个令牌。如果拿到令牌,那么就处理这个请求的业务逻辑;
  • 如果拿不到令牌,就直接拒绝这个请求。

漏桶算法伪代码实现如下:

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
arduino复制代码    /**
* 每秒处理数(放入令牌数量)
*/
private long putTokenRate;

/**
* 最后刷新时间
*/
private long refreshTime;

/**
* 令牌桶容量
*/
private long capacity;

/**
* 当前桶内令牌数
*/
private long currentToken = 0L;

/**
* 漏桶算法
* @return
*/
boolean tokenBucketTryAcquire() {

long currentTime = System.currentTimeMillis(); //获取系统当前时间
long generateToken = (currentTime - refreshTime) / 1000 * putTokenRate; //生成的令牌 =(当前时间-上次刷新时间)* 放入令牌的速率
currentToken = Math.min(capacity, generateToken + currentToken); // 当前令牌数量 = 之前的桶内令牌数量+放入的令牌数量
refreshTime = currentTime; // 刷新时间

//桶里面还有令牌,请求正常处理
if (currentToken > 0) {
currentToken--; //令牌数量-1
return true;
}

return false;
}

如果令牌发放的策略正确,这个系统即不会被拖垮,也能提高机器的利用率。Guava的RateLimiter限流组件,就是基于令牌桶算法实现的。

参考与感谢

  • 面试官:来,年轻人!请手撸5种常见限流算法!
  • 阿里云二面:你对限流了解多少?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…657658659…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%