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

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


  • 首页

  • 归档

  • 搜索

GitOps 应用实践系列 - 综述 什么是 GitOps

发表于 2021-10-19

大家好,我是张晋涛。

接下来会为大家带来一个 GitOps 的应用实践系列文章,这是第一篇。

什么是 GitOps

首先我们来了解下 GitOps:

GitOps 最早是在2017年由 Weaveworks 创立提出,它是一种进行 Kubernetes 集群管理和应用程序交付的方式。 GitOps 使用 Git 作为声明性基础设施和应用程序的单一事实来源。 GitOps 的核心思想是拥有一个 Git repository,包含目标环境中当前所需基础设施的声明性描述,以及使目标环境与 Git repository 中描述的状态相匹配的自动化过程。借助 GitOps,可以针对 Git repository 与集群中运行的内容之间的任何差异发出警报,如果存在差异,Kubernetes reconcilers会根据情况自动更新或回滚集群。 以 Git 作为 pipeline 的中心,开发人员可以使用自己熟悉的工具发出PR,以加速和简化 Kubernetes 中应用程序部署和操作任务。

这使得 GitOps 在 Twitter 和 KubeCon 上引起了不小的轰动。

img)img

这里说一下Weaveworks 这家公司,这是一家为开发人员提供最高效的方式来连接、观察和控制 Docker 容器的公司。在官网上,我们能看到,GitOps 工作流的原则和模式,如何实现它们在生产中大规模运行 Kubernetes, 以及GitOps 和基础设施即代码 (IAC) 配置管理工具之间的区别和最佳实践等等。www.weave.works/technologie…

K8S的创始人之一:Kelsey Hightower 曾发推文评论过 GitOps是基于声明式基础设施的版本化 CI/CD, 停止脚本化操作,开始(自动化)分发。

GitOps 是如何工作的呢?

把环境配置作为 Git repository

GitOps 以代码库为核心来组织部署。 我们需要至少有两个仓库:应用程序库和环境配置库。 应用程序库包含应用程序的源代码和部署应用程序的 manifests。 环境配置库包含部署环境当前所需基础架构的所有部署manifests。 它描述了哪些应用程序和基础设施服务(消息代理、服务网格、监控工具等)应该在部署环境中以何种配置和版本运行等内容。

基于 push 与基于 pull 的部署

两种部署类型之间的区别在于如何确保部署环境与所需的基础架构相同。这里推荐,首选基于 pull 的方法,实现 GitOps 更安全、也有很多已有的最佳实践来借鉴。

基于 pull 的部署

传统的 CI/CD pipeline由外部事件触发,比如新代码被推送到应用程序库时,就触发了。

而基于 Pull 的部署方法,引入了operator。 它通过不断将环境配置库中的期望状态与部署环境中的实际状态进行比较来接管pipeline的角色。 当发现差异时,operator会更新部署环境中的状态以匹配环境配置库。 此外,它还可以监控 image registry ,以查找要部署的新镜像版本。

img

基于 pull 模型的部署不仅能做到环境配置库更改时更新环境;

operator也能做到当实际环境与环境配置库中存在差异时进行还原。

这就确保了所有更改都可以在 Git 日志中进行跟踪,因为任何人都不允许对集群进行直接更改。

那么,这种方式的监控点就集中在 operator 及各个组件上了(比如,镜像仓库是否能正常拉取到镜像等等)。

为了避免基于 Push 场景中的上帝模式权限问题,operator 应该始终与要部署的应用程序在同一环境或集群中。(k8s RBAC授权:Kubernetes 从 1.6 开始支持基于角色的访问控制机制(Role-Based Access,RBAC),集群管理员可以对用户或 service account 的角色进行更精确的资源访问控制。在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。)

基于 push 的部署

基于 Push 的部署策略可以利用流行的 CI/CD 工具来实现,例如 Jenkins、CircleCI 或 Travis CI。 应用程序的源代码与部署的应用程序所需的 Kubernetes YAML 一起存在于应用程序库中。 每当更新应用程序代码时,都会触发构建pipeline,构建容器镜像,最后使用新的部署manifest,更新环境配置库。

也可以将 YAML 的模板存储在应用程序库中。 构建新版本时,可以使用模板在环境配置库中生成 YAML。

img

对环境配置库的更改会触发部署pipeline。pipeline负责将环境配置库中的所有manifests应用到基础设施。这就需要我们更关注部署权限细分及控制。同时,这种方式无法自动注意到环境及其所需状态的任何偏差。 我们需要额外的监控报警方式,来保障环境与环境存储库中描述的内容一致。

复杂应用环境

对于大多数应用程序来说,只使用一个应用程序库和一个环境配置库是不现实的。GitOps 也能应对。可以设置多个构建 pipeline 来更新环境配置库。 然后就像上两个描述过程一样,自动化 GitOps 工作流程开始并部署应用程序。

img

我们需要在环境配置库中使用独立的分支,来管理多个环境。 选择设置 operator 或者构建pipeline,自动化 GitOps 工作流程开始并部署应用程序。

Tips:

1.不使用k8s的环境也可以考虑使用 GitOps。(目前大多数基于 pull 的 GitOps 都是在Kubernetes 下实现的。)

2.密码。永远不要在 git 中以纯文本形式存储密码!在 K8s 生态系统中,有工具支持这种加密。(比如 Vault)

3.dev、qa、prod环境不能直接能用 GitOps 来处理,可以考虑引入 CI/CD pipeline 来管理环境。

4.DevOps 与GitOps 不冲突。DevOps 是关于组织中的文化变革,可以使程序员及系统维护者们更好地合作。而GitOps 是一种实现持续交付的技术。如果已经在推进 DevOps 那么可能会更好接入 GitOps。


欢迎订阅我的文章公众号【MoeLove】

TheMoeLove

本文转载自: 掘金

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

如何在 Python 中搜索和替换文件中的文本?

发表于 2021-10-19

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

🌊 作者主页:海拥

🌊 作者简介:🥇HDZ核心组成员、🏆全栈领域优质创作者、🥈蝉联C站周榜前十

🌊 粉丝福利:粉丝群 每周送四本书,每月送各种小礼品(搪瓷杯、抱枕、鼠标垫、马克杯等)

在本文中,我将给大家演示如何在 python 中使用四种方法替换文件中的文本。

方法一:不使用任何外部模块搜索和替换文本

让我们看看如何在文本文件中搜索和替换文本。首先,我们创建一个文本文件,我们要在其中搜索和替换文本。将此文件设为 Haiyong.txt,内容如下:

在这里插入图片描述

要替换文件中的文本,我们将使用 open() 函数以只读方式打开文件。然后我们将 t=read 并使用 read() 和 replace() 函数替换文本文件中的内容。

语法: open(file, mode=’r’)

参数:

file:文件的位置
mode : 要打开文件的模式

然后我们会以写模式打开同一个文件,写入替换的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码# 创建一个变量并存储我们要搜索的文本
search_text = "资源"

# 创建一个变量并存储我们要添加的文本
replace_text = "进群"

# 使用 open() 函数以只读模式打开我们的文本文件
with open(r'Haiyong.txt', 'r',encoding='UTF-8') as file:

# 使用 read() 函数读取文件内容并将它们存储在一个新变量中
data = file.read()

# 使用 replace() 函数搜索和替换文本
data = data.replace(search_text, replace_text)

# 以只写模式打开我们的文本文件以写入替换的内容
with open(r'Haiyong.txt', 'w',encoding='UTF-8') as file:

# 在我们的文本文件中写入替换的数据
file.write(data)

# 打印文本已替换
print("文本已替换")

输出:

1
python复制代码文本已替换

在这里插入图片描述

方法二:使用 pathlib2 模块搜索和替换文本

让我们看看如何使用 pathlib2 模块搜索和替换文本。首先,我们创建一个文本文件,我们要在其中搜索和替换文本。将此文件设为 Haiyong2.txt,内容如下:
在这里插入图片描述

使用以下命令安装 pathlib2 模块:

1
python复制代码pip install pathlib2

在这里插入图片描述
该模块提供表示文件系统路径的类,其语义适用于不同的操作系统。要使用 pathlib2 模块替换文本,我们将使用 pathlib2 模块的 Path 方法。

语法:路径(文件)

参数:

file:要打开的文件的位置

在下面的代码中,我们将文本文件中的“获取更多学习资料”替换为“找群主领取一本实体书”。使用 pathlib2 模块。

代码:

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
python复制代码# 从 pathlib2 模块导入路径
from pathlib2 import Path

# 创建一个函数来替换文本
def replacetext(search_text, replace_text):

# 使用Path函数打开文件
file = Path(r"Haiyong2.txt")

# 读取文件内容并将其存储在数据变量中
data = file.read_text()

# 使用替换功能替换文本
data = data.replace(search_text, replace_text)

# 在文本文件中写入替换的数据
file.write_text(data)

# 返回“文本已替换”字符串
return "文本已替换"


# 创建一个变量并存储我们要搜索的文本
search_text = "Python"

# 创建一个变量并存储我们要更新的文本
replace_text = "Java"

# 调用replacetext函数并打印返回的语句
print(replacetext(search_text, replace_text))

输出:

1
python复制代码文本已替换

在这里插入图片描述

方法 3:使用正则表达式模块搜索和替换文本

方法 3:使用正则表达式模块搜索和替换文本
让我们看看如何使用 regex 模块搜索和替换文本。我们将使用 re.sub() 方法来替换文本。

语法: re.sub(pattern, repl, string, count=0, flags=0)

参数:

repl :要添加的文本
string :要替换的文本

代码:

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
python复制代码# 导入 re 模块
import re

# 创建一个函数来替换文本
def replacetext(search_text,replace_text):

# 以读写模式打开文件
with open('SampleFile.txt','r+') as f:

# 读取文件数据并将其存储在文件变量中
file = f.read()

# 用文件数据中的字符串替换模式
file = re.sub(search_text, replace_text, file)

# 设置位置到页面顶部插入数据
f.seek(0)

# 在文件中写入替换数据
f.write(file)

# 截断文件大小
f.truncate()

# 返回“文本已替换”字符串
return "文本已替换"

# 创建一个变量并存储我们要搜索的文本
search_text = "World"

#创建一个变量并存储我们要更新的文本
replace_text = "Universe"

# 调用replacetext函数并打印返回的语句
print(replacetext(search_text,replace_text))

输出:

1
python复制代码文本已替换

在这里插入图片描述

方法四:使用文件输入

让我们看看如何使用 fileinput 模块搜索和替换文本。为此,我们将使用 FileInput() 方法迭代文件的数据并替换文本。

语法: FileInput(files=None, inplace=False, backup=”, *, mode=’r’)

参数:

files : 文本文件的位置
mode : 要打开文件的模式
inplace :如果值为 True 则文件被移动到备份文件并且
标准输出被定向到输入文件
backup : 备份文件的扩展名

代码:

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
python复制代码# 从文件输入模块导入文件输入
from fileinput import FileInput

# 创建一个函数来替换文本
def replacetext(search_text, replace_text):

# 使用 FileInput 打开文件
with FileInput("Haiyong4.txt", inplace=True,
backup='.bak') as f:

# 使用replace函数迭代每个并使用replace_text更改search_text
for line in f:
print(line.replace(search_text,
replace_text), end='')

# 返回“文本已替换”字符串
return "文本已替换"


# 创建一个变量并存储我们要搜索的文本
search_text = "unreplaced"

# 创建一个变量并存储我们要更新的文本
replace_text = "replaced"

# 调用replacetext函数并打印返回的语句
print(replacetext(search_text, replace_text))

输出:

1
python复制代码文本已替换

我已经写了很长一段时间的技术博客,并且主要通过掘金发表,这是我的一篇关于如何在 Python 中搜索和替换文件中的文本。我喜欢通过文章分享技术与快乐。您可以访问我的博客: juejin.cn/user/204034… 以了解更多信息。希望你们会喜欢!😊

💌 欢迎大家在评论区提出意见和建议!💌

掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章

本文转载自: 掘金

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

注册中心eureka的搭建

发表于 2021-10-19

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

1.搭建项目

新建普通的springboot项目。

1.修改Application

修改EurekaServerApplication类:关键注解@EnableEurekaServer,声明为注册中心。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码package com.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;


@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}

2.修改pom文件

然后修改pom文件:一定要看好版本,现在版本真的是乱七八糟,而且有的注解被干掉了。

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
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.baocl</groupId>
<artifactId>eureka-consumer-feign</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-consumer-feign</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR1</spring-cloud.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

3.修改application.properties

最后是最重要的配置文件application.properties,楼主总结了常用的配置。

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
ini复制代码spring.application.name=eureka-server
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
server.port=1001
eureka.instance.hostname=localhost
#是否向服务注册中心注册自己
eureka.client.register-with-eureka=false
#是否检索服务 ---表示是否从注册中心拉取服务列表
eureka.client.fetch-registry=false

#是否开启自我保护模式,默认为true。

#默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。
#但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。
#Eureka通过“自我保护模式”来解决这个问题——当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),
#那么这个节点就会进入自我保护模式。一旦进入该模式,Eureka Server就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。
#当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。
#综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留),
#也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。
#eureka.server.enable-self-preservation=false

#设置清理无效节点的时间间隔,默认60000,即是60s
#eureka.server.eviction-interval-timer-in-ms=5000
#自我保护续约百分比阀值因子。如果实际续约数小于续约数阀值,则开启自我保护
#eureka.server.renewalPercentThreshold = 0.85
#节点间连接的超时时间。
#eureka.server.peerNodeConnectTimeoutMs=200
#节点间读取信息的超时时间。
#eureka.server.peerNodeReadTimeoutMs=200

#每隔x秒更新自身缓存 作用于client从server获取缓存(1001页面跳过缓存不会生效)
#eureka.server.response-cache-update-interval-ms= 2000

然后启动服务,访问配置的端口号,出现这个界面,就代表ok啦。
在这里插入图片描述

4.大致原理

在这里插入图片描述

spring cloud通信主要是以http请求形式进行,当服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址,然后会将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中取,完成一次调用。 当服务注册中心Eureka Server检测到服务提供者因为宕机、网络原因不可用时,则在服务注册中心将服务置为DOWN状态,并把当前服务提供者状态向订阅者发布,订阅过的服务消费者更新本地缓存。 服务提供者在启动后,周期性(默认30秒)向Eureka Server发送心跳,以证明当前服务是可用状态。Eureka Server在一定的时间(默认90秒)未收到客户端的心跳,则认为服务宕机,注销该实例。

本文转载自: 掘金

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

链表学会了没,会做这些题就足够了,思路全在动图里了,不懂都难

发表于 2021-10-19

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

1.移除链表元素 <难度系数⭐>

📝 题述:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

💨 示例1:

    输入:head = [1,2,6,3,4,5,6], val = 6

    输出:[1,2,3,4,5]

💨 示例2:

    输入:head = [ ], val = 1

    输出:[ ]

💨 示例3:

    输入:head = [7,7,7,7], val = 7

    输出:[ ]

在这里插入图片描述

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

思路1,双指针,cur找到val值所在的节点,prev记录前一个位置,依次删除,细节请看下图:

请添加图片描述

思路2,新链表,把链表中所有不是val的值尾插至新链表,删除val的节点

请添加图片描述

leetcode原题

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
c复制代码#include<stdio.h>
#include<stdlib.h>

typedef int SLTDataType;

struct ListNode
{
int val;
struct ListNode *next;
};
//思路1
struct ListNode* removeElements1(struct ListNode* head, int val)
{
struct ListNode * cur = head;
struct ListNode * prev = NULL;
while (cur)//判断地址
{
//1、相等的情况
if (cur->val == val)
{
if (cur == head)//1.1、第1个节点等于val时
{
head = head->next;
free(cur);
cur = head;
}
else//1.2、其它节点等于val时
{
prev->next = cur->next;
free(cur);
cur = prev->next;
}
}
//2、不相等的情况
else
{
prev = cur;
cur = cur->next;
}
}
return head;

}
//思路2
struct ListNode* removeElements2(struct ListNode* head, int val)
{
if(head == NULL)
return NULL;
//创建新节点
struct ListNode* newhead = NULL, *tail = NULL;
struct ListNode* cur = head;
while(cur)
{
struct ListNode* next = cur->next;
if(cur->val == val)
{
free(cur);
}
else
{
if(tail == NULL)
{
newhead = tail = cur;
}
else
{
tail->next = cur;
tail = cur;
}
}
cur = next;
}
if(tail)
tail->next = NULL;
return newhead;
}
int main()
{
struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
n1->val = 1;

struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
n2->val = 2;

struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
n3->val = 6;

struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
n4->val = 3;

struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));
n5->val = 4;

struct ListNode* n6 = (struct ListNode*)malloc(sizeof(struct ListNode));
n6->val = 5;

struct ListNode* n7 = (struct ListNode*)malloc(sizeof(struct ListNode));
n7->val = 6;

n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = n5;
n5->next = n6;
n6->next = n7;
n7->next = NULL;

removeElements1(n1, 6);
removeElements2(n1, 6);
return 0;
}

2.反转一个链表<难度系数⭐>

📝 题述:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

💨 示例1:

    输入:head = [1,2,3,4,5]

    输出:[5,4,3,2,1]

💨 示例2:

    输入:head = [1,2]

    输出:[2,1]

💨 示例3:

    输入:head = [ ]

    输出:[ ]

在这里插入图片描述

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

思路1,调整节点指针的方向
请添加图片描述

思路2,头插

请添加图片描述

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
c复制代码#include<stdio.h>
#include<stdlib.h>

typedef int SLTDataType;

struct ListNode
{
int val;
struct ListNode *next;
};
//思路1
struct ListNode* reverseList1(struct ListNode* head)
{
//空链表
if(head == NULL)
return head;
struct ListNode* n1, *n2, *n3;
n1 = NULL;
n2 = head;
n3 = head->next;

while(n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
//最后一次进来时,n3为空指针
if(n3)
n3 = n3->next;
}
return n1;
}
//思路2
struct ListNode* reverseList2(struct ListNode* head)
{
struct ListNode *newnode, *cur, *next;
newnode = NULL;
cur = head;
while(cur)
{
next = cur->next;
cur->next = newnode;
newnode = cur;
cur = next;
}
return newnode;
}
int main()
{
struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
n1->val = 1;

struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
n2->val = 2;

struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
n3->val = 3;

struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
n4->val = 4;

struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));
n5->val = 5;

n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = n5;
n5->next = NULL;

reverseList1(n1);
reverseList2(n1);
return 0;
}

3.链表的中间结点<难度系数⭐>

📝 题述:给定一个头结点为 head 的非空单链表,返回链表的中间结点。 如果有两个中间结点,则返回第二个中间结点。

在这里插入图片描述

💨 示例1:

    输入:[1,2,3,4,5]

    输出:此列表中的结点 3 (序列化形式:[3,4,5])返回的结点值为 3 。 (测评系统对该结点序列化表述是 ([3,4,5])。注意,我们返回了一个 ListNode 类型的对象 ans,这样:ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

快慢指针,快指针走2步,慢指针走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
c复制代码#include<stdio.h>
#include<stdlib.h>

typedef int SLTDataType;

struct ListNode
{
int val;
struct ListNode *next;
};
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode *slow, *fast;
slow = fast = head;
//必须同时满足奇偶的条件
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
int main()
{
struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
n1->val = 1;

struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
n2->val = 2;

struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
n3->val = 3;

struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
n4->val = 4;

struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));
n5->val = 5;

n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = n5;
n5->next = NULL;

middleNode(n1);
return 0;
}

4.链表中倒数第k个节点<难度系数⭐>

📝 题述:输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

💨 示例:

    给定一个链表: 1->2->3->4->5, 和 k = 2.

    返回链表 4->5

在这里插入图片描述

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

快慢指针,快指针先走k步,此时它与慢指针相差的就是k步,然后快慢指针同时1步1步的走,直到快指针指向NULL,此时慢指针指向的节点就是目标节点

请添加图片描述

leetcode原题

牛客网原题

⚠ 这道题我在两个平台都做了下,按照常规的写法,发现它在leetcode能过的代码,在牛客网上却过不了

请添加图片描述

所以我们这里使用更严格的写法

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
c复制代码#include<stdio.h>
#include<stdlib.h>

typedef int SLTDataType;

struct ListNode
{
int val;
struct ListNode *next;
};
struct ListNode* getKthFromEnd(struct ListNode* head, int k)
{
if(k == 0)
return NULL;
struct ListNode *slow, *fast;
slow = fast = head;
//fast先走k步
while(k--)
{
//如果k大于链表的长度
if(fast == NULL)
return NULL;
fast = fast->next;
}
//slow和fast同时走,直至fast指向空
while(fast)
{
slow = slow->next;
fast = fast->next;
}

return slow;
}
int main()
{
struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
n1->val = 1;

struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
n2->val = 2;

struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
n3->val = 3;

struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
n4->val = 4;

struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));
n5->val = 5;

n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = n5;
n5->next = NULL;

getKthFromEnd(n1, 2);
return 0;
}

5.合并两个有序链表<难度系数⭐>

📝 题述:将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

💨 示例1:

    输入:l1 = [1,2,4], l2 = [1,3,4]

    输出:[1,1,2,3,4,4]

💨 示例2:

    输入:l1 = [], l2 = []

    输出:[ ]

💨 示例3:

    输入:l1 = [ ], l2 = [0]

    输出:[0]

在这里插入图片描述

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

思路1,见下图

请添加图片描述

思路2,带哨兵位的头节点

请添加图片描述

在之前的文章中没有了解过什么是哨兵位头节点,在这里就了解下:

在这里插入图片描述

1️⃣ 哨兵位头节点,这个节点不存储有效数据;而非哨兵位头节点存储有效数据

2️⃣ 非哨兵位头节点如果要修改头指针,需要使用二级指针操作;而哨兵位头节点,则不会修改头指针,因为它不存储有效数据

3️⃣ 这种哨兵位头节点在实际中很少出现,包括哈希桶、临接表做子结构都是不带头的。其次就是OJ中给的链表都是不带头的(从上面做的这些题就可以看出),所以我们在以后编码过程中,应该尽量使用不带头的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
c复制代码#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>

typedef int SLTDataType;

struct ListNode
{
int val;
struct ListNode *next;
};
//思路1
struct ListNode* mergeTwoLists1(struct ListNode* l1, struct ListNode* l2)
{
struct ListNode *n1 = l1, *m1 = l2;
struct ListNode *newhead = NULL, *tail = NULL;
//判断空链表
if(l1 == NULL)
return l2;
if(l2 == NULL)
return l1;

while(n1 && m1)
{
if(n1->val < m1->val)
{
if(newhead == NULL)
{
newhead = tail = n1;
}
else
{
tail->next = n1;
tail = n1;
}
n1 = n1->next;
}
else
{
if(newhead == NULL)
{
newhead = tail = m1;
}
else
{
tail->next = m1;
tail = m1;
}
m1 = m1->next;
}
}
//链接上剩余的数据
if(n1)
tail->next = n1;
if(m1)
tail->next = m1;

return newhead;
}
//思路2
struct ListNode* mergeTwoLists2(struct ListNode* l1, struct ListNode* l2)
{
struct ListNode *n1 = l1, *m1 = l2;
struct ListNode *newhead = NULL, *tail = NULL;
//判断空链表
if(l1 == NULL)
return l2;
if(l2 == NULL)
return l1;
//申请空间
newhead = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
while(n1 && m1)
{
if(n1->val < m1->val)
{
tail->next = n1;
tail = n1;
n1 = n1->next;
}
else
{
tail->next = m1;
tail = m1;
m1 = m1->next;
}
}
//链接上剩余的数据
if(n1)
tail->next = n1;
if(m1)
tail->next = m1;
//这里返回的是不带哨兵位的头节点。其次malloc开辟了空间,就要主动释放,否则会内存泄漏
struct ListNode* first = newhead->next;
free(newhead);
return first;
}
int main()
{
struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
n1->val = 1;

struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
n2->val = 2;

struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
n3->val = 4;

n1->next = n2;
n2->next = n3;
n3->next = NULL;
/*----------------------------------分割------------------------------------*/
struct ListNode* m1 = (struct ListNode*)malloc(sizeof(struct ListNode));
m1->val = 1;

struct ListNode* m2 = (struct ListNode*)malloc(sizeof(struct ListNode));
m2->val = 3;

struct ListNode* m3 = (struct ListNode*)malloc(sizeof(struct ListNode));
m3->val = 4;

n1->next = n2;
n2->next = n3;
n3->next = NULL;

mergeTwoLists1(n1, m1);
mergeTwoLists2(n1, m1);

return 0;
}

请添加图片描述

本文转载自: 掘金

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

SpringBoot 整合 Thymeleaf & 如何使用

发表于 2021-10-19

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

如果你和我一样,是一名 Java 道路上的编程男孩,其实我不太建议你花时间学 Thymeleaf,当然他的思想还是值得借鉴的。但是他的本质在我看来就是 Jsp 技术的翻版(Jsp 现在用的真的很少很少)。弄前端完全可以直接上手前端框架 vue。

并竟学Java在我眼里,目前没有什么是不要学的。兼测试、运维、前端啥都要会点。另外就目前来说,学Java的人数恐怕仍然后端中最庞大的。

免费后台模板在文末,大家有需求可以直接下载。

p244489528

我想如果不是学校作业,也不会心血来潮写这篇文章👩‍💻。

阅读本文收获 📖

  1. 学会 Thymeleaf 常用语法🏄‍♀️
  2. 知晓 Thymeleaf 如何与 SpringBoot 集成🤹‍♀️
  3. 使用 Thymeleaf 完成学校老师作业 👨‍🔬
  4. 如果有需求,可以直接下个模板,结合SpringBoot 写个毕业设计👨‍💻

一、 Thymeleaf 初介绍 📓

Thymeleaf 官网

Thymeleaf 官方文档

Thymeleaf是适用于 Web 和独立环境的现代服务器端 Java 模板引擎。

Thymeleaf 的主要目标是为您的开发工作流程带来优雅的自然模板——HTML可以在浏览器中正确显示,也可以作为静态原型工作,从而加强开发团队的协作。

凭借 Spring Framework 的模块、与您最喜欢的工具的大量集成以及插入您自己的功能的能力,Thymeleaf 是现代 HTML5 JVM Web 开发的理想选择——尽管它还有更多功能。 —官方介绍

二、SpringBoot 整合 Thymeleaf 📚

主要针对我们在项目中最常见的几种用法进行讲解。同时我们也是在项目中直接讲 Thymeleaf 的用法。

2.1、新建 SpringBoot 项目

这个就不用说了哈,我想大家都是会这个的吧。

2.2、导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

2.3、SpringBoot 静态资源存放路径

首先我们将模板下载下来:

image-20211019081512286

image-20211019081735052

我们点进 doc ,将需要的页面文件复制到 resources/templates包下,将css、images、js复制到 resources/static包下。

image-20211019081820677

2.4、书写配置文件

thymeleaf 可以配置的一些属性,这只是常见的哈。

1
2
3
4
5
6
7
8
yml复制代码spring:
thymeleaf:
enabled: true #开启thymeleaf视图解析
encoding: utf-8 #编码
prefix: classpath:/templates/ #前缀 当然默认也是这个,可以不配置
cache: false #是否使用缓存
mode: HTML #严格的HTML语法模式
suffix: .html #后缀名

2.5、编写Controller

我们以 登录页面 为例,写个Controller 跳转到 login.html。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码@Controller
@RequestMapping
public class LoginController {

/** * 跳转到登录页面*/
@GetMapping("/login")
public String login(){
return "login";
}

/** * 模拟登录请求 */
@PostMapping("/doLogin")
public String doLogin(String username,String password){
if(username!=null&&password!=null){
System.out.println(username+password);
//重定向到 /indedx 请求 也可以重定向页面
return "redirect:/index";
}
return "login";
}

/** * 跳转到index 页面*/
@GetMapping("/index")
public String index(){
return "index";
}

}

2.6、启动项目&问题处理

启动类没啥要改的,直接跑。

启动项目后,访问 localhost:8080/login ,可能会出现一个 缺少css、js的页面。而不是下面这个成功的页面。

image-20211019083757427

原因是在我们使用 Thyemleaf 后,在页面中就不应该再使用相对路径,如这种: <link rel="stylesheet" type="text/css" th:href="/css/main.css">。

而是应该修改为:<link rel="stylesheet" type="text/css" th:href="@{/css/main.css}"> 。

修改完之后,还应在 html 页面的头部做一下修改:

image-20211019084131276

1
html复制代码<html lang="en" xmlns:th="http://www.thymeleaf.org">

2.7、Thyemleaf 常用

Thymeleaf 官网快速入门介绍

Thymeleaf 官方文档

2.7.1、th:href | 链接 (URL) 表达式

其实我们刚刚已经说了这点:

1
2
3
4
html复制代码//以前的
<link rel="stylesheet" type="text/css" href="/css/main.css">
//修改后的:
<link rel="stylesheet" type="text/css" th:href="@{/css/main.css}">

至于这么做的原因是由于Thymeleaf 的语法规则规定。

错误示例:

1
html复制代码<link rel="stylesheet" type="text/css" th:href="@{/static/css/main.css}">

引入的资源路径千万不要静态资源路径的集合中路径的前缀。否则会导致请求不到资源。

我们在使用 Thymeleaf 的 @{} 修饰后,它会自己去 static 包下寻找。

注意:在springboot2.0版本以前拦截器会默认对静态资源不拦截,但是springboot 2.0 以后拦截器会拦截所有,所以需要重写addInterceptors方法,不管是自己的静态资源还是webjars中的资源,都要放行

当然我只是在这提上一嘴,本文没写拦截器相关知识。

2.7.2、th:text

我们都会有一个需要提示信息的时候,就是简单展示一段文本,如:

1
html复制代码<p th:text="#{home.welcome}">Welcome to our grocery store!</p>

我们修改一下之前的Controller:

1
2
3
4
5
6
java复制代码/*** 跳转到登录页面  */
@GetMapping("/login")
public String login(Model model){
model.addAttribute("systemName","学生管理系统");
return "login";
}

另外修改一下登录页面:

1
2
3
html复制代码<div class="logo">
<h1 th:text="${systemName}"></h1>
</div>

image-20211019090641923

2.7.3、th:action

表单提交我想是最常见的啦吧。

1
2
html复制代码<form class="login-form" method="post" th:action="@{/doLogin}">
</form>

在这里提交的路径,也是需要用 @{} 包裹起来。

后端还是照写,没有变化。

2.7.4、th:each & th:if

循环、判断应该是没有哪里用不到的啦吧。

写个Student 类,稍后会用到

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private Long id;
private String name;
private Integer age;
/**
* true 为男
* false 为女
*/
private Boolean gender;
}

写个controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* 添加多个学生
*/
@GetMapping("/doEach")
public String doEach(Model model){
List<Student> students = new ArrayList<>();
Student student1 = new Student(1L,"1号学生",20,true);
students.add(student1);
Student student2 = new Student(2L,"2号学生",21,true);
students.add(student2);
Student student3 = new Student(3L,"3号学生",22,false);
students.add(student3);
Student student4 = new Student(4L,"4号学生",23,true);
students.add(student4);
model.addAttribute("students",students);
return "each";
}

再写个 each.html 页面

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
html复制代码<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>for循环</title>
</head>
<body>

<table border="1">
<tr>
<th>id</th>
<th>姓名</th>
<th>年龄</th>
<th>性别</th>
</tr>
<tr th:each="student : ${students}" >
<td th:text="${student.id}"></td>
<td th:text="${student.name}"></td>
<td th:text="${student.age}"></td>
<!-- 三元表达式 -->
<td th:text="${student.gender}?男:女"></td>
<td th:if="${student.gender}">利用if判断性别 男</td>
<td th:if="${not student.gender}">利用if判断性别 女</td>
</tr>
</table>
</body>
</html>

成果:

image-20211019092923128

2.8、小结

我只是简单的说了一下 Thymeleaf,它支持的东西还是有不少的,在这没有一一说明,有需求时,可直接查询 Thymeleaf 文档即可。

三、免费后台模板 📋

1、免费的后台模板:Vail Admin

2、聚集多个免费的后台模板:免费模板

点进去直接下载就可以啦。在SpringBoot 项目中直接引用就可以啦。

image-20211018214021827

四、自言自语 🚀

你好,我是博主宁在春:主页

希望本篇文章能让你感到有所收获!!!

祝 我们:待别日相见时,都已有所成。

欢迎大家一起讨论问题😁,躺了🛌

image-20211014091239193

走过路过,不要错过,在文章下评论有一定几率获得来掘金酱的礼品哦。

评论越走心越有可能拿奖哦!!!

详情👉掘力星计划

本文转载自: 掘金

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

有个码龄 10 年的程序员跟我说:“他编程从来不用鼠标”,我

发表于 2021-10-19

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

呸,我也会~

如何成为编程大佬?

那当然是编码的时候手不离键盘,疯狂敲击。

毕竟每一次右手离开键盘去触碰鼠标,都是一次浪费时间的操作。

成为大佬的第一步,熟记键盘快捷键与常用命令。

假设你现在有一块下图所示 标准键盘。

2021 年程序员必备 Windows 快捷键大大大全

快捷键核心以 Windows7 操作系统为准,为啥?因为写博客的时候正在用 Windows 7 。

快捷键只介绍能让你成为开发大佬的,类似 Ctrl+C、Ctrl+V 这种大家熟知的,一概省略,咱们只来干货。

程序员必备 Windows 快捷操作大全

程序员如何打开一个程序

首先认识一下 Windows 键,一会能用到
与 Windows 键相关,一般我们叫做 Winkey 或 Win键 ,位置大概在计算机键盘左下角 Ctrl 和 Alt 键之间,上面有一个微软的窗口 LOGO,并且键盘上只有 一个。

Win 键单独按下,再选择

唤醒开始菜单,然后按方向键即可对菜单进行选择。只要操作的够快,别人的眼睛根本追不上你的操作。

Win 键+R

打开 运行,然后可输入命令,打开指定软件,程序员常用的有

  • calc:计算器;
  • osk:屏幕虚拟键盘(说好了不用键盘……);
  • write:写字板;
  • notepad:记事本;
  • mspaint:画图;
  • cmd:控制台;
  • control:控制面板(配合 Tab 键,好用的不行);
  • desk.cpl:打开控制面板中的桌面设置;
  • main.cpl:鼠标设置(同上);
  • inetcpl.cpl:Internet 属性;
  • ncpa.cpl:网络连接;
  • mmsys.cpl:声音和音频设备;
  • sysdm.cpl:系统属性;
  • firewall.cpl:防火墙;
  • compmgmt.msc:计算机管理;
  • devmgmt.msc:设备管理;
  • diskmgmt.msc:磁盘管理;
  • services.msc:本地服务;
  • eventvwr:事件查看器;
  • taskmgr:任务管理器,使用 Esc+Shift+Ctrl 也可以;
  • certmgr.msc:证书管理器,部分开发人员常用;
  • secpol.msc:本地安全策略;
  • regedit:注册表编辑器;
  • gpedit.msc:本地组策略;
  • .:打开我的文档(快去试试吧);
  • \:打开 C 盘;
  • %temp%:打开临时文件夹(作为一个程序员,经常用到)
  • mstsc:桌面远程连接(必知必会);
  • cleanmgr:磁盘清理;
  • winver:Windows 版本查看;
  • mmc:控制台;
  • nslookup:IP 地址检测器;

上面这些如果还不够用,不能让你在电脑上运指如飞,你还可以进行自定义。

将你希望通过运行命令打开的程序,创建一个快捷方式,

例如下图的开发者工具,然后给快捷方式起一个缩写的名字(自己要记住),

接下来把这个快捷方式复制到 C:/Windows 目录中,就可以使用 Windows+R 输入刚才的快捷方式名称缩写进行快速访问。
2021 年程序员必备 Windows 快捷键大大大全

本步骤也可通过设置环境变量实现,需求大的时,可以学习一下进行设置。

如果还觉得不够省力,可以在任意快捷方式上右键,点击属性。

出现下图所示界面,手动设置一个快捷键,例如下图给 VS 设置的 Ctrl +Alt+D,

每天上班打开电脑,按下快捷键,打开 VS,接下来一天都不用去碰鼠标了。

2021 年程序员必备 Windows 快捷键大大大全

Shift + Win 键 + 数字
如果你已经打开了一个 VSCODE,并且该程序在任务栏是第 1 号位置,那按下 Shift + Win键 + 数字,启动锁定到任务栏中,由该数字所表示位置处的程序的新实例,也就是打开了一个新的 VSCODE。

Ctrl + Win 键 + 数字
有新建就有切换,将上述命令的 Shift 切换为 Ctrl ,表示切换到锁定到任务栏中,由该数字所表示位置处的程序的最后一个活动窗口。

你如果在把 ctrl 修改为 Alt 呢?

快速关闭和切换程序

作为一个程序员,摸鱼时需要快速关闭某窗口,或者迅速切换程序,用到的命令如下。

Win 键+ 空格
透明化所有窗口,快速查看桌面,缺点是你要一直按着。

Win 键+ D
最小化所有窗口,快速查看桌面,再按一次可以复原。

Win 键+ M
最小化所有窗口,Win 键 + Shift + M 将最小化的窗口还原到桌面。

Win 键+ Home
隐藏除当前窗口外的其它窗口,再按一下就回来了。

Win 键+方向键
用过都知道,窗口动来动去的效果。

Win 键+Tab 键
花里胡哨的效果。

Win 键+T
在任务栏进行窗口切换。

Win 键+1,2,3,4…
打开任务栏中固定的程序,1 代表任务栏中第一个应用图标,如果没有,就对任务栏内容按照组进行切换。

Win + B
这个操作当你不用鼠标的时候,肯定会用到的,选中托盘区域的收起按钮,然后回车,之后方向键就可以控制操作了。

2021 年程序员必备 Windows 快捷键大大大全

Alt+F4
关闭活动项目或者退出活动程序,瞬间退出。

其它一些常用的快捷键

Win 键+P
多屏幕的时候,切换屏幕使用,去开会的时候,你会用到。

Win 键+L
锁屏,中午去吃饭的时候,你会用到。

Win 键+E
打开计算机/资源管理器。

Win+Break
非常好用,但是找 Break 键的位置,就要靠你自己了,功能是显示“系统属性”对话框。

Win+ +
唤起放大镜,放大,同理,+ -,缩小。

Shift+F10
打开右键菜单,当然键盘上在空格右侧的 Ctrl 与 Alt 中间也存在一个右键菜单的按键,直接点击即可。

Delete
删除被选择的选择项目,删除的文件将被放入回收站,Shift+Delete 永久删除。

F1 ~ F12

  • F1:帮助手册,可以看看,有时候还挺有用的,任何软件都可以试试。
  • F2:重命名选定项目
  • F3:搜索文件或文件夹

其余命令在不同的软件中,呈现效果不同。

Alt,Shift ,Ctrl 相关快捷键

键盘上的 Shift,Alt,Ctrl 按键是一个比较有趣的按键,因为它们是对称的。

有些快捷键组合如果只使用左手非常不方便,但是加上你的右手,这时候就便捷多了。

Alt +

Alt+空格+C
猛然间关闭了当前打开的窗口,只使用左手非常不方便,但是按下右 Alt 键,就舒服多了。

Alt+空格+N
最小化当前窗口。

Alt+空格+R
窗口恢复到最大化之前的状态。

Alt+Tab
窗口切换。

Alt+回车
查看所选文件属性。

在软件中按下 Alt
激活菜单快捷方式,例如下图出现下划线。

2021 年程序员必备 Windows 快捷键大大大全

Shift ,Ctrl 更多的是在软件中的快捷操作组合,操作电脑可以暂且忽略。

输入法切换与截图

切换输入法大家都知道,按键盘上的 Ctrl+Shift,不行就按 Ctrl+空格,但是很少有人去了解这二者的区别。

  • Ctrl+Shift :输入法间的逐一切换,当多个输入法存在时;
  • Ctrl+空格:输入法与非输入法切换;
  • Shift+空格:全角和半角间的切换。

这些其实 Windows 已经有地方给我们说明了,打开输入法设置即可查阅。

2021 年程序员必备 Windows 快捷键大大大全

键盘上的 Prtsc SysRq 是快捷截取屏幕,当然该按键也有称作 PRINT SCREEN

这个按键神奇的地方是当你有扩展屏的时候,扩展屏也能截取。


可能到我手离开鼠标的那一刻,我就成为了程序员大佬了吧。

本文转载自: 掘金

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

第四十九章 SQL命令 GROUP BY 第四十九章 SQL

发表于 2021-10-19

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

第四十九章 SQL命令 GROUP BY

SELECT子句,它根据一个或多个列对查询的结果行进行分组。

大纲

1
2
sql复制代码SELECT ...
GROUP BY field {,field2}

参数

  • field - 从其中检索数据的一个或多个字段。
    单个字段名或以逗号分隔的字段名列表。

描述

GROUP BY是SELECT命令的一个子句。
可选的GROUP BY子句出现在FROM子句和可选的WHERE子句之后,可选的HAVING和ORDER BY子句之前。

GROUP BY子句接受查询的结果行,并根据一个或多个数据库列将它们分成单独的组。
当将SELECT与GROUP BY结合使用时,将为GROUP BY字段的每个不同值检索一行。
GROUP BY将NULL(没有指定值)字段作为一个独立的值组。

GROUP BY子句在概念上类似于 IRIS聚合函数扩展关键字%FOREACH,但是GROUP BY操作整个查询,而%FOREACH允许在子填充上选择聚合,而不限制整个查询填充。

GROUP BY可以在INSERT命令的SELECT子句中使用。
不能在UPDATE或DELETE命令中使用GROUP BY。

指定字段

GROUP BY子句最简单的形式指定单个字段,如GROUP BY City。
这将为每个惟一的City值选择任意一行。
还可以指定以逗号分隔的字段列表,将其组合值视为单个分组术语。
它为每个City和Age值的唯一组合选择任意一行。
因此,GROUP BY City,Age返回与GROUP BY Age,City相同的结果。

字段必须通过列名指定。
有效的字段值包括以下内容:列名(GROUP BY City);
%ID(返回所有行);
指定列名的标量函数(GROUP BY ROUND(Age,-1));
指定列名的排序规则函数(GROUP BY %EXACT(City))。

不能通过列别名指定字段;
尝试这样做会产生SQLCODE -29错误。
不能通过列号指定字段;
这被解释为一个文字并返回一行。
不能指定聚合字段;
尝试这样做将生成SQLCODE -19错误。
不能指定子查询;
这被解释为一个文字并返回一行。

GROUP BY StreamField操作流字段的OID,而不是它的实际数据。
因为所有流字段oid都是唯一的值,GROUP BY对实际的流字段重复数据值没有影响。
GROUP BY StreamField将流字段为NULL的记录数量减少为一条记录。

GROUP BY子句可以使用箭头语法(- >)操作符在非基表的表中指定字段。
例如:GROUP BY Company->Name。

在GROUP BY子句中指定一个字面值作为字段值返回1行;
返回哪一行是不确定的。
因此,指定7、'Chicago'、''、0或NULL都返回1行。
但是,如果在逗号分隔的列表中指定一个字面值作为字段值,则该字面值将被忽略,并且GROUP BY将为指定字段名的每个惟一组合选择任意一行。

具有GROUP BY和DISTINCT BY的聚合函数

在计算聚合函数之前应用GROUP BY子句。
在下面的示例中,COUNT聚合函数计算每个GROUP BY组中的行数:

1
2
3
sql复制代码SELECT Home_State,COUNT(Home_State)
FROM Sample.Person
GROUP BY Home_State

image.png

在计算聚合函数之后应用DISTINCT BY子句。
在下面的例子中,COUNT聚合函数计算整个表中的行数:

1
2
sql复制代码SELECT DISTINCT BY(Home_State) Home_State,COUNT(Home_State)
FROM Sample.Person

为了计算整个表的聚合函数,而不是GROUP BY组,可以指定一个选择项子查询:

1
2
3
sql复制代码SELECT Home_State,(SELECT COUNT(Home_State) FROM Sample.Person)
FROM Sample.Person
GROUP BY Home_State

当选择列表由聚合字段组成时,不应将GROUP BY子句与DISTINCT子句一起使用。
例如,下面的查询旨在返回共享相同Home_State的不同数量的人:

1
2
3
4
5
6
sql复制代码/* 此查询不应用DISTINCT关键字 */
/* 这里提供了一个警示的例子 */
SELECT DISTINCT COUNT(*) AS mynum
FROM Sample.Person
GROUP BY Home_State
ORDER BY mynum

这个查询没有返回预期的结果,因为它没有应用DISTINCT关键字。
要同时应用DISTINCT聚合和GROUP BY子句,请使用子查询,如下例所示:

1
2
3
4
5
sql复制代码SELECT DISTINCT *
FROM (SELECT COUNT(*) AS mynum
FROM Sample.Person
GROUP BY Home_State) AS Sub
ORDER BY Sub.mynum

此示例成功返回共享相同Home_State的不同人数。
例如,如果任何Home_State被8个人共享,查询返回8。

如果查询仅由聚合函数组成且不返回表中的任何数据,则返回%ROWCOUNT=1,并为聚合函数返回一个空字符串(或0)值。
例如:

1
sql复制代码SELECT AVG(Age) FROM Sample.Person WHERE Name %STARTSWITH 'ZZZZ'

但是,如果这种类型的查询包含GROUP BY子句,它将返回%ROWCOUNT=0,并且聚合函数值仍未定义。

飘絮,字母大小写和优化

本节描述GROUP BY如何处理只有字母大小写不同的数据值。

  • 组合字母变体在一起(返回大写字母):

默认情况下,GROUP By根据创建字段时为其指定的排序规则将字符串值分组。
IRIS有一个默认的字符串排序规则,可以为每个名称空间设置;
所有名称空间的初始字符串排序规则默认值是SQLUPPER。
因此,除非另有说明,通常GROUP BY排序规则不区分大小写。

GROUP BY根据字段的大写字母排序规则,使用SQLUPPER排序规则对字段的值进行分组。
只有字母大小写不同的字段值被分组在一起。
分组字段值全部以大写字母返回。
这样做的性能优势在于允许GROUP BY为字段使用索引,而不是访问实际的字段值。
因此,只有在一个或多个选定字段的索引存在时才有意义。
它的结果是group by字段值全部以大写字母返回,即使实际数据值中没有一个都是大写字母。

  • 组合字母大小写变体在一起(返回实际的字母大小写):

GROUP BY可以将字母大小写不同的值分组在一起,并使用实际的字段字母大小写值返回分组的字段值(随机选择)。
这样做的好处是返回的值是实际值,显示数据中至少一个值的字母大小写。
它的性能缺点是不能使用字段的索引。
可以通过对select-item字段应用%EXACT排序函数来为单个查询指定这个值。

  • 不要将不同的字母组合在一起(返回实际的字母):

通过对GROUP BY字段应用%EXACT排序功能,GROUP BY可以对值进行区分大小写的分组。
这样做的好处是将每个字母变体作为一个单独的组返回。
它的性能缺点是不能使用字段的索引。

可以使用管理门户在系统范围内为包含GROUP BY子句的所有查询配置此行为。依次选择系统管理、配置、SQL和对象设置、SQL。查看和编辑GROUP BY和DISTINCT查询必须生成原始值复选框。默认情况下,此复选框未选中。此默认设置按字母值的大写排序规则对字母值进行分组。(此优化也适用于DISTINCT子句。)。

也可以使用$SYSTEM.SQL.Util.SetOption()方法快速区分选项在系统范围内设置此选项。要确定当前设置,请调用$SYSTEM.SQL.CurrentSettings(),它显示打开的不同优化设置;默认值为1。

此优化利用选定字段的索引。因此,只有在一个或多个选定字段存在索引时才有意义。它对存储在索引中的字段值进行排序;字母字符串以全部大写字母返回。可以设置此系统范围的选项,然后使用%exact排序规则函数为特定查询覆盖它以保留字母大小写。

以下示例显示了这些行为。这些示例假定Sample.Person包含具有Home_City字段的记录,该字段具有SQLUPPER排序规则,值为‘New York’和‘New York’:

1
2
sql复制代码SELECT Home_City FROM Sample.Person GROUP BY Home_City
/* 将Home_City值按其大写字母值组合在一起将以大写字母返回每个分组城市的名称。因此,返回‘NEW YORK’。

image.png

1
2
sql复制代码SELECT %EXACT(Home_City) FROM Sample.Person GROUP BY Home_City
/*将Home_City值按其大写字母值组合在一起将返回以原始字母大小写表示的分组城市的名称。因此,可以返回‘New York’或‘new York’,但不能同时返回两者。*/

image.png

1
2
sql复制代码SELECT Home_City FROM Sample.Person GROUP BY %EXACT(Home_City)
/*将Home_City值按其原始字母大小写组合在一起将返回每个分组的城市的名称(原始字母大小写)。因此,‘New York’和‘New York’都作为单独的组返回。*/

image.png

%ROWID

指定GROUP BY子句会导致基于游标的嵌入式SQL查询不设置%ROWID变量。即使GROUP BY不限制返回的行数,也不设置%ROWID。下面的示例显示了这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码ClassMethod GroupBy()
{
s %ROWID=999
&sql(
DECLARE EmpCursor11 CURSOR FOR
SELECT Name, Home_State
INTO :name,:state FROM Sample.Person
WHERE Home_State %STARTSWITH 'M'
GROUP BY Home_State
)
&sql(
OPEN EmpCursor11
)
q:(SQLCODE'=0)
FOR {
&sql(FETCH EmpCursor11)
q:SQLCODE
w !,"RowID: ",%ROWID," row count: ",%ROWCOUNT
w " Name=",name," State=",state
}
&sql(CLOSE EmpCursor11)
}
1
2
3
4
5
6
7
8
9
10
java复制代码DHC-APP>d ##class(PHA.TEST.SQLCommand).GroupBy()

RowID: 999 row count: 1 Name=O'Rielly,Chris H. State=MS
RowID: 999 row count: 2 Name=Orwell,John V. State=MT
RowID: 999 row count: 3 Name=Zevon,Heloisa O. State=MI
RowID: 999 row count: 4 Name=Kratzmann,Emily Z. State=MO
RowID: 999 row count: 5 Name=Hanson,George C. State=MD
RowID: 999 row count: 6 Name=Zucherro,Olga H. State=MN
RowID: 999 row count: 7 Name=Gallant,Thelma Q. State=MA
RowID: 999 row count: 8 Name=Xiang,Kirsten U. State=ME

查询行为的这种更改仅适用于基于游标的嵌入式SQL SELECT查询。动态SQL SELECT查询和非游标嵌入式SQL SELECT查询从未设置%ROWID。

事务提交的更改

包含GROUP BY子句的查询不支持READ COMMITTED隔离级别。在定义为READ COMMITTED的事务中,不带GROUP BY子句的SELECT语句仅返回已提交的数据修改;换句话说,它返回当前事务之前的数据状态。带有GROUP BY子句的SELECT语句返回所做的所有数据修改,无论它们是否已提交。

示例

下面的示例按名称的首字母对名称进行分组。它返回首字母、共享该首字母的姓名计数以及一个Name值的示例。名称使用其SQLUPPER排序规则进行分组,而不考虑实际值的字母大小写。请注意,名称SELECT-ITEM包含大写首字母;%Exact排序规则用于显示实际的Name值:

1
2
sql复制代码SELECT Name AS Initial,COUNT(Name) AS SameInitial,%EXACT(Name) AS Example
FROM Sample.Person GROUP BY %SQLUPPER(Name,2)

image.png

本文转载自: 掘金

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

OPC UA笔记(2):历史、组成与应用

发表于 2021-10-19

经典 OPC 与 OPC UA

工业设备通信分为以下几个层次:

OPC UA(2)_ Stage1.png

OPC (Object Linking and Embedding(OLE) for Process Control)

经典 OPC 基于 Windows COM/DCOM 技术,支持应用程序和现场过程控制应用交互。

  • 优点:解决了不同种类、协议的工业设备之间通信,应用与设备交互的问题
  • 缺点:无法满足以下需求:
需求点 经典 OPC
跨平台 经典OPC发展之初基于Windows技术栈,无法做到跨平台支持
标准化 经典OPC的数据管理还是基于原始数据,需要在应用层做数据模型,标准化是个问题
安全性 经典OPC在后来的发展中,加入了安全机制,从设计层面看不是很理想,没有从协议层提供统一的安全机制
开源 厂家主导的通信协议

OPC UA

OPC UA 是一套统一架构,应用在自动化技术的机器对机器传输协议。

OPC UA(1)_ Concepts.png

需求点 OPC UA
跨平台 支持不同的操作系统与编程语言
标准化 抽象层次更高
安全性 从框架层次提供安全机制,支持各种安全技术
开源 标准可以免费取得,设备使用无限制,没有其他限制

OPC UA 核心内容

  • 安全 Security
  • 数据建模 Modeling
  • 通信:根据不同场景,支持二进制、XML、JSON数据编解码
  • 服务:定义了数十种服务(标准)

OPC UA(2)_ Modeling.png

通过节点规范,可以像UML一样为实际的设备数据给出定义。

实现 OPC UA Server & Client

在 GitHub 上提供了很多开源的 OPC UA 框架实现:

比如 github.com/OPCFoundati…

Snipaste_2021-10-19_07-56-20.png

下面描述一个典型的开发过程:

OPC UA(2)_ Develop Prcess.png

通过开源 OPC UA 技术栈可以方便地建立模型,开发,实现通信。

本文转载自: 掘金

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

小心陷入MySQL索引的坑

发表于 2021-10-19

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

索引可以说是数据库中的一个大心脏了,如果说一个数据库少了索引,那么数据库本身存在的意义就不大了,和普通的文件没什么两样。所以说一个好的索引对数据库系统尤其重要,今天来说说MySQL索引,从细节和实际业务的角度看看在MySQL中B+树索引好处,以及我们在使用索引时需要注意的知识点。

合理利用索引

在工作中,我们可能判断数据表中的一个字段是不是需要加索引的最直接办法就是:这个字段会不会经常出现在我们的where条件中。从宏观的角度来说,这样思考没有问题,但是从长远的角度来看,有时可能需要更细致的思考,比如我们是不是不仅仅需要在这个字段上建立一个索引?多个字段的联合索引是不是更好?以一张用户表为例,用户表中的字段可能会有用户的姓名、用户的身份证号、用户的家庭地址等等。

「1.普通索引的弊端」

现在有个需求需要根据用户的身份证号找到用户的姓名,这时候很显然想到的第一个办法就是在id_card上建立一个索引,严格来说是唯一索引,因为身份证号肯定是唯一的,那么当我们执行以下查询的时候:

1
sql复制代码SELECT name FROM user WHERE id_card=xxx

它的流程应该是这样的:

  1. 先在id_card索引树上搜索,找到id_card对应的主键id
  2. 通过id去主键索引上搜索,找到对应的name

从效果上来看,结果是没问题的,但是从效率上来看,似乎这个查询有点昂贵,因为它检索了两颗B+树,假设一颗树的高度是3,那么两颗树的高度就是6,因为根节点在内存里(此处两个根节点),所以最终要在磁盘上进行IO的次数是4次,以一次磁盘随机IO的时间平均耗时是10ms来说,那么最终就需要40ms。这个数字一般,不算快。

「2.主键索引的陷阱」

既然问题是回表,造成了在两颗树都检索了,那么核心问题就是看看能不能只在一颗树上检索。这里从业务的角度你可能发现了一个切入点,身份证号是唯一的,那么我们的主键是不是可以不用默认的自增id了,我们把主键设置成我们的身份证号,这样整个表的只需要一个索引,并且通过身份证号可以查到所有需要的数据包括我们的姓名,简单一想似乎有道理,只要每次插入数据的时候,指定id是身份证号就行了,但是仔细一想似乎有问题。

这里要从B+树的特点来说,B+树的数据都存在叶子节点上,并数据是页式管理的,一页是16K,这是什么意思呢?哪怕我们现在是一行数据,它也要占用16K的数据页,只有当我们的数据页写满了之后才会写到一个新的数据页上,新的数据页和老的数据页在物理上不一定是连续的,而且有一点很关键,虽然数据页物理上是不连续的,但是数据在逻辑上是连续的。


也许你会好奇,这和我们说的身份证号当主键ID有什么关系?这时你应该关注连续这个关键字,身份证号不是连续的,这意味着什么?当我们插入一条不连续的数据的时候,为了保持连续,需要移动数据,比如原来在一页上的数据有1->5,这时候插入了一条3,那么就需要把5移到3后面,也许你会说这也没多少开销,但是如果当新的数据3造成这个页A满了,那么就要看它后面的页B是否有空间,如果有空间,这时候页B的开始数据应该是这个从页A溢出来的那条,对应的也要移动数据。如果此时页B也没有足够的空间,那么就要申请新的页C,然后移一部分数据到这个新页C上,并且会切断页A与页B之间的关系,在两者之间插入一个页C,从代码的层面来说,就是切换链表的指针。

总结来说,不连续的身份证号当主键可能会造成页数据的移动、随机IO、频繁申请新页相关的开销。如果我们用的是自增的主键,那么对于id来说一定是顺序的,不会因为随机IO造成数据移动的问题,在插入方面开销一定是相对较小的。

其实不推荐用身份证号当主键的还有另外一个原因:身份证号作为数字来说太大了,得用bigint来存,正常来说一个学校的学生用int已经足够了,我们知道一页可以存放16K,当一个索引本身占用的空间越大时,会导致一页能存放的数据越少,所以在一定数据量的情况下,使用bigint要比int需要更多的页也就是更多的存储空间。

「3.联合索引的矛与盾」

由上面两条结论可以得出:

  1. 尽量不要去回表
  2. 身份证号不适合当主键索引

所以自然而然地想到了联合索引,创建一个【身份证号+姓名】的联合索引,注意联合索引的顺序,要符合最左原则。这样当我们同样执行以下sql时:

1
sql复制代码select name from user where id_card=xxx

不需要回表就可以得到我们需要的name字段,然而还是没有解决身份证号本身占用空间过大的问题,这是业务数据本身的问题,如果你要解决它的话,我们可以通过一些转换算法将原本大的数据转换成小的数据,比如crc32:

1
csharp复制代码crc32.ChecksumIEEE([]byte("341124199408203232"))

可以将原本需要8个字节存储空间的身份证号用4个字节的crc码替代,因此我们的数据库需要再加个字段crc_id_card,联合索引也从【身份证号+姓名】变成了【crc32(身份证号)+姓名】,联合索引占的空间变小了。但是这种转换也是有代价的:

  1. 每次额外的crc,导致需要更多cpu资源
  2. 额外的字段,虽然让索引的空间变小了,但是本身也要占用空间
  3. crc会存在冲突的概率,这需要我们查询出来数据后,再根据id_card过滤一下,过滤的成本根据重复数据的数量而定,重复越多,过滤越慢。

关于联合索引存储优化,这里有个小细节,假设现在有两个字段A和B,分别占用8个字节和20个字节,我们在联合索引已经是[A,B]的情况下,还要支持B的单独查询,因此自然而然我们在B上也建立个索引,那么两个索引占用的空间为 8+20+20=48,现在无论我们通过A还是通过B查询都可以用到索引,如果在业务允许的条件下,我们是否可以建立[B,A]和A索引,这样的话,不仅满足单独通过A或者B查询数据用到索引,还可以占用更小的空间:20+8+8=36。

「4.前缀索引的短小精悍」

有时候我们需要索引的字段是字符串类型的,并且这个字符串很长,我们希望这个字段加上索引,但是我们又不希望这个索引占用太多的空间,这时可以考虑建立个前缀索引,以这个字段的前一部分字符建立个索引,这样既可以享受索引,又可以节省空间,这里需要注意的是在前缀重复度较高的情况下,前缀索引和普通索引的速度应该是有差距的。

1
2
csharp复制代码alter table xx add index(name(7));#name前7个字符建立索引
select xx from xx where name="JamesBond"

「5.唯一索引的快与慢」

在说唯一索引之前,我们先了解下普通索引的特点,我们知道对于B+树而言,叶子节点的数据是有序的。

假设现在我们要查询2这条数据,那么在通过索引树找到2的时候,存储引擎并没有停止搜索,因为可能存在多个2,这表现为存储引擎会在叶子节点上接着向后查找,在找到第二个2之后,就停止了吗?答案是否,因为存储引擎并不知道后面还有没有更多的2,所以得接着向后查找,直至找到第一个不是2的数据,也就是3,找到3之后,停止检索,这就是普通索引的检索过程。

唯一索引就不一样了,因为唯一性,不可能存在重复的数据,所以在检索到我们的目标数据之后直接返回,不会像普通索引那样还要向后多查找一次,从这个角度来看,唯一索引是要比普通索引快的,但是当普通索引的数据都在一个页内的话,其实也并不会快多少。在数据的插入方面,唯一索引可能就稍逊色,因为唯一性,每次插入的时候,都需要将判断要插入的数据是否已经存在,而普通索引不需要这个逻辑,并且很重要的一点是唯一索引会用不到change buffer(见下文)。

「6.不要盲目加索引」

在工作中,你可能会遇到这样的情况:这个字段我需不需要加索引?。对于这个问题,我们常用的判断手段就是:查询会不会用到这个字段,如果这个字段经常在查询的条件中,我们可能会考虑加个索引。但是如果只根据这个条件判断,你可能会加了一个错误的索引。我们来看个例子:假设有张用户表,大概有100w的数据,用户表中有个性别字段表示男女,男女差不多各占一半,现在我们要统计所有男生的信息,然后我们给性别字段加了索引,并且我们这样写下了sql:

1
sql复制代码select * from user where sex="男"

如果不出意外的话,InnoDB是不会选择性别这个索引的。如果走性别索引,那么一定是需要回表的,在数据量很大的情况下,回表会造成什么样的后果?我贴一张和上面一样的图想必大家都知道了:


主要就是大量的IO,一条数据需要4次,那么50w的数据呢?结果可想而知。因此针对这种情况,MySQL的优化器大概率走全表扫描,直接扫描主键索引,因为这样性能可能会更高。

「7.索引失效那些事」

某些情况下,因为我们自己使用的不当,导致mysql用不到索引,这一般很容易发生在类型转换方面,也许你会说,mysql不是已经支持隐式转换了吗?比如现在有个整型的user_id索引字段,我们因为查询的时候没注意,写成了:

1
sql复制代码select xx from user where user_id="1234"

注意这里是字符的1234,当发生这种情况下,MySQL确实足够聪明,会把字符的1234转成数字的1234,然后愉快的使用了user_id索引。
但是如果我们有个字符型的user_id索引字段,还是因为我们查询的时候没注意,写成了:

1
sql复制代码select xx from user where user_id=1234

这时候就有问题了,会用不到索引,也许你会问,这时MySQL为什么不会转换了,把数字的1234转成字符型的1234不就行了? 这里需要解释下转换的规则了,当出现字符串和数字比较的时候,要记住:MySQL会把字符串转换成数字。也许你又会问:为什么把字符型user_id字段转换成数字就用不到索引了? 这又要说到B+树索引的结构了,我们知道B+树的索引是按照索引的值来分叉和排序的,当我们把索引字段发生类型转换时会发生值的变化,比如原来是A值,如果执行整型转换可能会对应一个B值(int(A)=B),这时这颗索引树就不能用了,因为索引树是按照A来构造的,不是B,所以会用不到索引。

索引优化

「1.change buffer」

我们知道在更新一条数据的时候,要先判断这条数据的页是否在内存里,如果在的话,直接更新对应的内存页,如果不在的话,只能去磁盘把对应的数据页读到内存中来,然后再更新,这会有什么问题呢?

  1. 去磁盘的读这个动作稍显的有点慢
  2. 如果同时更新很多数据,那么即有可能发生很多离散的IO

为了解决这种情况下的速度问题,change buffer出现了,首先不要被buffer这个单词误导,change buffer除了会在公共的buffer pool里之外,也是会持久化到磁盘的。当有了change buffer之后,我们更新的过程中,如果发现对应的数据页不在内存里的话,也不去磁盘读取相应的数据页了,而是把要更新的数据放入到change buffer中,那change buffer的数据何时被同步到磁盘上去?如果此时发生读动作怎么办?首先后台有个线程会定期把change buffer的数据同步到磁盘上去的,如果线程还没来得及同步,但是又发生了读操作,那么也会触发把change buffer的数据merge到磁盘的事件。

需要注意的是并不是所有的索引都能用到changer buffer,像主键索引和唯一索引就用不到,因为唯一性,所以它们在更新的时候要判断数据存不存在,如果数据页不在内存中,就必须去磁盘上把对应的数据页读到内存里,而普通索引就没关系了,不需要校验唯一性。change buffer越大,理论收益就越大,这是因为首先离散的读IO变少了,其次当一个数据页上发生多次变更,只需merge一次到磁盘上。当然并不是所有的场景都适合changer buffer,如果你的业务是更新之后,需要立马去读,changer buffer会适得其反,因为需要不停地触发merge动作,导致随机IO的次数不会变少,反而增加了维护changer buffer的开销。

「2.索引下推」

前面我们说了联合索引,联合索引要满足最左原则,即在联合索引是[A,B]的情况下,我们可以通过以下的sql用到索引:

1
2
sql复制代码select * from table where A="xx"
select * from table where A="xx" AND B="xx"

其实联合索引也可以使用最左前缀的原则,即:

1
sql复制代码select * from table where A like "赵%" AND B="上海市"

但是这里需要注意的是,因为使用了A的一部分,在MySQL5.6之前,上面的sql在检索出所有A是“赵”开头的数据之后,就立马回表(使用的select *),然后再对比B是不是“上海市”这个判断,这里是不是有点懵?为什么B这个判断不直接在联合索引上判断,这样的话回表的次数不就少了吗?造成这个问题的原因还是因为使用了最左前缀的问题,导致索引虽然能使用部分A,但是完全用不到B,看起来是有点“傻”,于是在MySQL5.6之后,就出现了索引下推这个优化(Index Condition Pushdown),有了这个功能以后,虽然使用的是最左前缀,但是也可以在联合索引上搜索出符合A%的同时也过滤非B的数据,大大减少了回表的次数。

「3.刷新邻接页」

在说刷新邻接页之前,我们先说下脏页,我们知道在更新一条数据的时候,得先判断这条数据所在的页是否在内存中,如果不在的话,需要把这个数据页先读到内存中,然后再更新内存中的数据,这时会发现内存中的页有最新的数据,但是磁盘上的页却依然是老数据,那么此时这条数据所在的内存中的页就是脏页,需要刷到磁盘上来保持一致。所以问题来了,何时刷?每次刷多少脏页才合适?如果每次变更就刷,那么性能会很差,如果很久才刷,脏页就会堆积很多,造成内存池中可用的页变少,进而影响正常的功能。所以刷的速度不能太快但要及时,MySQL有个清理线程会定期执行,保证了不会太快,当脏页太多或者redo log已经快满了,也会立刻触发刷盘,保证了及时。

在脏页刷盘的过程中,InnoDB这里有个优化:如果要刷的脏页的邻居页也脏了,那么就顺带一起刷,这样的好处就是可以减少随机IO,在机械磁盘的情况下,优化应该挺大,但是这里可能会有坑,如果当前脏页的邻居脏页在被一起刷入后,邻居页立马因为数据的变更又变脏了,那此时是不是有种多此一举的感觉,并且反而浪费了时间和开销。更糟糕的是如果邻居页的邻居也是脏页…,那么这个连锁反应可能会出现短暂的性能问题。

「4.MRR」

在实际业务中,我们可能会被告知尽量使用覆盖索引,不要回表,因为回表需要更多IO,耗时更长,但是有时候我们又不得不回表,回表不仅仅会造成过多的IO,更严重的是过多的离散IO。

1
sql复制代码select * from user where grade between 60 and 70

现在要查询成绩在60-70之间的用户信息,于是我们的sql写成上面的那样,当然我们的grade字段是有索引的,按照常理来说,会先在grade索引上找到grade=60这条数据,然后再根据grade=60这条数据对应的id去主键索引上找,最后再次回到grade索引上,不停的重复同样的动作…,
假设现在grade=60对应的id=1,数据是在page_no_1上,grade=61对应的id=10,数据是在page_no_2上,grade=62对应的id=2,数据是在page_no_1上,所以真实的情况就是先在page_no_1上找数据,然后切到page_no_2,最后又切回page_no_1上,但其实id=1和id=2完全可以合并,读一次page_no_1即可,不仅节省了IO,同时避免了随机IO,这就是MRR。当使用MRR之后,辅助索引不会立即去回表,而是将得到的主键id,放在一个buffer中,然后再对其排序,排序后再去顺序读主键索引,大大减少了离散的IO。

往期精彩:

kafka!还好我留了一手

一切从MySQL删除说起

一文搞懂MySQL回滚和持久化

最后,微信搜【假装懂编程】,如果你有任何疑问,欢迎联系我,如果我的文章有问题,也欢迎指正,如果你爱学习,喜欢钻研,可以关注我。

本文转载自: 掘金

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

使用Docker安装NTP服务器与使用

发表于 2021-10-19

NTP服务器安装

由于集群机器时间不一致,导致程序频繁出bug,需要使用ntp服务器做时间同步。上周突然接到需求,赶快搞一套上去。

1. 选型

首先先选合适的镜像,由于时间紧迫,直接选用hub.docker上最热门的镜像

image.png

2. 查看官方文档

从文档中发现提供了docker-compose的启动方式,比较方便,所以决定使用docker-compose来启动镜像,docker-compose文件就存放在git仓库里面

image.png

从这里直接进入代码仓库查看代码
image.png

找到docker-compose文件复制到本地进行修改
image.png

首先,build肯定是不需要的,我们不需要重新去构建镜像,直接使用docker仓库里面构建完的镜像即可,其次,上游的ntp服务器要改成国内可用的ntp服务器,最后,加入本地时区即可。
image.png
修改过后的yml文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yml复制代码version: '3.4'
services:
ntp:
image: cturra/ntp:latest
container_name: ntp
restart: always
ports:
- 123:123/udp
read_only: true
tmpfs:
- /etc/chrony:rw,mode=1750
- /run/chrony:rw,mode=1750
- /var/lib/chrony:rw,mode=1750
environment:
- NTP_SERVERS=ntp1.aliyun.com,ntp2.aliyun.com,ntp3.aliyun.com,ntp4.aliyun.com
- LOG_LEVEL=0
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro

3. 拉取镜像并启动

将docker-compose.yml拷贝到服务器上面去,先pull已经构建好的镜像,docker pull cturra/ntp。然后docker-compose up -d启动容器

4. 测试

使用另一台机器,执行yum install ntpdate安装ntpdate,然后执行ntpdate ip 命令测试同步是否正常。

1634573429.png
经过测试,ntp服务正常使用并且已经将机器时间同步。

5.使用linux定时任务定时同步时间

  1. 先开启定时任务服务:service crond start。
  2. 使用crontab -e编辑定时任务。新增定时任务格式为:cron[command],保存后:wq退出

例:*****/usr/sbin/ntpdate 192.168.30.1 >> /opt/app/ntp.log 2>&1

本文转载自: 掘金

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

1…483484485…956

开发者博客

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