.Net Core应用搭建的分布式邮件系统设计

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 本篇分享的是由NetCore搭建的分布式邮件系统,主要采用NetCore的Api和控制台应用程序,由于此系统属于公司的所以这里只能分享设计图和一些单纯不设计业务的类或方法; 为什么要在公司中首例采用NetCore做开发 为什么要在公司中首例采用NetCore做开发,有些netcoreapi不是还不全面么,您都敢尝试?恐怕会有人这样问我,我只能告诉你NetCore现在出2.

本篇分享的是由NetCore搭建的分布式邮件系统,主要采用NetCore的Api控制台应用程序,由于此系统属于公司的所以这里只能分享设计图和一些单纯不设计业务的类或方法;

为什么要在公司中首例采用NetCore做开发

为什么要在公司中首例采用NetCore做开发,有些netcoreapi不是还不全面么,您都敢尝试?恐怕会有人这样问我,我只能告诉你NetCore现在出2.0版本了,很多Framwork的常用封装都已经有了,况且她主打的是MVC模式,能够高效的开发系统,也有很多Core的Nuget包支持了,已经到达了几乎可以放心大胆使用的地步,退一万不说有些东西不支持那这又如何,可以采用接口的方式从其他地方对接过来也是一种不错的处理方案。为了让C#这门优秀的语言被广泛应用,默默努力着。

目前我写的NetCore方面的文章

AspNetCore - MVC实战系列目录

.NetCore上传多文件的几种示例

开源一个跨平台运行的服务插件 - TaskCore.MainForm

NET Core-学习笔记

Asp.NetCore1.1版本没了project.json,这样来生成跨平台包

 

正片环节 - 分布式邮件系统设计图

分布式邮件系统说明

其实由上图可以知晓这里我主要采用了Api+服务的模式,这也是现在互联网公司经常采用的一种搭配默认;利用api接受请求插入待发送邮件队列和入库,然后通过部署多个NetCore跨平台服务(这里服务指的是:控制台应用)来做分布式处理操作,跨平台服务主要操作有:

. 邮件发送

. 邮件发送状态的通知(如果需要通知子业务,那么需要通知业务方邮件发送的状态)

. 通知失败处理(自动往绑定的责任人发送一封邮件)

. 填充队列(如果待发邮件队列或者通知队列数据不完整,需要修复队列数据)

Api接口的统一验证入口

这里我用最简单的方式,继承Controller封装了一个父级的BaseController,来让各个api的Controller基础统一来做身份验证;来看看重写 public override void OnActionExecuting(ActionExecutingContext context) 的验证代码:

 1 public override void OnActionExecuting(ActionExecutingContext context)
 2         {
 3             base.OnActionExecuting(context);
 4 
 5             var moResponse = new MoBaseRp();
 6             try
 7             {
 8 
 9                 #region 安全性验证
10 
11                 var key = "request";
12                 if (!context.ActionArguments.ContainsKey(key)) { moResponse.Msg = "请求方式不正确"; return; }
13                 var request = context.ActionArguments[key];
14                 var baseRq = request as MoBaseRq;
15                 //暂时不验证登录账号密码
16                 if (string.IsNullOrWhiteSpace(baseRq.UserName) || string.IsNullOrWhiteSpace(baseRq.UserPwd)) { moResponse.Msg = "登录账号或密码不能为空"; return; }
17                 else if (baseRq.AccId <= 0) { moResponse.Msg = "发送者Id无效"; return; }
18                 else if (string.IsNullOrWhiteSpace(baseRq.FuncName)) { moResponse.Msg = "业务方法名不正确"; return; }
19 
20                 //token验证
21                 var strToken = PublicClass._Md5($"{baseRq.UserName}{baseRq.AccId}", "");
22                 if (!strToken.Equals(baseRq.Token, StringComparison.OrdinalIgnoreCase)) { moResponse.Msg = "Token验证失败"; return; }
23 
24                 //验证发送者Id
25                 if (string.IsNullOrWhiteSpace(baseRq.Ip))
26                 {
27                     var account = _db.EmailAccount.SingleOrDefault(b => b.Id == baseRq.AccId);
28                     if (account == null) { moResponse.Msg = "发送者Id无效。"; return; }
29                     else
30                     {
31                         if (account.Status != (int)EnumHelper.EmStatus.启用)
32                         {
33                             moResponse.Msg = "发送者Id已禁用"; return;
34                         }
35 
36                         //验证ip
37                         var ipArr = account.AllowIps.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
38                         //当前请求的Ip
39                         var nowIp = this.GetUserIp();
40                         baseRq.Ip = nowIp;
41                         //默认*为所有ip , 匹配ip
42                         if (!ipArr.Any(b => b.Equals("*")) && !ipArr.Any(b => b.Equals(nowIp)))
43                         {
44                             moResponse.Msg = "请求IP为授权"; return;
45                         }
46                     }
47                 }
48                 else
49                 {
50                     var account = _db.EmailAccount.SingleOrDefault(b => b.Id == baseRq.AccId && b.AllowIps.Any(bb => bb.Equals(baseRq.Ip)));
51                     if (account == null) { moResponse.Msg = "发送者未授权"; return; }
52                     else if (account.Status != (int)EnumHelper.EmStatus.启用)
53                     {
54                         moResponse.Msg = "发送者Id已禁用"; return;
55                     }
56                 }
57 
58                 //内容非空,格式验证
59                 if (!context.ModelState.IsValid)
60                 {
61                     var values = context.ModelState.Values.Where(b => b.Errors.Count > 0);
62                     if (values.Count() > 0)
63                     {
64                         moResponse.Msg = values.First().Errors.First().ErrorMessage;
65                         return;
66                     }
67                 }
68 
69                 #endregion
70 
71                 moResponse.Status = 1;
72             }
73             catch (Exception ex)
74             {
75                 moResponse.Msg = "O No请求信息错误";
76             }
77             finally
78             {
79                 if (moResponse.Status == 0) { context.Result = Json(moResponse); }
80             }
81         }

邮件请求父类实体:

 1 /// <summary>
 2     /// 邮件请求父类
 3     /// </summary>
 4     public class MoBaseRq
 5     {
 6 
 7         public string UserName { get; set; }
 8 
 9         public string UserPwd { get; set; }
10 
11         /// <summary>
12         /// 验证token(Md5(账号+配置发送者账号信息的Id+Ip))   必填
13         /// </summary>
14         public string Token { get; set; }
15 
16         /// <summary>
17         /// 配置发送者账号信息的Id  必填
18         /// </summary>
19         public int AccId { get; set; }
20 
21         /// <summary>
22         /// 业务方法名称
23         /// </summary>
24         public string FuncName { get; set; }
25 
26         /// <summary>
27         /// 请求者Ip,如果客户端没赋值,默认服务端获取
28         /// </summary>
29         public string Ip { get; set; }
30 
31     }

第三方Nuget包的便利

此邮件系统使用到了第三方包,这也能够看出有很多朋友正为开源,便利,NetCore的推广努力着;

首先看看MailKit(邮件发送)包,通过安装下载命令: Install-Package MailKit 能够下载最新包,然后你不需要做太花哨的分装,只需要正对于邮件发送的服务器,端口,账号,密码做一些设置基本就行了,如果可以您可以直接使用我的代码:

 1 /// <summary>
 2         /// 发送邮件
 3         /// </summary>
 4         /// <param name="dicToEmail"></param>
 5         /// <param name="title"></param>
 6         /// <param name="content"></param>
 7         /// <param name="name"></param>
 8         /// <param name="fromEmail"></param>
 9         /// <returns></returns>
10         public static bool _SendEmail(
11             Dictionary<string, string> dicToEmail,
12             string title, string content,
13             string name = "爱留图网", string fromEmail = "841202396@qq.com",
14             string host = "smtp.qq.com", int port = 587,
15             string userName = "841202396@qq.com", string userPwd = "123123")
16         {
17             var isOk = false;
18             try
19             {
20                 if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(content)) { return isOk; }
21 
22                 //设置基本信息
23                 var message = new MimeMessage();
24                 message.From.Add(new MailboxAddress(name, fromEmail));
25                 foreach (var item in dicToEmail.Keys)
26                 {
27                     message.To.Add(new MailboxAddress(item, dicToEmail[item]));
28                 }
29                 message.Subject = title;
30                 message.Body = new TextPart("html")
31                 {
32                     Text = content
33                 };
34 
35                 //链接发送
36                 using (var client = new SmtpClient())
37                 {
38                     // For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS)
39                     client.ServerCertificateValidationCallback = (s, c, h, e) => true;
40 
41                     //采用qq邮箱服务器发送邮件
42                     client.Connect(host, port, false);
43 
44                     // Note: since we don't have an OAuth2 token, disable
45                     // the XOAUTH2 authentication mechanism.
46                     client.AuthenticationMechanisms.Remove("XOAUTH2");
47 
48                     //qq邮箱,密码(安全设置短信获取后的密码)  ufiaszkkulbabejh
49                     client.Authenticate(userName, userPwd);
50 
51                     client.Send(message);
52                     client.Disconnect(true);
53                 }
54                 isOk = true;
55             }
56             catch (Exception ex)
57             {
58 
59             }
60             return isOk;
61         }

Redis方面的操作包StackExchange.Redis,现在NetCore支持很多数据库驱动(例如:Sqlserver,mysql,postgressql,db2等)这么用可以参考下这篇文章AspNetCore - MVC实战系列(一)之Sqlserver表映射实体模型,不仅如此还支持很多缓存服务(如:Memorycach,Redis),这里讲到的就是Redis,我利用Redis的list的队列特性来做分布式任务存储,尽管目前我用到的只有一个主Redis服务还没有业务场景需要用到主从复制等功能;这里分享的代码是基于StackExchange.Redis基础上封装对于string,list的操作:

  1   public class StackRedis : IDisposable
  2     {
  3         #region 配置属性   基于 StackExchange.Redis 封装
  4         //连接串 (注:IP:端口,属性=,属性=)
  5         public string _ConnectionString = "127.0.0.1:6377,password=shenniubuxing3";
  6         //操作的库(注:默认0库)
  7         public int _Db = 0;
  8         #endregion
  9 
 10         #region 管理器对象
 11 
 12         /// <summary>
 13         /// 获取redis操作类对象
 14         /// </summary>
 15         private static StackRedis _StackRedis;
 16         private static object _locker_StackRedis = new object();
 17         public static StackRedis Current
 18         {
 19             get
 20             {
 21                 if (_StackRedis == null)
 22                 {
 23                     lock (_locker_StackRedis)
 24                     {
 25                         _StackRedis = _StackRedis ?? new StackRedis();
 26                         return _StackRedis;
 27                     }
 28                 }
 29 
 30                 return _StackRedis;
 31             }
 32         }
 33 
 34         /// <summary>
 35         /// 获取并发链接管理器对象
 36         /// </summary>
 37         private static ConnectionMultiplexer _redis;
 38         private static object _locker = new object();
 39         public ConnectionMultiplexer Manager
 40         {
 41             get
 42             {
 43                 if (_redis == null)
 44                 {
 45                     lock (_locker)
 46                     {
 47                         _redis = _redis ?? GetManager(this._ConnectionString);
 48                         return _redis;
 49                     }
 50                 }
 51 
 52                 return _redis;
 53             }
 54         }
 55 
 56         /// <summary>
 57         /// 获取链接管理器
 58         /// </summary>
 59         /// <param name="connectionString"></param>
 60         /// <returns></returns>
 61         public ConnectionMultiplexer GetManager(string connectionString)
 62         {
 63             return ConnectionMultiplexer.Connect(connectionString);
 64         }
 65 
 66         /// <summary>
 67         /// 获取操作数据库对象
 68         /// </summary>
 69         /// <returns></returns>
 70         public IDatabase GetDb()
 71         {
 72             return Manager.GetDatabase(_Db);
 73         }
 74         #endregion
 75 
 76         #region 操作方法
 77 
 78         #region string 操作
 79 
 80         /// <summary>
 81         /// 根据Key移除
 82         /// </summary>
 83         /// <param name="key"></param>
 84         /// <returns></returns>
 85         public async Task<bool> Remove(string key)
 86         {
 87             var db = this.GetDb();
 88 
 89             return await db.KeyDeleteAsync(key);
 90         }
 91 
 92         /// <summary>
 93         /// 根据key获取string结果
 94         /// </summary>
 95         /// <param name="key"></param>
 96         /// <returns></returns>
 97         public async Task<string> Get(string key)
 98         {
 99             var db = this.GetDb();
100             return await db.StringGetAsync(key);
101         }
102 
103         /// <summary>
104         /// 根据key获取string中的对象
105         /// </summary>
106         /// <typeparam name="T"></typeparam>
107         /// <param name="key"></param>
108         /// <returns></returns>
109         public async Task<T> Get<T>(string key)
110         {
111             var t = default(T);
112             try
113             {
114                 var _str = await this.Get(key);
115                 if (string.IsNullOrWhiteSpace(_str)) { return t; }
116 
117                 t = JsonConvert.DeserializeObject<T>(_str);
118             }
119             catch (Exception ex) { }
120             return t;
121         }
122 
123         /// <summary>
124         /// 存储string数据
125         /// </summary>
126         /// <param name="key"></param>
127         /// <param name="value"></param>
128         /// <param name="expireMinutes"></param>
129         /// <returns></returns>
130         public async Task<bool> Set(string key, string value, int expireMinutes = 0)
131         {
132             var db = this.GetDb();
133             if (expireMinutes > 0)
134             {
135                 return db.StringSet(key, value, TimeSpan.FromMinutes(expireMinutes));
136             }
137             return await db.StringSetAsync(key, value);
138         }
139 
140         /// <summary>
141         /// 存储对象数据到string
142         /// </summary>
143         /// <typeparam name="T"></typeparam>
144         /// <param name="key"></param>
145         /// <param name="value"></param>
146         /// <param name="expireMinutes"></param>
147         /// <returns></returns>
148         public async Task<bool> Set<T>(string key, T value, int expireMinutes = 0)
149         {
150             try
151             {
152                 var jsonOption = new JsonSerializerSettings()
153                 {
154                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
155                 };
156                 var _str = JsonConvert.SerializeObject(value, jsonOption);
157                 if (string.IsNullOrWhiteSpace(_str)) { return false; }
158 
159                 return await this.Set(key, _str, expireMinutes);
160             }
161             catch (Exception ex) { }
162             return false;
163         }
164         #endregion
165 
166         #region List操作(注:可以当做队列使用)
167 
168         /// <summary>
169         /// list长度
170         /// </summary>
171         /// <typeparam name="T"></typeparam>
172         /// <param name="key"></param>
173         /// <returns></returns>
174         public async Task<long> GetListLen<T>(string key)
175         {
176             try
177             {
178                 var db = this.GetDb();
179                 return await db.ListLengthAsync(key);
180             }
181             catch (Exception ex) { }
182             return 0;
183         }
184 
185         /// <summary>
186         /// 获取队列出口数据并移除
187         /// </summary>
188         /// <typeparam name="T"></typeparam>
189         /// <param name="key"></param>
190         /// <returns></returns>
191         public async Task<T> GetListAndPop<T>(string key)
192         {
193             var t = default(T);
194             try
195             {
196                 var db = this.GetDb();
197                 var _str = await db.ListRightPopAsync(key);
198                 if (string.IsNullOrWhiteSpace(_str)) { return t; }
199                 t = JsonConvert.DeserializeObject<T>(_str);
200             }
201             catch (Exception ex) { }
202             return t;
203         }
204 
205         /// <summary>
206         /// 集合对象添加到list左边
207         /// </summary>
208         /// <typeparam name="T"></typeparam>
209         /// <param name="key"></param>
210         /// <param name="values"></param>
211         /// <returns></returns>
212         public async Task<long> SetLists<T>(string key, List<T> values)
213         {
214             var result = 0L;
215             try
216             {
217                 var jsonOption = new JsonSerializerSettings()
218                 {
219                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
220                 };
221                 var db = this.GetDb();
222                 foreach (var item in values)
223                 {
224                     var _str = JsonConvert.SerializeObject(item, jsonOption);
225                     result += await db.ListLeftPushAsync(key, _str);
226                 }
227                 return result;
228             }
229             catch (Exception ex) { }
230             return result;
231         }
232 
233         /// <summary>
234         /// 单个对象添加到list左边
235         /// </summary>
236         /// <typeparam name="T"></typeparam>
237         /// <param name="key"></param>
238         /// <param name="value"></param>
239         /// <returns></returns>
240         public async Task<long> SetList<T>(string key, T value)
241         {
242             var result = 0L;
243             try
244             {
245                 result = await this.SetLists(key, new List<T> { value });
246             }
247             catch (Exception ex) { }
248             return result;
249         }
250 
251 
252         #endregion
253 
254         #region 额外扩展
255 
256         /// <summary>
257         /// 手动回收管理器对象
258         /// </summary>
259         public void Dispose()
260         {
261             this.Dispose(_redis);
262         }
263 
264         public void Dispose(ConnectionMultiplexer con)
265         {
266             if (con != null)
267             {
268                 con.Close();
269                 con.Dispose();
270             }
271         }
272 
273         #endregion
274 
275         #endregion
276     }

用到Redis的那些操作就添加哪些就行了,也不用太花哨能用就行;

如何生成跨平台的api服务和应用程序服务

这小节的内容最重要,由于之前有相关的文章,这里就不用再赘述了,来这里看看:Asp.NetCore1.1版本没了project.json,这样来生成跨平台包

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
存储 开发框架 JSON
ASP.NET Core OData 9 正式发布
【10月更文挑战第8天】Microsoft 在 2024 年 8 月 30 日宣布推出 ASP.NET Core OData 9,此版本与 .NET 8 的 OData 库保持一致,改进了数据编码以符合 OData 规范,并放弃了对旧版 .NET Framework 的支持,仅支持 .NET 8 及更高版本。新版本引入了更快的 JSON 编写器 `System.Text.UTF8JsonWriter`,优化了内存使用和序列化速度。
|
12天前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
25天前
|
JSON 算法 安全
JWT Bearer 认证在 .NET Core 中的应用
【10月更文挑战第30天】JWT(JSON Web Token)是一种开放标准,用于在各方之间安全传输信息。它由头部、载荷和签名三部分组成,用于在用户和服务器之间传递声明。JWT Bearer 认证是一种基于令牌的认证方式,客户端在请求头中包含 JWT 令牌,服务器验证令牌的有效性后授权用户访问资源。在 .NET Core 中,通过安装 `Microsoft.AspNetCore.Authentication.JwtBearer` 包并配置认证服务,可以实现 JWT Bearer 认证。具体步骤包括安装 NuGet 包、配置认证服务、启用认证中间件、生成 JWT 令牌以及在控制器中使用认证信息
|
1月前
|
人工智能 文字识别 Java
SpringCloud+Python 混合微服务,如何打造AI分布式业务应用的技术底层?
尼恩,一位拥有20年架构经验的老架构师,通过其深厚的架构功力,成功指导了一位9年经验的网易工程师转型为大模型架构师,薪资逆涨50%,年薪近80W。尼恩的指导不仅帮助这位工程师在一年内成为大模型架构师,还让他管理起了10人团队,产品成功应用于多家大中型企业。尼恩因此决定编写《LLM大模型学习圣经》系列,帮助更多人掌握大模型架构,实现职业跃迁。该系列包括《从0到1吃透Transformer技术底座》、《从0到1精通RAG架构》等,旨在系统化、体系化地讲解大模型技术,助力读者实现“offer直提”。此外,尼恩还分享了多个技术圣经,如《NIO圣经》、《Docker圣经》等,帮助读者深入理解核心技术。
SpringCloud+Python 混合微服务,如何打造AI分布式业务应用的技术底层?
|
2月前
|
开发框架 监控 前端开发
在 ASP.NET Core Web API 中使用操作筛选器统一处理通用操作
【9月更文挑战第27天】操作筛选器是ASP.NET Core MVC和Web API中的一种过滤器,可在操作方法执行前后运行代码,适用于日志记录、性能监控和验证等场景。通过实现`IActionFilter`接口的`OnActionExecuting`和`OnActionExecuted`方法,可以统一处理日志、验证及异常。创建并注册自定义筛选器类,能提升代码的可维护性和复用性。
|
2月前
|
开发框架 .NET 中间件
ASP.NET Core Web 开发浅谈
本文介绍ASP.NET Core,一个轻量级、开源的跨平台框架,专为构建高性能Web应用设计。通过简单步骤,你将学会创建首个Web应用。文章还深入探讨了路由配置、依赖注入及安全性配置等常见问题,并提供了实用示例代码以助于理解与避免错误,帮助开发者更好地掌握ASP.NET Core的核心概念。
99 3
|
2月前
|
存储 NoSQL Java
分布式session-SpringSession的应用
Spring Session 提供了一种创建和管理 Servlet HttpSession 的方案,默认使用外置 Redis 存储 Session 数据,解决了 Session 共享问题。其特性包括:API 及实现用于管理用户会话、以应用容器中性方式替换 HttpSession、简化集群会话支持、管理单个浏览器实例中的多个用户会话以及通过 headers 提供会话 ID 以使用 RESTful API。Spring Session 通过 SessionRepositoryFilter 实现,拦截请求并转换 request 和 response 对象,从而实现 Session 的创建与管理。
分布式session-SpringSession的应用
|
1月前
|
开发框架 JavaScript 前端开发
一个适用于 ASP.NET Core 的轻量级插件框架
一个适用于 ASP.NET Core 的轻量级插件框架
|
1月前
|
缓存 网络协议 API
分布式系统应用之服务发现!
分布式系统应用之服务发现!
|
1月前
|
NoSQL Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?

热门文章

最新文章