背景

在服务刚刚搭建时,通常的思维就是根据API编写业务逻辑:

// SendStream ...
func (d *Svc) SendStream(stream MyApi_data.ProxyDialOut_SendStreamServer) error {
for {
...
data, err := stream.Recv()
if err != nil {
logrus.Errorf("recv error:%v", err)
return err
}
...
// 对data做相关的操作
}
}

在服务暴露出越来越多的API后,相似的操作会越来越多。此时会进行抽象和封装,提取公共操作,例如提取函数、建立工厂等。

比如,在已有的API中添加监控统计。虽然对统计器做了抽象(对象或者函数),但可能仍然需要侵入到所有不同的API实现中。

// SendStream ...
func (d *MyApiSvc) SendStream(stream MyApi_data.ProxyDialOut_SendStreamServer) error {
for {
...
data, err := stream.Recv()
if err != nil {
logrus.Errorf("recv error:%v", err)
return err
}
...
// 对data做相关的操作
...
// 添加一个共享的监控统计器,调用上报业务,每个api都需要改动
counter.Add("MyApi", 1)
}
}

这在简单项目中无可厚非,但长此以往,随着各种功能的加入,API的业务代码会迅速臃肿起来。

后续,会发现每个API都各不相同,却又有公共部分。所以不得不写出大量形容相似的代码。这在部门大部分项目中都屡见不鲜

究其原因,这是因为抽象层次不够造成的。

摒除以API为中心的编程模式

在网络编程中,一般会引入中间件(比如trpc的filter)来处理共有逻辑,比如鉴权,日志,panic处理等。

但中间件一般太过于抽象并不直观,使得编写调试不易。但它的思路值得借鉴。

在对业务进行思考后,突发奇想。虽然对客户端(用户)而言,每个API都是服务(消费者)。但对于具体处理而言,每个API同时也是生产者

每个API看成data source,生产数据(data),就是对api最底层的抽象。

在这里,引入一个简单的流式编程包go-streams(github.com/reugn/go-streams),方便快速建立流式编程的架构。

建立抽象:每个API都是datasource

每个api,都实现Source的接口,将自己收到的数据,无脑封装往下一跳怼

import 	"github.com/reugn/go-streams/extension"
type Source interface{
GetSource() *extension.ChanSource
}

实现抽象:为每个API服务都创建chan,这是数据源的本质

type MyApiSvc struct {
name string
ctx context.Context
ch chan any // 就是它
protocol string
}
// GetSource 实现Source接口
func (t *MyApiSvc) GetSource() *extension.ChanSource {
return extension.NewChanSource(t.ch)
} type DataItem struct {
data any
session map[string]any
} // SendStream ...
func (d *MyApiSvc) SendStream(stream MyApi_data.ProxyDialOut_SendStreamServer) error {
for {
...
data, err := stream.Recv()
if err != nil {
logrus.Errorf("recv error:%v", err)
return err
}
...
// 这里不对数据做任何处理,封装之后,直接丢到chan里
td := new(DataItem)
td.session = make(map[string]any)
td.session["ip"] = ip
td.session["trace_id"] = grand.S(8)
td.data = data
d.ch <- td
}
}

每个api的chango-streams封装为一个数据源ChanSource类型。

将各种API的原始数据封装为DataItem在流中统一处理,内置session是神来之笔。这个session会包含每条数据的个性化信息。可以由每个步骤增添并提供给下一步骤使用。

这样,在编写业务逻辑时就能站在更上层、数据的角度思考问题。

流式处理

在上面,每个数据源都已经被封装为一个ChanSource(本质是chan),现在来统一规划业务逻辑。

使用go-streams,将整个业务逻辑抽象成数据流的多个步骤:

此编程模式的特色之处在于:

  1. 每个步骤接收上一个节点的数据,处理之后,将数据发往下一跳。编写单一步骤的时候,只需要考虑本步骤处理的事情,思维量大大减少。
  2. 在单个步骤,处理是并发的,但在不同的步骤,处理是顺序的。
  3. 围绕数据编程,方便抽象施加统一的处理过程,比如getParser,getSender两个工厂函数。
  4. 所有与主线(这里是格式转换和发送)无关的功能,以插件形式接入,在go-stream中,体现为一个步骤,不侵入已经编写好的业务逻辑。每个节点都有前驱和后继,拥有无限可能。没错,这就是面向切面编程。也是这套系统的核心魅力所在。
	source := getDataSource(ctx, cfg.Name) // cfg.Name == "MyApi",通过工厂函数载入配置,获得interface `Source`
// 调用接口
source.GetSource().Via(flow.NewMap(func(i interface{}) interface{} { // 步骤1,创建日志
// 从用户发来的每条消息都被打散成为了数据源的一条数据
msg := i.(model.*DataItem)
traceID := msg.GetSession()["trace_id"].(string) // 从数据的session中获取数据的附加信息
tags := map[string]interface{}{
"trace_id": traceID,
"ip": msg.GetSession()["ip"],
"name": c.Name,
}
log := logrus.WithFields(tags) // 这个步骤只是为了添加一个日志对象
return []any{msg, log}
// 使用8个协程来执行这个步骤
}, 8)).Via(flow.NewMap(func(i interface{}) interface{} { // 步骤2,解析数据
arr := i.([]any) // 这里的i是上一步骤return的数据
msg := arr[0].(*DataItem)
log := arr[1].(*logrus.Entry) parser := getParser(cfg.Name) // 这个工厂函数是每种数据源的个性化处理。根据配置获取一个解析器 // 解析数据
data, err := parser(ctx, msg, c.Name, msg.GetSession()["ip"])
if err != nil {
log.Error(err)
return err
}
return []any{data, log}
}, 8)).Via(flow.NewMap(func(i interface{}) interface{} { // 步骤3,发送数据到下个服务
arr,ok := i.([]any) // 这里的i,就是上一步骤return的数据
if !ok{
return i // 如果上一步骤return的是error,则直接跳过不再解析
} data := arr[0].(*MyApiData) // 这里的data,已经是上一步骤解析出来的数据
log := arr[1].(*logrus.Entry) // 发数数据
sender := getSender(cfg.Name) // 这个工厂函数为不同的数据源分配一个发送器
sender.Send(qdata)
return i
}, 8)).Via(flow.NewMap(func(i interface{}) interface{} { // 步骤4,统计发送成功的数据量
arr, ok := i.([]any) // 这里的i,就是上一步骤return的数据
if ok{
msg := arr[0].(*DataItem)
log := arr[1].(*logrus.Entry)
// 内部统计
log.Info("send success")
controller.TraceAfter(msg.GetSession()["ip"])
}
return i
}, 8)).To(extension.NewIgnoreSink())

为什么要使用go-streams

  1. 库非常的简单,实际就是对go chan的封装。简单是一种美,简单的东西一般不容易出错。
  2. 隐含了流式编程的主要思想,它并没有什么黑科技,但使用它会强制我们使用面向数据的,抽象的方式来思考问题。最终写出低耦合可调测的代码。这才是难能可贵的。

从围绕API到围绕数据-使用流式编程构建更简洁的架构的更多相关文章

  1. 文件是数据的流式IO抽象,mmap是对文件的块式IO抽象

    文件是数据的流式IO抽象,mmap是对文件的块式IO抽象

  2. Stream流式编程

    Stream流式编程   Stream流 说到Stream便容易想到I/O Stream,而实际上,谁规定“流”就一定是“IO流”呢?在Java 8中,得益于Lambda所带来的函数式编程,引入了一个 ...

  3. 万字详解 | Java 流式编程

    概述 Stream API 是 Java 中引入的一种新的数据处理方法.它提供了一种高效且易于使用的方法来处理数据集合.Stream API 支持函数式编程,可以让我们以简洁.优雅的方式进行数据操作, ...

  4. 20190827 On Java8 第十四章 流式编程

    第十四章 流式编程 流的一个核心好处是,它使得程序更加短小并且更易理解.当 Lambda 表达式和方法引用(method references)和流一起使用的时候会让人感觉自成一体.流使得 Java ...

  5. JDK8新特性(二) 流式编程Stream

    流式编程是1.8中的新特性,基于常用的四种函数式接口以及Lambda表达式对集合类数据进行类似流水线一般的操作 流式编程分为大概三个步骤:获取流 → 操作流 → 返回操作结果 流的获取方式 这里先了解 ...

  6. golang的极简流式编程实现

    传统的过程编码方式带来的弊端是显而易见,我们经常有这样的经验,一段时间不维护的代码或者别人的代码,突然拉回来看需要花费较长的时间,理解原来的思路,如果此时有个文档或者注释写的很好的话,可能花的时间会短 ...

  7. 【书籍知识回顾与总结-2022】Java语言重点知识-多线程编程、流式编程

    一.多线程编程 二.流式编程 1.目的 简化集合和数组的操作 注意:每个流只能使用一次 2.获取流的方式 (1)单列集合:stream方法 KeySet()/values()/EntrySet() ( ...

  8. “流式”前端构建工具——gulp.js 简介

    Grunt 一直是前端领域构建工具(任务运行器或许更准确一些,因为前端构建只是此类工具的一部分用途)的王者,然而它也不是毫无缺陷的,近期风头正劲的 gulp.js 隐隐有取而代之的态势.那么,究竟是什 ...

  9. java8 流式编程

    为什么需要流式操作 集合API是Java API中最重要的部分.基本上每一个java程序都离不开集合.尽管很重要,但是现有的集合处理在很多方面都无法满足需要. 一个原因是,许多其他的语言或者类库以声明 ...

  10. 让代码变得优雅简洁的神器:Java8 Stream流式编程

    原创/朱季谦 本文主要基于实际项目常用的Stream Api流式处理总结. 因笔者主要从事风控反欺诈相关工作,故而此文使用比较熟悉的三要素之一的[手机号]黑名单作代码案例说明. 我在项目当中,很早就开 ...

随机推荐

  1. 快速基于 ClickHouse + Grafana 搭建可观测性解决方案 - 日志篇(ClickHouse 官方博客)

    引言 作为一款高性能的 OLAP 数据库,ClickHouse 被用于多种应用场景,包括 时间序列(time series)数据的实时分析.其多样化的应用场景推动了大量分析函数的发展,这些函数有助于查 ...

  2. [学习笔记] 斜率优化DP - DP

    这个真的好容易啊 --wzw 斜率优化dP 例题 [SDOI2012] 任务安排 毒瘤题,让我惨淡经营了两天.这道题luogu有简单版,可以先去看简单版. 显然这是一只DP题,直接开始推狮子.令 dp ...

  3. SMU Spring 2023 Trial Contest Round 11

    A. The Text Splitting 题意:给出字符串长度,给出p和q两种切割方式,任选其中一种,把字符串分割输出结果. 题解:先进行判断,p和q是否能整个的分割n,利用p和q的函数关系判断(见 ...

  4. 2023 CCPC 哈尔滨游记

    board zsy 11.3 下了高代课跟教练聊了会,以为差点赶不上飞机了,结果还好.飞机上一直在看<笑傲江湖> 晚上本来想写作业的,还是摆了 拉 zsy 打雀魂,三人麻将到第二天了 11 ...

  5. freertos总结

    freertos学习总结:(别人的)https://blog.csdn.net/qq_39397153/article/details/123997346 freertos学习笔记:(别人的)http ...

  6. Kafka Topic 中明明有可拉取的消息,为什么 poll 不到

    开心一刻 今天小学女同学给我发消息她:你现在是毕业了吗我:嗯,今年刚毕业她给我发了一张照片,怀里抱着一只大橘猫她:我的眯眯长这么大了,好看吗我:你把猫挪开点,它挡住了,我看不到她:你是 sb 吗,滚我 ...

  7. NVM 使用

    NVM(Node Version Manager)是 Node.js 的版本管理工具. NVM 项目为 macOS 和 Linux 开发.Windows 用户需要使用 NVM for Windows. ...

  8. LaTeX 插入矢量图

    首先将矢量图保存为 PDF 格式. 使用 pdfcrop 工具裁剪 PDF 页面空白: pdfcrop <input.pdf> [output.pdf] 在 .tex 文件中使用 grap ...

  9. 查看 Linux 系统信息

    查看系统信息 查看发行版信息 cat /etc/os-release lsb_release -a 查看公网 IP 地址 curl -4 icanhazip.com 查看系统架构 uname -m # ...

  10. Oracle同一台服务器创建多个数据库

    有时候我们需要再同一台机器上创建多个数据库服务(不是单纯的数据库实例),每一个数据库可以有单独的服务运行,只是在一个机器环境而已.可以在不同的端口上监听,也可以在相同端口监听 创建多个数据库步骤 安装 ...