图片文件分布式存储

1、分布存储设计方案分析

系统架构设计需要满足以下4点要求:

(1)如何实现图片的分布式部署,图片上传时如何动态确定保存到哪台图片服务器;

(2)如何做到图片服务器的负载均衡,既要保证所有图片服务器都有均等的机会来保存图片.

(3)如何把一台图片服务器上图片均衡保存到多个子目录中以便突破操作系统在同一个目录中保存文件数的限制,对图片进行更好的管理和维护;

(4)如何能根据性能需要和图片数量的增加实现图片服务器的动态扩充。

2、基础模式设计

图片信息模型设计ImageInfo.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14

namespace Cms.Entity
{
/// <summary>
/// 图片模型
/// </summary>
public class ImageInfo : BaseEntity<long>
{
public string? ImageName { get; set; }

public int ImageServerId { get; set; }
}
}

针对以上模型的配置ImageInfoConfig.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace Cms.Entity
{
public class ImageInfoConfig : IEntityTypeConfiguration<ImageInfo>
{
public void Configure(EntityTypeBuilder<ImageInfo> builder)
{
builder.ToTable("T_ImageInfos");
builder.Property(i => i.ImageName).HasMaxLength(200).IsRequired();

}
}
}

图片服务器模型ImageServerInfo.cs

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
namespace Cms.Entity
{
/// <summary>
/// 图片服务器模型
/// </summary>
public class ImageServerInfo : BaseEntity<long>
{
/// <summary>
/// 服务器名称
/// </summary>
public string? ServerName { get; set; }

/// <summary>
/// 服务器地址
/// </summary>
public string? ServerUrl { get; set; }

/// <summary>
/// 图片服务器最多能存储多少张图片
/// </summary>
public long MaxPicAccount { get; set; }
/// <summary>
/// 当前存储了多少张图片
/// </summary>
public long CurrPicAccount { get; set; }
}
}

针对以上模型的配置ImageServerInfoConfig.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace Cms.Entity
{
public class ImageServerInfoConfig : IEntityTypeConfiguration<ImageServerInfo>
{
public void Configure(EntityTypeBuilder<ImageServerInfo> builder)
{
builder.ToTable("T_ImageServerInfos");
builder.Property(i => i.ServerUrl).HasMaxLength(200).IsRequired();
builder.Property(i => i.ServerName).HasMaxLength(100).IsRequired();
}
}
}

注意:在MyDbContext中添加对应的DbSet属性

1
2
3
4

public DbSet<ImageInfo> ImageInfos { get; set; }
public DbSet<ImageServerInfo> ImageServers { get; set; }

下面进行数据的迁移操作。

3、数据仓储与服务层创建

1
2
3
4
5
6
namespace Cms.IRepository
{
public interface IImageInfoRepository : IBaseRepository<ImageInfo>
{
}
}
1
2
3
4
5
6
7
namespace Cms.IRepository
{
public interface IImageServerInfoRepository : IBaseRepository<ImageServerInfo>
{
}
}

具体实现

1
2
3
4
5
6
7
8
namespace Cms.Repository
{
public class ImageInfoRepository : BaseRepository<ImageInfo>, IImageInfoRepository
{
public ImageInfoRepository(MyDbContext myDbContext) : base(myDbContext) { }
}
}

1
2
3
4
5
6
7
8

namespace Cms.Repository
{
public class ImageServerInfoRepository:BaseRepository<ImageServerInfo>, IImageServerInfoRepository
{
public ImageServerInfoRepository(MyDbContext myDbContext) : base(myDbContext) { }
}
}

以上完成了数据仓储的创建。

下面看一下服务层的创建

1
2
3
4
5
6
7
namespace Cms.IService
{
public interface IImageInfoService : IBaseService<ImageInfo>
{
}
}

1
2
3
4
5
6
7
namespace Cms.IService
{
public interface IImageServerInfoService : IBaseService<ImageServerInfo>
{
}
}

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
namespace Cms.Service
{
public class ImageInfoService : BaseService<ImageInfo>, IImageInfoService
{
private readonly IImageInfoRepository imageInfoRepository;
public ImageInfoService(IImageInfoRepository imageInfoRepository)
{
this.imageInfoRepository = imageInfoRepository;
base._repository = imageInfoRepository;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace Cms.Service
{
public class ImageServerInfoService : BaseService<ImageServerInfo>, IImageServerInfoService
{
private readonly IImageServerInfoRepository imageServerInfoRepository;
public ImageServerInfoService(IImageServerInfoRepository imageServerInfoRepository)
{
this.imageServerInfoRepository = imageServerInfoRepository;
base._repository = imageServerInfoRepository;

}
}
}

4、实现图片文件分布式上传

.net core中使用httpClientFactory

https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-7.0

Web项目中创建ImageInfoController.cs控制器。

1
2
3
4
public IActionResult Index()
{
return View();
}

上面的Index方法对应的Index视图中,只有上传图片的表单

1
2
3
4
5
<form method="post" action="/ImageInfo/FileUpload" enctype="multipart/form-data">

<input type="file" name="filePath" /><br/>
<input type="submit" value="上传图片"/>
</form>

下面实现ImageInfo控制器中的FileUpload方法,该方法中的代码如下所示:

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
[UnitOfWork(new Type[] { typeof(MyDbContext) })]
public async Task<IActionResult> FileUpload(IFormFile filePath)
{
string fileName = filePath.FileName;
string fileExt = Path.GetExtension(fileName);
// 需要判断类型
if (fileExt == ".jpg")
{
// 获取所有可用的图片服务器的信息
var imageServeList = imageServerInfoService.LoadEntities(i => i.DelFlag == false).ToList();
// 统计其数量
var count = imageServeList.Count();
Random random = new Random();
int r = random.Next(0, count + 1);
int i = r % count;
// 获取了具体的图片服务器的信息。(为了测试,可以在数据表中直接添加图片服务器的信息)
var serverInfo = imageServeList[i];
// 构建好对应的地址,开始进行上传。
if (serverInfo.CurrPicAccount < serverInfo.MaxPicAccount)
{
// 表示发送给服务器的表单数据
using (MultipartFormDataContent content = new MultipartFormDataContent())
{
using (Stream stream = filePath.OpenReadStream())
{
using (var fileContent = new StreamContent(stream))
{
content.Add(fileContent, "file", fileName);
// 如果需要添加其他的表单数据,采用如下方式。
// content.Add(new StringContent("value"), "key");
var httpClient = httpClientFactory.CreateClient();
Uri requestUri = new Uri(serverInfo.ServerUrl + "/Home/Upload");
// 开始上传
var respMsg = await httpClient.PostAsync(requestUri, content);
if (!respMsg.IsSuccessStatusCode)
{
string respString = await respMsg.Content.ReadAsStringAsync();
throw new HttpRequestException($"上传失败,状态码:{respMsg.StatusCode},响应报文:{respString}");
}
else
{
// 添加图片信息
ImageInfo imageInfo = new ImageInfo();
imageInfo.ImageServerId = Convert.ToInt32(serverInfo.Id);
imageInfo.UpdateTime = DateTime.Now;
imageInfo.AddTime = DateTime.Now;
imageInfo.ImageName = await respMsg.Content.ReadAsStringAsync(); // 接收图片服务器中返回的内容
await imageInfoService.AddEntity(imageInfo);
}
}
}
}
}
}

return Ok();
}
}

还需要注入相关的实例

1
2
3
4
5
6
7
8
9
10
11
12
public class ImageInfoController : Controller
{
private readonly IImageServerInfoService imageServerInfoService;
private readonly IImageInfoService imageInfoService;
private readonly IHttpClientFactory httpClientFactory;
public ImageInfoController(IImageServerInfoService imageServerInfoService, IHttpClientFactory httpClientFactory, IImageInfoService imageInfoService)
{

this.imageServerInfoService = imageServerInfoService;
this.httpClientFactory = httpClientFactory;
this.imageInfoService = imageInfoService;
}
1
builder.Services.AddHttpClient();

5、图片接收存储

关于图片的接收,需要创建一个MVC项目,然后将其部署到图片服务器上。

先创建一个解决方案文件夹(ImageService),在该文件夹中创建两个MVC项目

Cms.ImageServerFirstCms.ImageServerSecond

注意:实际应用中只需要创建一个MVC项目就可以了,这里为了演示所以创建了两个。

这两个项目中的代码是一样的。

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
public class HomeController : Controller
{

private readonly IWebHostEnvironment _environment;

public HomeController(IWebHostEnvironment environment)
{
_environment = environment;
}

public IActionResult Index()
{
return View();
}
/// <summary>
/// 接受图片文件
/// </summary>
/// <returns></returns>
public async Task<IActionResult> Upload(IFormFile file)
{
string fileName = file.FileName;
using (Stream stream = file.OpenReadStream())
{
string hash = HashHelper.ComputeSha256Hash(stream);
long fileSize = stream.Length;
DateTime today = DateTime.Today;
//用日期把文件分散在不同文件夹存储,同时由于加上了文件hash值作为目录,又用用户上传的文件夹做文件名,
//所以几乎不会发生不同文件冲突的可能
//用用户上传的文件名保存文件名,这样用户查看、下载文件的时候,文件名更灵活
string key = $"{today.Year}/{today.Month}/{today.Day}/{hash}/{fileName}";
stream.Position = 0;

string workingDir = Path.Combine(_environment.ContentRootPath, "wwwroot");
string fullPath = Path.Combine(workingDir, key);
string? fullDir = Path.GetDirectoryName(fullPath);//get the directory
Directory.CreateDirectory(fullDir!);
if (System.IO.File.Exists(fullPath))//如果已经存在,则尝试删除
{
System.IO.File.Delete(fullPath);
}
using (Stream outStream = System.IO.File.OpenWrite(fullPath))
{
await stream.CopyToAsync(outStream);
}
var req = HttpContext.Request;
string url = req.Scheme + "://" + req.Host + "/" + key;

return Ok(url);
}


}
}

以上接收到web服务器传递过来的图片文件,然后将其保存到了图片服务器中。

最后给web服务器返回的就是图片文件存储的相关路径。

下面可以进行测试。可以看到,图片分别存储了到不同图片服务器中。

6、图片展示

这里,图片的展示是通过轮播图展示。

ImageInfoController.cs控制器中添加方法

1
2
3
4
5
6
7
8
/// <summary>
/// 展示图片
/// </summary>
/// <returns></returns>
public IActionResult ShowImages()
{
return View();
}

对应的ShowImages视图中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
box-sizing: border-box;
}

.slider {
width: 560px;
height: 400px;
overflow: hidden;
}

.slider-wrapper {
width: 100%;
height: 320px;
}

.slider-wrapper img {
width: 100%;
height: 100%;
display: block;
}

.slider-footer {
height: 80px;
background-color: rgb(100, 67, 68);
padding: 12px 12px 0 12px;
position: relative;
}

.slider-footer .toggle {
position: absolute;
right: 0;
top: 12px;
display: flex;
}

.slider-footer .toggle button {
margin-right: 12px;
width: 28px;
height: 28px;
appearance: none;
border: none;
background: rgba(255, 255, 255, 0.1);
color: #fff;
border-radius: 4px;
cursor: pointer;
}

.slider-footer .toggle button:hover {
background: rgba(255, 255, 255, 0.2);
}

.slider-footer p {
margin: 0;
color: #fff;
font-size: 18px;
margin-bottom: 10px;
}

.slider-indicator {
margin: 0;
padding: 0;
list-style: none;
display: flex;
align-items: center;
}

.slider-indicator li {
width: 8px;
height: 8px;
margin: 4px;
border-radius: 50%;
background: #fff;
opacity: 0.4;
cursor: pointer;
}

.slider-indicator li.active {
width: 12px;
height: 12px;
opacity: 1;
}
</style>
</head>

<body>
<div class="slider">

<div class="slider-wrapper">
<img src="http://localhost:5071/2024/6/15/1949292f2eaf5f56b16bfdd22e6e4ae0e3b5202fe3e1cd3b3fd15dade917f6f4/17.jpg" alt="" />
</div>
<div class="slider-footer">
<p>对人类来说会不会太超前了?</p>
<ul class="slider-indicator">
<li class="active"></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
<div class="toggle">
<!-- 上一张图片 -->
<button class="prev">&lt;</button>
<!-- 下一张图片 -->
<button class="next">&gt;</button>
</div>
</div>
</div>
</body>


</html>
<script src="/js/axios.js" type="text/javascript" ></script>

<script>
let sliderData = []; // 存储服务端返回的图片数据
// 发送ajax请求
axios({
method:"get",
url:"/imageInfo/getImageInfos",
}).then(function({data:res}){
// 服务端返回的数据赋值给sliderData数组
// 同时在回调函数中实现轮播图效果
sliderData = res.data;
console.log("count==",sliderData.length);
let i = 0;
// 上一张图片处理
const prev = document.querySelector(".prev");
// 下一张图片按钮
const next = document.querySelector(".next");
const img = document.querySelector(".slider-wrapper img");
const p = document.querySelector(".slider-footer p");
const lis = document.querySelectorAll(".slider-indicator li");
// 获取最外层div
const slider = document.querySelector(".slider");
// 上一张图片
prev.addEventListener("click",function(){
i--;
i = i < 0 ? sliderData.length - 1:i;
toggle();
});
// 下一张图片
next.addEventListener("click",function(){
i++;
i = i>= sliderData.length ? 0 :i
toggle();
})
// 给最外层div添加鼠标经过事件
slider.addEventListener("mouseenter",function(){
clearInterval(timeId);
})
// 给最外层div添加鼠标离开事件
slider.addEventListener("mouseleave",function(){
// 如果已经有setInterval,要先删除,然后再添加
if(timeId){
clearInterval(timeId);
}

timeId = setInterval(function () {
next.click();// 调用了下一张按钮dom对象中的click方法,表示单击了按钮一次
}, 1000)
})

// 展示图片以及相关内容
function toggle(){
img.src = sliderData[i].imageName;// ------------------------------------注意:这里需要修改,服务端返回的imageName属性中存储了图片的路径
p.innerHTML = sliderData[i].title;
const li = lis[i];
// 全部删除已经具有了active的
document.querySelector(".slider-indicator .active ").classList.remove("active");
// 给当前的li添加active样式
li.classList.add("active");
}

let timeId = setInterval(function () {
next.click();// 调用了下一张按钮dom对象中的click方法,表示单击了按钮一次
}, 1000)
})



</script>

注意:服务端返回的图片数据要有8条,因为在上面的代码中,我们指定了8个li标签。

getImageInfos方法中的代码

1
2
3
4
5
6
7
8
9
10
/// <summary>
/// 获取图片数据
/// </summary>
/// <returns></returns>
public async Task<IActionResult> GetImageInfos()
{
var list = await imageInfoService.LoadEntities(u => u.DelFlag == false).ToListAsync();

return Json(new { StatusCode = 200, data = list });
}

注意:在进行测试的时候,一定要启动图片服务器部署的应用程序。