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

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


  • 首页

  • 归档

  • 搜索

☆打卡算法☆LeetCode 55、跳跃游戏 算法解析

发表于 2021-11-23

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

推荐阅读

  • CSDN主页
  • GitHub开源地址
  • Unity3D插件分享
  • 简书地址
  • 我的个人博客
  • QQ群:1040082875

大家好,我是小魔龙,Unity3D软件工程师,VR、AR,虚拟仿真方向,不定时更新软件开发技巧,生活感悟,觉得有用记得一键三连哦。

一、题目

1、算法题目

“给定一个非负整数数组,数组中每个元素代表可以跳跃的长度,判断能否达到最后一个下标。”

题目链接:

来源:力扣(LeetCode)

链接:55. 跳跃游戏 - 力扣(LeetCode) (leetcode-cn.com)

2、题目描述

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

1
2
3
4
ini复制代码示例 1:
输入: nums = [2,3,1,1,4]
输出: true
解释: 可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
1
2
3
4
ini复制代码示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

二、解题

1、思路分析

别想了,看到求最优解就用贪心算法。。。

对于任意一个位置x,它能跳跃到的位置为y,它能跳跃的最大长度为x+nums[x],这个值大于y,也就是x+nums[x]≥y.

那么这么一来,就可以依次遍历数组中的每个位置,并且记录最远长度,如果它在最远长度范围内,就可以通过跳跃到达该位置。

如果它等于数组中的最后一个位置,那就说明最后一个位置可以到达,可以返回trun,反之,范围flase。

2、代码实现

代码参考:

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码public class Solution {
public bool CanJump(int[] nums) {
if (nums == null || nums.Length <= 0) return false;
int max = 0;
for (int i = 0; i < nums.Length; ++i){
if (i > max) return false;
max = Math.Max(max, i + nums[i]);
}
return true;
}
}

image.png

3、时间复杂度

时间复杂度 : O(n)

其中n是数组的长度,只需要遍历一遍数组即可求得答案。

空间复杂度: O(1)

只需要常数级别的空间存放变量。

三、总结

初始化最远位置为 0,然后遍历数组,如果当前位置能到达,并且当前位置+跳数>最远位置,就更新最远位置。

最后比较最远位置和数组长度。

本文转载自: 掘金

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

知乎利用 JuiceFS 给 Flink 容器启动加速实践

发表于 2021-11-23

本文作者胡梦宇,知乎大数据架构开发工程师,主要负责知乎内部大数据组件的二次开发和数据平台建设。

背景

Flink 因为其可靠性和易用性,已经成为当前最流行的流处理框架之一,在流计算领域占据了主导地位。早在 18 年知乎就引入了 Flink,发展到现在,Flink 已经成为知乎内部最重要的组件之一,积累了 4000 多个 Flink 实时任务,每天处理 PB 级的数据。

Flink 的部署方式有多种,根据资源调度器来分类,大致可分为 standalone、Flink on YARN、Flink on Kubernetes 等。目前知乎内部使用的部署方式是 Flink 官方提供的 native Kubernetes。谈到 Kubernetes,就不得不说容器镜像的问题,因为 Flink 任务的依赖多种多样,如何给 Flink 打镜像也是一个比较头疼的问题。

Flink 镜像及依赖处理

Flink 的任务大致可分为两类,第一类是 Flink SQL 任务,Flink SQL 任务的依赖大致有以下几种:

  1. 官方的 connector JAR 包,如 flink-hive-connector、flink-jdbc-connector、flink-kafka-connector 等;
  2. 非官方或者是内部实现的 connector JAR 包;
  3. 用户的 UDF JAR 包,一些复杂的计算逻辑,用户可能会自己实现 UDF。

第二类 Flink 任务是 Flink 的 jar 包任务,除了以上三种依赖,还需要依赖用户自己写的 Flink jar 程序包。

显然,对于每一个 Flink 任务,它的依赖不尽相同,我们也不可能为每一个 Flink 任务单独打一个镜像,我们目前的处理如下:

  1. 将依赖进行分类,分为稳定依赖和非稳定依赖;
  2. 稳定依赖包括组件(如 Flink、JDK 等)以及官方的 connector 包,这类依赖十分稳定,只会在 Flink 版本升级和 bug 修复这两种情况下进行改动,因此我们会在构建镜像时,将这类依赖打入镜像;
  3. 非稳定依赖包括第三方的 connector 和用户自己的 JAR 包。第三方的 connector 因为不是 Flink 官方维护,所以出问题需要修复的概率相对更大;用户自己的 JAR 包对于每个任务来说都不相同,而且用户会经常改动重新提交。对于这类不稳定的依赖,我们会动态注入,注入的方式是将依赖存入分布式文件系统,在容器启动的时候,利用 pre command 下载进容器里。

经过以上处理,Flink 镜像具备了一定的动态加载依赖的能力,Flink Job 的启动流程大致如下:

image.png

文件系统选取

HDFS 存放依赖的痛点

存放 Flink 依赖的文件系统在之前我们一直都是选用的 HDFS, 但是在使用过程中我们遇到了以下痛点:

  1. NameNode 在任务高峰期压力过大,容器在下载依赖时向 NameNode 请求文件元数据会存在卡顿的情况,有些小的批任务,任务本身可能只需要运行十几秒,但是因为 NameNode 压力过大,导致下载依赖可能需要几分钟;
  2. 目前 Flink 集群我们是多数据中心部署,但是 HDFS 只有一个离线机房大集群,这样会存在跨数据中心拉文件的情况,消耗专线带宽;
  3. 有一些特殊的 Flink 任务完全不依赖 HDFS,换句话说它既不使用 checkpoint 也不读写 HDFS,但是因为 Flink 容器的依赖存放在 HDFS 上,导致这类任务依然离不开 HDFS。

使用对象存储的痛点

后面我们将 HDFS 换成了对象存储,解决了 HDFS 的一些痛点,但是很快我们发现了新的问题 — 对象存储单线程下载的速度慢。对象存储下载加速可选的方案一般有以下几种:

  1. 使用多线程下载进行分段下载,但是容器的 pre command 其实只适合执行一些比较简单的 shell 命令,如果采用分段下载,就必须对这一块进行比较大的改造,这是一个比较大的痛点;
  2. 给对象存储加代理层做缓存,加速的事情由代理来做,客户端依然可以单线程读取。这种办法的缺点是需要额外维护一个对象存储的代理组件,组件的稳定性也需要有保障。

尝试 JuiceFS

比较凑巧的是公司内部正在做 JuiceFS 的 POC, 有现成的对象存储代理层可用,我们对其进行了一系列测试,发现 JuiceFS 完全满足我们这个场景的需求,让我们比较惊喜的地方有以下几点:

  1. JuiceFS 自带 S3 gateway 完美兼容 S3 对象存储协议,能够让我们很快上线,无需任何改动,并且 S3 gateway 本身无状态,扩缩容非常方便;
  2. JuiceFS 自带缓存加速功能,经过测试,用 JuiceFS 代理对象存储后,单线程读取文件的速度是原来的 4 倍;
  3. JuiceFS 提供本地文件系统挂载的方式,后面可以尝试依赖直接挂载进容器目录;
  4. JuiceFS 可选用元数据与存储分离部署的方式,存储我们选用原来的对象存储,云厂商保证 11 个 9 的可用性;元数据我们选用分布式 KV 系统—TiKV,选用 TiKV 的原因是我们在线架构组的同事对 TiKV 有着丰富的开发和运维经验,SLA 能够得到极大的保障。这样 JuiceFS 的可用性和扩展性是非常强的。

JuiceFS 上线

JuiceFS 的上线过程分为以下阶段:

  1. 数据迁移,我们需要将原先存储在 HDFS 和对象存储上的数据同步到 JuiceFS 上,因为 JuiceFS 提供了数据同步的工具,并且 Flink 的依赖也不是特别大,所以这部分工作我们很快就完成了;
  2. 修改 Flink 镜像拉取依赖的地址,因为 JuiceFS 兼容对象存储协议,我们只需要在平台侧修改原来的对象存储的 endpoint 为 JuiceFS S3 gateway 的地址即可。

JuiceFS 上线后,我们 Flink 任务启动的流程图大致如下:

image.png

相比于使用 HDFS 的方式,我们能得到一个可预期的容器启动时间,容器下载依赖的速度不会受业务高峰期的影响;相比于原生的对象存储,容器下载依赖的速度提高约 4 倍。

展望

从开始调研 JuiceFS 到 JuiceFS 上线花费时间不到半个月,主要是因为 JuiceFS 的文档十分完备,让我们少走了很多弯路,其次是 JuiceFS 社区的伙伴也有问必答,因此我们的上线过程十分顺利。

初步尝试 JuiceFS 给我们带来的收益还是比较明显的,后续我们会考虑将 JuiceFS 应用在数据湖场景和算法模型加载的场景,让我们数据的使用更加灵活和高效。

推荐阅读
JuiceFS CSI Driver 的最佳实践

项目地址: Github (github.com/juicedata/j…)如有帮助的话欢迎关注我们哟! (0ᴗ0✿)

本文转载自: 掘金

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

PTA数据结构(C++版)——7-1 队列的实现及基本操作(

发表于 2021-11-23

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

1.编译运行

在这里插入图片描述

2.题目:

给定一个初始为空的队列和一系列入队、出队操作,请编写程序输出每次出队的元素。队列的元素值均为整数。

  • 输入格式:

输入第1行为1个正整数n,表示操作个数;接下来n行,每行表示一个操作,格式为1 d或0。1 d表示将整数d入队,0表示出队。n不超过20000。

  • 输出格式:

按顺序输出每次出队的元素,每个元素一行。若某出队操作不合法(如在队列空时出队),则对该操作输出invalid。

  • 输入样例:

在这里给出一组输入。例如:

7
1 1
1 2
0
0
0
1 3
0

  • 输出样例:

1
2
invalid
3

3.想法

本来这个是用一个最简单的有限长数组实现队列的性质即可,但是考虑到假如我们要对其频繁的动态增删,或者为其增加特别多的元素,那么有限长的数组就不能实现了。那么与顺序表结构相对的就是适合频繁增删的链式结构,所以为了实现动态添加,在这里我就本着玩的心态,选择了链式存储结构,用链队实现我们的想法,完成一个理论上没有上限,可以无限增加的队列。

4.代码块

  • 队列的链式存储结构
    设置一个结构体,每个结点包含数据 Data,和一个指向下一个结点的 指针 *next。
    再设置一个结构体,拥有可以指向队列头尾结点的指针。
1
2
3
4
5
6
7
8
9
cpp复制代码typedef struct QNode{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;

typedef struct {
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;
  • 链队的初始化

生成新结点作为头结点,队头和队尾指针指向此结点

1
2
3
4
5
6
cpp复制代码Status InitQueue(LinkQueue &Q){
Q.front = Q.rear=new QNode;
//头结点的指针域为空
Q.front->next = NULL;
return Ok;
}
  • 链队的入队
    1. 插入元素e为Q的新的队尾元素
    2. 为入队元素分配结点空间,用指针P指向
    3. 给新结点的数据域赋值
    4. 将新结点插入到队尾,修改队尾指针
1
2
3
4
5
6
7
8
cpp复制代码Status EnQueue(LinkQueue &Q,QElemType e){
QNode *p=new QNode;
p->data = e;
p->next = NULL;
Q.rear->next = p;
Q.rear = p;
return Ok;
}
  • 链队的出队
    1. 元素出队即删除队列的队头元素
    2. 首先判断队列是否为空
    3. 生成一个新结点起到一个数据中转的作用
    4. 修改头结点的指针域
    5. 元素被删,队尾指针指向头结点
    6. 释放原队头元素的空间
1
2
3
4
5
6
7
8
9
10
11
12
cpp复制代码Status DeQueue(LinkQueue &Q,QElemType &e){
//删除Q的队头元素,用e返回其值
if(Q.front==Q.rear) return Error;
//p指向队头元素
QNode *p=Q.front->next;
//e保存队头元素的值
e=p->data;
Q.front->next = p->next;
if(Q.rear == p) Q.rear =Q.front;
delete p;
return Ok;
}
  • 取链队的头元素
    1. 判断队列是否为空
    2. 取结点的 data 域
1
2
3
4
5
6
7
8
cpp复制代码SElemType GetHead(LinkQueue Q){
//返回Q的队头元素,不修改指针
//队列非空
if(Q.front!=Q.rear){
//返回队头元素的值,队头指针不变
return Q.front->next->data;
}
}

4.源码

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
cpp复制代码#include<iostream>
using namespace std;
typedef int QElemType;
typedef int SElemType;
typedef int Status;
#define Ok 1
#define Error 0
//队列的链式存储结构
typedef struct QNode{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;

typedef struct {
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;

//链队的初始化
Status InitQueue(LinkQueue &Q){
//生成新结点作为头结点,队头和队尾指针指向此结点
Q.front = Q.rear=new QNode;
//头结点的指针域为空
Q.front->next = NULL;
return Ok;
}

//链队的入队
Status EnQueue(LinkQueue &Q,QElemType e){
//插入元素e为Q的新的队尾元素
//为入队元素分配结点空间,用指针P指向
QNode *p=new QNode;
//新结点的数据域为e
p->data = e;
//将新结点插入到队尾
p->next = NULL;
//修改队尾指针
Q.rear->next = p;
Q.rear = p;
return Ok;
}

//链队的出队
Status DeQueue(LinkQueue &Q,QElemType &e){
//删除Q的队头元素,用e返回其值
//若队列为空,则返回Error
if(Q.front==Q.rear) return Error;
//p指向队头元素
QNode *p=Q.front->next;
//e保存队头元素的值
e=p->data;
//修改头结点的指针域
Q.front->next = p->next;
//最后一个元素被删,队尾指针指向头结点
if(Q.rear == p) Q.rear =Q.front;
//释放原队头元素的空间
delete p;
return Ok;
}

//取链队的头元素
SElemType GetHead(LinkQueue Q){
//返回Q的队头元素,不修改指针
//队列非空
if(Q.front!=Q.rear){
//返回队头元素的值,队头指针不变
return Q.front->next->data;
}
}
int main(){
QElemType e;
LinkQueue Q;
InitQueue(Q);
int n,b;
cin>>n;
while(n-->0){
cin>>b;
if(b==1){
cin>>b;
EnQueue(Q,b);
}
if(b==0){
if(Q.front==Q.rear)
cout<<"invalid"<<endl;
else{
DeQueue(Q,b);
cout<<b<<endl;
}
}
}
return 0;
}

本文转载自: 掘金

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

Rust所有权规则 所有权规则 变量作用域(scope) s

发表于 2021-11-23

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

所有权规则

所有权是有一套规则的,首先看一下所有权的具体规则:

1
2
3
markdown复制代码- 每个值都有一个变量,这个变量是该值的所有者(owner)
- 每个值任一时刻有且只能有一个所有者
- 当所有者(变量)离开其作用域,这个值就会被删除

变量作用域(scope)

上述所有权规则中提到当所有者(变量)离开其作用域,这个值就会被删除,这一小部分我们就来看一下变量的作用域。

作用域scope 就是一个item 在程序中的有效范围,比如如下代码,变量i绑定了一个字符串字面值,这个变量从声明的点开始直到当前 作用域 结束时都是有效的。

1
2
3
4
5
6
7
rust复制代码fn main() {
// i 在还没有声明的时候是无效的
let i = "rust"; // 从此处起,i 是有效的

println!("{}", i); // 可以对i变量进行操作
}
// i 作用域到此结束,i 不再有效

总结来说,上面代码有两个重要的事件点:

  • 当 i 进入作用域 时,它就是有效的。
  • 这一直持续到它 离开作用域 为止。

目前为止,变量是否有效与作用域的关系跟其他编程语言是类似的。现在我们在此基础上介绍 String 类型。

string类型

string类型是比前面文章介绍的基本的标量类型都要更复杂的数据类型,前面介绍的数据类型都是存储时被移出栈,因此需要一个存储在堆上的数据来探索 Rust 是如何知道该在何时清理数据的。而string类型就是存储在堆上的数据。

不过这里的重点是关注string类型喝所有权相关的部分,这些部分也同样适用于标准库提供的或者自己创建的其他的复杂数据类型,后面的文章中会更加详细的介绍string类型。

字符串字面值很方便,但是并不适合使用文本的每一种场景。原因之一就是它们是不可变的。另一个原因是并非所有字符串的值都能在编写代码时就知道:例如,要是想获取用户输入并存储该怎么办呢?为此,Rust 有第二个字符串类型,String。这个类型被分配到堆上,所以能够存储在编译时未知大小的文本。可以使用 from 函数基于字符串字面值来创建 String,如下:

1
2
3
rust复制代码fn main() {
let s = String::from("rust");
}

这两个冒号(::)是运算符,允许将特定的 from 函数置于 String 类型的命名空间(namespace)下,而不需要使用类似 string_from 这样的名字。

我们也可以修改此类字符串:

1
2
3
4
5
rust复制代码fn main() {
let s = String::from("rust");
s.push_str(", good"); // push_str 方法在字符串后面追加值
println!("{}", s); // 结果是 rust, good
}

这里就会产生一个问题,为什么String类型可变而字面值却不行呢?区别就在于两种数据类型对内存的处理上,这里留一个小悬念,在下一篇文章中进行解答哦~

结语

文章首发于微信公众号程序媛小庄,同步于掘金。

码字不易,转载请说明出处,走过路过的小伙伴们伸出可爱的小指头点个赞再走吧(╹▽╹)

本文转载自: 掘金

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

数仓如何限制临时数据文件下盘量

发表于 2021-11-23

摘要:查询的中间结果集如果太大导致落盘生成的临时数据文件,本文提供两种限制临时数据文件下盘数据量的方案,以防影响正常业务运行。

本文分享自华为云社区《GaussDB(DWS)如何限制临时数据文件下盘量》,作者: wangxiaojuan8。

有些SQL语句,会出现中间结果集太大,内存放不下,需要落盘到外存(比如存在对大数据量进行聚集等操作,导致聚集操作的中间结果集在内存中放不下时会下盘),且落盘生成的临时数据文件占用空间过大,则会影响正常的数据写入业务无法执行,磁盘只能提供只读操作。

对于上述场景,可以通过两种方式,来控制用户执行过程中间结果集可落盘的数额,当超过限额,会报错终止该语句的执行,以防临时数据文件占用空间过大:

  1. 方案1:设置每个线程的临时文件落盘数据量限制
  1. 方案2:为用户设置中间结果集落盘空间限额

方案1:设置每个线程的临时文件落盘数据量限制

设置GUC参数temp_file_limit可以限制每个线程的临时文件落盘数据量限制。temp_file_limit属于SUSET类型参数,取值范围:整型,单位为KB。其中-1表示没有限制。默认值:-1。

1. 如何设置temp_file_limit参数

可通过gs_guc工具进行全局设置,如下:

gs_guc reload -Z coordinator -Z datanode -N all -Iall -c “temp_file_limit = 1024”

2. temp_file_limit取值计算公式

可以用下面的公式粗略的计算一个temp_file_limit的取值:temp_file_limit = 预计的总下盘量/同时下盘线程数

总下盘量一般可设置为可用空间的20%,这个百分比可根据用户的可接受程度进行调节。同时下盘线程数是业务运行中,通常情况下并发的query中产生中间临时数据下盘的线程数。随着数据库中存储的数据量增加,temp_file_limit的值要适时调整。

注意:此参数是限制每个线程的临时文件落盘数据量,如果一个query有多个线程,单个线程落盘数据量超过此参数限制,query会报错退出。如果每个线程都没超过限制,但多个线程下盘数据量累计超过此参数限制,并不会报错退出。

3. 示例

以TPC-DS1x数据中的customer_demographics表为例。SQL查询不下推,中间结果集仅在CN上落盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sql复制代码postgres=# show temp_file_limit;
temp_file_limit
-----------------
1MB
(1 row)

postgres=# set enable_stream_operator=off;
SET

postgres=# explain select * from customer_demographics c1, customer_demographics c2 order by c1.cd_demo_sk;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
id | operation | E-rows | E-width | E-costs
----+--------------------------------------------------------------------------+---------------+---------+------------------
1 | -> Sort | 3689472640000 | 112 | 2329492473090.72
2 | -> Nested Loop (3,4) | 3689472640000 | 112 | 36894726400.00
3 | -> Data Node Scan on customer_demographics "_REMOTE_TABLE_QUERY_" | 1920800 | 56 | 0.00
4 | -> Data Node Scan on customer_demographics "_REMOTE_TABLE_QUERY_" | 1920800 | 56 | 0.00
(6 rows)

postgres=# select * from customer_demographics c1, customer_demographics c2 order by c1.cd_demo_sk;
ERROR: temporary file size exceeds temp_file_limit (1024kB)

方案2:为用户设置中间结果集落盘空间限额

1. 如何设置用户中间结果集落盘空间限额

有两种方式可以设置用户中间结果集落盘空间限额:

  1. 通过CREATE USER指定SPILLSPACE,为新建用户设置中间结果集落盘限额

CREATE USER user_name … SPILLSPACE ‘spillspacelimit’;

  1. 通过ALTER USER指定SPILLSPACE,修改已有用户的中间结果集落盘空间限额

ALTER USER user_name … SPILLSPACE ‘spillspacelimit’;

比如:CREATE USER u1 PASSWORD ‘abcd@1234’ SPILL SPACE ‘unlimited’; –创建用户并设置中间结果集落盘限额为无限制

ALTER USER u1 SPILL SPACE ‘1G’; –修改用户u1的中间结果集落盘限额为1G

说明:

  1. 此设置是对所有节点生效的,即一条SQL在集群的CN和所有DN的落盘数据量之和超过限制,则语句就会报错终止。
  2. 当中间结果集落盘时,该用户的临时文件落盘数据量相应增加;当临时文件删除时,该用户的临时文件落盘数据量相应减少。
  3. 此设置是用户级的,如果同一用户同时并发运行多个query,则会累计每个query中间结果集落盘数据量。

注意:

要使上面的设置生效,需要设置GUC参数enable_perm_space为on。

如果多个用户都会执行大量中间结果集下盘操作,那么需要对涉及到的每个用户都进行设置。

2. 示例

示例1:中间结果集在CN和DN上都会落盘,总的落盘数据量会超过1G

1
2
3
4
5
6
7
8
9
10
sql复制代码postgres=# create user u1 password 'abcd@1234';
CREATE USER
postgres=# grant select on customer_demographics to u1;
GRANT
postgres=# alter user u1 spill space '1G';
ALTER USER
postgres=# alter session set session authorization u1 password 'abcd@1234';
SET
postgres=> select * from customer_demographics c1, customer_demographics c2 order by c1.cd_demo_sk;
ERROR: spill space is out of user's spill space limit

​示例2:SQL查询不下推,中间结果集仅在CN上落盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vbnet复制代码postgres=# set enable_stream_operator=off;
SET
postgres=# alter session set session authorization u1 password 'abcd@1234';
SET
postgres=> explain select * from customer_demographics c1, customer_demographics c2 order by c1.cd_demo_sk;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
id | operation | E-rows | E-width | E-costs
----+--------------------------------------------------------------------------+---------------+---------+------------------
1 | -> Sort | 3689472640000 | 112 | 2329492473090.72
2 | -> Nested Loop (3,4) | 3689472640000 | 112 | 36894726400.00
3 | -> Data Node Scan on customer_demographics "_REMOTE_TABLE_QUERY_" | 1920800 | 56 | 0.00
4 | -> Data Node Scan on customer_demographics "_REMOTE_TABLE_QUERY_" | 1920800 | 56 | 0.00
(6 rows)

postgres=> select * from customer_demographics c1, customer_demographics c2 order by c1.cd_demo_sk;
ERROR: spill space is out of user's spill space limit

总结

第一种方案偏重于限制每个线程的临时文件下盘量,第二种方案偏重于限制用户的临时文件下盘量,要结合业务的目的来选择更适合的参数及参数设置,避免发生临时文件下盘量过大影响正常业务执行。

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

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

Java代理模式之Java适配器模式 Java适配器模式

发表于 2021-11-23

「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战」

Java适配器模式

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。

这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。

我们通过下面的实例来演示适配器模式的使用。其中,音频播放器设备只能播放 mp3 文件,通过使用一个更高级的音频播放器来播放 vlc 和 mp4 文件。

介绍

意图:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

主要解决:主要解决在软件系统中,常常要将一些”现存的对象”放到新的环境中,而新环境要求的接口是现对象不能满足的。

何时使用:

⒈系统需要使用现有的类,而此类的接口不符合系统的需要。

⒉想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。

⒊通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)

如何解决:继承或依赖(推荐)。

关键代码:适配器继承或依赖已有的对象,实现想要的目标接口。

应用实例:

⒈美国电器 110V,中国 220V,就要有一个适配器将 110V 转化为 220V。

⒉JAVA JDK 1.1 提供了 Enumeration 接口,而在 1.2 中提供了 Iterator 接口,想要使用 1.2 的 JDK,则要将以前系统的 Enumeration 接口转化为 Iterator 接口,这时就需要适配器模式。

⒊在 LINUX 上运行 WINDOWS 程序。 4、JAVA 中的 jdbc。

优点:

⒈可以让任何两个没有关联的类一起运行。

⒉提高了类的复用。

⒊增加了类的透明度。

⒋灵活性好。

缺点:

⒈过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。

2.由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。

使用场景:有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。

注意事项:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。

实现

我们有一个 MediaPlayer 接口和一个实现了 MediaPlayer 接口的实体类 AudioPlayer。默认情况下,AudioPlayer 可以播放 mp3 格式的音频文件。

我们还有另一个接口 AdvancedMediaPlayer 和实现了 AdvancedMediaPlayer 接口的实体类。该类可以播放 vlc 和 mp4 格式的文件。

我们想要让 AudioPlayer 播放其他格式的音频文件。为了实现这个功能,我们需要创建一个实现了 MediaPlayer 接口的适配器类 MediaAdapter,并使用 AdvancedMediaPlayer 对象来播放所需的格式。

AudioPlayer 使用适配器类 MediaAdapter 传递所需的音频类型,不需要知道能播放所需格式音频的实际类。AdapterPatternDemo,我们的演示类使用 AudioPlayer 类来播放各种格式。

步骤 1

为媒体播放器和更高级的媒体播放器创建接口。

1
2
3
arduino复制代码public interface MediaPlayer {
public void play(String audioType, String fileName);
}
1
2
3
4
arduino复制代码public interface AdvancedMediaPlayer { 
public void playVlc(String fileName);
public void playMp4(String fileName);
}

步骤 2

创建实现了 AdvancedMediaPlayer 接口的实体类。

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码public class VlcPlayer implements AdvancedMediaPlayer{
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: "+ fileName);
}

@Override
public void playMp4(String fileName) {
//什么也不做
}
}
1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码public class Mp4Player implements AdvancedMediaPlayer{

@Override
public void playVlc(String fileName) {
//什么也不做
}

@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file. Name: "+ fileName);
}
}

步骤 3

创建实现了 MediaPlayer 接口的适配器类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typescript复制代码public class MediaAdapter implements MediaPlayer {

AdvancedMediaPlayer advancedMusicPlayer;

public MediaAdapter(String audioType){
if(audioType.equalsIgnoreCase("vlc") ){
advancedMusicPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")){
advancedMusicPlayer = new Mp4Player();
}
}

@Override
public void play(String audioType, String fileName) {
if(audioType.equalsIgnoreCase("vlc")){
advancedMusicPlayer.playVlc(fileName);
}else if(audioType.equalsIgnoreCase("mp4")){
advancedMusicPlayer.playMp4(fileName);
}
}
}

步骤 4

创建实现了 MediaPlayer 接口的实体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码public class AudioPlayer implements MediaPlayer {
MediaAdapter mediaAdapter;

@Override
public void play(String audioType, String fileName) {

//播放 mp3 音乐文件的内置支持
if(audioType.equalsIgnoreCase("mp3")){
System.out.println("Playing mp3 file. Name: "+ fileName);
}
//mediaAdapter 提供了播放其他文件格式的支持
else if(audioType.equalsIgnoreCase("vlc")
|| audioType.equalsIgnoreCase("mp4")){
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
}
else{
System.out.println("Invalid media. "+
audioType + " format not supported");
}
}
}

步骤 5

使用 AudioPlayer 来播放不同类型的音频格式。

1
2
3
4
5
6
7
8
9
10
typescript复制代码public class AdapterPatternDemo {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();

audioPlayer.play("mp3", "beyond the horizon.mp3");
audioPlayer.play("mp4", "alone.mp4");
audioPlayer.play("vlc", "far far away.vlc");
audioPlayer.play("avi", "mind me.avi");
}
}

步骤 6

执行程序,输出结果:

1
2
3
4
yaml复制代码Playing mp3 file. Name: beyond the horizon.mp3
Playing mp4 file. Name: alone.mp4
Playing vlc file. Name: far far away.vlc
Invalid media. avi format not supported

本文转载自: 掘金

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

Elastic-Job动态修改定时任务-踩坑篇

发表于 2021-11-23

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战」

引言

最近小编一直在加班写需求,没有时间更文。项目中有一个需求,觉得挺有意思的,把做的思路分享给大家,需求是每个月的8号和20号我们系统会处理一些单子,这里将这两天称为审单日,如果超过这个时间的单子,需要系统在9号和20号的12:30,自动将单子撤销掉。如果8号或20号遇到周末,则需要提前或者延后到工作日作为审单,第二天的中午12:30做撤销单据的任务。

设计思路

针对这个问题我想了两个实现思路。

动态定时任务(放弃)

创建一个动态定时任务,每次执行任务成功后,修改定时任务的cron表达式,更新其下次执行的时间。处理撤销单据和获取撤销单据日的cron表达式同上面设计思路一致。

优点:

  • 只在需要的时间去执行代码
    缺点:
  • 代码实现逻辑相对复杂
  • 担心Elastic-Job出现问题后的处理成本更大

定时任务-12:30分执行(使用)

设置一个定时任务,每天12:30分执行,创建一张表预置撤销单据日期,每次任务执行时,判断当前日期是否是撤销单据日,如果不是直接跳过,如果日期一致的话,则执行撤销单据的操作,并且将这一条撤销单据时间状态修改为已执行。

优点:

  • 实现代码逻辑特别容易
    缺点:
  • 不够灵活,每日都需要执行一次定时任务,不能按照需要去执行

撤销时间表

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码-- auto-generated definition
create table cancel_schedule
(
id bigint auto_increment comment '主键' primary key,
title varchar(100) not null comment '标题',
execute_time date not null comment '执行时间:',
cron varchar(50) not null comment 'cron表达式',
status int default 0 not null comment '状态 0-正常',
remark varchar(256) null comment '备注',
created_at datetime default CURRENT_TIMESTAMP not null comment '创建时间',
modified_at datetime default CURRENT_TIMESTAMP not null comment '更新时间'
)
comment '撤销时间表';

初始化sql

1
2
sql复制代码INSERT INTO saos_csp_fop.fop_cancel_schedule (id, title, execute_time,cron, status, remark, created_at, modified_at) VALUES (1, '2021年11月的对公付款日后的取消', '2021-11-22','0 30 12 9 * ? *', 1, null, '2021-11-22 15:14:22', '2021-11-22 15:14:22');
INSERT INTO saos_csp_fop.fop_cancel_schedule (id, title, execute_time,cron, status, remark, created_at, modified_at) VALUES (2, '2021年12月的对公付款日', '2021-11-22','0 30 12 20 * ? *', 1, null, '2021-11-22 15:14:22', '2021-11-22 15:14:22');

动态定时任务

初始化配置(DynamicElasticJobConfig)

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
java复制代码import com.hanhang.service.listener.ElasticJobListener;
import com.dangdang.ddframe.job.event.JobEventConfiguration;
import com.dangdang.ddframe.job.event.rdb.JobEventRdbConfiguration;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperConfiguration;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperRegistryCenter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import javax.sql.DataSource;

/**
* @ClassName DynamicElasticJobConfig
* @Description 动态定时任务配置
* @Author hanhang
* @Date 2021/11/19 4:24 下午
*/
@Configuration
public class DynamicElasticJobConfig {
/**
* zookeeper的服务地址
*/
@Value("${zk.server}")
private String serverLists;
/**
* Elastic-Job的命名空间
*/
@Value("${chjJob.zookeeper.namespace}")
private String namespace;
@Resource
private DataSource dataSource;

@Bean
public ZookeeperConfiguration zookeeperConfiguration() {
return new ZookeeperConfiguration(serverLists, namespace);
}

@Bean(initMethod = "init")
public ZookeeperRegistryCenter zookeeperRegistryCenter(ZookeeperConfiguration zookeeperConfiguration){
return new ZookeeperRegistryCenter(zookeeperConfiguration);
}

@Bean
public ElasticJobListener elasticJobListener(){
return new ElasticJobListener(100, 100);
}

/**
* 将作业运行的痕迹进行持久化到DB
*
* @return
*/
@Bean
public JobEventConfiguration jobEventConfiguration() {
return new JobEventRdbConfiguration(dataSource);
}
}

动态定时任务相关操作工具(ElasticJobHandler)

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

import com.dangdang.ddframe.job.api.simple.SimpleJob;
import com.dangdang.ddframe.job.config.JobCoreConfiguration;
import com.dangdang.ddframe.job.config.simple.SimpleJobConfiguration;
import com.dangdang.ddframe.job.event.JobEventConfiguration;
import com.dangdang.ddframe.job.lite.api.listener.ElasticJobListener;
import com.dangdang.ddframe.job.lite.config.LiteJobConfiguration;
import com.dangdang.ddframe.job.lite.internal.schedule.JobRegistry;
import com.dangdang.ddframe.job.lite.spring.api.SpringJobScheduler;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperRegistryCenter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
* @ClassName ElasticJobHandler
* @Description 动态定时任务相关操作工具
* @Author hanhang
* @Date 2021/11/19 4:29 下午
*/
@Slf4j
@Component
public class ElasticJobHandler {

@Autowired
private ZookeeperRegistryCenter zookeeperRegistryCenter;
@Resource
private JobEventConfiguration jobEventConfiguration;
@Resource
private ElasticJobListener elasticJobListener;

/***
* 动态创建定时任务
* @param jobName:定时任务名称
* @param cron:表达式
* @param shardingTotalCount:分片数量
* @param instance:定时任务实例
* @param parameters:参数
* @param description:作业描述
*/
public void addJob(String jobName, String cron, int shardingTotalCount, SimpleJob instance, String parameters, String description){
log.info("动态创建定时任务:jobName = {}, cron = {}, shardingTotalCount = {}, parameters = {}", jobName, cron, shardingTotalCount, parameters);

LiteJobConfiguration.Builder builder = LiteJobConfiguration.newBuilder(new SimpleJobConfiguration(
JobCoreConfiguration.newBuilder(
jobName,
cron,
shardingTotalCount
).failover(true).jobParameter(parameters).description(description).build(),
instance.getClass().getName()
)).overwrite(true);
LiteJobConfiguration liteJobConfiguration = builder.build();

new SpringJobScheduler(instance,zookeeperRegistryCenter,liteJobConfiguration,jobEventConfiguration,elasticJobListener).init();
}

/**
* 更新定时任务
* @param jobName
* @param cron
*/
public void updateJob(String jobName, String cron) {
log.info("更新定时任务:jobName = {}, cron = {}", jobName, cron);

JobRegistry.getInstance().getJobScheduleController(jobName).rescheduleJob(cron);
}

/**
* 删除定时任务
* @param jobName
*/
public void removeJob(String jobName){
log.info("删除定时任务:jobName = {}", jobName);

JobRegistry.getInstance().getJobScheduleController(jobName).shutdown();
}
}

overwrite(true)此处是开启job可被重写,方便修改任务的cron表达式

配置ElasticJobListener监听器(ElasticJobListener)

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

import com.dangdang.ddframe.job.executor.ShardingContexts;
import com.dangdang.ddframe.job.lite.api.listener.AbstractDistributeOnceElasticJobListener;

/**
* @ClassName ElasticJobListener
* @Description 监听器
* 现分布式任务监听器
* 如果任务有分片,分布式监听器会在总的任务开始前执行一次,结束时执行一次
* @Author hanhang
* @Date 2021/11/19 4:27 下午
*/
public class ElasticJobListener extends AbstractDistributeOnceElasticJobListener {

public ElasticJobListener(long startedTimeoutMilliseconds, long completedTimeoutMilliseconds) {
super(startedTimeoutMilliseconds,completedTimeoutMilliseconds);
}

@Override
public void doBeforeJobExecutedAtLastStarted(ShardingContexts shardingContexts) {
}

@Override
public void doAfterJobExecutedAtLastCompleted(ShardingContexts shardingContexts) {
//任务执行完成后更新状态为已执行,当前未处理
}
}

动态任务执行类(PaymentCancelDynamicJob)

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

import com.hanhang.biz.PaymentPlanItemBiz;
import com.hanhang.common.constants.LockConstant;
import com.hanhang.common.enumeration.PaymentPlanItemStatusEnum;
import com.hanhang.common.util.DateUtil;
import com.hanhang.domain.CancelSchedule;
import com.hanhang.domain.PaymentPlanItem;
import com.hanhang.facade.request.PaymentPlanItemRevokeRequest;
import com.hanhang.service.CancelScheduleService;
import com.hanhang.service.PaymentPlanItemService;
import com.chehejia.starter.job.annotation.ElasticJobConf;
import com.dangdang.ddframe.job.api.ShardingContext;
import com.dangdang.ddframe.job.api.simple.SimpleJob;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
* @ClassName DynamicJob
* @Description 动态定时任务执行
* @Author hanhang
* @Date 2021/11/19 4:37 下午
*/
@Component
@ElasticJobConf(name = "saos-csp-fop-service-PaymentItemCancel", cron = "0 30 12 * * ?",
overwrite = true, description = "企业平台部-财经部-资金组-FOP-撤销定时任务")
@RequiredArgsConstructor
@Slf4j
public class PaymentCancelDynamicJob implements SimpleJob {
@Resource
private PaymentPlanItemService itemService;
@Resource
private CuratorFramework curatorClient;
@Resource
private PaymentPlanItemBiz itemBiz;
@Resource
private CancelScheduleService cancelScheduleService;

/**
* 业务执行逻辑
*
* @param shardingContext
*/
@Override
public void execute(ShardingContext shardingContext) {
log.info("{}动态定时任务执行逻辑start...", DateUtil.covert2String(LocalDate.now()));
String jobName = shardingContext.getJobName();
String jobParameter = shardingContext.getJobParameter();
log.info("---------PaymentCancelDynamicJob---------撤销动态定时任务正在执行:jobName = {}, jobParameter = {}", jobName, jobParameter);

List<CancelSchedule> cancelSchedules = cancelScheduleService.getNextSchedule();
List<PaymentPlanItem> items = itemService.getItemsByStatus(PaymentPlanItemStatusEnum.PLAN_SUBMIT.getCode());
if (isReturn(cancelSchedules,items)){
return;
}
//根据参数调用不同的业务接口处理,请远程调用业务模块处理,避免本服务与业务依赖过重...
InterProcessMutex lock = new InterProcessMutex(curatorClient, LockConstant.GLOBAL_LOCK_PATH);
try {
if (!lock.acquire(LockConstant.GLOBAL_LOCK_TIME_SECONDS, TimeUnit.SECONDS)) {
log.info("撤销动态定时任务>>其他任务还未执行完...");
return;
}
try {
items = itemService.getItemsByStatus(PaymentPlanItemStatusEnum.PLAN_SUBMIT.getCode());
List<String> vouCherNos = items.stream().map(PaymentPlanItem::getVoucherNo).collect(Collectors.toList());
PaymentPlanItemRevokeRequest request = PaymentPlanItemRevokeRequest.builder()
.voucherNos(vouCherNos).build();
itemBiz.revokeItem(request);
CancelSchedule cancelSchedule = cancelSchedules.get(0);
cancelSchedule.setStatus(1);
cancelScheduleService.updateByPrimaryKeySelective(cancelSchedule);
} catch (Exception ex) {
log.error("撤销动态定时任务>>系统异常", ex);
} finally {
lock.release(); // always release the lock in a finally block
}
} catch (Exception ex) {
log.error("撤销动态定时任务>>系统异常", ex);
}
log.info("撤销动态定时任务>>定时任务执行结束。");

log.info("{}动态定时任务执行逻辑end...", DateUtil.covert2String(LocalDate.now()));
}

private boolean isReturn(List<CancelSchedule> cancelSchedules,List<PaymentPlanItem> items){
if (CollectionUtils.isEmpty(cancelSchedules)){
return true;
}else {
CancelSchedule cancelSchedule = cancelSchedules.get(0);
String executeTime = DateUtil.covert2String(cancelSchedule.getExecuteTime(),"yyyy-MM-dd");
String nowTime = DateUtil.covert2String(LocalDate.now(),"yyyy-MM-dd");
if (!executeTime.equalsIgnoreCase(nowTime)){
return true;
}
}
return CollectionUtils.isEmpty(items);
}
}

@Transactional(rollbackFor = Exception.class)在execute方法加事务,会导致启动项目失败

扫描本地持久化的任务、添加任务(ScanDynamicJobHandler)

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

import com.hanhang.framework.beans.exception.BizRuntimeException;
import com.hanhang.common.enumeration.FopErrorCode;
import com.hanhang.domain.CancelSchedule;
import com.hanhang.service.CancelScheduleService;
import com.hanhang.service.job.PaymentCancelDynamicJob;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;

/**
* @ClassName ScanDynamicJobHandler
* @Description 扫描本地动态任务
* @Author hanhang
* @Date 2021/11/19 4:53 下午
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ScanDynamicJobHandler {
private final ElasticJobHandler elasticJobHandler;
private final CancelScheduleService cancelScheduleService;
private final PaymentCancelDynamicJob paymentCancelDynamicJob;

/**
* 扫描动态任务列表,并添加任务
*
* 循环执行的动态任务,本服务重启的时候,需要重新加载任务
*
* @author songfayuan
* @date 2021/4/26 9:15 下午
*/
public void scanAddJob() {
//这里为从MySQL数据库读取job_dynamic_task表的数据,微服务项目中建议使用feign从业务服务获取,
// 避免本服务过度依赖业务的问题,然后业务服务新增动态任务也通过feign调取本服务JobOperateController实现,
// 从而相对独立本服务模块
List<CancelSchedule> cancelSchedules = cancelScheduleService.getNextSchedule();
if (!CollectionUtils.isEmpty(cancelSchedules)){
CancelSchedule cancelSchedule = cancelSchedules.get(0);
elasticJobHandler.addJob("saos-csp-fop-service-PaymentItemCancel",cancelSchedule.getCron(),1,paymentCancelDynamicJob,"","撤销动态定时任务");
} else {
throw new BizRuntimeException(FopErrorCode.COMMON_ERROR.getCode(), "无可启动的任务");
}
}
}

项目启动程序中新增加载本地持久化任务

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

import com.hanhang.service.handler.ScanDynamicJobHandler;
import com.chehejia.starter.job.annotation.EnableElasticJob;
import com.chehejia.starter.mq.annotation.EnableMQConfiguration;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.core.env.Environment;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import tk.mybatis.spring.annotation.MapperScan;

import java.net.InetAddress;

@Slf4j
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableApolloConfig
@EnableElasticJob
@MapperScan(basePackages = "com.chehejia.saos.csp.fop.persistence")
@EnableFeignClients(basePackages = {"com.chehejia.saos.csp.fop"})
@EnableMQConfiguration
@EnableTransactionManagement
@RequiredArgsConstructor
public class RootApplication implements CommandLineRunner {
private final ScanDynamicJobHandler scanDynamicJobHandler;
public static void main(String[] args) throws Exception {
SpringApplication app = new SpringApplication(com.chehejia.saos.csp.fop.RootApplication.class);
Environment env = app.run(args).getEnvironment();
app.addListeners();
log.info("\n----------------------------------------------------------\n\t" +
"FOP Application '{}' is running! Access URLs:\n\t" +
"Local: \t\thttp://127.0.0.1:{}\n\t" +
"External: \thttp://{}:{}\n\t" +
"Swagger API:http://{}:{}/swagger-ui.html\n\t" +
"Swagger API:http://saos-csp-fop-service.dev.k8s.chj.com/swagger-ui.html\n\t" +
"Druid index:http://127.0.0.1:{}/druid/index.html\n" +
"----------------------------------------------------------",
env.getProperty("spring.application.name"),
env.getProperty("server.port"),
InetAddress.getLocalHost().getHostAddress(),
env.getProperty("server.port"),
InetAddress.getLocalHost().getHostAddress(),
env.getProperty("server.port"),
env.getProperty("server.port"));
}

@Override
public void run(String... args) throws Exception {
log.info(">>>>>>>>>>>>>>>服务启动执行,扫描动态任务列表,并添加任务<<<<<<<<<<<<<");
scanDynamicJobHandler.scanAddJob();
}
}

至此动态修改定时任务可以完成,通过配置界面也可以看到任务。

image.png

问题

1、通过代码测试,修改任务的cron会成功,定时任务也会按时去执行,但是在管理页面中看不到cron表达式修改。

2、如果Elastic-Job不通,或在定时的时间内重启服务,由于任务有【错过重执行】,那么有可能在撤销了不该撤销的单据。

3、Elastic-Job出现异常,无法做幂等。

综合以上的问题,放弃了这种方式。另一种实现方式就不在文章中体现了,处理逻辑比较简单。

总结

使用Elastic-Job动态修改定时任务,可能踩到的坑有:

1、在execute方法上使用@Transactional注解导致服务启动失败,报nullException。

image.png
需要用其他方式进行事务处理,请参照这篇文章@Transactional注解失效

2、在scanJob的时候,需要通过注入的方式,将Job添加到SpringJobScheduler中,否则在Job执行中注入的属性,将不会被Spring代理,出现空指针异常。

3、修改cron之后,在管理页面看不到cron表达式变更,但是通过Elastic-Job通过时间片的轮转,也可以正常执行。

本文转载自: 掘金

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

Java系统线上生产问题排查一把梭 1 环境 2 监控 分析

发表于 2021-11-23

1 环境

1.1 Dev

可以随意使用任何熟悉的工具排查。只要问题能重现,排查就不会太难,最多就是把程序调试到各种框架源码,所以这也是为何面试都会问源码,不求都看过,但要有思路知道如何去看能解决问题。

1.2 Test

比开发环境少了debug,不过也可使用jvisualvm或Arthas,附加到远程JVM进程。

还有测试环境是允许造数据来模拟我们需要的场景的哦,因此这时遇到问题记得主动沟通测试人员造数据让bug更容易复现。

1.3 Prd

该环境下开发人员的权限最低,所以排查问题时障碍很大:

  • 无法使用调试工具从远程附加进程
  • 快速恢复为先,即使在结婚,也得赶紧修复线上问题。而且生产环境流量大、网络权限严格、调用链路复杂,因此更容易出问题,也是出问题最多的环境。

2 监控

生产环境出现问题时,因为要尽快恢复应用,就不可能保留完整现场用于排查和测试。因此,是否有充足的信息(日志、监控和快照)可以了解历史、还原bug 场景。
最常用的就是 ELK 的日志了,注意:

  • 确保错误、异常信息可被完整记录到文件日志
  • 确保生产上程序的日志级别是INFO以上
    记录日志要使用合理的日志优先级,DEBUG用于开发调试、INFO用于重要流程信息、WARN用于需要关注的问题、ERROR用于阻断流程的错误

生产环境需开发配合运维才能做好完备监控:

主机维度

对CPU、内存、磁盘、网络等资源做监控。如果应用部署在虚拟机或k8s集群,那么除了对物理机做基础资源监控外,同样还要对虚拟机或Pod监控。监控层数取决于应用的部署方案,有一层OS就要做一层监控。

网络维度

监控专线带宽、交换机基本情况、网络延迟

所有的中间件和存储都要做好监控

不仅仅是监控进程对CPU、内存、磁盘IO、网络使用的基本指标,更重要的是监控组件内部的一些重要指标。比如最常用的Prometheus,就提供了大量exporter对接各种中间件和存储系统

应用层面

需监控JVM进程的类加载、内存、GC、线程等常见指标(比如使用Micrometer来做应用监控),此外还要确保能够收集、保存应用日志、GC日志

我们再来看看快照。这里的“快照”是指,应用进程在某一时刻的快照。通常情况下,我们会为生产环境的Java应用设置-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath=…这2个JVM参数,用于在出现OOM时保留堆快照。这个课程中,我们也多次使用MAT工具来分析堆快照。

分析定位问题的最佳实践

定位问题,首先要定位问题出在哪个层次:Java应用程序自身问题还是外部因素导致。

  • 可以先查看程序是否有异常,异常信息一般比较具体,可以马上定位到大概的问题方向
  • 如果是一些资源消耗型的问题可能不会有异常,我们可以通过指标监控配合显性问题点来定位。

一般问题原因可归类如下:

程序发布后 Bug

回滚,再慢慢通过版本差异分析根因。

外部因素

比如主机、中间件或DB问题。
这种按主机层面问题、中间件或存储(统称组件)的问题分为:

主机层

可使用工具排查:

CPU相关

使用top、vmstat、pidstat、ps

内存相关

使用free、top、ps、vmstat、cachestat、sar

IO相关

使用lsof、iostat、pidstat、sar、iotop、df、du

网络相关

使用ifconfig、ip、nslookup、dig、ping、tcpdump、iptables

组件

从如下方面排查:

  • 组件所在主机是否有问题
  • 组件进程基本情况,观察各种监控指标
  • 组件的日志输出,特别是错误日志
  • 进入组件控制台,使用一些命令查看其运作情况。

系统资源不够造成系统假死

通常先通过重启和扩容解决问题,之后再分析,最好能留个快照。

系统资源不够,一般可能:

CPU使用高

若现场还在,具体分析流程:

  • 在服务器执行top -Hp pid
    查看进程中哪个线程CPU使用高
  • 输入大写的P将线程按照 CPU 使用率排序,并把明显占用CPU的线程ID转换为16进制
  • 在jstack命令输出的线程栈中搜索这个线程ID,定位出问题的线程当时的调用栈

若无法直接在服务器执行top,可采样定位:间隔固定时间运行一次jstack,采样几次后,对比采样得出哪些线程始终处于运行状态,找出问题线程。

若现场没了,可排除法分析。CPU使用高,一般是由下面的因素引起的:

  • 突发压力
    可通过应用之前的负载均衡的流量或日志量确认,诸如Nginx等反向代理都会记录URL,可依靠代理的Access Log进行细化定位,也可通过监控观察JVM线程数的情况。压力问题导致CPU使用高的情况下,如果程序的各资源使用没有明显不正常,之后可以通过压测+Profiler(jvisualvm就有这个功能)进一步定位热点方法;如果资源使用不正常,比如产生了几千个线程,就需要考虑调参
  • GC
    可通过JVM监控GC相关指标、GC Log确认。如果确认是GC压力,那么内存使用也很可能会不正常,需要按照内存问题分析流程做进步分析。
  • 死循环或不正常处理流程
    可以结合应用日志分析。一般情况下,应用执行过程中都会产生一些日志,可以重点关注日志量异常部分。

内存泄露或OOM

最简单的就是堆转储后使用MAT分析。堆转储,包含了堆现场全貌和线程栈信息,一般观察支配树图、直方图就可以马上看到占用大量内存的对象,可以快速定位到内存相关问题
Java进程对内存的使用不仅仅是堆区,还包括线程使用的内存(线程个数*每一个线程的线程栈)和元数据区。每一个内存区都可能产生OOM,可以结合监控观察线程数、已加载类数量等指标分析
注意看JVM参数的设置是否有明显不合理的,限制了资源。

IO问题

除非是代码问题引起的资源不释放等问题,否则通常都不是由Java进程内部因素引发的。

网络

一般也是由外部因素引起。对于连通性问题,结合异常信息通常比较容易定位;对于性能或瞬断问题,可以先尝试使用ping等工具简单判断,如果不行再使用tcpdump或Wireshark。

迷茫时的最佳实践

偶尔可能分析和定位难题,会迷失自我。如果你也这样,可参考如下经验

cause or result?

比如业务执行的很慢,而且线程数增多,那就可能是:

  • 代码逻辑有问题、依赖的外部服务慢
    使得自己的业务逻辑执行缓慢,在访问量不变情况下,就需要更多线程处理。比如,10 TPS的并发原先一次请求1s即可完成,10个线程可支撑;现在执行完成需要10s,就需100个线程
  • 请求量增大
    使得线程数增多,应用本身CPU不足,上下文切换问题导致处理变慢

这时就需要多结合监控指标和各服务的入口流量,分析慢是cause or result。

探求规律

如果没头绪,那就试试总结规律吧!
比如

  • 有一堆服务器做负载均衡,出问题时可分析监控和日志看请求是否是均匀分布的,可能问题都集中在某个机器节点上
  • 应用日志一般会记录线程名称,出问题时可分析日志是否集中在某类线程
  • 若发现应用开启大量TCP连接,通过netstat可分析出主要集中连接到哪个服务

探求到了规律,就很容易突破了。

调用拓扑

比如看到Nginx返回502,一般可认为是下游服务的问题导致网关无法完成请求转发。
对于下游服务,不能想当然就认为是我们的Java程序,比如在拓扑上可能Nginx代理的是Kubernetes的Traefik Ingress,链路是Nginx->Traefik->应用,如果一味排查Java程序的健康,则始终找不到根因。

有时虽然使用了Feign进行服务调用,出现连接超时也不一定就是服务端问题,有可能是客户端通过URL调用服务端,并非通过Eureka的服务发现实现的客户端负载均衡。即客户端连接的是Nginx代理而非直接连接应用,客户端连接服务出现的超时,其实是Nginx代理宕机所致。

资源限制

观察各种监控指标,如果发现曲线慢慢上升然后稳定在一个水平线,一般就是资源达到瓶颈。

观察网络带宽曲线时,如果带宽上升到120MB左右不动了,很可能就是打满了1GB的网卡或传输带宽
观察到数据库活跃连接数上升到10个不动了,很可能是连接池打满了

观察监控一旦看到任何这样曲线,都要引起重视。

连锁反应

CPU、内存、IO和网络相辅相成,一个资源出现瓶颈,很可能同时引起其他资源连锁反应。

内存泄露后对象无法回收会造成大量Full GC,CPU会大量消耗在GC从而引起CPU使用增加

经常会把数据缓存在内存队列进行异步IO,网络或磁盘出现问题时,就很可能会引起内存暴涨。

所以出问题时,要综合考虑避免误判

客户端or服务端or传输问题?

比如MySQL访问慢了,可能:

  • 客户端原因,连接池不够导致连接获取慢、GC停顿、CPU占满
  • 传输过程问题
    包括光纤可能被挖断了呀、防火墙、路由表等设置有问题
  • 真的服务端背锅了

这都需要逐一排查区分。

服务端慢一般可以看到MySQL出慢日志,传输慢一般可以通过ping来简单定位,排除了这两个可能,并且仅仅是部分客户端出现访问慢的情况,就需要怀疑是客户端本身的问题。对于第三方系统、服务或存储访问出现慢的情况,不能完全假设是服务端的问题。

第七,快照类工具和趋势类工具需要结合使用。比如,jstat、top、各种监控曲线是趋势类工具,可以让我们观察各个指标的变化情况,定位大概的问题点;而jstack和分析堆快照的MAT是快照类工具,用于详细分析某一时刻应用程序某一个点的细节。

一般情况下,我们会先使用趋势类工具来总结规律,再使用快照类工具来分析问题。如果反过来可能就会误判,因为快照类工具反映的只是一个瞬间程序的情况,不能仅仅通过分析单一快照得出结论,如果缺少趋势类工具的帮助,那至少也要提取多个快照来对比。

第八,不要轻易怀疑监控。我曾看过一个空难事故的分析,飞行员在空中发现仪表显示飞机所有油箱都处于缺油的状态,他第一时间的怀疑是油表出现故障了,始终不愿意相信是真的缺油,结果飞行不久后引擎就断油熄火了。同样地,在应用出现问题时,我们会查看各种监控系统,但有些时候我们宁愿相信自己的经验,也不相信监控图表的显示。这可能会导致我们完全朝着错误的方向来排查问题。

如果你真的怀疑是监控系统有问题,可以看一下这套监控系统对于不出问题的应用显示是否正常,如果正常那就应该相信监控而不是自己的经验。

第九,如果因为监控缺失等原因无法定位到根因的话,相同问题就有再出现的风险,需要做好三项工作:

做好日志、监控和快照补漏工作,下次遇到问题时可以定位根因;
针对问题的症状做好实时报警,确保出现问题后可以第一时间发现;
考虑做一套热备的方案,出现问题后可以第一时间切换到热备系统快速解决问题,同时又可以保留老系统的现场。

总结

分析问题必须讲理

靠猜是猜不出来的,需要提前做好基础监控的建设。监控的话,需要在基础运维层、应用层、业务层等多个层次进行。定位问题的时候,我们同样需要参照多个监控层的指标表现综合分析。

定位问题要先对原因进行大致分类

比如是内部问题还是外部问题、CPU相关问题还是内存相关问题、仅仅是A接口的问题还是整个应用的问题,然后再去进一步细化探索,一定是从大到小来思考问题;在追查问题遇到瓶颈的时候,我们可以先退出细节,再从大的方面捋一下涉及的点,再重新来看问题。

经验很重要

遇到重大问题的时候,往往也需要根据直觉来第一时间找到最有可能的点,这里甚至有运气成分。我还和你分享了我的九条经验,建议你在平时解决问题的时候多思考、多总结,提炼出更多自己分析问题的套路和拿手工具。

定位到问题原因后,要做好复盘回溯。每次故障的解决都是宝贵经验,复盘不止是记录问题,更是为了架构优化。
复盘可重点关注如下:

  • 记录完整的时间线、处理措施、上报流程等信息
  • 分析问题的根本原因
  • 给出短、中、长期改进方案,包括但不限于代码改动、SOP、流程,并记录跟踪每一个方案进行闭环
  • 定期组织团队回顾过去的故障

本文转载自: 掘金

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

CompletableFuture 使用(一)

发表于 2021-11-23

如果我们有以下需求:

  1. 从数据库的User表获取所有用户的id
  2. 根据用户的id查询所有用户相关联的信息
  3. 获取信息后,处理信息
  4. 等待所有用户的信息处理完成后,完成下一步动作
  5. 在处理过程中主线程无需等待整个信息的处理完成

通过Future来处理:

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
scss复制代码public class CompletableFutureTest {
public static final String USER_MSG_FORMAT = "用户信息%d";
public static final String USER_MSG_START_FORMAT = "正在获取用户信息%d的信息";
public static final String USER_MSG_END_FORMAT = "获取结束";

private static ExecutorService executor = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws Exception {
Future<List<Integer>> userIdListFuture = executor.submit(CompletableFutureTest::getUserIdList);
mainThreadDo();
List<Integer> userIdList = userIdListFuture.get();
List<Future<String>> userMsg = new ArrayList<>();
userIdList.forEach(userId -> userMsg.add(executor.submit(() -> getUserMsg(userId))));
mainThreadDo();
for (Future<String> res : userMsg) {
System.out.println(Thread.currentThread().getName() + " " + res.get());
}
mainThreadDo();
executor.shutdown();
executor.awaitTermination(1, TimeUnit.DAYS);
System.out.println(Thread.currentThread().getName() + " 业务处理完毕");
}

public static void mainThreadDo() {
System.out.println(Thread.currentThread().getName() + " 主线程开始执行别的业务逻辑");
sleep();
System.out.println(Thread.currentThread().getName() + " 主线程结束执行别的业务逻辑");
}

public static String getUserMsg(Integer userId) {
System.out.println(Thread.currentThread().getName() + " " + String.format(USER_MSG_START_FORMAT, userId));
sleep();
System.out.println(Thread.currentThread().getName() + " " + USER_MSG_END_FORMAT);
return String.format(USER_MSG_FORMAT, userId);
}

public static List<Integer> getUserIdList() {
return Lists.newArrayList(1, 2, 3, 4, 5);
}

//模拟数据库延时操作
public static void sleep() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
css复制代码main 主线程开始执行别的业务逻辑
main 主线程结束执行别的业务逻辑
main 主线程开始执行别的业务逻辑
pool-1-thread-4 正在获取用户信息3的信息
pool-1-thread-3 正在获取用户信息2的信息
pool-1-thread-2 正在获取用户信息1的信息
pool-1-thread-5 正在获取用户信息4的信息
pool-1-thread-6 正在获取用户信息5的信息
main 主线程结束执行别的业务逻辑
pool-1-thread-4 获取结束
pool-1-thread-5 获取结束
pool-1-thread-6 获取结束
pool-1-thread-3 获取结束
pool-1-thread-2 获取结束
main 用户信息1
main 用户信息2
main 用户信息3
main 用户信息4
main 用户信息5
main 主线程开始执行别的业务逻辑
main 主线程结束执行别的业务逻辑
main 业务处理完毕

image.png

通过 CompletableFuture 来处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scss复制代码public static void main(String[] args) throws Exception {
System.out.println(Thread.currentThread().getName() + " 业务开始完毕");
List<CompletableFuture<Void>> completableFutureList = CompletableFuture.supplyAsync(CompletableFutureTest2::getUserIdList)//将getUserIdList封装成Callable放入ForkJoinPool中执行
.get()//等待执行结果
.stream()
.map(userId -> CompletableFuture.supplyAsync(() -> getUserMsg(userId)).thenAccept(System.out::println))//将stream List<UserId> 转换为List<CompletableFuture>
.collect(Collectors.toList());
mainThreadDo();
//将所有的List<CompletableFuture>包装成一个 CompletableFuture
CompletableFuture<Void> completableFuture = CompletableFuture.allOf(completableFutureList.toArray(new CompletableFuture[0]));
completableFuture.thenRun(() -> mainThreadDo());//将封装的一个CompletableFuture安装一个执行结束后的回调动作mainThreadDo()
completableFuture.get();//等待completableFuture执行完成
System.out.println(Thread.currentThread().getName() + " 业务处理完毕");
}

CompletableFuture执行回调示例图
image.png

本文转载自: 掘金

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

go语言笔记 程序基础二

发表于 2021-11-23

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

最近在跟着小册学习go 语言原理与实践。因为没有go语言基础,就跟着菜鸟教程学习了下go的基础语法go语言基础-菜鸟教程。go语言基础笔记,会有两篇,这是第二篇。

1 数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码package main

import "fmt"

func main() {
var n [10]int /* n 是一个长度为 10 的数组 */
var i,j int

/* 为数组 n 初始化元素 */
for i = 0; i < 10; i++ {
n[i] = i + 100 /* 设置元素为 i + 100 */
}

/* 输出每个数组元素的值 */
for j = 0; j < 10; j++ {
fmt.Printf("Element[%d] = %d\n", j, n[j] )
}
}

result

1
2
3
4
5
6
7
8
9
10
css复制代码Element[0] = 100
Element[1] = 101
Element[2] = 102
Element[3] = 103
Element[4] = 104
Element[5] = 105
Element[6] = 106
Element[7] = 107
Element[8] = 108
Element[9] = 109
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
go复制代码package main

import "fmt"

func main() {
var i,j,k int
// 声明数组的同时快速初始化数组
balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

/* 输出数组元素 */ ...
for i = 0; i < 5; i++ {
fmt.Printf("balance[%d] = %f\n", i, balance[i] )
}

balance2 := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
/* 输出每个数组元素的值 */
for j = 0; j < 5; j++ {
fmt.Printf("balance2[%d] = %f\n", j, balance2[j] )
}

// 将索引为 1 和 3 的元素初始化
balance3 := [5]float32{1:2.0,3:7.0}
for k = 0; k < 5; k++ {
fmt.Printf("balance3[%d] = %f\n", k, balance3[k] )
}
}

result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
css复制代码balance[0] = 1000.000000
balance[1] = 2.000000
balance[2] = 3.400000
balance[3] = 7.000000
balance[4] = 50.000000
balance2[0] = 1000.000000
balance2[1] = 2.000000
balance2[2] = 3.400000
balance2[3] = 7.000000
balance2[4] = 50.000000
balance3[0] = 0.000000
balance3[1] = 2.000000
balance3[2] = 0.000000
balance3[3] = 7.000000
balance3[4] = 0.000000

2 指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码package main

import "fmt"

func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */

ip = &a /* 指针变量的存储地址 */

fmt.Printf("a 变量的地址是: %x\n", &a )

/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )

/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}

result

1
2
3
css复制代码a 变量的地址是: 20818a220
ip 变量储存的指针地址: 20818a220
*ip 变量的值: 20
  • 空指针
1
2
3
4
5
6
7
8
9
10
11
12
go复制代码package main

import "fmt"

func main() {
var ptr *int

fmt.Printf("ptr 的值为 : %x\n", ptr )
}
//空指针判断
//if(ptr != nil) /* ptr 不是空指针 */
//if(ptr == nil) /* ptr 是空指针 */

result

1
复制代码ptr 的值为 : 0

3 结构体

  • 定义结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main

import "fmt"

type Books struct {
title string
author string
subject string
book_id int
}


func main() {

// 创建一个新的结构体
fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})

// 也可以使用 key => value 格式
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})

// 忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
}

result

1
2
3
复制代码{Go 语言 www.runoob.com Go 语言教程 6495407}
{Go 语言 www.runoob.com Go 语言教程 6495407}
{Go 语言 www.runoob.com 0}
  • 访问结构体成员
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
go复制代码package main

import "fmt"

type Books struct {
title string
author string
subject string
book_id int
}

func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */

/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407

/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700

/* 打印 Book1 信息 */
fmt.Printf( "Book 1 title : %s\n", Book1.title)
fmt.Printf( "Book 1 author : %s\n", Book1.author)
fmt.Printf( "Book 1 subject : %s\n", Book1.subject)
fmt.Printf( "Book 1 book_id : %d\n", Book1.book_id)

/* 打印 Book2 信息 */
fmt.Printf( "Book 2 title : %s\n", Book2.title)
fmt.Printf( "Book 2 author : %s\n", Book2.author)
fmt.Printf( "Book 2 subject : %s\n", Book2.subject)
fmt.Printf( "Book 2 book_id : %d\n", Book2.book_id)
}

result

1
2
3
4
5
6
7
8
yaml复制代码Book 1 title : Go 语言
Book 1 author : www.runoob.com
Book 1 subject : Go 语言教程
Book 1 book_id : 6495407
Book 2 title : Python 教程
Book 2 author : www.runoob.com
Book 2 subject : Python 语言教程
Book 2 book_id : 6495700
  • 结构体作为函数传参
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
go复制代码package main

import "fmt"

type Books struct {
title string
author string
subject string
book_id int
}

func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */

/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407

/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700

/* 打印 Book1 信息 */
printBook(Book1)

/* 打印 Book2 信息 */
printBook(Book2)
}

func printBook( book Books ) {
fmt.Printf( "Book title : %s\n", book.title)
fmt.Printf( "Book author : %s\n", book.author)
fmt.Printf( "Book subject : %s\n", book.subject)
fmt.Printf( "Book book_id : %d\n", book.book_id)
}

result

1
2
3
4
5
6
7
8
yaml复制代码Book title : Go 语言
Book author : www.runoob.com
Book subject : Go 语言教程
Book book_id : 6495407
Book title : Python 教程
Book author : www.runoob.com
Book subject : Python 语言教程
Book book_id : 6495700
  • 结构体指针
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
go复制代码package main

import "fmt"

type Books struct {
title string
author string
subject string
book_id int
}

func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */

/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407

/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700

/* 打印 Book1 信息 */
printBook(&Book1)

/* 打印 Book2 信息 */
printBook(&Book2)
}
func printBook( book *Books ) {
fmt.Printf( "Book title : %s\n", book.title)
fmt.Printf( "Book author : %s\n", book.author)
fmt.Printf( "Book subject : %s\n", book.subject)
fmt.Printf( "Book book_id : %d\n", book.book_id)
}

result

1
2
3
4
5
6
7
8
yaml复制代码Book title : Go 语言
Book author : www.runoob.com
Book subject : Go 语言教程
Book book_id : 6495407
Book title : Python 教程
Book author : www.runoob.com
Book subject : Python 语言教程
Book book_id : 6495700

4 切片

  • len()和cap()函数
1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码package main

import "fmt"

func main() {
var numbers = make([]int,3,5)

printSlice(numbers)
}

func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

result

1
go复制代码len=3 cap=5 slice=[0 0 0]
  • 空(nil)切片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package main

import "fmt"

func main() {
var numbers []int

printSlice(numbers)

if(numbers == nil){
fmt.Printf("切片是空的")
}
}

func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

result

1
2
go复制代码len=0 cap=0 slice=[]
切片是空的
  • 切片截取
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
go复制代码package main

import "fmt"

func main() {
/* 创建切片 */
numbers := []int{0,1,2,3,4,5,6,7,8}
printSlice(numbers)

/* 打印原始切片 */
fmt.Println("numbers ==", numbers)

/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4])

/* 默认下限为 0*/
fmt.Println("numbers[:3] ==", numbers[:3])

/* 默认上限为 len(s)*/
fmt.Println("numbers[4:] ==", numbers[4:])

numbers1 := make([]int,0,5)
printSlice(numbers1)

/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
number2 := numbers[:2]
printSlice(number2)

/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
number3 := numbers[2:5]
printSlice(number3)

}

func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

result

1
2
3
4
5
6
7
8
ini复制代码len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
len=3 cap=7 slice=[2 3 4]
  • append() 和 copy() 函数
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
go复制代码package main

import "fmt"

func main() {
var numbers []int
printSlice(numbers)

/* 允许追加空切片 */
numbers = append(numbers, 0)
printSlice(numbers)

/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers)

/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
printSlice(numbers)

/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)

/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)
}

func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

result

1
2
3
4
5
go复制代码len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]

5 range

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
go复制代码package main
import "fmt"
func main() {
//这是我们使用range去求一个slice的和。使用数组跟这个很类似
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
//在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
//range也可以用在map的键值对上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
//range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
for i, c := range "go" {
fmt.Println(i, c)
}
}

result

1
2
3
4
5
6
makefile复制代码sum: 9
index: 1
a -> apple
b -> banana
0 103
1 111

6 Map

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
go复制代码package main

import "fmt"

func main() {
var countryCapitalMap map[string]string /*创建集合 */
countryCapitalMap = make(map[string]string)

/* map插入key - value对,各个国家对应的首都 */
countryCapitalMap [ "France" ] = "巴黎"
countryCapitalMap [ "Italy" ] = "罗马"
countryCapitalMap [ "Japan" ] = "东京"
countryCapitalMap [ "India " ] = "新德里"

/*使用键输出地图值 */
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [country])
}

/*查看元素在集合中是否存在 */
capital, ok := countryCapitalMap [ "American" ] /*如果确定是真实的,则存在,否则不存在 */
/*fmt.Println(capital) */
/*fmt.Println(ok) */
if (ok) {
fmt.Println("American 的首都是", capital)
} else {
fmt.Println("American 的首都不存在")
}
}

result

1
2
3
4
5
复制代码France 首都是 巴黎
Italy 首都是 罗马
Japan 首都是 东京
India 首都是 新德里
American 的首都不存在
  • delete() 函数
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
go复制代码package main

import "fmt"

func main() {
/* 创建map */
countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}

fmt.Println("原始地图")

/* 打印地图 */
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}

/*删除元素*/ delete(countryCapitalMap, "France")
fmt.Println("法国条目被删除")

fmt.Println("删除元素后地图")

/*打印地图*/
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}
}

result

1
2
3
4
5
6
7
8
9
10
sql复制代码原始地图
India 首都是 New delhi
France 首都是 Paris
Italy 首都是 Rome
Japan 首都是 Tokyo
法国条目被删除
删除元素后地图
Italy 首都是 Rome
Japan 首都是 Tokyo
India 首都是 New delhi

7 递归函数

  • 阶乘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main

import "fmt"

func Factorial(n uint64)(result uint64) {
if (n > 0) {
result = n * Factorial(n-1)
return result
}
return 1
}

func main() {
var i int = 15
fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i)))
}

result

1
复制代码15 的阶乘是 1307674368000
  • 斐波那契数列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package main

import "fmt"

func fibonacci(n int) int {
if n < 2 {
return n
}
return fibonacci(n-2) + fibonacci(n-1)
}

func main() {
var i int
for i = 0; i < 10; i++ {
fmt.Printf("%d\t", fibonacci(i))
}
}

result

1
复制代码0    1    1    2    3    5    8    13    21    34

8 数据类型转换

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码package main

import "fmt"

func main() {
var sum int = 17
var count int = 5
var mean float32

mean = float32(sum)/float32(count)
fmt.Printf("mean 的值为: %f\n",mean)
}

result

1
matlab复制代码mean 的值为: 3.400000
1
go复制代码

result

1
复制代码

9 语言接口

  • 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
go复制代码package main

import (
"fmt"
)

type Phone interface {
call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}

func main() {
var phone Phone

phone = new(NokiaPhone)
phone.call()

phone = new(IPhone)
phone.call()

}

result

1
2
css复制代码I am Nokia, I can call you!
I am iPhone, I can call you!

10 错误处理

  • 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
go复制代码package main

import (
"fmt"
)

// 定义一个 DivideError 结构
type DivideError struct {
dividee int
divider int
}

// 实现 `error` 接口
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}

// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}

}

func main() {

// 正常情况
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}

}

result

1
2
3
4
5
yaml复制代码100/10 =  10
errorMsg is:
Cannot proceed, the divider is zero.
dividee: 100
divider: 0

11 并发

  • Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。

goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码package main

import (
"fmt"
"time"
)

func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}

func main() {
go say("world")
say("hello")
}

result

1
2
3
4
5
6
7
8
9
10
复制代码world
hello
hello
world
world
hello
hello
world
world
hello
  • 通道(channel)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码package main

import "fmt"

func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}

func main() {
s := []int{7, 2, 8, -9, 4, 0}

c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收

fmt.Println(x, y, x+y)
}

result

1
diff复制代码-5 17 12
  • 通道缓冲区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码package main

import "fmt"

func main() {
// 这里我们定义了一个可以存储整数类型的带缓冲通道
// 缓冲区大小为2
ch := make(chan int, 2)

// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2

// 获取这两个数据
fmt.Println(<-ch)
fmt.Println(<-ch)
}

result

1
2
复制代码1
2
  • 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
go复制代码package main

import (
"fmt"
)

func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}

func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
// 会结束,从而在接收第 11 个数据的时候就阻塞了。
for i := range c {
fmt.Println(i)
}
}

result

1
2
3
4
5
6
7
8
9
10
复制代码0
1
1
2
3
5
8
13
21
34

本文转载自: 掘金

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

1…222223224…956

开发者博客

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