图片文件分布式存储
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 { 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 { public class ImageServerInfo : BaseEntity<long> { public string? ServerName { get; set; }
public string? ServerUrl { get; set; }
public long MaxPicAccount { get; set; } 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); 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.ImageServerFirst
和Cms.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(); } 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; 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); 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
| 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"><</button> <button class="next">></button> </div> </div> </div> </body>
</html> <script src="/js/axios.js" type="text/javascript" ></script> <script> let sliderData = []; axios({ method:"get", url:"/imageInfo/getImageInfos", }).then(function({data:res}){ 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"); 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(); }) slider.addEventListener("mouseenter",function(){ clearInterval(timeId); }) slider.addEventListener("mouseleave",function(){ if(timeId){ clearInterval(timeId); } timeId = setInterval(function () { next.click(); }, 1000) }) function toggle(){ img.src = sliderData[i].imageName; p.innerHTML = sliderData[i].title; const li = lis[i]; document.querySelector(".slider-indicator .active ").classList.remove("active"); li.classList.add("active"); } let timeId = setInterval(function () { next.click(); }, 1000) })
</script>
|
注意:服务端返回的图片数据要有8条,因为在上面的代码中,我们指定了8个li
标签。
getImageInfos
方法中的代码
1 2 3 4 5 6 7 8 9 10
| public async Task<IActionResult> GetImageInfos() { var list = await imageInfoService.LoadEntities(u => u.DelFlag == false).ToListAsync();
return Json(new { StatusCode = 200, data = list }); }
|
注意:在进行测试的时候,一定要启动图片服务器部署的应用程序。