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

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


  • 首页

  • 归档

  • 搜索

495 提莫攻击

发表于 2021-11-10

题目描述

在《英雄联盟》的世界中,有一个叫 “提莫” 的英雄。他的攻击可以让敌方英雄艾希(编者注:寒冰射手)进入中毒状态。

当提莫攻击艾希,艾希的中毒状态正好持续 duration 秒。

正式地讲,提莫在 t 发起发起攻击意味着艾希在时间区间 [t, t + duration - 1](含 t 和 t + duration - 1)处于中毒状态。如果提莫在中毒影响结束 前 再次攻击,中毒状态计时器将会 重置 ,在新的攻击之后,中毒影响将会在 duration 秒后结束。

给你一个 非递减 的整数数组 timeSeries ,其中 timeSeries[i] 表示提莫在 timeSeries[i] 秒时对艾希发起攻击,以及一个表示中毒持续时间的整数 duration 。

返回艾希处于中毒状态的 总 秒数。

原题

题解分析

本题主要在于判断每次攻击的时候,当前是否是中毒状态。从而决定上次的中毒时间。

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码public int findPoisonedDuration(int[] timeSeries, int duration) {
int result = 0;
//遍历所有攻击
for (int i = 0; i < timeSeries.length - 1; i++) {
// 得到下一次攻击 和 本次攻击的 之间的时间差值,然后与 中毒持续时间 进行比较
int item = timeSeries[i + 1] - timeSeries[i];
// 如果时间差值 小于等于 中毒持续时间,那么下一次攻击时,中毒状态会重置,故本次中毒只会持续到下一次中毒的时候
if (item <= duration) {
result = result + item;
} else {
//如果时间差值 大于 中毒持续时间,那么本次中毒会持续完整的时间。
result = result + duration;
}
}
//最后一次攻击,会持续完整时间
result = result + timeSeries[timeSeries.length - 1];
return result;
}

本文转载自: 掘金

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

Flowable最新版670入门篇之基于REST API

发表于 2021-11-10

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

作者:汤圆

个人博客:javalover.cc

前言

上一篇学习了基于JavaApi的入门例子,参数主要是通过命令行传入;

这一篇学习下基于REST API的入门例子;

这里我们主要是学习如果使用现有的REST例子,因为官方已经提供了完整版的war包供我们使用

其中使用的流程跟上一篇的基本一致,如下面的目录所示:

目录

  1. 下载 flowable-rest.war 包
  2. 启动 flowable-rest 应用
  3. 部署一个流程定义
  4. 启动流程实例
  5. 获取任务列表
  6. 查询历史记录

正文

1. 下载 flowable-rest.war 包

下载地址:

下载解压后的目录如下所示:flowable-rest.war 就在wars目录中

image-20211108162709504

2. 启动 flowable-rest 应用

进入到wars目录,通过命令行启动:java -jar flowable-rest.war

启动后,显示如下:

image-20211108162958886

当最后一行显示:INFO [main] org.apache.catalina.startup.Catalina.start Server startup in xyz ms时,说明启动成功

此时我们可以通过一个简单的请求来确认:

1
bash复制代码curl --user rest-admin:test http://localhost:8080/flowable-rest/service/management/engine

image-20211108163337219

这里所有的请求认证方式都为 basic authentication

用户名/密码:reset-admin/test

返回json对象,内容包括flowable的版本号等,说明启动成功

3. 部署流程定义

下面开始,基本跟上一篇的步骤一致,就是请求方式不同;

上一篇是直接在Java程序中,基于Java API;

这里我们需要把流程定义文件bpmn.xml,先上传到数据库,如下所示:请求格式为 multipart/formdata

1
bash复制代码curl --user rest-admin:test -F "file=@holiday-request.bpmn20.xml" http://localhost:8080/flowable-rest/service/repository/deployments

image-20211108164349935

下面我们获取流程定义的列表,来查看刚才上传的流程定义:

1
bash复制代码curl --user rest-admin:test http://localhost:8080/flowable-rest/service/repository/process-definitions

image-20211108164557829

返回的列表中,有一个key为holidayRequest的,就是我们刚才上传的流程定义

我们流程定义中的process id="holidayRequest"对应的就是这里的key

4. 启动流程实例

启动时,需设定好流程变量;

上一篇是通过命令行输入的,这次我们直接传json对象,如下所示:

1
bash复制代码curl --user rest-admin:test -H "Content-Type: application/json" -X POST -d '{ "processDefinitionKey":"holidayRequest", "variables": [ { "name":"employee", "value": "John Doe" }, { "name":"nrOfHolidays", "value": 7 }]}' http://localhost:8080/flowable-rest/service/runtime/process-instances

image-20211108165132398

5. 获取任务列表

这里我们查询属于 经理 的任务列表

1
bash复制代码curl --user rest-admin:test -H "Content-Type: application/json" -X POST -d '{ "candidateGroup" : "managers" }' http://localhost:8080/flowable-rest/service/query/tasks

image-20211108165649623

这里查询到的任务就是流程定义中的如下用户任务:

<userTask id="approveTask" name="Approve or reject request" flowable:candidateGroups="managers"/>

6. 执行用户任务

下面我们就可以调用接口,来执行用户任务;

1
bash复制代码curl --user rest-admin:test -H "Content-Type: application/json" -X POST -d '{ "action" : "complete", "variables" : [ { "name" : "approved", "value" : true} ]  }' http://localhost:8080/flowable-rest/service/runtime/tasks/25

image-20211108171101080

上面请求url中 tasks后面的参数就是 任务id,上面获取任务列表时有返回

执行后,发现报错,如上图所示,提示找不到类;

这个类就是 请求同意后,执行的类,在bpmn.xml中有配置,如下所示:

1
2
xml复制代码<serviceTask id="externalSystemCall" name="Enter holidays in external system"
flowable:class="org.flowable.CallExternalSystemDelegate"/>

下面是缺失的类源代码:CallExternalSystemDelegate.java

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码package org.flowable;

import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;

public class CallExternalSystemDelegate implements JavaDelegate {

public void execute(DelegateExecution execution) {
System.out.println("Calling the external system for employee "
+ execution.getVariable("employee"));
}

}

那怎么添加呢?

这里我们没按照官方的教程,去把缺失的类打包然后部署到lib下(因为遇到了各种各样奇奇怪怪的错误);

而是直接将class文件,部署到war项目中,步骤如下:

  1. 解压flowable-rest.war:jar xf .\flowable-rest.war
  2. 添加缺失的class到org/flowable目录下
  3. 删除 flowable-rest.war: rm .\flowable-rest.war(删除之前记得备份)
  4. 重新打包成jar:jar cf0M flowable-rest.jar *
  5. 启动:java -jar flowable-rest.jar

然后再次访问刚才出错的接口,接口返回200,但是没有返回值;

但是后台有打印日志,如下所示:

image-20211108194359774

这里我的名字有中文(汤圆学Java),所以前面的没打印出来,这个不重要;

至此,一个基于REST API 的请求流程就算告一段落了;

更多的API可以参考官网REST API

总结

本篇主要介绍了通过Rest风格来调用flowable的API,核心流程跟上一篇是一样的;

本文转载自: 掘金

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

Python 爬虫小课 1-9 宝妈程序媛福利-育儿网问答数

发表于 2021-11-10

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

🌹 橡皮擦 叨叨 🌹
本专栏为爬虫小课系列,周更 3+篇,专栏合计 9 篇文章,本专栏所有案例都将采用 requests 库编写,通过 9 个案例,让你深入理解 requests 库。
以上就是本系列专栏的核心目标。

本系列课程需要一定的 Python 语法基础,数据匹配将采用 Python 自带的 re 模块,故对正则表达式有一定的基础要求。
关于爬取环境的需求,Python3.5 以上版本,安装 requests 模块。

爬虫前的分析

类别页面分析

本次爬虫小课要爬取的的网站为育儿网(ask.ci123.com/) 的问答模块,我们要采集一下红框内的资料。

Python 爬虫小课 1-12 宝妈程序媛福利-育儿网问答数据抓取

对于该网站涉及的问题类型非常多,具体分类可以通过上述链接左侧的菜单获取到。如下图所示区域:

Python 爬虫小课 1-12 宝妈程序媛福利-育儿网问答数据抓取

在这里需要略微分析一下,分类地址的规律,如果没有规律,那第一步就先获取一下所有的分类地址,鼠标点击各链接发现,分类列表页链接如下:

1
2
3
4
txt复制代码http://ask.ci123.com/categories/show/2
http://ask.ci123.com/categories/show/3
http://ask.ci123.com/categories/show/4
http://ask.ci123.com/categories/show/{类别ID}

在这里先不要下结论,说 ID 是依次递增的,写爬虫程序如果过早的假定一定的规则,很容易出现数据丢失的情况,所以尽量都尝试一遍。

在这里也可以直接通过查看网页源码,看一下所有的地址,当然看完之后还是为了我们可以爬取到。最终查阅到所有的地址都为http://ask.ci123.com/categories/show/{类别ID} 形式,只是最后的类别 ID 不是连续的。到这里问题分类分析完毕。

Python 爬虫小课 1-12 宝妈程序媛福利-育儿网问答数据抓取

问题列表页面分析

下面需要寻找列表页相关规律,点击任意类别之后,可以查阅到,页面数据样式都如下图所示:

Python 爬虫小课 1-12 宝妈程序媛福利-育儿网问答数据抓取

首先要做的第一件事请,就是查找分页规律,找到分页区域,鼠标依次点击分页,获取不同的分页地址。

Python 爬虫小课 1-12 宝妈程序媛福利-育儿网问答数据抓取

最后找到其规律链接地址如下:

1
txt复制代码http://ask.ci123.com/categories/show/4/all?p={页码}

有页码规律还不够,还需要找到末页,在源码中简单检索,找到末页对应的页码即可。

Python 爬虫小课 1-12 宝妈程序媛福利-育儿网问答数据抓取

到此爬虫前的分析分析完毕了,下面开始进行爬虫逻辑编码环节,也就是整理自己的思路。

逻辑编码(伪代码)

育儿网爬虫分为如下步骤:

  1. 通过 ask.ci123.com/ 页面,获取所有的分类页面地址
  2. 循环所有的分类页面地址
  3. 获取每个分类对应的列表页面,并获取总页码
  4. 从一开始循环到总页码
  5. 上一步循环过程中过去每一页待爬取的数据

思路整理完毕,编码环节其实就是一个简单的实现过程。

爬虫正式编码

request 库 get 方法说明

对于 requests 库来说,导入并快速应用是非常容易的,先通过抓取分类页面源码来看一下基本使用。

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码import requests

url = "http://ask.ci123.com/"

# 抓取分类页面
def get_category():
res = requests.get("http://ask.ci123.com/")
print(res.text)


if __name__ == "__main__":
get_category()

以上代码中最核心的方法就是 requests.get()了,该方法为 requests 模块通过 get 方式获取网站源码,该方法中的参数说明如下:

必选参数 url

1
python复制代码requests.get(url="http://ask.ci123.com/")

传递 URL 参数

通过该参数可以构造出如下格式 https://www.baidu.com/s?wd=你好&rsv_spt=1&rsv_iqid=0x8dd347e100002e04。
格式如下:

1
2
3
4
python复制代码import requests
payload = {'key1': 'value1', 'key2': 'value2'}
res = requests.get(url="http://ask.ci123.com/", params=payload)
print(res.url)

key1 为键名,value1 为键值。

定制请求头

在爬虫爬取的过程中,我们将尽量将爬虫模拟成真实的用户通过浏览器访问网站,所以很多时候需要定制浏览器请求头。格式如下:

1
2
3
4
5
6
7
8
python复制代码import requests
payload = {'key1': 'value1', 'key2': 'value2'}
headers = {
'user-agent': 'Baiduspider-image+(+http://www.baidu.com/search/spider.htm)'
}
res = requests.get(url="http://ask.ci123.com/",
params=payload, headers=headers)
print(res.url)

其中 headers 中可以配置更多的内容,本篇博客不做展开,只需要先记住 headers 参数即可。

Cookie

Cookie 在很多爬虫程序中属于必备内容,这里有时会存储加密信息,有时会存储用户信息,格式如下:

1
2
3
4
5
6
7
8
9
python复制代码import requests
payload = {'key1': 'value1', 'key2': 'value2'}
headers = {
'user-agent': 'Baiduspider-image+(+http://www.baidu.com/search/spider.htm)'
}
cookies = dict(my_cookies='nodream')
res = requests.get(url="http://ask.ci123.com/",
params=payload, headers=headers, cookies=cookies)
print(res.text)

禁用重定向处理

有些网站会携带重定向源码,在爬取的时候需要禁止网格员自动跳转,代码如下:

1
python复制代码r = requests.get('http://github.com', allow_redirects=False)

超时

对于一个网络请求,有时会出现无法请求到的情况,这部分在官方手册高级部分有相应的说明,不过对于初学者可以先进行忽略超时的高级用法。

为防止服务器不能及时响应,大部分发至外部服务器的请求都应该带着 timeout 参数。在默认情况下,除非显式指定了 timeout 值,requests 是不会自动进行超时处理的。如果没有 timeout,你的代码可能会挂起若干分钟甚至更长时间。

常规代码如下:

1
2
3
4
5
6
7
8
9
python复制代码import requests
payload = {'key1': 'value1', 'key2': 'value2'}
headers = {
'user-agent': 'Baiduspider-image+(+http://www.baidu.com/search/spider.htm)'
}
cookies = dict(my_cookies='nodream')
res = requests.get(url="http://ask.ci123.com/",
params=payload, headers=headers, cookies=cookies, timeout=3)
print(res.text)

高级部分参数

对于 get 方法,还有一些参数,在后续的博客中我们可能会用到,例如:

  • SSL 证书验证 (verify)
  • 客户端证书(cert)
  • 事件钩子(hooks)
  • 自定义身份验证(auth)
  • 流式请求(stream)
  • 代理(proxies)

以上参数都会出现在 get 方法中,所以 requests 库是一个非常非常强大的库。

获取所有的分类页面地址

有了上述详细的说明,在使用 requests 库去获取网页中的内容就变得简单一些了。这里需要有 Python 基础知识中 re 模块的使用与正则表达式的基础。具体的爬取代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码import requests
import re

url = "http://ask.ci123.com/"
headers = {
'user-agent': 'Baiduspider-image+(+http://www.baidu.com/search/spider.htm)'
}
# 抓取分类页面
def get_category():
res = requests.get("http://ask.ci123.com/", headers=headers)
pattern = re.compile(
r'<li><a href="/categories/show/(\d+)">', re.S)
categories_ids = pattern.findall(res.text)
print(f"获取到的分类ID如下:",categories_ids)

if __name__ == "__main__":
get_category()

循环所有的分类页面地址

在上述代码中通过re库的 findall 方法获取了所有的分类编号,用来拼接后续的待爬取页面。获取到 IDS 之后,就可以通过循环的方式获取到所有的列表页面了,具体如下:

1
2
3
4
5
6
7
8
9
10
11
python复制代码# 抓取分类页面
def get_category():
res = requests.get("http://ask.ci123.com/", headers=headers)
pattern = re.compile(
r'<li><a href="/categories/show/(\d+)">', re.S)
categories_ids = pattern.findall(res.text)
print(f"获取到的分类ID如下:", categories_ids)
for cate in categories_ids:
# 下述代码中有get_list()函数对应的代码
get_list(cate)
time.sleep(1)

上述代码为了防止被反爬,需要增加一个延时处理,time.sleep()。

获取每个分类对应的列表页面,并获取总页码

打开列表页面,首要目的先获取到总页码,本次实现的案例获取的页码途径比较简单,在列表页存在一项,数据直接在源码中可以看到,故直接抓取即可。

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码def get_list(cate):
# 获取总页码,循环抓取所有页

res = requests.get(
f"http://ask.ci123.com/categories/show/{cate}", headers=headers)

pattern = re.compile(
r'<a class="list_last_page" href="/categories/show/\d+/all\?p=(\d+)"', re.S)
totle = pattern.search(res.text).group(1)
for page in range(1, int(totle)):
print(f"http://ask.ci123.com/categories/show/{cate}/all?p={page}")
time.sleep(0.2)

从 1 开始循环到总页码

本部分代码比较容易,已经在上述代码实现。结果如图所示:

Python 爬虫小课 1-12 宝妈程序媛福利-育儿网问答数据抓取

本案例收尾环节

后续的内容就变得非常容易了,对每页数据进行分析,并进行存储数据操作,下述代码未编写存储部分,抓取部分代码已经填写完整,其中存在一个非常大的正则表达式,可以参考一下,如果爬取数据不是很严格,大量的使用.*\s这些常见元字符即可。

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
python复制代码import requests
import re
import time

url = "http://ask.ci123.com/"
headers = {
'user-agent': 'Baiduspider-image+(+http://www.baidu.com/search/spider.htm)'
}


def get_detail(text):
# 该函数实现解析页面数据,之后存储数据
pattern = re.compile(r'<li>[.\s]*<a href="/questions/show/(\d+)/" title="(.*?)" class="list_title" target="_blank" >.*?</a>\s*<span class="list_asw">(\d+)<font>.*?</font></span>\s*<a class="list_author" href="/users/show/\d+" title=".*?">(.*?)</a>\s*<span class="list_time">(.*?)</span>\s*</li>')
data = pattern.findall(text)
print(data)
# 数据存储代码不在编写


def get_list(cate):
# 获取总页码,循环抓取所有页

res = requests.get(
f"http://ask.ci123.com/categories/show/{cate}", headers=headers)

pattern = re.compile(
r'<a class="list_last_page" href="/categories/show/\d+/all\?p=(\d+)"', re.S)
totle = pattern.search(res.text).group(1)
for page in range(1, int(totle)):
print(f"http://ask.ci123.com/categories/show/{cate}/all?p={page}")
res = requests.get(
f"http://ask.ci123.com/categories/show/{cate}/all?p={page}", headers=headers)

time.sleep(0.2)
# 调取列表页数据提取函数
get_detail(res.text)


# 抓取分类页面
def get_category():
res = requests.get("http://ask.ci123.com/", headers=headers)
pattern = re.compile(
r'<li><a href="/categories/show/(\d+)">', re.S)
categories_ids = pattern.findall(res.text)
print(f"获取到的分类ID如下:", categories_ids)
for cate in categories_ids:
get_list(cate)
time.sleep(1)


if __name__ == "__main__":
get_category()

本文转载自: 掘金

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

在 Java Spring Boot 项目中使用结构化日志节

发表于 2021-11-10

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

【注】本文译自: Saving Time with Structured Logging - Reflectoring

日志记录是调查事件和了解应用程序中发生的事情的终极资源。每个应用程序都有某种类型的日志。

然而,这些日志通常很混乱,分析它们需要付出很多努力。在本文中,我们将研究如何利用结构化日志来大大增加日志的价值。

我们将通过一些非常实用的技巧来提高应用程序日志数据的价值,并使用 Logz.io 作为日志平台来查询日志。

代码示例

本文附有 GitHub 上的工作代码示例。

什么是结构化日志?

“正常”日志是非结构化的。它们通常包含一个消息字符串:

1
shell复制代码2021-08-08 18:04:14.721 INFO 12402 --- [ main] i.r.s.StructuredLoggingApplication : Started StructuredLoggingApplication in 0.395 seconds (JVM running for 0.552)

此消息包含我们在调查事件或分析问题时希望获得的所有信息:

  • 日志事件的日期
  • 创建日志事件的记录器的名称,以及
  • 日志消息本身。
    所有信息都在该日志消息中,**但很难查询这些信息!**由于所有信息都在一个字符串中,如果我们想从日志中获取特定信息,就必须解析和搜索这个字符串。

例如,如果我们只想查看特定记录器的日志,则日志服务器必须解析所有日志消息,检查它们是否具有识别记录器的特定模式,然后根据所需的记录器过滤日志消息。

结构化日志包含相同的信息,但采用结构化形式而不是非结构化字符串。通常,结构化日志以 JSON 格式呈现:

1
2
3
4
5
6
7
json复制代码{
"timestamp": "2021-08-08 18:04:14.721",
"level": "INFO",
"logger": "io.reflectoring....StructuredLoggingApplication",
"thread": "main",
"message": "Started StructuredLoggingApplication ..."
}

这种 JSON 结构允许日志服务器有效地存储,更重要的是检索日志。

例如,现在可以通过 timestamp 或 logger 轻松过滤日志,而且搜索比解析特定模式的字符串更有效。

但是结构化日志的价值并不止于此:我们可以根据需要向结构化日志事件中添加任何自定义字段! 我们可以添加上下文信息来帮助我们识别问题,或者我们可以向日志添加指标。

凭借我们现在触手可及的所有数据,我们可以创建强大的日志查询和仪表板,即使我们刚在半夜醒来调查事件,我们也能找到所需的信息。

现在让我们看几个用例,它们展示了结构化日志记录的强大功能。

为所有日志事件添加代码路径

我们首先要看的是代码路径。每个应用程序通常有几个不同的路径,传入请求可以通过应用程序。考虑这个图:

Java Spring Boot 项目中使用结构化日志节省时间
此示例具有(至少)三种不同的代码路径,传入请求可以采用这些路径:

  • 用户代码路径:用户正在从他们的浏览器使用应用程序。浏览器向 Web 控制器发送请求,控制器调用领域代码。
  • 第三方系统代码路径:应用程序的 HTTP API 也从第三方系统调用。在这个例子中,第三方系统调用与用户浏览器相同的 web 控制器。
  • 计时器代码路径:与许多应用程序一样,此应用程序有一些由计时器触发的计划任务。
    这些代码路径中的每一个都可以具有不同的特征。域服务涉及所有三个代码路径。在涉及域服务错误的事件期间,了解导致错误的代码路径将大有帮助!

如果我们不知道代码路径,我们很容易在事件调查期间做出毫无结果的猜测。

所以,我们应该将代码路径添加到日志中!以下是我们如何使用 Spring Boot 做到这一点。

为传入的 Web 请求添加代码路径

在 Java 中,SLF4J 日志库提供了 MDC 类(消息诊断上下文)。这个类允许我们向在同一线程中发出的所有日志事件添加自定义字段。

要为每个传入的 Web 请求添加自定义字段,我们需要构建一个拦截器,在每个请求的开头添加 codePath 字段,甚至在我们的 Web 控制器代码执行之前。

我们可以通过实现 HandlerInterceptor 接口来做到这一点:

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
java复制代码public class LoggingInterceptor implements HandlerInterceptor {


@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {


if (request.getHeader("X-CUSTOM-HEADER") != null) {
MDC.put("codePath", "3rdParty");
} else {
MDC.put("codePath", "user");
}


return true;
}


@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
MDC.remove("codePath");
}
}

在 preHandle() 方法中,我们调用 MDC.put() 将 codePath 字段添加到所有日志事件中。如果请求包含标识请求来自第三方方系统的标头,我们将代码路径设置为 3rdParty,否则,我们假设请求来自用户的浏览器。

根据应用的不同,这里的逻辑可能会有很大的不同,当然,这只是一个例子。

在 postHandle() 方法中,我们不应该忘记调用 MDC.remove() 再次删除所有先前设置的字段,否则线程仍会保留这些字段,即使它返回到线程池,以及下一个请求 由该线程提供服务的那些字段可能仍然设置为错误的值。

要激活拦截器,我们需要将其添加到 InterceptorRegistry 中:

1
2
3
4
5
6
7
8
9
java复制代码@Componentpublic
class WebConfigurer implements WebMvcConfigurer {


@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggingInterceptor());
}
}

就是这样。在传入日志事件的线程中发出的所有日志事件现在都具有 codePath 字段。

如果任何请求创建并启动子线程,请确保在新线程生命周期开始时调用 MDC.put()。

在计划作业中添加代码路径

在 Spring Boot 中,我们可以通过使用 @Scheduled 和 @EnableScheduling 注解轻松创建计划作业。

要将代码路径添加到日志中,我们需要确保调用 MDC.put() 作为调度方法中的第一件事:

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
java复制代码@Componentpublic
class Timer {


private final DomainService domainService;


private static final Logger logger = LoggerFactory.getLogger(Timer.class);


public Timer(DomainService domainService) {
this.domainService = domainService;
}


@Scheduled(fixedDelay = 5000)
void scheduledHello() {
MDC.put("codePath", "timer");
logger.info("log event from timer");
// do some actual work
MDC.remove("codePath");
}


}

这样,从执行调度方法的线程发出的所有日志事件都将包含字段 codePath。我们也可以创建我们自己的 @Job 注解或类似的注解来为我们完成这项工作,但这超出了本文的范围。

为了使预定作业的日志更有价值,我们可以添加其他字段:

  • job_status:指示作业是否成功的状态。
  • job_id:已执行作业的 ID。
  • job_records_processed:如果作业进行一些批处理,它可以记录处理的记录数。
  • ……
    通过日志中的这些字段,我们可以在日志服务器获取到很多有用的信息!

将用户 ID 添加到用户启动的日志事件

典型 Web 应用程序中的大部分工作是在来自用户浏览器的 Web 请求中完成的,这些请求会触发应用程序中的线程,为浏览器创建响应。

想象一下发生了一些错误,日志中的堆栈跟踪显示它与特定的用户配置有关。但是我们不知道请求来自哪个用户!

为了缓解这种情况,在用户触发的所有日志事件中包含某种用户 ID 是非常有帮助的。

由于我们知道传入的 Web 请求大多直接来自用户的浏览器,因此我们可以在创建的同一个 LoggingInterceptor 中添加 username 字段以添加 codePath 字段:

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
java复制代码public class LoggingInterceptor implements HandlerInterceptor {


@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {


Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();


if (principal instanceof UserDetails) {
String username = ((UserDetails) principal).getUsername();
MDC.put("username", username);
} else {
String username = principal.toString();
MDC.put("username", username);
}


return true;
}


@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
MDC.remove("username");
}
}

这段代码假设我们使用 Spring Security 来管理对 Web 应用程序的访问。我们使用 SecurityContextHolder 来获取 Principal 并从中提取用户名以将其传递给 MDC.put()。

从服务请求的线程发出的每个日志事件现在都将包含用户名字段和用户名。

有了这个字段,我们现在可以过滤特定用户请求的日志。如果用户报告了问题,我们可以根据他们的姓名过滤日志,并极大地减少我们必须查看的日志。

根据规定,您可能希望记录更不透明的用户 ID 而不是用户名。

为错误日志事件添加根本原因

当我们的应用程序出现错误时,我们通常会记录堆栈跟踪。堆栈跟踪帮助我们确定错误的根本原因。如果没有堆栈跟踪,我们将不知道是哪个代码导致了错误!

但是,如果我们想在应用程序中运行错误统计信息,堆栈跟踪是非常笨拙的。假设我们想知道我们的应用程序每天总共记录了多少错误,以及其中有多少是由哪个根本原因异常引起的。我们必须从日志中导出所有堆栈跟踪,并对它们进行一些手动过滤,才能得到该问题的答案!

但是,如果我们将自定义字段 rootCause 添加到每个错误日志事件,我们可以通过该字段过滤日志事件,然后在日志服务器的 UI 中创建不同根本原因的直方图或饼图,甚至无需导出数据。

在 Spring Boot 中执行此操作的一种方法是创建一个 @ExceptionHandle:

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
java复制代码@ControllerAdvicepublic
class WebExceptionHandler {


private static final Logger logger = LoggerFactory.getLogger(WebExceptionHandler.class);


@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public void internalServerError(Exception e) {
MDC.put("rootCause", getRootCause(e).getClass().getName());
logger.error("returning 500 (internal server error).", e);
MDC.remove("rootCause");
}


private Throwable getRootCause(Exception e) {
Throwable rootCause = e;
while (e.getCause() != null && rootCause.getCause() != rootCause) {
rootCause = e.getCause();
}
return rootCause;
}


}

我们创建了一个用 @ControllerAdvice 注解的类,这意味着它在我们所有的 web 控制器中都是有效的。

在类中,我们创建了一个用 @ExceptionHandler 注解的方法。对于任何 Web 控制器中出现的异常,都会调用此方法。它将 rootCause MDC 字段设置为导致错误的异常类的完全限定名称,然后记录异常的堆栈跟踪。

就是这样。所有打印堆栈跟踪的日志事件现在都有一个字段 rootCause,我们可以通过这个字段进行过滤以了解我们应用程序中的错误分布。

向所有日志事件添加跟踪 ID

如果我们运行多个服务,例如在微服务环境中,分析错误时事情会很快变得复杂。一个服务调用另一个服务,另一个服务调用再一个服务,并且很难(如果可能的话)跟踪一个服务中的错误到另一个服务中的错误。

跟踪 ID 有助于连接一个服务中的日志事件和另一个服务中的日志事件:

在上面的示例图中,服务 1 被调用并生成跟踪 ID“1234”。然后它调用服务 2 和 3,将相同的跟踪 ID 传播给它们,以便它们可以将相同的跟踪 ID 添加到其日志事件中,从而可以通过搜索特定的跟踪 ID 来连接所有服务的日志事件。

对于每个传出请求,服务 1 还会创建一个唯一的“跨度 ID”。虽然跟踪跨越服务 1 的整个请求/响应周期,但跨度仅跨越一个服务和另一个服务之间的请求/响应周期。

我们可以自己实现这样的跟踪机制,但是有一些跟踪标准和工具可以使用这些标准集成到跟踪系统中,例如 Logz.io 的分布式跟踪功能。

我们还是使用标准工具吧。在 Spring Boot 世界中,这就是 Spring Cloud Sleuth,我们可以通过简单地将它添加到我们的 pom.xml,从而把该功能集成到我们的应用程序中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement><dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
</dependencies>

这会自动将跟踪和跨度 ID 添加到我们的日志中,并在使用支持的 HTTP 客户端时通过请求标头将它们从一个服务传播到下一个服务。您可以在“使用 Spring Cloud Sleuth 在分布式系统中进行跟踪”一文中阅读有关 Spring Cloud Sleuth 的更多信息。

添加某些代码路径的持续时间

我们的应用程序响应请求所需的总持续时间是一个重要的指标。如果速度太慢,用户会感到沮丧。

通常,将请求持续时间作为指标公开并创建显示请求持续时间的直方图和百分位数的仪表板是一个好主意,这样我们就可以一目了然地了解应用程序的健康状况,甚至可能在违反某个阈值时收到警报。

然而,我们并不是一直在查看仪表板,我们可能不仅对总请求持续时间感兴趣,而且对某些代码路径的持续时间感兴趣。在分析日志以调查问题时,了解代码中特定路径执行所需的时间可能是一个重要线索。

在 Java 中,我们可能会这样做:

1
2
3
4
5
6
7
8
9
java复制代码void callThirdPartyService() throws InterruptedException {
logger.info("log event from the domain service");
Instant start=Instant.now();
Thread.sleep(2000); // simulating an expensive operation
Duration duration=Duration.between(start,Instant.now());
MDC.put("thirdPartyCallDuration",String.valueOf(duration.getNano()));
logger.info("call to third-party service successful!");
MDC.remove("thirdPartyCallDuration");
}

假设我们正在调用第三方服务并希望将持续时间添加到日志中。使用 Instant.now() 和 Duration.between(),我们计算持续时间,将其添加到 MDC,然后创建日志事件。

这个日志事件现在将包含字段 thirdPartyCallDuration,我们可以在日志中过滤和搜索该字段。 例如,我们可能会搜索这个调用耗时过长的实例。 然后,我们可以使用用户 ID 或跟踪 ID,当这需要特别长的时间时,我们也可以将它们作为日志事件的字段来找出模式。

在Logz.io中查询结构化日志

如果我们按照关于 per-environment logging 的文章中的描述设置了日志记录到 Logz.io,我们现在可以在 Logz.io 提供的 Kibana UI 中查询日志。

错误分布

例如,我们可以查询在 rootCause 字段中具有值的所有日志事件:

1
lua复制代码__exists__: "rootCause"

这将显示具有根本原因的错误事件列表。

我们还可以在 Logz.io UI 中创建一个可视化来显示给定时间范围内的错误分布:

此图表显示几乎一半的错误是由 ThingyException 引起的,因此检查是否可以以某种方式避免此异常可能是个好主意。如果无法避免,我们应该将其记录在 WARN 而不是 ERROR 上,以保持错误日志的清洁。

跨代码路径的错误分布

例如,假设用户抱怨预定的作业没有正常工作。如果我们在调度方法代码中添加了一个 job_status 字段,我们可以通过那些失败的作业来过滤日志:

1
lua复制代码job_status: "ERROR"

为了获得更高级的视图,我们可以创建另一个饼图可视化,显示 job_status 和 rootCause 的分布:

我们现在可以看到大部分预定的作业都失败了!我们应该为此添加一些警报! 我们还可以查看哪些异常是大多数计划作业的根本原因并开始调查。

检查用户的错误

或者,假设用户名为 “user” 的用户提出了一个支持请求,指定了它发生的大致日期和时间。我们可以使用查询 username: user 过滤日志以仅显示该用户的日志,并且可以快速将用户问题的原因归零。

我们还可以扩展查询以仅显示具有 rootCause 的该用户的日志事件,以直接了解何时出了什么问题。

1
lua复制代码username: "user" AND _exists_: "rootCause"

结构化您的日志

本文仅展示了几个示例,说明我们如何向日志事件添加结构并在查询日志时使用该结构。以后可以在日志中搜索的任何内容都应该是日志事件中的自定义字段。添加到日志事件中的字段在很大程度上取决于我们正在构建的应用程序,所以在编写代码时,一定要考虑哪些信息可以帮助您分析日志。

您可以在 GitHub 上找到本文中讨论的代码示例。

本文转载自: 掘金

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

Golang 实现协程池

发表于 2021-11-10

在 Golang 中要创建一个协程是一件无比简单的事情,你只要定义一个函数,并使用 go 关键字去执行它就行了。

如果你接触过其他语言,会发现你在使用使用线程时,为了减少线程频繁创建销毁还来的开销,通常我们会使用线程池来复用线程。

池化技术就是利用复用来提升性能的,那在 Golang 中需要协程池吗?

在 Golang 中,goroutine 是一个轻量级的线程,他的创建、调度都是在用户态进行,并不需要进入内核,这意味着创建销毁协程带来的开销是非常小的。

因此,我认为大多数情况下,开发人员是不太需要使用协程池的。

但也不排除有某些场景下是需要这样做,因为我还没有遇到就不说了。

抛开是否必要这个问题,单纯从技术的角度来看,我们可以怎样实现一个通用的协程池呢?

下面就来一起学习一下我的写法

首先定义一个协程池(Pool)结构体,包含两个属性,都是 chan 类型的。

一个是 work,用于接收 task 任务

一个是 sem,用于设置协程池大小,即可同时执行的协程数量

1
2
3
4
go复制代码type Pool struct {
work chan func() // 任务
sem chan struct{} // 数量
}

然后定义一个 New 函数,用于创建一个协程池对象,有一个细节需要注意

work 是一个无缓冲通道

而 sem 是一个缓冲通道,size 大小即为协程池大小

1
2
3
4
5
6
go复制代码func New(size int) *Pool {
return &Pool{
work: make(chan func()),
sem: make(chan struct{}, size),
}
}

最后给协程池对象绑定两个函数

1、NewTask:往协程池中添加任务

当第一次调用 NewTask 添加任务的时候,由于 work 是无缓冲通道,所以会一定会走第二个 case 的分支:使用 go worker 开启一个协程。

1
2
3
4
5
6
7
go复制代码func (p *Pool) NewTask(task func()) {
select {
case p.work <- task:
case p.sem <- struct{}{}:
go p.worker(task)
}
}

2、worker:用于执行任务

为了能够实现协程的复用,这个使用了 for 无限循环,使这个协程在执行完任务后,也不退出,而是一直在接收新的任务。

1
2
3
4
5
6
7
scss复制代码func (p *Pool) worker(task func()) {
defer func() { <-p.sem }()
for {
task()
task = <-p.work
}
}

这两个函数是协程池实现的关键函数,里面的逻辑很值得推敲:

1、如果设定的协程池数大于 2,此时第二次传入往 NewTask 传入task,select case 的时候,如果第一个协程还在运行中,就一定会走第二个case,重新创建一个协程执行task

2、如果传入的任务数大于设定的协程池数,并且此时所有的任务都还在运行中,那此时再调用 NewTask 传入 task ,这两个 case 都不会命中,会一直阻塞直到有任务执行完成,worker 函数里的 work 通道才能接收到新的任务,继续执行。

以上便是协程池的实现过程。

使用它也很简单,看下面的代码你就明白了

1
2
3
4
5
6
go复制代码func main()  {
pool := New(128)
pool.NewTask(func(){
fmt.Println("run task")
})
}

为了让你看到效果,我设置协程池数为 2,开启四个任务,都是 sleep 2 秒后,打印当前时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
css复制代码func main()  {
pool := New(2)

for i := 1; i <5; i++{
pool.NewTask(func(){
time.Sleep(2 * time.Second)
fmt.Println(time.Now())
})
}

// 保证所有的协程都执行完毕
time.Sleep(5 * time.Second)
}

执行结果如下,可以看到总共 4 个任务,由于协程池大小为 2,所以 4 个任务分两批执行(从打印的时间可以看出)

1
2
3
4
ini复制代码2020-05-24 23:18:02.014487 +0800 CST m=+2.005207182
2020-05-24 23:18:02.014524 +0800 CST m=+2.005243650
2020-05-24 23:18:04.019755 +0800 CST m=+4.010435443
2020-05-24 23:18:04.019819 +0800 CST m=+4.010499440

本文转载自: 掘金

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

科普 — 关于RabbitMQ与AMQP协议概念,你想了解的

发表于 2021-11-10

导语

本文从AMQP协议(Advanced Message Queuing Protocol,高级消息队列协议)、消息功能、消费模型、金融级用法及其他功能点对比等概念介绍对RabbitMQ做了科普, 希望对各位深入理解RabbitMQ有帮助。

AMQP协议概念

AMQP协议自身定义了很多概念,下面先对这些概念进行剖析,会更侧重从每个概念实体的作用域、职责范围、从属关系等维度进行介绍。

Connection

  • 对应底层一个AMQP-Client到RabbitMQ-Broker的一个TCP连接。
  • 这边要考虑两个端点问题,在TCP连接建立完成后,如下图所示,连接的目标Broker就已经确定是集群中的一台了,由于是长连接,除非断连重建,否则对端节点不可变。
  • 所以从这里可以看出RabbitMQ相比Pulsar、RocketMQ不一样的地方在于,其是一种服务端寻址模型,以Client的视角来看,想要连接任意Exchange、Queue,只要连上任意一台Broker就行。

Channel

  • 信道,可以理解为一种逻辑连接,体现了多路复用的设计思路。
  • 支持串行执行,包括收和发的指令,可以理解为一种半双工模式的“虚拟网络通道”。
  • 所有Exchange、Queue、Binding的操作都是在Channel之上进行的。

Vhost

  • 等价于一种租户隔离概念,不同Vhost下可以创建同名Exchange、Queue,这样可以进行业务隔离。
  • RabbitMQ的权限隔离和权限维度控制的机制是在Vhost级别的。
  • Rabbit官方原生的全局Policy控制在Vhost级别。

Exchange

  • 一个虚拟实体,声明不同消息的路由策略,自身不存储消息。
  • 一个路由器,基于消息头部的RoutingKey和Header将消息路由到符合条件的具体的Queue。
  • 支持单播和广播。

Queue

  • 消息存储实体,是消息底层存储的容器,类似Pulsar的Topic。
  • 单订阅模式,其下的Consumer分别消费到一部分消息。
  • 和存储关联,因此有容量上限、TTL等存储层的特性。
  • 支持多消费和独占消费,取决于你订阅时设置的参数。由于它是存储消息系统的消息,所以内部基于一个消息位点控制持久化消费进度,记录最后被消费并Ack的位置。
  • 面向Consumer。

Binding

  • 衔接Exchange和Queue的桥梁,本质是一个规则的声明。
  • 一个Exchange下可以有多个Binding。
  • 一个Queue也可以被多个Binding关联。
  • 一个Exchange到一个Queue也可以声明多个Binding。

消息功能

下面介绍RabbitMQ官方所提供的的开源原生功能,我们知道,AMQP协议可以看做成一种可编程式的消息队列协议,可以基于其提供的基础模型,通过自己的巧妙搭配组合,构造出多种多样的业务模型。

消息结构

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
bash复制代码# publishInfo
exchange: amq.direct
immediate: false
mandatory: false
routingKey: test

# headerBody
bodySize: 1024
properties:
- contentType:
- encoding:
- deliveryMode:
- priority:
- correlationId:
- replyTo:
- expiration:
- messageId:
- timestamp:
- type:
- userId:
- appId:
- clusterId:
- headers: {}

# contentBody
二进制消息体bytes

每个消息分为三个部分,在网络层面即三个独立数据帧:

  • PublishInfo: 消息路由声明信息,可以联想成写电子邮件时填写的目标邮箱、是否接收回执等前置声明。
  • HeaderBody: 消息头部,用于存储RabbitMQ自身事先定义的声明,可以联想层HTTP协议的Header一样,此处可以放置一些对业务透明的上下文信息用于提供某种功能,比如分布式链路追踪的TraceId。
  • ContentBody: 消息体,无差别二进制数据块,服务端不感知其是否压缩、是否加密等,只进行透明的存储和读取投递。

work queue 工作队列

  • 它是一种模型简化,发送消息时指定Exchange为空,RoutingKey为QueueName,Broker以后会直接把这个消息发送至目标Queue,这样对用户来说相当于没Exchange,他认为是直接用Queue来消费,就比较简单。
  • 工作队列只适用于单订阅的场景,因为Queue只适用于单订阅。
  • 官方讲解

Publish/Subscribe 发布订阅模式

Queue不支持多订阅,通过转换思路实现:

  • 一个Fanout类型的Exchange:相当于多订阅场景的Topic。
  • 多个不同的Queue:绑定到该Exchange,相当于多订阅场景下的Subscription。
  • 多个Consumer消费同一个Queue:常规场景多订阅。
  • 每个Consumer各自消费一个Queue:实例级别广播。
  • 官方讲解
  • 对齐RocketMQ、Pulsar的多订阅消费、广播消费

Routing 路由模式

  • 路由模式是用Rabbit最常用的一种模式。
  • Producer发布了一个Exchange,这个Exchange的类型是Direct,在Message中指定RoutingKey,并设置一个非空的值,接下来声明一些Queue,这些 Queue在声明和绑定Exchange的时候,需要指定Binding,消息在路由的时候判断消息里的RoutingKey和BindingKey是不是equal,如果是对等的就可以路由过来。
  • 类似tag过滤的消息分发场景。
  • 官方讲解
  • 对齐rocketmq、pulsar的tag过滤消费

Topic 通配符模式

  • 路由模式的升级版,支持通配符匹配。
  • Exchange类型为Topic。
  • 匹配规则不是正则表达式,是AMQP自己的语法。
  • 官方讲解

Header模式

  • 不常用,匹配规则不基于RoutingKey,而是基于HeaderBody.Properties.Headers中的键值对。
  • 支持完全匹配所有键值对。
  • 支持只匹配一个键值对。

RPC模式

  • RPC模式并不常用,基于回复队列。
  • 生产者和消费者采用一问一答的模式。
  • 等价于RPC的request-response模型。
  • 官方讲解

消费模型

消费模型也是使用一个消息系统所需要特别关心的一环,在业务的使用过程中,更多地会关注一条消息从生产到投递至消费者整个过程中都经历了什么,整个消息的声明周期是如何闭环的?

下面主要从TDMQ RabbitMQ版的实现来剖析RabbitMQ协议的消息生命周期。

从消息的生命周期看待消费模型

  • 已投递未Ack:Consumer独占,直到Consumer触发Ack或者Consumer断开。
  • Ack消息:标记已消费,位点前进。
  • Nack消息:底层操作等价于Ack,会根据配置转发到死信Exchange,否则丢弃。
  • Requeue:消息放回队头,待下次投递。

从内部核心组件看消费模型

  • Queue:负责存储原始消息数据,按序存储。
  • RedeliveryTracker:负责记录Consumer端Requeue的消息,并触发重新投递,标识投递次数。
  • Dispatcher:负责管理连接Queue的所有Consumer,负责消息的负载均衡、分发、进度管理等。
  • Limiter:QoS限流器,基于Unack数限流,而不是QPS,呼应上方消息生命周期。
  • Unack Tracker:跟踪当前Channel中已投递未Ack的消息。

从这张图可以获取那些信息?

  • 一个Queue可以被不同Connection连接、被同一个Connection的不同Channel连接。
  • 一个Channel中可以启动两个Consumer连接同一个Queue。
  • QoS限流作用域为Channel,即一个Channel中创建的多个Cconsumer享有相同的配额。
  • 如果BasicQoS Global设置为true,那么同一个Channel中的Consumer用尽配额,该Channel下的所有Consumer全部阻塞,无法接收新消息。
  • Unack追踪器也是Channel作用域,故一个Channel关闭,被该Channel独占的所有未Ack消息全部回收到Queue级别的跟踪器,进行全局重投递。
  • basic指令集: www.rabbitmq.com/amqp-0-9-1-…
  • Consumer Prefetch: www.rabbitmq.com/consumer-pr…

金融级用法

  • 消息确认:发送反馈,给予Producer发送成功的确认。
  • 备选Exchange:发送成功的消息无法匹配任何Binding的场景。
  • 消息回退:消息无法匹配任何Binding时退回到Producer。
  • 重投递:网络错误、Consumer端宕机、业务处理偶发错误等场景,重试消费恢复。
  • 死信Exchange:业务多次重试、长时间无法成功,放入死信,待人工处理或者下一步的自动化修正or告警系统。

功能点对比

经过上述说明,你应该能利用RabbitMQ的功能点,结合自己的业务场景组织一个相对合理的生产消费拓扑。
除了上面提到的功能点,RabbitMQ本身还提供了很多其他功能,下面主要列举一部分对比,可供参考和借鉴:

通道类

功能点 说明 TDMQ支持情况
认证和授权 基于User/Password的登录鉴权机制 整合Pulsar自身的JWT(Role+Token)机制进行对齐
连接协商机制 连接握手协商连接通信参数 完全对齐RabbitMQ原生
认证和授权 Vhost维度配置和User的权限关系 AMQP SDK使用层面完全对齐
限流协商机制(QoS) 基于Unack数进行配额限制 完全对齐RabbitMQ原生
  • 注意:QoS机制RabbitMQ的实现是和标准AMQP协议有出入的,我们选择对齐RabbitMQ而不是AMQP规范,我们也认为RabbitMQ的模式较合理,详见www.rabbitmq.com/consumer-pr…

Exchange类

功能点 说明 TDMQ支持情况
Exchange绑定Exchange RabbitMQ在AMQP协议上的扩展,使Exchange不局限于只绑定Queue,借此可以构建出更加复杂的拓扑逻辑 暂未支持,排期中
死信Exchange Queue的扩展参数,用于Queue中丢弃消息时转发至死信Exchange 完全对齐RabbitMQ原生
备选Exchange Exchange的扩展参数,用于消息发送至Exchange时,无法匹配任何路由规则到下游Queue,转发至备选Exchange 完全对齐RabbitMQ原生

Queue类

功能点 说明 TDMQ支持情况
优先队列 消息可设置优先级,同时到达的消息可根据优先级投递,是一种局部性破坏先入先出机制的功能 暂未支持,排期中
独占队列 声明队列只能被声明的Connection实体所连接,通常和临时队列配合使用 暂未支持,排期中
临时队列 随机生成一个临时队列名,可用于当前进程专用,通常配合独占队列和AutoDelete一起使用 暂未支持,排期中
回复队列 用于声明消息Producer处理完成后,向Producer进行回包的队列,以此实现一问一答的通信模型 暂未支持,排期中
TTL 针对消息设置TTL(time to live),过期未投递的消息将会被丢弃 or 进入死信 目前支持Vhost级别的TTL机制
镜像队列 RabbitMQ为了解决单点储存问题而引入的,为了实现队列消息多副本存储 TDMQ天然多副本分布式存储,不需要该功能

收发机制类

功能点 说明 TDMQ支持情况
消息确认 消息在Broker成功存储后,回包Producer,进行发送成功确认 完全对齐RabbitMQ原生
事务消息 消息确认功能出现前的发送确认机制,性能很差,不建议使用 暂未支持,待定
延迟消息 消息发送成功后,延迟一定时间后才进行投递 完全对齐RabbitMQ原生
RPC 基于回复队列封装出的一问一答模型,使用场景较少,建议用主流RPC框架 暂未支持,待定

参考

  • RabbitMQ协议官方文档
  • RabbitMQ官方功能介绍
  • RabbitMQ协议指令集
  • RabbitMQ官方教程
  • RabbitMQ官方Demo

后记

通过这篇文章,希望能对RabbitMQ进行一定程度的科普,也从一个从0到1设计一个RabbitMQ Broker的开发的角度,浅析了一些RabbitMQ的一些消费模型细节,补充点当前网络上对这部分细节的缺漏,可能可以起到一些启发作用。

后续,我们将会着重分享,我们如何在apache pulsar生态上构建出一套完全对齐RabbitMQ协议的高性能、高可用、云原生消息队列,相比原生RabbitMQ,我们有何优势,以及我们在过程中遇到的问题,产生的思考。

敬请期待~

本文转载自: 掘金

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

Python项目:爬取智联招聘网站的数据分析职位信息并进行可

发表于 2021-11-10

作者:进击的西西弗斯

本文链接:blog.csdn.net/qq_42216093…

版权声明:本文为作者原创文章,未经作者同意不可转载


1.项目说明以及流程概要

  • 爬取网站:智联招聘(https://sou.zhaopin.com/)
  • 开发环境:Python3.7(Pycharm编辑器),全流程通过代码实现
  • 爬取时间:2021/3/30 上午1:13 的实时招聘信息数据
  • 爬取城市:共12个,上海、北京、广州、深圳、天津、武汉、西安、成都、南京、杭州、重庆、厦门
  • 主要用到的python库:requests、BeautifulSoup、pandas、matplotlib、seaborn
  • 说明:本人大四,想在毕业后进入数据分析行业工作,于是,为了更深入地了解数据分析职位相关信息,我使用python在智联招聘网站爬取了我国主要的12个城市的“数据分析师”职位的全部招聘信息数据,包括薪资、公司名称以及规模、学历要求、技能要求、工作经验要求等数据,对数据清洗和整理后进行可视化分析,得到了薪资分布、不同学历占比、技能词频等图表,目的是能从繁杂的招聘数据中直观地看到有价值的信息。

2.爬取网站数据并整理为csv

流程概要:

根据url和相关参数获取网页的html,对html解析后正则提取我们需要的标签信息,最终以dataframe二维表形式保存为csv文件,其中要注意:智联招聘在未登陆状态下无法爬取职位数据,于是我们可以先登陆网站,然后在浏览器开发者模式下找到需求头信息(Request Headers),复制下来后通过copyheaders库转换为字典后加入requests请求的headers参数中。(建议不要直接使用我的代码,虽然可以运行但很多人运行可能会被网站检测出来,然后可能会被反爬甚至封禁我的cookie)

代码:(附注释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
python复制代码#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Created on Fri Aug 14 17:47:47 2020: 2021/3/30 上午1:13
@Author : liudong
@Software: PyCharm
"""

import requests
import re
from copyheaders import headers_raw_to_dict
from bs4 import BeautifulSoup
import pandas as pd


# 根据url和参数获取网页的HTML:

def get_html(url, params):

my_headers = b'''
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: max-age=0
Connection: keep-alive
Cookie: x-zp-client-id=448f2b96-6b3a-48e3-e912-e6c8dd73e6cb; sts_deviceid=178832cf3f2680-0b20242883a4a9-6618207c-1296000-178832cf3f3780; Hm_lvt_38ba284938d5eddca645bb5e02a02006=1624877846; urlfrom2=121114584; adfcid2=www.google.com; adfbid2=0; FSSBBIl1UgzbN7NO=5QbLj2_L5kKhv8gnuJa.E1._8RKksG1y5Nt4FRrSajQ7PKGJ8CcWopqTuOLay__ida1esO2ud4AdXKKDI69j9UA; locationInfo_search={%22code%22:%22538%22%2C%22name%22:%22%E4%B8%8A%E6%B5%B7%22%2C%22message%22:%22%E5%8C%B9%E9%85%8D%E5%88%B0%E5%B8%82%E7%BA%A7%E7%BC%96%E7%A0%81%22}; selectCity_search=538; ssxmod_itna=7qGxnDRG0=KGqAKGHKiQRSDQwKfkKqYteb87Dla=xA5D8D6DQeGTb0NpYeYietdigMWPqKYG4iteiFlYfPtb+4OEdD84i7DKqibDCqD1D3qDkbCYxiinDCeDIDWeDiDG+8D0hXl7DjQNXZKkULfBNDz4X2/4UgQDDHfG024dLRIqIgFA+5HYbDbxp9DB6rxBQ/Iqj6znUDgMTTibwbj8DoGiP=fifwn7Dq0YoYCA44fDx=bb4ee2hso7DYFDqojR8DG4xL2iD===; ssxmod_itna2=7qGxnDRG0=KGqAKGHKiQRSDQwKfkKqYteb8D61Fgj40y4rP03aKenjt6D6QMTiBeG2Yn408DewGD; urlfrom=121114584; adfcid=www.google.com; adfbid=0; sts_sg=1; sts_chnlsid=Unknown; zp_src_url=https%3A%2F%2Fwww.google.com.hk%2F; LastCity=%E4%B8%8A%E6%B5%B7; LastCity%5Fid=538; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%221071739258%22%2C%22first_id%22%3A%22178832cf3bd20f-0be4af1633ae3d-6618207c-1296000-178832cf3be4b8%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E8%87%AA%E7%84%B6%E6%90%9C%E7%B4%A2%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC%22%2C%22%24latest_referrer%22%3A%22https%3A%2F%2Fwww.google.com%2F%22%7D%2C%22%24device_id%22%3A%22178832cf3bd20f-0be4af1633ae3d-6618207c-1296000-178832cf3be4b8%22%7D; acw_tc=276082a716357771539376802e2983bc8a5c6ad6d09a856f4d30d3892a3cd8; 1420ba6bb40c9512e9642a1f8c243891=68d62e0e-9c02-4c51-b5be-268470d6b21e; d4d6cd0b4a19fa72b8cc377185129bb7=1e7229ad-ee24-4063-9e4b-6522acfeefc7; at=01e2bf60daa14c479e524b22cfaf306f; rt=0747ac22bd424c8da3c28cb6bbd7a8f6; zpfe_probe_token=3d5af381s32ee94285a4e785bfcdba4df809; FSSBBIl1UgzbN7NP=53Ud_uDmd57aqqqmZC5Xn3qKkeoR73_UtjtoQEvodODN_.CWXzEhTjq8aUd0_FtFCmJ7zHbxzlDmsdsmVKETzSt0C8oeOyH7oQmVQMzAfCehTWeQ6QfajFpiObY8ukPfhc73vMi1pSbFiE4Iu4rGZjz8L_8Ww80.iFXTkrYYJ.C4nZ1OPCmdGhgVIZBVau1P0P1.qTYIvWuWSQyPdlNvBFfVCjF4x0XIP4AL9VK0E4YZZzV54JqXOXzFr6ox5zzXRW4NTRXe_iYnJ0B7XRWx07n
Host: sou.zhaopin.com
Referer: https://sou.zhaopin.com/
sec-ch-ua: "Google Chrome";v="95", "Chromium";v="95", ";Not A Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
'''
my_headers = headers_raw_to_dict(my_headers) # 把复制的浏览器需求头转化为字典形式
req = requests.get(url, headers=my_headers, params=params)
req.encoding = req.apparent_encoding
html = req.text

return html


# 输入url和城市编号,获取由所有职位信息的html标签的字符串组成的列表:

def get_html_list(url, city_num):

html_list = list()

for i in range(1, 12):
params = {'jl': str(city_num), 'kw': '数据分析师', 'p': str(i)}
html = get_html(url, params)
soup = BeautifulSoup(html, 'html.parser')
html_list += soup.find_all(name='a', attrs={'class': 'joblist-box__iteminfo iteminfo'})

for i in range(len(html_list)):
html_list[i] = str(html_list[i])

return html_list


# 根据上面的HTML标签列表,把每个职位信息的有效数据提取出来,保存csv文件:

def get_csv(html_list):

# city = position = company_name = company_size = company_type = salary = education = ability = experience = evaluation = list() #
# 上面赋值方法在这里是错误的,它会让每个变量指向同一内存地址,如果改变其中一个变量,其他变量会同时发生改变

# table = pd.DataFrame(columns = ['城市','职位名称','公司名称','公司规模','公司类型','薪资','学历要求','技能要求','工作经验要求'])
city, position, company_name, company_size, company_type, salary, education, ability, experience = ([] for i in range(9)) # 多变量一次赋值

for i in html_list:

if re.search(
'<li class="iteminfo__line2__jobdesc__demand__item">(.*?)</li> <li class="iteminfo__line2__jobdesc__demand__item">(.*?)</li> <li class="iteminfo__line2__jobdesc__demand__item">(.*?)</li>',
i):
s = re.search(
'<li class="iteminfo__line2__jobdesc__demand__item">(.*?)</li> <li class="iteminfo__line2__jobdesc__demand__item">(.*?)</li> <li class="iteminfo__line2__jobdesc__demand__item">(.*?)</li>',
i).group(1)
city.append(s)
s = re.search(
'<li class="iteminfo__line2__jobdesc__demand__item">(.*?)</li> <li class="iteminfo__line2__jobdesc__demand__item">(.*?)</li> <li class="iteminfo__line2__jobdesc__demand__item">(.*?)</li>',
i).group(2)
experience.append(s)
s = re.search(
'<li class="iteminfo__line2__jobdesc__demand__item">(.*?)</li> <li class="iteminfo__line2__jobdesc__demand__item">(.*?)</li> <li class="iteminfo__line2__jobdesc__demand__item">(.*?)</li>',
i).group(3)
education.append(s)
else:
city.append(' ')
experience.append(' ')
education.append(' ')


if re.search('<span class="iteminfo__line1__jobname__name" title="(.*?)">', i):
s = re.search('<span class="iteminfo__line1__jobname__name" title="(.*?)">', i).group(1)
position.append(s)
else:
position.append(' ')

if re.search('<span class="iteminfo__line1__compname__name" title="(.*?)">', i):
s = re.search('<span class="iteminfo__line1__compname__name" title="(.*?)">', i).group(1)
company_name.append(s)
else:
company_name.append(' ')

if re.search(
'<span class="iteminfo__line2__compdesc__item">(.*?) </span> <span class="iteminfo__line2__compdesc__item">(.*?) </span>',
i):
s = re.search(
'<span class="iteminfo__line2__compdesc__item">(.*?) </span> <span class="iteminfo__line2__compdesc__item">(.*?) </span>',
i).group(1)
company_type.append(s)
s = re.search(
'<span class="iteminfo__line2__compdesc__item">(.*?) </span> <span class="iteminfo__line2__compdesc__item">(.*?) </span>',
i).group(2)
company_size.append(s)
else:
company_type.append(' ')
company_size.append(' ')

if re.search('<p class="iteminfo__line2__jobdesc__salary">([\s\S]*?)<', i):
s = re.search('<p class="iteminfo__line2__jobdesc__salary">([\s\S]*?)<', i).group(1)
s = s.strip()
salary.append(s)
else:
salary.append(' ')

s = str()
l = re.findall('<div class="iteminfo__line3__welfare__item">.*?</div>', i)
for i in l:
s = s + re.search('<div class="iteminfo__line3__welfare__item">(.*?)</div>', i).group(1) + ' '
ability.append(s)

table = list(zip(city, position, company_name, company_size, company_type, salary, education, ability, experience))

return table



if __name__ == '__main__':

url = 'https://sou.zhaopin.com/'
citys = {'上海':538, '北京':530, '广州':763, '深圳':765, '天津':531, '武汉':736, '西安':854, '成都':801, '南京':635, '杭州':653, '重庆':551, '厦门':682}
for i in citys.keys():
html_list = get_html_list(url, citys[i])
table = get_csv(html_list)
df = pd.DataFrame(table, columns=['city', 'position', 'company_name', 'company_size', 'company_type', 'salary',
'education', 'ability', 'experience'])
file_name = i + '.csv'
df.to_csv(file_name)

结果:

在这里插入图片描述

附结果csv文件下载链接

3.对数据结果进行可视化

流程概要:

先对数据结果进行清洗,salary属性下的字段都是类似于“8千-1.5万”这种无法进行后续统计和处理的字符串,我们需要将其全部修改为数值结果从而方便后续处理,此处我使用pandas和re(正则表达式)把每个字段的薪资统一处理成了范围的中间值。关于薪资的缺失值,我本来打算用所在城市薪资的平均值替换处理,但考虑到后续可视化分析只使用到平均值,替换处理与否并不影响结果,故没有再做处理。由于时间关系,还有其他空值、异常值等数据清洗并没有再继续处理。最后,使用matplotlib和seaborn进行数据可视化,共得到5个结果图

代码:(附注释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
python复制代码#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Created on Fri Aug 14 17:47:47 2020: 2021/4/2 上午1:30
@Author : liudong
@Software: PyCharm
"""


import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
plt.rcParams['font.sans-serif'] = ['Heiti TC'] # 指定默认字体:解决plot不能显示中文问题
plt.rcParams['axes.unicode_minus'] = False # 解决保存图像是负号'-'显示为方块的问题
import re
import os
import seaborn as sns
from wordcloud import WordCloud


citys = ['上海', '北京', '广州', '深圳', '天津', '武汉', '西安', '成都', '南京', '杭州', '重庆', '厦门']


#数据清洗:

def data_clear():

for i in citys:

file_name = './' + i + '.csv'
df = pd.read_csv(file_name, index_col = 0)

for i in range(0, df.shape[0]):

s = df.loc[[i],['salary']].values.tolist()[0][0]

if re.search('(.*)-(.*)',s):
a = re.search('(.*)-(.*)', s).group(1)
if a[-1] == '千':
a = eval(a[0:-1]) * 1000
elif a[-1] == '万':
a = eval(a[0:-1]) * 10000
b = re.search('(.*)-(.*)', s).group(2)
if b[-1] == '千':
b = eval(b[0:-1]) * 1000
elif b[-1] == '万':
b = eval(b[0:-1]) * 10000
s = (a + b) / 2
df.loc[[i], ['salary']] = s
else:
df.loc[[i], ['salary']] = ''

os.remove(file_name)
df.to_csv(file_name)



#各个城市数据分析职位数量条形图:

def citys_jobs():

job_num = list()
for i in citys:
file_name = './' + i + '.csv'
df = pd.read_csv(file_name, index_col = 0)
job_num.append(df.shape[0])
df = pd.DataFrame(list(zip(citys, job_num)))
df = df.sort_values(1, ascending = False)
x = list(df[0])
y = list(df[1])

fig = plt.figure(dpi=200)
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8])
ax.bar(x,y,alpha = 0.8)
ax.set_title('数据分析职位在全国主要城市的数量分布')
ax.set_ylim(0,350)

plt.savefig('./数据分析职位在全国主要城市的数量分布.jpg')
plt.show()


#不同城市薪资分布条形图:

def citys_salary():

y = list()
x = citys

for i in citys:
file_name = './' + i + '.csv'
df = pd.read_csv(file_name, index_col=0)
y0 = df['salary'].mean()
y.append(round(y0/1000, 1))

df = pd.DataFrame(list(zip(x,y)))
df = df.sort_values(1, ascending = False)
x = list(df[0])
y = list(df[1])

fig = plt.figure(dpi=200)
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8])
ax.bar(x, y, alpha = 0.8)
ax.set_title('数据分析职位在一些主要城市的薪资分布(单位:千)')
ax.set_ylim(5, 18)
for a, b, label in zip(x, y, y): # 内置函数zip():将几个列表合并为二维列表并转置,返回一个特殊对象,可通过list()列表化之后查看
plt.text(a, b, label, horizontalalignment = 'center', fontsize = 10) # plt.text()函数:在图中(a,b)位置添加一个文字标签label

plt.savefig('./数据分析职位在一些主要城市的薪资分布.jpg')
plt.show()


#数据分析岗位总体薪资的分布

def salary_distribute():

salary_list = list()
for i in citys:
file_name = './' + i + '.csv'
df = pd.read_csv(file_name, index_col = 0)
salary_list += list(df['salary'])
salarys = list()
for i in range(len(salary_list)):
if not pd.isnull(salary_list[i]): #由于该列表是从pandas中读出的数据,故不能用if salary_list[i] == np.nan,会识别不出来
salarys.append(round(salary_list[i]/1000, 1))
mean = np.mean(salarys)

plt.figure(dpi=200)
sns.distplot(salarys, hist = True, kde = True, kde_kws={"color":"r", "lw":1.5, 'linestyle':'-'})
plt.axvline(mean, color='r', linestyle=":")
plt.text(mean, 0.01, '平均薪资: %.1f千'%(mean), color='r', horizontalalignment = 'center', fontsize = 15)
plt.xlim(0,50)
plt.xlabel('薪资分布(单位:千)')
plt.title('数据分析职位整体薪资分布')
plt.savefig('./数据分析职位整体薪资分布.jpg')
plt.show()


#数据分析职位对学历要求的分布

def education_distribute():

table = pd.DataFrame()
for i in citys:
file_name = './' + i + '.csv'
df = pd.read_csv(file_name, index_col=0)
table = pd.concat([table, df])
table = pd.DataFrame(pd.value_counts(table['education']))
table = table.sort_values(['education'], ascending = False)
x = list(table.index)
y = list(table['education'])
print(x)

fig = plt.figure(dpi=200)
ax = fig.add_axes([0.1,0.1,0.8,0.8])
explode = (0, 0, 0, 0.2, 0.4, 0.6, 0.8)
ax.axis('equal')
ax.pie(y,labels = x,autopct='%.1f%%',explode=explode) #autopct显示每块饼的百分比属性且自定义格式化字符串,其中%%表示字符串%,类似正则
ax.set_title('数据分析职位对学历要求的占比')
ax.legend(x, loc = 1)
plt.savefig('./数据分析职位对学历要求的占比.jpg')
plt.show()


#技能关键词频统计

def wordfrequence():

table = pd.DataFrame()
for i in citys:
file_name = './' + i + '.csv'
df = pd.read_csv(file_name, index_col=0)
table = pd.concat([table, df])
l1 = list(table['ability'])
l2 = list()
for i in range(len(l1)):
if not pd.isnull(l1[i]):
l2.append(l1[i])
words = ''.join(l2)

cloud = WordCloud(
font_path='/System/Library/Fonts/STHeiti Light.ttc', # 设置字体文件获取路径,默认字体不支持中文
background_color='white', # 设置背景颜色 默认是black
max_words=20, # 词云显示的最大词语数量
random_state = 1, # 设置随机生成状态,即多少种配色方案
collocations = False, # 是否包括词语之间的搭配,默认True,可能会产生语意重复的词语
width=1200, height=900 # 设置大小,默认图片比较小,模糊
).generate(words)
plt.figure(dpi=200)
plt.imshow(cloud) # 该方法用来在figure对象上绘制传入图像数据参数的图像
plt.axis('off') # 设置词云图中无坐标轴
plt.savefig("./技能关键词频统计.jpg")
plt.show()


if __name__ == "__main__":

data_clear()
citys_jobs()
citys_salary()
salary_distribute()
wordfrequence()

结果:

1). 在这12个城市总体的薪资分布情况:(直方图+核密度分布函数)

可以看出,数据分析职位整体上薪资分布大致符合左偏态分布,薪资分布的密集区间大约在8k-15k之间,而平均薪资12.4k在整个IT行业中大致处于中等薪酬位置

2). 在不同城市的职位招聘数量分布情况:(已降序处理)

可以看出,一线城市北上广深位列榜首,结果符合常理,接下来是成都、杭州、西安,如果想去二线城市发展,这几个城市应该重点考虑。

3). 在不同城市的薪资分布情况:(已降序处理)

可以看出,在不同城市间的薪资分布大致与上面的职位数量分布相似,但出乎意料的是,广州被二线城市杭州、南京超越,这可能是由于杭州的阿里巴巴公司等以及南京的苏宁等这些大公司拉高了杭州和南京的数据分析薪资水平,也可能是爬取的智联招聘网站的数据样本有一定的局限性,具体原因有待进一步考查。

4). 招聘中对学历要求的占比:

可以看出,本科占比最大,为66.5%,其次是大专,而其余学历加起来只占比7.1%,因此,数据分析职位目前对于学历要求相对较低,适合于不打算读研的本科毕业生等人群。

5). 职位技能要求关键词频统计:

可以看出,数据分析职位主要需求技能是Python、SQL、数据挖掘、大数据、数据建模等,因此熟练掌握这些技能能够增加求职中的核心竞争力。

有问题欢迎留言交流!

如果你对Python数据分析、数据挖掘等内容感兴趣,欢迎关注我!

本文转载自: 掘金

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

质效中台助力实现质量度模型规模化落地 一、背景介绍 二、解决

发表于 2021-11-10

导读:前文"测试智能分级和风险评估在商业平台的应用探索"介绍,通过引入质量度模型,依托数据+模型的客观评估,实现了自主测试的转化和召回能力的提升。为了在更多业务落地和策略迭代,需要进行大规模的落地,但会像策略算法研究一样,遇到流程、特征挖掘、数据采集、模型训练和标准规范等一系列问题,百度智能测试团队基于百度优质的质效中台形成了一套成熟的方案和规模化的落地经验。

本文主要介绍依托流程管控、白盒、数据、策略、标注中台的紧密结合,通过配置化业务接入、标准化提测准入流程、统一化数据检索、规范化质量度风险预估和标注反馈机制,帮助业务低成本接入,实现质量度模型的快速落地。

一、背景介绍

质量度模型在商业平台业务的落地,带来了自主测试的转化和召回能力的提升。为了将此项能力应用在更多的业务线,带来更大业务收益;进一步反哺策略进行准确性提升,不断迭代质量度模型,因此,对质量度模型依赖的各项能力进行了梳理。

图片

▲点击图片放大查看

从图中可见,实现质量度模型的落地,本质上依赖六方面能力的建设:

(一)流程管控:对质量度模型进行整体流程控制、保存过程凭证并提供可视化能力。需要通过流程管控将质量度模型的触发、模型结果的校验有机整合在研发测试过程中,结合自主测试的判定依据,形成测试类型的转化。

(二)特征挖掘:基于业务特点,需要对质量模型依赖的特征进行挖掘,抽象为通用特征和业务自定义特征两部分。通用特征涵盖研发过程工具链的所有信息,如构建、项目需求管理、人员属性等;自定义特征则依据各业务的不同特点,覆盖测试活动、代码白盒分析、覆盖率、代码变更等信息。

(三)特征数据采集:模型的特征数据往往有多个来源,为了保障特征数据快速、方便进行接入,需要多种数据采集方式:API接入、agent接入、远端接入、配置接入、同步接入等,从而保障业务特征数据快速接入。

(四)特征数据存储与处理:特征数据需要有统一的数据托管和数据服务,如数据检索、数据标签及数据场景管理等。此外,还需要对样本数据或特征数据建立血缘关系,形成项目评估的完整数据集。

(五)策略管理:依托策略管理,可以更方便的托管各业务的策略模型,同时快速完成工具的服务化;在模型选型方面,无论是规则建模或者数据建模,都需要专业的模型选取、模型训练和模型调优等工具辅助迭代,从而提升模型准确度。

(六)标注中台:建立模型的本质,是一个不断完善和不断修正的过程。因此,不断提供置信的训练样本集,是提升质量度模型准确率和召回率的重要手段。需要通过标注中台对策略特征、结论进行可视化展现和标记,为模型训练提供丰富样本数据。

通过以上分析可以看出,实现质量度模型的规模化落地,对于业务部门来讲,门槛较高:因为除了业务项目特征数据外,还需要建立流程、数据采集、数据存储、模型迭代、标注反馈等各项能力。

为了实现质量度模型的低成本接入和稳定高效执行,我们对质效中台进行了有效的整合和改造,实现了统一的工程化方案。

二、解决思路

1、利用中台优势,统一负责流程整合、特征数据生产、数据采集、数据处理、建模、模型训练,形成标准化方案;

2、业务团队利用专项测试中台能力,构建完备有效的CICD能力,不断挖掘风险特征,进行风险的持续迭代。

下图为各质效中台的组织和关系,各中台能力介绍及在质量度模型应用中的作用,如下所述:

图片

▲点击图片放大查看

流程管控中台:作为准入/准出的统一控制台和可视化平台,一方面通过统一的插件,实现入口收敛和数据格式标准化;另一方面,通过合理的流程设置,适配多种开发模式。同时,由于准入/准出控制+质量度模型评估,整个链路涉及多次交互,因此在质量度落地中,性能是其核心要解决的问题。

为了提升整体性能体验,不仅对关联各方提出了性能要求,中台自身也通过合并请求、异步提交、并行处理等方式来尽可能降低耗时,从而保障性能达标。

专项测试中台:各业务借助专项测试中台来进行完备的测试活动,测试活动的数据同时是质量模型的重要数据特征。因此,为了能将各测试中台的数据进行采集,开发了各项数据接入能力;

如API接入、agent接入、配置接入、远端接入等,从而可以将测试输入、测试输出、测试分析等数据,利用标准格式写入数据中台,同时在数据中台对各种数据进行血缘关系的建立。

白盒分析中台(数据采集):白盒分析数据,包括代码特征、调用依赖、覆盖度等,这些是最客观的数据,也是质量度模型最为重要的数据来源。

白盒分析中台针对各种语言引入抽象语法树和函数调用链分析器,产出多项静态代码分析特征,如变更函数出入度、圈复杂度、服务依赖关系等;通过动态白盒分析,产出了覆盖率、trace和日志等动态特征,这些动静态特征以标准格式存储在数据中台,作为质量度模型的重要特征。

数据中台:数据中台除了提供es、db、afs等多种数据存储能力外,最为重要的是,数据中台要解决数据特征标签管理、数据血缘关系管理,从而将各方割裂的数据形成统一的数据服务。同时,通过抽象不同的质量度数据使用场景,提供了配置化的数据场景检索能力。

策略中台:策略中台提供策略托管服务,同时实现了策略的注册、训练、调试、调度执行、结果检索回调等功能,为策略开发者提供了策略快速开发、迭代的服务化能力。

同时通过规范化标准输入输出,降低了质量度模型应用中的策略运行时环境和策略适配成本。此外,策略中台通过通讯协议升级,大大优化了策略调度性能,也保障了质量度模型应用的性能体验。

通过以上中台能力的优化和协同,可实现质量度模型的以下技术目标:

  • 配置化业务接入
  • 标准化提测准入流程
  • 统一化数据采集、存储和处理
  • 标准化策略开发、训练、迭代
  • 规范化质量度风险预估和标注反馈机制

三、技术方案

(一)整体的交互流程

图片

▲点击图片放大查看

如图,通过CI和测试环节,将需求特征、研发特征、提测信息、专项测试中台特征等数据进行采集并以标准格式持久化在数据中台,之后通过流程管控中台统一的插件入口,触发测试准入、准入模型评估,模型通过对所有相关特征的检索、聚类,产出项目质量风险评估结论。

之后,流程控制中台依据质量模型结论,进行后处理,如通过、打回、重试等,同时要求项目负责人对模型结果进行标注反馈,并将反馈结论自动并入训练样本集,从而形成模型触发、模型决策、模型反馈的完整闭环和标准化流程。

(二)方案细节

1)流程控制

图片

▲点击图片放大查看

整个过程,借助流程管控中台来实现模型的全生命周期管理,并实现测试类型的切换。

具体而言,主要实现以下功能:

模型特征可视化:在流程管控平台,实现了模型依赖特征的抽象,并可通过可视化页面,与模型、数据场景实现数据互通。

实时特征数据采集并入库:大部分的特征可以在CI任务环节进行采集和入库数据中台。但是对于研发周期、需求提测信息等特征只能在提测准入时实时获取。为了保证这些特征数据的获取和入库性能,在对这些特征进行采集计算时,采用缓存+并行计算方式,实现快速获取特征;特征推送入库时,则重点对推送失败、超时等异常情况下的降级预案进行了处理;

模型触发及后处理:模型触发并给出结论后,流程管控中台对不同的结论会做不同的后处理动作,后处理中会进行自主测试类型的转换和提测流程状态的转换。

此外,为了保证整体的低使用成本,流程管控中台还从通用性和易用性上进行了优化。在功能满足条件下,兼顾整体性能、体验。

2)数据处理

数据处理方面,主要需要解决两方面问题:数据采集及关联关系、多维数据检索。

数据采集方面,对来源数据和数据中台的数据表schema建立了映射,并且通过队列来对接统一的数据源,实现数据生产、消费的解耦。其次,采用rulemap规则映射,对源数据进行映射配置,确保准确对齐schema数据定义。

图片

▲点击图片放大查看

数据检索方面,主要支持多层级的数据检索,如关联关系链路检索、精确检索、宽泛检索、自定义检索,从而支持不同的质量度特征数据的检索需求。

如下图可见详细的检索过程:用户定义检索场景下各层检索能力的检索条件后,首先根据关联关系链路进行并行检索,获取的关联数据结合配置的检索条件,进行细粒度精确检索;此外,通过宽泛检索和自定义检索对精确检索结果进行补充,解决多场景下精确检索无法完整检索数据问题。

通过对各层检索结果进行merge,形成一个完整的质量度模型数据,并持久化到大宽表。以此形成通用的,支持多维查询的数据检索能力,为质量度模型提供完整的特征数据。

图片

▲点击图片放大查看

3)质量度模型

风险预估高低或项目能否自测问题的本质是项目分级的一个0/1化决策问题,也就是分类问题。给出一个样本x,判断样本所属的类别y,分类器就是映射函数f: y=f(x)。这个函数需要根据以往的经验(大量已知类别的样本集)来构造的,构造的过程就是模型训练的过程。模型生成包含模型训练和模型预测两部分。

i. 模型训练

通过数据检索服务查询指定时段的历史数据,对数据进行处理,针对多模块粒度,需要依据每个特征的聚合规则进行值的聚合计算。依据入库的特征和数据量,决定采用那种哪种模型,目前应用人工规则、逻辑回归与决策树模型。

ii. 模型计算

分类算法通过对已知类别训练数据集的分析,从中发现分类规则产出模型,以此预测新数据的类别。

对于线上数据通过数据检索服务查询实时数据,并对数据进行处理,包括多模块的聚合计算。使用离线训练的模型或者人工规则进行预测,按指定格式返回给提测入口。

iii. 风险评估

产出风险评估结果得分,同时返回风险报告。基于不同产品线抽象出通用特征与私有特征,开发完成通用报告框架,进行风险点的可视化,同时支持标注功能完成闭环。

4)配置化接入

在质量度项目进行推广过程中,面对的一个现实问题就是不同产品线不同业务方向所进行的测试活动均有所不同,也就意味着模型特征不同。

为了更好的系统易用性,这里进行特征配置可视化。将风险特征拆分为通用特征和业务自定义特征,业务线自行配置。进行特征配置的主要的目的是结合统一数据检索,进行特征数据获取,使特征选取与配置对业务透明,新增业务方向可复用已有配置,特征变更在线审核,流程精简。

同时,质量度模型开发完成或复用已有模型后,流程控制中台自动在接入业务方向上线模型,无需人工介入。

图片

▲点击图片放大查看

四、当前效果及后续计划

借助于质效中台在流程控制、特征挖掘、数据处理、建模能力等方面的有机结合,质量度模型整体可以实现配置化接入、统一化数据处理和标准化质量度风险评估,业务团队则更聚焦于业务自身的CICD能力建设。

通过业务完备有效的CICD能力,利用模型+数据客观的对项目风险进行评估,经过2个Q的运行,当前效果如下:

1、业务在自定义特征数据具备情况下,可以实现小时级模块接入,2个Q累计接入20+业务,服务数1000+;

2、模型客观评估准确性达到94%,召回率达到90%;

3、8%左右的提测可以有效转为自主测试,大幅提升了项目交付吞吐;

4、得益于代码白盒分析、增量代码覆盖等客观特征的建设,质量度召回了1%+提测,并成功召回30+bug;

为了提升质量度模型的准确率和召回率,一方面会尝试探索新的风险评估模型、并对历史badcase进行review和修正,另一方面也会深度跟白盒能力进行合作,挖掘更多特征信息,如代码变更性能风险、稳定性风险、复杂度、影响面等。

同时,业务上在探索无人值守模式,即QA在项目或需求测试过程中,完全由机器执行,全程无人干预的情况。**质量度模型作为质量评估的重要客观手段,将是业务进行无人值守的重要支撑能力,**因此质效中台和质量度模型也会跟业务深入合作,支撑业务质效建设向测试无人值守前进。

推荐阅读:

|百度爱番番数据分析体系的架构与实践

|托管页前端异常监控与治理实战

|Flux架构思想在度咔App中的实践

———- END ———-

百度 Geek 说

百度官方技术公众号上线啦!

技术干货 · 行业资讯 · 线上沙龙 · 行业大会

招聘信息 · 内推信息 · 技术书籍 · 百度周边

欢迎各位同学关注

本文转载自: 掘金

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

这个网站,要火!

发表于 2021-11-10

大家好,我是程序员cxuan!今天和大家一起Look一下这个有趣的国外编程网站!

“今天我们来学习 Java “ 。

“Java 是一门面向对象的编程语言” 。

“Java 的特性有 balabalabala ……”。

“Java 与 C 的区别是 xxxxxx”。

“Java 能用来 balabalabala @!#$$” 。

balabalabalabalabalabalabalabala。

原文链接:这个网站,要火!

我还是不知道什么是 Java。为什么我不知道什么是 Java 呢,我想是因为我都不知道为什么要学习 Java ,确实,人在接触未知事物的时候,总是持有一副 90% 的拒绝、80% 的犹豫和 70% 的困惑的感觉。

我记不清楚数学老师讲过多少次二元一次方程组求解了,我记不清楚语文老师说过多少次背诵全文了,我也记不清楚多少次物理老师说先画受力分析图了。这种硬性要求和填鸭式的教学直到现在让我想起来,仍旧打了两个哈欠。

image-20211106211202651

我想,为什么会这样呢?

我突然想起来了隔壁老王家的大儿子,他家大儿子在上早教课,他家大儿子有一个兴趣,那就是拼积木。每次隔壁老王不想带孩子了,就让我带他儿子拼积木,我看到这孩子看到积木的时候,两眼放光,一拼就是一个小时,也不吵着闹着要吃糖了。

小孩子在看到玩具的时候,是一种发自内心的开心,这种发自内心的开心也同时体现在我们打游戏这件事情上。

为什么打游戏开心呢?因为有趣;为什么做数学题会犯困呢?因为无趣;那么我想,为什么做数学题和打游戏不能结合一下呢?

虽然我现在不做数学题了,现在我的主业是编程,编程和数学题在某种情况下是一样的,因为都需要我们逻辑思维能力,所以编程为什么不能和做游戏结合一下呢?

于是,我打算做一个网站,这个网站的目的就是只有一个,降低大家学习编程的门槛,采用玩游戏、讲故事的这种方式带你走入 Java 的世界,好了目的有了,说干就干。

软件的开发原则上表明:如果有现有的轮子,最好是直接使用,而不是再开发一个。

所以,当我在网络上遨游,搜索关于如何开发一个从零开始做游戏学 Java 轮子的时候,网站上一个你以为我在玩游戏,其实我在学 Java 的词条吸引了我,点进去,发现了一个不一样的东西。

地址是:

codegym.cc/zh/

打开网站后,发现这是一个通过实战来学习 Java 的网站,里面有非常多的示例,这倒是没什么,因为通过实战驱动学习 Java 的网站有很多,我们继续往下走,选择中文(这里我完全是想快速搞清楚这个网站是干啥的,所以为了快餐文化,我选择了中文,大家最好还是选择英文)。

然后它会提示我想成为一种什么类型的程序员。

image-20211107060959716

那必须是 Java 开发人员,然后 next ,提示我以哪种方式进行学习,诶我觉得这有点意思,我从来没尝试过通过阅读故事情节来学习 Java(难道学习 Java 还有故事背景?),而通过游戏学习 Java,这不就是我们想要的吗?

image-20211107061041379

然后看到了传统课堂,果断选择了游戏化的学习方式,继续 next。

后面提示我是否想要同伴的激励和选择何种的编程背景,我果断选择了不需要激励和黑色,因为黑色才是代表着程序员的信仰。注册完成后,会提示我们是否有编程经验,考虑了一下,毕竟我们想要做从零开始,所以选择了萌新上手。

然后我们就来到了 Codegym 的主界面,迫不及待的点进了学习课程界面,从 Java 语法开始学习。

映入眼帘的是一则有趣的故事。

image-20211107062746552

故事的背景是遥远的未来——3018 年,那时人类与机器人在地球上和谐相处,太空旅行已成为家常便饭。有一天,一艘名为银河系狂奔号宇宙飞船坠毁在一个未知的星球上……

在坠落过程中,宇宙飞船撞上山腰,几乎被完全埋在碎石下。船员们努力数日,想让飞船脱离困境,但没有成功,于是失去了回家的希望,开始在这个陌生的新地方安顿下来…… 飞船导航员发现未知星球上有大量的野生机器人,甚至机器狼,他们需要教会机器人编程指令,以帮助人类回家。

如此,一个极具沉浸感的故事配合着卡通画面,Java 学习之路正式开启。图片

图片

在玩游戏的过程中,虚拟的导师会指导你学习各种 Java 知识点,并且分配完成各种任务。

image-20211107063643844

你会尝试进行编写一些 Java 代码,这些代码会在 Codegym 平台进行验证,验证成功后,突破下一关,通过这种方式来让自己不断升级,提高自己的编程能力。

image-20211107064149730

我想,这不就是我们想要的东西么?这还用自己造个轮子,直接用 Codegym 的就好了呀!

除了基本的 Java 知识结构(Java 语法、Java 核心、Java 多线程、Java 集合)外,还有 SQL 和 Hibernate框架(开发中)、JSP 和 Servlet(开发中)。

image-20211107063400051

image-20211107063434451

我大吃一惊,这是通过玩游戏的方式,让我把整个 Java 学习路线都掌握了呀!

除此之外,我还在 Codegym 的左侧菜单栏发现了一些好东西。

image-20211107074253981

这里面是 Codegym 中的游戏,带你从创建包开始一步一步制作自己的游戏,当然你也可以查看其他用户发布的游戏。

如果你担心你没有计划的学习,不用怕,CodeGym 网站还给你搞了个提醒时间表的功能,把自己的学习计划安排上去,到时间后,他就会通知你的邮箱。

image-20211107074940316

我还发现,Codegym 有自己的论坛和社区,因为它是国外的网站,所以论坛和文章有非常多的外国人一起交流技术,交流你做的游戏,交流你的学习心得。

image-20211107074617800

image-20211107074642088

image-20211107074744840

一个人学习是孤独的,而一群人学习是幸福的。

在这里,你能够和其他人一起冲!

image-20211107075655317

看到这里,我觉得我整个人都燃起来了,恨不得马上学起来!

看的出来,这个网站做的非常用心,看得出来,这个网站的背后是一个在用心做事的团队。

如果时光能倒流,我希望能早点知道这个网站,这样我就能够在大学中珍惜那些让我上课睡觉的日子了。

最后,再给大家隆重的介绍一下这个网站

codegym.cc/zh/

小白可以用它轻松学 Java,Java 老手也可以去 Codegym 编写自己的游戏,分享学习经验,帮助他人,快乐自己。

最后给大家推荐一下我自己的Github,里面有非常多的硬核文章,绝对会对你有帮助。

本文转载自: 掘金

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

必会的 5 条 SQL 查询优化法则!

发表于 2021-11-10

这篇文章,是对SQL常用查询优化法则的总结,值得细看

文章目录

法则一:只返回需要的结果

法则二:确保查询使用了正确的索引

法则三:尽量避免使用子查询

法则四:不要使用 OFFSET 实现分页

法则五:了解 SQL 子句的逻辑执行顺序

总结

SQL 作为关系型数据库的标准语言,是分析师必不可少的技能之一。SQL 本身并不难学,编写查询语句也很容易,但是想要编写出能够高效运行的查询语句却有一定的难度。

查询优化是一个复杂的工程,涉及从硬件到参数配置、不同数据库的解析器、优化器实现、SQL 语句的执行顺序、索引以及统计信息的采集等,甚至应用程序和系统的整体架构。本文介绍几个关键法则,可以帮助我们编写高效的 SQL 查询;尤其是对于初学者而言,这些法则至少可以避免我们写出性能很差的查询语句。

以下法则适用于各种关系型数据库,包括但不限于:MySQL、Oracle、SQL Server、PostgreSQL 以及 SQLite 等。

法则一:只返回需要的结果

一定要为查询语句指定 WHERE 条件,过滤掉不需要的数据行。通常来说,OLTP 系统每次只需要从大量数据中返回很少的几条记录;指定查询条件可以帮助我们通过索引返回结果,而不是全表扫描。绝大多数情况下使用索引时的性能更好,因为索引(B-树、B+树、B*树)执行的是二进制搜索,具有对数时间复杂度,而不是线性时间复杂度。以下是 MySQL 聚簇索引的示意图:

)

举例来说,假设每个索引分支节点可以存储 100 个记录,100 万(1003)条记录只需要 3 层 B-树即可完成索引。通过索引查找数据时需要读取 3 次索引数据(每次磁盘 IO 读取整个分支节点),加上 1 次磁盘 IO 读取数据即可得到查询结果。

相反,如果采用全表扫描,需要执行的磁盘 IO 次数可能高出几个数量级。当数据量增加到 1 亿(1004)时,B-树索引只需要再增加 1 次索引 IO 即可;而全表扫描则需要再增加几个数量级的 IO。

同理,我们应该避免使用 SELECT * FROM, 因为它表示查询表中的所有字段。这种写法通常导致数据库需要读取更多的数据,同时网络也需要传输更多的数据,从而导致性能的下降。

法则二:确保查询使用了正确的索引

如果缺少合适的索引,即使指定了查询条件也不会通过索引查找数据。因此,我们首先需要确保创建了相应的索引。一般来说,以下字段需要创建索引:

经常出现在 WHERE 条件中的字段建立索引可以避免全表扫描;

将 ORDER BY 排序的字段加入到索引中,可以避免额外的排序操作;

多表连接查询的关联字段建立索引,可以提高连接查询的性能;

将 GROUP BY 分组操作字段加入到索引中,可以利用索引完成分组。

即使创建了合适的索引,如果 SQL 语句写的有问题,数据库也不会使用索引。导致索引失效的常见问题包括:

在 WHERE 子句中对索引字段进行表达式运算或者使用函数都会导致索引失效,这种情况还包括字段的数据类型不匹配,例如字符串和整数进行比较;

使用 LIKE 匹配时,如果通配符出现在左侧无法使用索引。对于大型文本数据的模糊匹配,应该考虑数据库提供的全文检索功能,甚至专门的全文搜索引擎(Elasticsearch 等);

如果 WHERE 条件中的字段上创建了索引,尽量设置为 NOT NULL;不是所有数据库使用 IS [NOT] NULL 判断时都可以利用索引。

执行计划(execution plan,也叫查询计划或者解释计划)是数据库执行 SQL 语句的具体步骤,例如通过索引还是全表扫描访问表中的数据,连接查询的实现方式和连接的顺序等。如果 SQL 语句性能不够理想,我们首先应该查看它的执行计划,通过执行计划(EXPLAIN)确保查询使用了正确的索引。

法则三:尽量避免使用子查询

以 MySQL 为例,以下查询返回月薪大于部门平均月薪的员工信息:

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码EXPLAIN ANALYZE
SELECT emp_id, emp_name
FROM employee e
WHERE salary > (
SELECT AVG(salary)
FROM employee
WHERE dept_id = e.dept_id);
-> Filter: (e.salary > (select #2)) (cost=2.75 rows=25) (actual time=0.232..4.401 rows=6 loops=1)
-> Table scan on e (cost=2.75 rows=25) (actual time=0.099..0.190 rows=25 loops=1)
-> Select #2 (subquery in condition; dependent)
-> Aggregate: avg(employee.salary) (actual time=0.147..0.149 rows=1 loops=25)
-> Index lookup on employee using idx_emp_dept (dept_id=e.dept_id) (cost=1.12 rows=5) (actual time=0.068..0.104 rows=7 loops=25)

从执行计划可以看出,MySQL 中采用的是类似 Nested Loop Join 实现方式;子查询循环了 25 次,而实际上可以通过一次扫描计算并缓存每个部门的平均月薪。以下语句将该子查询替换为等价的 JOIN 语句,实现了子查询的展开(Subquery Unnest):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码EXPLAIN ANALYZE
SELECT e.emp_id, e.emp_name
FROM employee e
JOIN (SELECT dept_id, AVG(salary) AS dept_average
FROM employee
GROUP BY dept_id) t
ON e.dept_id = t.dept_id
WHERE e.salary > t.dept_average;
-> Nested loop inner join (actual time=0.722..2.354 rows=6 loops=1)
-> Table scan on e (cost=2.75 rows=25) (actual time=0.096..0.205 rows=25 loops=1)
-> Filter: (e.salary > t.dept_average) (actual time=0.068..0.076 rows=0 loops=25)
-> Index lookup on t using <auto_key0> (dept_id=e.dept_id) (actual time=0.011..0.015 rows=1 loops=25)
-> Materialize (actual time=0.048..0.057 rows=1 loops=25)
-> Group aggregate: avg(employee.salary) (actual time=0.228..0.510 rows=5 loops=1)
-> Index scan on employee using idx_emp_dept (cost=2.75 rows=25) (actual time=0.181..0.348 rows=25 loops=1)

改写之后的查询利用了物化(Materialization)技术,将子查询的结果生成一个内存临时表;然后与 employee 表进行连接。通过实际执行时间可以看出这种方式更快。

以上示例在 Oracle 和 SQL Server 中会自动执行子查询展开,两种写法效果相同;在 PostgreSQL 中与 MySQL 类似,第一个语句使用 Nested Loop Join,改写为 JOIN 之后使用 Hash Join 实现,性能更好。

另外,对于 IN 和 EXISTS 子查询也可以得出类似的结论。由于不同数据库的优化器能力有所差异,我们应该尽量避免使用子查询,考虑使用 JOIN 进行重写。

法则四:不要使用 OFFSET 实现分页

分页查询的原理就是先跳过指定的行数,再返回 Top-N 记录。分页查询的示意图如下:

)

数据库一般支持 FETCH/LIMIT 以及 OFFSET 实现 Top-N 排行榜和分页查询。当表中的数据量很大时,这种方式的分页查询可能会导致性能问题。以 MySQL 为例:

1
2
3
4
5
sql复制代码-- MySQL
SELECT *
FROM large_table
ORDER BY id
LIMIT 10 OFFSET N;

以上查询随着 OFFSET 的增加,速度会越来越慢;因为即使我们只需要返回 10 条记录,数据库仍然需要访问并且过滤掉 N(比如 1000000)行记录,即使通过索引也会涉及不必要的扫描操作。

对于以上分页查询,更好的方法是记住上一次获取到的最大 id,然后在下一次查询中作为条件传入:

1
2
3
4
5
6
sql复制代码-- MySQL
SELECT *
FROM large_table
WHERE id > last_id
ORDER BY id
LIMIT 10;

如果 id 字段上存在索引,这种分页查询的方式可以基本不受数据量的影响。

法则五:了解 SQL 子句的逻辑执行顺序

以下是 SQL 中各个子句的语法顺序,前面括号内的数字代表了它们的逻辑执行顺序:

1
2
3
4
5
6
7
8
9
10
sql复制代码(6)SELECT [DISTINCT | ALL] col1, col2, agg_func(col3) AS alias
(1) FROM t1 JOIN t2
(2) ON (join_conditions)
(3) WHERE where_conditions
(4) GROUP BY col1, col2
(5)HAVING having_condition
(7) UNION [ALL]
...
(8) ORDER BY col1 ASC,col2 DESC
(9)OFFSET m ROWS FETCH NEXT num_rows ROWS ONLY;

也就是说,SQL 并不是按照编写顺序先执行 SELECT,然后再执行 FROM 子句。从逻辑上讲,SQL 语句的执行顺序如下:

首先,FROM 和 JOIN 是 SQL 语句执行的第一步。它们的逻辑结果是一个笛卡尔积,决定了接下来要操作的数据集。注意逻辑执行顺序并不代表物理执行顺序,实际上数据库在获取表中的数据之前会使用 ON 和 WHERE 过滤条件进行优化访问;

其次,应用 ON 条件对上一步的结果进行过滤并生成新的数据集;

然后,执行 WHERE 子句对上一步的数据集再次进行过滤。WHERE 和 ON 大多数情况下的效果相同,但是外连接查询有所区别,我们将会在下文给出示例;

接着,基于 GROUP BY 子句指定的表达式进行分组;同时,对于每个分组计算聚合函数 agg_func 的结果。经过 GROUP BY 处理之后,数据集的结构就发生了变化,只保留了分组字段和聚合函数的结果;

如果存在 GROUP BY 子句,可以利用 HAVING 针对分组后的结果进一步进行过滤,通常是针对聚合函数的结果进行过滤;

接下来,SELECT 可以指定要返回的列;如果指定了 DISTINCT 关键字,需要对结果集进行去重操作。另外还会为指定了 AS 的字段生成别名;

如果还有集合操作符(UNION、INTERSECT、EXCEPT)和其他的 SELECT 语句,执行该查询并且合并两个结果集。对于集合操作中的多个 SELECT 语句,数据库通常可以支持并发执行;

然后,应用 ORDER BY 子句对结果进行排序。如果存在 GROUP BY 子句或者 DISTINCT 关键字,只能使用分组字段和聚合函数进行排序;否则,可以使用 FROM 和 JOIN 表中的任何字段排序;

最后,OFFSET 和 FETCH(LIMIT、TOP)限定了最终返回的行数。

了解 SQL 逻辑执行顺序可以帮助我们进行 SQL 优化。例如 WHERE 子句在 HAVING 子句之前执行,因此我们应该尽量使用 WHERE 进行数据过滤,避免无谓的操作;除非业务需要针对聚合函数的结果进行过滤。

除此之外,理解 SQL 的逻辑执行顺序还可以帮助我们避免一些常见的错误,例如以下语句:

1
2
3
4
sql复制代码-- 错误示例
SELECT emp_name AS empname
FROM employee
WHERE empname ='张飞';

该语句的错误在于 WHERE 条件中引用了列别名;从上面的逻辑顺序可以看出,执行 WHERE 条件时还没有执行 SELECT 子句,也就没有生成字段的别名。

另外一个需要注意的操作就是 GROUP BY,例如:

1
2
3
4
vbnet复制代码-- GROUP BY 错误示例
SELECT dept_id, emp_name, AVG(salary)
FROM employee
GROUP BY dept_id;

由于经过 GROUP BY 处理之后结果集只保留了分组字段和聚合函数的结果,示例中的 emp_name 字段已经不存在;从业务逻辑上来说,按照部门分组统计之后再显示某个员工的姓名没有意义。如果需要同时显示员工信息和所在部门的汇总,可以使用窗口函数。

如果使用了 GROUP BY 分组,之后的 SELECT、ORDER BY 等只能引用分组字段或者聚合函数;否则,可以引用 FROM 和 JOIN 表中的任何字段。

还有一些逻辑问题可能不会直接导致查询出错,但是会返回不正确的结果;例如外连接查询中的 ON 和 WHERE 条件。以下是一个左外连接查询的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码SELECT e.emp_name, d.dept_name
FROM employee e
LEFT JOIN department d ON (e.dept_id = d.dept_id)
WHERE e.emp_name ='张飞';
emp_name|dept_name|
--------|---------|
张飞 |行政管理部|

SELECT e.emp_name, d.dept_name
FROM employee e
LEFT JOIN department d ON (e.dept_id = d.dept_id AND e.emp_name ='张飞');
emp_name|dept_name|
--------|---------|
刘备 | [NULL]|
关羽 | [NULL]|
张飞 |行政管理部|
诸葛亮 | [NULL]|
...

第一个查询在 ON 子句中指定了连接的条件,同时通过 WHERE 子句找出了“张飞”的信息。

第二个查询将所有的过滤条件都放在 ON 子句中,结果返回了所有的员工信息。这是因为左外连接会返回左表中的全部数据,即使 ON 子句中指定了员工姓名也不会生效;而 WHERE 条件在逻辑上是对连接操作之后的结果进行过滤。

总结

SQL 优化本质上是了解优化器的的工作原理,并且为此创建合适的索引和正确的语句;同时,当优化器不够智能的时候,手动让它智能。

本文转载自: 掘金

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

1…381382383…956

开发者博客

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