在学习前必须将docker安装好,可以参考我博客里Docker安装教程,执行查找
FastDFS是一款开源的轻量级分布式文件系统纯C实现,支持Linux、FreeBSD等UNIX系统类google FS,不是通用的文件系统,只能通过专有API访问,目前提供了C、Java和PHP API为互联网应用量身定做,解决大容量文件存储问题,追求高性能和高扩展性FastDFS可以看做是基于文件的key value pair存储系统,称作分布式文件存储服务更为合适。
tracker-server:
跟踪服务器, 主要做调度工作, 起负载均衡的作用。 在内存中记录集群中所有存储组和存储服务器的状态信息, 是客户端和数据服务器交互的枢纽。 相比GFS中的master更为精简, 不记录文件索引信息, 占用的内存量很少。
storage-server:
存储服务器( 又称:存储节点或数据服务器) , 文件和文件属性( metadata) 都保存到存储服务器上。 Storage server直接利用OS的文件系统调用管理文件。
group:
组, 也可称为卷。 同组内服务器上的文件是完全相同的 ,同一组内的storage server之间是对等的, 文件上传、 删除等操作可以在任意一台storage server上进行 。
meta data:
meta data:文件相关属性,键值对( Key Value Pair) 方式,如:width=1024,heigth=768 。
文件访问路径:
组名:文件上传后所在的 storage 组名称,在文件上传成功后有storage 服务器返回,需要客户端自行保存。 (group1)
虚拟磁盘路径:storage 配置的虚拟路径,与磁盘选项store_path/*对应。 默认store_path0 则是 M00
数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件。(02/44) 这个一般好像是(00/00)
文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储 服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
以上所有信息结合起来就是文件的访问路径
单机文件和分布式文件系统的对比
单机文件系统: 低,依赖于单机服务器,只要服务器崩溃,完全不可用。 低,要扩容只能停机增加硬盘。 当文件数量多到一定的程度,磁盘IO寻址操作将会成为瓶颈,好处就是配置快
分布式文件系统: 一个group内的服务器崩溃后,group内的其他storage将接管服务。 可以不停机增加group机器 ,部署较复杂 通过集群或者分布式的方式分担服务器的压力。
适用场景:
特别适合以中小文件( 建议范围: 4KB 到 500MB ) 为载体的在线服务, 如相册网站、 视频网站等等。
部署结构
最小化部署图
注意:为了做到高可用,一个group建议分为两台以上的机器。
为了简化FastDFS的搭建我们使用dokcer容器来搭建
需要准备Centos7并安装Docker 我的ip是 192.168.66.67
准备工作
安装FastDFS镜像
docker pull morunchang/fastdfs
可以通过docker images查看是否拉取下来
创建 tracker 容器
docker run -d --name tracker --restart=always --network=host morunchang/fastdfs sh tracker.sh
创建 storage 容器
docker run -d --name storage --restart=always --network=host -e TRACKER_IP=192.168.66.67:22122 -e GROUP_NAME=group1 morunchang/fastdfs sh storage.sh
默认访问端口是8080 如果不想使用8080 端口访问那么进入 storage容器里吧 nginx 的端口号监听 改了就行
docker exec -it storage bash
修改 nginx
vi etc/nginx/conf/nginx.conf
listen 8021; #设置监听端口
location ~ /M00 {
add_header Cache-Control no-store; #关闭nginx缓存 否则数据不同步
root /data/fast_data/data;
ngx_fastdfs_module;
}
然后 重启nginx
/etc/nginx/sbin/nginx -c /etc/nginx/conf/nginx.conf
/etc/nginx/sbin/nginx -s reload
一直使用 直到出现下面这种情况 ok
nginx: [emerg] bind() to 0.0.0.0:8021 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:8021 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:8021 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:8021 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:8021 fai
使用Springboot 操作FastDFS
1.创建Maven项目
需要的Maven依赖
<dependencies>
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>provided</scope>
</dependency>
</dependencies>
配置文件fdfs_client.conf
connect_timeout=60
network_timeout=60
charset=UTF-8
#tracker访问nginx的反向代理端口号
http.tracker_http_port=8021
tracker_server=192.168.66.67:22122
测试文件FastdfsClientTest
package com.fastdfs;
import org.csource.common.MyException;
import org.csource.fastdfs.*;
import org.junit.Before;
import org.junit.Test;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
public class FastdfsClientTest {
StorageClient storageClient;
TrackerClient trackerClient;
TrackerServer trackerServer;
@Before
public void Init() throws IOException, MyException {
//加载全局的配置文件
ClientGlobal.init("fdfs_client.conf");
//创建TrackerClient客户端对象
trackerClient = new TrackerClient();
//通过TrackerClient对象获取TrackerServer信息
trackerServer = trackerClient.getConnection();
//获取StorageClient对象
storageClient = new StorageClient(trackerServer, null);
}
//文件路径
public String fileName="M00/00/00/wKhCR1_LGd6ACMUBAABdrZgsqUU452_big.jpg";
//组
public String group="group1";
/** * 文件上传 * * @throws Exception */
@Test
public void upload() throws Exception {
//获取StorageClient对象
StorageClient storageClient = new StorageClient(trackerServer, null);
//执行文件上传 自己修改要上传文件的 路径
String[] jpgs = storageClient.upload_file("C:\\Users\\12841\\Pictures\\test.jpg", "jpg", null);
for (String jpg : jpgs) {
System.out.println(jpg);
}
}
//删除 文件
@Test
public void delete() throws Exception {
//获取StorageClient对象
StorageClient storageClient = new StorageClient(trackerServer, null);
//执行文件删除
int group1 = storageClient.delete_file(group, fileName);
System.out.println(group1);
}
//下载 文件
@Test
public void download() throws Exception {
//执行文件上传
byte[] bytes = storageClient.download_file(group, fileName);
File file = new File("D:\\1234.jpg");
FileOutputStream fileOutputStream = new FileOutputStream(file);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
bufferedOutputStream.write(bytes);
bufferedOutputStream.close();
fileOutputStream.close();
}
//获取文件的信息数据
@Test
public void getFileInfo() throws Exception {
//执获取文件信息
FileInfo group1 = storageClient.get_file_info(group, fileName);
System.out.println(group1);
}
//获取组相关的信息
@Test
public void getGroupInfo() throws Exception {
StorageServer group1 = trackerClient.getStoreStorage(trackerServer, group);
System.out.println(group1.getStorePathIndex());
//组对应的服务器的地址 因为有可能有多个服务器.
ServerInfo[] group1s = trackerClient.getFetchStorages(trackerServer, group, fileName);
for (ServerInfo serverInfo : group1s) {
System.out.println(serverInfo.getIpAddr());
System.out.println(serverInfo.getPort());
}
}
@Test
public void getTrackerInfo() throws Exception {
InetSocketAddress inetSocketAddress = trackerServer.getInetSocketAddress();
System.out.println(inetSocketAddress);
}
}
自己测试就行了
VM 虚拟机搭建
名称 | IP |
---|---|
服务器系统 | Centos7 |
服务器1 nginx+keepalved | 192.168.66.67 |
服务器2 nginx+keepalved | 192.168.66.68 |
服务器3 nginx+keepalved | 192.168.66.69 |
对外统一IP(vip) keepalved | 192.168.66.70 |
搭建前必须 看的内容
特别注意不要 把storage和tracker 的容器重启 包括 重启服务器 否则/etc下面的数据同步配置 一切恢复默认配置 需要从新配置
如果你不配置 你会发现访问的时候某明奇妙么的404 时而可以访问 时而不能访问
解决办法:
在全部服务器 需要配置的文件中/etc 下面有两个 文件 而且都在storage容器里
docker exec -it storage bin/bash 启动storage容器
vi /etc/fdfs/storage.conf 和 vi /etc/fdfs/mod_fastdfs.conf
可以看到 关联的服务器器 配置都编程本机的了
tracker_server=192.168.66.67:22122
tracker_server=192.168.66.67:22122
tracker_server=192.168.66.67:22122
改为 其他服务器
tracker_server=192.168.66.67:22122
tracker_server=192.168.66.68:22122
tracker_server=192.168.66.69:22122
然后重启服务
usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
以上配置好了 你在404 来找我 (亲自测试 10遍 )
还有就是容器 重启动的话(服务器重启) 那么容器内服务也要手动启动 不要想着 靠 配置自动重启 不靠谱 有时莫名其妙的启动失败
我们手动重启
tracker
docker exec -it tracker bin/bash
/etc/nginx/sbin/nginx -s reload
storage
docker exec -it storage bin/bash
usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
/etc/nginx/sbin/nginx -c /etc/nginx/conf/nginx.conf
/etc/nginx/sbin/nginx -s reload
如果执行上面的命令出现
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
表示你docker 没启动
systemctl restart docker
还有就是 如果你发现你配置都没问题就是死活上传失败
来回执行下面命令 看报不报错 报错看报错内容解决
usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
/etc/nginx/sbin/nginx -c /etc/nginx/conf/nginx.conf
/etc/nginx/sbin/nginx -s reload
如果还不行
在监控前先删除访问记录
cat /dev/null > etc/nginx/logs/access.log
然后在 各 storage 容器 执行 tail -f etc/nginx/logs/access.log 来开启实时监控 看访问情况 看看到底是那个一个没有启动
好了 就这么多 切记 否则 你会发现昨天好好的今天 怎么死活都访问不了 我明明配置好了啊
以上能解决你百分之95的 错误 其他错误 一定就是你配置文件有问题 仔细检查 有可能就是一个标点符号写错误
亲自测试 至少10遍
我们开始搭建集群 FastDFS
192.168.66.67
创建 tracker 容器
docker run -d --name tracker --restart=always --network=host morunchang/fastdfs sh tracker.sh
创建 storage 容器
docker run -d --name storage --restart=always --network=host -e TRACKER_IP=192.168.66.67:22122 -e GROUP_NAME=group1 morunchang/fastdfs sh storage.sh
192.168.66.68
创建 tracker 容器
docker run -d --name tracker --restart=always --network=host morunchang/fastdfs sh tracker.sh
创建 storage 容器
docker run -d --name storage --restart=always --network=host -e TRACKER_IP=192.168.66.68:22122 -e GROUP_NAME=group1 morunchang/fastdfs sh storage.sh
192.168.66.69
创建 tracker 容器
docker run -d --name tracker --restart=always --network=host morunchang/fastdfs sh tracker.sh
创建 storage 容器
docker run -d --name storage --restart=always --network=host -e TRACKER_IP=192.168.66.69:22122 -e GROUP_NAME=group1 morunchang/fastdfs sh storage.sh
修改所有服务器 storage容器的 配置文件
docker exec -it storage bin/bash (进入容器)
vi /etc/fdfs/storage.conf
tracker_server=192.168.66.67:22122
tracker_server=192.168.66.68:22122
tracker_server=192.168.66.69:22122
storage.conf 除了以上配置建议修改 (优化)
max_connections=512 #最大线程数 如果人多的话建议改大点 disk_reader_threads每+1 这个总线程就要+256
buff_size = 2048 KB #上传图片 缓存区 建议大一点
accept_threads=10 #排队中 的线程 建议大一点 否则有可能人多的时候 访问失败了
work_threads=4 #工作线程 也就是一直保持活动状态的 建议和 cpu的核一样
disk_reader_threads = 2 # 每个存储基路径的磁盘读取器线程数 这个建议也大一点 提高访问速度 不建议太大
vi /etc/fdfs/mod_fastdfs.conf
tracker_server=192.168.66.67:22122
tracker_server=192.168.66.68:22122
tracker_server=192.168.66.69:22122
vi /data/fastdfs/conf/client.conf
tracker_server=192.168.66.67:22122
tracker_server=192.168.66.68:22122
tracker_server=192.168.66.69:22122
mkdir -p /home/yuqing/fastdfs #创建client的默认存储路径
重启启动服务
usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
/etc/nginx/sbin/nginx -c /etc/nginx/conf/nginx.conf
/etc/nginx/sbin/nginx -s reload
以上内容都完事的话 那么 这些storage服务之间的数据就会开始同步了
我们可以利用入门案例里的SpringBoot项目 来进行测试 无效修改项目直接上传文件就行 向192.168.66.67上传文件
返回 的url如下
http://192.168.66.67:8080/group1/M00/00/00/wKhCQ1_LrZCAD3H6AAQplCTU_z8232.jpg
然后我们使用其他服务访问看看
http://192.168.66.68:8080/group1/M00/00/00/wKhCQ1_LrZCAD3H6AAQplCTU_z8232.jpg
http://192.168.66.69:8080/group1/M00/00/00/wKhCQ1_LrZCAD3H6AAQplCTU_z8232.jpg
发现也是能访问到的 端口8080是fastDfs默认的端口 后面我们会改成80的 这里不用担心
到这里我们发现一个问题的是 我们始终都是在使用 192.168.66.67 这个服务 我们另外2个服务除了同步数据 作为备份服务没啥用了
如果在访问量特别大的情况下 192.168.66.67肯定是撑不住的 我们可以使用 nginx的负载均衡 来让备用服务也参与进来
在入门案例中我们创建了 tracker容器 但是根本就是摆设 此刻我们的 tracker容器就 可以派上用处了 而我们只用容器里的ngxin做反向代理 其他的都不需要 重要事情说三遍 注意 注意 注意 很多人都在瞎配置 导致 根本就无法实现 负载均衡
修改所有服务 storage 里的nginx
docker exec -it storage bin/bash
vi etc/nginx/conf/nginx.conf
listen 8888;
location ~ /M00 {
add_header Cache-Control no-store; #关闭nginx缓存
root /data/fast_data/data;
ngx_fastdfs_module;
}
其他的内容都不要默认就行
listen 8888; 必须 和 storage 里的 storage.conf 内的 http.server_port 一致
启动服务
usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
/etc/nginx/sbin/nginx -c /etc/nginx/conf/nginx.conf
/etc/nginx/sbin/nginx -s reload
修改所有服务tracker里的nginx
docker exec -it tracker bin/bash (进入容器)
vi etc/nginx/conf/nginx.conf
http {
#限流 每秒200个线程
limit_req_zone $binary_remote_addr zone=allips:10m rate=200r/s;
#复制均衡配置
upstream fastdfs {
server 192.168.66.67:8888;
server 192.168.66.68:8888;
server 192.168.66.69:8888;
}
server {
listen 80;
location / {
add_header Cache-Control no-store; #关闭nginx缓存
#如果后端的服务器返回502、504、执行超时等错误,自动将请求转发到upstream负载均衡池中的另一台服务器,实现故障转移。
proxy_next_upstream http_502 http_504 error timeout invalid_header;
#负载均衡
# 防止转发后请求头 数据丢失
proxy_set_header Host $http_host;
proxy_set_header X-Real_IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#转发
# proxy_pass http://fastdfs/$args; #get请求带参数
proxy_pass http://fastdfs
index index.html index.htm;
}
location = /favicon.ico {
log_not_found off;
access_log off;
}
.....
把 location ~ /M00删除
listen 80; 用于外网访问的端口 必须和storage里的client.conf内http.tracker_server_port保持一致 默认是80
然后执行 下面命令 启动nginx
/etc/nginx/sbin/nginx
我们在游览器中直接访问 http://192.168.66.67:8000/ 如果能进入 nginx初始化的页面代表 配置没问题
http://192.168.66.67:80/group1/M00/00/00/wKhCQ1_LrZCAD3H6AAQplCTU_z8232.jpg
http://192.168.66.68:80/group1/M00/00/00/wKhCQ1_LrZCAD3H6AAQplCTU_z8232.jpg
http://192.168.66.69:80/group1/M00/00/00/wKhCQ1_LrZCAD3H6AAQplCTU_z8232.jpg
测试是否实现了负载均衡 我们一直访问一个 地址 比如:
http://192.168.66.67:80/group1/M00/00/00/wKhCQ1_LrZCAD3H6AAQplCTU_z8232.jpg
进入192.168.66.67 和 192.168.66.68 和 192.168.66.69 中的storage里 然后使用下面的命令来监控访问情况
docker exec -it storage bin/bash
在监控前先删除访问记录
cat /dev/null > etc/nginx/logs/access.log
开启实时监控
tail -f etc/nginx/logs/access.log
访问20次看看 是不是在这些服务器之间来回跳转请求 而不是一直死磕在192.168.66.67里
通过上面我们完成了 负载均衡 但是还面临一个新的问题及时 如果 主服务器宕机了那么 不就完了
就拿上面3个服务器来说 在同一时间 我们只能有一个是主服务器 对外提供访问的 比如 192.168.66.67 这个服务器
那么 192.168.66.67 宕机了 怎么办呢? 很简单 就是将ip转移就行了 下面就会教你怎么ip漂移的
简单来说就是 你有3台服务器 192.168.66.67(主) ,192.168.66.68(副) 192.168.66.69 (副) vip 虚拟地址是192.168.66.70 (用来漂移ip的)
如果主服务器宕机了 那么 vip 虚拟地址自动漂移绑定到其他副服务器上 当做主服务器使用
而我们以后绑定域名也是绑定 vip 这个地址 192.168.66.70
注意 : 绑定vip 虚拟地址的电脑或者服务器 必须是在一个路由(同一个网络)下的才有效 才能实现自动漂移绑定
当然如果你不想有这个局限性那么 可以使用 HeartBeat 但是这个配置比较难
每个服务器都要下载和配置 keepalived 才能实现地址漂移
下载地址
http://www.keepalived.org/download.html
我下载的是 keepalived-1.2.18.tar.gz
链接:https://pan.baidu.com/s/17MpeV2GX_S9ZcEaJuR66xw
提取码:1234
上传到这个 /usr/local/src目录下
解压安装
cd /usr/local/src
tar -zxvf keepalived-1.2.18.tar.gz
cd /usr/local/src/keepalived-1.2.18
yum install gcc
yum install openssl-devel -y
./configure --prefix=/usr/local/keepalived
make && make install
执行下面命令
mkdir /etc/keepalived
cp /usr/local/keepalived/etc/keepalived/keepalived.conf /etc/keepalived/
复制 keepalived 服务脚本到默认的地址
cp /usr/local/keepalived/etc/rc.d/init.d/keepalived /etc/init.d/
cp /usr/local/keepalived/etc/sysconfig/keepalived /etc/sysconfig/
ln -s /usr/local/sbin/keepalived /usr/sbin/
rm -rf /sbin/keepalived # 如果有就删没有跳过
ln -s /usr/local/keepalived/sbin/keepalived /sbin/
设置 keepalived 服务开机启动
chkconfig keepalived on
修改 Keepalived 配置文件
先查询网卡的名称 我的是 ens33
ip addr
192.168.66.67 (主服务器的Keepalived 配置)
rm -rf /etc/keepalived/keepalived.conf
vi /etc/keepalived/keepalived.conf
! Configuration File for keepalived
global_defs {
router_id jackfang # 非常重要,标识本机的hostname 随意名称
}
vrrp_script check_nginx {
script "/etc/keepalived/nginx_check.sh" #nginx服务检查脚本
interval 2 # 检测时间间隔
weight -20 # 如果条件成立则权重减20
}
vrrp_instance VI_1 {
state MASTER
interface ens33 #本机网卡 我的是 ens33 使用ip addr 查询
virtual_router_id 100 所有的Keepalived id必须一样
mcast_src_ip 192.168.66.67 ## 本机 IP 地址
priority 100 #主备的优先级priority
advert_int 1 #检查时间1秒
nopreempt #不争抢vip
authentication {
auth_type PASS
auth_pass 1111
}
track_script {
check_nginx # 检查haproxy健康状况的脚本
}
virtual_ipaddress {
192.168.66.70/24 #虚拟 ip,可以定义多个 地址/网段 网段要和实际ip相同 否则外网访问不了
}
}
192.168.66.68 (副服务器的Keepalived 配置)
rm -rf /etc/keepalived/keepalived.conf
vi /etc/keepalived/keepalived.conf
! Configuration File for keepalived
global_defs {
router_id jackfang # 非常重要,标识本机的hostname 随意名称
}
vrrp_script check_nginx {
script "/etc/keepalived/nginx_check.sh" #nginx服务检查脚本
interval 2 # 检测时间间隔
weight -20 # 如果条件成立则权重减20
}
vrrp_instance VI_1 {
state BACKUP
interface ens33 #本机网卡 我的是 ens33 使用ip addr 查询
virtual_router_id 100 所有的Keepalived id必须一样
mcast_src_ip 192.168.66.68 ## 本机 IP 地址
priority 90 #主备的优先级priority
advert_int 1 #检查时间1秒
authentication {
auth_type PASS
auth_pass 1111
}
track_script {
check_nginx # 检查haproxy健康状况的脚本
}
virtual_ipaddress {
192.168.66.70/24 #vip地址/网段 网段要和实际ip相同 否则外网访问不了
}
}
192.168.66.69 (副服务器的Keepalived 配置)
rm -rf /etc/keepalived/keepalived.conf
vi /etc/keepalived/keepalived.conf
! Configuration File for keepalived
global_defs {
router_id jackfang # 非常重要,标识本机的hostname 随意名称
}
vrrp_script check_nginx {
script "/etc/keepalived/nginx_check.sh" #nginx服务检查脚本
interval 2 # 检测时间间隔
weight -20 # 如果条件成立则权重减20
}
vrrp_instance VI_1 {
state BACKUP
interface ens33 #本机网卡 我的是 ens33 使用ip addr 查询
virtual_router_id 100 所有的Keepalived id必须一样
priority 80 #主备的优先级priority
mcast_src_ip 192.168.66.69 8088 ## 本机 IP 地址
advert_int 1 #检查时间1秒
authentication {
auth_type PASS
auth_pass 1111
}
track_script {
check_nginx # 检查haproxy健康状况的脚本
}
virtual_ipaddress {
192.168.66.70/24 #vip地址/网段 网段要和实际ip相同 否则外网访问不了
}
}
这些配置文件不同地方就是 priority 的优先级 和 state (MASTER(主)|BACKUP(副)) 以及本机ip设置
创建检测nginx脚本
用于发送心跳的
192.168.66.67 192.168.66.68 192.168.66.69
vi /etc/keepalived/nginx_check.sh
#!/bin/bash
A=`ps -C nginx -no-header |wc -l`
if [ $A -eq 0 ];then
/etc/nginx/sbin/nginx
sleep 2
if [ `ps -C nginx --no-header |wc -l` -eq 0 ];then
killall keepalived
fi
fi
添加权限:
chmod +x /etc/keepalived/nginx_check.sh
keepalived命令
service keepalived start #启动 (1)
查看keepalived运行状态
service keepalived status
启动服务后观察网卡信息
192.168.66.67
ip addr
可以发现 我们现在 是可以 ping 192.168.66.70的
192.168.66.68(服务器) 和 192.168.66.69(服务器) 同上配置 但是配置完ip addr 不会发生变化只有 192.168.66.67 宕机了 那么 192.168.66.70 才会进行地址漂移到192.168.66.68上…
测试地址漂移
当 192.168.66.67 192.168.66.68 192.168.66.69 都配置好了 我们来检测 是否能漂移成功
192.168.66.69 进行 ping 192.168.66.70 不要关
1.
将 192.168.66.67 服务器关闭
1.
查询 192.168.66.68 的 ip addr 看看 192.168.66.70地址漂移过来没
然后我们将192.168.66.67 恢复 看看地址漂移回去了没 可以发现 地址又漂移回去了
这样就能保住 就算 某个服务器宕机了 还能继续工作
什么是裂脑?
由于两台高可用服务器之间在指定的时间内,无法互相检测到对方心跳而各自启动故障转移功能,取得了资源以及服务的所有权,而此时的两台高可用服务器
对都还活着并作正常运行,这样就会导致同一个IP湖综合服务在两端同时启动而发生冲突的严重问题,最严重的就是两台主机同时占用一个VIP的地址,当用户写入数
据的时候可能会分别写入到两端,这样可能会导致服务器两端的数据不一致或造成数据的丢失,这种情况就本成为裂脑,也有的人称之为分区集群或者大脑垂直分隔
在上面我们已经解决了 可能发裂脑 的问题
nopreempt #不争抢vip
keepalived的日志文件路径:
vi /var/log/messages
还有就是 如果你重启 服务器 那么 keepalived 生效时间很慢的 等个几分钟后在看看 ip addr 有没有 vip
到这里可能你会吧 Keepalived 和 nginx 弄混 我这里在解释一遍
Keepalived 是为了防止服务器宕机 导致 不能正常工作
而 nginx 是将请求进行代理转发给其他的服务器 (负载均衡) 从而减少服务器的压力
测试
http://192.168.66.70/group1/M00/00/00/wKhCQ1_LrZCAD3H6AAQplCTU_z8232.jpg
你把 192.168.66.67服务器宕机 看看还能访问吗
答案是还能的 因为 192.168.66.68 和192.168.66.69 都还没死呢 自动会代替死去的主机
以上我们FastDFS集群 搭建完成
结构图:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
</dependencies>
connect_timeout=60
network_timeout=60
charset=UTF-8
#tracker文件上传是访问nginx的反向代理
http.tracker_http_port=8088
#tracker服务器地址,如果有多个tracker可以配置多个tracker_server
tracker_server=192.168.66.67:22122
tracker_server=192.168.66.68:22122
tracker_server=192.168.66.69:22122
server:
port: 20210
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
application:
name: file
mybatis:
configuration:
map-underscore-to-camel-case: true
mapper-locations: classpath:mapper/*Mapper.xml
type-aliases-package: com.fastdfs.pojo
package com.fastdfs;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
public class FileApplication {
public static void main(String[] args) {
SpringApplication.run(FileApplication.class);
}
}
package com.fastdfs.controller;
import com.fastdfs.pojo.FastDFSFile;
import com.fastdfs.util.FastDFSClient;
import com.fastdfs.util.Result;
import com.fastdfs.util.StatusCode;
import org.csource.fastdfs.FileInfo;
import org.csource.fastdfs.ServerInfo;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
@RestController
@CrossOrigin
public class FileController {
/*** * 文件上传 * @param file 表单的name * @return 返回文件地址 */
@PostMapping(value = "/upload")
public Result upload(@RequestParam("file") MultipartFile file) throws Exception {
//封装一个FastDFSFile
FastDFSFile fastDFSFile = new FastDFSFile(
file.getOriginalFilename(), //文件名字
file.getBytes(), //文件字节数组
StringUtils.getFilenameExtension(file.getOriginalFilename()));//文件扩展名
String s=null;
//文件上传
String[] uploads = FastDFSClient.upload(fastDFSFile);
if(FastDFSFile.vip_ip!=null){
s = FastDFSClient.getTrackerUrl(FastDFSFile.vip_ip) + "/" + uploads[0] + "/" + uploads[1]; //拼接结果
}else {
s = FastDFSClient.getTrackerUrl() + "/" + uploads[0] + "/" + uploads[1]; //拼接结果
}
//组装文件上传地址
return new Result<FastDFSClient>(true, StatusCode.OK,"文件上传成功",s ) ;
}
/*** * 文件信息查询 * @param groupName 组名称 * @param fileName 文件名称 * @return 返回文件详细信息 */
@GetMapping(value = "/selectFileName")
@ResponseBody
public Result selectFileName(String groupName, String fileName) {
FileInfo file = FastDFSClient.getFile(groupName, fileName);
//组装文件上传地址
return new Result<FastDFSClient>(true, StatusCode.OK,"文件信息查询成功",file.toString() ) ;
}
/*** * 文件下载 * @param groupName:组名 * @param fileName:文件存储完整名 * @return */
@GetMapping(value = "/downFile")
public static InputStream downFile(String groupName, String fileName){
try {
InputStream inputStream = FastDFSClient.downFile(groupName, fileName);
//将字节数组转换成字节输入流
return inputStream;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/*** * 文件删除实现 * @param groupName:组名 * @param fileName:文件存储完整名 */
@GetMapping(value = "/deleteFile")
public static Result deleteFile(String groupName, String fileName){
try {
FastDFSClient.deleteFile(groupName,fileName);
} catch (Exception e) {
e.printStackTrace();
return new Result(false,StatusCode.ERROR,"文件删除失败");
}
return new Result(true,StatusCode.OK,"文件删除成功");
}
/*** * 根据文件组名和文件存储路径获取Storage服务的IP、端口信息 * @param groupName :组名 * @param fileName :文件存储完整名 * @return */
@GetMapping(value = "/getServerInfo")
@ResponseBody
public static Result getServerInfo(String groupName, String fileName){
try {
ServerInfo[] serverInfo = FastDFSClient.getServerInfo(groupName, fileName);
return new Result<FastDFSClient>(true, StatusCode.OK,"根据文件组名和文件存储路径查询Storage服务的IP端口信息成功",serverInfo) ;
} catch (Exception e) {
e.printStackTrace();
return new Result<FastDFSClient>(false, StatusCode.ERROR,"根据文件组名和文件存储路径查询Storage服务的IP端口信息失败",e.getMessage()) ;
}
}
}
package com.fastdfs.pojo;
import java.io.Serializable;
import java.util.Arrays;
public class FastDFSFile implements Serializable {
//文件名字
private String name;
//文件内容
private byte[] content;
//文件扩展名
private String ext;
//文件MD5摘要值
private String md5;
//文件创建作者
private String author;
public final static String vip_ip="192.168.66.70";
public FastDFSFile(String name, byte[] content, String ext, String md5, String author) {
this.name = name;
this.content = content;
this.ext = ext;
this.md5 = md5;
this.author = author;
}
public FastDFSFile(String name, byte[] content, String ext) {
this.name = name;
this.content = content;
this.ext = ext;
}
public FastDFSFile() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
public String getExt() {
return ext;
}
public void setExt(String ext) {
this.ext = ext;
}
public String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@Override
public String toString() {
return "FastDFSFile{" +
"name='" + name + '\'' +
", content=" + Arrays.toString(content) +
", ext='" + ext + '\'' +
", md5='" + md5 + '\'' +
", author='" + author + '\'' +
'}';
}
}
package com.fastdfs.util;
import com.fastdfs.pojo.FastDFSFile;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;
import org.springframework.core.io.ClassPathResource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FastDFSClient {
/*** * 初始化tracker信息 */
static {
try {
//获取tracker的配置文件fdfs_client.conf的位置
String filePath = new ClassPathResource("fdfs_client.conf").getPath();
//加载tracker配置信息
ClientGlobal.init(filePath);
} catch (Exception e) {
e.printStackTrace();
}
}
/**** * 文件上传 * @param file : 要上传的文件信息封装->FastDFSFile * @return String[] * 1:文件上传所存储的组名 * 2:文件存储路径 */
public static String[] upload(FastDFSFile file){
//获取文件作者
NameValuePair[] meta_list = new NameValuePair[1];
meta_list[0] =new NameValuePair(file.getAuthor());
/*** * 文件上传后的返回值 * uploadResults[0]:文件上传所存储的组名,例如:group1 * uploadResults[1]:文件存储路径,例如:M00/00/00/wKjThF0DBzaAP23MAAXz2mMp9oM26.jpeg */
String[] uploadResults = null;
try {
//获取StorageClient对象
StorageClient storageClient = getStorageClient();
//执行文件上传
uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
} catch (Exception e) {
e.printStackTrace();
}
return uploadResults;
}
/*** * 获取文件信息 * @param groupName:组名 * @param remoteFileName:文件存储完整名 */
public static FileInfo getFile(String groupName,String remoteFileName){
try {
//获取StorageClient对象
StorageClient storageClient = getStorageClient();
//获取文件信息
return storageClient.get_file_info(groupName,remoteFileName);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/*** * 文件下载 * @param groupName:组名 * @param remoteFileName:文件存储完整名 * @return */
public static InputStream downFile(String groupName,String remoteFileName){
try {
//获取StorageClient
StorageClient storageClient = getStorageClient();
//通过StorageClient下载文件
byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
//将字节数组转换成字节输入流
return new ByteArrayInputStream(fileByte);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/*** * 文件删除实现 * @param groupName:组名 * @param remoteFileName:文件存储完整名 */
public static void deleteFile(String groupName,String remoteFileName){
try {
//获取StorageClient
StorageClient storageClient = getStorageClient();
//通过StorageClient删除文件
storageClient.delete_file(groupName,remoteFileName);
} catch (Exception e) {
e.printStackTrace();
}
}
/*** * 根据文件组名和文件存储路径获取Storage服务的IP、端口信息 * @param groupName :组名 * @param remoteFileName :文件存储完整名 */
public static ServerInfo[] getServerInfo(String groupName, String remoteFileName){
try {
//创建TrackerClient对象
TrackerClient trackerClient = new TrackerClient();
//通过TrackerClient获取TrackerServer对象
TrackerServer trackerServer = trackerClient.getConnection();
//获取服务信息
return trackerClient.getFetchStorages(trackerServer,groupName,remoteFileName);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/*** * 获取Tracker服务地址 */
public static String getTrackerUrl(){
try {
//创建TrackerClient对象
TrackerClient trackerClient = new TrackerClient();
//通过TrackerClient获取TrackerServer对象
TrackerServer trackerServer = trackerClient.getConnection();
//获取Tracker地址
return "http://"+trackerServer.getInetSocketAddress().getHostString()+":"+ClientGlobal.getG_tracker_http_port();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static String getTrackerUrl(String vip_ip){
//获取Tracker地址
return "http://"+vip_ip+":"+ClientGlobal.getG_tracker_http_port();
}
/*** * 获取TrackerServer */
public static TrackerServer getTrackerServer() throws Exception{
//创建TrackerClient对象
TrackerClient trackerClient = new TrackerClient();
//通过TrackerClient获取TrackerServer对象
TrackerServer trackerServer = trackerClient.getConnection();
return trackerServer;
}
/*** * 获取StorageClient * @return * @throws Exception */
public static StorageClient getStorageClient() throws Exception{
//获取TrackerServer
TrackerServer trackerServer = getTrackerServer();
//通过TrackerServer创建StorageClient
StorageClient storageClient = new StorageClient(trackerServer,null);
return storageClient;
}
}
package com.fastdfs.util;
import java.io.Serializable;
/** * 描述 * * @author 三国的包子 * @version 1.0 * @package entity * * @since 1.0 */
public class Result<T> implements Serializable {
private boolean flag;//是否成功
private Integer code;//返回码
private String message;//返回消息
private T data;//返回数据
public Result(boolean flag, Integer code, String message, Object data) {
this.flag = flag;
this.code = code;
this.message = message;
this.data = (T) data;
}
public Result(boolean flag, Integer code, String message) {
this.flag = flag;
this.code = code;
this.message = message;
}
public Result() {
this.flag = true;
this.code = StatusCode.OK;
this.message = "操作成功!";
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
package com.fastdfs.util;
/** * 返回码 */
public class StatusCode {
public static final int OK = 20000;//成功
public static final int ERROR = 20001;//失败
public static final int LOGINERROR = 20002;//用户名或密码错误
public static final int ACCESSERROR = 20003;//权限不足
public static final int REMOTEERROR = 20004;//远程调用失败
public static final int REPERROR = 20005;//重复操作
public static final int NOTFOUNDERROR = 20006;//没有对应的抢购数据
}
使用Postman 测试
选择post请求方式,输入请求地址 http://localhost:20210/upload
填写Headers
Key:Content-Type Value:multipart/form-data
然后选择Body
或者form表单
<form action="http://localhost:18082/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit">
</form>
除了自己搭建FastDFS外我们还可以使用阿里云的OSS (收费)
阿里云对象存储服务(Object Storage Service,简称OSS),是阿里云对外提供的海量、安全、低成本、高可靠的云存储服务。您可以通过本文档提供的简单的REST接口,在任何时间、任何地点、任何互联网设备上进行上传和下载数据。基于OSS,您可以搭建出各种多媒体分享网站、网盘、个人和企业数据备份等基于大规模数据的服务。
帮助文档: https://help.aliyun.com/document_detail/31947.html
还有要提醒的就是超时问题 不要 一 出现 Read timed out 或者 connect timed out 等 超时的问题 你就慌了
你先自己ping 一下 ip看看能不能通 如果能那么就代表 不是 你的问题
不要总是想是不是配置有问题 是不是代码写错误了 这里告诉你 一般正常情况默认配置能满足百分之90以上的需求的 所以基本就是 是网络问题 比如网络波动 或者 服务器突然卡死又恢复了
特别是上传大文件的时候突然网络一波动 呵呵连接就断开了 这一断开 那么就会出现 超时
所以在上传的时候 尽量限制 上传文件的大小
特别是批量上传 如果网络不好 一波动 那么部分就会导致上传失败
我觉得游览器上传文件一次总量不要超过20mb最好 如果服务器稍微好点这个可以调整高点
如果服务器不是很好 那么 10mb 就行 文件图片上传前尽量先使用本地工具压缩后在上传 这样也能提高速度
相册是用于存储图片的管理单元 就好比是垃圾分类 把相同类型的图片放在一个相册里 这样比较好找 比较好管理
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
<!--分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.4</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>0.9.5</version>
</dependency>
<!--http 服务-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<!-- 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<!-- 处理图片-->
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>2.15.0</version>
</dependency>
<!-- 配置gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
</dependencies>
<!-- 自动查找主类 用于打包-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
imageitems表 (图片仓库) 所有图片都在此表中
字段名称 | 字段含义 | 字段类型 | 备注 |
---|---|---|---|
id | 编号 (IdWorker 生成) | VARCHAR(100) | 主键 |
name | 图片名称 | VARCHAR(100) | |
image | 图片路径 | VARCHAR(100) | |
image_describe | 图片描述 | text | |
image_type | 图片类型 | VARCHAR(20) | |
image_size | 图片大小(mb) | int(100) | |
image_px | 图片像素 | VARCHAR(100) | |
image_data | 图片上传的时间 | data | |
image_group | 图片对应fastDFS的组 | VARCHAR(50) | |
image_path | 图片全路径 | VARCHAR(100) |
CREATE TABLE `imageitems` (
`id` varchar(100) NOT NULL,
`name` varchar(100) DEFAULT NULL,
`image` varchar(255) DEFAULT NULL,
`image_describe` text,
`image_type` varchar(20) DEFAULT NULL,
`image_size` int(100) DEFAULT NULL,
`image_px` varchar(100) DEFAULT NULL,
`image_data` datetime DEFAULT NULL,
`image_group` varchar(100) DEFAULT NULL,
`image_path` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `id` (`id`) USING BTREE,
KEY `image` (`image`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
图片仓库表 和 相册表 的 中间表 (将对应的图片关联到相册里)
album_imageitems
字段名称 | 字段含义 | 字段类型 | 备注 |
---|---|---|---|
id | 编号 (IdWorker 生成) | VARCHAR(100) | 主键 |
album_id | 相册名称 | int(70) | |
imageitems_id | 相册封面图片路径 | int(70) |
CREATE TABLE `album_imageitems` (
`id` varchar(100) NOT NULL,
`album_id` varchar(70) DEFAULT NULL,
`imageitems_id` varchar(70) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `album_id` (`album_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
album 表(相册表)
字段名称 | 字段含义 | 字段类型 | 备注 |
---|---|---|---|
id | 编号(IdWorker 生成) | VARCHAR(100) | 主键 |
title | 相册名称 | VARCHAR(100) | |
image | 相册封面图片路径 | VARCHAR(100) | |
image_describe | 相册描述 | text | |
image_data | 创建相册的时间 | data |
CREATE TABLE `album` (
`id` varchar(100) NOT NULL,
`title` varchar(255) DEFAULT NULL,
`image` varchar(255) DEFAULT NULL,
`image_describe` text,
`image_data` datetime DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `id` (`id`) USING BTREE,
KEY `title` (`title`) USING BTREE,
KEY `image` (`image`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
application.yml
server:
port: 20210
tomcat:
max-http-post-size: -1
spring:
servlet:
multipart:
max-file-size: 500MB
max-request-size: 500MB
application:
name: file
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8
username: root
password: root
thymeleaf:
cache: false
mybatis:
configuration:
map-underscore-to-camel-case: true
mapper-locations: classpath:mapper/*Mapper.xml
type-aliases-package: com.fastdfs.pojo
fdfs_client.conf
connect_timeout=60
network_timeout=60
charset=UTF-8
#tracker文件上传是访问nginx的反向代理
http.tracker_http_port=80
#tracker服务器地址 会自动轮询
tracker_server=192.168.66.67:22122
tracker_server=192.168.66.68:22122
tracker_server=192.168.66.69:22122
… 代码省略 自己根据业务情况 写
我自己写的一个类似 图床的项目 管理图片的 前端没怎么写 后端代码基本能干的都实现了
本来是 集群的3个 FasDFS+3个nginx+3个Keepalived + 分布式SpringCloud 因为服务器太贵没钱买
变成了 一个FasDFS +SpringBoot 后果就是 响应的效率 以及上传图片的时间都大大的增加了
而且我买的 服务器也是最垃圾的 1核 1CPU 40G硬盘 5Mbps宽带 可想而知速度是有多慢
主页相册
鼠标点击效果
进入到相册里
查看图片
可直接查看和体验效果 http://fastdfs.huitoushian.cn/image.html 服务器有点小,响应图片有点慢 耐心点,毕竟图片这么多,加载是需要时间的
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/weixin_45203607/article/details/120248966
内容来源于网络,如有侵权,请联系作者删除!