一次Golang单体架构中的复杂长函数的重构实践和思考

在现代应用程序开发中,信息流(Feed)是许多平台核心功能的一部分。信息流往往会聚合大量的数据,构建这样一个信息流列表是一个复杂的任务。需要从多个微服务和数据库中获取大量数据,包括用户、频道、标签、等级、用户状态和互动等,并进行过滤、转换和计算,最终拼装成目标数据结构。在这个过程中,性能和代码设计的合理性尤为重要。

难题:一个超过1000行的长函数

在互联网服务端代码中,分层架构是一种常见的业务逻辑组织方式。我们通常根据实体,将每一层分为以实体名称命名的文件和无状态对象,每个对象包含数据获取、业务逻辑处理和数据库读写的方法。

通常情况下,大多数方法都是内聚的,只与单个实体相关,代码行数较少。但类似于微服务中的聚合根,无论数据结构如何设计组织,服务端接口最终需要根据产品和业务的需求,在前后端的交界处,为了满足UI展示的需求,将数据聚合起来。无论架构层面如何治理和拆分,为满足业务需求,接口层必然需要将数据按照使用需求进行组合。

信息流产品层面正是这样的场景。信息流列表中会聚合各种不同类型的信息和广告,以及信息作者的状态、互动数据和统计信息。为了满足这些需求,需要从各种数据源或服务中获取数据,并根据不同领域逻辑进行转换和组合,最终拼装成目标数据结构。在考虑性能的情况下,这些拼装数据的函数往往变得非常庞大。

在我参与的一个项目中,就遇到过一个信息流列表对象拼装函数。由于处理复杂数据,该函数长度超过1000行,甚至接近2000行,维护起来非常痛苦。

抽象和组合:拆分复杂函数

大多数小型业务探索驱动的项目,由于开发时间短、变化快,往往不会在代码设计上花费太多时间。开发人员也不愿意在调用业务逻辑函数时进行复杂的组装,因此很容易出现Service中包含一个长函数,并通过包的公开方法直接访问的情况。

为了降低代码复杂度并解耦核心逻辑和具体实现,在重构复杂长函数的过程中,我采用了以下方法,使代码能够适应微服务架构。无论外部数据获取代码如何变化,核心的信息流拼装逻辑都不会受到影响。具体方法非常类似六边形架构的思想。

代码重构的核心思想可以总结为:采用六边形架构,将核心业务逻辑与数据获取和转换方法分离,通过工厂模式和依赖倒置实现灵活的架构设计。

具体实现思路

我把这种模式称之为组装工厂,像流水线一样,初始化一个空对象,然后逐步把各种字段拼到对应位置,最后交付一个完整的对象。

1. 定义数据Provider接口,拼装工厂获取数据,通过自己定义的Provider接口函数,数据的提供者,或者控制反转的容器,负责实现Provider接口。为工厂提供数据。

type DataProvider interface {GetUser(ctx context.Context, userID int) (*User, error)GetChannel(ctx context.Context, channelID int) (*Channel, error)GetMessageBody(ctx context.Context, msgId string) (*Message, error)// 其他数据获取方法}

在最初的长函数中,依赖service层和dao层的大量方法调用,获取数据,Provider的实现类,将这些方法和数据拼装的核心逻辑做了桥接,实现了解耦。

2. 实现单个实体的拼装工厂类,工厂对象接收一个Provider,作为数据源,接收一个信息流的基础数据结构或者id。

拼装工厂类将复杂的拼装过程,按照逻辑拆分成一个一个小函数,通过协程,并行拼装目标实体对象的不同字段。

因为目标对象和各种中间数据,可以作为工厂的私有字段,因此可以减少函数调用时的参数传递。


type PostFactory struct {provider DataProviderpost *BasePosttarget *TargetPosterr error
}func NewDataFactory(provider DataProvider, post *BasePost) *PostFactory {return &PostFactory{provider: provider, post: pose}
}func(df *PostFactory) prepare(ctx context.Context) (*PostFactory) {df.target = &TargetPost{}return df
}func(df *PostFactory) throw(err error) (*PostFactory) {df.err = errreturn df
}func(df *PostFactory) composeUser(ctx context.Context) (*PostFactory) {if df.err != nil {return df}user, err := df.provider.GetUser(userID)if err != nil {return df.throw(err)}df.target.SetUser(user)// 其他数据获取和拼装逻辑return df
}// 其他各种composeXXXX函数,负责拼装各种其他数据func(df *PostFactory) Build(ctx context.Context) (*TargetPost, error) {err := df.prepare(ctx).composeUser(ctx)// 其他数据获取和拼装逻辑if err != nil {return nil, err}return df.target, nil
}

在实际实现的时候,调用composeXXX的链式调用,可以结合mapreduce的多线程编程范式,将调用放在子协程中并行处理,需要注意的是,如果存在对相同字段的并发写入,要注意加锁,并且注意执行的先后顺序。

如果有必要可以由Factory或者TargetPost自己实现一系列并发安全的SetXXX方法。

3. 实现支持共享数据的Provider

Provider最基础的能力,就是通过接口,隔离拼装工厂对各种数据源的物理依赖,这样就可以达到依赖抽象而不依赖具体实现的目标,最基本的Provider实现类,就是将工厂对数据获取方法的调用,转发给各自领域的Service方法。

但是因为我们信息流的场景,往往是需要在一个列表中,拼装一个数组所包含的实体对象,因此,我们可以通过在基础Provider之上,再包装一层,对外也实现了Provider接口,自己维护了一层缓存,在缓存找不到数据的时候,调用基础Provider,从数据源重新获取数据。

type BaseProvider struct {us UserService
}func (bp *BaseProvider) GetUser(ctx context.Context, userID int) (*User, error) {return bp.us.GetUserById(ctx, userID)
}type CachedProvider struct {um sync.Mapbp DataProvider
}func (cp *CachedProvider) GetUser(ctx context.Context, userID int) (*User, error) {if u, ok := cp.um.Load(userID); ok {return u.(*User), nil}u, err := cp.bp.GetUserById(ctx, userID)if err != nil {return u, err}cp.Store(userID, u)return u, nil
}

除了使用并发安全的sync.Map之外,其实也可以用map,加上读写锁的方式,控制并发,但是在实际实现的时候,通过benchmark测试,我们发现加锁会验证的影响并发执行速度,所以采用无锁化,进程安全的sync.Map。

如果一定不想使用sync.Map的话,一定要避免一个全局锁,而是针对不同的共享数据结构,使用各自的锁,将锁分开,尽可能的避免互斥。

缓存的时候,可以采用进程内存,也可以结合redis,实现成二级缓存,需要注意的是,如果采用二级缓存的话,要使用LFU或者LRU,控制内存中缓存占用的空间大小,防止溢出。

4. 链式调用和批处理

无论是拼装工厂,还是在Provider的组合实现过程中,我个人都比较偏好采用链式调用的风格,通过内部状态,控制某一个中间处理异常之后,后面的调用就不再继续执行,只是函数的空调用。

另外在对象上,采用WithXXXX命名的一系列函数,我们可以给对象更多的能力,每个With函数,都是返回对象本身,这样所有调用都是链式的。

在我们的应用场景中,主要是结合批处理,兼顾数据查询的性能,和拼装工厂代码逻辑的简单通用。为此,我们可以对CacheProvider进行扩展,在调用之前,可以提前注入数据,也就是预加载缓存。这样,拼装数据之前,就可以利用微服务或者数据库提供的批量查询接口,提前加载一批数据,获得更好的查询性能。

func(cp *CachedProvider) WithUserList(users []*User) (*CachedProvider) {for _, u := range users {cp.Store(u.UserId, u)}return cp
}

链式调用的好处是写出来的代码更加简洁。

5.并发控制和性能

经过一系列的代码重构之后,我们成功的将原来几千行的函数,拆分成了结构更清晰,每一个函数都不大的小函数调用,而且通过接口,实现了抽象和实现的分离,极大的强化了代码的灵活性。

这种实现方式,需要大量的设计和分析,实现成本很高,所以一定不是系统中代码的常态,而是只有在非常重要,而且复杂的时候,才需要通过设计精细的结构,简化复杂度。

另外还要考虑性能的因素,毫无疑问,无论是针对service层对象上的一个ToMessage这样的简单长函数,还是Provider加Factory的组合,都是通过协程并发的加载,我们很直观就可以想到,简单的静态函数调用,比起对象的内存分配和回收,成本更低。

经过Benchmark测试,我们发现,在没有任何优化的情况下,使用Provider加Factory的方式,对象分配和回收的开销,的确是远远高于静态函数调用。

但是,我们可以通过使用Golang的sync.Pool,存储和复用Provider对象,结合并发数的控制,可以极大的改善Provider加Factory模式的内容分配和回收开销,经过实际验证,在负载越高的情况下,拥有局部缓存和内部并发机制的Provider加Factory组合,性能相比长函数,也更有优势。

这也体现出了长函数的一个缺点,就是难以阅读、维护和优化,长函数本身调用虽然简单,但是几千行的代码,调用过程中也免不了会有各种对象的分配和回收,而且因为是一大块集中的代码,很难针对某一段进行优化。

总结和回顾

总的来说,通过这样的一次重构,我们验证了通过定义抽象接口,可以解耦了代码,从而让复杂的业务逻辑的核心代码,可以达到微服务就绪,不再受限于分层的代码。

没有度量就没有优化,采用Benchmark度量性能,根据度量的指标进行优化,是性能优化的正确方式。更好地掌握了锁和并发控制还有线程安全的数据结构。

精心设计的代码结构,不但有助于理解和维护,而且通过降低局部复杂性,也有助于性能的优化和问题排查。

一次Golang单体架构中的复杂长函数的重构实践和思考icon-default.png?t=N7T8https://mp.weixin.qq.com/s/cpRZnLFJkM-LVVBTKPtfLQ一次Golang单体架构中的复杂长函数的重构实践和思考

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/381086.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Langchain大语言模型开发教程】基于文档问答

🔗 LangChain for LLM Application Development - DeepLearning.AI Embedding: https://huggingface.co/BAAI/bge-large-en-v1.5/tree/main 学习目标 1、Embedding and Vector Store 2、RetrievalQA 引包、加载环境变量 import osfrom dotenv import…

react开发-配置开发时候@指向SRC目录

这里写目录标题 配置开发时候指向SRC目录VScode编辑器给出提示总体1.配置react的 2.配置Vscode的1.配置react的2,配置VSCode的提示支持 配置开发时候指向SRC目录VScode编辑器给出提示 总体1.配置react的 2.配置Vscode的 1.配置react的 1. 我么需要下载一个webpack的插件 这样…

Java案例斗地主游戏

目录 一案例要求: 二具体代码: 一案例要求: (由于暂时没有学到通信知识,所以只会发牌,不会设计打牌游戏) 二具体代码: Ⅰ:主函数 package three;public class test {…

filebeat,kafka,clickhouse,ClickVisual搭建轻量级日志平台

springboot集成链路追踪 springboot版本 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.3</version><relativePath/> <!-- lookup parent from…

CentOS 7报错:yum命令报错 “ Cannot find a valid baseurl for repo: base/7/x86_6 ”

参考连接&#xff1a; 【linux】CentOS 7报错&#xff1a;yum命令报错 “ Cannot find a valid baseurl for repo: base/7/x86_6 ”_centos linux yum search ifconfig cannot find a val-CSDN博客 Centos7出现问题Cannot find a valid baseurl for repo: base/7/x86_64&…

88个群智能算法优化BP神经网络 多特征输入单输出回归预测Matlab程序

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、BP神经网络原理二、优化BP神经网络初始权重过程三、程序内容3.1 88个优化算法清单3.2 实验数据3.3 实验结果 代码获取 前言 在Matlab中优化神经网络的初始权…

图论模型-迪杰斯特拉算法和贝尔曼福特算法★★★★

该博客为个人学习清风建模的学习笔记&#xff0c;部分课程可以在B站&#xff1a;【强烈推荐】清风&#xff1a;数学建模算法、编程和写作培训的视频课程以及Matlab等软件教学_哔哩哔哩_bilibili 目录 ​1图论基础 1.1概念 1.2在线绘图 1.2.1网站 1.2.2MATLAB 1.3无向图的…

【毕业论文】| 基于Unity3D引擎的冒险游戏的设计与实现

&#x1f4e2;博客主页&#xff1a;肩匣与橘 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由肩匣与橘编写&#xff0c;首发于CSDN&#x1f649; &#x1f4e2;生活依旧是美好而又温柔的&#xff0c;你也…

学生成绩管理系统(C语言)

系统分析 1. 主菜单的实现 2. 增加人员功能的实现 3. 删除数据功能的实现 4. 编辑人员功能的实现 5. 排序功能的实现 6. 输出功能 7. 查找信息功能 具体代码 #include <stdio.h> #include <string.h> #include <stdlib.h> #define SIZE 100000typedef struc…

level 6 day2-3 网络基础2---TCP编程

1.socket&#xff08;三种套接字&#xff1a;认真看&#xff09; 套接字就是在这个应用空间和内核空间的一个接口&#xff0c;如下图 原始套接字可以从应用层直接访问到网络层&#xff0c;跳过了传输层&#xff0c;比如在ubtan里面直接ping 一个ip地址,他没有经过TCP或者UDP的数…

MVCC数据库并发控制技术

一、引言 MVCC&#xff08;Multi-Version Concurrency Control&#xff09;是一种广泛使用的数据库并发控制技术&#xff0c;它允许数据库读操作和写操作并发执行&#xff0c;而无需加锁整个表或行&#xff0c;从而大大提高了数据库的并发性能和吞吐量。MVCC主要被应用于支持事…

最新全新UI异次元荔枝V4.4自动发卡系统源码

简介&#xff1a; 最新全新UI异次元荔枝V4.4自动发卡系统源码 更新日志&#xff1a; 1增加主站货源系统 2支持分站自定义支付接口 3目前插件大部分免费 4UI页面全面更新 5分站可支持对接其他分站产品 6分站客服可自定义 7支持限定优惠 图片&#xff1a; 会员中心截图&…

docker默认存储地址 var/lib/docker 满了,换个存储地址操作流程

1. 查看docker 存储地址 docker info如下 var/lib/docker2、查看内存大小 按需执行 df -h 找超过100M的大文件 find / -type f -size 100M -exec ls -lh {} \; df -Th /var/lib/docker 查找这个文件的容量 df -h 查找所有挂载点 du -hs /home/syy_temp/*1、df -h 2、sud…

Java中锁的全面详解(深刻理解各种锁)

一.Monitor 1. Java对象头 以32位虚拟机位例 对于普通对象,其对象头的存储结构为 总长为64位,也就是8个字节, 存在两个部分 Kclass Word: 其实也就是表示我们这个对象属于什么类型,也就是哪个类的对象.而对于Mark Word.查看一下它的结构存储 64位虚拟机中 而对于数组对象,我…

卡片式组件封装demo

效果视频&#xff1a; 卡片组件 样式还得细调~&#xff0c;时间有限&#xff0c;主要记录一下逻辑。 html结构&#xff1a; 目录 父组件数据处理数据格式 父组件的全部代码 子组件数据处理props参数 样式部分三个圆点点击三圆点在对应位置显示查看弹框点击非内容部分隐藏查看…

从千台到十万台,浪潮信息InManage V7解锁智能运维密码

随着大模型技术的深度渗透&#xff0c;金融行业正经历着前所未有的智能化变革。从“投顾助手”精准导航投资蓝海&#xff0c;到“智能客服”秒速响应客户需求&#xff0c;大模型以其对海量金融数据的深度挖掘与高效利用&#xff0c;正显著提升金融服务的智能化水准&#xff0c;…

Python爬虫(2) --爬取网页页面

文章目录 爬虫URL发送请求UA伪装requests 获取想要的数据打开网页 总结完整代码 爬虫 Python 爬虫是一种自动化工具&#xff0c;用于从互联网上抓取网页数据并提取有用的信息。Python 因其简洁的语法和丰富的库支持&#xff08;如 requests、BeautifulSoup、Scrapy 等&#xf…

Springboot同时支持http和https访问

springboot默认是http的 一、支持https访问 需要生成证书&#xff0c;并配置到项目中。 1、证书 如果公司提供&#xff0c;则直接使用公司提供的证书&#xff1b; 如果公司没有提供&#xff0c;也可自己使用Java自带的命令keytool来生成&#xff1a; &#xff08;1&#x…

postman创建mock server

B站博主的说明&#xff1a;

开源模型应用落地-FastAPI-助力模型交互-进阶篇-RequestDataclasses(三)

一、前言 FastAPI 的高级用法可以为开发人员带来许多好处。它能帮助实现更复杂的路由逻辑和参数处理&#xff0c;使应用程序能够处理各种不同的请求场景&#xff0c;提高应用程序的灵活性和可扩展性。 在数据验证和转换方面&#xff0c;高级用法提供了更精细和准确的控制&#…