为什么程序员工作后就喜欢用 stream 流_-郭靖
我就非常喜欢写 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,虽然更多是做摄影和生活的,我露脸,你不露脸分享,也没啥好说的了。而且我平时也很低调不在网上涉及技术相关的东西,存在网上的技术内容不多。
-------- 看得懂就看得懂,看不懂就看不懂的补充的分割线 -----------
为什么我之前说 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) {}
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| 0| 0
10-20| 0| 0| 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
最后:
如果 Case 有什么问题,或者你有更好的设计,或者对细节有什么疑问,欢迎在评论区咱们讨论。
评论区
JUJU: 复杂的业务,用流只怕会更难写,而且更难改。最好写也最好改的是使用迭代器遍历,其次是range for。函数式编程的目的只是为了少写几行代码罢了。复杂的业务,代码必定是复杂的,不可能通过改个写法就变得简单。建议你能举几个具体例子说明使用流可以简化业务 👍🏽83 💭广东 🕐2022-10-26 19:58:50
│ └── ccc: 就是因为他的业务太简单呀 所以才能这样呀 👍🏽18 💭湖北 🕐2022-11-01 10:37:54
│ │ └── 郭靖: 如果真的觉得太简单,我可以推荐你年薪至少 150万 的工作,现金部分占 70%,股票 30%,至少当时我解决问题的时候差不多是这个价位,期待你联系我![爱] 👍🏽6 💭北京 🕐2022-11-01 17:26:06
│ │ └── wang: 这就没意思了 硬说钱你这搁国外也就vp/svp水平。。。。 技术文章不如举例子反驳下[好奇] 👍🏽7 💭上海 🕐2022-11-04 09:32:31
│ │ │ └── 郭靖: 我写的几个case很简单吗? 👍🏽0 💭北京 🕐2022-11-04 12:30:38
│ │ └── hdyyusfjuy: 请问还可以推荐下工作么 👍🏽0 💭上海 🕐2024-05-05 14:18:03
│ │ │ └── 郭靖: 如果觉得水平不错的话可以自荐啊。 👍🏽0 💭湖北 🕐2024-05-06 20:26:02
│ │ │ └── hdyyusfjuy: 可以先介绍下工作信息么[握手] 👍🏽0 💭上海 🕐2024-05-06 21:52:43
│ │ │ └── 郭靖: 是你在找工作还是我在找工作?一般没意愿就算了,现在随便发个职位一大堆简历,多了筛选起来还烦,算了。太麻烦了。 👍🏽0 💭北京 🕐2024-05-08 12:28:12
│ │ │ └── hdyyusfjuy: 不发岗位信息我怎么知道有没有意愿?我怎么知道我的技术栈是否适合这个岗位?[飙泪笑]要是996月薪三千换你你有意愿么?如果真的是你随便发个岗位都收到一大堆简历那你何必来知乎评论区招人[飙泪笑] 👍🏽0 💭上海 🕐2024-05-09 07:57:14
│ │ │ └── 郭靖: 我没在知乎招人,是你问我的啊。 👍🏽0 💭北京 🕐2024-05-09 11:45:48
│ │ │ └── hdyyusfjuy: 如果真的觉得太简单,我可以推荐你年薪至少 150万 的工作,现金部分占 70%,股票 30%,至少当时我解决问题的时候差不多是这个价位,期待你联系我这是你之前的评论原话,不会是这个推荐的工作现在不招人了吧[尴尬] 👍🏽0 💭上海 🕐2024-05-09 21:42:45
│ │ │ └── 郭靖: 这句话我不是回复你的,是之前回复别人的。然后是你问我要工作岗位的,对吧? 👍🏽0 💭北京 🕐2024-05-10 16:15:56
│ │ └── 王起: 用steam流,出问题的时候怎么断点调试,还是说你们的业务开发时不用断点调试? 👍🏽0 💭广东 🕐2024-05-15 11:30:05
│ │ │ └── 千山不倒: 说句话,我是底层程序员,我们用的java8,只有stream速度才快。我也不得不用stream流。至于调试,用的时候我就知道进去和出来的结果啊。结果不符再读读,我比较菜,超过只有套了3层stream流我才会考虑拆分出来。 👍🏽0 💭广东 🕐2024-06-02 01:15:00
│ │ │ │ └── 王起: 你说得对,自带并发处理确实时stream流的好处,而且优势不小 👍🏽0 💭广东 🕐2024-06-07 17:30:53
│ │ │ └── 郭靖: stream 不能调试不能打断点?第一次听说。 👍🏽0 💭北京 🕐2024-06-06 16:51:43
│ │ │ └── 王起: 不是不能打,而是stream的链式表达将多个操作写在同一行,所以断点时不容易定位到预期的代码段 👍🏽0 💭广东 🕐2024-06-07 17:29:58
│ │ │ └── 二狗: 虽然是在同一行,但是你可以写成多行,然后调试的时候就是一行一行执行了… IDEA 的 stream 调试工具还是很清晰的,不是吗? 👍🏽1 💭北京 🕐2024-07-01 16:32:42
│ │ │ └── 随便就好: 其实更大的问题是在Stream流里抛出的异常信息不够清晰,除非在流里面手动处理异常信息。这种问题在上线后对排查bug的影响很大,很不好定位bug位置 👍🏽0 💭新加坡 🕐2024-09-05 20:10:22
│ │ └── 王勤奋: 24 年看到的,请教个问题,你说有数据全部通过 id 查询? 那么请问怎么保证缓存和数据库数据一致性问题的? redis 的存储大小又是多少呢? 👍🏽1 💭上海 🕐2024-09-03 20:25:30
│ └── sir.Wu: 楼主可能想表述input、output的函数式编程的思维,stream只是编程思维的一个类比,各位不要再纠结stream这个点。用for你也可以封装一个stream 👍🏽0 💭湖北 🕐2024-05-30 11:37:28
│ └── 一杯美式: 24年才刷到 评价下 我是觉得stream 结构简单 我是真不喜欢在for里面写一堆逻辑 看到的我都得给他改了;其次很多人在说stream不好调试 碰到需要调试的bug迭代器遍历和函数不都一样需要一行一行下去 最终会执行到报错的一行 完全能定位到 你要说在stream写很多逻辑;难道不能拆分出来吗[发呆] 👍🏽0 💭广东 🕐2024-08-31 14:19:50
│ └── JUJU: 你坚持下去就好 👍🏽0 💭广东 🕐2024-09-17 20:24:00
wzy: 写了十几年,程序了啊两种都用过,实话实说,对于流省下来的代码和性能,在整个庞大的工程里并不算什么。只有一个准则,好不好排查问题,好不好维护。真正的项目中,你做的代码不是给你一个人的,是给整个团队,以及你走了之后其他人的。从我的视角来看,for循环很容易debug,出问题,有时候看行数对应的代码就能知道问题在哪儿。用流的话都挤在一起,不太容易看出来。 👍🏽38 💭浙江 🕐2022-11-05 12:30:01
│ └── Sherhom: 挤在一堆是因为写steam的时候,没有把每个函数换行。如果有换行的好习惯,并不会挤在一堆。另外,现在开源大数据生态的kafka、spark、flink、presto等,都是能用stream,绝不用for。阅读和排查问题没半点问题。说到头还是代码习惯的问题。要是写个for循环,而不换行,排查问题依旧挤在一堆啊。。这种在大型项目中,是需要用checkStyle等工具去限制编码风格的。 👍🏽9 💭广东 🕐2023-03-11 14:04:40
│ └── 耳东: spark、flink这些业务单一,系统虽然庞大但是也不复杂,实际的项目需求恶心程度远远高于这些基架,你再写个stream没人愿意去看代码了 👍🏽4 💭上海 🕐2024-01-30 17:39:55
│ └── Sherhom: 分情况吧,迭代涉及太多中间变量的确实不适合stream。 不过,适当的使用lambda减少代码量。4-5行可以完成十几行代码的功能,反而能让人一眼看清楚代码含义。还是上面说的,主要是命名规范和换行的问题。换行完全可以通过code style+快捷键高效解决。而如果命名不规范,即使不用stream依旧没人愿意去看的。 👍🏽0 💭广东 🕐2024-02-07 22:22:33
诸果之因: steam流真就调试火葬场 👍🏽45 💭江苏 🕐2022-11-01 18:10:00
Kirin: 我能说,数据量一大你不怕gc么 👍🏽16 💭福建 🕐2022-10-27 23:26:08
mysolitude: 自己写的能比数据库join出来的好? 👍🏽10 💭江苏 🕐2022-10-27 16:32:14
│ └── 快丶放开那御姐: join不是性能差么。自己写就是多次请求然后一直forforfor[害羞]应该快点 👍🏽1 💭江苏 🕐2022-11-01 17:01:16
│ │ └── 小雨儿: 我刚毕业,大哥你说的是真的吗? 👍🏽1 💭美国 🕐2022-11-02 23:54:32
│ └── applepearTree: 水平得多差才觉着数据库join好,[尴尬]数据、访问一大死都不知道怎么死 👍🏽8 💭北京 🕐2022-11-02 21:11:15
│ │ └── 泰坦造物: 那是数据库的问题,数据库没做好[捂脸] 👍🏽0 💭河北 🕐2022-11-04 09:26:05
│ │ └── 知乎用户: 自己好好再学学,你该不会真以为 join 就是笛卡尔积,数据库很多年前就进行性能优化了 👍🏽0 💭江苏 🕐2025-02-24 07:56:52
│ └── 泰坦造物: 个人经验,java,mysql这个开源生态里,别人说好的基本都有坑,别人说不要用的,基本就是大坑[捂脸] 👍🏽1 💭河北 🕐2022-11-04 09:25:35
│ └── Sherhom: 不一定比数据库join好。但是业务上不用mysql做join是后端的共识。这是为了避免高并发的慢查询阻塞其他的业务查询。 👍🏽6 💭广东 🕐2023-03-11 13:57:28
│ └── 妇科圣手张无忌: 请问你知道什么是笛卡尔积吗 👍🏽0 💭广东 🕐2023-08-01 16:47:14
│ └── 布莱恩史努比: 很少join,宁愿每个表有数据冗余也不愿在常用业务表里join 👍🏽0 💭天津 🕐2024-04-30 09:23:16
│ └── 程序猿: 后期做业务拆分,join也是个问题 👍🏽0 💭广东 🕐2024-05-05 15:20:59
油面筋塞肉: 摊上这么各领导的员工真惨,各种蜜汁自信,把别人的功劳算作自己厉害。。。C#的扩展方法推荐用,但是java的stream太差了,不推荐用 👍🏽5 💭上海 🕐2024-05-01 13:12:56
│ └── 郭靖: 那抱歉了。 👍🏽0 💭湖北 🕐2024-05-02 16:47:26
│ └── 油面筋塞肉: 跟你下属道歉才是真的 👍🏽2 💭上海 🕐2024-05-02 16:48:11
AI布道师Warren: 「尽量往 stream 靠拢」,不苟同,看做的是什么类型的工程,从后期维护成本来看,好的函数抽象以及函数式编程的风格,不易于现在很多程序员上手维护和调试(感觉普遍情况,团队水平低的是大多数?猜测的);这种风格感觉尤其适合那种高性能的组件工程;但这个没有可比性,就像我们会选择 java 做复杂业务系统而不是 cpp ,原因类似我的工作常跟数据和文件打交道,总体来说,流式编程作为编程范式,有优点和缺点, 小到 java,scala, 大到 flink 和 spark,感觉已经上升到一种哲学理念了,很酷还有关于多表 join 有些感悟,其实我有针对自己的业务不同规模的数据做过 benchmark,结论就是,数据库提供的 join 方案其实就是适用于「大多数水平一般」人,因为性能其实还不错;想不使用这个方案,自己做,需要高水平,考虑很多问题(不是那种背背面试资料就能弄明白的问题)不幸的是,我也是属于其中「水平一般」的一员 = = 👍🏽6 💭北京 🕐2022-11-04 11:03:12
LiuYang: 前同事写了个五六行的stream语句,报错了,错误信息中行数显示在第一行。心中万马奔腾:写就写吧,输入数据不做校验的么。 👍🏽5 💭河南 🕐2024-02-16 10:03:10
│ └── 王起: 哈哈,我也想问作者这句,但好像推荐用stream流的人从来都没有正面回答过这个问题 👍🏽1 💭广东 🕐2024-05-15 11:35:49
│ └── 二狗: 拆分成多行。 如果stream中涉及的bean自身没有处理空值的能力,那么就加 .filter(Object::notNull) 来处理。 这样的话就变得臃肿了, 所以空值问题应该在 bean 对象内部处理, 比如默认返回 [] 空数组而非 Null。 👍🏽1 💭北京 🕐2024-07-01 16:36:37
阿鲁达比拉崩: 有个同事特别喜欢用流 ,修改他的代码的时候都要气的砸键盘 👍🏽4 💭浙江 🕐2023-06-21 11:54:48
│ └── 大黄: 那不就是我吗[酷],不过我的代码很少比人改,bug比较少,基本上都是我改别人的多。我是C后来转的java,一开始看不懂流,后来上手了之后,感觉这东西真的就是主打一个方便,理解的人看起来一眼就能知道你流的目的是干啥的。反而for循环还要阅读理解一番。当然我指的不是那种在流里面硬塞一大段函数代码而不是抽象一个函数的那种。 👍🏽5 💭中国香港 🕐2023-11-30 14:02:35
大叮叮猫: 大量的少数据处理,用流效率太低 👍🏽3 💭江苏 🕐2022-10-31 18:22:33
不会空大很多年: leader工作太空了,写这么多,工作不饱和[doge] 👍🏽1 💭上海 🕐2024-04-29 14:08:25
逆风飞翔: 再多想想吧 👍🏽2 💭湖南 🕐2022-10-30 03:55:13
是经济危机: 这是什么鬼畜设计?不用join?这么说吧,我们业务是有关船的。一艘船有多个舱单,一个舱单里面有多个箱子,一个箱子里一般是一种货,也有可能是多种货。那么我要查询载有某种货的船都有哪些?你不用join,把4个表都缓存到Redis里面? 👍🏽2 💭山东 🕐2023-07-25 06:16:03
│ └── 山鬼: 我们公司之前也不让jion,这种通过es做搜索,但是es又有单独人的负责沟通起来太麻烦就jion了。如果数据量没那么多并发也不大jion完全没问题,数据量大要分库分表的话jion就不行了 👍🏽0 💭浙江 🕐2024-03-03 02:26:48
│ │ └── 码农还怎么玩游戏: 是join啊[飙泪笑] 👍🏽0 💭江苏 🕐2024-05-03 06:52:00
│ │ └── 山鬼: 人有时候打字会突然抽风[捂脸] 👍🏽0 💭浙江 🕐2024-05-03 12:14:20
│ └── 浮生若梦: join四个表不慢吗,查四次库和join四个表哪个快 👍🏽0 💭北京 🕐2024-09-13 17:19:29
│ └── 是经济危机: 我说的那个业务必然是join快。因为有些表你要in后面几万个 👍🏽0 💭山东 🕐2024-09-13 18:06:44
丑的猪都傻眼了: 老师你好关于你写的stream部分表示相当认同。但是join这个我有点疑问,假如存在A,B(A、B相关联,B数据量大约为A数据量的十倍),业务希望分页展示A,但是分页的筛选条件中既有A条件,又同时有B条件。遇到这种分页需求,如果不采用join,应该如何实现呢。真心求教 👍🏽2 💭浙江 🕐2024-04-19 08:52:53
│ └── 二狗: 这种场景设计从直觉上讲是不合理的。B相当于是A的继承子类,如果对A做分页的话,不应该涉及到对B的筛选,即使A下没有候选集,是否也应该展示A呢?可能需要具体问题具体分析吧, 如果说一定要考虑B中的条件,那么只能join了。但是这一定属于产品设计上的强耦合。 👍🏽0 💭北京 🕐2024-07-01 17:04:26
│ │ └── 吾家有女初长成: 那全都是强耦合 产品什么查询条件都想要 👍🏽0 💭浙江 🕐2024-07-27 08:36:32
│ └── 浮生若梦: 可以先查b,查出来b拿到a的ID,去查a的时候加上in ID的条件 👍🏽0 💭北京 🕐2024-09-13 17:23:43
妇科圣手张无忌: 不允许 MySQL 的 join后端的数据都是各种 map filter distinct 等抽象而来的这两句话不是老开发是说不出来的 👍🏽2 💭广东 🕐2023-08-01 16:45:52
乖一点就抱你: 为啥都说不利于维护呢,好的抽象和日志有啥不好维护的[思考]不抽象无日志啥代码又好维护呢 👍🏽2 💭北京 🕐2022-12-10 12:32:15
做一场梦: 厉害[赞]。醍醐灌顶,从业务逻辑到数学模型,再到程序设计。答主是做底层的吗?对于一般业务,程序员很少会去建立数学模型的。 👍🏽2 💭上海 🕐2022-11-05 22:57:59
陈陈陈陈: 我不管,for循环天下第一[吃瓜] 👍🏽1 💭湖北 🕐2023-12-26 13:35:08
eqqeeqee: redis这个思路学到了,其他评论提到的join我感觉还是不要教条,该用还得用 👍🏽1 💭天津 🕐2023-09-12 17:08:23
云云云: 这个通过id去查数据库的全部数据是怎么的查法呀,用in吗?还是一条一条查,还是中间表关联 👍🏽1 💭北京 🕐2023-09-10 10:48:08
闲敲棋子落灯花: 请问为什么不能select *,我自己试了一下mysql,特意用的limit m,n的语法。我的服务器500多万条数据,select *大概是1.75秒,select字段大概是1.8秒,这两个响应时间没有什么区别吧 👍🏽1 💭重庆 🕐2022-11-04 16:52:38
│ └── 郭靖: 顺序,序列化反序列化,缓存等。 👍🏽2 💭北京 🕐2022-11-04 17:46:57
│ │ └── 高木同学: 还有一点,select * 的写法,如果后期某人维护时,新增了一个字段,但是mapper文件中的do没有这个字段,系统就会跪了,尤其是多系统都有这样的mapper文件时[doge] 👍🏽3 💭浙江 🕐2023-06-25 12:44:00
│ └── Day1: 看引擎 👍🏽0 💭湖南 🕐2022-11-05 09:35:11
码婆Doph: 谁能想到你是一位摄影up 👍🏽1 💭陕西 🕐2022-11-03 08:52:33
Leonard: 受教了 👍🏽0 💭上海 🕐2023-09-01 14:43:14
我只是在等你: 好 👍🏽0 💭四川 🕐2023-03-09 02:28:54
络腮胡小阿东: 写时一时爽,debug火葬场 👍🏽0 💭北京 🕐2024-10-12 10:46:08
沐丶: 佬, 我对你说的这个思想有点感觉了, 对你那个矩阵的设计方案很感兴趣, 但是这一遍看下来, 没太懂, 等我研究研究之后来请教. 👍🏽0 💭江苏 🕐2024-09-13 16:59:16
圈外人: 工作后还用什么stream吗 不应该是steam吗 👍🏽0 💭河南 🕐2024-09-06 11:07:01
三清铃悠然: [滑稽]以前wegame,工作了steam,没啥问题 👍🏽0 💭北京 🕐2024-09-04 14:17:26
小旋风: 来享受一下视觉盛宴吧,[图片] 👍🏽0 💭江苏 🕐2024-09-04 17:25:46
我写代码像cxk: 简历设计那个有必要这么复杂吗,直接一个 text字段,无脑更新就行了。如果想进行搜索的话,得上 es 索引。基建不支持的话,得把字段拆分出来,这样就冗余了很多信息。 👍🏽0 💭上海 🕐2024-09-02 16:44:46
啥也不懂: 关系型数据库不准用关系,牛逼 👍🏽0 💭广东 🕐2024-08-16 10:11:55
撒加: 这几个问题讲得挺不错的 👍🏽0 💭上海 🕐2024-08-09 17:51:29
乌鸦: 啊 看到微博的相互关注了,这个据我了解是放在redis集合里做的,Redis的集合有很多相关操作 👍🏽0 💭北京 🕐2024-08-09 11:06:28
乌鸦: 感觉受益匪浅,还有没有类似的带case的技术贴?求[拜托] 👍🏽0 💭北京 🕐2024-08-09 11:50:30
乌鸦: 99%的测试覆盖率真的夸张。关于Join,我的理解是数据量小的时候无所谓,设计表的时候可以直接冗余一部分字段来避免join。完全避免join对于大多数公司业务来说并非必须,毕竟不是人人互联网。答主的方案确实很不错。 👍🏽0 💭北京 🕐2024-08-09 10:48:45
CCafe: stream这个仁者见仁智者见智,复杂逻辑肯定不会用这个写,他只是会让代码看着少了,但会变成一大坨,对于简单逻辑我也喜欢用,毕竟一目了然。关于join这个,我只能说要用这个的人绝对没有接手过百万级别的业务系统。就一个分库分表join的语如何跨库跨集群谁来解决一下?另外一个就是用join你如何保证项目上的所有人水平和你一样,不会产生慢查询?慢查询在这个级别的系统就是致命的 👍🏽0 💭四川 🕐2024-07-27 06:57:08
夏末: 哈哈,是的,尤其是报错的时候看到一坨stream 👍🏽0 💭重庆 🕐2024-06-05 16:26:11
青铜时代: 如果做过财务报表系统,就不会一句轻飘飘的不允许用join了 👍🏽0 💭北京 🕐2024-06-04 08:59:49
│ └── 郭靖: 财务系统一样做的了,我们就在做,并且用同样的方案做 SaaS,而且为了更方便,还自己做了一套上层的编程语言。类似 Salesforce。 👍🏽0 💭北京 🕐2024-06-06 16:50:06
│ └── 青铜时代: 要不你先看下评论主题再说,我的侧重点join,你是saas 👍🏽0 💭北京 🕐2024-06-25 12:07:37
低调: 如果做过一定规模的业务复杂的系统,估摸着就不会说勉强用join了。两三千张表,简单一点的三五张表关联查询,多的十多张表,关联查询条件还不仅仅是主键,用二次查询,N次查询试试? 👍🏽0 💭湖北 🕐2024-05-31 08:34:56
│ └── 繁华不过一场春梦: 我在想,不 join,关联的分页需求怎么做 👍🏽0 💭北京 🕐2024-06-26 22:30:03
│ └── 二狗: 这个看业务场景了。 比如A需要B字段来补充, 先查询出A后发现没有B的候选集,那么也应该把A显示出来吧,但是此时会有一个状态来进行标识。 👍🏽0 💭北京 🕐2024-07-01 17:07:03
啦啦啦641: 我写stream的目的就是为了把代码的可读性降低,提高不可替代性,即使被替代也不能让公司好受[爱] 👍🏽0 💭广东 🕐2024-05-29 08:44:23
Garfield: 你这写出来,给谁看?这怎么阅读和调试,让别人怎么接管[发呆] 👍🏽0 💭浙江 🕐2024-05-25 21:13:30
广东后生仔: Up主业务太简单,滥用stream的情况不适用于发大型业务[惊喜] 👍🏽0 💭广东 🕐2024-04-29 19:15:06
│ └── 郭靖: 那抱歉了,确实业务量不大。[爱] 👍🏽0 💭湖北 🕐2024-05-02 16:47:50
│ └── 二狗: 请不要动不动教育年薪百万的人。。。 企业比你更会选人用人 👍🏽0 💭北京 🕐2024-07-01 17:07:51
│ └── 广东后生仔: 便宜能快速迭代就行了[惊喜] 👍🏽0 💭广东 🕐2024-07-04 02:57:58
陈小哥: 其实很好奇, mysql不用join的话, 管理后台查主表的某个字段id对应的名称的时候怎么查,先搜出那个字段的所有id再使用in嘛,这样查的效率真的会比join高嘛? 👍🏽0 💭广东 🕐2024-04-29 14:32:06
星辰: 个人觉得博主,如果按照上百万老师设计可能会影响影响性能,利用缓存固定了老师的课程数据。但是这样会使流程复杂化,而且如果老师的业务出现变动,改动也是很苦恼的。如果通过数据库设计,根据每个高校租户不同就可以过滤掉很多了,何况还可以加上每个院系过滤。每个时间段识别有重复这个问题,完全可以通过数据库排序进行过滤。 👍🏽0 💭广东 🕐2024-03-25 17:08:39
│ └── 郭靖: 互联网公司基本都是百万千万级别的。[爱] 👍🏽0 💭北京 🕐2024-03-26 12:28:37
java李: 把《重构》,和设计模式相关的书籍多看两遍。就知道怎么写代码了。无关乎用不用流。 👍🏽0 💭辽宁 🕐2024-03-12 13:55:27
我爱一条柴: 同意 👍🏽0 💭浙江 🕐2024-02-21 09:36:58
Daimon: 用stream随便搞点业务处理,那单行代码长度大概有长城那么长! 👍🏽0 💭陕西 🕐2024-01-16 09:04:58
冷酸灵智取王世昌: 我没懂,为啥要写那么多函数呢?我只会在这个需求或者功能有扩展性或者共性才会提出来弄函数方法, 👍🏽0 💭北京 🕐2024-01-13 10:44:30
清泉: show code 一下 👍🏽0 💭北京 🕐2024-01-08 00:17:16
whatShouldIDo: 为什么不允许join啊 👍🏽0 💭四川 🕐2023-12-01 16:21:08
runfriends: 炫技的做法。我做过测试,相同的遍历,流的耗时是for(int i = 0; i< size; i++){…}的五倍左右。 👍🏽0 💭北京 🕐2023-11-28 11:23:13
C先生丶陳: 优秀的回答,必须赞一波 👍🏽0 💭北京 🕐2023-11-20 21:27:12
vnirs: 感觉稍微有点复杂,用户1万就顶不住了。说实话,不是代码就是数据库的问题。我做过一个系统4万用户,单表每日最高插入20万数据,嘎嘎流畅。服务器配置也很简单,64G的内存。4万用户并发其实并没有多高,平均100,高峰400。我把核心业务表只保留用户一周的数据,其它的备份到历史表中。这样高频使用的核心表速度永远不会慢,用到7天之前的历史数据时查的稍微慢点也无所谓,不影响客户整体体验。 👍🏽0 💭河北 🕐2023-09-22 10:16:10
│ └── 郭靖: 抱歉,千万用户,让您失望了。正常时段流量实时 QPS 6000 以上。原来双十一,618 还火的时候压力更高,动态加机器。4万用户太少了,不够看。我们一个刚毕业的实习生做一个弹窗的访问量要求都是上亿的。抱歉了。 👍🏽0 💭北京 🕐2023-09-22 14:50:38
│ └── vnirs: 抱歉,哪个大厂只有千万用户?弹窗访问量上亿?是一年上亿吗?每秒3个访问量也是够多的,够复杂的。不会是每秒上亿吧?哦用户才千万。 👍🏽0 💭河北 🕐2023-09-22 15:12:22
│ │ └── 郭靖: 腾讯,QQ。 👍🏽0 💭北京 🕐2023-09-22 15:55:07
│ └── 闪电芦苇: 大家讨论技术问题,有必要说话这么带情绪吗?再说了,你的描述从技术上看也不专业啊,4万用户是总用户还是日活用户?访问量要求都是上亿,是一秒的访问量吗? 👍🏽1 💭北京 🕐2023-10-09 11:30:26
│ └── 郭靖: 如果只是讨论技术,已经说的很明白了,QPS。 👍🏽0 💭北京 🕐2023-10-10 11:08:26
│ └── 闪电芦苇: 哈哈,那是我理解错了。[大笑] 👍🏽0 💭北京 🕐2023-10-10 11:26:06
李狗蛋: 个人理解下来就是空间换时间问题。但也要基于业务场景做好平衡点的。而不是一刀切。在不同场景下选择更优解才是正道 👍🏽0 💭上海 🕐2023-09-21 16:50:22
南极鲶鱼: 笑死,看你怎么调试 👍🏽0 💭山东 🕐2023-09-13 22:09:52
│ └── 程序猿: 一般做数据过滤,排序,提取数据,熟悉的一眼就看出问题,不需要提示。除非你使用stream做复杂的东西 👍🏽0 💭广东 🕐2024-05-05 15:29:25
huang minwei: 前两天看到有人讨论大量stream的写法难以维护,纯属装逼,今天反方辩手出场了,开始pk了 👍🏽0 💭四川 🕐2023-09-01 13:24:01
lalalaneo: 我领导看不懂我写的stream流,咋办? 👍🏽0 💭江苏 🕐2023-08-28 17:10:41
征龙: 关于不用JOIN 我有一个问题啊,加入有一个学生表存学生id名称性别等基本信息,有一个成绩表有学生ID和分数,现在前端有一个分页查询,查询男生成绩在90分以上的按照成绩和年龄降序排序,你不用join怎么做 👍🏽0 💭辽宁 🕐2023-08-09 10:21:05
│ └── Leopold: 面对这样的问题,希望作者能来回复一下。看看你的大厂思维怎么解决 @郭靖 👍🏽0 💭广东 🕐2023-10-04 17:40:59
│ └── 二狗: [图片] 👍🏽0 💭北京 🕐2024-07-01 17:51:34
│ └── 二狗: 首先学生数肯定不多,在这个场景下,复杂场景再考虑复杂场景的。 跟join区别也不大,只不过是把join 的工作再做一遍而已,但是可以在业务上控制到最小数据集。 👍🏽0 💭北京 🕐2024-07-01 17:53:49
xieeliandao: 这种方案成本最大的是缓存管理 小公司小项目哪有那么大的redis给你造 👍🏽0 💭四川 🕐2023-07-26 11:20:37
│ └── xieeliandao: 如果没有缓存层的话 多次查询的性能肯定是不如直接join的 👍🏽0 💭四川 🕐2023-07-26 11:22:05
newbility: 学到了,感谢分享 👍🏽0 💭北京 🕐2023-07-08 12:39:29
懒人爱吃鱼: 干活,感谢分享! 👍🏽0 💭四川 🕐2023-05-11 19:35:38
知乎用户VzpxFQ: 试试eclipse collection 👍🏽0 💭湖北 🕐2023-04-02 17:09:20
我只是在等你: 。。。 👍🏽0 💭四川 🕐2023-03-09 02:29:36
zhang: [捂脸]不让join什么鬼?简单的问题复杂化? 👍🏽0 💭河南 🕐2023-03-08 10:08:30
│ └── Sherhom: 这是后端开发的共识。为了避免高并发的慢查询阻塞其他的业务查询。 👍🏽0 💭广东 🕐2023-03-11 13:58:08
│ └── 丶吻如雪上霜: 倘若我们系统是jdk1.7+单体+jsp呢,阁下该如何应对 👍🏽0 💭上海 🕐2023-07-21 08:58:56
│ └── Sherhom: 登录人力系统——>离职 👍🏽1 💭广东 🕐2023-07-22 08:26:26
│ └── 丶吻如雪上霜: 没有用钉钉呢 👍🏽0 💭上海 🕐2023-07-24 19:20:15
│ └── Sherhom: 那可不是,更得跑路了 👍🏽0 💭广西 🕐2023-07-30 20:34:25
黎曼: 我就不推荐,不利于维护的代码不是好代码 👍🏽0 💭上海 🕐2022-11-08 11:29:09
zzz: 只说好处不讲缺点的吗[好奇] 👍🏽0 💭山东 🕐2022-11-06 05:23:48
只有云知道: 学到了,真正站得高,看得远[赞同][赞同][赞同] 👍🏽0 💭陕西 🕐2022-11-05 01:05:05
白菜不白: 小作坊玩不转啊 👍🏽0 💭四川 🕐2022-11-04 15:11:00
看山看水: 知音知音,同感同感 👍🏽0 💭广东 🕐2022-11-04 13:08:36
你在教我做事吗: 牛逼 👍🏽0 💭浙江 🕐2022-11-03 10:19:54
AuguryBot: 关于第一个例子,初学的时候上手就比这个复杂。但是是同类问题,直接搞类插入排序啊[惊喜][惊喜][惊喜] 👍🏽0 💭河南 🕐2022-11-03 01:00:47
晴天: 最近也遇到mysql join的数据性能问题、感觉很难把问题刨析开进行分析。关注一下作者这篇文章、有时间研读一下。另外好的框架 还是要程序员们执行起来、我们这三线小城市、程序员之间参差不齐、遇到这种问题真的很难统一思路、愁人 👍🏽0 💭山东 🕐2022-11-03 09:38:03
jinlei liu: 思路非常清晰,学习到了学习到了 👍🏽0 💭上海 🕐2022-11-02 12:09:46
妒与诚: 获益匪浅 👍🏽0 💭辽宁 🕐2022-11-02 17:06:40
野猪佩奇: 请教一下,看到您说很少用join,那假设a表存了b表的code,我查询时想看b表code对应的name字段,不join怎么操作呢?二次查询吗 👍🏽0 💭北京 🕐2022-11-03 08:27:07
│ └── solo-coding: 二次查询就行了,我所有项目都是这么干的 👍🏽1 💭广东 🕐2023-03-18 19:11:39
Patrick4ever: 先mark 大佬 👍🏽0 💭福建 🕐2022-11-02 19:28:55
hello: 这直接一个关注 👍🏽0 💭上海 🕐2022-11-01 16:45:40
YGGY: 上手和维护成本高哇 👍🏽0 💭北京 🕐2022-11-01 09:19:37
│ └── 郭靖: 单元测试覆盖率 99%。 👍🏽0 💭北京 🕐2022-11-02 10:09:07
快乐煎土豆: 学了三个月python了, 还不会写stream流[大哭] 👍🏽0 💭中国香港 🕐2022-11-01 11:10:10
我脑中的橡皮擦: 谢谢楼主!很有帮助。 👍🏽0 💭广东 🕐2022-10-31 18:53:31
wgyscsf: 流-响应式编程 👍🏽0 💭上海 🕐2022-11-01 09:35:57
泡泡: 刚毕业境界太低。。第一个矩阵没看懂 👍🏽0 💭上海 🕐2022-10-31 23:31:42
L1S3N: 好巧 我是因为拍照关注up 竟然因为本专业问题在知乎碰到[爱] 👍🏽0 💭广东 🕐2022-11-01 17:23:58
Alicandra: 催更… case study理解起来很快[赞] 👍🏽0 💭中国香港 🕐2022-10-31 15:28:26
理想三旬: 我也挺喜欢stream的,我也感觉每次用stream就有一种思路很清晰的感觉 👍🏽0 💭四川 🕐2022-11-01 21:04:22
kuolemax: 牛的,原来数学真的可以用于实际工作,给我打开了新世界[捂脸],我以前真的是只会想crud 👍🏽0 💭浙江 🕐2022-10-31 08:43:47
熊熊熊: 牛啊牛啊 👍🏽0 💭四川 🕐2022-10-31 14:24:21
今日乱侃: 不允许join,inner join也不行吗,那全都是单表查询了 👍🏽0 💭江苏 🕐2022-10-29 20:49:05
│ └── 别跟我谈理想: 大厂确实只要求单表查询,单体应用中一个关联查询就出来了,在微服务中会要求你单表查出id,再用id去另一样表查询,感觉已经成规范了[思考] 👍🏽0 💭陕西 🕐2022-11-01 10:21:46
│ └── 知乎用户NBm1O0: 不允许join是为了提高缓存效率和命中率,核心是应用基于缓存数据,对应用规模和开发人员有一点要求,应用规模不大上不上缓存无所谓,开发人员水平不行他会觉得join不是更方便吗,一条sql语句的事情搞那么复杂。规模不大和初级开发人员用mybatis就好了,缓存对性能提升不打,还增加大家的学习能力。自学还是可以的,你可以按照答主的案例做一下测试[机智] 👍🏽0 💭江西 🕐2022-11-01 11:09:02
│ └── 快丶放开那御姐: [捂脸]我试了下stream对我来说大部分情况下都不如for快 👍🏽0 💭江苏 🕐2022-11-01 17:02:20
│ │ └── d君c: 接口的整体性能一般不会出现在stream 上,最主要还是网络io ,还有数据库查询性能上 👍🏽0 💭四川 🕐2022-12-06 11:29:54
│ └── 快丶放开那御姐: 原来大佬们都是禁用join和子查询的。。。我用的可欢了 👍🏽0 💭江苏 🕐2022-11-01 17:02:56
│ └── 今日乱侃: 看了下答主更新的的内容,原来在答主用的框架中,所有通过id去查数据库的数据都在缓存中,我不知道有多少项目这样搞,在我们的项目中这是不可能的,大量业务依赖事务管理,缓存维护起来很麻烦 👍🏽1 💭江苏 🕐2022-11-01 18:01:28
│ └── 郭靖: 如果是银行系统的话,依赖事务是没问题的,这是所谓的【强一致性】和【最终一致性】,不同设计范式场景也不同。如果用好的数据库和好的机器,join 能完成这种场景的业务也是可以的,互联网大部分不追求强一致性,和钱相关的业务追求强一致性,是很正常的。设计方案不是一拍子打死的。 👍🏽6 💭北京 🕐2022-11-02 23:12:04
│ └── 前进的肥肥: 这个借鉴意义挺大的 👍🏽0 💭福建 🕐2022-11-13 07:06:59
啥花啥月: 自以为是 👍🏽0 💭天津 🕐2024-09-02 10:28:32
csgo: 一眼外行 👍🏽0 💭四川 🕐2024-05-10 17:59:54
小风: 你这种代码,自己觉得牛的不行,后续接手你代码的要更改业务,把你祖宗十八代估计都骂完了[大笑] 👍🏽0 💭湖北 🕐2024-04-22 16:52:57
│ └── 广东后生仔: 不要怎么忽悠初学者买他的课,固定套路了[惊喜] 👍🏽0 💭广东 🕐2024-04-29 19:15:40
│ └── Phytha: 所有人都用统一的代码风格不就行了?答主本身就在大公司工作,形成固定的规范很正常。我在阿里工作,我们这边风格和他基本一样。 👍🏽0 💭江西 🕐2024-05-03 11:04:52