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

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


  • 首页

  • 归档

  • 搜索

在 MacOS 上安装并配置 OpenJDK

发表于 2021-11-23

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

背景

陆续升级到 MacOS BigSur/Monterey 后,电脑上装了两个虚拟机(PD、VMWare Funison),日常开发体验不好。一番取舍后,备份好数据,抹除硬盘给 MacBook 装了双系统,然后要重新装环境。

安装 OpenJDK

OpenJDK 是 Java 的开源版本,有 Oracle 构建维护的,也有其他组织或公司构建并维护的,这里选择安装 Oracle 构建的(OpenJDK builds from Oracle)。

Since September 2017, Oracle provides JDK releases under a free open source license (similar to that of Linux). Availability and community support of OpenJDK releases provided by Oracle is listed separately on jdk.java.net.

自 2017 年 9 月起,Oracle 在免费开源许可(类似于 Linux)下提供 JDK 版本。 Oracle 提供的 OpenJDK 版本的可用性和社区支持在 jdk.java.net 上单独列出。—— Oracle Java SE Support Roadmap

安装 Homebrew

使用 Homebrew 安装 OpenJDK,方便管理(查看、更新、卸载)

在终端中执行安装脚本:

1
bash复制代码/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

安装 OpenJDK@8

使用 brew 命令行工具:

1
bash复制代码brew install openjdk@8

配置 OpenJDK@8

根据输出的安装信息的提示,为了让 Java wrappers 找到 JDK,需要手动建立链接:

1
2
bash复制代码# For the system Java wrappers to find this JDK, symlink it with
sudo ln -sfn /usr/local/opt/openjdk@8/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-8.jdk

配置 $JAVA_HOME 环境变量,找到 .bash_profile/.bashrc/.zshrc 等配置文件中的任意一个,添加下面这行代码:

1
bash复制代码export JAVA_HOME=$(/usr/libexec/java_home -v1.8)

然后退出终端重新打开,或者重新加载配置文件:

1
2
bash复制代码# 以 .zshrc 为例
source ~/.zshrc

在终端中使用 java 命令行工具,来检查配置是否生效:

1
bash复制代码java -version

输出版本信息,表示配置成功:

1
2
3
bash复制代码openjdk version "1.8.0_312"
OpenJDK Runtime Environment (build 1.8.0_312-bre_2021_10_20_23_15-b00)
OpenJDK 64-Bit Server VM (build 25.312-b00, mixed mode)

JDK 版本切换(会话级别)

新项目应该都在用 JDK@11 或者 JDK@17,但行业内目前依然有不少项目在使用 JDK@8。

仅供学习参考,实际使用可以看看 jenv - Manage your Java environment

动态查找 Java Home

developer.apple.com_library_archive_qa_qa1170__index.html.png

使用 /usr/libexec/java_home 命令行工具(支持动态查找 Java Home,默认为最新版本 JDK 的 Java Home)

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码➜  ~ /usr/libexec/java_home
/usr/local/Cellar/openjdk/17.0.1/libexec/openjdk.jdk/Contents/Home

➜ ~ /usr/libexec/java_home -v1.8
/usr/local/Cellar/openjdk@8/1.8.0+312/libexec/openjdk.jdk/Contents/Home

➜ ~ /usr/libexec/java_home -V
Matching Java Virtual Machines (3):
17.0.1 (x86_64) "Homebrew" - "OpenJDK 17.0.1" /usr/local/Cellar/openjdk/17.0.1/libexec/openjdk.jdk/Contents/Home
11.0.12 (x86_64) "Homebrew" - "OpenJDK 11.0.12" /usr/local/Cellar/openjdk@11/11.0.12/libexec/openjdk.jdk/Contents/Home
1.8.0_312 (x86_64) "Homebrew" - "OpenJDK 8" /usr/local/Cellar/openjdk@8/1.8.0+312/libexec/openjdk.jdk/Contents/Home
/usr/local/Cellar/openjdk/17.0.1/libexec/openjdk.jdk/Contents/Home

动态设置 Java Home

在动态查找 Java Home 基础上,结合自定义环境变量、alias,实现动态设置 $JAVA_HOME(动态切换 JDK 版本)。

找到 .bash_profile/.bashrc/.zshrc 等配置文件中的任意一个,添加下面的代码:

1
2
3
4
5
6
7
8
9
10
bash复制代码## JAVA
export JAVA_HOME=$(/usr/libexec/java_home -v1.8)
export JAVA_8_HOME=$(/usr/libexec/java_home -v1.8)
export JAVA_11_HOME=$(/usr/libexec/java_home -v11)
export JAVA_17_HOME=$(/usr/libexec/java_home -v17)

## alias for JAVA
alias java8='export JAVA_HOME=$JAVA_8_HOME'
alias java11='export JAVA_HOME=$JAVA_11_HOME'
alias java17='export JAVA_HOME=$JAVA_17_HOME'

然后退出终端重新打开,或者重新加载配置文件:

1
2
bash复制代码# 以 .zshrc 为例
source ~/.zshrc

在终端中使用 alias,设置 $JAVA_HOME 环境变量,并查看版本信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码➜  ~ java -version
openjdk version "1.8.0_312"
OpenJDK Runtime Environment (build 1.8.0_312-bre_2021_10_20_23_15-b00)
OpenJDK 64-Bit Server VM (build 25.312-b00, mixed mode)

➜ ~ java11
➜ ~ java -version
openjdk version "11.0.12" 2021-07-20
OpenJDK Runtime Environment Homebrew (build 11.0.12+0)
OpenJDK 64-Bit Server VM Homebrew (build 11.0.12+0, mixed mode)

➜ ~ java17
➜ ~ java -version
openjdk version "17.0.1" 2021-10-19
OpenJDK Runtime Environment Homebrew (build 17.0.1+0)
OpenJDK 64-Bit Server VM Homebrew (build 17.0.1+0, mixed mode, sharing)

不足之处,如何改进

通过 alias 设置 $JAVA_HOME 环境变量,能达到切换 JDK 版本,但是没有持久化,仅仅只是会话级别的切换:

  • 旧窗口设置时,没有保存版本
  • 新窗口打开时,没有读取版本

要做到持久化的话,需要在设置默认版本时,将版本信息保存在本地文件中,新窗口打开时,读取文件中的版本信息来做初始化。

其他 JDK

Oracle JDK

最近发布的 Oracle JDK 17 可以免费使用,Java SE Development Kit 17.0.1 downloads

Since September 2021, Oracle provides the Oracle JDK for Java 17 and later under a free use license for All Users.

自 2021 年 9 月起,Oracle 在所有用户的免费使用许可下为 Java 17 及更高版本提供 Oracle JDK。—— Oracle Java SE Support Roadmap

The NFTC is the license for Oracle JDK 17 and later releases. Subject to the conditions of the license, it permits free use for all users – even commercial and production use.

NFTC 是 Oracle JDK 17 及更高版本的许可证。 根据许可证的条件,它允许所有用户免费使用——甚至商业和生产用途。—— What is the new “Oracle No-Fee Terms and Conditions” License (NFTC)?

其他 OpenJDK

  • Adoptium OpenJDK
  • MicroSoft OpenJDK
  • Alibaba OpenJDK
  • …

注意点

  1. JDK@8 不支持 java --version,只支持使用一个分隔符的 java -version
  2. Mac OS X 10.5 以上才支持 /usr/libexec/java_home 命令行工具

总结

从大学时候的 JDK 1.5/1.6 到现在的 JDK 17,我已经记不起是第多少次在网上找怎么安装 JDK,在哪里下载最新版 JDK。

在写这篇文章之前也犹豫要不要写,因为有很多类似的文章(怎么安装 JDK,什么是 OpenJDK,什么是 Oracle JDK 之类的信息)。

思考之后,还是觉得要自己提炼一遍,追根溯源,加深印象的同时,也是记录下来方便以后自己查阅。

参考链接

  • 新 Mac 如何优雅地配置 Java 开发环境
  • 2021 年 Java 开发者生产力报告
  • Oracle Releases Java 17
  • Oracle Java SE Support Roadmap
  • Important Java Directories on Mac OS X
  • Oracle Java SE Licensing FAQ
  • OpenJDK FAQ

本文转载自: 掘金

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

大数据Flume学习之旅第一篇 一、Flume 概述 二、F

发表于 2021-11-23

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

一、Flume 概述

1、Flume 定义

Flume 是 Cloudera 提供的一个高可用的,高可靠的,分布式的海量日志采集、聚合和传输的系统。Flume 基于流式架构,灵活简单。

image.png

2、Flume 基础架构

Flume 组成架构如下图所示

image.png

2.1、Agent

Agent 是一个 JVM 进程,它以事件的形式将数据从源头送至目的。

Agent 主要有 3 个部分组成,Source、Channel、Sink。

2.2、Source

Source 是负责接收数据到 Flume Agent 的组件。Source 组件可以处理各种类型、各种格式的日志数据,包括 avro、thrift、exec、jms、spooling directory、netcat、taildir、sequence generator、syslog、http、legacy。

2.3、Sink

Sink 不断地轮询 Channel 中的事件且批量地移除它们,并将这些事件批量写入到存储或索引系统、或者被发送到另一个 Flume Agent。Sink 组件目的地包括 hdfs、logger、avro、thrift、ipc、file、HBase、solr、自定义。

2.4、Channel

Channel 是位于 Source 和 Sink 之间的缓冲区。因此,Channel 允许 Source 和 Sink 运作在不同的速率上。Channel 是线程安全的,可以同时处理几个 Source 的写入操作和几个Sink 的读取操作。

Flume 自带两种 Channel:Memory Channel 和 File Channel。

Memory Channel 是内存中的队列。Memory Channel 在不需要关心数据丢失的情景下适用。如果需要关心数据丢失,那么 Memory Channel 就不应该使用,因为程序死亡、机器宕机或者重启都会导致数据丢失。

File Channel 将所有事件写到磁盘。因此在程序关闭或机器宕机的情况下不会丢失数据。

2.5、Event

传输单元,Flume 数据传输的基本单元,以 Event 的形式将数据从源头送至目的地。

Event 由 Header 和 Body 两部分组成,Header 用来存放该 event 的一些属性,为 K-V 结构,

Body 用来存放该条数据,形式为字节数组。

image.png

二、Flume 入门

1、Flume 安装部署

1.1、安装地址

(1)Flume 官网地址:flume.apache.org/

(2)文档查看地址:flume.apache.org/FlumeUserGu…

(3)下载地址:archive.apache.org/dist/flume/

1.2、安装部署

  • 将 apache-flume-1.9.0-bin.tar.gz 上传到 linux 的/opt/software 目录下
  • 解压 apache-flume-1.9.0-bin.tar.gz 到/opt/module/目录下
1
shell复制代码[moe@hadoop102 module]$ tar -zxvf apache-flume-1.9.0-bin.tar.gz -C /opt/module/
  • 修改 apache-flume-1.9.0-bin 的名称为 flume
1
shell复制代码[moe@hadoop102 module]$ mv apache-flume-1.9.0-bin flume
  • 将 lib 文件夹下的 guava-11.0.2.jar 删除以兼容 Hadoop 3.1.3
1
shell复制代码[moe@hadoop102 lib]$ rm guava-11.0.2.jar

2、Flume 入门案例

2.1、监控端口数据官方案例

  • 案例需求:

使用 Flume 监听一个端口,收集该端口数据,并打印到控制台。

  • 需求分析:

image.png

  • 实现步骤:
1. 安装 netcat 工具



1
shell复制代码[moe@hadoop102 flume]$ sudo yum install -y nc
2. 判断 44444 端口是否被占用
1
shell复制代码[moe@hadoop102 flume]$ sudo netstat -nlp | grep 44444
3. 在 flume 目录下创建 job 文件夹并进入 job 文件夹
1
2
shell复制代码[moe@hadoop102 flume]$ mkdir job
[moe@hadoop102 flume]$ cd job/
4. 在 job 文件夹下创建 Flume Agent 配置文件 flume-netcat-logger.conf
1
shell复制代码[moe@hadoop102 job]$ vim flume-netcat-logger.conf
5. 在 flume-netcat-logger.conf 文件中添加如下内容 [flume.apache.org/releases/co…](http://flume.apache.org/releases/content/1.9.0/FlumeUserGuide.html#a-simple-example)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
conf复制代码# Name the components on this agent
a1.sources = r1
a1.sinks = k1
a1.channels = c1

# Describe/configure the source
a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 44444

# Describe the sink
a1.sinks.k1.type = logger

# Use a channel which buffers events in memory
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100

# Bind the source and sink to the channel
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1
![image.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/80b1144744058173c10113a4b68350d41a27853e5da5e671cd3b7d0bd80d86de) 6. 先开启 flume 监听端口 + 第一种写法:
1
shell复制代码[moe@hadoop102 flume]$ bin/flume-ng agent --conf conf/ --name a1 --conf-file job/flume-netcat-logger.conf -Dflume.root.logger=INFO,console
+ 第二种写法:
1
shell复制代码[moe@hadoop102 flume]$ bin/flume-ng agent -c conf/ -n a1 -f job/flume-netcat-logger.conf -Dflume.root.logger=INFO,console
参数说明: --conf/-c:表示配置文件存储在 conf/目录 --name/-n:表示给 agent 起名为 a1 --conf-file/-f:flume 本次启动读取的配置文件是在 job 文件夹下的 flume-telnet.conf文件。 -Dflume.root.logger=INFO,console :-D 表示 flume 运行时动态修改 flume.root.logger参数属性值,并将控制台日志打印级别设置为 INFO 级别。日志级别包括:log、info、warn、error。 7. 使用 netcat 工具向本机的 44444 端口发送内容
1
2
3
4
5
6
7
shell复制代码[moe@hadoop102 flume]$ nc localhost 44444
hello
OK
moe
OK
zoe
OK
8. 在 Flume 监听页面观察接收数据情况 ![image.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/d4fc8b6b392c334fd1ebd83a895b9659656e175c8f14c5e1e145b10c78bac617)

2.2、实时监控单个追加文件

  1. 案例需求:实时监控 Hive 日志,并上传到 HDFS 中
  2. 需求分析:

image.png
3. 实现步骤:

* Flume 要想将数据输出到 HDFS,依赖 Hadoop 相关 jar 包检查 Hadoop 和 Java 环境变量配置正确
* 创建 flume-file-hdfs.conf 文件



1
shell复制代码[moe@hadoop102 job]$ vim flume-file-hdfs.conf
注:要想读取 Linux 系统中的文件,就得按照 Linux 命令的规则执行命令。由于 Hive日志在 Linux 系统中所以读取文件的类型选择:exec 即 execute 执行的意思。表示执行Linux 命令来读取文件。 添加如下内容
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
conf复制代码# Name the components on this agent
a2.sources = r2
a2.sinks = k2
a2.channels = c2

# Describe/configure the source
a2.sources.r2.type = exec
a2.sources.r2.command = tail -F /opt/module/hive-3.1.2/logs/hive.log

# Describe the sink
a2.sinks.k2.type = hdfs
a2.sinks.k2.hdfs.path = hdfs://hadoop102:8020/flume/%Y%m%d/%H

#上传文件的前缀
a2.sinks.k2.hdfs.filePrefix = logs-
#是否按照时间滚动文件夹
a2.sinks.k2.hdfs.round = true
#多少时间单位创建一个新的文件夹
a2.sinks.k2.hdfs.roundValue = 1
#重新定义时间单位
a2.sinks.k2.hdfs.roundUnit = hour
#是否使用本地时间戳
a2.sinks.k2.hdfs.useLocalTimeStamp = true
#积攒多少个 Event 才 flush 到 HDFS 一次
a2.sinks.k2.hdfs.batchSize = 100
#设置文件类型,可支持压缩
a2.sinks.k2.hdfs.fileType = DataStream
#多久生成一个新的文件
a2.sinks.k2.hdfs.rollInterval = 30
#设置每个文件的滚动大小
a2.sinks.k2.hdfs.rollSize = 134217700
#文件的滚动与 Event 数量无关
a2.sinks.k2.hdfs.rollCount = 0

# Use a channel which buffers events in memory
a2.channels.c2.type = memory
a2.channels.c2.capacity = 1000
a2.channels.c2.transactionCapacity = 100

# Bind the source and sink to the channel
a2.sources.r2.channels = c2
a2.sinks.k2.channel = c2
注意:对于所有与时间相关的转义序列,Event Header 中必须存在以 “timestamp”的key(除非 hdfs.useLocalTimeStamp 设置为 true,此方法会使用 TimestampInterceptor 自动添加 timestamp) a3.sinks.k3.hdfs.useLocalTimeStamp = true * 运行 Flume
1
shell复制代码[moe@hadoop102 flume]$ bin/flume-ng agent -c conf/ -n a2 -f job/flume-file-hdfs.conf
* 开启 Hadoop 和 Hive 并操作 Hive 产生日志
1
2
3
shell复制代码[moe@hadoop102 logs]$ sbin/start-dfs.sh
[moe@hadoop102 logs]$ sbin/start-yarn.sh
[moe@hadoop102 logs]$ bin/hive
* 在 HDFS 上查看文件 ![image.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/78ffd716a3befcfed94b85c4561ad903ab84af742ae2865246d9e78921c5c0fa)

2.3、实时监控目录下多个新文件

  1. 案例需求:使用 Flume 监听整个目录的文件,并上传至 HDFS
  2. 需求分析:

image.png
3. 实现步骤:

* 创建配置文件 flume-dir-hdfs.conf



1
shell复制代码[moe@hadoop102 job]$ vim flume-dir-hdfs.conf
添加如下内容
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
conf复制代码# Name the components on this agent
a3.sources = r3
a3.sinks = k3
a3.channels = c3

# Describe/configure the source
a3.sources.r3.type = spooldir
a3.sources.r3.spoolDir = /opt/module/flume/upload
a3.sources.r3.fileSuffix = .COMPLETED
a3.sources.r3.fileHeader = true
#忽略所有以.tmp 结尾的文件,不上传
a3.sources.r3.ignorePattern = ([^ ]*\.tmp)

# Describe the sink
a3.sinks.k3.type = hdfs
a3.sinks.k3.hdfs.path = hdfs://hadoop102:8020/flume/upload/%Y%m%d/%H

#上传文件的前缀
a3.sinks.k3.hdfs.filePrefix = upload-
#是否按照时间滚动文件夹
a3.sinks.k3.hdfs.round = true
#多少时间单位创建一个新的文件夹
a3.sinks.k3.hdfs.roundValue = 1
#重新定义时间单位
a3.sinks.k3.hdfs.roundUnit = hour
#是否使用本地时间戳
a3.sinks.k3.hdfs.useLocalTimeStamp = true
#积攒多少个 Event 才 flush 到 HDFS 一次
a3.sinks.k3.hdfs.batchSize = 100
#设置文件类型,可支持压缩
a3.sinks.k3.hdfs.fileType = DataStream
#多久生成一个新的文件
a3.sinks.k3.hdfs.rollInterval = 30
#设置每个文件的滚动大小大概是 128M
a3.sinks.k3.hdfs.rollSize = 134217700
#文件的滚动与 Event 数量无关
a3.sinks.k3.hdfs.rollCount = 0

# Use a channel which buffers events in memory
a3.channels.c3.type = memory
a3.channels.c3.capacity = 1000
a3.channels.c3.transactionCapacity = 100

# Bind the source and sink to the channel
a3.sources.r3.channels = c3
a3.sinks.k3.channel = c3
* 启动监控文件夹命令
1
shell复制代码[moe@hadoop102 flume]$ bin/flume-ng agent -c conf/ -n a3 -f job/flume-dir-hdfs.conf
* 向 upload 文件夹中添加文件
1
shell复制代码[moe@hadoop102 flume]$ mkdir upload
向 upload 文件夹中添加文件
1
2
3
4
5
shell复制代码[moe@hadoop102 flume]$ touch upload/moe.txt
[moe@hadoop102 flume]$ touch upload/moe.tmp
[moe@hadoop102 flume]$ touch upload/moe.log
[moe@hadoop102 flume]$ touch upload/zoe.log
[moe@hadoop102 flume]$ touch upload/zoe.txt
* 查看 HDFS 上的数据 ![image.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/0f4192a2cf38c49603046acbb39d909b6824e528b20b8e94beae483c337efb01)

2.4、实时监控目录下的多个追加文件

Exec source 适用于监控一个实时追加的文件,不能实现断点续传;Spooldir Source适合用于同步新文件,但不适合对实时追加日志的文件进行监听并同步;而 Taildir Source适合用于监听多个实时追加的文件,并且能够实现断点续传。

  1. 案例需求:使用 Flume 监听整个目录的实时追加文件,并上传至 HDFS
  2. 需求分析:

image.png
3. 实现步骤:

* 创建配置文件 flume-taildir-hdfs.conf



1
shell复制代码[moe@hadoop102 job]$ vim flume-taildir-hdfs.conf
添加如下内容
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
conf复制代码# Name the components on this agent
a3.sources = r3
a3.sinks = k3
a3.channels = c3

# Describe/configure the source
a3.sources.r3.type = TAILDIR
a3.sources.r3.positionFile = /opt/module/flume/tail_dir.json
a3.sources.r3.filegroups = f1 f2
a3.sources.r3.filegroups.f1 = /opt/module/flume/files/.*file.*
a3.sources.r3.filegroups.f2 = /opt/module/flume/files2/.*log.*

# Describe the sink
a3.sinks.k3.type = hdfs
a3.sinks.k3.hdfs.path = hdfs://hadoop102:8020/flume/upload2/%Y%m%d/%H

#上传文件的前缀
a3.sinks.k3.hdfs.filePrefix = upload-
#是否按照时间滚动文件夹
a3.sinks.k3.hdfs.round = true
#多少时间单位创建一个新的文件夹
a3.sinks.k3.hdfs.roundValue = 1
#重新定义时间单位
a3.sinks.k3.hdfs.roundUnit = hour
#是否使用本地时间戳
a3.sinks.k3.hdfs.useLocalTimeStamp = true
#积攒多少个 Event 才 flush 到 HDFS 一次
a3.sinks.k3.hdfs.batchSize = 100
#设置文件类型,可支持压缩
a3.sinks.k3.hdfs.fileType = DataStream
#多久生成一个新的文件
a3.sinks.k3.hdfs.rollInterval = 30
#设置每个文件的滚动大小大概是 128M
a3.sinks.k3.hdfs.rollSize = 134217700
#文件的滚动与 Event 数量无关
a3.sinks.k3.hdfs.rollCount = 0

# Use a channel which buffers events in memory
a3.channels.c3.type = memory
a3.channels.c3.capacity = 1000
a3.channels.c3.transactionCapacity = 100

# Bind the source and sink to the channel
a3.sources.r3.channels = c3
a3.sinks.k3.channel = c3
* 启动监控文件夹命令
1
shell复制代码[moe@hadoop102 flume]$ bin/flume-ng agent -c conf/ -n a3 -f job/flume-taildir-hdfs.conf
* 向 files 文件夹中追加内容 在/opt/module/flume 目录下创建 files 文件夹
1
shell复制代码[moe@hadoop102 flume]$  mkdir files
1
2
3
shell复制代码moe@hadoop102 files2]$ echo 'hello flume' >> log1.txt
[moe@hadoop102 files2]$ echo 'hello java' >> log1.txt
[moe@hadoop102 files2]$ echo 'hello spark' >> log2.txt
* 查看 HDFS 上的数据 ![image.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/ee952d63134a29005709ecab674cb5043d90a0ca19efeb71a5c43942cfa489fb) Taildir Source 维护了一个 json 格式的 position File,其会定期的往 position File中更新每个文件读取到的最新的位置,因此能够实现断点续传。Position File 的格式如下:
1
2
json复制代码[moe@hadoop102 flume]$ cat tail_dir.json 
[{"inode":1843859,"pos":14,"file":"/opt/module/flume/files/file1.txt"},{"inode":1843860,"pos":9,"file":"/opt/module/flume/files/file2.txt"},{"inode":1843862,"pos":23,"file":"/opt/module/flume/files2/log1.txt"},{"inode":1843863,"pos":25,"file":"/opt/module/flume/files2/log2.txt"}
注:Linux 中储存文件元数据的区域就叫做 inode,每个 inode 都有一个号码,操作系统用 inode 号码来识别不同的文件,Unix/Linux 系统内部不使用文件名,而使用 inode 号码来识别文件。

本文转载自: 掘金

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

树莓派4B“重启计划”Ⅲ——docker和mariadb、g

发表于 2021-11-23

已经给树莓派安装了ubuntu系统,并且配置了内网穿透和开机自启,现在我甚至可以从公交车上使用手机连接到树莓派中进行操作。

然后,下一步我需要的是docker服务。

安装docker服务

安装docker服务只需要一个命令。

1
lua复制代码sudo apt install docker.io

执行完成后docker服务已经自行启动,但是还需要稍微进行一些配置。

为了能快速拉取镜像,需要给docker服务添加镜像源,镜像源的配置文件位于/etc/docker/daemon.json,这个文件默认是不存在的,需要自行创建,执行sudo vim /etc/docker/daemon.json编辑文件,在“registry-mirrors”字段下添加镜像源 ,保存文件后重启服务。

1
2
3
4
5
6
7
8
9
10
11
ruby复制代码ubuntu@ubuntu:~$ sudo vim /etc/docker/daemon.json
ubuntu@ubuntu:~$ cat /etc/docker/daemon.json
{
"registry-mirrors": [
"https://registry.docker-cn.com",
"https://docker.mirrors.ustc.edu.cn",
"http://hub-mirror.c.163.com"
]
}
ubuntu@ubuntu:~$ sudo systemctl daemon-reload
ubuntu@ubuntu:~$ sudo systemctl restart docker

国内可选的镜像源有:

  • Docker中国区官方镜像 registry.docker-cn.com
  • 网易蜂巢 hub-mirror.c.163.com
  • 中国科技大学 docker.mirrors.ustc.edu.cn

配置完毕后可以使用docker pull命令拉取想要的镜像,我需要的镜像有:

1
2
3
4
5
arduino复制代码docker pull redis:6.2.6  # redis服务
docker pull mariadb:10.6.4 # mariadb数据库,mysql的开源分支
docker pull ubuntu:focal # ubuntu系统focal版,因为当前使用的树莓派系统本身也是focal版,这个镜像可以用于以后制作其他需要的镜像
docker pull python:3.9.7-bullseye # python镜像,可以用于学习python、编译so文件、编译readthedocs的文档。
docker pull nginx:1.20.1 # nginx镜像,可做web项目的反向代理

镜像全部拉取完毕后可以执行docker images命令查看:

1
2
3
4
5
6
7
ruby复制代码ubuntu@ubuntu:~$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
python 3.9.7-bullseye 1c3e5106c1ab 5 days ago 860MB
mariadb 10.6.4 6819e5d163d1 2 weeks ago 393MB
ubuntu focal d5ca7a445605 2 weeks ago 65.6MB
redis 6.2.6 c3d77c9fc5b8 2 weeks ago 107MB
nginx 1.20.1 5eb25865d142 2 weeks ago 126MB

恢复mariadb数据库

MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可,MariaDB的目的是完全兼容MySQL,包括API和命令行,使之能轻松成为MySQL的代替品。鉴于mysql早已被oracle收购了,所以我只好零成本切换到mariadb。

上次在树莓派使用apt安装了mariadb数据库,存了一些重要数据如gitea的私人仓库数据,后来树莓派raspbian系统所在的U盘坏掉了,但是data目录被抢救下来了,这次使用docker部署mariadb 10.6.4,顺便使用data目录做数据恢复。

1.启动数据库

首先需要从新启动一个数据库服务,使用docker启动数据库只需要一个命令:

1
2
3
4
ini复制代码docker run --name recovery_db -d -v /mnt/mysqlrecovery:/var/lib/mysql \
-e MARIADB_ROOT_PASSWORD=jwirLAST -p 3406:3306 mariadb:10.6.4 \
--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci \
--lower_case_table_names=1

命令行参数说明:

docker run命令用于运行容器。

–name指定此容器名称为recovery_db,注意名称不能与其他容器的名称相同;

-d选项指定容器在后台运行,区别于-it选项指定容器以交互方式运行;

-v选项用于映射文件目录,此处是把容器内的/var/lib/mysql(数据文件路径)映射到容器外的/data/mysqlrecovery,注意外部目录需要先准备好,需要映射多个路径可以连续使用多个-v;

-e参数用于指定环境变量,此处按照mariadb官方docker镜像的说明通过MARIADB_ROOT_PASSWORD环境变量指定容器内mariadb的root密码, 和-v选项一样可以连续使用;

-p选项用于做端口映射,此处是把树莓派的3406端口映射到容器内的3306端口供mariadb服务监听并提供服务,-p参数也可连续使用;

mariadb:10.6.4是指定要使用的docker镜像为mariadb,标签为10.6.4。

命令最后的部分是指定服务端的编码集和指定表名不区分大小写字母。

执行命令之后,使用docker ps命令可以看到名称为recovery_db的容器已经开始运行,使用ls -l /data/mysqlrecovery命令可以看到/mnt/mysqlrecovery目录正在被初始化,出现了mariadb的数据库文件。

image.png

根据树莓派的io效率,可能需要等待两三分钟,等数据目录初始化完毕之后就可以使用客户端连接树莓派的3406端口进行验证了,个人推荐使用heidisql工具。

image.png

2.复制文件

经过上一步操作,mariadb数据库服务已经开始运行,可以正常使用,之后的步骤都是做数据恢复,我之前抢救出的数据库文件在/home/ubuntu/backups/mysql.zip,需要解压文件、停服务、覆盖文件、启动服务、导出脚本。

1
2
3
4
5
6
7
8
9
shell复制代码mkdir /mnt/temp && cd /mnt/temp  # 创建并切换工作目录
cp /home/ubuntu/backups/mysql.zip . && unzip -q mysql.zip # 复制并静默解压文件
ls -l /mnt/temp/mysql
docker stop recovery_db # 停服务
docker ps -a | grep recovery_db # 确认服务状态
cp -rp /mnt/temp/mysql/gitea /mnt/mysqlrecovery/gitea # 复制gitea数据库
cp /mnt/temp/mysql/ibdata1 /mnt/mysqlrecovery/ibdata1 # 复制表结构文件
docker start recovery_db # 启动数据库服务
docker ps -a | grep recovery_db # 确认服务状态

数据库启动后,重新使用heidisql或其他支持mysql的客户端连接数据库,可以看到名为gitea的数据库已恢复。

image.png

image.png

复制数据库文件时需要注意文件的属主和权限,否则数据库无法正常使用。

3.删旧启新

使用数据库客户端工具导出gitea库的数据,以sql文件形式保存,执行docker stop recovery_db && docker rm recovery_db命令,停止并删除recovery_db容器,执行rm -rf /mnt/mysqlrecovery命令删除不再使用的数据库文件。

依照第一步重新启动新的数据库容器,等待数据库可用。

1
2
3
4
shell复制代码docker run --name work_db -d -v /data/mysqldata:/var/lib/mysql --restart=always \
-e MARIADB_ROOT_PASSWORD=jwirLAST -p 3306:3306 mariadb:10.6.4 \
--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci \
--lower_case_table_names=1

4.导入sql脚本

使用上一步导出的sql文件导入数据到新的数据库。

另可参考官方教程

恢复gitea版本仓库

Gitea是一个开源社区驱动的轻量级代码托管解决方案,后端采用 Go 语言编写,遵循 MIT 开源许可证,你可以从这里查看gitea的完整代码。gitea具有开源化、易安装、跨平台和轻量级四大特点,即使在树莓派上也能完美运行。

gitea支持使用docker部署和使用二进制方式部署等多种方式,本文属于数据恢复的范畴,采用二进制方式启动gitea服务,与重新部署略有不同。

环境准备

首先需要创建git账号,之后的gitea服务将以git账号权限启动。

1
2
shell复制代码groupadd git  # 添加用户组,组名git
useradd -d /home/git -m -g git -s /bin/bash git # 在git组下创建账号git,指定家目录和登录shell

之前抢救的gitea版本仓库文件备份包位于/home/ubuntu/backups/giteadata.tar.bz2,需要使用git用户解压文件到/home/git/gitea。

1
2
3
4
shell复制代码cp /home/ubuntu/backups/giteadata.tar.bz2 /home/git/
chown git.git /home/git/giteadata.tar.bz2
su - git
tar xf giteadata.tar.bz2

解压完成后,配置文件位于/home/git/gitea/custom/conf/app.ini,版本仓库位于/home/git/gitea/gitea-repositories。

1
2
3
4
5
6
7
yaml复制代码git@ubuntu:~/gitea$ ls -l /home/git/gitea
total 97920
drwxrwxrwx 3 git git 4096 4月 22 2020 custom
drwxrwxrwx 11 git git 4096 11月 6 20:14 data
-rwxr-xr-x 1 git git 100246088 11月 8 19:39 gitea
drwxrwxrwx 3 git git 4096 4月 22 2020 gitea-repositories
drwxrwxrwx 2 git git 4096 11月 22 00:09 log

下载最新版gitea,树莓派4B使用arm64架构CPU,可以使用gitea-1.15.6-linux-arm64.xz包,树莓派3B及之前的版本,需要下载gitea-1.15.6-linux-arm-6.xz或gitea-1.15.6-linux-arm-5.xz,下载后执行xz -d命令解压文件,替换到/home/git/gitea/gitea。

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
shell复制代码git@ubuntu:~/temp$ wget https://dl.gitea.io/gitea/1.15.6/gitea-1.15.6-linux-arm64.xz
--2021-11-22 23:34:26-- https://dl.gitea.io/gitea/1.15.6/gitea-1.15.6-linux-arm64.xz
Resolving dl.gitea.io (dl.gitea.io)... 2606:4700:3034::ac43:bad3, 2606:4700:3036::6815:3c07, 104.21.60.7, ...
Connecting to dl.gitea.io (dl.gitea.io)|2606:4700:3034::ac43:bad3|:443... connected.
HTTP request sent, awaiting response... 307 Temporary Redirect
Location: https://storage.gitea.io/gitea-artifacts/gitea/1.15.6/gitea-1.15.6-linux-arm64.xz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=SU5ZZ3Q6D6AFIQSCOO65%2F20211122%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20211122T153428Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=937898e9a6c24308f6a0a55a496defb94a6cd325e2b85498219e3072ad4c5599 [following]
--2021-11-22 23:34:28-- https://storage.gitea.io/gitea-artifacts/gitea/1.15.6/gitea-1.15.6-linux-arm64.xz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=SU5ZZ3Q6D6AFIQSCOO65%2F20211122%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20211122T153428Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=937898e9a6c24308f6a0a55a496defb94a6cd325e2b85498219e3072ad4c5599
Resolving storage.gitea.io (storage.gitea.io)... 2604:1380:2000:c600::5, 147.75.84.81
Connecting to storage.gitea.io (storage.gitea.io)|2604:1380:2000:c600::5|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 44287796 (42M) [application/x-xz]
Saving to: ‘gitea-1.15.6-linux-arm64.xz’

gitea-1.15.6-linux-arm64.xz 100%[==========================================================>] 42.24M 946KB/s in 53s

2021-11-22 23:35:22 (815 KB/s) - ‘gitea-1.15.6-linux-arm64.xz’ saved [44287796/44287796]

git@ubuntu:~/temp$ ls -l
total 43252
-rw-rw-r-- 1 git git 44287796 10月 28 17:08 gitea-1.15.6-linux-arm64.xz
git@ubuntu:~/temp$ xz -d gitea-1.15.6-linux-arm64.xz
git@ubuntu:~/temp$ ls -l
total 97900
-rw-rw-r-- 1 git git 100246088 10月 28 17:08 gitea-1.15.6-linux-arm64
git@ubuntu:~/temp$ mv gitea-1.15.6-linux-arm64 /home/git/gitea/gitea
git@ubuntu:~/temp$ chmod +x /home/git/gitea/gitea
git@ubuntu:~/temp$

添加配置

修改配置文件/home/git/gitea/custom/conf/app.ini使数据库配置和文件路径配置与实际情况相符:

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
ini复制代码APP_NAME = Gitea: Git with a cup of tea
RUN_USER = git ; 以git用户启动
RUN_MODE = prod ; 生产环境模式

[oauth2]
JWT_SECRET = BMZHkyeWxyQe********* ; jwt认证token

[security]
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5c********
INSTALL_LOCK = true
SECRET_KEY = 7x9LiJyJwrteuPmIh5iZAXlEA********

[database]
DB_TYPE = mysql ; 数据库类型,可选postgresql、sqlite和mysql
HOST = 192.168.3.14:3306 ; 数据库服务地址
NAME = gitea ; 数据库名
USER = gitea ; 数据库账号
PASSWD = ***** ; 数据库密码
SSL_MODE = disable ; 禁用ssl
CHARSET = utf8mb4 ; 编码格式使用utf8mb4,与utf8相比支持表情符号
PATH = /home/git/gitea/data/gitea.db ; 使用sqlite数据库时的数据文件路径

[repository]
ROOT = /home/git/gitea/gitea-repositories ; 版本仓库位置

[server]
SSH_DOMAIN = 192.168.3.14 ; git服务的ssh模式地址
DOMAIN = 192.168.3.14 ; git服务的HTTP模式地址
HTTP_PORT = 3000 ; gitea服务监听端口
ROOT_URL = http://192.168.3.14:3000/
DISABLE_SSH = false
SSH_PORT = 22
LFS_START_SERVER = true
LFS_CONTENT_PATH = /home/git/gitea/data/lfs
LFS_JWT_SECRET = 0zK7KHBPfffQu8i5********
OFFLINE_MODE = true
LOCAL_ROOT_URL = http://localhost:3000/

[mailer]
ENABLED = true
HOST = smtp.qq.com:465 ; 邮件smtp服务的地址和端口
FROM = nightstream@foxmail.com ; smtp发送者
USER = nightstream@foxmail.com ; smtp服务账号
PASSWD = kni******************** ; smtp服务密码

[service]
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DISABLE_REGISTRATION = false
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
ENABLE_CAPTCHA = false
REQUIRE_SIGNIN_VIEW = true
DEFAULT_KEEP_EMAIL_PRIVATE = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING = true
NO_REPLY_ADDRESS = noreply.localhost

[picture]
DISABLE_GRAVATAR = true
ENABLE_FEDERATED_AVATAR = false

[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true

[session]
PROVIDER = file

[log]
MODE = file
LEVEL = info ; 日志级别
ROOT_PATH = /home/git/gitea/log ; 日志文件路径

附数据库utf8mb4编码说明

执行/home/git/gitea/gitea web --config /home/git/gitea/custom/conf/app.ini测试gitea进程是否可以正常启动,查看/home/git/gitea/log/gitea.log日志文件确认是否存在报错,如数据库连接失败、文件系统权限异常等。

1
2
3
4
5
6
7
8
9
shell复制代码git@ubuntu:~/gitea$ /home/git/gitea/gitea web --config /home/git/gitea/custom/conf/app.ini
2021/11/22 23:43:42 cmd/web.go:102:runWeb() [I] Starting Gitea on PID: 18955
2021/11/22 23:43:42 cmd/web.go:146:runWeb() [I] Global init
2021/11/22 23:43:42 routers/init.go:74:GlobalInit() [I] Git Version: 2.25.1, Wire Protocol Version 2 Enabled
2021/11/22 23:43:42 routers/init.go:77:GlobalInit() [I] AppPath: /home/git/gitea/gitea
2021/11/22 23:43:42 routers/init.go:78:GlobalInit() [I] AppWorkPath: /home/git/gitea
2021/11/22 23:43:42 routers/init.go:79:GlobalInit() [I] Custom path: /home/git/gitea/custom
2021/11/22 23:43:42 routers/init.go:80:GlobalInit() [I] Log path: /home/git/gitea/log
2021/11/22 23:43:42 routers/init.go:81:GlobalInit() [I] Run Mode: Prod
1
2
3
4
shell复制代码git@ubuntu:~/temp$ cat /home/git/gitea/log/gitea.log
2021/11/22 23:43:44 cmd/web.go:196:listen() [I] Listen: http://0.0.0.0:3000
2021/11/22 23:43:44 cmd/web.go:199:listen() [I] LFS server enabled
2021/11/22 23:43:44 ...s/graceful/server.go:62:NewServer() [I] Starting new Web server: tcp:0.0.0.0:3000 on PID: 18955

此时可以访问gitea页面查看效果,您可以使用自己的账号登录,同时日志将有刷新:

image.png

系统服务

为了避免每次启动树莓派都需要手动启动gitea进程,我们可以参照官方文档将gitea二进制文件注册成系统服务,由于我的数据库是使用docker部署的,所以service文件与官方示例相比略有不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码[Unit]
Description=Gitea (Git with a cup of tea)
After=syslog.target ; 在syslog服务启动后启动
After=network.target ; 在network服务启动后启动
After=docker.target ; 在docker服务启动后启动,因为数据库使用docker启动

[Service]
RestartSec=2s ; 重启间隔时间
Type=simple
User=git ; 使用git账号启动服务进程
Group=git ; 使用git组
WorkingDirectory=/home/git/gitea/ ; 设置工作路径
ExecStart=/home/git/gitea/gitea web --config /home/git/gitea/custom/conf/app.ini # 进程启动命令
Restart=always ; 失败自动重启
Environment=USER=git ; 设置环境变量
HOME=/home/git ; 设置home路径

[Install]
WantedBy=multi-user.target ; 系统开机自启

将以上内容保存到/etc/systemd/system/gitea.service文件(可能需要root权限),然后设置开机自启并启动gitea服务。

1
2
shell复制代码systemctl enable gitea  # 设置服务开机自启
systemctl start gitea # 手动立即启动

执行systemctl status gitea查看服务状态,打开浏览器查看页面情况。

image.png

image.png

数据完美恢复~

本文转载自: 掘金

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

整明白 Golang slice 声明方式、浅复制现象、深复

发表于 2021-11-22

什么是切片

切片(slice)是对数组一个连续片段的引用。切片是一个引用类型,它实际并不存储元素,它只是标识了数组上的某一个连续片段。

数组在内存中是一连串的内存空间,每个元素占据一块内存。

切片的数据结构是一个结构体,结构体内由三个参数。

  • Pointer 指向数组中它要表示的片段的起始元素;
  • len 长度
  • cap 最大容量
1
2
3
4
5
golang复制代码type slice struct {
array unsafe.Pointer
len int
cap int
}

slice示意图:

slice1.png

声明方式

slice 有[]T{}、new 、make三种声明方式。具体有哪些区别将会根据下面实例进行分析。

1
2
3
4
5
6
golang复制代码sl := []string{"a", "b", "c", "d"}

sl := make([]string, 4)

sl := new([]string)
*sl = make([]string, 4)

浅复制现象

赋值过程中发生的浅复制

来看实例代码

1
2
3
4
5
6
7
8
9
10
11
12
golang复制代码func example1a()  {
sl := []string{"a", "b", "c", "d"}
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

// 浅复制1:赋值过程中发生的浅复制
sl1 := sl
fmt.Printf("sl1:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl1, sl1, &sl1)
sl1[0] = "a被修改"
fmt.Println("================ sl1 被修改后 ================")
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
fmt.Printf("sl1:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl1, sl1, &sl1)
}
1
2
3
4
5
ini复制代码sl:[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl1:[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc0000040c0
================ sl1 被修改后 ================
sl:[a被修改 b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl1:[a被修改 b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc0000040c0

sl 声明得到了一个切片,并在创建了一个数组,sl 切片的内部指针指向这个数组。

sl1 由 sl 赋值而来,sl1 得到了一个和 sl 一样的切片,同样它的内部指针也指向最初创建的数组。

当对 sl1 的索引 0 进行修改后,打印 sl 对应的元素值也将发生变化。

slice2.png

通常,在没有了解切片结构的开发者,会误以为 sl1 与 sl 是完全独立,互相的修改并不影响对方。实际上,它们确实是两个完全独立的内存,但是它们的内部结构都指向了同一个数组。

切片并不存储数组元素,它只是搬运工,标识了数组上的片段区间。

所以, sl1[0] 的修改实际上是修改的 sl1 索引0 对应的在数组上的元素值。当访问 sl 时,它读取自己在数组上的片段时,也将受到影响。

这一现象也被称之为浅复制。

函数形参中发生的浅复制

浅复制不只发生在变量赋值过程中,在调用函数实参传给形参的时候也在悄然发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
golang复制代码func example1b()  {
sl := []string{"a", "b", "c", "d"}
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

// 浅复制2:函数形参中发生的浅复制
func (slParam []string) {
fmt.Printf("slParam:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", slParam, slParam, &slParam)
slParam[0] = "a被修改"
fmt.Println("================ slParam 被修改后 ================")
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
fmt.Printf("slParam:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", slParam, slParam, &slParam)
}(sl)
// 外部的 sl 也将受到变化
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
}
1
2
3
4
5
6
ini复制代码sl:[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
slParam:[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc0000040c0
================ slParam 被修改后 ================
sl:[a被修改 b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
slParam:[a被修改 b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc0000040c0
sl:[a被修改 b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078

在函数内部对形参切片的修改,将影响函数外实参。看过实例1a后,相信你对于这个结果并不会太震惊。

切片实参和形参是两个不同变量,但它们拥有同样的内部结构,内部结构中的指针依然是分别指向数组。

深复制操作

实例1a和1b中展示了切片的浅复制现象,对于如何解决浅复制问题在本例中将会解答。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
golang复制代码func example3()  {
sl := []string{"a", "b", "c", "d"}
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

// 深复制:通过 copy 解决赋值过程中发生的浅复制
sl2 := make([]string, 4)
fmt.Printf("sl2:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
copy(sl2, sl)
fmt.Println("================ copy 复制后 ================")
fmt.Printf("sl2:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
sl2[0] = "a被修改了"
fmt.Println("================ sl2 被修改后 ================")
fmt.Printf("sl2:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
}
1
2
3
4
5
6
7
8
ini复制代码sl:[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl2:[ ] 变量(或变量结构某个指针)指向地址(变量值):0xc0000200c0 变量地址:0xc0000040c0
================ copy 复制后 ================
sl2:[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc0000200c0 变量地址:0xc0000040c0
sl:[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
================ sl2 被修改后 ================
sl2:[a被修改了 b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc0000200c0 变量地址:0xc0000040c0
sl:[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078

**本例中通过 copy 方法深复制操作解决了赋值过程中的浅复制现象。**sl2 和 sl 将是两个完全不同的切片,并且其内部指针也将指向两个不同的数组。这样,一方的修改就不会影响另一方了。

slice3.png

append 操作

本例中展示了 append 操作。

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
golang复制代码func example4()  {
sl := []string{"a", "b", "c", "d"}
sl2 := sl
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
fmt.Printf("sl2:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl2:cap:%d,len:%d\n", cap(sl2), len(sl2))

fmt.Println("================ 数组每个元素对应的地址 ================")
fmt.Printf("a:%p b:%p c:%p d:%p \n", &sl[0], &sl[1], &sl[2], &sl[3])
sl2 = sl2[1:2]
fmt.Println("================ sl2[1:2] 使切片 sl2 指向了 b 元素 ================")
fmt.Printf("sl2:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)

fmt.Printf("sl2:cap:%d,len:%d\n", cap(sl2), len(sl2))
sl2 = append(sl2[:1], "e")
fmt.Println("================ 切片还有空闲容量进行 append e ================")
fmt.Printf("sl2:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

sl2 = append(sl2, "f")
fmt.Println("================ 切片还有空闲容量进行 append f ================")
fmt.Printf("sl2:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

fmt.Printf("sl2:cap:%d,len:%d\n", cap(sl2), len(sl2))
sl2 = append(sl2, "g")
fmt.Println("================ 切片没有空闲容量进行 append g ================")
fmt.Printf("sl2:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

fmt.Println("================ 发生扩容后 ================")
fmt.Printf("sl2:cap:%d,len:%d\n", cap(sl2), len(sl2))
fmt.Printf("sl2:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
sl2 = sl2[:6]
fmt.Printf("sl2:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
fmt.Println("================ 新数组每个元素对应的地址 ================")
fmt.Printf("b:%p c:%p e:%p f:%p \n", &sl2[0], &sl2[1], &sl2[2], &sl2[3])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码sl:[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl2:[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004090
sl2:cap:4,len:4
================ 数组每个元素对应的地址 ================
a:0xc000020080 b:0xc000020090 c:0xc0000200a0 d:0xc0000200b0
================ sl2[1:2] 使切片 sl2 指向了 b 元素 ================
sl2:[b] 变量(或变量结构某个指针)指向地址(变量值):0xc000020090 变量地址:0xc000004090
sl2:cap:3,len:1
================ 切片还有空闲容量进行 append e ================
sl2:[b e] 变量(或变量结构某个指针)指向地址(变量值):0xc000020090 变量地址:0xc000004090
sl:[a b e d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
================ 切片还有空闲容量进行 append f ================
sl2:[b e f] 变量(或变量结构某个指针)指向地址(变量值):0xc000020090 变量地址:0xc000004090
sl:[a b e f] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl2:cap:3,len:3
================ 切片没有空闲容量进行 append g ================
sl2:[b e f g] 变量(或变量结构某个指针)指向地址(变量值):0xc00004e060 变量地址:0xc000004090
sl:[a b e f] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
================ 发生扩容后 ================
sl2:cap:6,len:4
sl2:[b e f g] 变量(或变量结构某个指针)指向地址(变量值):0xc00004e060 变量地址:0xc000004090
sl2:[b e f g ] 变量(或变量结构某个指针)指向地址(变量值):0xc00004e060 变量地址:0xc000004090
================ 新数组每个元素对应的地址 ================
b:0xc00004e060 c:0xc00004e070 e:0xc00004e080 f:0xc00004e090

1.最初切片刚创建的时候,sl、sl2 切片内部指针指向数组第一个元素a。

2.经过 sl2 = sl2[1:2] 后,sl2 指向了数组中的第二个 b 元素。

3.往 sl2 切片 append e 时,此时 sl2 还有空闲空间(cap-len>0),append 操作直接修改了数组元素 c => e。

4.往 sl2 切片 append f 时,此时 sl2 依然还有空闲空间(cap-len>0),append 操作直接修改了数组元素 d => f。

slice4.png

5.往 sl2 切片 append g 时,此时 sl2 已经没有空闲空间了(cap-len=0),append 操作会导致扩容。由于数组空间是固定不变的,扩容将使 sl2 指向新的数组。sl2 第一个元素仍然是 b,但它指向地址已经不再是最初数组中元素b的地址了,这一点可以证明发生了扩容,并产生了新数组。

实际上 sl2 仅需要 4 个空间,对应的新数组却提供了 6 个空间,至于这点应该和切片的扩容机制有关,后续文章可能会继续深入探讨。

其余几种 slice 声明和操作方式

&[]T

1
2
3
4
golang复制代码sl := &[]string{"a", "b", "c", "d"}
// 等价于
s := []string{"a", "b", "c", "d"}
sl := &s

sl 将得到的是指向切片的地址,它是一个指针,指向切片,而切片内部指针指向数组。

slice5.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
golang复制代码func example2()  {
sl := &[]string{"a", "b", "c", "d"}
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

sl1 := sl
fmt.Printf("sl1:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl1, sl1, &sl1)
*sl1 = append(*sl1, "e")
fmt.Println("================ append 后 ================")
fmt.Printf("sl1:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl1, sl1, &sl1)
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

(*sl1)[0] = "a被修改"
fmt.Println("================ sl1 被修改后 ================")
fmt.Printf("sl1:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl1, sl1, &sl1)
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
}
1
2
3
4
5
6
7
8
ini复制代码sl:&[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028
sl1:&[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006038
================ append 后 ================
sl1:&[a b c d e] 变量(或变量结构某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006038
sl:&[a b c d e] 变量(或变量结构某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028
================ sl1 被修改后 ================
sl1:&[a被修改 b c d e] 变量(或变量结构某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006038
sl:&[a被修改 b c d e] 变量(或变量结构某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028

make

make 方式创建切片,make 初始化了数组空间大小,元素初始值默认为零值。

1
2
3
4
5
6
7
8
9
10
11
golang复制代码func example5()  {
sl := make([]string, 4)
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
sl[0] = "a"
sl[1] = "b"
sl[2] = "c"
sl[3] = "d"
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
sl = append(sl, "e", "f", "g", "h")
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
}
1
2
3
ini复制代码sl:[   ] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl:[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl:[a b c d e f g h] 变量(或变量结构某个指针)指向地址(变量值):0xc00010a000 变量地址:0xc000004078

new

new 创建切片将返回地址,sl 此时拿到的仅是地址,切片对应的数组甚至都没有初始化,此时无法使用这个切片。

直到经过*sl = make([]string, 4),之后才能正常通过指针操作切片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
golang复制代码func example6()  {
sl := new([]string)
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
// new 只拿到了一个指针,并没法使用这个slice,必须经过 make 初始化后,才能使用
*sl = make([]string, 4)
fmt.Println("================ make 后 ================")
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

(*sl)[0] = "a"
(*sl)[1] = "b"
(*sl)[2] = "c"
(*sl)[3] = "d"
fmt.Println("================ 赋值后 ================")
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

*sl = append(*sl, "b")
fmt.Println("================ append 后 ================")
fmt.Printf("sl:%+v 变量(或变量结构某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
}
1
2
3
4
5
6
7
ini复制代码sl:&[] 变量(或变量结构某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028
================ make 后 ================
sl:&[ ] 变量(或变量结构某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028
================ 赋值后 ================
sl:&[a b c d] 变量(或变量结构某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028
================ append 后 ================
sl:&[a b c d b] 变量(或变量结构某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028

总结

  1. slice 有[]T{}、new 、make三种声明方式。
  2. slice 会在变量赋值时发生浅复制。
  3. copy() 可以让 slice 进行深复制。
  4. append 再操作切片时,切片空闲容量不足时会发生扩容。

end!

文章来自 整明白 Golang slice 声明方式、浅复制现象、深复制、append操作 | 猴子星球|Mr-houzi

本文转载自: 掘金

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

Python的线程03 线程同步 线程同步 总结延伸

发表于 2021-11-22

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

正式的Python专栏第42篇,同学站住,别错过这个从0开始的文章!

前面两篇文章介绍了线程概念和多线程程序的代码演示,但是在多线程程序中,它们互相独立打印的时间却是错乱的!

如下文,明明t-0 > t-1 > t-2 (按照线程创建时间早晚排列)。最后输出居然是t-1最落后。

屏幕快照 2021-11-23 上午12.26.44.png

我们怎么样做避免错乱呢, 下面看看。

线程同步

多线程,就是多个独立的运行单位,同时执行同样的事情。

多线程不是已经做到同时执行了吗?还需要同步干嘛?

是的,线程是同时被调用执行了,但是每个线程之间互相独立,也互相竞争了。

这就跟跑道上有3个运动员,枪响之后同时开跑,但是他们通常却不是同时到达终点。

同步是什么意思?

同步就是原本这条跑道跑三个人的加上同步之后,在任意时间上,只有一个人在跑道。

听起来是不是匪夷所思,怎么多线程不是为多个任务提高效率吗?加个同步不就一个时间只有一个任务执行了,这还扯啥多线程。

很遗憾,同步就是这个意思,我们有时会说完整一点,同步互斥!总结来说就是:同步是一种机制,它保证跑道上面任何时候只有一个运动员。技术上来说就是,同步保证 程序数据 任何时候只被一个线程操作。

我们使用同步机制的时候,也是在找那些应该被限制的’跑道‘,利用同步机制保证在那个跑道上任意时刻只有一个‘运动员’在上面跑步。

(解释的很清楚了,看不懂的可以找同学讨论上面的这几句)

我们了解了同步机制,下面看看锁。

threading.Lock获取同步锁

threading.Lock是一个类,我们能用它创建一个锁对象。

什么是锁?

维持同步互斥机制的媒介

相当于跑道有个大门,每次只开门让一个程序员进去跑

说错了,运动员(程序员还是需要多锻炼啊)。

锁要是坏了,后果可以自己想象(后面文章会说)。

我们下面代码会用到Lock的两个函数:

acquire函数:获取锁
release函数:释放锁

前文说过了,加上主线程,一共是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
python复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/21 12:02 上午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : __init__.py.py
# @Project : hello

import threading
import datetime
import time


def dianzan_guanzhu(lock: threading.Lock):
thread_name = threading.current_thread().getName()
print("线程启动了:", thread_name)
now = datetime.datetime.now()
name = "python萌新" + thread_name
lock.acquire()
print("%s - %s name:%s" % (thread_name, now, name))
time.sleep(1)
result = "好棒!" + name + " 关注雷学委,学会了开发知识!"
print("%s - %s result:%s" % (thread_name, now, result))
lock.release()
return result


my_lock = threading.Lock()
for i in range(3):
mythread = threading.Thread(name="t-" + str(i), target=lambda: dianzan_guanzhu(my_lock))
print("mythread:", mythread)
print("is_alive:", mythread.is_alive())
mythread.start()
ac = threading.active_count()
print("active_count:", ac)

下面是运行结果:

屏幕快照 2021-11-23 上午12.46.35.png

我们看到每个线程都完整完成了任务,不会出现三个线程互相穿插错乱的输出。

这里初学者可以感受一下同步的作用,效果。

总结延伸

以上就是一种线程协调方案。

线程同步,也并非同一步调,而是同步互斥!

上面学委也提到了同步和多线程是矛盾存在的,后面继续看吧。

这里提出一个小问题:

怎么t-0输出函数内第一行后,还输出了其他像activate_acount等信息?

对了,有些同学看学委文章经常忘记关注,好多文章别错过了。

另外喜欢Python的朋友,请关注学委的 Python基础专栏 or Python入门到精通大专栏

持续学习持续开发,我是雷学委!

编程很有趣,关键是把技术搞透彻讲明白。

欢迎关注微信,点赞支持收藏!

本文转载自: 掘金

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

【笔记】 拓扑排序

发表于 2021-11-22

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


对于任何有向图而言,其拓扑排序为其所有结点的一个线性排序(对于同一个有向图而言可能存在多个这样的结点排序)。该排序满足这样的条件——对于图中的任意两个结点u和v,若存在一条有向边从u指向v,则在拓扑排序中u一定出现在v前面。

拓扑排序常见于判断有向图是否有环、统计dag的信息等。

算法

记录每个节点的入度,把入度为0的节点加入队列中。
每次从队列中取出一个点,把它相连的节点的入度都减一,表示删除了这个节点。
队列为空后,如果还有点没有被访问过,证明有环。
无环的情况下,节点出队顺序即是一个拓扑序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cpp复制代码struct Node
{
int indegree; //入度
vector<int> edgs; //邻接表
}node[M];

/* 返回是否有环 */
bool topo_sort(int n)
{
queue<int> q;
for(int i=1; i<=n; ++i)
if(node[i].indegree==0)
q.push(i);
int cnt = 0; //可被排序的节点
while(!q.empty())
{
int u=q.front(); q.pop();
++cnt;
for(int v:node[u].edgs)
if(--node[v].indegree==0)
q.push(v);
}
return cnt<n;
}

例题

1. hihoCoder #1174 : 拓扑排序·一

给一张有向图,判断是否有环。

模板题

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
cpp复制代码/* LittleFall : Hello! */
#include <bits/stdc++.h>
using namespace std; using ll = long long; inline int read();
const int M = 100016, MOD = 1000000007;
/*topological-sort*/

struct Node
{
int indegree;
vector<int> edgs;
}node[M];

/* 返回是否有环 */
bool topo_sort(int n)
{
queue<int> q;
for(int i=1; i<=n; ++i)
if(node[i].indegree==0)
q.push(i);
int cnt = 0; //可被排序的节点
while(!q.empty())
{
int u=q.front(); q.pop();
++cnt;
for(int v:node[u].edgs)
if(--node[v].indegree==0)
q.push(v);
}
return cnt<n;
}

int main(void)
{
#ifdef _LITTLEFALL_
freopen("in.txt","r",stdin);
#endif

int T = read();
while(T--)
{
memset(node,0,sizeof(node));
int n=read(), m=read();
while(m--)
{
int a = read(), b = read();
node[a].edgs.push_back(b);
++node[b].indegree;
}
printf("%s\n",topo_sort(n)?"Wrong":"Correct");
}

return 0;
}


inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}

2. hihocoder #1175 : 拓扑排序·二

给一张有向无环图,初始时给某些节点释放一个病毒。病毒会在本节点留下一个标本,然后复制给一个节点指向的所有子节点,并且从子节点再次重复这个过程。问整个过程结束后所有节点的病毒数量总和。

考虑拓扑排序,当一个节点没有入度时,就让它开始复制,统计完它上面的病毒数后删去这个节点即可。

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
cpp复制代码/* LittleFall : Hello! */
#include <bits/stdc++.h>
using namespace std; using ll = long long; inline int read();
const int M = 100016, MOD = 142857;

struct Node
{
vector<int> edgs;
int ind;
int val;
}node[M];

int topo_sort(int n)
{
queue<int> q;
for(int i=1; i<=n; ++i)
{
if(node[i].ind==0)
q.push(i);
}
int ans = 0;
while(!q.empty())
{
int u=q.front(); q.pop();
ans = (ans + node[u].val) % MOD;
for(int v:node[u].edgs)
{
node[v].val = (node[v].val + node[u].val) % MOD;
if(--node[v].ind == 0) q.push(v);
}
}
return ans;
}
int main(void)
{
#ifdef _LITTLEFALL_
freopen("in.txt","r",stdin);
#endif

memset(node, 0, sizeof(node));
int n=read(), m=read(), k=read();
for(int i=1; i<=k; ++i)
++node[read()].val;
for(int i=1; i<=m; ++i)
{
int a=read(),b=read();
node[a].edgs.push_back(b);
++node[b].ind;
}
int ans = topo_sort(n);
printf("%d\n",ans );
return 0;
}


inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}

3. HDU 4857

现在有n个人需要排序,给定一些要求(i,j)(i,j)(i,j),表示iii必须在jjj的前面。在满足这些条件的同时,需要让1号尽量往前,再让2号尽量往前,依次类推。数据保证有解。

注意这个顺序并非字典序最小,比如对于以下dag:
在这里插入图片描述
字典序最小的排序是:3 5 6 4 1 7 8 9 2
题目要求排序是: 6 4 1 3 9 2 5 7 8,因为要让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
cpp复制代码/* LittleFall : Hello! */
#include <bits/stdc++.h>
using namespace std; using ll = long long; inline int read();
const int M = 30016, MOD = 1000000007;

struct Node
{
int ind;
vector<int> edges;
}node[M];
vector<int> topo_sort(int n)
{
priority_queue<int> pq;
for(int i=1; i<=n; ++i)
{
if(node[i].ind==0)
pq.push(i);
}
vector<int> res;
while(!pq.empty())
{
int u=pq.top(); pq.pop();
res.push_back(u);
for(int v:node[u].edges)
{
if(--node[v].ind==0)
pq.push(v);
}
}
return res;
}
int main(void)
{
#ifdef _LITTLEFALL_
freopen("in.txt","r",stdin);
#endif

int T = read();
while(T--)
{
memset(node, 0, sizeof(node));
int n=read(), m=read();
while(m--)
{
int a=read(),b=read();
node[b].edges.push_back(a);
++node[a].ind;
}
vector<int> ord = topo_sort(n);
reverse(ord.begin(), ord.end());
for(int i=0;i<(int)ord.size();++i)
printf("%d%c",ord[i]," \n"[i==(int)ord.size()-1] );
}

return 0;
}


inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}

本文也发表于我的 csdn 博客中。

本文转载自: 掘金

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

Go语言入门之基础知识 写在前面👀 写在后面

发表于 2021-11-22

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

写在前面👀

今天主要聊聊Go的基础知识❣

一、声明与初始化

1.变量声明

  • 标准格式
    var <变量名> <数据类型>
1
go复制代码  var a int
  • 批量格式
    如果要声明的变量很多就可以用小括号( )进行批量声明
1
2
3
4
go复制代码  var( a int
b bool
c string
)

2.变量初始化与赋值

  • 声明变量的时候同时赋值
  • 语句*:var <变量名> <数据类型> = 值
1
go复制代码var a int =18
  • 先声明后赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main

import "fmt"

var (
a int
b bool //先声明变量
c string
)

func main() {
a = 18
b = true //后给变量赋值
c = "倔强的牛角"
fmt.Println(a, b, c)
}
  • 靠智能的编译器自动识别
  • 语句*:var <变量名> = 值 Go 编译器的智商已经高到可以根据变量的值来自动推断其类型,但也不是万能的,像设置一个具体的数据类型如int8,还得是用var a int8
1
2
3
4
5
go复制代码   var( 
a = 15
b = true
c = "倔强的牛角"
)
  • 简短变量声明
  • 语句*:<变量名> := 值 注意:简短声明语法 := 只能在函数体内使用,属于局部变量;像用 var 定义的变量一般都在函数体外,是全局变量,属于包级别。
1
go复制代码a := 18 //注意:等号左边的变量名必须是没有定义过的变量名,否则将会发生编译错误!

3.常量

常量使用关键字 const 定义,用于存储不会改变的数据,所以它能定义的数据类型比较少,只能是布尔型、数字型(整数型、浮点型和复数)和字符串型。

  • 标准定义格式(显式类型定义)
    const <变量名> <数据类型> = 值
1
go复制代码const name string = "倔强的牛角"
  • 隐式类型定义
    const <变量名> = 值

也是由智能的编译器自动判断厂里的数据类型,但同样不是万能的,想要定义一个具体的数据类型还得用标准定义格式。

1
go复制代码const name string = "倔强的牛角"
  • 常量的定义和初始化的方法与变量类似,但是常量不能用 := 语法声明。

4.匿名变量

顾名思义,一个没有名字的变量,它用下划线 _ 表示。匿名变量不占用命名空间,也不会占用内存,可以多次声明而不冲突。可以理解为工具人,今后我会在函数中讲他的实际用法。

二、基本数据类型

类型 长度(字节) 零值 说明
bool 1 false true flase
byte 1 0 uint8
rune 4 0 Unicode Code Point, int32
int, uint 4或8 0 32 或 64 位
int8, uint8 1 0 -128 ~ 127, 0 ~ 255,byte是uint8 的别名
int16, uint16 2 0 -32768 ~ 32767, 0 ~ 65535
int32, uint32 4 0 -21亿~ 21亿, 0 ~ 42亿,rune是int32 的别名
int64, uint64 8 0 3.4e38
float32 4 0.0
float64 8 0.0 1.8e308
complex64 8 复数
complex128 16 复数
uintptr 4或8 以存储指针的 uint32 或 uint64 整数
string “” UTF-8 字符串

三、类型转换

和C语言不同的是,Go语言中只有强制类型转换,没有隐式类型转换。
转换格式:T(表达式) T表示要转换的数据类型

写在后面

感谢观看✨

如有不足,欢迎指出💖

本文转载自: 掘金

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

二叉树刷题记(七-二叉树的右侧视图-层次遍历)

发表于 2021-11-22

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

前言

  • 大家好呀,小嘟我又写文章啦!(今天还蛮比较轻松,所以小嘟我就抓紧时间更新一篇),嘿嘿嘿,希望可以帮助读者哦!
  • 小嘟在这说两句:读者如果想刷题的话,请一定要坚持下去,不能保证一天一题的话,两天一题也可以呀!(这是小嘟为自己的懒惰找的借口,嘿嘿嘿)。在小嘟本人看来,做题这个东西,你一直写还好,隔很长时间不写,然后又想写,就有点困难了(因为你已经养成不写的习惯了,一个事情一旦养成习惯就很难再改变)。
  • 小嘟我对做题的一个看法:要学某一类数据结构,比如说数组、链表、二叉树,首先你得熟练掌握如何遍历这种数据结构,其次才能进阶,做中等,做复杂的难题。如果最基本的遍历都不会,那还做啥题,洗洗睡吧! (😂😂😂小嘟是开玩笑的,不会的话那就请继续加油吧!!!)
  • 聊天结束,开始进入今天的正题👍👍👍
  • 今天小嘟给大家带来的是另一种遍历方式,发现没,我们写的这几篇文章,都离不开一个词:遍历,所以说,我们一定要把遍历这个最基本的掌握好,小嘟和大家继续加油哦!

主题(Topic)

  • 一种新的遍历方式,层次遍历,有时也称广度优先搜索,提到广度,我们不免就想到了深度优先搜索,但这个不是今天的重点,小嘟就稍微提一下,深度优先搜索,就相当于一条路走到底,走不通了然后回溯再接着找另一条路,还是走到路的尽头。
  • 广度优先搜索(层次遍历)

image.png

看到这个图是不是好熟悉啊,哈哈哈,其实从第一篇题目开始,小嘟一直都用的是这个图。
层次遍历的思路:我们把在同一个水平上的结点当做同层(这里我用箭头线当做层级的基准线),我们要遍历下一层级,那么首先必须把本层的元素都遍历完,才能进行下一层级的遍历,这就是层序遍历的简单介绍。

正文

  • 今日题目(是个中等题哦,但是不要慌,嘿嘿嘿,本嘟在此,不惧)

image.png
image.png

这个题什么意思呢?答:你把这些结点当做葡萄(在同一个平面上的一串葡萄),我们给这些葡萄编上序号,然后站在图中箭头所在的一边,以箭头所指的方向看这些葡萄,能直接看到的葡萄结点就是我们要摘下来的。以示例为例,我们要摘下的葡萄序号为[1,3,4]。

题目分析:我们是站在右侧,然后要摘下我们能直接看到的结点,那么什么样的结点我们能直接看到?它有什么特点呢?想到这里,问题就解决了,小嘟替读者说了哦:那就是每层的最右边的结点被看到,每层?是不是就想到层次遍历了呢?,话不多说,看代码呗!

  • 层次遍历的代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码var rightSideView = function(root) {
let res = [];//要返回的数组
let thisFloor = [];//存放本层结点的数组
let nextFloor = [];//存放下一层结点的数组
if (root == null) return res;//如果树为空树,直接返回
thisFloor.push(root);
while(thisFloor.length != 0){//本层还有结点
let node = thisFloor.shift();//从数组的首部取出一个结点,
//并将该结点删除
if(node.left) nextFloor.push(node.left)//左结点有
if((node.right)) nextFloor.push(node.right)//右结点有
if(thisFloor.length == 0){//本层遍历完,开始重新赋值,更新
res.push(node.val);
thisFloor = nextFloor;
nextFloor = [];
}
}
return res;
};

image.png
代码的解释小嘟都详细的写在代码哪里啦,小嘟在这里简单提一下:首先我们是模拟的数据结构是队列(队列的特点是先进先出,先到达先处理),每次循环遍历的都是本层的结点,同时把该结点的下一层孩子结点也保存起来,等到遍历到本层结点的最后一个元素,将该结点返回,然后将下一层的结点又更新为本层结点,然后又接着找下一层的结点,直到下一层对的元素个数为空,循环退出。

结尾

  • 今天小嘟给大家留一个思考题,请看题:还是上边这个题的思想不变,这个时候我从左往右看,返回我直接能看到的元素集合,该怎么做呢?。
  • 若有问题,欢迎留言!!!,小嘟会尽自己最大的努力解决问题。
  • 明天更新关于深度优先搜索的题目。
  • 希望读者看完能有所收获!
  • 谢谢读者点赞点赞点赞啦!
  • 小嘟溜啦溜啦...

本文转载自: 掘金

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

MySQL图解JOIN算法,超简单,很详细

发表于 2021-11-22

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

前言

在上一篇中,花哥和大家分享了JOIN的初步优化,以及两种对应的算法 Index Nested-Loop Join(NLJ) 和 Block Nested-Loop Join(BNL)。读完上篇的小伙伴应该都知道,BNL算法由于查询过程中比较次数较大(两种表数量乘积),非常消耗内存,很明显是有优化空间的,而NLJ算法其实效率还是不错的,是否可以继续优化呢?

本文就以下几个知识点展开讲解:

  1. 什么是回表
  2. MRR算法是什么
  3. BKA算法是什么
  4. BNL如何优化
  5. NLJ算法优化

回表是什么

这里需要了解一个知识点,什么是回表?这里大致解释一下MySQL根据辅助索引查询数据时的过程:

– a是辅助索引,t表中有字段id,a,b

select * from t where a = 50

image.png

  1. 根据辅助索引 a ,获取到值等于5的数据,因为辅助索引只会存储主键,因此可以获取主键ID=500;
  2. 因为我们sql中需要的结果还包含b字段,所以我们还需要继续第三步;
  3. 遍历右边聚簇索引的B+树,拿到最终数据。

上述这个过程就叫做 回表,回表最大的弊端是随机访问,比如我们进行一段范围查找 select id,a,b from t where a > 10 and a< 100 ,在实际需求中,我们拿到的主键ID并不一定就是递增的,那在回表的过程中就会产生大量的随机访问,极大的影响性能。

如何解决这种情况呢,这里就引入了MRR算法。

MRR算法认识

MRR算法的全称是Multi-Range-Read,它是MySQL 5.6引入的新特性,其目的是为了减少磁盘的随机访问,尽量使用顺序读盘。

上面不是说到回表可能导致大量的随机访问,从而影响性能嘛,MRR的做法简单理解就是先给你ID在内存中先排序,保证ID是递增的,然后再去查询,这样就可以尽可能保证,在读取磁盘时是顺序读取,从而提高性能。

比如上面例子中的范围查询a在区间(10,100)的值,可以分解为下面几个步骤:

image.png

  1. 根据辅助索引a,查询区间(10,100)的所有id,并放入到内存 read_rnd_buffer中;
  2. 在内存中,将id递增排序;
  3. 根据顺序,到主键索引中查询数据,并返回结果集。

read_rnd_buffer:由read_rnd_buffer_size 变量定义的内存大小中,默认256K

上面就是MRR的执行过程,它的优势是针对范围查找的语句,可以将大量的主键id排序后,能够保证读取主键索引时是顺序读写,从而提高性能。

如何开启MRR

我们可以通过设置参数来开启MRR

1
ini复制代码set optimizer_switch="mrr_cost_based=off"

通过执行计划,在Extra中就可以看到,我们已经开启了MRR。

image.png

NLJ算法优化

MySQL 5.6引入了Batched Key Access(简称BKA),它是对NLJ算法的一种优化,NLJ算法在上一章我们就已经知道其内部原理了,这里就不再赘述,小伙伴们可以回顾上一篇文章查看。

  • NLJ算法

image-20211119152513214.png

NLJ算法其实效率还是不错的,但是该算法是通过单值来匹配获取结果,那我们能不能同时传递多个值给t2表来查询呢,如今我们学习完上面的MRR,小伙伴们是不是也有一个想法,可以通过MRR的思想对NLJ进行优化呢?

其实BKA确实是基于MRR算法的,观察下面这幅图,查询的时候把驱动表的数据取出部分放入到join_buffer,如果出现join_buffer放不下,就会进行分段策略,然后再执行MRR算法。

image.png

如何开启BKA

我们可以通过设置参数来开启BAK,前面两个参数用于设置MRR,因为BAK依赖MRR

1
ini复制代码set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

BNL 算法优化

上一章我们学完了BNL算法,也知道该算法的缺点,总结起来可归结以下几点:

  1. join过程中需要M*N(M、N为两张表行数)次的对比次数,对于大表这个是相当可怕的。
  2. 使用 join 语句多次扫描一个冷表,并且这个语句执行时间超过 1 秒,就会在再次扫描冷表的时候,把冷表的数据页移到 LRU 链表头部,从而造成Buffer Pool的热数据被淘汰,影响内存命中率,这段内容后续章节会详细讲解,这里简单了解即可。

对于上面出现的问题,我们最简单的方法就是在被驱动表上新建索引,但是这种方式并不是对所有情况都适合,比如我们例子中,被驱动表中有千万条数据,同时查询的sql又是低频sql,直接添加索引就非常浪费。

还有一种方式我们可以添加一个临时表,大致过程如下:

  1. 新建临时表temp;
  2. 将满足条件的数据插入到新表中;
  3. 将链式表temp添加索引;
  4. 使用驱动表和临时表temp进行join操作

整体看来,上面的做法目的都是为了能够使用索引,从而触发BAK算法,用以提升性能。

总结

通过本文,总结起来有以下几点:

  1. 尽量使用BKA算法;
  2. BNL算法效率最低,可以通过添加被驱动表索引来转换为BKA算法;
  3. 根据实际需求,可以考虑临时表方案。

本文转载自: 掘金

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

同事说,组合索引加上,但是没生效

发表于 2021-11-22

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

写在前面

最近可能和Mysql系列杠上了,同事又来问我了。正好我也学习下,我一直坚信一句话,你教别人的同时 自己也能从中收获到以前没有认知到的东西。感谢同事 感谢努力,感谢现在正在看文章的你。

以前我一直有个认知,组合索引必须按照顺序,否则会让索引失效。

所以当同事问我的时候,我毫不犹豫的说,请检查你组合索引的顺序。哈哈 翻车了

组合索引匹配规则

直接进入正题,给下面该表添加上组合索引

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码CREATE TABLE `t_book` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`cover` varchar(255) DEFAULT '',
`title` varchar(255) NOT NULL DEFAULT '',
`author` varchar(255) DEFAULT '',
`date` varchar(20) DEFAULT '',
`press` varchar(255) DEFAULT '',
`abs` varchar(255) DEFAULT NULL,
`cid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_title_author_press` (`title`,`author`,`press`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=107 DEFAULT CHARSET=utf8;

我们添加了一个index_title_author_press 组合索引,将title(书名),author(作者),press(出版社) 组合在一起。

1 、接着我们explain一下,全值匹配的情况

1
2
java复制代码explain select * from t_book where title = '且在人间' 
and author = '余秀华' and press= '湖南文艺出版社';

image.png

可以看到 正常走索引了

2、接着 我们换下顺序,验证下上面的我一直认为换顺序会影响索引的想法

1
2
java复制代码explain select * from t_book where title = '且在人间' and press= '湖南文艺出版社'
and author = '余秀华' ;

image.png

可以看到 无论怎么变换顺序,都是正常走索引的。where后的顺序会在Mysql优化器中进行重组,不会失效

3、那么我们考虑下如果缺失组合索引中其中一个字段呢 ?

1
java复制代码explain select * from t_book where title = '且在人间' and press= '湖南文艺出版社'

结果如下图。虽然passible_keys 和 key都有值 但是大家看下Extra

image.png

Using index condition 这个什么意思,只是表示:用到了索引作为查询条件。查找使用了索引,但是需要回表查询数据,后面我会针对这个详细讲下,

总之一句话 Using index condition需要回表查询数据,所以当你组合索引中缺少一个字段。效率会大大降低,这也是同事遇到的问题

最左匹配

上面我们说到组合索引中字段缺少一个便会影响索引,那么下面大家看这种情况,还记得上面我们的组合索引顺序依次是KEY index_title_author_press (title,author,press)

那么我们依照最左匹配法试下

1
2
3
java复制代码explain select * from t_book where title = '且在人间' and author = '余秀华' 

explain select * from t_book where title = '且在人间'

image.png

看到没,依照最左匹配法则,依然正常走索引,不会影响效率。

接下来我们看like匹配是什么情况

模糊匹配

1
2
3
java复制代码1)explain select * from t_book where title like '且在%'

2)explain select * from t_book where title like '%人间'

上面哪种会索引失效,毋庸置疑第二种会直接索引失效。但是第一种会走索引,但是和上面缺字段的那个情况是一样的,会出现Using index condition,需要回查表,所以总之建议,索引不要进行like匹配

OK 今天关于组合索引就说这么多,当然还有注意点和优化点。我这里就不一一赘述了,后面遇到我们再一起学习下

下面来进行几点重要的总结

总结

  • 不要在索引上进行任何计算
  • 不在索引列上做任何操作(计算、函数、(自动 or 手动)类型转换),会导致索引失效而转向全表扫描(机制)
  • 尽量使用主键查询,而不是其他索引,因此主键查询不会触发回表查询
  • 组合索引遵循最左匹配
  • 最后最重要的是,创建索引的列,不允许为null,可能会得到不符合预期的结果

OK 我们下期再见。一起努力

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作

本文转载自: 掘金

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

1…227228229…956

开发者博客

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