深入学习 Redis Cluster - 基于 Docker、DockerCompose 搭建 Redis 集群,处理故障、扩容方案

news2024/12/23 23:14:41

目录

一、基于 Docker、DockerCompose 搭建 Redis 集群

1.1、前言

1.2、编写 shell 脚本

1.3、执行 shell 脚本,创建集群配置文件

1.4、编写 docker-compose.yml 文件

1.5、启动容器

1.6、构建集群

1.7、使用集群

1.8、如果集群中,有节点挂了,怎么办?

二、集群故障、扩容处理

2.1、集群故障处理

a)故障判定

b)故障迁移

2.2、集群宕机

2.3、集群扩容

a)分析

b)将新的主节点 110 加入到集群中

c)重新分配 slots

问题:如果在搬运 slots / key 的过程中,客户端能否访问 redis 集群呢?


一、基于 Docker、DockerCompose 搭建 Redis 集群


1.1、前言

当前阶段,由于我只有一个 云服务器,搞分布式系统就比较麻烦,而实际工作中,一般是通过多个主机的方式来搭建集群的.

因此这里我会 基于 docker、docker-compose(容器编排) 来搭建 redis 集群.

Ps:搭建前,一定要把之前启动的 redis 容器停止.

1.2、编写 shell 脚本

在 linux 上,以 .sh 为后缀的文件称为 “shell 脚本” ,通过这个文件,我们就可以把平时在 linux 上执行的指令,批量化执行,同时,还能加入 条件、循环、函数等机制.

这里我们创建 11 个 redis 节点. 这些 redis 的配置文件内容,大同小异,因此这里使用 脚本来批量生成(也可以不使用脚本,手动一个一个改).

for port in $(seq 1 9); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.10${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done

# 注意 cluster-announce-ip 的值有变化,和上面分开写也是因为这个原因

for port in $(seq 10 11); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.1${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done

for port in $(seq 1 9):表示一个循环,seq 是一个 linux 命令,生成 [1, 9] 数字,依次赋值给变量 port.

do、done:在 shell 中 { } 用来表示变量,不是代码块,对于 for, 就是使用 do 和 done 来表示 代码块 开始 和 结束 的(上古时期的编程语言就是这样的).

\:表示续航符,就是把下一行的内容和当前行,合并成一行.  shell 默认情况下,要求把所有代码都写到一行里,但是可以使用续航符来换行继续补充.

第一个循环体的内容(第二个循环体思想也一样):通过 mkdir 创建 9 个名字分别为 redis1、redis2、redis3......的文件夹,接着,通过 touch redis${port} 在每个文件夹下,创建 redis.conf 文件. 文件的内容(从 EOF 开始,到 EOF 结束)通过  cat  写入到每个 redis.conf 文件中.

字符串拼接:shell 中拼接字符串是直接写在一起的,不需要使用 +.

cluster-enabled yes:开启集群

cluster-config-file:后续启动节点后,自动生成的节点配置文件,会配置一些 redis 集群信息.

cluster-node-timeout 5000:心跳包的超时时间设置为 5000 ms.

cluster-announce-ip:表示当前 redis 节点所在的主机 ip 地址(当前是使用 docker 容器模拟的主机,此处因该是 docker 容器的 ip).

cluster-announce-port:表示当前 redis 节点自身绑定的端口(容器内的端口).  不同容器内部可以有相同端口,后续进行端口映射,再把容器外的端口不同端口映射到容器内的端口即可.

cluster-announce-bus-port:一个服务器可以绑定多个端口号,当前这个表示管理端口(刚刚上面讲的是业务端口,是用来进行业务数据通信的),用来完成一些管理以上任务进行通信的(如果某个 分片 中的 redis 主节点挂了,就需要让从节点成为主节点,就需要通过 管理端口 来完成).

1.3、执行 shell 脚本,创建集群配置文件

通过以下命令执行 shell 脚本

centos 执行 shell 脚本命令:

sh generate.sh

ubuntu 执行 shell 脚本命令:

bash generate.sh

执行后,会得到 11 个目录,每个目录里都有一个配置文件,配置文件中,ip 地址各不相同

1.4、编写 docker-compose.yml 文件

在配置文件中,需要先手动创建 networks 网络,然后分配 网段 给后续创建 redis 集群中每一个节点的静态ip.

Ps:这里配置静态 ip(固定 ip)是为了后续观察.

version: '3.3'
networks:
  mynet:
    ipam:
      config:
        - subnet: 172.30.0.0/24

接着创建 redis 集群中每一个节点

services:
  redis1:
    image: 'redis:5.0.9'
    container_name: redis1
    restart: always
    volumes:
      - ./redis1/:/etc/redis/
    ports:
      - 6371:6379
      - 16371:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.101

  redis2:
    image: 'redis:5.0.9'
    container_name: redis2
    restart: always
    volumes:
      - ./redis2/:/etc/redis/
    ports:
      - 6372:6379
      - 16372:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.102

  redis3:
    image: 'redis:5.0.9'
    container_name: redis3
    restart: always
    volumes:
      - ./redis3/:/etc/redis/
    ports:
      - 6373:6379
      - 16373:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.103

  redis4:
    image: 'redis:5.0.9'
    container_name: redis4
    restart: always
    volumes:
      - ./redis4/:/etc/redis/
    ports:
      - 6374:6379
      - 16374:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.104

  redis5:
    image: 'redis:5.0.9'
    container_name: redis5
    restart: always
    volumes:
      - ./redis5/:/etc/redis/
    ports:
      - 6375:6379
      - 16375:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.105

  redis6:
    image: 'redis:5.0.9'
    container_name: redis6
    restart: always
    volumes:
      - ./redis6/:/etc/redis/
    ports:
      - 6376:6379
      - 16376:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.106

  redis7:
    image: 'redis:5.0.9'
    container_name: redis7
    restart: always
    volumes:
      - ./redis7/:/etc/redis/
    ports:
      - 6377:6379
      - 16377:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.107

  redis8:
    image: 'redis:5.0.9'
    container_name: redis8
    restart: always
    volumes:
      - ./redis8/:/etc/redis/
    ports:
      - 6378:6379
      - 16378:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.108
 
  redis9:
    image: 'redis:5.0.9'
    container_name: redis9
    restart: always
    volumes:
      - ./redis9/:/etc/redis/
    ports:
      - 6379:6379
      - 16379:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.109

  redis10:
    image: 'redis:5.0.9'
    container_name: redis10
    restart: always
    volumes:
      - ./redis10/:/etc/redis/
    ports:
      - 6380:6379
      - 16380:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.110
 
  redis11:
    image: 'redis:5.0.9'
    container_name: redis11
    restart: always
    volumes:
      - ./redis11/:/etc/redis/
    ports:
      - 6381:6379
      - 16381:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.111

- subnet: 172.30.0.0/24:此处 172.30.0 是网络号,并且 ip 是内网 ip,这就要求 不能和你当前主机上现有的其他网段冲突(每个人主机上已有的网段,具体不一定一样).

ipv4_address: 172.30.0.101:此处配置静态 ip。注意网路号部分要和前面的网一致,主机号部分可以在(1~255 之间随意配置,不重复就行),但是这里我们需要按照之前在配置文件中写的 cluster-announce-ip 要对应的上,如下图

1.5、启动容器

通过 docker-compose up -d 启动 yml 中配置的所有容器.

1.6、构建集群

此处是把前 9 个主机构建成集群, 3 主 6 从. 后 2 个主机暂时不⽤.

通过以下命令构建即可

redis-cli --cluster create 172.30.0.101:6379 172.30.0.102:6379 172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379 172.30.0.107:6379 172.30.0.108:6379 172.30.0.109:6379  --cluster-replicas 2

--cluster create:表⽰建⽴集群. 后⾯填写每个节点的 ip 和地址(确保这个命令的 IP 和实际环境一致).

--cluster-replicas 2: 表⽰每个主节点需要两个从节点备份.  这个配置设置了以后,redis 就知道 3 个节点是一伙的(一个分片上的),一共 9 个节点,一共是 3 个分片.

输入 yes 后,如下

1.7、使用集群

现在从 101 - 109 九个节点,是一个集群,使用客户端端连上任意一个节点,本质上都是等价的(每个集群分别存储 “全集” 数据中的一部分,连接任意一个客户端时加上 -c 选项,都可以访问到全集数据).

建立号集群后,连接客户端,可以通过 -h -p 连接、可以通过 -h 连接,也可以直接通过 -p 连接对外端口,如下(以下都是连接 172.30.0.103:6379 ):

通过 cluster nodes 查看当前集群的信息.

在集群中存储数据

上图 error 的原因是 k1 这个 key 通过 hash 计算后,得到 slot 是 12706, 这个槽位号在刚刚查看的集群信息中是属于 3 号分片的.

 报错信息中提示我们,要把客户端请求转发给 103 这个节点.

这样岂不是很麻烦?

实际上,我们可以在启动 redis-cli 的时候,加上 -c 选项,此时 redis 客户端就会根据当前 key 计算出的槽位号,自动找到匹配的分片主机,进一步完成操作.

注意上图,重定向后,redis 连接的客户端也会改变.

另外,如果尝试在从节点上写操作,也会自动重定向到指定的主节点上.

Ps:实际上,以前所讲到的 redis 相关命令,基本上都是都是适用的(除了个别,例如 mset,mget...... 这种可以操作多个 key 的,是不可用的,因为 key 可能都是分散在不同分片上的).

1.8、如果集群中,有节点挂了,怎么办?

如果挂了的节点是从节点?没事~

如果挂了的事主节点?写操作就不能进行了!此时集群做的工作,和 哨兵 做的有点类似了,会自动从主节点下的从节点挑一个出来,提拔成主节点.

这里我让 redis1 这个主节点挂掉

docker stop redis1

redis1 挂掉前,集群信息如下:

redis1 挂掉后,集群信息如下:

可以看出,集群机制,也能处理故障转移.

二、集群故障、扩容处理


2.1、集群故障处理

a)故障判定

1. 每个节点,每秒钟,都会给一些随机的节点发送 ping 包(这里既包含了集群的配置信息,如 id、属于哪个分片、是主节点还是从节点、持有哪些 slots......),收到的节点会返回一个 pong 包.  这里的 ping 包,不是全发一遍,这样设定是为了避免在节点很多的时候,心跳包也是非常多的,严重消耗网络带宽.

2. 当节点 A 给节点 B 发送 ping 包, B 不能如期回应的时候,A 就会重置和 B 的 TCP 连接,如果重连失败,A 就会把 B 设为 PFAIL (相当于主观下线)。

3. A 判定 B PFAIL 后,会通过 redis 内置的 Gossip 协议,和其他节点沟通,向其他节点确认 B 的状态.

4. 如果 A 发现其他还跟多节点也认为 B 为 PFAIL,并且数目超过集群个数的一般,那么 A 就会把 B 标记为 FAIL(相当于客观下线),并且把这个消息同步给其他节点,让其他节点也把 B 标记为 FAIL.

b)故障迁移

首先会有个判定:

  • 如果 B 是从节点,就不需要进行故障迁移.
  • 如果 B 是主节点,那么就会由 B 的从节点(比如 C 和 D),触发故障迁移.

具体的:

1. 从节点判定自己是否具有参选资格,如果从节点太久没和主节点通信(太久没有同步数据,差异太大),就会失去竞选资格.

2. 有资格的节点,比如 C 和 D ,就会先休眠一定的时间,休眠时间 = 500ms 基础时间 + [0, 500ms] 随机时间 + 排名 * 1000ms,offset 值越大(表明数据越接近主节点),排名越靠前(休眠时间越短),也就是说,休眠时间主要取决于排名.

3. 此时如果 C 的休眠时间到了,C 就会给所有集群中的节点,进行拉票操作,但是只有主节点才有投票资格.(谁休眠时间短,大概率就是新的主节点了).

4. 主节点会把自己的票投给 C(每个主节点只有 1 票),当 C 收到的票数超过主节点数目的一半,C 就会晋升成为主节点(C 自己执行 slaveof no one,并且让 D 执行 slaveof C).

5. 同时,C 还会把自己成为主节点的消息,同步给其他集群的节点,大家都会更新自己保存的集群结构信息.

6. 最后,如果之前宕机的主机点恢复了,就会变成从节点接入到集群中.

2.2、集群宕机

以下三种情况会出现集群宕机:

某个分片,所有的主节点和从节点都挂了,这个时候分片就无法提供数据服务了.

某个分片,主节点挂了,但是没有从节点,也无法提供数据服务了.

超过半数的 master 节点挂了,说明集群遇到了非常严重的情况,就得感觉停止下来,检查看是不是有什么问题!

Ps:如果集群中有一个节点挂了,无论是什么节点,我们程序员都因该尽快处理好(最晚,也是第二天上班之前处理好).

2.3、集群扩容

a)分析

上述操作,已经将 101 ~ 109  9 个主机,构成了 3 主, 6 从结构的集群了.

接下来为了演示扩容,就把 110 和 111 也加入到集群中.

以 110 为 master ,111 为 slave,把数据分片从 3 -> 4.

b)将新的主节点 110 加入到集群中

redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379

add-node:第一个 ip 和 端口号 表示新增的节点是什么,第二个 ip 和端口号 表示集群上任意一个节点(任何一个都行,只要是你想加入的某一个集群中的节点即可),表示要把新节点加入到哪个集群中.

之后通过 cluster nodes 就可以看到 redis10 主节点加入集群,但是并没有分配 slots.

c)重新分配 slots

把之前的三组 master 上面的 slots 拎出来,分配给新的 master 

redis-cli --cluster reshard 172.30.0.101:6379

输入命令后,会先打印出当前集群每个机器的情况,然后要求用户输入要切分多少个 slots

4 个分片,一共是 16384,除以 4 得到的就是 4096,因此这里填 4096 即可(给 redis10 切分出 4096 个槽位号).

紧接着,会询问让哪个节点来接收,直接粘贴 redis10 这个主机的 id 即可.

接着,就让你选择从哪些节点且分出 slots:

  1. all:表示从其他每个持有 slots 的 master 都点过来.
  2. 手动指定,从某一个或者某几个节点来移动 slots(以 done 为结尾).

输入 all 之后,不会真正的搬运,而是先给出搬运的计划.

当输入 yes 之后,搬运真正开始,此时不仅仅是 slots 重新划分,也会把 slots 上对应的数据,也搬运到新的主机上.(这是比较重量级的操作)

d)给新的主节点添加从节点

redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave

 

执⾏完毕后, 从节点就已经被添加完成了.

 

问题:如果在搬运 slots / key 的过程中,客户端能否访问 redis 集群呢?

之前咱们了解到哈希槽分区算法,可以知道 大部的 key 是不用搬运的 ,针对这些未搬运的 key,此时可以正常访问的. 针对正在搬运中的 key,是有可能会出现访问出错的情况.

假设 客户端 访问 k1,集群通过分片算法得到的 k1 是第一个分片的数据,就会重定向到第一个分片的节点,那么就有一种可能,在重定向过去之后,正好 k1 被搬走了,自然就无法访问了.

如果像针对生产环境进行扩容操作,还是得悠着点,比如找个夜深人静的时候,没啥客户端访问集群的时候,进行扩容,就可以把损失降到最低.

很明显,要想追求更高的可用性,让扩容对于用户影响更小,就需要搞一组新的机器,重新搭建集群,并且把数据导进来,使用新集群代替旧集群(但是成本是最高的).

Ps:关于集群的缩容,就是把一些节点拿掉,减少分片的数量.

不过一般都是进行扩容,很少缩容.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1018543.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于springboot+vue的养老院管理系统 前后端分离项目

养老院管理系统 前后端分离项目 后端技术&#xff1a;springbootmybatis-plusredismysql 前端&#xff1a;vue3.0elementui-plus 【人员管理】 用户管理 客户管理【药品食品管理】 药品管理 食品管理【报修管理】【外出管理】外出管理 访客管理【留言管理】【新闻管理】新闻…

服务器杀掉死进程

清掉服务器上的死进程 查看服务器使用情况 nvidia-smi发现并没有显示进程&#xff0c;那应该是有死进程。 查看占用情况 ps aux会显示如上图的进程列表。 PID进程号STAT状态(S休眠R运行Z死掉了)TIME占用时间(过长的有问题)COMMAND进程启动命令(应该能帮助回忆起这是什么程…

IDEA——工程项目的两种窗口开发模式

文章目录 引言一、多项目窗口模式的便利1.1 源码 debug 二、多项目窗口模式的弊端三、多项目窗口的版本管理四、单项目、多项目窗口模式转换 引言 idea编辑器有两种窗口模式&#xff0c;一种是单项目窗口&#xff0c;另一种是多项目窗口。 我个人使用较多的是单项目窗口&#…

指针和数组笔试题讲解(3)

&#x1f435;本篇文章将对指针相关笔试题进行讲解&#xff0c;同时也是指针和数组笔试题讲解的最后一篇文章&#xff0c;那么接下来将会对8道笔试题进行逐一讲解 笔试题1&#x1f4bb; int main() {int a[5] { 1, 2, 3, 4, 5 };int* ptr (int*)(&a 1);printf("%d…

C#调用C++ dll 返回数组

先看一下C语言函数返回数组的问题&#xff1b; 先看一个错误的示范&#xff1b; 因为 a 是局部变量&#xff0c;只存在函数 function() 中&#xff0c;返回给main中的b是错误的&#xff1b; 函数返回数组的一种写法如下&#xff1b; #include<stdio.h> int function(in…

二维凸包(Graham) 模板 + 详解

&#xff08;闲话&#xff09; 上了大学后没怎么搞oi&#xff0c;从土木跑路到通信了&#xff08;提桶开润大成功&#xff01;&#xff09;&#xff0c;但是一年上两年的课&#xff08;补的&#xff09;&#xff0c;保研也寄掉了&#xff08; 说起来自从博客被大学同学发现并…

地牢大师问题(bfs提高训练 + 免去边界处理的特殊方法)

地牢大师问题 文章目录 地牢大师问题前言题目描述题目分析输入处理移动方式【和二维的对比】边界判断问题的解决 代码总结 前言 在之前的博客里面&#xff0c;我们介绍了bfs 基础算法的模版和应用,这里我们再挑战一下自己&#xff0c;尝试一个更高水平的题目&#xff0c;加深一…

vue2——电商项目 黑马

创建项目 初始化 router app.vue vant 组件库 Viewport 布局 vw适配 路由配置 底部导航组件 二级路由配置 登录页面 新建默认样式 main.js 引入commonless 登录静态页面—头部组件NavBar 导入navbar 引用 axios封装 图形验证码 获取 get 渲染 api接口模块 toast轻提示 使用 …

Flutter图标

https://fluttericon.cn/ Flutter 内置了丰富的图标。 Icon(Icons.ac_unit)

智能批量重命名,轻松删除文件名后缀数字并添加编号!

亲爱的用户们&#xff0c;您是否曾经为繁琐而重复的文件重命名工作而感到头疼&#xff1f;现在&#xff0c;我们为您提供一款智能化的工具&#xff0c;让文件重命名变得如此简单&#xff01; 首先&#xff0c;我们要进入文件批量改名高手&#xff0c;并在板块栏里选择“文件批…

overleaf 插入图片,引用图片,图标标题Fig与文章引用Figure不一致解决

目录 1.一般插图 2.插入双栏图片 3 插入子图 4. 引用出现问题 问题1 &#xff1a; pdf 文中引用只出现了图片序号&#xff0c;如“3”。没有出现“Fig.3 或者Figure.3” 问题2&#xff1a;文中引用的标题和图片下面的标题不一致 1 首先&#xff0c;在导言区添加以下行…

七天学会C语言-第二天(数据结构)

1. If 语句&#xff1a; If 语句是一种条件语句&#xff0c;用于根据条件的真假执行不同的代码块。它的基本形式如下&#xff1a; if (条件) {// 条件为真时执行的代码 } else {// 条件为假时执行的代码 }写一个基础的If语句 #include<stdio.h> int main(){int x 10;…

HarmonyOS开发环境搭建

一 鸿蒙简介&#xff1a; 1.1 HarmonyOS是华为自研的一款分布式操作系统&#xff0c;兼容Android&#xff0c;但又区别Android&#xff0c;不仅仅定位与手机系统。更侧重于万物物联和智能终端&#xff0c;目前已更新到4.0版本。 1.2 HarmonyOS软件编程语言是ArkTS&#xff0c…

STM32DMA原理和应用

目录 1.什么是DMA 2.DMA的意义 3.DMA搬运的数据和方式 4.DMA 控制器和通道 5.DMA通道的优先级 6.DMA传输方式 7.DMA应用 实验一: 内存到内存搬运 CubeMX配置&#xff1a; ​编辑用到的库函数&#xff1a; 代码实现思路&#xff1a; 实验二: 内存到外设搬运 CubeMX…

简单返回封装实体类(RespBean)

RespBean的作用 返回状态码&#xff0c;返回信息&#xff0c;返回数据 package com.example.entity;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;Data AllArgsConstructor NoArgsConstructor public class RespBean {private lon…

基于springboot实现的极验校验

概述 在系统业务中&#xff0c;需要想客户发送手机验证码&#xff0c;进行验证后&#xff0c;才能提交。但为了防止不正当的短信发送&#xff08;攻击&#xff0c;恶意操作等&#xff09;&#xff0c;需要在发送短信前添加一个行为验证&#xff08;这里使用的是极验&#xff0…

利用Python将dataframe格式的所有列的数据类型转换为分类数据类型

一、样例理解 import pandas as pd import numpy as np# 创建测试数据 feature_names [col1 , col2, col3, col4, col5, col6] values np.random.randint(20, size(10,6))dataset pd.DataFrame(data values, columns feature_names)print("转换前的数据为\n",d…

【C进阶】指针和数组笔试题解析

做题之前我们先来回顾一下 对于数组名的理解&#xff1a;除了以下两种情况&#xff0c;数组名表示的都是数组首元素的地址 &#xff08;1&#xff09;sizeof&#xff08;数组名&#xff09;&#xff1a;这里的数组名表示整个数组 &#xff08;2&#xff09;&&#xff08;数…

Maven3.6.1下载和详细配置

1.下载maven 说明&#xff1a;以下载maven3.6.1为例 1.1网址 Maven – Welcome to Apache Maven 1.2点击下载 1.3点击Maven 3 archives 1.4 点击相应的版本 1.5 点击binaries下载 说明&#xff1a;binaries是二进制的意思 1.6点击zip格式 1.7 蓝奏云获取 说明&#xff1a…

C语言——自定义类型结构体_学习笔记

结构体的基本概念 结构体是一种用户自定义的数据类型&#xff0c;可以包含多个不同类型的变量。通过使用结构体&#xff0c;我们可以将相关联的数据组织在一起&#xff0c;便于管理和使用。 结构体的声明 正常的结构体声明 在C语言中&#xff0c;结构体(struct)指的是一种数…