ALGORITHM
This is a custom summary and does NOT appear in the post.
Table of Contents
作者:郭靖 链接:https://www.zhihu.com/question/562037084/answer/2730259900 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我就非常喜欢写 stream,并且要求我的下属也尽量写 stream。
for 循环里复杂的超过 5 行的逻辑,需要单独写函数,所以也不会允许在 map 里写超级复杂的 for 的逻辑。超过这些行数的代码段,即便写在 for 循环里,使得一个函数里一大堆 for 循环,同样不好读。所以我更推荐函数多,而每个函数行数少。
至于为什么推荐 stream,我觉得 stream 非常适合抽象思维去解决业务,而且我们就算做 CRM,ERP 等业务系统,我,至少我自己,对运行中的算法复杂度和空间复杂度都是很看重的(并且我们不允许 MySQL 的 join)。所以经常在业务逻辑中用数组,哈希表,树,对我来说,后端的数据都是各种 map filter distinct 等抽象而来的,而且写起来很有数学+抽象+逻辑思维。这一点上,我非常喜欢 stream。并且 stream 的一些特性也非常好用,比如保持原有的顺序。
况且我非常喜欢函数式的思维,无论是在业务开发,还是 AI,还是策略开发,还是运维各个领域,甚至到架构,函数式的思维也非常有用,甚至是非常有意义的。比如 serverless,有没有想过,在底层逻辑上,这两着之间有共同之处呢?Linux 的 terminal 的 pipeline,其实也和 stream 很像。
这是我喜欢用 stream 的逻辑。
——– 补充的分割线 ———–
我之前没有添加更多的例子的原因是,第一我不知道有没有人看,第二我看其他的回答认同这个道理的就会理解,不认同的也无所谓,但是看到最近的评论,而且也有不少人点了赞和喜欢,我觉得有必要要补充一下。
这里有一个争议是,MySQL 我们不允许 join,在一些复杂场景下,实在不行,可以先勉强 join,但是为什么不允许,因为当服务做拆分的时候,这些 join 是最麻烦最有问题的,机房不在一个地方,数据库也是比较难维护的。我职业生涯10几年,join 的情况非常少,即便是业务复杂的情况。那么下面我来给大家讲解一下计算机基础课。。。
不过知乎肯定会有人质疑各种,没办法,虽然我很讨厌背景介绍,但是。。哎。。我在大厂工作将近10年,大部分都是创业的时候就在,最多带人30人,在核心部门做核心业务,高级开发,架构师,技术管理,涉及B和C还有AI Ops,web3 都相关。我也是B站UP,虽然更多是做摄影和生活的,我露脸,你不露脸分享,也没啥好说的了。而且我平时也很低调不在网上涉及技术相关的东西,存在网上的技术内容不多。
还是辞掉了年薪百万的工作,哈哈_哔哩哔哩_bilibiliwww.bilibili.com/video/BV1zF411x7NR/
——– 看得懂就看得懂,看不懂就看不懂的补充的分割线 ———–
为什么我之前说 stream 非常好用,我觉得大部分初级程序员会认为只是写法的区别,而我认为最重要的是思路的问题,大部分人做业务确实是 CRUD,并且,也不考虑架构和性能,一个简单的 MySQL join,异地机房兼容,就让很多只做业务开发的程序员为难,所以只能说理解就是理解,不理解就是不理解。而且,有没有想过为什么外企总是考算法?所以,我会一直强调 空间复杂度 和时间复杂度。
设计范式
我先说我们的开发模式,不能说我们的就是对,只是有这样一个范式,大部分情况下,逻辑是:
- 从数据库查询出来的,是 id,不包含全部数据,如 select id from xxx where xxx…
- where xxx 必须命中索引
- 通过 id 再去查数据库的全部数据 select xxx,xxx from xxx where id=id,并且不能用 *
这个时候你会说,查那么多次有什么用,性能还降低了。那么在我们的框架里,通过 id 去查数据库的数据,一定会在缓存中,这个缓存使用 redis。所以我们提供了几个接口:
- get(id),先去缓存中取,如果有就直接返回,如果没有就从数据库中取,并且加入到缓存中
- getByIds(ids),批量(batch)的从缓存中取,如果有直接从缓存中取,没有就从数据库中取,并加入到缓存中
这个模式在很多大厂的公司内的设计范式中都已经很成熟了,这样的好处就是,当查询比较大的时候,只返回 id,在网络包上相对较少,举个例子,就算是 1万的 id,网络包有多大?有的人就算命中索引的查询了 1万个全数据,MySQL 返回到操作系统,再通过网络返回到后端服务器,光网络就要 5 秒,所以我们要求返回就算比较大,也能在一个可接受的范围内。这是算法复杂度,MySQL 的引擎基于 B+ 树,我们就以基础的 B+ 树来说,命中索引的查询在 O(logn) 甚至 O(n),网络的返回数据量也小,一般来说几百个 id,可以忽略本地网络的延迟。
拿到 id 后,去缓存服务器换详细数据,在这里,我们提供的中间件会批量去取,如果一个一个的取,网络请求数多,tcp 占用多,redis 服务器也容易请求不过来(虽然 qps 达到 4 万没什么问题),所以我们会用 mget 和 pipeline 取进行操作,这个是在底层实现,开发者无需自己思考(大厂就是这样,基建好,虽然我是做这个开发的),缓存的 get,mget,算法复杂度是 O(1),n 个 O(1) 接近线性 O(n),批量 50 个 key,或者 100 个 key 去查询,性能接近 O(1)。数据包大小接近数据库字段的大小乘以 key 的数量,这没办法。
所以,从这个范式中,你可以清楚的知道,你的业务系统的算法复杂度,空间复杂度在哪里,如果要优化的话,数据库慢?那你可以优化索引,如果是从缓存中读取慢?可以优化存储。(比如压缩算法)
同时,我们还会自动存储(Java) vo 层面的数据,缓存也有设计范式:
- 只通过 id 缓存对象,dao 和 vo
- 列表缓存 ids,不缓存对象内容
- 数据更新自动删除缓存
举例:
// 伪代码
@Cacheable
ABC get(Long id) {}
@Cacheable
List<Long> getIdsBy(...) {}
List<Long> ids1 = abcService.getIdsBy(...)
List<Long> ids2 = bcdService.getABCIdsBy(...)
List<Long> myIds = ids1 - ids2 // 差集,伪代码
List<ABC> abcList = ids1.steam().map(abcService::get).collect..
List<ABCVo> abcViewList = abcList.stream().map(vo::convert).collect // convert 也是 @Cacheable
可以从上面看出,为什么我们要用 stream,因为 stream 非常适合数据的处理,一进一出,中间做转换,而且在中间件支持的情况下,自动命中缓存。从这个范式你可以知道,通过条件查 id 列表,命中索引,ids 命中缓存,批量取 id,命中缓存,转缓存个 vo 对象,也命中缓存。
上面说的是设计范式。
这也是为什么我们面试会喜欢算法思路清晰的,反正我从来不问 Java 什么 String 和 StringBuilder 有什么区别。。。
详细解析
*CASE 1: 系统设计 - 时间匹配系统*
我们要设计一个系统,这个系统是有两边的用户,S 是学生,T 是老师,老师给学生上课,要设计一个时间管理系统,老师可以随意的放时间,学生可以选择批量的时间,从这些老师中找到适合学生需求的时间段。
在看思路的时候大家可以先自己想想,审一下题。
- 老师随意放时间,时间长度也不固定,就像预定会议室那样
- 学生批量选择,是按照范式选,比如,周一,周三,周五,下午 7 点到 8 点,从 10月1 号开始到 11 月 1 号结束,n 个学生一组(如果太难就一个也行)
- 节假日自动跳过
这是我们这个系统最早版本的需求,其实是有一点难度的,那么大家先想想,数据库怎么处理。
大部分人上来就是:
id, student_id, teacher_id, start_time, end_time, status
然后有 n 个 student 就多个 id,先不提 有的学生 中间可能会退出,有的老师中间不干了,数据状态变化很大,就先从这个 随意放置 时间,就会有限制。
虽然是系统支持随意放置,但是业务是固定时间,但是!但是系统要支持随意!因为,有的课程是 40 分钟,有的课程是 1个小时。
好,现在开始提问,先从简单的来:
- 怎么显示在日历上
- 一个老师放时间的时候,怎么高效(默认数据量至少大于百万)的查到时间重叠,就是,老师放了一个 6-7 点的时间,再放 5点50 到 6点10分,快速告诉他时间有重叠了
- 如何找到 周一,周三,周五,下午 7 点到 8 点连续的时间段,并且节假日跳过,而且,这个【周一,周三,周五 下午7点到8点】是可以配置的,比如【周一,周六 上午 10点】,同样,选择的时候两个时间不能重叠
这样很多人就会在原来的数据上写上
id, student_id, teacher_id, start_time, end_time, status, week_type, day_type, time_long … 来完成这个需求。
就算这样,还有很多很多问题,而且也不够抽象。只从数据库里去做,这个抽象很难,而且也很难命中索引,查询性能也低。也许你会说,这和 stream 有什么关系,我想说的是,如果你常用 stream,用数据结构的方式去思考,你会很快找到这个答案,如果用 for 循环在 POJO 和数组之间 add 一下,remove 一下,你想到这个答案的时间会很慢,不高效。最主要的,还是思路!
所以,有一个简单的设计,数据库就是:
id, teacher_id, start_timestamp, end_timestamp, status
这样简短就可以了。
我们可以把这个结构抽象成一个 矩阵。(数组也可以实现)
- 矩阵的横坐标是,天,周一到周日
- 矩阵的纵坐标是按分钟分割的时间段,比如 5 分钟,10分钟,1分钟也可以,但是矩阵会很大
- 你会发现大部分情况下矩阵都很空,可以压缩矩阵
- 矩阵按周存储在 redis 里,一年的矩阵数量是固定的
- 老师的量 x 年矩阵数量也是固定的
- 老师放置的时间 / 总时间 = 矩阵空置量,大概率 80% 都是空的,压缩之后空间很小
- 矩阵中包含这个时间的置为1,空的置为 0
简单表示一下:
00-10 = 0:10 | 周一 | 周二 | 周三 |
---|---|---|---|
00-10 | 0 | 1 | 0 |
10-20 | 0 | 1 | 0 |
20-30 | 1 | 1 | 0 |
数据库变成矩阵的方式也很简单:
- 从数据库中查出数据,stream 变为矩阵(逻辑互相独立),迭代性能也高,虽然内存多了一些,但不是完整 POJO,只是 id 和 时间,基本可以忽略 O(logn) 或 O(N)
- 构建矩阵 O(N),key 为周开始的时间存储
- 存储在 redis 里,k 量级
现在你就会发现,这个矩阵就是数据库的一个映射,一个指针了。
那么,逻辑就简单了:
- 如果老师要放时间,比如 3:20,通过 3:20 找到矩阵 x 和 y,看看是不是等于 1 就可以了,O(N) 的复杂度,好的时候,O(1) 就能查到
- 如果要查时间段的,比如 9月1号到7月22号,通过开始时间和结束时间找到矩阵列表(因为 key 是开始时间),O(N),k 量级的存储
- 学生要求是随便哪天开始,哪天结束,随便的时间范围,也可以生成矩阵,学生的在内存中生成
简单表示一下,我们定义为目标矩阵:
00-10 = 0:10 | 周一 | 周二 | 周三 |
---|---|---|---|
00-10 | 0 | 0 | 0 |
10-20 | 0 | 0 | 0 |
20-30 | 1 | 1 | 0 |
这样学生的要求可以随意。
我们的逻辑就抽象为:
老师矩阵 - 学生矩阵 没有数值 存在小于 0 的值的,则符合。
如:老师:101, 学生 101,101-101=000,符合。老师:111,学生 110,111-110=001,则符合。老师:010,学生:101,010-101=-11-1,则不符合(当然,在第一个-1的时候就已经可以跳出迭代器了)。
矩阵的加减法 O(N),逻辑也很简单。
所以代码就是:
teacherMatrixList.stream().filter(x-> x-s > 0).collect...
如果业务要看一共有多少老师多少显示,返回矩阵的总和:
teacherMatrixList.stream().sum().collect...
业务发现这段时间老师被选完了,推荐学生选其他老师,也可以通过上面逻辑来筛选。
teacherMatrixList.stream().sum().filter(rowCount>0).collect...
最后
List<Long> teacherIds = teacherMatrixList.stream().map(X::getId).distinct().collect
这个逻辑就很简单了。
抽象起来就是,矩阵加减法。
优点:
- 数据库字段少,上千万数据,索引也没问题
- 索引好设计
- 构建矩阵之后,逻辑全部抽象为加减法
- 空间复杂度可以计算(100万老师,一年52周,放置率 30%,redis 滚动超时,自动构建),时间复杂度也可以计算(数据库读出来的复杂度完全了然于心,构建成矩阵没问题,O(N),迭代,O(N))
- 如果中间出现性能问题,完全知道是哪里有问题,方便优化
- 架构层面,可以拆分,轻松拆分
- 运维层面,数据库和缓存的运维可以完全分开,责任到人
缺点:
- 我感觉就是有点难写,对其他人来说,云里雾里的
最后补充一下啊,我们是要求程序员写单元测试和性能测试,200万数据起,并且提交的时候会自动跑,过了才能提交。
这个项目单元测试覆盖率 99%,性能测试 200 万数据在一周之内,满矩阵的情况下,查询大概在 3秒,线上的情况要空很多。
这个逻辑被封装成包,对业务开发不可见,内核上线后 3 年没有改动。
这里的重点就在于,stream 给你的感觉是流水线的感觉,就是,数据进,数据出,就像玩那个过家家装扮玩偶那个游戏一样,先换发型,再换颜色,再选衣服,等等,一个流水线下来,成为了最终的数据。
从上面那么多 filter 最后,一页可能只显示 20 个 id,最后,我们再通过 20 个 id 去批量读 POJO,空间复杂度和算法复杂度,完全了然于心,你会发现整个操作系统对你来说都完全是透明的。
这个系统是我做的底层,在我做之前,就是最早的纯数据库设计,为了不同的需求 join 了自己至少 3 次,更不要说 join 别的表了。在用户量只有 1 万的时候,系统性能就崩了,为了活命,买了 AWS 最贵的数据库,一年大概百万的费用。上了之后,省了。。
好吧,这是补充的第一个业务层面的 Case,你会发现我没提很多 stream 语言的写法,毕竟只是语言,但是 stream 能够帮助有函数式思维的同学,更好的去抽象,而且最后你发现一个非常复杂的业务能用几个公式来抽象(而且产品也能看懂),是非常有成就感的。
如果有人读了,我再补充架构层面的。我只是想强调,培养写 stream,是培养思路,不是在语言层面,格局打开。
哦,对了,评论里有说,不怕 GC 的问题,我写的这么细了,你觉得我怕 GC 么。。。
*这个 Case 我可以留一个思考题:*
如果一个系统中没有老师可以有连续的时间服务一个班级,有没有办法找到两个老师,并且,一个老师占大多数时间(固定老师)?提醒一下,一个老师在周一,周三,周五的时间段里,一个月内,只有某一个周五不能服务,丢了太可惜了,为了节约成本,需要要找到另一个老师,并且不会浪费掉另一个老师(比如另一个老师时间是比较满的,扣掉了一个,就变成无用老师了)。
:)
2022.11.1
*CASE 2: 写代码 - 表单存储*
再跟大家分享一个非常简单的,但是有点另类的 case,就是我自己做为程序员的完美洁癖的东西。就是一个简单的表单存储。
这个表单是一个简历系统,简历系统包括基本信息,包括扩展信息,比如学历,就有多家大学信息,工作经历,就是说,有多个工作经历信息。
这个表单虽然说很简单吧,但是还有一个非常重要的功能,这个功能就是【实时保存】,也就是说在 50-70 个表单中,每移出一个文本框,就需要保存,这个逻辑很简单。当然,有的人在一开始会想,那咱们用 ES 或者任何一种 NoSQL 存储就行呀,不用像你说的那么复杂。但是,首先,我们当时没有 ES 和 NoSQL 的预算和运维来维护,数据量也没有那么大(大厂对于新技术的预算是很严格的,就算有这个基建,如果量不大,申请也不一定会给批),还需要进行反向索引。举个例子,有一个人写简历,这个简历有一个学校是,清华大学,那么我们是需要拿到清华大学的简历列表的。而这个对应关系是 1对多,所以是不同的表存储。当然,ES 和 NoSQL 也可以做索引,有很好的基建咱们不谈。
所以,结构是:
- 个人简历信息:id, student_id, name, mobile … 都是基本信息
- 学校信息:id, school_name, … 基本信息
- 工作公司:id, company_name, 基本信息
- 个人简历和学校的映射关系 school_map_id, student_id, school_id
- 个人简历和公司的映射关系,不赘述了
这个结构很简单吧,而且实现起来也不难。
但是,但是,唯一的一点就是要实时保存,很多人对于映射关系的解决方案是,全部删除,然后再全部增加,或者查询 in,如果不 in 就增加。后面的方式实现的很麻烦,数据库可能要 join 或者要 in,当数据量大又是需要优化的。而前面的方式最大的问题是,数据库的自增 id 会增加。比如,如果有 50 个表单,每次都要删除映射关系,再增加映射关系,这个 id 会增加的很快(即便不修改任何信息点击保存,也会删除映射关系)。一个人,有可能 id 会增到 50 多,如果有一万个人呢?那么增速是非常快的,虽然,要达到上限还需要一点时间,但是,在设计大型系统的时候,细节是很重要的。
同时,我们也知道,一个人的简历信息只有一条(历史记录可以放在冷库里),一个人在的学校也是有限的,不可能超过 20 个,一个人的工作经历也是有限的,再夸张也就 100 个了。所以,只通过 student_id 索引拿到的信息,直接在内存处理,要比数据量大了之后,数据库的 in 要高效,并且,这个性能不是线性降低的,只要索引正常,性能基本不会变。
所以,我们的目的是,实时保存的同时并且 id 尽可能不增加,所以我们只要拿到哪些信息要增加,哪些信息要删除,哪些信息要更新就行了,所以。
前端传学校/公司名称(假设在数据库中唯一),我们去更新:
//伪代码
List<Long> hadSchoolIds = schoolMapService.getIdsByStudentId(studentId);
List<Long> schoolIdsInRequest = schoolNames.stream().map(studentService::getByName).collect
// 在请求中不包含 id 的说明是要删除的
List<Long> needToDeleteIds = hadSchoolIds.stream().filter(!schoolIdsInRequest::contains).collect
// 请求中的 id 但是不在已经有的,说明要添加
List<StudentSchoolMap> needToAdd = schoolIdsInRequest.stream().filter(!hadSchoolIds::contains).map(StudentSchoolMap::toEntity).collect
List<StudentSchoolMap> needToUpdate = ..
上面的逻辑随便手写的,可能有bug,但大意就是通过两个 id 列表的,交集,差集,可以知道哪些是需要更新,哪些是需要删除的。这个逻辑还是基于 id 的逻辑。
比如,前端先请求 (A,B)。然后再请求 (B),会发现已有的(A,B)中,A 没有在请求中了,则删除 A。这个时候就是(B),然后再传(B,C),发现已有的是(B),可以知道(C)是新增的,B 可以不用管。再传(A),可以知道已有的事(B,C),交集为空,所以(B,C)删除,增加 A。所以,除非每次交集都为空,变成最坏的情况,就是每次全删除,每次全添加,但大部分的情况都不会增加,因为大部分时候都是更新其他字段,而不更新这个字段,但是自动更新就一定要考虑到这个情况。
当然,这个 case 比较简单,也比较细节,你会想,这个和 stream 有什么关系呢,用 for 循环不是更好嘛,你这还用了多个迭代。我想说,如果你是用业务逻辑的方式抽象的话,在实现业务逻辑上,用 for 循环是没问题的(但大部分人用 for 的时候是不会用这种交集差集的方式思考的),但是 for 写起来要在 for 逻辑里检查是否 in,增加到 for 循环外面的数组,排序等等都很麻烦,如果用 stream 写,就很简单了,变量名清晰,后面的每一步是在做 filter,在做对象的转换,都很清晰。
这个简单的 case 里,stream 也可以合成一个,变成 map,然后 map 里分别保存需要增加的,需要删除的,需要更新的,然后抽象成集合 utils,就变得更简单更容易理解了。大家可以自己考虑一下。
之前的同学就是通过全部删除和全部增加去处理这个的,一下子就增加了几万的 id,虽然用户量不大,在更新这个逻辑后,id 增长大概为原来的 1/20。(大部分人一般不更新映射信息,可能只是修改一个姓名。。)
我觉得很重要的一点就是,stream,永远给人的感觉是 input 数据,通过一些操作,一些转换,变成 output 数据的逻辑。这个在思维上和 for 是完全不同的。
同样,有个思考题,比如,在做微博系统的时候,我关注的,和关注我的,还有互相关注的,是不是也可以抽象成集合的概念?那么大的系统,总不能用 MySQL 的 join 或者 in 吧。
*CASE 3: 架构 - AI Ops 数据策略平台架构开发*
最后再写一点架构的设计吧,我一直在文章里强调的是,stream 的设计思路,是数据处理的思路,和 for 的思路是不一样的,我们不是禁止使用 for,而是更希望用 stream 来抽象,所以我们并不只是谈 Java 的 stream 语法糖,更多的是聊 stream 这个东西,不同的语言对 stream 有不同的支持,问题可能是聊的是【为什么喜欢写 stream】,我更愿意说,懂得使用 stream 思路的程序员是更厉害的,更懂得抽象的。
前面讲了几个设计和代码中对于 stream 的思考,我来说一个架构的设计吧,其实 stream 的思路,我感觉是 mini 化的 map reduce 的感觉,在架构上,微服务架构,lambda,其实也多多少少贯彻了这样的思维。所以在这点上,如果你做架构设计,就必须要考虑存储的平台性质,即不能在一台机器上。在服务架构和服务治理上,也需要有数据 in 和 out 的感觉,不然规划起业务架构来,更是无从下手。所以很多人没有自己的所谓【架构设计的价值观】,只有【阿里这么做了所以我们才这么做】的奇怪理由。
举个例子吧,我们的一个系统需要通过不同的策略来最终定价和定数量,假设这个系统是一个淘宝的商城的系统,我们需要根据天气,时间,历史数据,等等条件,通过机器学习(大部分是概率的工具和置信区间的验证)和一些策略脚本来进行最终的结果的推送,帮助业务进行最终的决策。那么这个策略非常多,有很多版本,需要很多提前验证,事后验证,就是评测和回测的逻辑。
那么现在的架构情况是:
- 每个部门有上百个版本的脚本,包括但不限于 Python 脚本,SQL,Spark,Hadoop 任务
- 每个文件没有版本,随时更新覆盖,就是普通的文件
- 有一个调度器,用来调度脚本,一个脚本执行完之后,再执行另一个脚本
- 脚本有几万个之多
这个其实就是一个 AI Ops 的工程问题,像现在,有很多 AI 模型,AI 的框架可以快速的进行模型的训练,也有很多存储和工具,可以大量存储数据,也可以使用 map reduce 的方法去多机器执行计算(比如 Scala 在这里写成类似 stream 和函数式方法,就特别适合 AI 和数学的场景),但是编排的工程能力很弱。我知道的 Databricks 做的还不错,叫特征工程平台,今年年初才有试用版。但,不管怎么样吧,现在的情况就是这么个情况。
这个时候就需要用一点抽象和 stream 的方法来设计了。stream 强调的是什么,是流水线的感觉,数据 in 数据 out,所以,我们可以这样抽象。
- 定义一个叫算子的概念(即计算单元),这个算子是可执行的代码文件,可以是 Python,可以是 SQL,也可以是 Spark 任务
- 算子输入是 Dataset,输出也是 Dataset
- 用表达式赋予算子之间的关系
- (技术细节)算子算出来的结果,自动根据名称和时间和版本保存在中间表,可以作为结果,或者作为缓存
- 可以对算子进行版本控制
下面我来解释一下上面的设计,包括为什么用了 steam 那个感觉的思维。不过在介绍之前,我先说一下之前的最大问题,就是:
- 每个人维护自己的文件,文件无法平台化,所有的跑逻辑都靠人
- 跑出来的数据无法标准化
- 每个文件都没有版本,所以无法根据某种情况来测试
- 概念没有抽象,所以无法进行有效的进行决策的更新
前面两点有经验的同学可以自己理解,后面怎么说呢,可能没有做过 AI Ops 架构的同学不太了解,说白了,就是跑脚本 A,出结果,再跑 B,再出结果,再跑 C,现在要对 B 进行更新,然后结果怎么样呢?得手动改 B,然后重跑 B,结果好或者不好,无法对比,不然还得跑 A。然后所有的脚本都可能是不同部门的人维护的,每个人没办法有完整的时间支持,所以在整个公司是无法推动的。中间也没有自动跑数,和缓存逻辑,就非常复杂。特别是一个策略可能牵扯到上百个脚本的时候,更是没发去做测试。而且,到这里,我都无法跟你说这个抽象的逻辑是什么。
现在,使用算子这个概念之后,我可以跟你简单的抽象为,就是策略的计算公式。所以抽象如下:
- 定义一个算子,是可执行文件,如 A, B, C
- 每发布一个版本,版本号自动更新如 A1, B1, C1
- 策略用 DAG 有向无环图来编排,确保没有循环引用
- 策略,用公式来抽象,如,(A + B) - C,如果是不同的版本,则 (A + B1) - C
- 每次算子跑出来的数,滚动存储,如 A_1_20221101,这样在执行的时候,可以利用前面的数据来进行快速重跑,如 (A(cached) + B1(not cached)) - C(cached)
- 结果根据公式的 hash 来存储,可以进行不同策略的结果进行对比,可以进行评测和回测
注意:在这里 +,- 符号仅仅是代表抽象,比如不同的 Dataset 字段不同,格式也不同,那怎么处理呢?聪明的你已经想到了,那就是 map 啊!
如:
(A + B) - C
我们可以定义为 +,为两个表格的笛卡尔积,- 为同字段数字相减。当然这样,肯定不符合逻辑了。现在,聪明的你又可以说,我们不用 +,- 符号了,我们定义函数不就行了。如:
定义 1Function = 取 A 的价格字段 分别乘以 B 的折扣字段, 然后设置到列中
定义 2Function = 去上面结果的价格列,减去 C 中的利润
那么上面的那个抽象就是
(A 1Function B) 2Function C
这样很好理解吧,然后
(A 3Function B1) 2Function C2
你们就能理解是用不同版本的算子,不同版本的操作符来进行策略的是用
说到这里,和 stream 的思路有什么关系呢。。那就简单了。
伪代码,Java 在这里并不支持这个写法
A.stream().map(1Function, B).map(2Function, C).collect
所以在代码层面,map 相当于做函数调用,映射等逻辑。如果你说 Java 语言本身,这个 stream 语法糖可能不支持,但是这个思路在 Spark 和 Hadoop 做的很完善,更不要说用 Scala 写的话,感觉就像数学公式一样。比如可以这么写。。
f(x) = sum( g(h(x) - i(x)) )
。。。
这个 Case 是一个架构设计的 Case,和语言层面的 stream 没关系,但是和背后的思路和思考逻辑有很大的关系,在大型应用中,如上千的微服务设计,你需要考虑数据流怎么进怎么出,数据怎么进行计算(如加减法这样的抽象),在 AI Ops 的设计中(最近特别火),如果没思路,也可以模拟 Spark 和 Map Reduce 的思路,在工程实现上,可以模仿操作系统的线程调度等思路,包括一级缓存和二级缓存,都可以考虑。抽象成数据转换,无论是在微服务设计还是 AI Ops,还有 K8S 等只要需要进行调度,业务边界的划分,都可以用这种思维。
不过,可能我说 stream 思维,是非常不准确的,大家理解这个意思就行。
另外,我说的这个解法,可是很值钱的。如果能落地的话,年薪 200 万不成问题,还可以更高。。。
相信我,要自信。:D