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

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


  • 首页

  • 归档

  • 搜索

Go入门之控制语句(中) 写在前面👀 写在后面

发表于 2021-11-24

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

写在前面👀

今天主要聊聊循环语句🍕

一、for循环语句🍔

在Go语言中,虽然循环语句只有for语句,没有while语句,但也是够用了🍟

1.无条件的for循环🧂

  • 语法👇
1
2
3
4
go复制代码for{               //类似 while true{} 
// 替代 for (;;) {}
循环体代码 //无限循环做这条语句
} //无限循环的经典应用是服务器,用于不断等待和接受新的请求
  • 示例👇
    工作使我幸福😵
1
2
3
4
5
6
7
8
9
10
go复制代码package main

import "fmt"

func main() {
for {
fmt.Println("我要一直工作下去!")
}

}
  • 结果👇
    刚开始还被360检测成木马了😂

image.png

2.只有条件语句的for循环🥓

  • 语法👇
1
2
3
go复制代码for 条件语句 {   //类似while(条件语句){}
循环体代码
}
  • 示例👇
    反向输出字符串🎗
1
2
3
4
5
6
7
8
9
10
11
12
go复制代码package main

import "fmt"

func main() {
s := "Go!Go!Go!"
n := len(s)
for n > 0 { // 替代 while (n > 0) {}
n--
fmt.Printf("%c",s[n]) // 替代 for (; n > 0;) {}
}
}
  • 结果👇

image.png

3.常规的for循环🥚

  • 语法👇
1
2
3
go复制代码for 初始语句;条件语句;结束语句{
循环体代码 // 常⻅的 for 循环,⽀持初始化语句
}
  • 示例👇
    倒计时⏱
1
2
3
4
5
6
7
8
9
10
11
12
go复制代码package main
import (
"fmt"
"time"
)
func main() {
for count := 10; count > 0; count-- {
fmt.Println(count)
time.Sleep(time.Second)
}
fmt.Println("发射!")
}
  • 结果👇

image.png

二、for range循环语句🍳

for 循环的 range 格式可以可以遍历数组、字符串、切片(slice)映射(map) 及通道(channel)等

  • 语法👇
1
2
3
go复制代码for key, value := range object {
循环代码
}
类型 索引 值 编码
string index s[index] unicode, rune
array/slice index s[index]
map key m[key]
channel element
  • 首先提一个问题:如果使用 for 循环迭代一个 Unicode 编码的字符串,会发生什么?
    示例👇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码package main

import "fmt"

func main() {
s1 := "我是要成为海贼王的男人!"
s2 := "海賊王になる男です!"

for i := 0; i < len(s1); i++ {
fmt.Printf("%c", s1[i])
}
for i := 0; i < len(s2); i++ {
fmt.Printf("%c", s2[i])
}
}
  • 结果👇

image.png
结果就很离谱,是一堆乱码,该怎么办呢?我们可以用for fange变绿字符串

1. 用for range遍历字符串🧇

ASCII 编码的字符占用 1 个字节,既每个索引都指向不同的字符,而非 ASCII 编码的字符(占有 2 到 4 个字节)不能单纯地使用索引来判断是否为同一个字符

range 对应字符串遍历比较特殊,value 会对应一个 rune(int32) 类型;而不是一个 byte。
每个 rune 字符和索引在 for-range 循环中是一一对应的。它能够自动根据 UTF-8 规则识别 Unicode 编码的字符。rune 类型是 4byte,因此他有足够的长度表示中文字符(3byte)

  • 示例👇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码package main

import "fmt"

func main() {
s1 := "我是要成为海贼王的男人!"
s2 := "海賊王になる男です!"

for index, value := range s1 {
fmt.Printf("index: %d, value: %c\n", index, value)
}
for index, value := range s2 {
fmt.Printf("index: %d, value: %c\n", index, value)
}
}
  • 结果👇

image.png

2.用for range 遍历map

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

import (
"fmt"
)

func main() {
m := map[int](string){
2103030105: "小王",
2103030106: "小郦",
2103030107: "小白",
}
for key, value := range m {
fmt.Printf("学号 : %d 姓名 : %s\n", key, value)
}
}
  • 结果👇

image.png

3.在for range中使用匿名变量🥞

如果在 for range 遍历中只想获取下标索引,或者是值可以用匿名变量 _ 占位符,它既不会分配内存,也不会占用变量名,可以通过匿名变量接受键 key,也可以接受值 value

  • 示例👇
    比如我只想取出姓名,那就可以用匿名变量接受学号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main

import (
"fmt"
)

func main() {
m := map[int](string){
2103030105: "小王",
2103030106: "小郦",
2103030107: "小白",
}
for _, value := range m {
fmt.Printf("姓名 : %s\n", value)
}
}
  • 结果👇

image.png

写在后面

感谢观看✨

如有不足,欢迎指出💖

今天本来想把跳转语句也写完了,但宿舍突然断电,黑灯瞎火看不清键盘😣

本文转载自: 掘金

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

网络协议知识:三次、四次

发表于 2021-11-24

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

网络模型并非一开始就有的,在网络发展初期,网络协议都是互联网公司自己定义的。由于各家公司的网络协议不同,没有统一标准的网络协议来规定,各个公司的协议都不能互通。这对于网络发展很不利,为了解决这个问题,国际标准化组织 1984 提出的模型标准,简称 OSI(Open Systems Interconnection Model),这是一个标准,并非实现。TCP/IP 协议就是基于此模型设计。

TCP/IP模型是一个四层模型,自底而上分别是网络接口层、网络层、传输层和应用层

  • 网络接口层:实现网卡接口的网络驱动程序,以处理数据在物理媒介(比如以太网、令牌环等)上的传输
  • 网络层:实现数据包的选路和转发
  • 传输层:为主机的应用程序提供端到端的通信,传输层只关心通信的起始端和目的端,而不在乎数据包的中转过程
  • 应用层:负责处理应用程序的逻辑

TCP

TCP头部格式

三次握手

TCP是面向连接的,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接。在TCP/IP协议中,TCP协议提供可靠的连接服务,连接是通过三次握手进行初始化的。三次握手的目的是同步连接双方的序列号和确认号并交换 TCP窗口大小信息。

第一次握手: 建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;

第二次握手: 服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;

第三次握手: 客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。

为什么要三次握手?

为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

具体例子: “已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接

四次挥手

为什么要四次分手?

TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就愉快的中断了这次TCP连接。

为什么要等到2MSL?

MSL:报文段最大生存时间,它是任何报文段被丢弃前在网络内的最长时间。

原因有二:

  1. 保证TCP协议的全双工连接能够可靠关闭
  2. 保证这次连接的重复数据从网络中消失

第一点: 如果主机1直接CLOSED了,那么由于IP协议的不可靠性或者是其他的网络原因,导致主机2没有收到主机1最后回复的ACK。那么主机2就会在超时之后继续发送FIN,此时由于主机1已经CLOSED了,那就找不到与重发的FIN对应的连接了。所以,主机1不是直接进入CLOSED,而是要保持TIME_WAIT,当再次收到FIN的时候,能够保证对方收到ACK,最后正确的关闭连接。

第二点: 如果主机1直接CLOSED,然后又再向主机2发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟的数据是属于新连接的,这样和真正的新连接的数据包发生混淆了。所以TCP连接还要在TIME_WAIT状态等待2倍MSL,这样可以保证本次连接的所有数据从网络中消失。

本文转载自: 掘金

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

SpringCloud升级之路20200x版-41 S

发表于 2021-11-24

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

本系列代码地址:github.com/JoJoTec/spr…

接下来,将进入我们升级之路的又一大模块,即网关模块。网关模块我们废弃了已经进入维护状态的 zuul,选用了 Spring Cloud Gateway 作为内部网关。为何选择 Spring Cloud Gateway 而不是 nginx 还有 Kong 的原因是:

  1. 项目组对于 Java 更加熟悉,并且对于 Project Reactor 异步编程也比较熟悉,这个比较重要
  2. 需要在网关中使用我们之前实现的基于请求的有状态重试的压力敏感的负载均衡器
  3. 需要在网关中实现重试
  4. 需要在网关中实现实例路径断路
  5. 需要在网关中进行业务统一加解密
  6. 需要在网关中实现 BFF(Backends For Frontends)接口,即根据客户端请求,将某几个不同接口的请求一次性组合返回
  7. 需要在网关中使用 Redis 记录一些与 Token 相关的值

因此,我们使用了 Spring Cloud Gateway 作为内部网关,接下来,我们就来依次实现上面说的这些功能。同时在本次升级使用过程中, Spring Cloud Gateway 也有一些坑,例如:

  1. 结合使用 spring-cloud-sleuth 会有链路信息追踪,但是某些情况链路信息会丢失。
  2. 对于三方 Reactor 封装的异步 API (例如前面提到的操作 Redis 使用的 spring-data-redis)理解不到位导致关键线程被占用。

但是首先,我们需要简单理解下 Spring Cloud Gateway 究竟包括哪些组件以及整个调用流程是什么样子的。由于 Spring Cloud Gateway 基于 Spring-Boot 和 Spring-Webflux 实现,所以我们会从外层 WebFilter 开始说明,然后分析如何走到 Spring Cloud Gateway 的封装逻辑,以及 Spring Cloud Gateway 包含的组件,请求是如何转发出去,回来后又经过了哪些处理,这些我们都会逐一分析。

创建一个简单的 API 网关

为了详细分析流程,我们先来创建一个简单的网关,用于快速上手并分析。

首先创建依赖:

pom.xml

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-parent</artifactId>
<groupId>com.github.jojotech</groupId>
<version>2020.0.3-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-cloud-api-gateway</artifactId>

<dependencies>
<dependency>
<groupId>com.github.jojotech</groupId>
<artifactId>spring-cloud-webflux</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
</project>

parent 指向了我们项目的 spring-cloud-parent,同时加入了上一节实现的 spring-cloud-webflux 依赖,同时还需要加入 spring-cloud-starter-gateway,由于在我们的 spring-cloud-parent 已经指定了 spring-cloud-parent 的版本依赖管理,所以这里不需要指定 spring-cloud-starter-gateway 的版本

然后,我们开始编写配置文件:

application.yml

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
yaml复制代码server:
##端口为 8181
port: 8181
spring:
application:
# 微服务名称是 apiGateway
name: apiGateway
cloud:
gateway:
httpclient:
# 网关转发到其他微服务的 HTTP 连接超时为 500ms
connect-timeout: 500
# 网关转发到其他微服务的 HTTP 响应超时为 500ms
response-timeout: 60000
routes:
# 编写转发规则
- id: first_route
# 转发到微服务 test-service
uri: lb://test-service
# 包含哪些路径
predicates:
- Path=/test-ss/**
# 转发到的微服务访问路径,去掉路径中的第一块,即去掉 /test-ss
filters:
- StripPrefix=1
loadbalancer:
# 指定 zone,因为我们之前在负载均衡中加入了只有同一个 zone 的实例才能互相访问的逻辑
zone: test
ribbon:
# 关闭ribbon
enabled: false
cache:
# 本地微服务实例列表缓存时间
ttl: 5
# 缓存大小,你的微服务调用多少个其他微服务,大小就设置为多少,默认256
capacity: 256
discovery:
client:
simple:
# 使用 spring-common 中的简单 DiscoveryClient 服务发现客户端,就是将微服务实例写死在配置文件中
instances:
# 指定微服务 test-service 的实例列表
test-service:
- host: httpbin.org
port: 80
metadata:
# 指定该实例的 zone,因为我们之前在负载均衡中加入了只有同一个 zone 的实例才能互相访问的逻辑
zone: test
eureka:
client:
# 关掉 eureka
enabled: false

最后编写启动入口类:

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码package com.github.jojotech.spring.cloud.apigateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.github.jojotech.spring.cloud.apigateway")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

启动,访问路径: http://127.0.0.1:8181/test-ss/anything,可以看到请求被发送到 httpbin.org 的 anything 路径中,这个接口会返回请求中的所有信息。

这样,我们就实现了一个简单的网关。接下来我们来详细分析其工作流程和源码。

异步环境下请求处理的核心 - Spring Boot + Spring WebFlux 的 WebHandler

我们创建的简易网关,外层的服务容器其实就是基于 Netty 和 Project Reactor 的容器,我们跳过这些,直接进入 Spring Boot 相关的处理逻辑。我们只需要知道,请求和其对应的响应,会被外层的容器封装成为 ServerHttpRequest request 和 ServerHttpResponse response(都在 org.springframework.http.server.reactive 这个包下)。

然后,会交由 WebHandler 进行处理。WebHandler 的实现,其实是一种责任链装饰模式,如下图所示。每一层的 WebHandler 会将 request 和 response 进行对应自己责任的装饰,然后交给内层的 WebHandler 处理。

image

HttpWebHandlerAdapter - 将请求封装成 ServerWebExchange

WebHandler 的接口定义是:

1
2
3
kotlin复制代码public interface WebHandler {
Mono<Void> handle(ServerWebExchange exchange);
}

但是最外层传进来的参数是 request 和 response,需要将他们封装成 ServerWebExchange,这个工作就是在 HttpWebHandlerAdapter 中做的。HttpWebHandlerAdapter 其实主要任务就是将各种参数封装成 ServerWebExchange(除了和本次请求相关的 request 和 response,还有会话管理器 SessionManager,编码解码器配置,国际化配置还有 ApplicationContext 用于扩展)。

除了这些,处理 Forwarded 还有 X-Forwarded* 相关的 Header 的配置逻辑,也在这里进行。然后将封装好的 ServerWebExchange 交给内层的 WebHandler 即 ExceptionHandlingWebHandler 继续处理。同时,从源码中可以看出,交给内层处理的 Mono 还加入了异常处理和记录响应信息的逻辑:

HttpWebHandlerAdapter.java

1
2
3
4
5
6
7
8
scss复制代码//交给内层处理封装好的 `ServerWebExchange`
return getDelegate().handle(exchange)
//记录响应日志,trace 级别,一般用不上
.doOnSuccess(aVoid -> logResponse(exchange))
//处理内层没有处理的异常,一般不会走到这里
.onErrorResume(ex -> handleUnresolvedError(exchange, ex))
//在所有处理完成后,将 response 设为 complete
.then(Mono.defer(response::setComplete));

剩下的内层的 WebHandler,我们将在下一节中继续分析

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer:

本文转载自: 掘金

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

手把手教你实现分库分表

发表于 2021-11-24

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

为什么要分库分表?

答案很简单:数据库出现性能瓶颈
数据库出现性能瓶颈,对外表现有几个方面:
大量请求阻塞

  • 在高并发场景下,大量请求都需要操作数据库,导致连接数不够了,请求处于阻塞状态。
  • SQL 操作变慢
    • 如果数据库中存在一张上亿数据量的表,一条 SQL 没有命中索引会全表扫描,这个查询耗时会非常久。
  • 存储出现问题
    • 业务量剧增,单库数据量越来越大,给存储造成巨大压力。

数据库相关优化方案

数据库优化方案很多,主要分为两大类:软件层面、硬件层面。
软件层面包括:SQL 调优、表结构优化、读写分离、数据库集群、分库分表等;
硬件层面主要是增加机器性能。

SQL调优

SQL 调优往往是解决数据库问题的第一步,往往投入少部分精力就能获得较大的收益。
SQL 调优主要目的是尽可能的让那些慢 SQL 变快,手段其实也很简单就是让 SQL 执行尽量命中索引。
主要使用explain命令来查看sql语句的执行计划,通过观察执行结果很容易就知道该 SQL 语句是不是全表扫描、有没有命中索引。
我们观察TYPE列可以得知,sql语句是否进行了全表扫描,常见取值有:
ALL、index、range、 ref、eq_ref、const、system、NULL(从左到右,性能从差到好)
ALL 代表这条 SQL 语句全表扫描了,需要优化。一般来说需要达到range 级别及以上。

表结构优化

拿我们的表举例,现在需要向前端返回落地页的相关信息,包括落地页详情,对应产品分类,公司名称…
怎么样获取数据?
可以通过表的关联 join 最后返回整合后的结果
但是landpage表的数据有很多,通过表的关联比较费力,随着数据量增长,为了取个别的字段要关联查询几十上百万的落地页表,速度肯定会大打折扣。
优化:

  • 先查landpage表,获取主要信息,根据这批数据信息,再去查询其他表,获得其他所需字段,然后在java层面进行组合
  • 可以尝试将需要的别的表的字段添加到landpage表中,这种做法通常叫做数据库表冗余字段。这样做的好处是展示落地页信息不需要再关联查询产品分类表、公司名称表…

冗余字段的做法也有一个弊端,如果这个字段更新会同时涉及到多个表的更新,因此在选择冗余字段时要尽量选择不经常更新的字段。

架构优化

当单台数据库实例扛不住,我们可以增加实例组成集群对外服务。
当发现读请求明显多于写请求时,我们可以让主实例负责写,从实例对外提供读的能力;
如果读实例压力依然很大,可以在数据库前面加入缓存如 redis,让请求优先从缓存取数据减少数据库访问。
缓存分担了部分压力后,数据库依然是瓶颈,这个时候就可以考虑分库分表的方案了。

硬件优化

硬件成本非常高,一般来说不可能遇到数据库性能瓶颈就去升级硬件。
在前期业务量比较小的时候,升级硬件数据库性能可以得到较大提升;但是在后期,升级硬件得到的收益就不那么明显了。

项目数据库演变

多应用单数据库

我刚进入项目组的时候,我们是有两个客户端的:portal(前台)、managemen(后台)
这两个项目是共用一个数据库的。
image.png

多应用多数据库

随着项目的不断推进迭代,我们需要开发新的模块—创意工坊。
为了开发新的模块,我们需要创建新的数据库,为了不使数据库更加混乱,我们建了一个新库用来存放创意工坊的数据。这其实就是“分库”了。

单数据库的能够支撑的并发量是有限的,拆成多个库可以使服务间不用竞争,提升服务的性能。如果只拆分应用不拆分数据库,不能解决根本问题,整个系统也很容易达到瓶颈。

随着数据量的不断增大,读写分离也就在我们考虑的范畴了

如果随着业务量的逐渐增多,读数据库顶不住压力,我们可以在业务和数据库之间加一层缓存。缓存分担一部分压力后,数据库依然是瓶颈,这时候就可以考虑分库分表了。

分表

分库说完了,那什么时候分表呢?
拿我们的产品表、素材表等为例,爬虫每天都在爬取数据存放到这些表中,当数据增长到一定阶段后数据库查询效率就会出现明显下降。
因此,当单表数据量过大后,我们就可以考虑分表了。
如何分表呢?
水平切分和垂直拆分
水平拆分和垂直拆分
用户表(user)来说,表中有7个字段:id,name,age,sex,nickname,description,如果 nickname 和 description 不常用,我们可以将其拆分为另外一张表:用户详细信息表,这样就由一张用户表拆分为了用户基本信息表+用户详细信息表,两张表结构不一样相互独立。但是从这个角度来看垂直拆分并没有从根本上解决单表数据量过大的问题,因此我们还是需要做一次水平拆分。

还有一种拆分方法,比如表中有一万条数据,我们拆分为两张表,id 为奇数的:1,3,5,7……放在 user1, id 为偶数的:2,4,6,8……放在 user2中,这样的拆分办法就是水平拆分了。
水平拆分的方式也很多,除了上面说的按照 id 拆表,还可以按照时间维度取拆分,比如订单表,可以按每日、每月等进行拆分。

  • 每日表:只存储当天的数据。
  • 每月表:可以起一个定时任务将前一天的数据全部迁移到当月表。
  • 历史表:同样可以用定时任务把时间超过 30 天的数据迁移到 history表。

按照我们当前的日数据量、月数据量,还不需要按照时间维度进行拆分。
特点:

  • 垂直切分:基于表或字段划分,表结构不同。
  • 水平拆分:基于数据划分,表结构相同,数据不同。

总结:分表主要是为了减少单张表的大小,解决单表数据量带来的性能问题。

分库分表带来的复杂性

跨库关联查询

在单库未拆分表之前,我们可以很方便使用 join 操作关联多张表查询数据,但是经过分库分表后两张表可能都不在一个数据库中,如何使用 join 呢?
有几种方案可以解决:

  • 字段冗余:把需要关联的字段放入主表中,避免 join 操作;
  • 数据抽象:通过ETL等将数据汇合聚集,生成新的表;
  • 全局表:比如一些基础表可以在每个数据库中都放一份;
  • 应用层组装:将基础数据查出来,通过应用程序计算组装;

分布式事务

单数据库可以用本地事务搞定,使用多数据库就只能通过分布式事务解决了。
常用解决方案有:基于可靠消息(MQ)的解决方案、两阶段事务提交、柔性事务等。

排序、分页、函数计算问题

在使用 SQL 时 order by, limit 等关键字需要特殊处理,一般来说采用分片的思路:
先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终得到结果。

分布式 ID

如果使用 Mysql 数据库在单库单表可以使用 id 自增作为主键,分库分表了之后就不行了,会出现id 重复。
常用的分布式 ID 解决方案有:

  • UUID
  • 基于数据库自增单独维护一张 ID表
  • 雪花算法(Snowflake)

多数据源

分库分表之后可能会面临从多个数据库或多个子表中获取数据,一般的解决思路有:客户端适配和代理层适配。
业界常用的中间件有:

  • shardingsphere(前身 sharding-jdbc)
  • Mycat

ShardingJdbc概览

shardingJdbc是什么

官网给出的解释是这样的:

Sharding-JDBC

定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。

  • 适用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
  • 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
  • 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。

shardingJdbc能干什么

  • 数据分片:
    • 读写分离
    • 分库分表
    • 分布式主键
  • 分布式事务:
    • XA强一致事务
    • 柔性事务
  • 数据库治理:
    • 配置动态化
    • 熔断&禁用
    • 调用链路追踪

架构图:

其中:Registry Center中存放着一份 数据库结构和分片规则

主从复制

原理

  1. 当Master节点进行insert、update、delete操作时,会按顺序写入到binlog(二进制日志)中。
  2. salve从库连接master主库,Master有多少个slave就会创建多少个binlog dump线程。
  3. 当Master节点的binlog(二进制日志)发生变化时,binlog dump 线程会通知所有的salve节点,并将相应的binlog内容推送给slave节点。
  4. I/O线程接收到 binlog 内容后,将内容写入到本地的 relay-log(中继日志)。
  5. SQL线程读取I/O线程写入的relay-log,并且根据 relay-log 的内容对从数据库做对应的操作。

知道了主从复制的原理,我们也可以很清楚的知道,读写分离的写操作是必须要在master节点上进行,因为salve节点实现数据统一根据的是master节点的binlog,我们如果在slave节点进行写操作,在master节点进行读操作,二者数据是不会统一的,主从复制也就失去的意义。

主实例搭建

  • 修改mysql配置文件/etc/my.cnf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码[mysqld]
## 设置server_id,同一局域网中需要唯一
server_id=11
## 指定不需要同步的数据库名称
binlog-ignore-db=mysql
## 开启二进制日志功能
log-bin=mall-mysql-bin
## 设置二进制日志使用内存大小(事务)
binlog_cache_size=1M
## 设置使用的二进制日志格式(mixed,statement,row)
binlog_format=mixed
## 二进制日志过期清理时间。默认值为0,表示不自动清理。
expire_logs_days=7
## 跳过主从复制中遇到的所有错误或指定类型的错误,避免slave端复制中断。
## 如:1062错误是指一些主键重复,1032错误是因为主从数据库数据不一致
slave_skip_errors=1062
  • 修改完配置后重启实例:
1
复制代码service mysqld restart
  • 创建数据同步用户:
1
2
3
4
5
6
7
8
9
10
bash复制代码# 连接数据库
mysql -uroot -proot

# 创建数据同步用户
# 为slave1创建同步账号
CREATE USER 'slave1'@'192.168.200.12' IDENTIFIED BY '123456';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'slave1'@'192.168.200.12';
# 为slave2创建同步账号
CREATE USER 'slave2'@'192.168.200.13' IDENTIFIED BY '123456';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'slave2'@'192.168.200.13';

从实例搭建

  • 修改mysql配置文件/etc/my.cnf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bash复制代码[mysqld]
## 设置server_id,同一局域网中需要唯一
server_id=102
## 指定不需要同步的数据库名称
binlog-ignore-db=mysql
## 开启二进制日志功能,以备Slave作为其它数据库实例的Master时使用
log-bin=mall-mysql-slave1-bin
## 设置二进制日志使用内存大小(事务)
binlog_cache_size=1M
## 设置使用的二进制日志格式(mixed,statement,row)
binlog_format=mixed
## 二进制日志过期清理时间。默认值为0,表示不自动清理。
expire_logs_days=7
## 跳过主从复制中遇到的所有错误或指定类型的错误,避免slave端复制中断。
## 如:1062错误是指一些主键重复,1032错误是因为主从数据库数据不一致
slave_skip_errors=1062
## relay_log配置中继日志
relay_log=mall-mysql-relay-bin
## log_slave_updates表示slave将复制事件写进自己的二进制日志
log_slave_updates=1
## slave设置为只读(具有super权限的用户除外)
read_only=1
  • 修改完配置后重启实例:
1
复制代码service mysqld restart

将主从数据库进行连接

  • 连接到主数据库的mysql客户端,查看主数据库状态:
1
ini复制代码show master status;
  • 主数据库状态显示如下:

  • 连接从数据库的mysql的客户端
1
复制代码mysql -uroot -proot
  • 在从数据库中配置主从复制
1
ini复制代码change master to master_host='192.168.200.11', master_user='slave1', master_password='123456', master_port=3306, master_log_file='mall-mysql-bin.000001', master_log_pos=645, master_connect_retry=30;
  • 主从复制命令参数说明:
+ master\_host:主数据库的IP地址;
+ master\_port:主数据库的运行端口;
+ master\_user:在主数据库创建的用于同步数据的用户账号;
+ master\_password:在主数据库创建的用于同步数据的用户密码;
+ master\_log\_file:指定从数据库要复制数据的日志文件,通过查看主数据的状态,获取File参数;
+ master\_log\_pos:指定从数据库从哪个位置开始复制数据,通过查看主数据的状态,获取Position参数;
+ master\_connect\_retry:连接失败重试的时间间隔,单位为秒。
  • 查看主从同步状态:
1
ini复制代码show slave status \G;
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
yaml复制代码*************************** 1. row ***************************
Slave_IO_State:
Master_Host: 192.168.200.11
Master_User: slave
Master_Port: 3306
Connect_Retry: 30
Master_Log_File: mall-mysql-bin.000001
Read_Master_Log_Pos: 645
Relay_Log_File: mall-mysql-relay-bin.000001
Relay_Log_Pos: 4
Relay_Master_Log_File: mall-mysql-bin.000001
Slave_IO_Running: No #表示还没开始同步
Slave_SQL_Running: No #表示还没开始同步
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 645
Relay_Log_Space: 154
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: NULL
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 0
Master_UUID:
Master_Info_File: /var/lib/mysql/master.info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State:
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set:
Executed_Gtid_Set:
Auto_Position: 0
Replicate_Rewrite_DB:
Channel_Name:
Master_TLS_Version:
1 row in set (0.00 sec)
  • 开启主从同步:
1
ini复制代码start slave;
  • 查看从数据库状态发现已经同步:
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
yaml复制代码*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.200.11
Master_User: slave1
Master_Port: 3306
Connect_Retry: 30
Master_Log_File: mall-mysql-bin.000001
Read_Master_Log_Pos: 645
Relay_Log_File: mall-mysql-relay-bin.000002
Relay_Log_Pos: 325
Relay_Master_Log_File: mall-mysql-bin.000001
Slave_IO_Running: Yes # 同步成功
Slave_SQL_Running: Yes # 同步成功
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 645
Relay_Log_Space: 537
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 11
Master_UUID: cabb05d9-d404-11eb-9530-000c299fd1be
Master_Info_File: /var/lib/mysql/master.info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set:
Executed_Gtid_Set:
Auto_Position: 0
Replicate_Rewrite_DB:
Channel_Name:
Master_TLS_Version:
1 row in set (0.00 sec)

主从复制测试

  • 在主实例中创建一个数据库mall;

  • 在从实例中查看数据库,发现也有一个mall数据库,可以判断主从复制已经搭建成功。

读写分离

主从复制完成后,我们还需要实现读写分离,master负责写入数据,两台slave负责读取数据。怎么实现呢?

解决读写分离的方案有两种:应用层解决和中间件解决。

1
2
3
4
5
6
7
8
sql复制代码CREATE TABLE `tb_commodity_info` (
`id` varchar(32) NOT NULL,
`commodity_name` varchar(512) DEFAULT NULL COMMENT '商品名称',
`commodity_price` varchar(36) DEFAULT '0' COMMENT '商品价格',
`number` int(10) DEFAULT '0' COMMENT '商品数量',
`description` varchar(2048) DEFAULT '' COMMENT '商品描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表';

应用层解决


优点:

  1. 多数据源切换方便,由程序自动完成;
  2. 不需要引入中间件;
  3. 理论上支持任何数据库;

缺点:

  1. 由程序员完成,运维参与不到;
  2. 不能做到动态增加数据源;

中间件解决


优点:

  1. 源程序不需要做任何改动就可以实现读写分离;
  2. 动态添加数据源不需要重启程序;

缺点:

  1. 程序依赖于中间件,会导致切换数据库变得困难;
  2. 由中间件做了中转代理,性能有所下降;

应用层是采用AOP的方式,通过方法名判断,方法名中有get、select、query开头的则连接slave,其他的则连接master数据库。
但是通过AOP的方式实现起来代码有点繁琐,有没有什么现成的框架呢,答案是有的。
Apache ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 JDBC、Proxy两部分组成。
ShardingSphere-JDBC定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
读写分离就可以使用ShardingSphere-JDBC实现。


下面演示一下SpringBoot+Mybatis+druid+ShardingSphere-JDBC代码实现。
项目配置
版本说明:

1
2
3
4
yml复制代码SpringBoot:2.5.3
druid:1.1.20
mybatis-spring-boot-starter:1.3.2
sharding-jdbc-spring-boot-starter:4.0.0-RC1

添加sharding-jdbc的maven配置:

1
2
3
4
5
sql复制代码<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>

然后在application.yml添加配置:

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
sql复制代码# 这是使用druid连接池的配置,其他的连接池配置可能有所不同
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
datasource:
names: master,slave0,slave1
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.200.11:3306/mall?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: root
slave0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.200.12:3306/mall?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: root
slave1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.200.13:3306/mall?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: root

props:
sql.show: true
masterslave:
load-balance-algorithm-type: round_robin #round_robin random
sharding:
master-slave-rules:
master:
master-data-source-name: master
slave-data-source-names: slave1,slave0

sharding.master-slave-rules是标明主库和从库,一定不要写错,否则写入数据到从库,就会导致无法同步。
load-balance-algorithm-type是路由策略,round_robin表示轮询策略,random表示随机策略。
启动项目,可以看到以下信息,配置的三个数据源初始化,代表配置成功:

编写实体类接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码import lombok.Data;

/**
* @author wangpeixu
* @date 2021/8/12 13:41
*/
@Data
public class Commodity {
private String id;
private String commodityName;
private String commodityPrice;
private Integer number;
private String description;
}

准备就绪,开始测试!
测试
编写测试类

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
java复制代码import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import wang.cai2.shardingSphere.dao.CommodityMapper;
import wang.cai2.shardingSphere.entity.Commodity;

import java.util.List;
import java.util.Random;

/**
* @author wangpeixu
* @date 2021/8/12 13:56
*/
@SpringBootTest
public class masterTest {
@Autowired
private CommodityMapper commodityMapper;

@Test
void masterT() {
Commodity commodity = new Commodity();
commodity.setCommodityName("冬瓜");
commodity.setCommodityPrice("6");
commodity.setNumber(10000);
commodity.setDescription("卖冬瓜");
commodity.setId(String.valueOf(new Random().nextInt(1000)));
commodityMapper.addCommodity(commodity);
}

@Test
void queryTest() {
for (int i = 0; i < 10; i++) {
List<Commodity> query = commodityMapper.query();
System.out.println("-------");
}
}
}

插入数据masterT():

查询数据queryTest():

成功

Sharding-Jdbc实现分库分表

shardingJdbc的配置文件如下:

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
yaml复制代码# datasource
spring:
main:
allow-bean-definition-overriding: true
# 配置真实数据源
shardingsphere:
# 打开sql输出日志
props:
sql:
show: true
datasource:
names: db1,db2
# 第一个配置源
db1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_1?serverTimezone=UTC
username: root
password: root
# 第二个配置源
db2:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_2?serverTimezone=UTC
username: root
password: root
sharding:
#库的分片策略
default-database-strategy:
inline:
sharding-column: user_id
algorithm-expression: db$->{user_id %2 +1}
tables:
course:
actual-data-nodes: db$->{1..2}.course_$->{1..2}
# 表的分片策略
table-strategy:
inline:
sharding-column: cid
algorithm-expression: course_$->{cid % 2 + 1}
# cid 的生成策略
key-generator:
column: cid
type: SNOWFLAKE #雪花算法

测试代码:

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
java复制代码import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import wang.cai2.shardingSphere.dao.CourseMapper;
import wang.cai2.shardingSphere.entity.Course;

import java.util.List;

@SpringBootTest
class ShardingSphereApplicationTests {

@Autowired
private CourseMapper courseMapper;

@Test
void saveCourse() {
for (int i = 1; i <= 10; i++) {
Course course = new Course();
course.setCname("java" + i);
course.setUserId(Long.valueOf(i));
course.setCstatus("Normal" + i);
courseMapper.saveCourse(course);
}
}

@Test
void findAll() {
List<Course> all = courseMapper.findAll();
for (Course course : all) {
System.out.println(course);
}
}

}

我们可以看到,数据是按照我们的配置插入了

本文转载自: 掘金

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

操作系统学习笔记(十一)~CPU调度单元测试 前言

发表于 2021-11-24

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

前言

Hello!小伙伴!

非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出~

自我介绍 ଘ(੭ˊᵕˋ)੭

昵称:海轰

标签:程序猿|C++选手|学生

简介:因C语言结识编程,随后转入计算机专业,有幸拿过一些国奖、省奖…已保研。目前正在学习C++/Linux/Python

学习经验:扎实基础 + 多做笔记 + 多敲代码 + 多思考 + 学好英语!

1、以下有关抢占式调度的论述,错误的是()。C
A.可防止单一进程长时间独占CPU
B.进程切换频繁
C.系统开销小
D.调度程序可根据某种原则暂停某个正在执行的进程,将已分配给它的CPU重新分配给另一进程

解释:在这里插入图片描述

2、假设一个系统中有3个进程,到达时间依次为0,1,3。运行时间依次为3、5和2。若按照时间片轮转(时间片为2)调度算法调度CPU,那么各进程的平均周转时间为()。C
A.其它
B.8
C.6
D.7

解释:
周转时间:进程从提交到运行结束的全部时间
对于本题有个小坑:时刻4应该先调度p1,因为在就绪队列里p1排在p3的前面。p1轮转完执行P2,时刻2时队列是P2P1,时刻3p3才到达,所以时刻4P2执行完要先执行P1–咨询了老师,这道题之前一直没有算出来
在这里插入图片描述

3、假设一个系统中有5个进程,它们到达的时间依次为0、2、4、6和8,运行时间依次为3、6、4、5和2。若按照抢占式短作业优先调度算法调度CPU,那么各进程的平均周转时间为()。D
A.7.4
B.其它
C.8
D.7.2

解释:
在这里插入图片描述

4、假设一个系统中有5个进程,它们到达的时间依次为0、1、2、3和4,运行时间依次为2、3、2、4和1,优先数分别为3、4、2、1、5。若按照非抢占优先数调度算法(优先数小则优先级高)调度CPU,那么各进程的平均周转时间为()。D
A.5
B.3.3
C.其它
D.5.4

解释:在这里插入图片描述

5、 假设一个系统中有4个进程,它们到达的时间依次为0、2、4和6,运行时间依次为3、6、4和5。若按照抢占式短作业优先调度算法调度CPU,那么各进程的平均周转时间为()。B
A.其它
B.7.5
C.6
D.8

解释:在这里插入图片描述

6、在时间片轮转算法中,假如时间片大小为5ms,系统中处于就绪队列的进程有10个(运行期间没有新进程加入就绪队列),则最长的响应时间为()。B
A.其它
B.45ms
C.50ms
D.5ms

7、抢占式CPU调度可能发生在一个进程()时。A
A.从运行转到就绪
B.从运行转到终止
C.新建进程
D.从运行转到等待

解释:在这里插入图片描述

8、分时系统一般采用的调度算法是()。A
A.时间片轮转
B.短作业优先
C.先来先服务
D.优先级算法

9、不具有抢占和非抢占模式的调度算法是()。C
A.PR
B.其它都不是
C.FCFS
D.SJF

10、FCFS调度算法实现简单,可以使用FIFO队列来实现,当一个进程进入就绪队列,就是将其PCB链接到队列()。C
A.第二个
B.头部
C.尾部
D.中间

11、多核处理器的CPU调度和单核处理器调度相比,还需要考虑()。A、B
A.负载平衡
B.亲和性
C.CPU利用率
D.吞吐量

12、在时间片轮转算法中,时间片越小,则()。A、C、D
A.平均响应时间短
B.平均等待时间小
C.进程切换越频繁
D.系统开销大

13、具有抢占和非抢占两种调度模式的调度算法有()。B、C
A.RR
B.PR
C.SJF
D.FCFS

14、下列进度调度算法中,( )可能出现进程长期得不到运行的情况。B、C
A.时间片轮转调度算法
B.抢占式短作业优先算法
C. 静态优先数算法
D.先来先服务算法

15、若进程P一旦被唤醒就能够马上投入运行,系统可能为( )。B、D
A.分时系统,进程P的优先级最高
B.就绪队列为空队列,并且没有进程在运行
C.抢占调度方式,就绪队列上的所有进程的优先级皆比P的低
D. 抢占调度方式,P的优先级高于当前运行的进程

16、在进程调度中,每个进程的等待时间加上运行时间等于周转时间。√

解释:在这里插入图片描述

17、给定一批进程,抢占式调度一定比非抢占式调度获得小的平均周转时间。×

18、一般而言,交互进程需要短的响应时间。√

19、动态优先级是指在创建进程之初先赋予每个进程一个优先级,然后其值随进程的推进或等待时间的增加而改变,以便获得更好的调度性能。√

20、抢占式SJF的平均等待时间一定小于非抢占式SJF。×

本文转载自: 掘金

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

Spring全家通之SpringMVC核心技术

发表于 2021-11-24

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

👨‍🎓作者:Java学术趴

🏦仓库:Github、Gitee

✏️博客:CSDN、掘金、InfoQ、云+社区

💌公众号:Java学术趴

🚫特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系小编授权。

🙏版权声明:文章里的部分文字或者图片来自于互联网以及百度百科,如有侵权请尽快联系小编。微信搜索公众号Java学术趴联系小编。

☠️每日毒鸡汤:微笑拥抱每一天,做像向日葵般温暖的女子。

👋大家好!我是你们的老朋友Java学术趴。最近小编又在整了Spring全家桶笔记,笔记会每天定时的进行发放,喜欢的大佬们欢迎收藏点赞关注呦。小编会每天分享的呦。今天给大家带来新的框架技术SpringMVC。

Spring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面。Spring 框架提供了构建 Web 应用程序的全功能 MVC 模块。使用 Spring 可插入的 MVC 架构,从而在使用Spring进行WEB开发时,可以选择使用Spring的Spring MVC框架或集成其他MVC开发框架。

第四章 SpringMVC 核心技术

4.1 请求重定向和转发

  • 当处理器对请求处理完毕后,向其它资源进行跳转时,有两种跳转方式:请求转发与重 定向。而根据所要跳转的资源类型,又可分为两类:跳转到页面与跳转到其它处理器。
  • 注意,对于请求转发的页面,可以是WEB-INF中页面;而重定向的页面,是不能为WEB-INF 中页的。因为重定向相当于用户再次发出一次请求,而用户是不能直接访问 WEB-INF 中资 源的

image-20211124223840114

SpringMVC 框架把原来 Servlet 中的请求转发和重定向操作进行了封装。现在可以使用简 单的方式实现转发和重定向。

  • forward:表示转发,实现 request.getRequestDispatcher(“xx.jsp”).forward()
  • redirect:表示重定向,实现 response.sendRedirect(“xxx.jsp”

4.4.1 请求转发

  • 处理器方法返回 ModelAndView 时,需在 setViewName()指定的视图前添加 forward:,且 此时的视图不再与视图解析器一同工作,这样可以在配置了解析器时指定不同位置的视图。 视图页面必须写出相对于项目根的路径。forward 操作不需要视图解析器。
  • 处理器方法返回 String,在视图路径前面加入 forward: 视图完整路径。
1
2
3
4
5
6
7
8
9
10
java复制代码    @RequestMapping(value = "/some.do")
   public ModelAndView doSome(String name,int age){
       ModelAndView mv = new ModelAndView();
       mv.addObject("myName",name);
       mv.addObject("myAge",age);
       /*使用forward请求转发的方式*/
       /*这个是请求转发的方式,可以请求到WEB-INF下的页面*/
       mv.setViewName("forward:/WEB-INF/view/show.jsp");
       return mv;
  }

4.1.2 请求重定向

  • 在处理器方法返回的视图字符串的前面添加 redirect:,则可实现重定向跳转。
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码    @RequestMapping(value = "/some2.do")
   public ModelAndView doSome2(String name,int age){
       ModelAndView mv = new ModelAndView();
       /*此时的数据放到request作用域中*/
       mv.addObject("myName",name);
       mv.addObject("myAge",age);
       /*使用redirect请求转发的方式*/
       /*这个是重回定向的方式,请求不到WEB-INF下的网页*/
       mv.setViewName("redirect:/show.jsp");
       // http://localhost:8080/myWeb/show.jsp?myName=lisi&myAge=20
       return mv;
  }

4.2 异常处理

  • SpringMVC 框架处理异常的常用方式:使用@ExceptionHandler 注解处理异常。

4.2.1 @ExceptionHandler 注解

  • 使用注解@ExceptionHandler 可以将一个方法指定为异常处理方法。该注解只有一个可 选属性 value,为一个 Class数组,用于指定该注解的方法所要处理的异常类,即所要匹 配的异常。
  • 而被注解的方法,其返回值可以是 ModelAndView、String,或 void,方法名随意,方法 参数可以是 Exception 及其子类对象、HttpServletRequest、HttpServletResponse 等。系统会 自动为这些方法参数赋值。
  • 对于异常处理注解的用法,也可以直接将异常处理方法注解于 Controller 之中。

(1) 自定义异常类

定义三个异常类:NameException、AgeException、MyUserException。其中 MyUserException 是另外两个异常的父类。

1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码public class MyUserException extends Exception{
​
   /*继承父类的有参构造个无参构造方法*/
​
   public MyUserException() {
       super();
  }
​
   public MyUserException(String message) {
       super(message);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码/*
* 当输入的年龄存在异常的时候抛出异常
* */
public class AgeException extends MyUserException{
   public AgeException() {
       super();
  }
​
   public AgeException(String message) {
       super(message);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
scala复制代码/*
* 当用户的姓名存在异常的时候抛出的异常,抛出NameException
* */
public class NameException extends MyUserException{
   public NameException() {
       super();
  }
​
   public NameException(String message) {
       super(message);
  }
}
​

(2) 修改 Controller 抛出异常

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
java复制代码/*
* 这个类中我们使用Java代码处理异常(也就是try...catch)
* 我们使用框架来处理异常
* */
@Controller
public class MyController {
​
   @RequestMapping(value = "/exception.do")
   public ModelAndView exception(String name,int age) throws MyUserException {
       ModelAndView mv = new ModelAndView();
       // 根据请求参数抛出异常
       /*
       * 处理的顺序:
       *   代码从上往下执行,如果满足第一个if条件的话,那么程序会进入到这个异常的类中,
       *   不会在继续执行这个处理器方法,此时跳转到异常处理的类中,也就是被@ControllerAdvice注解
       *   标注的类。这里就是 GlobalExceptionHandel 这个类。
       *
       * 在异常类中,会根据抛出异常的类型在异常类中寻找被@ExceptionHandler(value = NameException.class)
       * 标注的方法上的这个注解中的value属性的值,进行异常的处理。
       * */
       if (!"程云博".equals(name)){
           throw  new NameException("输入的姓名不正确!");
      }
       if (age == 0|| age > 80){
           /*
               这里指定的抛出异常的信息相当于系统抛出的异常,类似于NullPointException
               属于系统界别的异常
           * */
           throw new AgeException("年龄不符合要求!");
      }
       mv.addObject("myName",name);
       mv.addObject("myAge",age);
       mv.setViewName("show");
       return mv;
  }
}
​

(3) 定义异常响应页面

定义三个异常响应页面。

image-20211124224421872

  • 不过,一般不这样使用。而是将异常处理方法专门定义在一个类中,作为全局的异常处 理类。
  • 需要使用注解@ControllerAdvice,字面理解就是“控制器增强”,是给控制器对象增强 功能的。使用@ControllerAdvice 修饰的类中可以使用@ExceptionHandler
  • 当使用@RequestMapping 注解修饰的方法抛出异常时,会执行@ControllerAdvice 修饰的 类中的异常处理方法。
  • @ControllerAdvice 是使用@Component 注解修饰的,可以 扫描到@ControllerAdvice 所在的类路径(包名),创建对象。

(4) 定义全局异常处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
java复制代码/*
* @ControllerAdvice :控制类增强(也就是给控制器类增加功能-->异常处理的功能)
*   位置:在类的上面
*   特点:必须让框架知道这个注解所在的包名,需要在SpringMVC的配置文件中加入组件扫描器。
*       指定@ControllerAdvice所在的包名
*   这个使用的是Spring中的AOP技术,就是在原有的业务代码中加入一些与业务无关的方法。
*   比如日志、错误信息等。
* */
@ControllerAdvice
public class GlobalExceptionHandel {
   // 定义方法来处理发生的异常
   /*
   * 处理异常的方法和处理控制器的方法一样,
   * 可以有多种参数:普通数据类型、Object对象类型、List、Map集合都可以。
   * 可以有多种返回值类型ModelAndView、String、void对象、list对象集合。
   *
   * 形参:Exception。表示Controller中抛出的异常对象。
   *     通过形参可以获取发生的异常信息。
   *
   * @ExceptionHandel(value = 异常类.class):表示异常的类型,当发生此类型异常时
   *                               由当前方法进行处理。
   * */
​
   /*
   * SpringMVC处理异常的方式,他会在Controller类中接收到一个异常,之后在这个处理异常的类中
   * 寻找处理这个异常的方法,通过@ExceptionHandler注解的value值进行匹配。
   * 如果Controller类抛出的异常和@ExceptionHandler注解的value值都没有匹配到,
   * 那么就执行最后没有value值的@ExceptionHandler注解所对应的方法。
   *
   * 注意 :@ExceptionHandler没有value属性的注解只能存在一个。相当于if...else中的else。
   * */
   @ExceptionHandler(value = NameException.class)
   public ModelAndView doNameException(Exception exception){
       // 处理NameException的异常。
       /*
       * 异常发生我们要处理的逻辑:
       *   1. 需要把异常记录下来,记录到日志文件或者数据库中。
       *     记录日志发生的时间,哪个方法发生的,异常信息内容。
       *   2. 发送通知,把异常的信息通过邮件、短信、维信发送给相关人员。
       *
       *   3. 给用户一个很好的复杂。
       * */
       ModelAndView mv = new ModelAndView();
       /*这里的异常信息只是一个提示用户的信息,这个信息是我们自己给的,显示给前端jsp页面的*/
       mv.addObject("msg","姓名必须的是程云博,其他用户不可以访问!");
       // 异常对象,这个就相当于系统抛出异常的那个对象。比如:NullPointException对象。
       // 或者是我们自定义的 AgeException、NameException。
​
       mv.addObject("ex",exception);
       mv.setViewName("nameError");
       return mv;
  }
​
   @ExceptionHandler(value = AgeException.class)
   public ModelAndView doAgeException(Exception exception){
       // 处理AgeException的异常。
       /*
        * 异常发生我们要处理的逻辑:
        *   1. 需要把异常记录下来,记录到日志文件或者数据库中。
        *     记录日志发生的时间,哪个方法发生的,异常信息内容。
        *   2. 发送通知,把异常的信息通过邮件、短信、维信发送给相关人员。
        *
        *   3. 给用户一个很好的复杂。
        * */
       ModelAndView mv = new ModelAndView();
       mv.addObject("msg","您的年龄不符合要求!");
       // 异常对象
       mv.addObject("ex",exception);
       mv.setViewName("ageError");
       return mv;
  }
​
   // 处理其他异常,NameException、AgeException以外的异常,不知道的异常类型
   // 当错误信息不是NameException、AgeException的时候,就交给这个方法来处理异常
   @ExceptionHandler
   public ModelAndView doOtherException(Exception exception){
       // 处理其他的异常。
       /*
        * 异常发生我们要处理的逻辑:
        *   1. 需要把异常记录下来,记录到日志文件或者数据库中。
        *     记录日志发生的时间,哪个方法发生的,异常信息内容。
        *   2. 发送通知,把异常的信息通过邮件、短信、维信发送给相关人员。
        *
        *   3. 给用户一个很好的复杂。
        * */
       ModelAndView mv = new ModelAndView();
       mv.addObject("msg","您的年龄不符合要求!");
       // 异常对象
       mv.addObject("ex",exception);
       mv.setViewName("defaultError");
       return mv;
  }
}
​

(5) 定义 Spring 配置文件

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:context="http://www.springframework.org/schema/context"
      xmlns:mvc="http://www.springframework.org/schema/mvc"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
​
   <!--SpringMVC配置文件,声明controller和其他web相关的对象-->
​
   <!--声明组件扫描器,使用动态代理的方式创建Servlet的动态代理对象-->
   <context:component-scan base-package="com.yunbocheng.controller" />
​
   <!--配置视图解析器-->
   <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
       <!--这里使用value属性,因为使用的是set注入的方式进行赋值-->
       <property name="prefix" value="/WEB-INF/view/" />
       <property name="suffix" value=".jsp" />
   </bean>
​
   <!--注解驱动,因为处理ajax请求(转换格式)以及静态资源(解决冲突)都需要用到注解驱动-->
   <mvc:annotation-driven/>
​
   <!--处理异常的组件扫描器,指定处理异常类所在的包-->
   <context:component-scan base-package="com.yunbocheng.handel"/>
</beans>

4.3 拦截器

  • SpringMVC 中的 Interceptor 拦截器是非常重要和相当有用的,它的主要作用是拦截指定的用户请求,并进行相应的预处理与后处理。其拦截的时间点在“处理器映射器根据用户提 交的请求映射出了所要执行的处理器类,并且也找到了要执行该处理器类的处理器适配器, 在处理器适配器执行处理器之前”。当然,在处理器映射器映射出所要执行的处理器类时, 已经将拦截器与处理器组合为了一个处理器执行链,并返回给了中央调度器。

4.3.1 拦截器的执行

自定义拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
java复制代码/*
* 这是一个拦截器类,需要继承HandlerInterceptor接口
* 并且实现其中的三个类。
*
* 这个类用于拦截用户的请求。
* */
​
public class MyInterceptor implements HandlerInterceptor {
​
   // 实现HandlerInterceptor这个接口中的三个方法,查看源码可以看到者三个方法是使用Default声明的方法
   // 所以我们不需要全部的实现这三个方法
   // 我们这里实现这三个方法。
​
   /*
   * preHandle() : 这个方法叫做预处理方法。
   * 参数:
   *   Object handler :被拦截的控制器对象(也就是项目中的MyController对象)
   * 返回值:boolean
   *   true : 表示此时请求通过了拦截器的验证,可以执行处理器方法处理这个请求。
   *   false : 表示此时的请求没有通过拦截器的验证,不可以执行处理器方法处理这个请求。
   *
   * 特点:
   *   1. 方法是在控制器方法(MyController的doSome)之前先执行的。
   *     用户的请求首先到达此方法
   *   2. 在这个方法中可以获取请求的信息。验证请求是否符合要求。
   *     可以验证用户是否可以登录,验证用户是否有权限访问某个连接地址(url)
   *     如果验证失败,可以截断请求,请求不能被处理。
   *     如果验证成功,可以放行请求,此时控制器方法才可以执行。
   * */
   private long bTime;
   @Override
   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       bTime = System.currentTimeMillis();
       System.out.println("拦截器的MyInterceptor的perHandler()");
​
       // 在这个方法中进行业务逻辑的判断,返回true还是false,进而控制这个请求可不可以交给处理器方法进行处理。
​
       /*当请求被拦截器拦截下来的时候,给浏览器一个返回页面结果*/
       /*request.getRequestDispatcher("/tips.jsp").forward(request,response);*/
       return true;
  }
​
   /*
   * postHandle : 后处理方法。
   *
   * 参数:
   *   Object handler : 被拦截的处理器对象MyController。
   *   ModelAndView modelAndView : 处理器方法的返回值。
   *
   * 返回值:void
   *
   * 特点:
   *   1. 方法是在处理方法之后执行的(MyController.doSome())
   *   2. 能够获取到处理器方法的返回值ModelAndView,可以修改ModelAndView中的
   *     数据和视图,可以影响最后的执行结果。
   *   3. 主要对原来的执行结果进行二次修饰。
   * */
   @Override
   public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
       /*执行这个处理结果的方法的前提是预处理方法返回的为true*/
       System.out.println("拦截器的MyInterceptor的postHandle()");
​
       // 对原来的处理器方法的返回值进行处理。
       if (modelAndView != null){
           // 添加返回值中的数据
           modelAndView.addObject("myDate",new Date());
           // 修改返回值的数据
           modelAndView.addObject("myAge",40);
           // 修改返回的视图
           modelAndView.setViewName("other");
      }
  }
​
​
   /*
   * afterCompletion : 最后执行的方法
   *   参数:
   *     Object handler : 被拦截的处理器对象。
   *     Exception ex : 程序中发生的异常。
   * 特点:
   *     1. 是在请求处理完成后执行的。框架中的规定是当你的视图处理完成后,对视图执行了forward。就认为是请求处理完成了。
   *     2. 一般是做资源回收工作的,程序请求过程中创建一些对象,在这里可以删除,把占用的内存回收。
   * */
   @Override
   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
       System.out.println("拦截器的MyInterceptor的afterCompletion");
       long eTime = System.currentTimeMillis();
       System.out.println("计算preHandler到请求处理完成的时间:" + (eTime - bTime));
  }
}

自定义拦截器,需要实现 HandlerInterceptor 接口。而该接口中含有三个方法:

➢ preHandle(request,response, Object handler): 该方法在处理器方法执行之前执行。其返回值为 boolean,若为 true,则紧接着会执行处理器方 法,且会将 afterCompletion()方法放入到一个专门的方法栈中等待执行。

➢ postHandle(request,response, Object handler,modelAndView): 该方法在处理器方法执行之后执行。处理器方法若最终未被执行,则该方法不会执行。 由于该方法是在处理器方法执行完后执行,且该方法参数中包含 ModelAndView,所以该方法可以修 改处理器方法的处理结果数据,且可以修改跳转方向。

➢ afterCompletion(request,response, Object handler, Exception ex): 当 preHandle()方法返回 true 时,会将该方法放到专门的方法栈中,等到对请求进行响应的所有 工作完成之后才执行该方法。即该方法是在中央调度器渲染(数据填充)了响应页面之后执行的,此 时对 ModelAndView 再操作也对响应无济于事。

afterCompletion 最后执行的方法,清除资源,例如在 Controller 方法中加入数据

拦截器中方法与处理器方法的执行顺序如下图:

换一种表现方式,也可以这样理解:

(1) 注册拦截器

  • 用于指定当前所注册的拦截器可以拦截的请求路径,而/**表示拦截所 有请求。

(2) 修改 index 页面

image-20211124225203409

(3) 修改处理器

(4) 修改 show 页面

(5) 控制台输出结果

以上项目的源代码,点击星球进行免费获取 星球 (Github地址)如果没有Github的小伙伴儿。可以关注本人微信公众号:Java学术趴,发送SpringMVC,免费给发给大家项目源码,代码是经过小编亲自测试的,绝对可靠。免费拿去使用。

本文转载自: 掘金

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

uvicorn源码分析

发表于 2021-11-24

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

前记

Uvicorn是一个基于uvloop和httptools的ASGI服务器, 性能比较强劲, 通过它可以与使用ASGI规范的Python应用程序进行交互。ASGI与WSGI很像, 只不过ASGI原生支持HTTP2.0和WebSocket, 同时更多的是支持Python的Asyncio生态的WEB应用程序。通过了解Uvicron,能知道一个稳定的Web服务器的工作方式以及能更好的去了解其他基于ASGI的WEB应用程序。

最新修订见原文, 关注公众号<博海拾贝diary>可以及时收到新推文通知

Uvicron通过一个通用的协定接口与ASGI应用程序交互, 应用程序只要实现如下代码, 即可通过Uvicorn发送和接收信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Python复制代码async def app(scope, receive, send):
# 一个最简单的ASGI应用程序
assert scope['type'] == 'http'
await send({
'type': 'http.response.start',
'status': 200,
'headers': [
[b'content-type', b'text/plain'],
]
})
await send({
'type': 'http.response.body',
'body': b'Hello, world!',
})


if __name__ == "__main__":
# uvicorn服务
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=5000, log_level="info")

其中应用程序的scope代表有关传入连接信息的字典, receive是一个从服务器接收传入信息的通道, send是一个将消息发送到服务器的通道, 不过这不是本文的重点, 更多scope信息可以访问ASGI interface了解, 接下来将从例子的uvicorn.run开始, 通过源码分析uvicorn工作原理。

1.uvicorn主流程源码分析

分析源码之前, 首先是了解它的源码结构, uvicorn的源码结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
markdown复制代码├── lifespan
├── loops
├── protocols
├── middleware
├── supervisors
├── config.py
├── importer.py
├── __init__.py
├── logging.py
├── __main__.py
├── main.py
├── server.py
├── subprocess.py
├── _types.py
└── workers.py

uvicron做了很好的分类, 每个文件夹/文件都有自己的功能:

  • lifespan
    告诉基于ASGI的应用程序uvicorn即将启动和停止的消息, uvicorn在启动的时候会初始化,然后发送初始化协议并等待ASGI应用程序返回, 如果ASGI应用程序返回complele则uvicorn会继续运行, 返回failed则报错退出。
  • loops
    自动加载事件循环, 优先加载uvloop, 这将会获得极大的性能提升
  • protocols
    里面存放着读取连接数据和解析消息体的协议, 如HTTP和WebSockets, 可以把他认为是一个序列化器。
  • middleware
    存放着一些简单通用的ASGI中间件
  • supervisors
    uvicorn本身是以一个进程启动的, 这个文件夹存放着uvicorn的几种启动方式, 如多进程启动,监控文件变动自动重启的方式等。
  • config.py
    uvicorn的配置文件, 它不仅读取用户的配置, 还自动加载上面所述的lifespan, loops, protocols等等
  • importer.py
    uvicron中很多地方使用了动态加载配置和动态加载库, 这里是把这个方法进行统一封装
  • logging.py
    提供了根据日志等级渲染不同颜色的日志以及访问日志(但是很少人用)
  • main.py
    uvicorn的入口文件, 包括代码运行和命令行运行两种方式
  • server.py
    uvicorn的核心服务, 用于处理进出流量以及处理自身的服务状态
  • subprocess.py
    给supervisors/multiprocess.py使用的, 可能是为了以后拓展需要, 才放在一级目录
  • workers.py
    其他工作模式的Uvicorn, 比如里面有个UvicornWorker, 就是用于gunicorn启动uvicorn

结构了解完, 接下来开始正式步入源码之旅, 这里直接忽略掉命令行的启动方式, 从uvicorn.run开始, 实际上命令行启动方式也是通过获取参数, 然后传入uvicorn.run方法中, 这个uvicorn.run方法会接受符合ASGI的app和kwargs参数, 然后生成对应的配置实例config, 再生成server, 接着依靠配置判断执行不同的启动模式, 具体代码如下:

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
Python复制代码def run(app, **kwargs):
# 加载配置
config = Config(app, **kwargs)
# 加载server
server = Server(config=config)

if (config.reload or config.workers > 1) and not isinstance(app, str):
# 只有命令行模式才可以使用reload
logger = logging.getLogger("uvicorn.error")
logger.warning(
"You must pass the application as an import string to enable 'reload' or "
"'workers'."
)
sys.exit(1)

if config.should_reload:
# 启动reload逻辑
sock = config.bind_socket()
supervisor = ChangeReload(config, target=server.run, sockets=[sock])
supervisor.run()
elif config.workers > 1:
# 多进程方式启动
sock = config.bind_socket()
supervisor = Multiprocess(config, target=server.run, sockets=[sock])
supervisor.run()
else:
# 最普通的方法启动
server.run()

config很简单, 它负责装填配置, 然后调用configure_logging配置全局的logger, 此外还有一个load的方法, 将会在Server中调用, 接下来, 先忽略其中涉及到的模板, 到server.run之中, 看看普通模式下, 服务是怎么启动的。

这个run方法很简单, 就是设置事件循环, 然后通过事件循环调用serve来启动服务:

1
2
3
4
Python复制代码def run(self, sockets=None):
self.config.setup_event_loop()
loop = asyncio.get_event_loop()
loop.run_until_complete(self.serve(sockets=sockets))

serve是启动服务的最核心代码, 首先会执行config.load方法加载一些动态的配置, 如解析http的库, 解析websocket的库, 还有通过用户传过来的app来加载app, 并判断是使用WSGI,ASGI2或者是ASGI3, 并进行配置(uvicorn在这里是通过ASGI中间件的方式来支持), 最后根据配置启动对应的中间件。
接着会跳转到server.startup方法, 该方法首先会通过lifaspan.startup与用户传过来的app通信, 校验是否是合法的应用程序, 然后初始化变量, 先是初始化一个信号处理函数, 当收到信号时, 会把变量should_exit设置会True。
然后会初始化一个名为create_protocol的变量, 它是继承于asyncio.Protocol, asyncio.Protocol主要用于从socket获取数据和写入数据, 同时也有一些TCP相关的调用, create_protocol的主要作用就是作为socket和应用程序的中间层, 负责把HTTP数据与ASGI数据互转, 如下图:
协议转换
接着根据用户传过来的变量方式来启动服务, 这些都是Python的Asyncio封装好的, 具体为以下几种:

  • 当用户传socket过来的时候: 基于该scoket和create_protocol创建服务, 如果是多进程且是Windows系统, 则要显示的共享socket。
  • 当用户传文件描述符的时候: 基于该文件描述符获取scoket, 并通过该socket和create_protocol创建服务。
  • 当用户传unix domain socket的时候: 基于unix domain socket和create_protocol创建服务。
  • 当用户传host和port参数的时候: 基于host和port和create_protocol创建服务。

创建完服务后, socket的处理就转给了应用程序了, 但是采用了事件循环的思路, 需要uvicorn使用while使程序一直跑, 防止主程序退出:

1
2
3
4
5
6
7
8
Python复制代码async def main_loop(self):
counter = 0
should_exit = await self.on_tick(counter)
while not should_exit:
counter += 1
counter = counter % 864000
await asyncio.sleep(0.1)
should_exit = await self.on_tick(counter)

每次循环执行的时候都会调用on_tick方法, 该方法主要是进行服务统计以及判断啥时候可以退出服务, 比如请求总数超过配置的限制数, 或者收到信号,把变量should_exit设置为True等等, 如果在循环中判断程序需要进行退出, 就会进入退出逻辑shutdown, 该逻辑比较简单, 注释和代码如下:

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
Python复制代码async def shutdown(self, sockets=None):
logger.info("Shutting down")

# 关闭socket, 不让有新的连接建立
for server in self.servers:
server.close()
for sock in sockets or []:
sock.close()
for server in self.servers:
await server.wait_closed()

# 关闭已经创建的连接, 并等待他们处理完毕
for connection in list(self.server_state.connections):
connection.shutdown()
await asyncio.sleep(0.1)

# 等待连接关闭或者用户强制关闭
if self.server_state.connections and not self.force_exit:
msg = "Waiting for connections to close. (CTRL+C to force quit)"
logger.info(msg)
while self.server_state.connections and not self.force_exit:
await asyncio.sleep(0.1)

# 等待后台任务完成或者用户强制关闭
if self.server_state.tasks and not self.force_exit:
msg = "Waiting for background tasks to complete. (CTRL+C to force quit)"
logger.info(msg)
while self.server_state.tasks and not self.force_exit:
await asyncio.sleep(0.1)

# 通过lifespan告诉ASGI应用程序即将关闭
if not self.force_exit:
await self.lifespan.shutdown()

至此整个主流程分析完毕, 下图是我整理后的一个流程图:

从图中可以很清晰的看清uvicorn与ASGI应用程序的关系, 接下来是上面部分没有详细讲过的小组件源码分析。

2.uvicorn.protocols源码分析

在了解了uvicorn的主流程后只大概的知道uvicorn是通过uvicorn.protocols与应用程序进行通信, 但是不明白他们具体是如何通信的, 接下来就开始了解uvicorn中最核心的uvicorn.protocols。

从上面的分析中我们可以知道, 作为一个Web服务器, uvicorn是通过一个socket来接收发发送请求的。 而对于socket来说, 它只关心怎么创建连接,关闭连接以及如何传输内容, 它不会关心传输的字节流的上层协议是如何实现的。
好在Asyncio提供了几种简单的网络传输模型, 它们都是对于这些传输数据的抽象, 这些抽象会在如loop.create_server和loop.create_unix_server方法中使用, 通过这个抽象我们能很方便的使用TCP和UDP连接。

uvicorn在基于Asyncio创建服务器时, 把protocol抽象通过protocols参数传入loop.create_server和loop.create_unix_server后, 维护他们返回的server对象, 剩下的与ASGI应用程序的数据交互全由protocol对象处理。
uvicorn封装的对象继承于asyncio.Protocols, 它是针对TCP协议的封装, 它总共有6个方法,包括启动时的connection_made, 接收数据时的data_received, 断开时的eof_received以及丢失连接时的connection_lost, 然后还有当TCP连接出现堵塞时的暂停pause_writing和恢复resume_writing, 具体的方法传输的参数和使用方法如下:

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
Python复制代码class Protocol(BaseProtocol):
def connection_made(self, transport):
"""
在建立连接时调用.

参数是表示管道连接的transport,
此时得到的transport需要设置为该类的transport, 方便后续connection_lost控制关闭管道.
"""

def data_received(self, data):
"""
通过该方法可以获取到客户端传输过来的数据
"""

def eof_received(self):
"""
当另一端调用write_eof()或等效函数时调用.

如果返回一个假值(包括None),则传输将关闭自身。
如果它返回true值,则关闭传输取决于协议.
"""

def connection_lost(self, exc):
"""
当连接丢失或关闭时调用, 根据exc判断是否要关闭trnasport.
参数是一个异常对象或None(后者表示接收到常规EOF或中止或关闭连接).
"""

def pause_writing(self):
"""
当transport缓冲区超过高水位(high-water mark)时调用,
此时应该能控制外部不再写入数据(通常是一个asyncio.Future),
同时应该通过transport.pause_reading来停止获取数据, 之后TCP就会通过拥塞机制使得客户端减缓发送数据的速度。
"""

def resume_writing(self):
"""
当transport缓冲区排放低于低水位线(low-water mark)时调用.
此时要释放标志, 使得外部可以继续写入数据,
同时通过transport.resume_reading来恢复获取数据,
之后TCP就会通过拥塞机制知道服务端的处理能力上来了, 使客户端加快发送速度。
"""

了解完asycnio.Protocol后, 可以正式了解uvicorn对asyncio.Protocol做了哪些修改来达到跟应用程序进行通信, 由于各个Protocol的封装差不多, 这里以httptools_impl.HttpToolsProtocol为例子进行说明。

首先是类的初始化, HttpToolsProtocol 在初始化时会加载config并配置到日志,对应的websocket协议处理,对应的http解析器以及serve创建的统计容器等, 其中需要注意的是, 在初始化时, 传入的变量是HttpToolsProtocol的实例化本身。
接着是Protocol的几大主要协议接口函数, 这里以源码和注释进行分析:

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
Python复制代码class HttpToolsProtocol(asyncio.Protocol):
def connection_made(self, transport):
# 添加实例本身到集合, 代表当前还有连接在处理
self.connections.add(self)

self.transport = transport
# 初始化流控制
self.flow = FlowControl(transport)
# 简单的初始化实例trsnaport的相关编列
self.server = get_local_addr(transport)
self.client = get_remote_addr(transport)
self.scheme = "https" if is_ssl(transport) else "http"

if self.logger.level <= TRACE_LOG_LEVEL:
prefix = "%s:%d - " % tuple(self.client) if self.client else ""
self.logger.log(TRACE_LOG_LEVEL, "%sConnection made", prefix)

def connection_lost(self, exc):
# 从集合删除实例本身, 代表当前连接已经处理玩了, 不需要进入统计容器
self.connections.discard(self)

if self.logger.level <= TRACE_LOG_LEVEL:
prefix = "%s:%d - " % tuple(self.client) if self.client else ""
self.logger.log(TRACE_LOG_LEVEL, "%sConnection lost", prefix)

# 设置cycle, 告诉他连接已经断开
if self.cycle and not self.cycle.response_complete:
self.cycle.disconnected = True
if self.cycle is not None:
self.cycle.message_event.set()
if self.flow is not None:
self.flow.resume_writing()

def _unset_keepalive_if_required(self):
"""取消keep alive timeout的任务,
一般来说, 在发送数据后服务端会等待客户端发送数据, 如果超过多少秒没有发送数据则可以判断该客户端已经断开了, 服务端可以主动关闭连接
而uvicorn通过timeout_keep_alive_task来实现
"""
if self.timeout_keep_alive_task is not None:
self.timeout_keep_alive_task.cancel()
self.timeout_keep_alive_task = None

def data_received(self, data):
self._unset_keepalive_if_required()

try:
# 接受字节数据, 并交由http解析器进行解析
self.parser.feed_data(data)
except httptools.HttpParserError as exc:
# 解析失败, 应该不是http协议的数据, 断开连接
msg = "Invalid HTTP request received."
self.logger.warning(msg, exc_info=exc)
self.transport.close()
except httptools.HttpParserUpgrade:
# 已经超过了解析器能解析的协议版本, 应该交由更新的协议解析器处理
self.handle_upgrade()

分析完了几个跟连接相关的主要方法后就会发现分析路线已经断了, 而该类中还有很多on_xxx的方法, 它们也没有被其他方法调用。
这是因为在初始化HTTP协议解析器的时候,uvicorn.protocol把自己的实例传入了HTTP解析器中, 解析器会边接收数据边按照url, header, body来顺序解析, 并在执行每种数据解析后, 会通过回调告诉传入的实例, uvicorn正是通过on_xxx方法来监听这些回调并处理解析完的HTTP数据:

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
Python复制代码class HttpToolsProtocol(asyncio.Protocol):
def on_url(self, url):
"""这是收到一个请求后的第一次解析, 可以认为是该请求体的初始化, 此时会根据url和连接数据进行初始化, 并存放在实例的scope中"""
method = self.parser.get_method()
parsed_url = httptools.parse_url(url)
raw_path = parsed_url.path
path = raw_path.decode("ascii")
if "%" in path:
path = urllib.parse.unquote(path)
self.url = url
self.expect_100_continue = False
self.headers = []
self.scope = {
"type": "http",
"asgi": {"version": self.config.asgi_version, "spec_version": "2.1"},
"http_version": "1.1",
"server": self.server,
"client": self.client,
"scheme": self.scheme,
"method": method.decode("ascii"),
"root_path": self.root_path,
"path": path,
"raw_path": raw_path,
"query_string": parsed_url.query if parsed_url.query else b"",
"headers": self.headers,
}

def on_header(self, name: bytes, value: bytes):
"""解析器在解析header时, 是按照header一行一行进行解析的, 所以每即系一行header都会调用一次on_header, 并把他们存在实例的headers中"""
name = name.lower()
if name == b"expect" and value.lower() == b"100-continue":
self.expect_100_continue = True
self.headers.append((name, value))

def on_headers_complete(self):
"""对于大部分前置web框架来说, 一般解析到header后就结束不再解析了, 会开始发送到正真处理的应用程序, uvicorn也是这样的"""
http_version = self.parser.get_http_version()
if http_version != "1.1":
self.scope["http_version"] = http_version
if self.parser.should_upgrade():
# 如果发现当前http版本更加高级(比如websocket), 则不再处理, 在另外一个逻辑会转到websocket处理
return

# Handle 503 responses when 'limit_concurrency' is exceeded.
if self.limit_concurrency is not None and (
len(self.connections) >= self.limit_concurrency
or len(self.tasks) >= self.limit_concurrency
):
# 当前并发数过高, 不再转发给后面的应用程序, 直接返回错误, 这里是一个具有ASGI标准函数签名的函数, 里面实现的功能是发送错误信息到socket
app = service_unavailable
message = "Exceeded concurrency limit."
self.logger.warning(message)
else:
app = self.app

# cycle相当于一个request的处理流程
# 普通的HTTP请求只对应一个cycle就可以了, 这里是兼容Pipelined HTTP请求
existing_cycle = self.cycle
self.cycle = RequestResponseCycle(
scope=self.scope,
transport=self.transport,
flow=self.flow,
logger=self.logger,
access_logger=self.access_logger,
access_log=self.access_log,
default_headers=self.default_headers,
message_event=asyncio.Event(),
expect_100_continue=self.expect_100_continue,
keep_alive=http_version != "1.0",
on_response=self.on_response_complete,
)
if existing_cycle is None or existing_cycle.response_complete:
# 如果上个请求已经处理完了, 则开始处理这个请求(通过run_asgi来运行)
task = self.loop.create_task(self.cycle.run_asgi(app))
task.add_done_callback(self.tasks.discard)
self.tasks.add(task)
else:
# 如果上个请求没有处理完, 就先暂停读取数据, 并把该cycle放到pipeline暂存
self.flow.pause_reading()
self.pipeline.insert(0, (self.cycle, app))

def on_body(self, body: bytes):
"""读取到原生的body字节, 如果ASGI处理者还在运行, 且不是websocket, 则转给ASGI处理者
注: 一个请求可能会触发多次on_body"""
if self.parser.should_upgrade() or self.cycle.response_complete:
return
self.cycle.body += body
if len(self.cycle.body) > HIGH_WATER_LIMIT:
# 由于ASGI应用程序会根据调用者需要才来获取body(比如starlette的 await request.body()), 如果应用程序没有需要则会暂缓获取body数据
self.flow.pause_reading()
# 告诉ASGI应用程序, body已经获取结束(通常在cycle的more_body为False的时候, 才会检查message_event)
self.cycle.message_event.set()

def on_message_complete(self):
if self.parser.should_upgrade() or self.cycle.response_complete:
return
# 表示body已经读取结束了
self.cycle.more_body = False
self.cycle.message_event.set()

def on_response_complete(self):
"""返回响应时的回调"""
self.server_state.total_requests += 1

if self.transport.is_closing():
return

# 设置一个keep_alive的机制, 服务端返回响应后会设置一个倒计时future, 该future只有在上面data_received收到请求的时候才会取消
# 如果该future没有取消, 则会调用timeout_keep_alive_handler函数来关闭transport通道
self._unset_keepalive_if_required()

self.timeout_keep_alive_task = self.loop.call_later(
self.timeout_keep_alive, self.timeout_keep_alive_handler
)

# 恢复读取数据
self.flow.resume_reading()

if self.pipeline:
# 如果是pipeline请求, 则开始处理刚才暂存的cycle
cycle, app = self.pipeline.pop()
task = self.loop.create_task(cycle.run_asgi(app))
task.add_done_callback(self.tasks.discard)
self.tasks.add(task)

在了解解析HTTP数据的时候, 经常会遇到一个cycle对象, 这个对象是基于ASGI负责读写数据转换的对象, 这个对象有send和receive两个方法, 这两个方法的命名是站在ASGI应用程序的角度来命名的。
其中send通过传入的参数message获取到ASGI应用程序返回的数据, 并依据ASGI协议进行解析, 并拼接成HTTP协议的字节流, 当ASGI应用程序发送结束标记时, send会把拼接的字节流通过socket返回给客户端, 同时触发on_response_complete方法。
而receive比较简单, 它只负责接收获取到已经解析完成的HTTP数据(早前面on_xxx时会把数据传给cycle), 然后发送到ASGI应用程序中。
这两个方法都是通过on_headers_complete中执行的run_asgi方法来调用的, 通过该方法, uvicorn会把数据的处理权转给ASGI应用程序, 如果ASGI应用程序处理异常, 则会返回HTTP状态码为500的响应给客户端并关闭transport。

分析完了cycle对象后, 再次回到protocol的data_received的方法中, 这里通过获取httptools.HttpParserUpgrade异常的方式得知当前可能是一个WebSocket请求, 于是进入handle_upgrade逻辑, 这个逻辑会检查是否加载了解析WebSocket解析器以及请求体是否满足WebSocket条件, 如果不满足就会返回一个响应体告诉客户端当前无法支持该HTTP请求的升级协议, 如果满足则会生成WebSocketProtocols来处理socket的数据, 并把它设置为当前的transport向关联, 不过这个WebsocketProtocols基本上是HTTP protocols和cycle两个对象的融合, 具体处理步骤也差不多, 这里就不多做描述了。

至此, 大体上的uvicorn.protocols源码就分析完了, 由于uvicorn是把asyncio.Protocol, 解析器, cycle三者结合在一起, 所以分析起来要经常跳转, 因此我把他们的流程转成如下的图:
流处理图

3.总结

至此, uvicorn的核心流程已经分析完了, 它先是通过server来启动一个服务, 并管理服务状态, 然后再通过protocol负责做双端的序列化, 使ASGI应用程序能够按照ASGI协议读写数据, 其中protocol还融合了HTTP解析器解析HTTP并通过它来解析数据。
当然, 除了上述主流程外, uvicorn还包括了中间件, 多进程启动以及监控文件变化重启服务等组件, 这些组件的代码量不大, 分析源码也说不出啥, 这里就简单略过。

本文转载自: 掘金

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

开发人员,怎能没有个人博客!记录一次基于宝塔切换网站域名的过

发表于 2021-11-24

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

前言

  周末,发现有台服务器快到期了,然后需要切换服务器和域名。本次将基于宝塔进行服务器和域名的切换,特记录下,分享给大家。

准备

  一台云服务器记录好外网的ip地址,在切换域名解析的时候需要填写外网地址,备案好的域名。两个服务器的宝塔地址和账号信息。

域名解析

  域名解析是把我们通过信管局备案的域名解析到云空间服务器的外网ip上,使用户能够通过域名访问到相关服务信息。因此当我们切换服务器的时候,需要重新解析一下域名。如下图,已经建立了为test的二级域名,在登录宝塔服务器之后,就可以直接进行配置啦。

  需要注意的是,新添加的解析,预计在10分钟左右生效。
域名申请教程可以参考:开发人员,怎能没有个人博客!如何选择合适自己的域名

图片.png

登录宝塔

图片.png
  登录两台服务器的宝塔页面,在控制台找到网站,选择需要切换的网站信息,点击设置。

图片.png
  点击设置出现如下图中的信息,需要输入新的域名信息,点击添加按钮。这时已经将域名添加好了。

图片.png
  这时可以点击查看一下配置文件,如下图,可以看到基于test.*****.com 的域名已经出现在配置文件中,证明已经配置成功了。
图片.png

配置SSL

  在配置文件下面可以配置SSL信息。如下图中,可以配置SSL的密钥(KEY)和密钥(KEY)和证书(PEM格式)。建议开启强制HTTPS ,这样用户访问的时候,就会提示可信任的网站。

大家可以申请免费的SSL证书。具体教程参考
开发人员,怎能没有个人博客!HTTP升级HTTPS,网站部署SSL证书
开发人员,怎能没有个人博客!记录一次SSL证书到期更换SSL证书的过程
开发人员,怎能没有个人博客!网站基于宝塔部署SSL证书

图片.png

加载配置

  当我们将上面所有的信息配置完成之后,需要重新加载一下刚刚的配置。操作流程是如下图,在首页中选择系统使用的代理工具,点击重新加载配置即可。
图片.png

访问服务

  重启完代理工具,稍等片刻,清楚浏览器缓存之后,即可通过域名访问服务。下面是配置之后,通过新的域名访问的网站服务。

图片.png

结语

  好了,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

  如果大家有个人技术网站或者博客,也可以一起交流学习,加个友情链接。作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

本文转载自: 掘金

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

如何使用find和locate 命令在Linux 中查找文件

发表于 2021-11-24

我们在使用Linux的时候,难免要在系统中查找某个文件,比如查找xxx配置文件在哪个路径下、查找xxx格式的文件有哪些等等。

既然是Linux系统,那么使用命令行形式去查找肯定是最快最直接的方法,虽然现在有很多连接工具可以提供查找功能,但是归根到底还是利用了相关查找的命令,那么今天瑞哥就带大家来学习一下,如何用命令的形式查找文件。

因为涉及到很多骚操作,大家肯定在工作中没有全部用到,建议你先收藏本文,用到的时候记得回来查阅。

find命令是 Linux 中最重要和最常用的命令之一。

使用 find 命令在 Linux 中查找文件和目录

Linux find 命令是一个强大的工具,它使系统管理员能够根据模糊的搜索条件定位和管理文件和目录,它支持按文件、文件夹、名称、创建日期、修改日期、所有者和权限进行搜索。

find 命令用于查找文件和目录并对其进行后续操作,它递归地搜索每个路径中的文件和目录,因此,当find命令遇到给定路径中的目录时,它会在其中查找其他文件和目录。

按名称查找文件

find命令的一般语法是:

1
css复制代码find [path] [options] [expression]

让我们分解一下这个语法:

  • path: 定义 find 将搜索文件的起始目录。
  • options:控制find 进程的行为和优化方法。
  • expression:此属性由选项、搜索模式和操作符分隔的操作组成。

find 命令最常见的用途是按文件名搜索文件,-type f 选项告诉系统我们正在寻找一个文件,要使用文件名查找文件,请使用-name 带有默认命令的 标志。

例如,要搜索wljslmz.ppt 在/home 目录中命名 的 文件,您可以使用以下命令:

1
arduino复制代码find /home -type f -name wljslmz.ppt

Linux 对文件名区分大小写,因此如果您要查找名为 的文件Wljslmz.ppt,上面显示的命令将不会返回任何结果,在这种情况下,您将需要使用该-iname选项而不是-name.

该-iname选项运行不区分大小写的搜索,因此我们可以这样做:

1
arduino复制代码find /home -type f -iname wljslmz.ppt

此命令将找到具有以下任何名称的文件:wljslmz.ppt,Wljslmz.ppt,WLJSLMZ.ppt等。

按部分名称查找文件

您可以使用文件名元字符,例如星号 *,但您应该在每个字符前放置一个转义字符\ 或将它们括在引号中。

例如,要查找所有以**.ppt**结尾的文件,您可以运行:

1
arduino复制代码find /home -type f -name '*.ppt'

这与:

1
arduino复制代码find /home -type f -name \*.ppt

同样,要查找 Linux 系统上名称以 开头的所有文件wljslmz,您可以运行:

1
arduino复制代码find /home -type f -name 'wljslmz*'

按大小查找文件

使用 find 命令,我们还可以轻松实现一些看起来很复杂的事情:找到比给定大小更大或更小的文件。

该-size选项上find允许我们搜索特定大小的文件,+和-前缀表示“大于”和“小于”。

下面的示例将搜索所有大于 2 GB 的文件,注意+ 符号的使用:

1
arduino复制代码find /home -type f -size +2G

在上面的示例中,后缀 G 表示千兆字节,其他后缀:

  • c: 字节。
  • k: 千字节。
  • M: 兆字节。

上述 find 命令用于搜索所有大于指定大小的文件。

find 命令示例将搜索所有小于 100 KB 的文件,注意- 符号的使用:

1
arduino复制代码find /home -type f -size -100k

如何在 Linux 中查找特定大小的文件?

使用以下命令查找大小介于 200 兆字节和 320 兆字节之间的文件:

1
arduino复制代码find /home -type f -size +200M -size -320M

使用时间戳查找文件

Linux 为文件系统中的每个文件分配特定的时间戳,find 命令还可以根据上次修改、访问或更改时间搜索文件。

-mtime选项用于指定文件存在的天数,表达式可以以两种方式使用:

  • -mtime +N 查找多N 天前修改的文件 (大于)。
  • -mtime -N 查找少于N 几天前修改的文件 (少于)。

如果您输入+3,它将查找/etc目录中超过 4 天的所有文件。

1
bash复制代码find /etc -type f -mtime +4

查找过去 24 小时内发生更改的所有文件:

1
bash复制代码find /etc -type f -mtime -1

使用 -mmin N 表达式来依赖分钟而不是天。

从/etc 上一分钟修改的目录中查找所有文件 。

1
bash复制代码find /etc -type f -mmin -1

可以组合表达式,以下是如何在 Linux 中查找不到 60 分钟前和超过 30 分钟前更改过的文件:

1
bash复制代码find /etc -type f -mmin -60 -mmin +30

按所有者查找文件

要查找特定用户或组拥有的文件,请使用 -user 和 -group 选项。

例如,要在服务器的/home文件夹中查找用户wljslmz拥有的文件:

1
arduino复制代码find /home -type f -user wljslmz

www-data在/home目录中查找属于某个组的所有文件:

1
arduino复制代码find /home -type f -group www-data

按权限查找文件

该 -perm 选项允许用户搜索具有特定权限集的文件。

下面的命令将在当前目录中查找权限为 777 的文件。

1
arduino复制代码find /home -type f -perm 777

使用该 - 选项意味着“至少设置了这个权限级别,以及任何更高的权限”。

1
arduino复制代码find /home -type f -perm -644

此示例显示/home目录中至少具有 644 权限的所有资源,这意味着,与权限的文件,例如777,745,666,655,654,等,将匹配,同时与权限的文件642,611,600,544,等,将不匹配。

按名称查找目录

到目前为止,我们看到的所有示例都返回文件,但是,如果您只需要搜索目录,则可以使用该 -type d 参数。

换句话说,您可以find通过使用-type d标志(d表示目录)阻止Linux 中的命令搜索目录以外的其他文件类型。

查找/opt目录下名字为app的文件夹:

1
bash复制代码find /opt -type d -name app

使用 locate 命令在 Linux 中查找文件和目录

虽然 find 是Linux 中最流行和最强大的用于文件搜索的命令行实用程序之一,但对于需要即时结果的情况来说,它的速度还不够快。

该locate命令比find命令更快,find因为它使用先前构建的数据库,而该locate命令实时搜索所有实际目录和文件。

如果 locate 未安装,您可以使用 Linux 发行版的包管理器轻松安装它。

在 Ubuntu 或任何其他 基于Debian的系统上,键入以下命令:

1
复制代码sudo apt install mlocate

如果您使用的是 Fedora 或 CentOS,请改用以下命令:

1
复制代码sudo dnf install mlocate

Arch Linux 用户需要执行:

1
复制代码sudo pacman -S mlocate

在locate 可以使用之前 ,需要创建数据库,这是通过updatedb 命令完成的,该 命令顾名思义就是更新数据库。

1
复制代码sudo updatedb

数据库将每天自动更新,但您也可以随时自行更新,以便获得最新结果。

按名称查找文件

locate命令非常易于使用。您所要做的就是将您要搜索的文件名传递给它。

1
复制代码locate wljslmz.ppt

就像find命令一样,locate配置为以区分大小写的方式处理查询,要让locate命令忽略区分大小写并显示大写和小写查询的结果,您需要使用该-i选项。

1
css复制代码locate -i wljslmz.ppt

按部分名称查找文件

如果要搜索包含字符串的所有文件名.ppt,则可以使用locate以下方式进行搜索:

1
arduino复制代码locate '*.ppt'

限制搜索结果

您可以使用-n选项返回搜索所需数量的结果,以避免搜索结果出现冗余 。

例如,如果您只需要查询的 20 个结果,则可以键入以下命令:

1
arduino复制代码locate -n 20 '*.ppt'

显示匹配条目的数量

要使用 locate 命令计算文件名或搜索模式的出现次数,请调用-c 选项。

1
arduino复制代码locate -c '*.ppt'

总结

Linux 用户可以使用两个最广泛使用的文件搜索实用命令:find、locate,两者都是在系统上查找文件的好方法,使用哪个命令还是根据情况来。

本文应该让您对如何在 Linux 系统上查找文件有一个基本的了解,想要将搜索命令玩的溜,别忘了使用各类参数!

本文转载自: 掘金

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

Mysql 学习 Client 端调用主流程

发表于 2021-11-24

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 .前言

出于好奇 , 这一篇来看一下 MySQL Client 端的调用主流程 , 为后续的 MySQL 系列文档开个头

二 . 创建 Connect

以获取连接为例 , 当获取连接时 , 会通过多种方式调用 Spring 的 DataSourceUtils # getConnection

此时还处在 Spring 的业务体系中. Connect 的流程在启动时创建和运行时调用是两个完全不同的流程 , 先来看一 CreateConnect 的主流程

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
java复制代码// Step 1 : Connect 的创建入口
public static Connection doGetConnection(DataSource dataSource) throws SQLException {

// ...
Connection con = fetchConnection(dataSource);
// ...

}


// Step 2 : 连接池处理
// 如果使用了连接池 , 此时的连接会交给对应连接池来处理
pool = result = new HikariPool(this);


> 以下跳过连接池的相关原理 ,直接看到 com.mysql.cj.jdbc.Driver 的核心处理流程


// Step 3 : MySQL 驱动入口
public java.sql.Connection connect(String url, Properties info) throws SQLException {

try {
if (!ConnectionUrl.acceptsUrl(url)) {
/*
* According to JDBC spec:
* The driver should return "null" if it realizes it is the wrong kind of driver to connect to the given URL. This will be common, as when the
* JDBC driver manager is asked to connect to a given URL it passes the URL to each loaded driver in turn.
*/
return null;
}

ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
// 根据类型创建不同的连接方式
switch (conStr.getType()) {
case SINGLE_CONNECTION:
return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());

case LOADBALANCE_CONNECTION:
return LoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl) conStr);

case FAILOVER_CONNECTION:
return FailoverConnectionProxy.createProxyInstance(conStr);

case REPLICATION_CONNECTION:
return ReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl) conStr);

default:
return null;
}

} catch (UnsupportedConnectionStringException e) {
// when Connector/J can't handle this connection string the Driver must return null
return null;

} catch (CJException ex) {
throw ExceptionFactory.createException(UnableToConnectException.class,
Messages.getString("NonRegisteringDriver.17", new Object[] { ex.toString() }), ex);
}
}

三 . 运行 SQL 流程

这一节来看一下执行 SQL 时的调用流程

在一个 SQL 的生命周期中 , 主要有2个主要的流程 :

  • 流程一 : 基于事务起点的 SET autocommit
  • 流程二 : 真正核心的 SQL 执行语句

3.1 事务的入口

Spring 的事务起点是 TransactionAspectSupport , 进入一系列流程后 , 会进入连接池的处理中 , 这里涉及到 SpringTransaction 的流程 , 可以看看这篇文章 基于 Spring 的事务管理, 这里只是简单过一下

  • Step 1 : TransactionImpl # begin : 由 Begin 开启事务流程
  • Step 2 :ConnectionImpl # setAutoCommit : 开启自动提交流程
  • Step 3 : NativeSession # execSQL : 进入 SQL 执行核心流程
1
2
3
4
5
6
java复制代码public void begin() {
if ( !doConnectionsFromProviderHaveAutoCommitDisabled() ) {
getConnectionForTransactionManagement().setAutoCommit( false );
}
status = TransactionStatus.ACTIVE;
}

事务会调用setAutoCommit , 其根本也是调用一个execSQL 语句来控制事务

1
java复制代码this.session.execSQL(null, autoCommitFlag ? "SET autocommit=1" : "SET autocommit=0", -1, null, false, this.nullStatementResultSetFactory, this.database, null, false);

到了这里会直接调用到 execSQL , 而流程二的普通 SQL 语句 , 会由对应的 executeUpdate / executeQuery /executeInternal 发起流程处理

3.2 普通业务执行流程

  • Step 1 : AbstractEntityPersister # insert : 由 Hibernate/JPA 发起的操作流程
  • Step 2 : ResultSetReturnImpl # executeUpdate : 执行 Update 语句 (或者 Query -> executeQuery)
  • Step 3 : HikariProxyPreparedStatement # executeUpdate : 连接池的中间处理 , 后续可以专门看看
  • Step 4 : ClientPreparedStatement # executeUpdate : 由 mysql 驱动接管
  • Step 5 : ClientPreparedStatement # executeUpdateInternal :
  • Step 5 : ClientPreparedStatement # executeInternal : 由底层方法调用抽象类 , 最终调用 execSQL
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复制代码// 处理 Update 语句 , 核心流程如下 : 
protected long executeUpdateInternal(QueryBindings<?> bindings, boolean isReallyBatch) throws SQLException {

// 1. 获取 JDBC 连接
JdbcConnection locallyScopedConn = this.connection;

// 2. 解析出要发送给 MySQL 的语句包
Message sendPacket = ((PreparedQuery<?>) this.query).fillSendPacket(bindings);

// 3. 调用通过处理方法执行 SQL
rs = executeInternal(-1, sendPacket, false, false, null, isReallyBatch);

// 4. 设置处理结果
this.results = rs;

// 设置更新数量 ,这里会对重复语句进行统计处理
this.updateCount = rs.getUpdateCount();

// 5. 获取最后插入的 ID
this.lastInsertId = rs.getUpdateID();

// 6. 返回更新数量
return this.updateCount;

}

executeInternal 主流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码protected <M extends Message> ResultSetInternalMethods executeInternal(int maxRowsToRetrieve, M sendPacket, boolean createStreamingResultSet,
boolean queryIsSelectOnly, ColumnDefinition metadata, boolean isBatch) throws SQLException {

// Step 1 : 获取连接
JdbcConnection locallyScopedConnection = this.connection;

// Step 2 : 获取插入值得绑定关系
((PreparedQuery<?>) this.query).getQueryBindings()
.setNumberOfExecutions(((PreparedQuery<?>) this.query).getQueryBindings().getNumberOfExecutions() + 1);

// Step 3 :设置返回结果设置方法
ResultSetInternalMethods rs;

// Step 4 : 设置超时方法
CancelQueryTask timeoutTask = startQueryTimer(this, getTimeoutInMillis());

// Step 5 : 调用具体的 SQL 执行语句
rs = ((NativeSession) locallyScopedConnection.getSession()).execSQL(this, null, maxRowsToRetrieve, (NativePacketPayload) sendPacket,
createStreamingResultSet, getResultSetFactory(), this.getCurrentCatalog(), metadata, isBatch);

// Step 6 : 超时时间处理 ,省略
}

3.3 业务处理通用流程

看了入口方法, 现在来看一下 execQuery 的具体处理流程 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码// C- NativeSession
public <T extends Resultset> T execSQL(Query callingQuery, String query, int maxRows, NativePacketPayload packet, boolean streamResults,
ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory, String catalog, ColumnDefinition cachedMetadata, boolean isBatch) {

//
int endOfQueryPacketPosition = endOfQueryPacketPosition = packet.getPosition();

//
long queryStartTime = System.currentTimeMillis();

// 如果 packet == null , 调用如下处理
return ((NativeProtocol) this.protocol).sendQueryString(callingQuery, query, encoding, maxRows, streamResults, catalog, cachedMetadata,
this::getProfilerEventHandlerInstanceFunction, resultSetFactory);

//
return ((NativeProtocol) this.protocol).sendQueryPacket(callingQuery, packet, maxRows, streamResults, catalog, cachedMetadata,
this::getProfilerEventHandlerInstanceFunction, resultSetFactory);

}

在上文调用 sendQueryPacket 发起了SQL 执行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码// C- NativeProtocol
public final <T extends Resultset> T sendQueryPacket(Query callingQuery, NativePacketPayload queryPacket, int maxRows, boolean streamResults,
String catalog, ColumnDefinition cachedMetadata, GetProfilerEventHandlerInstanceFunction getProfilerEventHandlerInstanceFunction,
ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory) throws IOException {
this.statementExecutionDepth++;

byte[] queryBuf = null;
int oldPacketPosition = 0;
long queryStartTime = 0;
long queryEndTime = 0;

queryBuf = queryPacket.getByteBuffer();
oldPacketPosition = queryPacket.getPosition(); // save the packet position

// 查询启动时间
queryStartTime = getCurrentTimeNanosOrMillis();

// 查询语句
LazyString query = new LazyString(queryBuf, 1, (oldPacketPosition - 1));

// 发送命令
NativePacketPayload resultPacket = sendCommand(queryPacket, false, 0);

// 获取所有的 Result 结果
T rs = readAllResults(maxRows, streamResults, resultPacket, false, cachedMetadata, resultSetFactory);

// 反射拦截器
T interceptedResults = invokeQueryInterceptorsPost(query, callingQuery, rs, false);

// 返回结果
return rs;

}

3.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
java复制代码// C- NativeProtocol
public final NativePacketPayload sendCommand(Message queryPacket, boolean skipCheck, int timeoutMillis) {

int command = queryPacket.getByteBuffer()[0];
this.commandCount++;

if (this.queryInterceptors != null) {
NativePacketPayload interceptedPacketPayload = (NativePacketPayload) invokeQueryInterceptorsPre(queryPacket, false);

if (interceptedPacketPayload != null) {
return interceptedPacketPayload;
}
}

this.packetReader.resetMessageSequence();

// 获取旧 timeout 时间并且设置新的超时时间
// PS : 这里的 oldTimeout 在 finally 中会再次设置 soTimeout
int oldTimeout = 0;
oldTimeout = this.socketConnection.getMysqlSocket().getSoTimeout();
this.socketConnection.getMysqlSocket().setSoTimeout(timeoutMillis);

//
checkForOutstandingStreamingData();

// 设置互斥锁
this.serverSession.setStatusFlags(0, true);

// 清空输入流
clearInputStream();
this.packetSequence = -1;

// 发起包
send(queryPacket, queryPacket.getPosition());

// 获取 Return 结果
// 1. resultPacket = readMessage(this.reusablePacket)
// 2. checkErrorMessage(resultPacket)
NativePacketPayload returnPacket = checkErrorMessage(command);

return returnPacket;
}
1
2
3
4
5
6
7
java复制代码// C- NativeProtocol
public final void send(Message packet, int packetLen) {
//....

// 通过 Sender 发送远程包
this.packetSender.send(packet.getByteBuffer(), packetLen, this.packetSequence);
}

C- SimplePacketSender

1
2
3
4
5
6
7
8
9
10
11
JAVA复制代码public void send(byte[] packet, int packetLen, byte packetSequence) throws IOException {
PacketSplitter packetSplitter = new PacketSplitter(packetLen);

// 持续从远程读取包
while (packetSplitter.nextPacket()) {
this.outputStream.write(NativeUtils.encodeMysqlThreeByteInteger(packetSplitter.getPacketLen()));
this.outputStream.write(packetSequence++);
this.outputStream.write(packet, packetSplitter.getOffset(), packetSplitter.getPacketLen());
}
this.outputStream.flush();
}

3.6 解析 Result

Step 1 : packetReader.readMessage

这里的 packetReader 主要包含以下几种实现 MultiPacketReader :

MySQL-MessageReader.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
JAVA复制代码public NativePacketPayload readMessage(Optional<NativePacketPayload> reuse, NativePacketHeader header) throws IOException {

// 获取长度和 Message 实现对哦下
int packetLength = header.getMessageSize();
NativePacketPayload buf = this.packetReader.readMessage(reuse, header);

// 此处通过 do-while 进行循环获取
do {

//......
this.packetReader.readMessage(Optional.of(multiPacket), hdr);
// 写入 byte 数据
buf.writeBytes(StringLengthDataType.STRING_FIXED, multiPacket.getByteBuffer(), 0, multiPacketLength);
// 循环获取 , 直到最大长度 -> MAX_PACKET_SIZE = 256 * 256 * 256 - 1;
} while (multiPacketLength == NativeConstants.MAX_PACKET_SIZE);

return buf;
}

checkErrorMessage 判断是否为错误返回

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
java复制代码public void checkErrorMessage(NativePacketPayload resultPacket) {

resultPacket.setPosition(0);

// 获取状态嘛
byte statusCode = (byte) resultPacket.readInteger(IntegerDataType.INT1);

// Error handling
// 此处通过状态码判断是否为异常结果
if (statusCode == (byte) 0xff) {
// 省略 error 处理环节


// 此处会通过状态和异常处理的结果抛出对应的异常 , 该方法没有具体的返回值
if (xOpen != null) {
if (xOpen.startsWith("22")) {
throw new DataTruncationException(errorBuf.toString(), 0, true, false, 0, 0, errno);
}

if (errno == MysqlErrorNumbers.ER_MUST_CHANGE_PASSWORD) {
throw ExceptionFactory.createException(PasswordExpiredException.class, errorBuf.toString(), getExceptionInterceptor());

} else if (errno == MysqlErrorNumbers.ER_MUST_CHANGE_PASSWORD_LOGIN) {
throw ExceptionFactory.createException(ClosedOnExpiredPasswordException.class, errorBuf.toString(), getExceptionInterceptor());
}
}

throw ExceptionFactory.createException(errorBuf.toString(), xOpen, errno, false, null, getExceptionInterceptor());

}
}

获取最终返回结果

1
2
3
4
5
6
7
8
9
10
java复制代码public <T extends Resultset> T readAllResults(int maxRows, boolean streamResults, NativePacketPayload resultPacket, boolean isBinaryEncoded,
ColumnDefinition metadata, ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory) throws IOException {

// 调用具体的实现类获取最终结果 , 结果会被放在 rowData 中
T topLevelResultSet = read(Resultset.class, maxRows, streamResults, resultPacket, isBinaryEncoded, metadata, resultSetFactory);


}

// 这里的 Read 有多种 , 后面再来看一看如何实现 Byte 读取的

image.png

总结

东西不多 , 主要是一些主流程代码 , 其中很多环节都比较模糊 , 主要是为了串联整个流程 , 后续小细节会抽空深入的了解一下.

相对于其他的框架代码 , Mysql 的代码看起来很生涩难懂 , 每个主流程间参杂了很多辅助属性 , 如果业务上出现了问题 , 又不确定最终的执行语句 ,可以考虑在 sendCommand 等方法中添加断点

连接池的使用通常在 ResultSetReturnImpl 和 ClientPreparedStatement 之间进行处理 , 后续可以关注一下

image.png

image.png

本文转载自: 掘金

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

1…206207208…956

开发者博客

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