2024年9月

说明

该文章是属于OverallAuth2.0系列文章,每周更新一篇该系列文章(从0到1完成系统开发)。

该系统文章,我会尽量说的非常详细,做到不管新手、老手都能看懂。

说明:OverallAuth2.0 是一个简单、易懂、功能强大的权限+可视化流程管理系统。

有兴趣的朋友,请关注我吧(*^▽^*)。

使用前提

1、Visual Studio使用2022版本

搭建项目

OverallAuth2.0依然和OverallAuth1.0一样,采用前后端分离模式,所以搭建后端,我们选择  .net core web api(如下图)


选择项目模板后,我们点击【下一步】

选择.net 8.0(最新长期支持版本),随后创建项目

默认项目结构如下图

直接运行,查看默认界面

运行起来可以看到,系统默认的swagger界面非常简介,也少了很多信息比如:

1、系统说明、版本、作者等。

2、接口的描述、参数等信息。

3、接口的分类等。

优化Swagger

上面说道,系统默认的接口文档是非常简洁的,接下来我们在系统中,这样做,让swagger看起来更优美和专业。

在项目下新增一个文件件PlugInUnit,然后再该文件夹下新建一个类SwaggerPlugInUnit


建好SwaggerPlugInUnit后,在webapi同级建一个类库Utility,用于存放系统的辅助工具等,然后再该类库下建一个Enum文件夹,并新建ModeuleGroupEnum该枚举。

如图:

建好文件后,在ModeuleGroupEnum文件中写一个枚举SysMenu,并保存。

/// <summary>
///模块分组/// </summary>
public enumModeuleGroupEnum
{
SysMenu
= 1,
}

随后在SwaggerPlugInUnit中编写一个方法,具体代码如下

/// <summary>
///swagger插件/// </summary>
public static classSwaggerPlugInUnit
{
/// <summary> ///初始化Swagger/// </summary> /// <param name="services"></param> public static void InitSwagger(thisIServiceCollection services)
{
//添加swagger services.AddSwaggerGen(optinos =>{typeof(ModeuleGroupEnum).GetEnumNames().ToList().ForEach(version =>{
optinos.SwaggerDoc(version,
newOpenApiInfo()
{
Title
= "权限管理系统",
Version
= "V2.0",
Description
= "求关注,求一键三连",
Contact
= new OpenApiContact { Name = "微信公众号作者:不只是码农 b站作者:我不是码农呢", Url = new Uri("http://www.baidu.com") }
});

});
//反射获取接口及方法描述 var xmlFileName = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
optinos.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFileName),
true);

});
}
/// <summary> ///swagger加入路由和管道/// </summary> /// <param name="app"></param> public static void InitSwagger(thisWebApplication app)
{
app.UseSwagger();
app.UseSwaggerUI(options
=>{typeof(ModeuleGroupEnum).GetEnumNames().ToList().ForEach(versoin =>{
options.SwaggerEndpoint($
"/swagger/{versoin}/swagger.json", $"接口分类{versoin}");
});
});
}
}
然后再program中使用自定义swagger中间件

做好以上步骤,我们的swagger基本算是搭建好了,只需要在控制器上方,添加路由和分组。

 /// <summary>
 ///系统模块/// </summary>
[ApiController]
[Route(
"api/[controller]/[action]")]
[ApiExplorerSettings(GroupName
= nameof(ModeuleGroupEnum.SysMenu))]

做完以上这些,我们对swagger就算优化完成,只需要运行系统,就可以查看效果

注意:
必须生成接口的xml文件,不然会报错。

好了,以上就是搭建WebApi+优化Swagger的全部过程,你快来试试吧

如果对你有帮助,请关注我吧(*^▽^*)。

源代码地址:https://gitee.com/yangguangchenjie/overall-auth2.0-web-api

帮我Star,谢谢。

有兴趣的朋友,请关注我吧(*^▽^*)。

关注我:一个全栈多端的宝藏博主,定时分享技术文章,不定时分享开源项目。关注我,带你认识不一样的程序世界

前言

这个考题的大部分内容,我感觉都是我们会的,但所有的考题都穿上了马甲,穿上马甲我们就不好认了,而且如果是一个两个人穿马甲,还好推断,如果1000人穿马甲,你识别的概率就会急速下降。
有些题的内容则是即无法识别,也无法背,因为它也没有个前因后果,完全是出题人拍脑袋想的,所以,这种题我们是无法通过知识来判断的,因为用知识来判断,你会发现,四个选项全是正确的,这时我们可以采用逐字读题法,就是一个字一个字的读,然后把答案分别放里,看看哪个读起来更顺。

  • 例如这题:
    信息系统规划方法中,关键成功因素法通过对关键成功因素的识别,找出实现目标所需要的关键信息集合,从而确定系统开发的()。关键成功因素来源于组织的目标,通过组织的目标分解和关键成功因素识别、( )识别,一直到产生数据字典。
    A.系统边界 B. 功能指标 C.优先次序 D.性能指标
    逐字阅读,会发现C和D适合俩个空。

正题

嵌入式系统

就是安卓系统,安装系统是计算模式。

  • 混成系统
    嵌入式系统子类
    就是安卓系统加入了传感器的SDK,就是汽车上的安卓系统

控制模式

  • 计算模式控制
    就是cpu分配模式,合理利用资源,缺点一瘫痪全瘫痪。
  • 同步异步控制
    就是同步异步,缺点就是会有延迟,优点是一个延迟不影响别人。市面的大系统没有这模式的,这模式一般是我们开发的系统,比如交易,流水线等等。

tcp端口号作用

这个比较冷门,是对应用层进程寻址用的,我们写tcp代码时是要写端口号的,但那个是属于tcp协议部分,tcp协议用那个端口号建立连接,如果单问端口号的作用,那它就是应用层进程寻址用的。

DHCP

DHCP(Dynamic Host Configuration Protocol)就是自动给电脑分配IP的一个协议,电脑插上网线就会发这个协议,路由器收到后就会回一个消息,给电脑一个ip,然后我们使用ipconfig命令就能看到IP地址了。

概要设计和详细设计

  • 概要设计:讲软件需求转化为数据结构和软件系统结构。
  • 详细设计:过程设计,通过对结构细化,得到软件详细数据结构和算法。
    注意1:这里的数据结构不是我们认知中的数据结构,他是指流程图,我们在写概要设计时,即使再省略,也还是要写流程图的。
    注意2:详细设计的算法,就是我们写的伪代码,加减乘除,连表查询在这里都是算法。

软件结构化设计

就是网站开发,好听点就是叫网站应用开发,或者高级点ERP,MIS,CRM。
步骤是:系结构设计、接口设计、数据设计和过程设计。
这个其实是特指java+vue的前后台分离开发中的java后台接口项目的设计。具体对应真实操作如下:
系结构设计:框架开发。
接口设计:controller设计。
数据设计:数据库设计。
过程设计:代码开发。

软件逆向工程

软件逆向工程导出信息的四个抽象层次:
A.实现级:代码
B.结构级:函数
C.功能级:类
D.领域级:package包/命名空间

测试方法

强度测试:是在系统资源特别低的情况下考查软件系统极限运行情况。
负载测试:用于测试超负荷环境中程序是否能够承担。通过负载测试,确定在各种工作负载下系统的性能,目标是测试当负载逐渐增加时,系统各项性能指标的变化情况。
压力测试:通过确定系统的瓶颈或不能接收的性能点,来获得系统能够提供的最大服务级别的测试。负载测试和压力测试可以结合进行,统称为负载压力测试。
容量测试:并发测试也称为容量测试,主要用于测试系统可同时处理的在线最大用户数量。

  • 强度测试:测试性能,测系统卡不卡,什么时候卡。
  • 负载测试:测可靠性,测系统挂不挂,什么情况挂,理解为用人的数量测。
  • 压力测试:测可靠性,测系统挂不挂,什么情况挂,理解为用接口调用次数测。
  • 容量测试:测并发数。

设计方法

ABSD/ABSDM——基于体系结构的软件设计,这个应该是必考的,需要背。
其步骤如下:
【需求=>设计=>文档化=>复审=>实现=>演化】
这个设计模式还是挺好的,可以学习一下,我以前也写过关于他的文章。
ABSD/ABSDM

软件方法学是

软件方法学是以软件开发方法为研究对象的学科。
自定向下开发方法:是先对最高层次中的问题.进行定义、设计、编程和测试,而将其中未解决的问题作为一个子任务放到下一层次中去解决。
自底向上开发方法:是根据系统功能要求,从具体的器件、逻辑部件或者相似系统开始,通过对其进行相互连接、修改和扩大,构成所要求的系统。
形式化开发方法:是建立在严格数学基础上的软件开发方法。
非形式化方法(代表-头脑风暴):不把严格性作为其主要着眼点,通常以各种开发模型的形式得以体现。从适应范围来看,可分为整体性方法与局部性方法。适用于软件开发全过程的方法称为整体性方法;适用于开发过程某个具体阶段的软件方法称为局部性方法。

必死题

关于模块化设计,( )是错误的。
A.模块是指执行某一特定任务的数据结构和程序代码
B.模块的接口和功能定义属于其模块自身的内部特性
C.每个模块完成相对独立的特定子功能,与其他模块之间的关系最简单
D.模块设计的重要原则是高内聚、低耦合

首先要理解,这里的模块是[专指]java的项目下的子项目,就是IDEA里的new->module。
这道题答案是B,但4个答案都是正确的,而且是那种完全正确的那种。
解析:

模块的接口是模块与其他模块进行交互的部分,所以接口的定义不仅仅属于其模块自身的内部特性,与外部模块也具有相关性。B 选项的描述是错误的。

这是纯纯的文字游戏,按这种解析方式,我也可以把上面4个答案都解析成错的。
那也就是说,今年再出这题,换一个解析方式,答案就可以是A,C,D。
比如模块设计的重要原则是不仅仅是高内聚、低耦合,还有职责分离原则。所以D是错的,这也能解释的过去。
所以这样的题,就只能放弃了,必死题。

构件组装是指将库中的构件经适当修改后相互连接构成新的目标软件。()不属于构件组装技术。
A.基于功能的构件组装技术
B. 基于数据的构件组装技术
C.基于实现的构件组装技术
D.面向对象的构件组装技术

解析:

构件组装技术大致可分为基于功能的组装技术、基于数据的组装技术和面向对象的组装技术。
答案:C。

构件组装有这个概念,但基于xxx的构件组装技术,这是纯纯的他们凭空想象出来的概念,这种题一般都只考一次,因为明年他们会再想一个新的概念考你,背也没用,首先不会再考,其次,现实中没这定义。


注:此文章为原创,任何形式的转载都请联系作者获得授权并注明出处!



若您觉得这篇文章还不错,请点击下方的【推荐】,非常感谢!

https://www.cnblogs.com/kiba/p/18409540

引言

这些年在自研产品,对于如何做好产品进行了一些思考。随着开源软件的蓬勃发展,许多开源项目已经成为IT行业的核心组成部分。像Redis、Nginx、SQLite、Elasticsearch这些知名的开源软件,已经成为了开发者的首选工具。这些开源软件不仅在技术性能上取得了重大突破,还在社区建设、生态系统、商业模式等方面获得了广泛的成功。那么,是什么因素促使它们取得了如此巨大的成功?它们的成功给技术人员和开发者带来了哪些启示?

本文将从技术优势、社区建设、灵活性与扩展性、开源精神、商业化等多个角度来分析Redis、Nginx、SQLite、Elasticsearch等开源软件的成功原因,进而探讨这些成功背后的普遍规律,并为IT技术人员提供一些有益的启示。

一、Redis的成功分析

Redis是一个高性能的Key-Value存储系统,广泛应用于高速缓存、消息队列、会话管理、计数器等场景中。

  1. 极致的性能优化
    Redis的核心优势在于其卓越的性能。作为内存级存储系统,Redis能够在毫秒级内响应大规模请求。其底层的数据结构经过精心设计,能够支持多种复杂的操作,如列表、集合、哈希、排序集等。这使得Redis不仅能够存储简单的键值对,还能够支持更多复杂的数据操作场景。

    Redis的成功在于充分利用内存的性能优势,提供了极高的读写速度。技术人员应当关注如何优化软件以达到极致性能,以及如何通过选择合适的底层数据结构来提升效率。

  2. 简单易用的API设计
    Redis的API设计简单明了,开发者可以快速上手,无需深入学习复杂的操作。Redis通过简单的命令行工具和客户端库,帮助开发者轻松地在应用中集成Redis。

    API设计的简洁性和易用性是软件成功的关键因素之一。技术人员在设计系统时,应注重接口的友好性和学习曲线的平缓性,避免过度复杂化。

  3. 丰富的功能集与灵活性
    Redis不仅提供基本的Key-Value存储,还支持持久化、主从复制、分片集群等功能。它的模块化设计让Redis可以被灵活扩展,满足不同应用场景的需求。

    开源软件的灵活性和可扩展性使其在不同应用场景下都能够发挥作用。技术人员应关注如何通过模块化设计提高系统的扩展性,确保系统能够适应业务需求的变化。

  4. 强大的社区支持
    Redis的开源社区非常活跃。大量的开发者、公司为Redis贡献代码,改进功能,修复bug,并提供了丰富的文档和教程。这为Redis的发展提供了持续的动力和保障。

    一个活跃的开源社区是项目成功的关键。技术人员不仅应积极参与开源社区,学习他人的经验,还可以通过贡献代码、提交PR来提升自己的影响力。

二、Nginx的成功分析

Nginx作为一款高性能的Web服务器,已经成为现代互联网基础设施不可或缺的组成部分。

  1. 高并发处理能力
    Nginx的非阻塞、事件驱动架构使其在高并发场景下表现尤为出色。与传统的多线程或多进程模型相比,Nginx能够更加高效地处理大量的连接请求。

    Nginx的成功表明,选择适合的并发处理模型对系统的性能至关重要。技术人员在设计高并发系统时,应权衡不同的架构模型,选择最适合的解决方案。

  2. 模块化架构
    Nginx的模块化设计使得其可以通过插件扩展功能,如负载均衡、反向代理、静态资源服务、SSL/TLS加密等。用户可以根据实际需求定制Nginx的功能,减少不必要的资源消耗。

    模块化设计是系统灵活性的关键。技术人员应在设计系统时确保各个功能模块的可插拔性,以提高系统的适应性。

  3. 开源社区与商业化平衡
    Nginx不仅是一个开源项目,还通过Nginx Plus提供了商业化支持。Nginx Plus提供了高级的负载均衡、监控、故障转移等功能,帮助公司通过商业化的方式盈利。

    Nginx的成功表明,开源项目可以通过适度的商业化获得长期的发展动力。技术人员应思考如何在开源与商业化之间找到平衡,以推动项目的持续发展。

  4. 轻量级设计
    Nginx的轻量级设计使其在资源占用上表现优异。与其他Web服务器相比,Nginx在内存和CPU消耗上更具优势,这使得它成为资源有限的服务器环境中的首选。

    轻量级设计不仅可以提升系统的运行效率,还可以降低资源消耗。技术人员应关注如何通过优化设计来减少系统对资源的依赖。

三、SQLite的成功分析

SQLite是一款嵌入式的轻量级数据库,被广泛应用于移动设备、浏览器等资源有限的环境中。

  1. 零配置的便捷性
    SQLite不需要安装、配置,也不依赖于单独的数据库服务。所有的数据都存储在一个文件中,这使得SQLite在嵌入式系统、移动设备中得到了广泛应用。

    便捷性是影响软件采用率的重要因素。技术人员在开发软件时,应该尽量减少用户的配置和管理成本,提升软件的易用性。

  2. 高效的存储引擎
    虽然SQLite是一个轻量级数据库,但它提供了ACID事务支持,具备较高的可靠性和安全性。SQLite通过高度优化的数据结构和存储算法,提供了优异的读写性能,即便在资源有限的设备上也能运行良好。

    技术人员应注重性能与资源消耗之间的平衡,尤其是在嵌入式和移动设备等受限环境中。

  3. 广泛的应用场景
    SQLite不仅仅是嵌入式数据库,它在很多场景中都表现出色,如开发测试、移动应用本地存储等。它的简洁性和高性能使得它在众多不同领域中得到了广泛应用。

    软件的成功很大程度上取决于其适用的场景越广泛,技术人员在设计系统时可以考虑通用性和灵活性,确保其能够在多种环境中使用。

四、Elasticsearch的成功分析

Elasticsearch是一个基于Lucene的分布式搜索引擎,主要用于全文搜索、日志分析和数据可视化。

  1. 强大的搜索和分析能力
    Elasticsearch基于倒排索引,能够以极快的速度处理全文检索。这使得它在需要高效搜索的场景中具有独特的优势,如日志分析、产品搜索等。同时,Elasticsearch还提供了聚合功能,能够进行复杂的数据分析。

    技术人员应了解底层技术如何为高性能需求提供支持,尤其是在需要快速检索和处理大量数据时,选择适合的算法和数据结构至关重要。

  2. 分布式架构
    Elasticsearch通过分片和副本机制,能够轻松地实现水平扩展,适应海量数据的存储和查询需求。它的分布式架构不仅提高了性能,还增强了系统的容错能力。

    分布式架构是大规模系统设计的核心。技术人员应学习如何通过分片、复制等技术来提高系统的扩展性和容错性。

  3. 开放的生态系统
    Elasticsearch与Kibana、Logstash、Beats等工具组合形成了Elastic Stack,提供了从数据采集、分析到可视化的完整解决方案。这种生态系统让用户能够构建复杂的日志分析和监控平台。

    构建完整的生态系统可以为用户提供更多的价值,技术人员应思考如何通过集成不同工具来为用户提供完整的解决方案。

  4. 活跃的开源社区与企业支持
    Elasticsearch由Elastic公司主导开发,同时吸引了大量开发者参与。社区的活跃推动了Elasticsearch的快速迭代和功能完善。同时,Elastic公司也通过提供企业支持、云服务等方式实现商业化。

    技术人员应重视开源社区的力量,通过社区的支持和反馈来不断完善软件,同时也可以通过商业化模式实现开源项目的可持续发展。

五、开源软件成功的普遍规律

通过分析Redis、Nginx、SQLite、Elasticsearch等
开源软件的成功,我们可以总结出以下几个普遍规律:

1.
技术上的卓越性能

无论是Redis的高性能内存操作、Nginx的高并发处理、SQLite的轻量级存储,还是Elasticsearch的快速全文检索,这些开源软件都在各自的技术领域中表现出了卓越的性能。开源项目的核心竞争力通常来自其在特定技术领域的深耕与创新。通过高效的算法设计、架构优化和底层技术的创新,它们能够在解决特定问题时提供远超竞争对手的性能。

启示:
技术人员在开发软件时,应深入理解问题领域,并针对性地进行性能优化和创新,打造出具备独特竞争力的核心技术。通过对底层技术的优化,可以实现更高效、更具优势的解决方案。

2.
简洁和易用性

成功的开源项目通常都非常注重用户体验和易用性。Redis和SQLite的API设计简洁明了,Nginx的配置相对简单,Elasticsearch提供了直观的接口用于复杂查询操作。这些项目的开发者意识到,过度复杂的工具虽然功能强大,但会吓退一大部分用户。因此,他们在开发软件时始终保持简洁易用的设计原则,让用户能够迅速上手。

启示:
在软件开发中,设计简洁易用的接口是非常重要的。技术人员不仅要考虑如何让软件功能强大,还要思考如何让用户以最小的学习成本使用这些功能。提供良好的文档、示例代码和简单的操作界面,可以大大提升软件的可用性和受欢迎程度。

3.
模块化与可扩展性

这些开源软件都具备良好的模块化设计和可扩展性。Nginx通过模块扩展不同功能,Elasticsearch通过插件和分布式架构扩展搜索能力,Redis也支持多种数据结构和功能模块。模块化设计不仅让软件在初期保持轻量级,同时也让用户可以根据需求动态扩展软件的功能,使其适应不断变化的场景和需求。

启示:
模块化设计是构建灵活、可扩展软件的基础。技术人员在设计系统时应确保各个功能模块相互独立,这样可以方便后期扩展、优化或替换某个模块而不影响整个系统。同时,模块化还可以减少初期的开发工作量,让项目尽早投入使用并根据需求逐步扩展。

4.
活跃的开源社区

Redis、Nginx、Elasticsearch等开源软件的成功离不开它们背后强大的开源社区。这些社区不仅为项目提供了丰富的代码贡献、功能扩展和bug修复,还通过文档编写、在线支持等方式帮助新用户快速上手使用软件。一个活跃的社区不仅可以为项目提供技术上的支持,还能够带来更广泛的传播和用户基础。

启示:
技术人员应意识到,社区的力量是不可忽视的。在使用开源项目时,积极参与社区交流,贡献代码、文档或帮助解答问题,可以不仅提高个人的技术能力,也能获得更多业界的认可和机会。同时,如果你正在开发自己的开源项目,社区的建立和维护将是项目长期发展的关键。

5.
适度的商业化

许多成功的开源软件项目通过适度的商业化实现了自我可持续发展。Nginx有商业版Nginx Plus,Elasticsearch由Elastic公司提供企业支持,Redis Labs也提供托管和支持服务。这些公司在保持开源版本活跃发展的同时,通过提供额外的商业服务和支持实现盈利。这样的商业模式让开源项目既能保持技术创新,又能通过商业化手段维持项目的长期健康发展。

启示:
开源软件并不意味着完全免费。技术人员在开发开源项目时,可以探索如何在保持开源精神的前提下,实现适度的商业化,以此来推动项目的持续创新和发展。通过提供增值服务、企业支持、云托管等形式,既可以满足大企业的需求,也能为项目的开发者提供收益。

6.
广泛的应用场景

成功的开源软件通常都具备广泛的应用场景。Redis被用于缓存、消息队列、分布式锁等多个场景,Nginx广泛用于Web服务器、反向代理和负载均衡,Elasticsearch可以处理日志分析、数据检索和商业搜索应用。软件的通用性不仅帮助它们吸引了更多的用户,还使得它们在各种业务场景中得到了长期的应用和推广。

启示:
软件的通用性和适用性是其广泛传播的基础。技术人员在设计系统时,可以尝试考虑如何让软件适应更多的场景和应用需求,以此扩大其用户基础。多功能性和灵活性不仅让软件具备更强的竞争力,也可以推动其在不同市场和行业中获得成功。

7.
与时俱进的迭代更新

开源软件的成功离不开持续的迭代更新。Redis、Nginx、Elasticsearch等项目都在不断根据用户的反馈和市场需求进行功能更新和优化。快速响应社区需求并提供及时的修复和优化让这些项目始终处于技术前沿。

启示:
持续迭代更新是软件项目保持生命力的关键。技术人员在开发项目时,应该保证项目的灵活性和可扩展性,并通过持续更新和维护来满足用户的需求。定期推出更新版本、修复已知问题并添加新功能,可以让项目始终保持活力和竞争力。

8.
扎实的文档和教学资源

成功的开源项目通常都具备丰富的文档和教学资源。Redis和Elasticsearch提供了详细的API文档和使用指南,Nginx和SQLite也有丰富的在线资源供开发者参考。良好的文档和教学资源让用户可以快速上手,并在遇到问题时能够轻松查找解决方案。

启示:
技术文档和教学资源的质量对于软件的推广至关重要。技术人员在开发软件时,不仅要关注核心功能的实现,还应投入足够的精力来编写清晰、详尽的文档,提供示例代码和教程,以便用户能够顺利使用软件。优质的文档不仅可以减少用户的学习成本,还能够减少技术支持的工作量。

六、对IT技术人员的启示

通过对Redis、Nginx、SQLite、Elasticsearch等开源软件的分析,我们可以总结出对IT技术人员的一些重要启示:

  1. 深耕技术领域,优化性能
    :成功的软件往往在某一技术领域中表现出色,性能优化是其核心竞争力。IT技术人员应在自己的领域内不断深入学习和研究,注重软件的性能提升,成为技术专家。

  2. 简洁而强大的设计
    :软件设计应简洁易用,同时具备强大的功能。通过简化接口和操作,降低用户的使用门槛,能够吸引更多的用户使用你的软件。

  3. 注重可扩展性与模块化设计
    :在设计软件时,保持模块化和灵活性,让系统能够根据需求扩展和优化。这样的设计不仅能够适应未来的业务变化,还能够让系统具备更长的生命周期。

  4. 积极参与开源社区
    :开源社区是技术人员学习、交流和提升的重要平台。积极参与开源项目,贡献代码、撰写文档、帮助他人,能够提升个人的技术能力和行业影响力。

  5. 适度探索商业化模式
    :即使是开源项目,也可以通过商业化手段实现长期发展。IT技术人员应思考如何通过增值服务或企业支持来为开源项目找到可持续发展的路径。

  6. 保持持续更新与创新
    :快速响应用户反馈,持续迭代更新软件,是保持竞争力的关键。IT技术人员应培养敏锐的技术嗅觉,不断学习新技术,推动项目的持续创新。

  7. 编写优质的文档
    :文档质量是软件推广的基础之一。技术人员不仅要开发出高质量的软件,还要投入足够的精力编写清晰、详尽的文档,让更多的用户能够快速上手并使用。

结论

Redis、Nginx、SQLite、Elasticsearch等开源软件的成功并非偶然,它们在技术性能、社区建设、灵活性与扩展性、商业化等方面的成功经验为IT技术人员提供了宝贵的启示。通过不断追求卓越的技术性能,保持简洁而灵活的设计,积极参与开源社区,并探索适度的商业化路径,技术人员不仅可以提升个人能力,还能推动自己参与

状态模式(State Pattern)的定义是这样的:
类的行为是基于它的状态改变的。

注意这里的状态不是狭义的指对象维护了一个“状态”字段,我们传入了不同的枚举值,对象整体的表现行为(对外方法)就改变了。
而是指内部的(任意)字段如果发生了变化,那么它的状态就变了,那么它对外的表现形式就变了。
它是面向对象的23种设计模式中的一种,属于行为模式的范围。
通常我们在解决不同状态下,对外方法的不同表现时,可以定义若干的枚举,然后写一大堆if、 elseif、 switch等选择命令来区分不同的状态,然后走不同的业务分支。
而状态模式是支持将这些分支业务抽离出一个独立类(状态类),我们通过传入不同的状态类,就可以动态的执行不同的业务方法。
整体的结构大概是这样的:

业务类维护了一个内部状态对象,这个状态对象支持由外部传入,切换为不同的状态对象。
而这些状态对象都统一实现了具体的方法,业务类内部在执行业务方法时,会调用这些状态对象中实现的方法。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )这样在切换状态时,业务方法就会调用不同的状态对象的方法了。从面向对象的角度,实现了状态变化,类行为的同步变化。
来看一个具体的代码示例:

枚举类

1 packagecom.example.demo.learn.pattern.behavior.status;2 
3 public enumTextStatusEnum {4 ONLY_READ,5 READ_WRITE,6 UNAVAILABLE;7 
8 }

状态定义接口

1 packagecom.example.demo.learn.pattern.behavior.status;2 
3 /**
4 * @discription5  */
6 public interfaceTextState {7 TextStatusEnum getStatus();8 
9      voidwrite(String content);10 
11      voidclear();12 
13 String read();14 
15      voidsetContent(StringBuilder sb);16 }

只读状态

1 packagecom.example.demo.learn.pattern.behavior.status;2 
3 importlombok.Data;4 importlombok.extern.slf4j.Slf4j;5 
6 /**
7 * @discription8  */
9 @Slf4j10 @Data11 public class OnlyReadState implementsTextState {12     private static final TextStatusEnum textStatus =TextStatusEnum.ONLY_READ;13 
14     privateStringBuilder sb;15 
16 @Override17     publicTextStatusEnum getStatus() {18         returntextStatus;19 }20 
21     public voidwrite(String content) {22         log.error("sorry, you can not write");23 }24 
25     public voidclear() {26         log.error("sorry, you can not clear");27 }28 
29     publicString read() {30         returnsb.toString();31 }32 
33 @Override34     public voidsetContent(StringBuilder sb) {35         this.sb =sb;36 }37 }

读写状态

1 packagecom.example.demo.learn.pattern.behavior.status;2 
3 importlombok.Data;4 importlombok.extern.slf4j.Slf4j;5 
6 /**
7 * @discription8  */
9 @Data10 @Slf4j11 public class ReadWriteState implementsTextState {12     private static final TextStatusEnum textStatus =TextStatusEnum.ONLY_READ;13 
14     private StringBuilder sb = newStringBuilder();15 
16 @Override17     publicTextStatusEnum getStatus() {18         returntextStatus;19 }20 
21     public voidwrite(String content) {22 sb.append(content);23 }24 
25     public voidclear() {26         sb.setLength(0);27 }28 
29     publicString read() {30         returnsb.toString();31 }32 
33 @Override34     public voidsetContent(StringBuilder sb) {35         this.sb =sb;36 }37 }

本文编辑器(业务类/上下文)

1 packagecom.example.demo.learn.pattern.behavior.status;2 
3 importlombok.Data;4 importlombok.extern.slf4j.Slf4j;5 
6 /**
7 * @discription8  */
9 @Slf4j10 public classTextEditor {11 
12     private StringBuilder sb = newStringBuilder();13 
14     privateTextState textState;15 
16     public voidsetState(TextState textState) {17 textState.setContent(sb);18         this.textState =textState;19 }20 
21     public voidwrite(String content) {22         if (textState == null) {23             log.error("no state exist");24             return;25 }26 textState.write(content);27 }28 
29     public voidclear() {30         if (textState == null) {31             log.error("no state exist");32             return;33 }34 textState.clear();35 }36 
37     publicString read() {38         if (textState == null) {39             log.error("no state exist");40             return "no state";41 }42         returntextState.read();43 }44 
45 }

主类

1 packagecom.example.demo.learn.pattern.behavior.status;2 
3 importlombok.extern.slf4j.Slf4j;4 
5 /**
6 * @discription7  */
8 @Slf4j9 public classPatternMain {10     public static voidmain(String[] args) {11         TextEditor editor = newTextEditor();12 String text;13 
14         //可读写状态
15         TextState rw = newReadWriteState();16 editor.setState(rw);17         for (int i = 0; i < 3; i++) {18             editor.write("write" +i);19             text =editor.read();20             log.warn("read :" +text);21 }22 editor.clear();23         text =editor.read();24         log.warn("after clear, we read :" +text);25         editor.write("last write");26 
27         log.warn("-----------------------now, we exchange state to only read-----------------------");28         //只读状态
29         TextState or = newOnlyReadState();30 editor.setState(or);31         for (int i = 0; i < 3; i++) {32             editor.write("write" +i);33             text =editor.read();34             log.warn("read :" +text);35 }36 editor.clear();37         text =editor.read();38         log.warn("after clear, we read :" +text);39 }40 }

输出效果如下:

10:02:52.356 [main] WARN com.example.demo.learn.pattern.behavior.status.PatternMain -read :write010:02:52.368 [main] WARN com.example.demo.learn.pattern.behavior.status.PatternMain -read :write0write110:02:52.369 [main] WARN com.example.demo.learn.pattern.behavior.status.PatternMain -read :write0write1write210:02:52.371 [main] WARN com.example.demo.learn.pattern.behavior.status.PatternMain -after clear, we read :(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )10:02:52.372 [main] WARN com.example.demo.learn.pattern.behavior.status.PatternMain - -----------------------now, we exchange state to only read-----------------------
10:02:52.376 [main] ERROR com.example.demo.learn.pattern.behavior.status.OnlyReadState -sorry, you can not write10:02:52.378 [main] WARN com.example.demo.learn.pattern.behavior.status.PatternMain -read :last write10:02:52.378 [main] ERROR com.example.demo.learn.pattern.behavior.status.OnlyReadState -sorry, you can not write10:02:52.378 [main] WARN com.example.demo.learn.pattern.behavior.status.PatternMain -read :last write10:02:52.379 [main] ERROR com.example.demo.learn.pattern.behavior.status.OnlyReadState -sorry, you can not write10:02:52.379 [main] WARN com.example.demo.learn.pattern.behavior.status.PatternMain -read :last write10:02:52.379 [main] ERROR com.example.demo.learn.pattern.behavior.status.OnlyReadState -sorry, you can not clear10:02:52.380 [main] WARN com.example.demo.learn.pattern.behavior.status.PatternMain -after clear, we read :last write

Process finished with exit code
0

我们可以看到在最初设置读写状态后,可以做读、写、清除等操作

在设置读状态后则只能读了。

这样回头来看,其实我们就是将不同if Switch的选择分支,连同选择的状态,一同封装到不同的状态类中,我们需要新增一种分支逻辑,不再需要修改选择分支,而是只需要新增一个状态类即可。
那是否状态模式可以替代传统的if 选择分支,答案是不能,本质上还是一个度的原因,面相对象如果过度设计,会导致类的数量无限膨胀,难以维护,试想如果存在多个状态字段(status、type等),则实体对象的状态是由多个状态字段组合而成的,每增加一个新的状态字段,都会导致状态的数量快速增加,这显然不是我们想看到的。

作者:来自 vivo 互联网存储团队- Xu Xingbao

Redis 集群经常需要进行在线水平扩缩容,实际操作过程中发现迁移期间服务时延剧烈抖动,业务侧感知明显,为了应对以上问题对原生 Redis 集群 slot 迁移功能进行优化改造。

一、背景介绍

Redis 集群服务在互联网公司被广泛使用,众所周知服务集群化可以突破单节点的能力瓶颈,带来规模、可用性、扩展性等多方面的收益。在实际使用 Redis 集群的过程中,发现在进行涉及集群数据迁移的水平扩缩容操作时,业务侧多次反馈 Redis 请求的时延升高问题,甚至发生过扩容操作导致集群节点下线的可用性故障,并进一步引发迁移流程中断、节点间数据脑裂等一系列严重影响,给运维同事带来极大困扰,严重影响线上服务的稳定。

二、问题分析

2.1 原生迁移介绍

Redis 集群功能采用无中心架构设计,集群中各个节点都维护各自视角的集群拓扑并保存自有的分片数据,集群节点间通过 gossip 协议进行信息协调和变更通知。具体来说 Redis 集群数据管理上采用虚拟哈希槽分区机制,将数据的键通过哈希函数映射到 0~16383 整数槽内,此处的槽位在 Redis 集群设计中被称为 slot。这样实际上每一个节点只需要负责维护一部分 slot 所映射的键值数据,slot 就成为 Redis 集群管理数据的基本单位,集群扩缩容本质上就是 slot 信息和 slot 对应数据数据在节点之间的转移。Redis 集群水平扩展的能力就是基于 slot 维度进行实现,具体流程如下图所示。

上图所示的迁移步骤中,步骤 1-2 是对待迁移 slot 进行状态标记,方便满足迁移过程中数据访问,步骤 3-4 是迁移的核心步骤,这两个步骤操作会在步骤 5 调度下持续不断进行,直到待迁移 slot 的键值数据完全迁移到了目标节点,步骤 6 会在数据转移完成后进行,主要是发起集群广播消息更新集群内节点 slot 拓扑。

由于正常的迁移时一个持续的处理过程,不可避免地会出现正在迁移 slot 数据分布于迁移两端地“分裂”状态,这种状态会随着 slot 迁移的流程进行而持续存在。为了保证迁移期间正在迁移的 slot 数据能够正常读写,Redis 集群实现了下图所示的一种 ask-move 机制,如果请求访问正在迁移的 slot 数据,请求首先会按照集群拓扑正常访问到迁移的源节点,如果在源节点查询到数据则正常处理响应请求;如果在源节点没有找到请求所需数据,则会给客户端回复 ASK {ip}:{port}  消息回包。

Redis 集群智能客户端收到该回包后会按照包内节点信息找到新节点重试命令,但是由于此时目标节点还没有迁移中 slot 的所属权,所以在重试具体命令之前智能客户端会首先向目的节点发送一个 asking 命令,以此保证接下来访问迁移中 slot 数据的请求能被接受处理。由于原生迁移时按照 key 粒度进行的,一个 key 的数据要不存在源节点,要不存在目的节点,所以 Redis 集群可以通过实现上述 ask-move 机制,保证迁移期间数据访问的一致性和完整性。

2.2 迁移问题分析

(1)时延分析
根据上述原生 Redis 集群迁移操作步骤的了解,可以总结出原生迁移功能按照 key 粒度进行的,即不断扫描源节点上正在迁移的 slot 数据并发送数据给目的节点,这是集群数据迁移的核心逻辑。微观来说迁移单个 key 数据对于服务端来说包含以下操作:

  • 序列化待迁移键值对数据;
  • 通过网络连接发送序列化的数据包;
  • 等待回复(目标端接收完包并加载成功才会返回);
  • 删除本地残留的副本,释放内存。

上述操作中涉及多个耗费线程处理时长的操作,首先序列化数据是非常耗费 CPU 时间的操作,如果遇到待迁移 key 比较大线程占用时长也会随之恶化,这对于单工作线程的 Redis 服务来说是不可接受的,进一步地网络发送数据到目标节点时会同步等待结果返回,而迁移目的端又会在进行数据反序列化和入库操作后才会向源节点进行结果返回。需要注意的是在迁移期间会不断循环进行以上步骤的操作,而且这些步骤是在工作线程上连续处理的,期间无法对正常请求进行处理,所以此处就会导致服务响应时延持续突刺,这一点可以通过 slowlog 的监控数据得到验证,迁移期间会在 slowlog 抓取到大量的 migrate 和 restore 命令。

(2)ask-move 开销
正常情况下每个正在迁移的 slot 数据都会一段时间内存在数据分布在迁移的两端的情况,迁移期间该 slot 数据访问请求可以通过 ask-move 机制来保证数据一致性,但是不难看出这样的机制会导致单个请求网络访问次数出现成倍的增加,对客户端也存在一定的开销压力。另外,对于可能存在的用户采用 Lua 或者 Pipline 这种需要对单个 slot 内多 key 连续访问的场景,目前大部分集群智能客户端支持有限,可能会遇到迁移期间相关请求不能正常执行的报错。另外需要说明的是,由于 ask-move 机制的只在迁移两端的主节点上能触发,所以迁移期间从节点是不能保证数据请求结果一致性的,这对于采用读写分离方式访问集群数据的用户也非常不友好。

(3)拓扑变更开销
为了降低迁移期间数据 ask-move 的机制对请求的影响,正常情况下原生迁移每次只会操作一个 slot 迁移,这就导致对每一个迁移完成的 slot 都会触发集群内节点进行一次拓扑更新,而每次集群拓扑的更新都会触发正在执行指令的业务客户端几乎同时发送请求寻求更新集群拓扑,拓扑刷新请求结果计算开销高、结果集大,大大增加了节点的处理开销,也会造成正常服务请求时延的突刺,尤其对于连接数较大、集群节点多的集群,集中的拓扑刷新请求很容易造成节点计算资源紧张和网络拥塞,容易触发出各种服务异常告警。

(4)迁移无高可用
原生的迁移的 slot 标记状态只存在于迁移双端的主节点,其对应的从节点并不知道迁移状态,这也就导致一旦在迁移期间发生节点的 failover,迁移流程将会中断和出现 slot 状态残留,也将进一步导致迁移 slot 数据的访问请求无法正常触发 ask-move 机制而发生异常。例如迁移源节点异常,那么其 slave 节点 failover 上线,由于新主节点并不能同步到迁移状态信息,那么对于迁移中 slot 的请求就不能触发 ask 回复,如果是一个对已经迁移至目标节点的数据的写请求,新主节点会直接在本节点新增 key,导致数据出现脑裂,类似地如果处理的是已经迁移数据的读取请求也无法保证返回正确结果。

三、优化方案

3.1 优化方向思考

通过原生数据迁移机制分析,可以发现由于迁移操作涉及大量的同步阻塞操作会长时间占用工作线程,以及频繁的拓扑刷新操作,会导致请求时延不断出现上升。那么是否可以考虑将阻塞工作线程的同步操作改造成为异步线程处理呢?这样改造有非常大的风险,因为原生迁移之所以能够保证迁移期间数据访问的正确性,正是这些同步接口进行了一致性保证,如果改为异步操作将需要引入并发控制,还要考虑迁移数据请求与 slave 节点的同步协调问题,此方案也无法解决拓扑变动开销问题。所以 vivo 自研 Redis 放弃了原生按照 key 粒度进行迁移的逻辑,结合线上真实扩容需求,采用了类似主从同步的数据迁移逻辑,将迁移目标节点伪装成迁移源节点的从节点,通过主从协议来转移数据。

3.2 功能实现原理

Redis 主从同步机制是指在 Redis 主节点(Master)和从节点(Slave)之间进行数据同步和复制的过程,主从同步机制可以提高 Redis 集群的可用性,避免单点故障和数据丢失等问题。Redis 目前主从同步有全量同步和部分同步两种方式,从节点发送同步位点给主节点,如果是首次同步则需要走全量同步逻辑,主节点通过发送 RDB 基础数据文件和传播增量命令方式将数据同步给从节点;如果不是首次同步,主节点则会通过从节点同步请求中的位点等信息判断是否满足增量同步条件,优先进行增量同步以控制同步开销。由于主节点在同步期间也在持续处理新的命令请求,所以从节点对主节点的数据同步是一个动态追齐的过程,正常情况下,主节点会持续发送写命令给从节点。

基于同步机制,我们设计实现了一套如下图所示的 Redis 集群数据迁移的功能。迁移数据逻辑主要走的全量同步逻辑,迁移数据和同步数据最大的区别在于,正常情况下需要迁移的是源节点部分 slot 数据,目标节点并不需要复制源节点的全量数据,完全复用同步机制会产生不必要的开销,需要对主从同步逻辑进行修改适配。为了解决该问题,我们对相关逻辑做了一些针对性的改造。首先在同步命令交互上,针对迁移场景增加了迁移节点间 slot 信息交互,从而让迁移源节点获知需要迁移哪些 slot 到哪个节点。另外,我们还对 RDB 文件文件结构按照 slot 顺序进行了调整改造,并且将各个 slot 数据的文件起始偏移量数据作为元数据记录到 RDB 文件尾部固定位置,这样在进行迁移操作的 RDB 传输步骤时就可以方便地索引到 RDB 文件中目标 slot 数据片段。

3.3 改造效果分析

(1)时延影响小
对于 slot 迁移操作而言,主要涉及迁移源和目的两端的开销,对于基于主从同步机制实现的新 slot 迁移,其源节点主要开销在于生成 RDB 和传送网络包,正常对于请求时延影响不大。但是因为目的节点需要对较大的 RDB 文件片段数据进行接收、加载,由于目的节点迁移时也需要对正常服务请求响应,此时不再能采用类似 slave 节点将所有数据收取完以后保存本地文件,然后进行阻塞式数据加载的方案,所以新 slot 迁移功能对迁移目的节点的数据加载流程进行了针对性改造,目的节点会按照接收到的网络包粒度将数据按照下图所示进行递进式加载,即 slot 迁移目标节点每接收完一个 RDB 数据网络包就会尝试加载,每次只加载本次网络包内包含的完整元素,这样复合类型数据就可以按照 field 粒度加载,从而降低多元素大 key 数据迁移对访问时延的剧烈影响。通过这样的设计保持原来单线程简洁架构的同时,有效地控制了时延影响,所有数据变更操作都保持在工作线程进行,不需要进行并发控制。通过以上改造,基本消除了迁移大 key 对迁移目的节点时延影响。

(2)数据访问稳定
新 slot 迁移操作期间,正在迁移的数据还是存储在源节点上没有变,请求继续在源节点上正常处理,用户侧的请求不会触发 ask-move 转发机制。这样用户就不需要担心读写分离会出现数据不一致现象,在进行事务、pipeline 等方式封装执行命令时也不会出现大量请求报错的问题。迁移动作一旦完成,残留在源端的已迁移 slot 数据将成为节点的残留数据,这部分数据不会再被访问,对上述残留数据的清理被设计在 serverCron 中逐步进行,这样每一次清理多少数据可以参数化控制,可以根据需要进行个性化设置,保证数据清理对正常服务请求影响完全可控。

(3)拓扑变更少
原生的迁移功能为了降低 ask-move 机制对正常服务请求的影响,每次仅会对一个 slot 进行数据迁移,迁移完了会立即发起拓扑变更通知来集群节点转换 slot 的属主,这就导致拓扑变化的次数随着迁移 slot 的数量增加而变多,客户端也会在每一次感知到拓扑变化后发送命令请求进行拓扑更新。更新拓扑信息的命令计算开销较大,如果多条查询拓扑的命令集中处理,就会导致节点资源的紧张。新的 slot 迁移按照节点进行数据同步,可以支持同时迁移源节点的多个 slot 甚至全部数据,最后可以通过一次拓扑变更转换多个 slot 的属主,大大降低了拓扑刷新的影响。

(4)支持高可用
集群的数据迁移是一个持续的过程,这个过程可能长达几个小时,期间服务可能发生各种异常情况。正常情况下的 Redis 集群具有 failover 机制,从节点可感知节点异常以代替旧主节点进行服务。新 slot 迁移功能为了应对这样的可用性问题,将 slot 迁移状态同步给从节点,这样迁移期间如果集群迁移节点发生 failover,其从节点就可以代替旧主节点继续推进数据迁移流程,保证了迁移流程的高可用能力,避免人工干预,大大简化运维操作复杂度。

四、功能测试对比

为了验证改造后迁移功能的效果,对比自研迁移和原生迁移对请求响应的影响,在三台同样配置物理机上部署了原生和自研两套相同拓扑的集群,选择后对 hash 数据类型的 100k 和 1MB 两种大小数据分别进行了迁移测试,每轮在节点间迁移内存用量 5G 左右的数据。测试主要目的是对比改造前后数转移对节点服务时延影响,所以在实际测试时没有对集群节点进行背景流量操作,节点的时延数据采用每秒钟 ping 10 次节点的方式进行采集,迁移期间源节点和目的节点的时延监控数据入下表所示(纵轴数值单位:ms)。

通过对比以上原生和自研集群 slot 迁移期间的时延监控数据,可以看出自研 slot 迁移功能迁移数据期间迁移两端节点的请求响应时延表现非常平稳,也可以表现出经过主从复制原理改造的 Redis 集群 slot 迁移功能具备的优势和价值。

五、总结和展望

原生 Redis 集群的扩缩容功能按照 key 粒度进行数据转移,较大的 key 会造成工作线程的长时间占用,进而引起正常服务请求时延飙高问题,甚至导致节点长时间无法回复心跳包而被判定下线的情况,存在稳定性风险。通过同步机制改造实现的新 slot 迁移功能,能显著降低数据迁移对用户访问时延的影响,提升线上 Redis 集群稳定性和运维效率,同时新的 slot 迁移功能还存在一些问题,例如新的迁移造成节点频繁的 bgsave 压力,迁移期间节点内存占用增加等问题,未来我们将围绕这些具体问题,继续不断优化总结。