首页
关于
Search
1
我们的邀请函
5 阅读
2
结婚物品事项清单
3 阅读
3
杭州 露营 🏕
3 阅读
4
PVE 虚拟机转 ESXI
3 阅读
5
轻量级.Net Core服务注册工具CodeDi发布啦
2 阅读
默认分类
小白入门
开源项目
深度技术
技术弄潮
数据库
事故总结
万物互联
旅行
户外
notes
生活瞬间
小工具
IT
PMP
登录
Search
标签搜索
欧洲
生活瞬间
航拍
摄影
旅游
自驾
telegram
tdl
申根
防盗
装修
pve
esxi
PMP
Jason Liu
累计撰写
26
篇文章
累计收到
1
条评论
首页
栏目
默认分类
小白入门
开源项目
深度技术
技术弄潮
数据库
事故总结
万物互联
旅行
户外
notes
生活瞬间
小工具
IT
PMP
页面
关于
搜索到
2
篇与
的结果
2022-05-25
数据同步的一些思考与改进
数据同步的一些思考与改进背景闲的没事,自己写了个小网站,搭建在自己国外的VPS上,VPS内存极小(512M),而且还要跑点别的(你懂的),内存更紧张巴巴. 改造之前小网站用到了时髦的Redis,Rabbmitmq,Mysql,那时候阿里云的学生主机内存富足,装这么多中间件压力不大,可到了这样的小内存VPS上,一切都变得水土不服,索性啥中间件都不要了,数据库也不要了.没了数据库,网站的数据从哪里来?存在哪里? 文本形式持久化到本地磁盘?国外的VPS不比国内,可能哪天说不能访问就不能访问了,VPS的磁盘存储显然不踏实.同事给我建议了万能的Github,听过Github托管代码📜,托管静态页面🔮,托管女装大佬💃,但托管网站数据倒是第一次听说,于是我对网站架构进行了重新设计.Plan1 数据的同步小网站数据不多,10M左右,所有数据直接加载到内存中服务器也不会吃力,网站启动,自动从Github Clone数据,并定期把内存中的数据序列化后Push到Github.可以看到,整个过程中,好像没有磁盘啥事了,在我的眼里,Github就是一块延时略高的磁盘(其实延时也还好,国外的Github访问速度飞快).Plan2 同步的频率磁盘的读取速度和内存无法比,何况远程的Github,那么如果减少数据从内存到Github的同步开销呢?显然就是减少同步的频率.一小时同步一次,应该够了.但如果我的网站在这一小时挂了boom🌋,而数据还没来得及同步,那上次一同步到网站挂掉这个时间段内的数据不就没了吗?细思极恐😱!Plan3 多多不益善既然一小时一次不安全,那就一分钟同步一次!其实这样也是有问题的,小网站一般都是无人问津,如果以较高的频率进行数据同步,可以说绝大多数(用互联网的所法是百分之N个9)的数据同步都是没意义的,同时还增大了数据的同步开销,没准Github还会把我的账号给封了.Plan4 内存数据变更立即触发数据同步在我的网站中,有统一的数据访问层,只要数据访问层中的insert,update,delete处加入数据同步事件,即可实现一旦更新立即同步.这样是数据是安全了,可是一次访问请求往往伴随着多次数据更新,每更新一次同步一次,可能是最脑残🙈的做法吧.Question数据更改一次同步一次不合理,同步频率太低数据不安全,频率太高多数同步没有意义,到底该怎样呢?局部性原理在揭开我的设计方案前,我们先来过一下CPU访问存储器时所遵守的局部性原理.在计算机存储介质这个金字塔中,越靠近金字塔顶端,空间越小,但是读取数据越快;越靠近金字塔底端,空间越大,但访问速度也越慢.正式因为这样,所以每次自下而上的数据数据流大小逐层递增, 交换频率逐层递减,如何在时间与空间上取到平衡点是关键.于是有了空间局部性原理和时间局部性原理,力求让计算机的数据流动更高效.空间局部性如果一条数据被访问,那么与它临近的数据也可能要被用到. 比如数组,你访问了索引1上的数据,那么1附近的数据当然很有可能被访问,所以这个时候干脆把1附近的数据也往上加载一个层级.时间局部性如果一条数据项正在被访问,那么在近期它很可能还会被再次访问,所以这个时候干脆就把它留在当前层级,先不急着回收掉.而网站的数据的更新也是具有时间局部性的,像我这样并冷门的网站,基本没人访问,但是一旦访问了,立即就要进行点击量的更新,站点响应速度的记录,没准又会有评论留言,然后要通知管理员进行留言审核.这大概就是不鸣则已,一鸣惊人,一次访问短期内往往立即触发一连串的数据更新,我认为这也是一种时间局部性.所以,在数据同步上,我设计了如下方案. 另起一个线程作为定时任务,主要负责定时数据同步 正常情况下,每小时与Github进行数据同步. 一旦网站数据被更新,检查剩余同步时间是否大于30秒.** 如果大于三十秒,强行把计时器剩余时间设置为30秒.** 如果小于三十秒,不做操作. 计时器时间走完,立即同步数据到Github. 定时沙漏⏳原本文章说到这里就可以结束了,但程序员注定爱代码爱过文字,又恰好我天生爱造轮子,我从令牌桶得到灵感设计了一个乞丐版沙漏计时器,可以用于任何定时任务的执行,班门弄斧,欢迎提出改进意见.Show timepublic class BlogsTimer { private static Stack<int> _upFunnel; //沙漏上部分 private static Stack<int> _downFunnel; //沙漏下部分 private static readonly List<Action> TimerEvents; //定时执行的事件 private static bool _timerSwitch; //沙漏开关 private static readonly int Speed; //每秒消费令牌数量 private static Thread _timerThread; private static readonly object TimerLock; static BlogsTimer() { _upFunnel = new Stack<int>(); _downFunnel = new Stack<int>(); Speed = 1 * 1000; TimerEvents = new List<Action>(); TimerLock = new object(); } //计时器开始 public static void Start(TimeSpan timeSpan) { lock (TimerLock) { _upFunnel.Clear(); _downFunnel.Clear(); for (var i = 0; i < timeSpan.TotalSeconds; i++) { _upFunnel.Push(i); } } _timerSwitch = true; _timerThread = new Thread(Consume); //起一个线程消费桶里的令牌 _timerThread.Start(); LunchEvents(); // 触发事件 } public static void Stop() { _timerSwitch = false; } //给沙漏注册定时执行事件 public static void Register(Action timeEvent) { TimerEvents.Add(timeEvent); timeEvent.Invoke(); } //把沙漏加速到指定的时间 public static void AccelerateTo(TimeSpan timeSpan) { var accelerateSeconds = timeSpan.TotalSeconds; lock (TimerLock) { if (_upFunnel.Count < accelerateSeconds) //当前沙漏中剩余令牌小于设置中秒数,则返回不加速 return; while (_upFunnel.Count > accelerateSeconds && _upFunnel.Count > 1) //令牌数大于秒数,则释放出多余令牌 { _downFunnel.Push(_upFunnel.Pop()); } } } private static void LunchEvents() { TimerEvents.ForEach(a => a.Invoke()); } private static void Consume() { while (_timerSwitch) { lock (TimerLock) { if (_upFunnel.TryPop(out var item)) { _downFunnel.Push(item); } else { LunchEvents(); var tempStack = _downFunnel; //旋转沙漏 _downFunnel = _upFunnel; _upFunnel = tempStack; } } Thread.Sleep(Speed); } } } 源码地址: https://github.com/liuzhenyulive/iBlogs/blob/master/Src/iBlogs.Site.Core/Common/iBlogsTimer.cs演示地址: https://www.iblogs.site
2022年05月25日
1 阅读
0 评论
0 点赞
2022-05-25
轻量级.Net Core服务注册工具CodeDi发布啦
为什么做这么一个工具因为我们的系统往往时面向接口编程的,所以在开发Asp .net core项目的时候,一定会有大量大接口及其对应的实现要在ConfigureService注册到ServiceCollection中,传统的做法是加了一个服务,我们就要注册一次(service.AddService()),又比如,当一个接口有多个实现,在构造函数中获取服务也不是很友好,而据我所知, .Net Core目前是没有什么自带的库或者方法解决这些问题,当然,如果引入第三方容器如AutoFac这些问题时能迎刃而解的,但是如何在不引入第三方容器来解决这个问题呢?所以我就设计了这样的一个轻量级工具.首先,放上该项目的Github地址(记得Star哦!!)https://github.com/liuzhenyulive/CodeDiCodeDi是一个基于 .Net Standard的工具库,它能帮助我们自动地在Asp .net core或者 .net core项目中完成服务的注册.OverviewCodeDi 是 Code Dependency Injection的意思,在上次我在看了由依乐祝写的<.NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了>后,回想起我之前遇到的那些问题,感觉拨云见日,所以,我就开始着手写这个工具了.如何使用CodeDi安装Nuget包CodeDi的Nuget包已经发布到了 nuget.org,您可以通过以下指令在您的项目中安装CodeDiPM> Install-Package CodeDi ConfigureServices中的配置方法 1您可以在Startup的ConfigureService方法中添加AddCodeDi完成对CodeDi的调用.服务的注册CodeDi会自动为您完成. public void ConfigureServices(IServiceCollection services) { services.AddCoreDi(); services.AddMvc(); } 方法 2您也可以在AddCodeDi方法中传入一个Action<CodeDiOptions>参数,在这个action中,您可以对CodeDiOptions的属性进行配置. public void ConfigureServices(IServiceCollection services) { services.AddCoreDi(options => { options.DefaultServiceLifetime = ServiceLifetime.Scoped; }); services.AddMvc(); } 方法 3当然您也可以直接给AddCodeDi()方法直接传入一个CodeDiOptions实例. public void ConfigureServices(IServiceCollection services) { services.AddCoreDi(new CodeDiOptions() { DefaultServiceLifetime = ServiceLifetime.Scoped }); services.AddMvc(); } 你也可以在appsetting.json文件中配置CodeDiOptions的信息,并通过Configuration.Bind("CodeDiOptions", options)把配置信息绑定到一个CodeDiOptions实例.appsetting.json file{ "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*", "CodeDiOptions": { "DefaultServiceLifetime": 1, "AssemblyNames": [ "*CodeDi" ], "AssemblyPaths": [ "C:\\MyBox\\Github\\CodeDI\\CodeDI\\bin\\Debug\\netstandard2.0" ], "IgnoreAssemblies": [ "*Test" ], "IncludeSystemAssemblies": false, "IgnoreInterface": [ "*Say" ], "InterfaceMappings": { "*Say": "*English" }, "ServiceLifeTimeMappings": { "*Say": 0 } } } ConfigureService方法 public void ConfigureServices(IServiceCollection services) { var options=new CodeDiOptions(); Configuration.Bind("CodeDiOptions", options); services.AddCoreDi(options); services.AddMvc(); } CodeDiOptions详解 属性名称 属性描述 数据类型 默认值 AssemblyPaths 在指定目录下加载Dll程序集 string[] Bin目录 AssemblyNames 选择要加载的程序集名称 (支持通配符) string[] * IgnoreAssemblies 忽略的程序集名称 (支持通配符) string[] null IncludeSystemAssemblies 是否包含系统程序集(当为false时,会忽略含有System,Microsoft,CppCodeProvider,WebMatrix,SMDiagnostics,Newtonsoft关键词和在App_Web,App_global目录下的程序集) bool false IgnoreInterface 忽略的接口 (支持通配符) string[] null InterfaceMappings 接口对应的服务 (支持通配符) ,当一个接口有多个实现时,如果不进行配置,则多个实现都会注册到SerciceCollection中 Dictionary<string, string> null DefaultServiceLifetime 默认的服务生命周期 ServuceLifetime( Singleton,Scoped,Transient) ServiceLifetime.Scope ServiceLifeTimeMappings 指定某个接口的服务生命周期,不指定为默认的生命周期 Dictionary<string, ServiceLifetime> null InterfaceMappings如果 ISay 接口有SayInChinese 和SayInEnglish 两个实现,我们只想把SayInEnglish注册到ServiceCollection中 public interface ISay { string Hello(); } public class SayInChinese:ISay { public string Hello() { return "您好"; } } public class SayInEnglish:ISay { public string Hello() { return "Hello"; } } 那么我们可以这样配置InterfaceMappings.options.InterfaceMappings=new Dictionary<string, string>(){{ “ISay”, “SayInChinese” } }也就是{接口名称(支持通配符),实现名称(支持通配符)}ServiceLifeTimeMappings如果我们希望ISay接口的服务的生命周期为Singleton,我们可以这样配置ServiceLifeTimeMappings.options.ServiceLifeTimeMappings = new Dictionary<string, ServiceLifetime>(){{“*Say”,ServiceLifetime.Singleton}};也就是也就是{接口名称(支持通配符),Servicelifetime}关于ServiceLifetime: https://github.com/aspnet/DependencyInjection/blob/master/src/DI.Abstractions/ServiceLifetime.cs获取服务实例当然, 您可以和之前一样,直接在构造函数中进行依赖的注入,但是当某个接口有多个实现而且都注册到了ServiceCollection中,获取就没有那么方便了,您可以用ICodeDiServiceProvider 来帮助您获取服务实例.例如,当 ISay 接口有 SayInChinese 和 SayInEnglish两个实现, 我们我们如何获取我们想要的服务实例呢? public interface ISay { string Hello(); } public class SayInChinese:ISay { public string Hello() { return "您好"; } } public class SayInEnglish:ISay { public string Hello() { return "Hello"; } } public class HomeController : Controller { private readonly ISay _say; public HomeController(ICodeDiServiceProvider serviceProvider) { _say = serviceProvider.GetService<ISay>("*Chinese"); } public string Index() { return _say.Hello(); } } ICodeDiServiceProvider.GetService<T>(string name=null)参数中的Name支持通配符.CodeDi如何实现的?既然是一个轻量级工具,那么实现起来自然不会太复杂,我来说说比较核心的代码. private Dictionary<Type, List<Type>> GetInterfaceMapping(IList<Assembly> assemblies) { var mappings = new Dictionary<Type, List<Type>>(); var allInterfaces = assemblies.SelectMany(u => u.GetTypes()).Where(u => u.IsInterface); foreach (var @interface in allInterfaces) { mappings.Add(@interface, assemblies.SelectMany(a => a.GetTypes(). Where(t => t.GetInterfaces().Contains(@interface) ) ) .ToList()); } return mappings; } GetInterfaceMapping通过反射机制,首先获取程序集中的所有接口allInterfaces,然后遍历allInterfaces找到该接口对应的实现,最终,该方法返回接口和实现的匹配关系,为Dictionary<Type, List>类型的数据. private void AddToService(Dictionary<Type, List<Type>> interfaceMappings) { foreach (var mapping in interfaceMappings) { if (mapping.Key.FullName == null || (_options.IgnoreInterface != null && _options.IgnoreInterface.Any(i => mapping.Key.FullName.Matches(i)))) continue; if (mapping.Key.FullName != null && _options.InterfaceMappings != null && _options.InterfaceMappings.Any(u => mapping.Key.FullName.Matches(u.Key))) { foreach (var item in mapping.Value.Where(value => value.FullName != null). Where(value => value.FullName.Matches(_options.InterfaceMappings.FirstOrDefault(u => mapping.Key.FullName.Matches(u.Key)).Value))) { AddToService(mapping.Key, item); } continue; } foreach (var item in mapping.Value) { AddToService(mapping.Key, item); } } } 该方法要判断CodeDiOptions中是否忽略了该接口,同时,是否指定实现映射关系.什么叫实现映射关系呢?参见InterfaceMappings如果指定了,那么就按指定的来实现,如果没指定,就会把每个实现都注册到ServiceCollection中. private readonly IServiceCollection _service; private readonly CodeDiOptions _options; private readonly ServiceDescriptor[] _addedService; public CodeDiService(IServiceCollection service, CodeDiOptions options) { _service = service ?? throw new ArgumentNullException(nameof(service)); _options = options ?? new CodeDiOptions(); _addedService = new ServiceDescriptor[service.Count]; service.CopyTo(_addedService, 0); //在构造函数中,我们通过这种方式把Service中已经添加的服务读取出来 //后面进行服务注册时,会进行判断,避免重复添加 } private void AddToService(Type serviceType, Type implementationType) { ServiceLifetime serviceLifetime; try { serviceLifetime = _options.DefaultServiceLifetime; if (_options.ServiceLifeTimeMappings != null && serviceType.FullName != null) { var lifeTimeMapping = _options.ServiceLifeTimeMappings.FirstOrDefault(u => serviceType.FullName.Matches(u.Key)); serviceLifetime = lifeTimeMapping.Key != null ? lifeTimeMapping.Value : _options.DefaultServiceLifetime; } } catch { throw new Exception("Service Life Time Only Can be set in range of 0-2"); } if (_addedService.Where(u => u.ServiceType == serviceType).Any(u => u.ImplementationType == implementationType)) return; _service.Add(new ServiceDescriptor(serviceType, implementationType, serviceLifetime)); } AddToService中,要判断有没有对接口的生命周期进行配置,参见ServiceLifeTimeMappings,如果没有配置,就按DefaultServiceLifetime进行配置,DefaultServiceLifetime如果没有修改的情况下时ServiceLifetime.Scoped,即每个Request创建一个实例. private readonly IServiceProvider _serviceProvider; public CodeDiServiceProvider(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public T GetService<T>(string name) where T : class { return _serviceProvider.GetService<IEnumerable<T>>().FirstOrDefault(u => u.GetType().Name.Matches( name)); } 这CodeDiServiceProvider的实现代码,这里参考了依乐祝写的<.NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了>给出的一种解决方案,即当某个接口注册了多个实现,其实可以通过IEnumerable获取所有的实现,CodeDiServiceProvider对其进行了封装.Enjoy it只要进行一次简单的CodeDi配置,以后系统中添加了新的接口以及对应的服务实现后,就不用再去一个个地Add到IServiceCollection中了.如果有问题,欢迎Issue,欢迎PR.最后,赏个Star呗! 前往Star
2022年05月25日
2 阅读
0 评论
0 点赞