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

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


  • 首页

  • 归档

  • 搜索

从TCP头看TCP协议 为什么要有网络分层 从TCP头看TC

发表于 2021-11-29

本文正在参与 “网络协议必知必会”征文活动

为什么要有网络分层

网络的五层/七层架构模型时一个老生常谈的问题,分层的目的是为了让各层独立负责各自的工作,实现每层协议的标准化。分层后的网络架构中,发送数据时由上层传递到下层,每层对报文增加协议头后传递给下层;接收数据时由下层传递到上层,每层对报文解析协议头后传递给上层。如下图所示

image.png

本文主要阐述传输层的TCP协议,从TCP头入手,用通俗的语言讲清楚TCP协议。

从TCP头看TCP协议

三次握手

三次握手的目的是为了保证数据的可靠性,是TCP协议的重要特点之一
用通俗的话来阐述A和B进行三次握手

  1. A对B说:我的序列号是x,你准备好了吗?
  2. B对A说:我确认收到了你的x序列号报文,我的序列号是y,你准备好了吗?
  3. A对B说:我确认收到了你的y序列号报文,我的序列号是x+1

根据上面的描述,我们需要两个标志位

  1. SYN(synchronize)是指这个报文的目的是同步连接需要,即用于表示“你准备好了吗?”。
  2. ACK(acknowledge)是指确认报文,即“我确认收到了你的报文”。
    还需要两个值
  3. seq(sequence number)是报文的序列号,即“我的序列号是x”,每个报文都应该有序列号用于表示顺序。
  4. ack(acknowledge number)是报文应答的确认号,即“我确认收到了你的x序列号报文”,如果接收了x序列号的报文,确认号为x+1。

这样我们可以表示出三次握手的具体报文为

  1. A向B发送:SYN=1,seq=x
  2. B向A发送:SYN=1,seq=y,ACK=1,ack=x+1
  3. A向B发送:seq=x+1,ACK=1,ack=y+1

TCP头如下图所示,其包含了三次握手中需要的标志位(SYN、ACK)和两个值(序号和确认号)。

image.png

在三次握手中初步展示了TCP头的格式,本节我们根据TCP头来看一下TCP协议。

数据是如何传递给不同进程的

进程各自占据了单独的端口号,因此只要知道了端口,就能知道数据应该传递给哪个应用。在TCP头部保存源端口和目的端口,从而知晓这个数据是从哪里来要到哪里去,接收方拿到数据包后调换源端口和目的端口后回传数据。

如何发送并接收字节流

TCP是一个基于字节流的协议,发送和接收的数据都是字节流,TCP协议通过设置发送和接收窗口来完成数据的传递。这两个窗口的大小不固定,因而称为滑动窗口,这样可以在各种网络条件下保证接收方来得及接收。那么接收方是如何告知发送方这个窗口的大小呢,这就靠TCP头中的“窗口”部分进行控制。

以下图为例,接收方告知发送方当前的窗口值为20字节,那么发送窗口从收到确认的最后一个字节(即30)开始,设定了[31,50]的发送窗口,该窗口内字节允许被发送;接收方在收到一系列有序的字节后,对最后一个按序收到的字节进行确认。

image.png

从TCP头中可以看出,这个窗口值的大小使用2字节表示,最大只能到64KB,这显然是不够的。因此TCP后来引入了缩放因子,用于对窗口值大小的倍率转换。这个值作为扩展字段在TCP头中的“选项”中存储,使用「种类,长度,值」三元组进行表示,窗口缩放值的种类值为3。

四次挥手

  1. A对B说:我的序列号为x,我要关闭连接
  2. B对A说:我确认收到了你的x序列号报文,我的序列号为y
  3. B向A发送完剩余的数据
  4. B对A说:我确认收到了你的x序列号报文,我的序列号为z,我要关闭链接
  5. A对B说:我确认收到了你的z序列号报文

根据上面的描述,除了ACK外还需要一个标志位FIN

FIN(finish):告诉对方我要关闭连接,之后不再发送数据。

这样我们可以表示出四次挥手的具体报文为

  1. A向B发送:seq=x,FIN=1
  2. B向A发送:seq=y,ACK=1,ack=x+1
  3. B向A发送完剩余的数据
  4. B向A发送:seq=z,ACK=1,ack=x+1,FIN=1
  5. A向B发送:seq=x+1,ACK=1,ack=z+1

^C中断请求

control+c 是我们常用的中断方法,对于基于字节流的TCP协议来说,接收方需要知道哪些数据是紧急数据,正如这个中断请求,需要被加急处理。其实现起来需要让接收方知道这个报文“是否包含紧急数据?”、“紧急数据是什么?”,这就要一个标识位和一个指示数据范围的值,这就是我们在TCP头中看到的URG标识位和紧急指针字段

URG(urgent):告诉对方这个报文中包含紧急数据

紧急指针:指出报文中紧急数据的截止位,其开始位是从序号seq开始,因此紧急数据的范围是[seq, urgent pointer]

下面引自RFC1122

the urgent pointer points to the sequence number of the LAST octet (not LAST+1) in a sequence of urgent data

为什么基于TCP的SSH可以使用短命令?

为了高效地进行数据的收发,TCP协议在请求侧和发送侧都设有缓冲区用于存储数据,如下图所示

image.png

那么对于短命令(例如ssh登陆),如何保证在传输层绕过缓冲区,发送侧立即将报文交给下游处理,接收侧立即将数据返回给上层?

答案就是TCP头中的PUSH标志位。

PUSH标志位被置为1时,报文数据会被立即处理,而不是等待其他数据进入缓冲区后再处理。注意这个标志位通常不是在应用层进行设置,而是在TCP层清空并发送缓存,将报文段交给IP层时有TCP进行设置的。

本文转载自: 掘金

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

实践使用Jest的Nest单元测试

发表于 2021-11-29

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

前置条件

安装 Nodejs 环境,(>= 10.13.0, v13 版本除外),此部分网上有很多资料可以参考。

初始化项目

使用 Nest CLI 搭建的项目,操作简单且也提供了开箱即用的Jest框架

1
2
3
4
5
bash复制代码# 安装 Nest Cli
npm i -g @nestjs/cli
​
# 创建 Nest 项目
nest new project-name

使用 Nest CLI 搭建的项目会创建一个初始项目结构。nestjs框架集成了jest测试框架,测试文件必须以 .spec 或 .test 结尾,测试文件位于与src同级的test目录下。

  • app.e2e-spec.ts文件是实现当前程序的端到端(end to end)测试,也可视为系统测试;
  • jest-e2e.json为jest配置文件

整个过程结束了, 然后在测试文件中写测试用例。

遇到问题

  1. Import 路径问题
  • 在spec.ts文件中引入模块时,但在执行 jest 时报错,找不到对应的模块,可以使用使用相对路径解决访问,若不是相对路径,单元测试容易导致找不到对应的模块。
1
2
3
4
python复制代码# 找不到对应模块
import { UserService } from 'src/modules/user/user.service';
# 使用下面方法
import { UserService } from '../../user/user.service';
  1. 写用例时的describe ,不支持从 “describe”返回 Promise
1
2
3
4
dart复制代码describe('', ()=> { 
   it('', async ()=> {
  });
})
  1. 覆盖率和测试报告: jest --coverage
  • coverageReporters [array<string | [string, options]>]
  • Default: ["clover", "json", "lcov", "text"]
  1. 加入 jest-html-reporter 后报错:Could not resolve a module for a custom reporter
  • 因为未找到 模块,所以一直报错,配置错误,未能找到模块,使用相对路径可解决问题
  • 不建议使用 jest-html-report 显示的信息没有jest 自带的覆盖率显示的详细
  • yarn add jest-html-reporter --dev
  1. 本地测试时或者只运行某个测试用例时,可以通过 test.only() 方式。此方法适用在当你调试过程中部分方法出问题,可以进行运行这条测试。
  2. 支持按文件夹模块进行单元测试
  • 通过使用:testRegex 属性进行配置
  • 单个文件匹配:"testRegex":"user.controller.spec.ts",
  • 对个文件匹配: "testRegex": ["(user|role).*.spec.ts$"],
  • test 文件下对每个新建测试模块,然后在jest-cofing.json 中配置 "testRegex": ["./test/{testModule}/*"],

本文转载自: 掘金

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

关于函数的学习(四) — 传递列表

发表于 2021-11-29

传递列表

我们向函数传递列表很有用,这种列表包含的可能是名字、数字或更复杂的对象(如字典)。将列表传递给函数后,函数就能直接访问其内容。下面使用函数来提高处理列表的效率。

假设有一个用户列表,我们要问候其中的每位用户。下面的示例将一个名字列表传递给一个名为users()的函数,这个函数问候列表中的每个人:

1
2
3
4
5
6
7
8
handlebars复制代码def users(names):
"""向列表中的每位用户都发出简单的问候"""
for name in names:
msg = "Hello, "+name.title()+"!"
print(msg)

usernames = ['zhang', 'ji', 'bin']
users(usernames)

我们将users()定义成接受一个名字列表,并将其存储在形参names中。这个函数遍历收到的列表,并对其中的每位用户都打印一条问候语。我们定义了一个用户列表——usernames,然后调用users(),并将这个列表传递给它:

1
2
3
handlebars复制代码Hello, Zhang!
Hello, Ji!
Hello, Bin!

输出完全符合预期,每位用户都看到了一条个性化的问候语。每当你要问候一组用户时,都可调用这个函数。   

1.在函数中修改列表

将列表传递给函数后,函数就可对其进行修改。在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量的数据。

来看一家为用户提交的设计制作3D打印模型的公司。需要打印的设计存储在一个列表中,打印后移到另一个列表中。

下面是在不使用函数的情况下模拟这个过程的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
handlebars复制代码designs = ['iphone case', 'robot pendant', 'dodecahedron']
models = []

# 模拟打印每个设计,直到没有未打印的设计为止
# 打印每个设计后,都将其移到列表completed_models中
while designs:
design = designs.pop()

#模拟根据设计制作3D打印模型的过程
print("Printing model: "+design)
models.append(design)

# 显示打印好的所有模型
print("\nThe following models have been printed:")
for model in models:
print(model)

这个程序首先创建一个需要打印的设计列表,还创建一个名为的空列表,每个设计打印都将移到这个列表中。只要列表designs中还有设计,while循环就模拟打印设计的过程:从该列表末尾删除一个设计,将其存储到变量design中,并显示一条消息,指出正在打印当前的设计,再将该设计加入到列表models中。循环结束后,显示已打印的所有设计:

1
2
3
4
5
6
7
8
handlebars复制代码Printing model: dodecahedron
Printing model: robot pendant
Printing model: iphone case

The following models have been printed:
dodecahedron
robot pendant
iphone case

为重新组织这些代码,我们可编写两个函数,每个都做一件具体的工作。大部分代码都与原来相同,只是效率更高。第一个函数将负责处理打印设计的工作,而第二个将概述打印了哪些设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
handlebars复制代码def print_models(designs, models):
"""
模拟打印每个设计,直到没有未打印的设计为止
打印每个设计后,都将其移到列表models中
"""
while designs:
design = designs.pop()
# 模拟根据设计制作3D打印模型的过程
print("Printing model: "+design)
models.append(design)

def show_models(models):
"""显示打印好的所有模型"""
print("\nThe following models have been printed:")
for model in models:
print(model)

designs = ['iphone case', 'robot pendant', 'dodecahedron']
models = []
print_models(designs, models)
show_models(models)

我们定义了函数print_models(),它包含两个形参:一个需要打印的设计列表和一个打印好的模型列表。
给定这两个列表,这个函数模拟打印每个设计的过程:将设计逐个地从未打印的设计列表中取出,并加入到打印好的模型列表中。

我们定义了函数show_models(),它包含一个形参:打印好的模型列表。给定这个列表,函数show_models()显示打印出来的每个模型的名称。

这个程序的输出与未使用函数的版本相同,但组织更为有序。完成大部分工作的代码都移到了两个函数中,让主程序更容易理解。只要看看主程序,你就知道这个程序的功能容易看清得多。

我们创建了一个未打印的设计列表,还创建了一个空列表,用于存储打印好的模型。

由于我们已经定义了两个函数,因此只需调用它们并传入正确的实参即可。我们调用print_models()并向它传递两个列表;

我们调用show_models(),并将打印好的模型列表传递给它,让其能够指出打印了哪些模型。

描述性的函数名让别人阅读这些代码时也能明白,虽然其中没有任何注释。

相比于没有使用函数的版本,这个程序更容易扩展和维护。如果以后需要打印其他设计,只需再次调用print_models()即可。

如果我们发现需要对打印代码进行修改,只需修改这些代码一次,就能影响所有调用该函数的地方;与必须分别修改程序的多个地方相比,这种修改的效率更高。

每个函数都应只负责一项具体的工作。第一个函数打印每个设计,而第二个显示打印好的模型;

这优于使用一个函数来完成两项工作。编写函数时,如果你发现它执行的任务太多,请尝试将这些代码划分到两个函数中。

总是可以在一个函数中调用另一个函数,这有助于将复杂的任务划分成一系列的步骤。

2.禁止函数修改列表

有时候,需要禁止函数修改列表。例如,假设像前一个示例那样,你有一个未打印的设计列表,并编写了一个将这些设计移到打印好的模型列表中的函数。你可能会做出这样的决定:即便打印所有设计后,也要保留原来的未打印的设计列表,以供备案。但由于你将所有的设计都移出了designs,这个列表变成了空的,原来的列表没有了。为解决这个问题,可向函数传递列表的副本而不是原件;这样函数所做的任何修改都只影响副本,而丝毫不影响原件。

要将列表的副本传递给函数,可以像下面这样做:

1
handlebars复制代码function_name(list_name[:])

切片表示法[:]创建列表的副本。在print_models.py中,如果不想清空未打印的设计列表,可像下面这样调用print_models():

1
handlebars复制代码print_models(designs[:], models)

这样函数print_models()依然能够完成其工作,因为它获得了所有未打印的设计的名称,但它使用的是列表designs的副本,而不是列表designs本身。像以前一样,列表models也将包含打印好的模型的名称,但函数所做的修改不会影响到列表designs。
虽然向函数传递列表的副本可保留原始列表的内容,但除非有充分的理由需要传递副本,否则还是应该将原始列表传递给函数,因为让函数使用现成列表可避免花时间和内存创建副本,从而提高效率,在处理大型列表时尤其如此。

本文转载自: 掘金

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

Git 你也许遇到过Branch操作这些问题!

发表于 2021-11-29

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

🤞 个人主页:@青Cheng序员石头

🤞 粉丝福利:加粉丝群 一对一问题解答,获取免费的丰富简历模板、提高学习资料等,做好新时代的卷卷王!

分享一些生产过程中,使用Git管理代码过程中,需要解决的一些特定问题的解决方案。部分已经在生产开发环境测试过,能达到效果。

一、分支代码错误提交到Master分支

问题:我需要提交到一个新分支,但错误的提交到了Master。

解决方案

在master下创建一个新分支,不切换到新分支,master:

1
(master)$ git branch my-branch

把main分支重置到前一个提交:

1
(master)$ git reset --hard HEAD^

HEAD^ 是 HEAD^1 的简写,你可以通过指定要设置的HEAD来进一步重置。

或者, 如果你不想使用 HEAD^, 找到你想重置到的提交(commit)的hash(git log 能够完成), 然后重置到这个hash。 使用git push 同步内容到远程。

例如, master分支想重置到的提交的hash为a13b85e:

1
2
(master)$ git reset --hard a13b85e
HEAD is now at a13b85e

签出(checkout)刚才新建的分支继续工作:

1
(main)$ git checkout my-branch

二、不小心删除了分支

问题:不小心删除了我的某个分支,想恢复分支的数据。

解决方案:

如果你定期推送到远程, 多数情况下应该是安全的,但有些时候还是可能删除了还没有推到远程的分支。 让我们先创建一个分支和一个新的文件:

1
2
3
4
5
(main)$ git checkout -b my-branch
(my-branch)$ git branch
(my-branch)$ touch foo.txt
(my-branch)$ ls
README.md foo.txt

添加文件并做一次提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(my-branch)$ git add .
(my-branch)$ git commit -m 'foo.txt added'
(my-branch)$ foo.txt added
1 files changed, 1 insertions(+)
create mode 100644 foo.txt
(my-branch)$ git log
​
commit 4e3cd85a670ced7cc17a2b5d8d3d809ac88d5012
Author: siemiatj <siemiatj@example.com>
Date:   Wed Jul 30 00:34:10 2014 +0200
​
  foo.txt added
​
commit 69204cdf0acbab201619d95ad8295928e7f411d5
Author: Kate Hudson <katehudson@example.com>
Date:   Tue Jul 29 13:14:46 2014 -0400
​
  Fixes #6: Force pushing after amending commits

现在我们切回到主(main)分支,‘不小心的’删除my-branch分支

1
2
3
4
5
6
7
(my-branch)$ git checkout main
Switched to branch 'main'
Your branch is up-to-date with 'origin/main'.
(main)$ git branch -D my-branch
Deleted branch my-branch (was 4e3cd85).
(main)$ echo oh noes, deleted my branch!
oh noes, deleted my branch!

在这时候你应该想起了reflog, 一个升级版的日志,它存储了仓库(repo)里面所有动作的历史。

1
2
3
4
(main)$ git reflog
69204cd HEAD@{0}: checkout: moving from my-branch to main
4e3cd85 HEAD@{1}: commit: foo.txt added
69204cd HEAD@{2}: checkout: moving from main to my-branch

正如你所见,我们有一个来自删除分支的提交hash(commit hash),接下来看看是否能恢复删除了的分支。

1
2
3
4
5
6
(main)$ git checkout -b my-branch-help
Switched to a new branch 'my-branch-help'
(my-branch-help)$ git reset --hard 4e3cd85
HEAD is now at 4e3cd85 foo.txt added
(my-branch-help)$ ls
README.md foo.txt

看! 我们把删除的文件找回来了。 Git的 reflog 在rebasing出错的时候也是同样有用的。

三、明确想删除一个分支

问题:明确的想删除一个无用的分支。

解决方案

删除一个远程分支:

1
(main)$ git push origin --delete my-branch

你也可以:

1
(main)$ git push origin :my-branch

删除一个本地分支:

1
(main)$ git branch -D my-branch

少年,没看够?点击石头的详情介绍,随便点点看看,说不定有惊喜呢?欢迎支持点赞/关注/评论,有你们的支持是我更文最大的动力,多谢啦!

本文转载自: 掘金

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

你想过吗,为什么说面向对象最符合人的思维?

发表于 2021-11-29

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

在学习Java的过程中,我觉得面向对象是我遇到的第二个难题(第一个就是配置环境变量,哈哈)。我相信也有很多同胞也在这里被绊脚了吧,今天的话也就是其实也只是作为笔记写的这篇博客。

一、面向对象

这里也就不讲的太官方了,简单来说,我们编程都是基于对象的。就是我们做数学计算需要用Math类,我们画图需要Graphic类,我们操作文件需要File类。。。要具体解释什么是基于对象,那就要像讲两个概念。

1、类

类的话,通俗来说就是菜谱、模具之类的东西。我们可以根据菜单做出菜,可以根据模具做出相应的东西。类就和它们类似,通过类我们可以做出相应的实体,也就是对象。

2、对象

在学习面向对象时,经常会遇到“万物皆对象”这句话。其实这就已经解释了啥是对象。像普通的现实生活中,一个人,一条狗,一只猪,这种对象比较好理解。再抽象一点,一个多用的螺丝刀也是一个对象。而里面每个工具都是它的属性(Field)或者方法(Method)。

类别Java代码的话,人就是通过Person类new出来的一个对象。多用螺丝刀就是一个工具类,每种螺丝刀就是一只方法。下面举个例子,先写一个人的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arduino复制代码/**
* 人类
*/
public class Person(){

String name;

int age;

String sex;

/**
* 构造方法
*/
public Person(){

}
}

这个时候我们可以通过构造方法,创建人的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码public class Test{

public static void main(String[] args){

//创建名为zack的Person类对象
Person zack = new Person();

//设置zack的属性
zack.name = "zack";
zack.age = 20;
zack.sex = "male";

}

}

接下来写个工具类,多用螺丝刀:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csharp复制代码public class Tools{

/*十字螺丝刀*/
public void toolForTen(){
System.out.println("我是一把十字螺丝刀");
}


/*一字螺丝刀*/
public void toolForOne(){
System.out.println("我是一把一字螺丝刀");
}

/*手机螺丝刀*/
public void toolForPhone(){
System.out.println("我是一把手机螺丝刀");
}

}

然后我们来使用这个工具箱:

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码public static void main(String[] args){

//先创建(制造)一个多用螺丝刀
Tool tool = new Tool();

//用螺丝刀开手机
tool.toolForPhone();

//用螺丝刀开十字螺丝
tool.toolForTen();

}

关于面向对象的概念就先说这么多,然后说说面向对象的三大特性。

二、封装

1、封装思想

这里的话,也不讲多复杂。有两个计较贴切的词语“隐藏细节”、“数据安全”。直白的说,就是有所保留。我们前面写的Person类,对于女性来说年龄是比较敏感的。但是我们只要把这个女性创建出来,就可以自己访问或者设置她的年龄,这样就非常的不安全,没有隐私可言。这个时候就需要用到封装思想。

2、封装的实现

主要的话就是给属性和方法设置访问权限,一般的话就是把全部属性(变量)设置为private,然后创建get、set方法。按照实际情况或者程序员的期望给方法设置不同权限。下面举个例子:

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
arduino复制代码/**
* 人类,利用封装思想改进代码
*
* 在这个类中sex为私有变量,且为提供相应的方法获取,故外部不能直接获取Person的性别
*/
public class Person{

private String name;

private int age;

private String sex;

/**
* 允许其他人知道我的名字,故设置为public(公共的)
*/
public String getName(){
return name;
}

public void setName(String name){
this.name = name;
}

}

修改之后性别就成了隐私了(当然了,在反射面前还是不值一提)。不过这些修饰词只在编译期起作用,所以这些属性其实还是可以通过反射获取。

这边的再扩展一下其它权限修饰词,就一张图,我盗来的:

​

三、继承

1、父类/基类

我在一篇文章中看到:不是先有父类(基类)再有子类,而是在不同类中发现许多共同点,然后把这些共同点抽出一个父类。先不说对错,我觉得这句话很好的解释了父类的意义。通俗来说的话,父类就是一个类,被继承的类。

2、子类

子类就是继承别人的类。

上面的解释好像毫无意义,但是把父类子类拆开讲本身就是毫无意义的。

3、继承的含义及性质

3.1、继承的话以来与关键词extends(延伸),就如它的意思,延伸、扩展。可以理解为,子类是父类的延伸


3.2、继承的话只能继承父类的非私有变量和方法


3.3、子类创建时会调用父类的构造方法

为了让大家深刻理解继承,这里多说几句(这里不是严格讲生物,希望不要有生物专业的学生揪我错哈?)。最开始的时候,人类对动物的了解少之右少,觉得生物大致就是动物和人。随着对生物的理解,发现了各种分类方法:界、门、纲、目、科、属、种。我们这里也不细揪,就拿老虎 狮子打比方。本来是两种不相干的动物,但是随着,发现这两种动物有许多共同点。在研究更多动物之后,发现有许多动物和狮子老虎也有着共同点,随之而来的就是我们所谓的猫科动物。而这里的猫科动物就是一个父类,老虎狮子都继承了这个类。而后又在犬科动物或者其它科动物里面发现各个科的动物又有些共同点,然后又抽出了哺乳动物。就这样慢慢的,对动物的理解越来越方便,也越来越深刻。而Java中继承也是这个概念。

接下来,用代码解释一下,先编写个猫科动物类:

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
csharp复制代码/**
* 猫科动物的类
*/
public class Felidae{

protected String name;

protected int age;

//省略get、set方法

public void run(){
System.out.println(name + "在跑步");
}

public void eat(){
System.out.println(name + "在吃东西");
}

/**
* 无参构造
*/
public Felidae(){

}

/**
* 带参构造
*/
public Felidae(String name){
this.name = name;
}

}

然后定义一个老虎类继承猫科动物类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scala复制代码/**
* 老虎类,继承猫科动物
*
* 在编写老虎类时,不需要考虑猫科动物的共性(年龄/名称)。
*
*/
public class Tiger extends Felidae{
//不是所有猫科动物都有花纹,其它什么特点我也想不到了就不纠结了
private String pattern;

//老虎会游泳
public void swim(){
System.out.println("老虎在游泳");
}

}

因为想象力匮乏,就这样简单介绍一下。这个继承了猫科动物的老虎没有定义name和age,但实际上我是可以使用这两个变量的:

1
2
3
4
5
6
7
8
9
java复制代码@Test
public void demo1(){
Tiger tiger = new Tiger();

//Tiger中没有setName()方法,但是Tiger继承了Felidae
//中的get/set方法(当然了,我在写类的时候省略了)
tiger.setName("rudy");
tiger.setAge(4);
}

四、多态

1、多态的关键在于继承、方法重写、方法重载

2、方法重写:在子类中创建一个与父类中同名,参数相同的方法。如下:

先写个Animal类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
csharp复制代码/**
* 动物类
*/
public class Animal{

protected String name;

protected String sex;

protected int age;

public void eat(){
System.out.println("动物会吃饭");
}

public void walk(){
System.out.println("动物会走路");
}

public void sleep(){
System.out.println("动物会睡觉");
}

}

然后定义一个Cat类,继承Animal,这里只重写一个方法用来举例:

1
2
3
4
5
6
7
scala复制代码public class Cat extends Animal{

public void eat(){
System.out.println("猫会吃东西");
}

}

这个时候调用Cat的eat方法的结果就是:

1
复制代码猫会吃东西

而父类中的eat方法被覆盖重写了。

3、方法重载,就是在同一个类中,方法名相同,但是传入参数不同,前面我们已经用到了方法重载。就是构造方法,有无参构造和有参构造,这就是一种方法重载,下面再给大家举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码public class Cat extends Animal{

public void eat(){
System.out.println("猫会吃东西");
}

public void eat(String name){
System.out.println(name + "猫会吃东西");
}

public void eat(String name, String food){
System.out.println(name + "会吃" + food);
}

}

上面三个方法同名,但是参数不同,在使用的时候可以通过传入参数自动识别是哪个方法,所以就实现了方法重载的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Test
public void demo1(){
Cat cat = new Cat();

//输出结果为:猫会吃东西
cat.eat();

//输出结果为:Tom会吃东西
cat.eat("Tom");

//输出结果为:Tom会吃Jerry
cat.eat("Tom", "Jerry");
}

4、多态三个必要条件继承、方法重写、父类引用指向子类:

1
2
3
4
5
6
7
8
9
java复制代码@Test
public void demo1(){
//前面创建Cat的方式可以修改为,这就是父类的引用指向子类
Animal cat = new Cat();

//在Cat中重写了Animal的eat()方法
cat.eat();

}

那么这段代码输出结果是:

1
复制代码猫会吃东西

因为是通过Cat类new出来的,而Cat中有一个eat方法,把Animal中的eat方法重写了。所以最后的结果是“猫会吃东西”。

面向对象中还有很多奇妙的东西,需要你们自己慢慢探索。

​

本文转载自: 掘金

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

高频算法面试题(四十四)- 二叉树的所有路径

发表于 2021-11-29

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

刷算法题,从来不是为了记题,而是练习把实际的问题抽象成具体的数据结构或算法模型,然后利用对应的数据结构或算法模型来进行解题。个人觉得,带着这种思维刷题,不仅能解决面试问题,也能更多的学会在日常工作中思考,如何将实际的场景抽象成相应的算法模型,从而提高代码的质量和性能

二叉搜索树与双向链表

题目来源:LeetCode-257. 二叉树的所有路径

题目描述

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点

示例 1:

1.png

1
2
css复制代码输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]

示例 2:

1
2
css复制代码输入:root = [1]
输出:["1"]

提示:

  • 树中节点的数目在范围 [1, 100] 内
  • 100 <= Node.val <= 100

解题

解法一:深度优先搜索

思路

如果你看过我前边发的题的话,它跟我前边发的

二叉树中和为某一值的路径(一)

二叉树中和为某一值的路径(二)

二叉树中的最大路径和

这三道题是差不多的,本题等于是解这三道题的基础,首先能知道如何获取所有的路径,才能在获取路径的过程中,根据一些条件来过滤掉一些不满足条件的路径

本题比较简单,其实就是遍历,关键是如何遍历。我们知道,要获取到所有路径,需要知道什么情况下,我已经遍历完一条路径了?那肯定就是遍历到叶子结点了。因此,遍历的过程中,我们可以分非叶子节点和叶子节点两种情况进行出来

  • 如果当前节点不是叶子节点,则在当前的路径末尾添加该节点,并继续递归遍历该节点的每一个孩子节点
  • 如果当前节点是叶子节点,则在当前路径末尾添加该节点后我们就得到了一条从根节点到叶子节点的路径,将该路径加入到结果路径数组中即可

这是典型的前序遍历,也是深度优先搜索的思想,下边就是具体代码的实现

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码//二叉树的所有路径
//深度优先搜索
var treePath []string
func binaryTreePaths(root *TreeNode) []string {
treePath = []string{}
dfsEachPath(root, "")
return treePath
}

func dfsEachPath(root *TreeNode, path string) {
if root != nil {
eachPath := path
eachPath += strconv.Itoa(root.Val)
if root.Left == nil && root.Right == nil {
treePath = append(treePath, eachPath)
} else {
eachPath += "->"
dfsEachPath(root.Left, eachPath)
dfsEachPath(root.Right, eachPath)
}
}
}

解法二:广度优先搜索

思路

前边已经做了很多二叉树的题,很多都用深度和广度优先搜索进行解题,基本上用深度优先搜索能解的,都可以尝试用广度优先搜索来解

广度优先搜索首先我们知道需要借助队列,本题我们需要维护两个队列,分别用来记录每一层的节点和根节点到当前节点的路径

如果遍历的当前节点是叶子结点,则说明当前这条路径可以放到结果集中了;如果是非叶子结点,则将它的子节点放入到节点队列中

代码

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复制代码//广度优先搜索实现
func binaryTreePaths2(root *TreeNode) []string {
paths := []string{}
if root == nil {
return paths
}
nodeQueue := []*TreeNode{root}
pathQueue := []string{strconv.Itoa(root.Val)}
for i :=0; i < len(nodeQueue); i++ {
node, path := nodeQueue[i], pathQueue[i]
if node.Left == nil && node.Right == nil {
paths = append(paths, path)
continue
}
if node.Left != nil {
nodeQueue = append(nodeQueue, node.Left)
pathQueue = append(pathQueue, path+"->"+strconv.Itoa(node.Left.Val))
}
if node.Right != nil {
nodeQueue = append(nodeQueue, node.Right)
pathQueue = append(pathQueue, path+"->"+strconv.Itoa(node.Right.Val))
}
}

return paths
}

本文转载自: 掘金

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

如何在 django 应用程序中自动识别 n+1 个查询(二

发表于 2021-11-29

问题:

  • 1、日志输出在控制台
  • 2、从输出的日志中并不能确定具体是哪一个接口出现的问题

改进:

  • 1、将日志保存在文件中,方便我们定期检查哪些接口问题存在问题
  • 2、修改日志格式,将当前的url拼接在日志格式中,便于我们定位问题
1
2
3
4
csharp复制代码格式:
Potential n+1 query detected on `<model>.<field>`
输出:
2021-11-29 11:20:24,515;WARNING;notifiers.py:40;Potential n+1 query detected on `OutboundOrder.created_by`

结果:

f1e81d2f629d68b91c9801ada9a4fde.png

创建nplusone.py

1、重新定义日志格式

将当前接口的url拼接在日志格式上

1
2
3
4
5
6
7
8
ini复制代码class LazyLoadMessage2(Message2):
    label = 'n_plus_one'
    formatter = '{url}:Potential n+1 query detected on `{model}.{field}`'


class EagerLoadMessage2(Message2):
    label = 'unused_eager_load'
    formatter = '{url}:Potential unnecessary eager load detected on `{model}.{field}`'

2、重写Message

将url拼接在日志中

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码class Message2(Message):
    def __init__(self, model, field, url):
        self.url = url
        super(Message2, self).__init__(model, field)

    @property
    def message(self):
        return self.formatter.format(
            label=self.label,
            model=self.model.__name__,
            field=self.field,
            url=self.url
        )

3、重写监听函数

setup方法新增request参数,handle_lazy中将当前接口路径传给对应的Message

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
python复制代码class LazyListener2(LazyListener):
    def setup(self, **kwargs):
        self.request = kwargs.get('request', None)
        super(LazyListener2, self).setup()

    def handle_lazy(self, caller, args=None, kwargs=None, context=None, ret=None,
                    parser=None):
        model, instance, field = parser(args, kwargs, context)
        if instance in self.loaded and instance not in self.ignore:
            url = self.request.path if self.request else '' 
            message = LazyLoadMessage2(model, field, url)
            self.parent.notify(message)


class EagerListener2(EagerListener):
    def setup(self, **kwargs):
        self.request = kwargs.get('request', None)
        super(EagerListener2, self).setup()

    def log_eager(self):
        self.tracker.prune([each for each in self.touched if each])
        for model, field in self.tracker.unused:
            url = self.request.path if self.request else ''
            message = EagerLoadMessage2(model, field, url)
            self.parent.notify(message)


listeners2 = {
    'lazy_load': LazyListener2,
    'eager_load': EagerListener2,
}

4、重写中间件NPlusOneMiddleware

这里我们只需要将process_request方法中的listeners.listeners改成我们上面重写的listeners2

1
2
3
4
5
6
7
8
ruby复制代码class NPlusOneMiddleware2(NPlusOneMiddleware):

    def process_request(self, request):
        self.load_config()
        self.listeners[request] = self.listeners.get(request, {})
        for name, listener_type in six.iteritems(listeners2):
            self.listeners[request][name] = listener_type(self)
            self.listeners[request][name].setup(request=request)

5、修改settings

重新导入中间件和日志输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
arduino复制代码MIDDLEWARE = (
    'common.nplusone.NPlusOneMiddleware2',
    ...
)

LOGGING = {
    ...
    'handlers': {
        ...
        'nplusone': {
            'level': 'DEBUG',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': LOG_DIR / 'nplusone.log',
            'formatter': 'standard'
        }
    },
    'loggers': {
        ...
        'nplusone': {
            'handlers': ['nplusone'],
            'level': 'WARN',
        },
    }
}

扫码_搜索联合传播样式-标准色版.png

本文转载自: 掘金

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

撸了一个可调试 gRPC 的 GUI 客户端 前言 核心功能

发表于 2021-11-29

前言

平时大家写完 gRPC 接口后是如何测试的?往往有以下几个方法:

  1. 写单测代码,自己模拟客户端测试。
  2. 可以搭一个 gRPC-Gateway 服务,这样就可以在 postman 中进行模拟。

但这两种方法都不是特别优雅;第一种方法当请求结构体嵌套特别复杂时,在代码中维护起来就不是很直观;而且代码会特别长。

第二种方法在 postman 中与请求 HTTP 接口一样,看起来非常直观;但需要额为维护一个 gRPC-Gateway 服务,同时接口定义发生变化时也得重新发布,使用起来稍显复杂。

于是我经过一番搜索找到了两个看起来还不错的工具:

  • BloomRPC
  • github.com/fullstoryde…

首先看 BloomRPC 页面美观,功能也很完善;但却有个非常难受的地方,那就是不支持 int64 数据的请求, 会有精度问题。

这里我写了一个简单的接口,直接将请求的 int64 返回回来。

1
2
3
4
5
6
7
go复制代码func (o *Order) Create(ctx context.Context, in *v1.OrderApiCreate) (*v1.Order, error) {
fmt.Println(in.OrderId)
return &v1.Order{
OrderId: in.OrderId,
Reason: nil,
}, nil
}

会发现服务端收到的数据精度已经丢失了。

这个在我们大量使用 int64 的业务中非常难受,大部分接口都没法用了。



grpcui 是我在使用了 BloomRPC 一段时间之后才发现的工具,功能也比较完善; BloomRPC 中的精度问题也不存在。

但由于我之前已经习惯了在 BloomRPC 中去调试接口,加上日常开发过程中我的浏览器几乎都是开了几十个 tap 页面,导致在其中找到 grpcui 不是那么方便。

所以我就想着能不能有一个类似于 BloomRPC 的独立 APP,也支持 int64 的工具。


准备

找了一圈,貌似没有发现。恰好前段时间写了一个 gRPC 的压测工具,其实已经把该 APP 需要的核心功能也就是泛化调用实现了。

由于核心能力是用 Go 实现的,所以这个 APP 最好也是用 Go 来写,这样复用代码会更方便一些;正好也想看看用 Go 来实现 GUI 应用效果如何。

但可惜 Go 并没有提供原生的 GUI 库支持,最后翻来找去发现了一个库:fyne

从 star 上看用的比较多,同时也支持跨平台打包;所以最终就决定使用该库在构建这个应用。

核心功能

整个 App 的交互流程我参考了 BloomRPC ,但作为一个不懂审美、设计的后端开发来说,整个过程中最难的就是布局了。

这是我花了好几个晚上调试出来的第一版页面,虽然也能用但查看请求和响应数据非常不方便。

于是又花了一个周末最终版如下(乍一看貌似没区别):

虽然页面上与 BloomRPC 还有一定差距,但也不影响使用;关键是 int64 的问题解决了;又可以愉快的撸码了。

安装

有类似需求也想体验的朋友可以在这里下载使用:
github.com/crossoverJi…

由于我手上暂时没有 Windows 电脑,所以就没有打包 exe 程序;有相关需求的朋友可以自行下载源码编译:

1
2
3
go复制代码git clone git@github.com:crossoverJie/ptg.git
cd ptg
make pkg-win

后续计划

当前版本的功能还比较简陋,只支持常用的 unary 调用;后续也会逐步加上 stream、metadata、工作空间的存储与还原等支持。

对页面、交互有建议也欢迎提出。

原本是准备上传到 brew 方便安装的,结果折腾了一晚上因为数据不够被拒了,所以对大家有帮助或者感兴趣的话帮忙点点关注(咋有种直播带货的感觉🐶)

源码地址:github.com/crossoverJi…

本文转载自: 掘金

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

你在工作中遇到过印象深刻的困难是什么,你怎么克服的?

发表于 2021-11-29

你好呀,我是歪歪。

这期我想简单的聊一个面试中出现频率比较高的,但又没有标准答案的面试题。

你在工作中遇到过印象深刻的困难是什么,你怎么克服的?

为什么我想聊聊这个问题呢?

因为我发现这个问题经常出现在各个技术交流群中,大家聊到这个话题的时候大多都苦不堪言,纷纷表示不知道怎么去回答这个问题。

或者说之前就没有想过这样的问题,突然一下被问起来,由于没有准备,也是摸不着头脑的样子。

匆匆的回顾一下自己的职业生涯,发现天天写的都是 crud,也没觉得有什么困难啊。

一时间,竟然想脱口而出:我觉得吧,也没有啥特别大的困难,我做的就挺好的。

面试官听了微微一笑:好了,那我们今天的面试就先到这里,请回去等通知吧。

考的是什么

你必须要知道正常情况下面试官在面试的过程中问的每一个问题,都一定是有他的目的。

比如面试官上来就要求候选人做个简单的自我介绍,很多人说这个目的是为了在候选人自我介绍的时间内看一下他的简历。

也许在早几年,要候选人自己带着简历去面试的情况下,确实是这样的。

但是现在来说,都是无纸化面试了,你的简历的电子版早就到面试官手上了。

正常来说,面试官会在面试之前已经看过你的简历了,不需要面试的时候借着你自我介绍的时间,浏览简历。

我一般让候选人自我介绍的时候,我是在认真的听,我想要从他的自我介绍找挖掘到简历上没有体现的东西,也是在寻找面试的切入点,如果自我介绍中有让我感兴趣的地方,我就会从这个地方开始,围绕着简历往下问。

再比如,问你项目的时候:

说一下你最熟悉的项目。

是问你项目是干啥的,业务场景有哪些吗?

不是的。

问这个问题的目的是想知道你所熟悉的项目的架构是怎么样的,是单体服务还是拆了微服务,拆了哪些模块,每个服务大概多大的体量,它们之间是怎么相互的,涉及到的技术栈有哪些。

知道了这些,面试官才能从中找到讨论的点,从而展开技术面试。

至于项目是干啥的,简单说几句,铺垫一个背景就行了。

有的同学介绍项目的时候把领导在业务上给他画的饼,又给面试官描述一遍。如果不是同一业务线的话,面试官是不会关心你的业务的。

你要知道,如果你要介绍业务场景的话,其目的必然是为了引出背后的一个比较复杂的技术解决方案。否则,面试官不会太感兴趣,多说无益,反而占用了面试的时长。

还不如拿出纸笔,在上面画一下你们的服务交互,同时描述一下对应地方涉及到的技术栈。

你要不会画的话,可以参考一下我之前分享的这篇文章:

面试官:给我画一下项目架构图吧。

再说这个问题:

你在工作中遇到过印象深刻的困难是什么,你怎么克服的?

有的同学说他不会回答,我分析了一下,不会回答的原因其实就是因为不知道面试官考察的是什么方向。

所以只能给出一些诸如查询慢了就加索引、热点数据加缓存、出了问题重启了就好了…这类泛泛地回答,找不到什么让面试官眼前一亮的东西。

怎么能答的闪亮一点呢?

一般来说,我认为这个题有两个回答的方向。

第一个方向就是往技术的深度,对于技术的追求这个方向答。想看看你有没有碰到过什么棘手的技术问题,然后是怎么定位,怎么解决的。

第二个方向就是往主人翁意识,体现主观能动性的方向答。面对一个项目或者领导给到的任务涉及到其他项目组、甚至其他部门的时候,自己是怎么去推动的。

技术的深度

如果你往这个方法答,就得自己平时工作中多积累,多观察相关的案例,然后记录下来。

可以把观察的目光放的长远一点,不一定非得是自己所在的项目组遇到的问题,也可以是其他的项目组遇到的问题。

这里就需要自己有一个情报收集的能力,和对于技术的敏感度。

一听到这问题就应该要知道:这是一个好素材呀,可以深入了解一下。

这个问题都不一定是你解决的,但是你要清楚的知道来龙去脉,就可以包装成自己的经历。

面试官是察觉不出来的。

而且我一直认为,适度的包装,也不算是面试造假。

当然了,这个方向你也可以去背。

但是不能纯粹的背诵,得适当的去扩展。

比如我之前分享过这样的一篇文章:

这个生产案例,好,很好,非常好。

这个案例从最开始 Dubbo 调用超时的这个表象,分别从数据库、GC、网络、链路追踪等各个角度去分析了问题,且是一个循序渐进的过程。

你会发现大家对于超时这一类的问题的排查套路都无外乎这样,层层递进的排查,抽丝剥茧的寻找问题。

这个案例你就是可以自己拿去用的,套一个自己工作相关的业务场景。

我就不信了,你们接口调用没出现过超时的情况?

网上这样的文章很多很多,但是作者写的只专注于面试问题的本身。

如果你想要把这个案例套过来自己用,那么而这个问题能延伸出来的东西,你也必须得去研究。

比如前面这个文章里面,为什么要说“失败策略是 failFast,快速失败不会重试”?因为如果是failover,会默认重试,且超时时间是重试时间之和。所以,他告诉我们,这里没有重试,超时不是因为请求重试带来的时间叠加导致的。

文章提到的 ElapsedFilter 过滤器,“超过 300 毫秒的接口耗时都会打印”,是作者公司自己扩展的 Filter,基于Dubbo 的 SPI 实现的,并不是 Dubbo 官方的自带功能。所以,他才额外提了一句“ElapsedFilter是 Dubbo SPI 机制的(自定义)扩展点之一”。

作者用的 Druid 连接池,猜测连接长时间不被使用后都回收了,那么关于 Druid 的配置文件中的有关时间的配置,你是否知道且清楚其作用?

如果要观察 GC 日志,你是否大概知道应该配置什么参数,是否知道应该关注的信息是什么?为什么他这里要提到安全点?安全点和 STW 的时间之间的关系又是什么?

等等后面的一些关于容器的、Arthas工具使用的、网络抓包工具使用的相关技能和知识储备。

当我们把这些知识单独拎出来形成面试题的时候,也许你会觉得,为什么你老是问我 MySQL 的知识、问我网络相关的知识、问我一些用不上的垃圾回收的知识?

问你,把你问的哑口无言不是目的。考察你知识的广度,让你学以致用才是目的。

重要的是把你学的一个个孤立的点,通过某种方式,串联起来。

而“你在工作中遇到过印象深刻的困难是什么”就是你把这些知识点串联起来的一种方式。

除了前面的文章,我还分享这些类似的,你以为我只是单纯的给你分享吗?

不是的,我自己也在收集,也在融会贯通,也想着“拿来主义”:

一个不错的线上故障排查案例,现在它是你的了。

你在工作中有没有碰到什么比较棘手的问题?

笑了,面试官问我遇到过线上问题没有?

我给Apache顶级项目提了个Bug

另外,还有一个人尽皆知的面试小窍门。

回答问题的时候尽量有意识的引导面试官到自己熟悉的领域中来。

怎么引导呢?

不可能别人问你:你给我说一下线程池吧?

你回答说:这多没意思啊,我给你说一下 HashMap 吧。

面试官一定当时就觉得自己的头大。

我们可以在这些开放的问题上就可以去引导面试官。

如果你对 kafak、RabbitMQ、RocketMQ 这一类技术了解的比较深入,又或者对于 Redis、MySQL 这一类存储的技术学习的比较多,你准备这类问题的时候就可以多讲这方面的原因。

比如如果让我来讲,我可能就会选择回答一些由于 Dubbo 框带来的技术问题,让面试官进入到我熟悉的领域,让他在这里面和我展开博弈。

再或者说往 JVM 方向引导,反正大家学这东西,看的都是同一份资料,就看你记得多还是我记得多了。

又或者我们可以 battle 一下多线程领域相关的问题,但是现在多线程都烂大街了,我可能不太会去在这里面和面试官博弈太长时间。因为就算你回答的滴水不漏,面试官大多也会认为这只是需要掌握的基本技能而已,用的熟练,不足为奇,没有闪光点。

总之一句话吧:如果你向往技术的深度这方面去回答,一定要言之有物,最终定位到的问题可以是一个很小的问题,比如配置的原因、网络的原因、框架的 bug,但是重点得体现出排查的过程。而排查问题的过程,有一定的方法论,提炼出来就好了。

对于这个问题,上策是加工一下自己的亲身经历,实实在在的有解决问题的经验,只是如何把它包装的高大上而已。

下策是包装别人的经历,要包的惟妙惟肖,以假乱真。

如果你真的很无奈要选下策,那么我只能再送你一句话了:加入一些细节的描述,可以是点击工具的什么按钮、翻看了什么类的源码、参照了某个大牛的博客一类的。

能不能过,就看你的造化了。

主观能动性

主观能动性这方面其实我没有什么特别想要说的。

核心思想就是前面说的:主人翁意识。

你所负责的任务,是别人分配给你的。但是你就是这个任务的主人翁,你要想着怎么去积极主动的去完成它。

举个例子:

你本来只是一个写着 crud 的快乐的程序员,每天等着领导分配任务,然后领任务写代码。

突然有一天,领导给你说:小王啊,这边有一个项目很着急,但是我这边有更加紧急的事情要做,所以我把这个任务分配给你,你去全权负责一下吧。

你当时就懵逼了,敲键盘的手一下就不快乐了。

这意味着你不再是一个只写代码的程序员了,你还是一个项目的负责人。

一个项目的负责人得统筹帷幄,去协调需求、产品、开发、测试、运维等各方面的资源。

而这事儿,你之前从来没干过。要命的是,这事还挺着急。

怎么办?

这不就是你在工作中遇到的印象深刻的困难吗?

场景框架都给你了,你就按照你们公司的流程往里面填内容就行了。

你是怎么拆分任务的,怎么组织评审会的,最后项目成功上线自己的收获是什么。

可以多讲点从程序员视野看不到的东西。着重体现自己协调资源和跨部门合作时遇到的困难和自己的解决方案。

什么,你说你没有这个方面的经历?

你就不能假设吗?

你就不能观察你们公司的一个项目周期的全过程吗?

得发挥你的主观能动性呀。

然后再说一下,如果你往这个方向去回答,大概率会遇到一个追问的问题:

如果让你再来一次,你会怎么处理的更好。

这玩意考的又是什么?

考的就是你的复盘的能力,考的就是有没有对于项目进行复盘。

如果你回答说:由于是我第一次负责一个项目,并跟进了它一期需求的全过程,所以对我来说是一次非常宝贵的经历,于是在项目上线之后我对其进行了一次复盘,发现了其中还有一二三四点可以优化的地方…

我觉得基本上朝着这个方法回答就十拿九稳了。

你要说你不会复盘,好的,没救了,回去等通知吧。

总结

面试的时候对于这类开放性题目,其实并不是想象中的那么好回答,处处都是暗流涌动。

所以一定要在面试前做好这类题目的准备,临场发挥肯定是效果很一般的。

就拿我个人作为面试官的经历来说,特别是三到五年工作经验的朋友容易遇到这个问题。

校招生我肯定是不问这个问题的。三年以下的经验还不够丰富,回答起来也很难有什么满意的答案,所以我也不问。问这个还不如多问几个技术问题,考察他的技术是否扎实。

还有,前面说的两个方向,都得准备一下。

如果是前两轮面试问题了,可以往技术的方法回答,因为这个时候一般都是在一线编码的程序员充当技术面试官,他更愿意在技术方面和你切磋。

如果是后几轮面试,可以往主人翁意识的方向去回答,因为到后面的面试大多都是部门负责人一类的管理人员,已经很少在一线编码了,他们更愿意看到你除了技术之外的软实力。

最后说一句

好了,看到了这里了,转发、在看、点赞随便安排一个吧,要是你都安排上我也不介意。写文章很累的,需要一点正反馈。

给各位读者朋友们磕一个了:

本文已收录自个人博客,欢迎大家来玩。

www.whywhy.vip/

本文转载自: 掘金

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

2《数据库实战案例》-数据库sql优化总结之1-百万级数据库

发表于 2021-11-29

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

❤️作者简介:大家好,我是小虚竹。Java领域优质创作者🏆,CSDN博客专家认证🏆,华为云享专家认证🏆

❤️技术活,该赏

❤️点赞 👍 收藏 ⭐再看,养成习惯


项目背景

有三张百万级数据表

知识点表(ex_subject_point)9,316条数据

试题表(ex_question_junior)2,159,519条数据 有45个字段

知识点试题关系表(ex_question_r_knowledge)3,156,155条数据

测试数据库为:mysql (5.7)

1.对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

案例分析:

1
2
3
sql复制代码SELECT ex_question_junior.QUESTION_ID 
FROM ex_question_junior
WHERE ex_question_junior.GRADE_ID=1

    执行时间:17.609s (多次执行,在17s左右徘徊)

  优化后:给GRADE_ID字段添加索引后

  执行时间为:11.377s(多次执行,在11s左右徘徊)

  备注:我们一般在什么字段上建索引?

  这是一个非常复杂的话题,需要对业务及数据充分分析后再能得出结果。主键及外键通常都要有索引,其它需要建索引的字段应满足以下条件:

    a、字段出现在查询条件中,并且查询条件可以使用索引;

    b、语句执行频率高,一天会有几千次以上;

    c、通过字段条件可筛选的记录集很小,那数据筛选比例是多少才适合?

  这个没有固定值,需要根据表数据量来评估,以下是经验公式,可用于快速评估:

  小表(记录数小于10000行的表):筛选比例<10%;

  大表:(筛选返回记录数)<(表总记录数*单条记录长度)/10000/16

  单条记录长度≈字段平均内容长度之和+字段数*2

  以下是一些字段是否需要建B-TREE索引的经验分类:

2、应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描

select id from t where num is null

最好不要给数据库留NULL,尽可能的使用 NOT NULL填充数据库.

备注、描述、评论之类的可以设置为 NULL,其他的,最好不要使用NULL。

不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(NULL也包含在内),都是占用 100个字符的空间的,如果是varchar这样的变长字段, null 不占用空间。

可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:

select id from t where num = 0

案例分析:

在mysql数据库中对字段进行null值判断,是不会放弃使用索引而进行全表扫描的。

1
2
3
sql复制代码SELECT ex_question_junior.QUESTION_ID
FROM ex_question_junior
WHERE IS_USE is NULL

执行时间是:11.729s

1
2
3
sql复制代码SELECT ex_question_junior.QUESTION_ID
FROM ex_question_junior
WHERE IS_USE =0

执行时间是12.253s

时间几乎一样。

3、应尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描。

案例分析:

在mysql数据库中where 子句中使用 != 或 <> 操作符,引擎不会放弃使用索引。

1
2
3
4
sql复制代码EXPLAIN
SELECT ex_question_junior.QUESTION_ID
FROM ex_question_junior
WHERE ex_question_junior.GRADE_ID !=15

执行时间是:17.579s

执行时间是:16.966s

4.应尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描

案例分析:

GRADE_ID字段有索引,QUESTION_TYPE没索引

执行时间是:11.661s

优化方案:

通过union all 方式,把有索引字段和非索引字段分开。索引字段就有效果了

执行时间是:11.811s

但是,非索引字段依然查询速度会很慢,所以查询条件,能加索引的尽量加索引

5.in 和 not in 也要慎用,否则会导致全表扫描

案例分析

注:在mysql数据库中where 子句中对索引字段使用 in 和 not in操作符,引擎不会放弃使用索引。

注:在mysql数据库中where 子句中对不是索引字段使用 in 和 not in操作符,会导致全表扫描。

案例分析2:

用between和in的区别

1
2
3
sql复制代码SELECT ex_question_junior.QUESTION_ID
FROM ex_question_junior
WHERE ex_question_junior.QUESTION_TYPE IN(1,2,3,4)

执行时间为1.082s

1
2
3
sql复制代码SELECT ex_question_junior.QUESTION_ID
FROM ex_question_junior
WHERE ex_question_junior.QUESTION_TYPE between 1 and 4

执行时间为0.924s

时间上是相差不多的

案例分析3:

用exists 和 in区别:结论

用exists 和 in区别:结论

  1. in()适合B表比A表数据大的情况(A<B)

select * from A

where id in(select id from B)

  1. exists()适合B表比A表数据小的情况(A>B)

  select * from A

  where exists(

  select 1 from B where B.id = A.id

  )

3.当A表数据与B表数据一样大时,in与exists效率差不多,可任选一个使用.语法

select * from A

where id in(select id from B)

ex_question_r_knowledge表数据量大,ex_subject_point表数据量小

****************************************************************************

ex_question_r_knowledge(A)表数据量大,ex_subject_point表数据量小(B)(A>B)

用exists适合

1
2
3
4
5
6
7
8
sql复制代码SELECT *
FROM ex_question_r_knowledge
WHERE ex_question_r_knowledge.SUBJECT_POINT_ID IN
(
SELECT ex_subject_point.SUBJECT_POINT_ID
FROM ex_subject_point
WHERE ex_subject_point.SUBJECT_ID=7
)
1
2
3
4
5
6
7
8
9
sql复制代码SELECT *
FROM ex_question_r_knowledge
WHERE exists
(
SELECT 1
FROM ex_subject_point
WHERE ex_subject_point.SUBJECT_ID=7
AND ex_subject_point.SUBJECT_POINT_ID = ex_question_r_knowledge.SUBJECT_POINT_ID
)

执行时间是:13.537s

*************************************************************************

ex_subject_point表数据量小(A),ex_question_r_knowledge(B)表数据量大(A<B)

用in适合

1
2
3
4
5
6
7
sql复制代码SELECT *
FROM ex_subject_point
WHERE
ex_subject_point.SUBJECT_POINT_ID IN( SELECT
ex_question_r_knowledge.SUBJECT_POINT_ID FROM
ex_question_r_knowledge WHERE
ex_question_r_knowledge.GRADE_TYPE=2 )
1
2
3
4
5
html复制代码SELECT * FROM ex_subject_point WHERE
ex_subject_point.SUBJECT_POINT_ID IN( SELECT
ex_question_r_knowledge.SUBJECT_POINT_ID FROM
ex_question_r_knowledge WHERE
ex_question_r_knowledge.GRADE_TYPE=2 )

执行时间是:1.554s

1
2
3
4
5
6
7
8
html复制代码SELECT *
FROM ex_subject_point
WHERE exists(
SELECT ex_question_r_knowledge.SUBJECT_POINT_ID
FROM ex_question_r_knowledge
WHERE ex_question_r_knowledge.GRADE_TYPE=2
AND ex_question_r_knowledge.SUBJECT_POINT_ID= ex_subject_point.SUBJECT_POINT_ID
)

执行时间是:11.978s

6、like模糊全匹配也将导致全表扫描

案例分析

1
2
3
4
sql复制代码EXPLAIN
SELECT *
FROM ex_subject_point
WHERE ex_subject_point.path like "%/11/%"

若要提高效率,可以考虑全文检索。lucene了解一下。或者其他可以提供全文索引的nosql数据库,比如tt server或MongoDB

还会陆续更新,还有几个小节。

昨天晚上突发奇想,like 模糊全匹配,会导致全表扫描,那模糊后匹配和模糊前匹配也会是全表扫描吗?

今天开电脑,做了下测试。结果如下:

like模糊后匹配,不会导致全表扫描

like模糊前匹配,会导致全表扫描

MY SQL的原理就是这样的,LIKE模糊全匹配会导致索引失效,进行全表扫描;LIKE模糊前匹配也会导致索引失效,进行全表扫描;但是LIKE模糊后匹配,索引就会有效果。

本文转载自: 掘金

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

1…123124125…956

开发者博客

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