V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
xzour
V2EX  ›  程序员

大佬求助,生成 6000 条数据居然花费 30 分钟?

  •  
  •   xzour · 2020-12-17 16:41:26 +08:00 · 3221 次点击
    这是一个创建于 1469 天前的主题,其中的信息可能已经有所发展或是发生改变。

    不知不觉,做开发已有 2 年,传统行业的 IT 开发,需求的大概是全能手,SAP 上了一期,对接了 SAP,了解了一些 ABAP 语法,写了几个接口,慢慢的对语言不在执着,对技术也不再执着,因为出身工厂,对工厂业务还算熟悉,对解决方案的思考还算比较热衷。所以就沉浸在计算的一块,但是技术算法认知的先天不足,所以想请大佬把把脉,以后的学习方向。比如以下一题:

    1. 一个财务的一个清账模拟功能
    2. 已知系统目前有几千个客户
    3. 计算周期为一月一次清账
    • 大概业务逻辑如下:
    1. 有 3 个数组,第一个数组是客户组,大概有几千条。第二个数组是各个收款明细,有几千条,不一定全部客户都有,第三个数组是应收发票,大概有几千条,不一定全部客户都有
    2. 需求是每个客户的收款与发票对碰核销,记录每笔核销的记录及每个客户的期末余额。

    目前这个模块我是通过暴力遍历实现的,但是速度太慢了,逻辑如下

    1. 遍历每个客户
    2. 读取该客户的收款及发票
    3. 遍历收款,取发票一条一条核销,一条销完,换另一张发票,未销完,记录发票 INDEX 及剩余金额
    4. 最后将结果批量插入数据库。 大概 6000 多条核销明细花了我 30 分钟+ 不可忍受。

    因为没到月末,基本收款及发票是不一定真实的,有可能会冲销掉。实时清账就有可能反清账,所以目前清账功能财务需求是预清账,点击就跑,期末定了再关账。

    第二个问题是关于动态计算的表达式引擎问题,2W 条数据动态计算,用了 C#的库,居然还要几分钟。 大概需求如下
    一个单据,有很多属性,然后不同的属性,会对应不同的成本及利润计算公式。所以我把它放在前端用动态表达式的方式配置,而不是代码写 IF (因为情况大概目前有 70+种,再加以后的新的营销活动啥的,财务提一个需求就要改一次代码太麻烦,所以找了一些动态表达式的库,通过前端配置触发条件) for example.

    1. 某个人(参与 A 活动),提成是,毛利提成》销售额提成,则毛利提成,反之销售额提成
    2. 某个人是负责 A 区域的,结果此张单成交区域在区域外,提成需打折。
    3. 某个活动提成不计成本,直接按某个比例算提成。等等。

    1.目前逻辑是,遍历 2W 条,先匹配布尔条件生效判定(可能不止 1 个布尔条件),但终归 70+种情况组组合,最后得到的算法,然后计算提成。

    因为用了 C#的表达式引擎,Flee ( Fast Lightweight Expression Evaluator ),目前用时 3~5 分钟,但是这 5 分钟不太适合体感使用,因为在结算的时候,表达式会调整的,然后重算验证就等的不耐烦了。

    题外话:公司的技术大佬是中小方案很熟练,BS/CS 端直接上手,打印报表配置,运维都可以,中小企业不可少的人才,但是技术深度不行,喜欢代码一把梭,项目 MVC 还是我来了之后分的,以前直接控制器+静态类打天下,我想了一下我又不想成为绑定公司的人才,所以对多语言的学习比较抗拒,对技术逻辑比较感兴趣,对技术遇到的难题,也喜欢思考,但是非科班,所以很多时候都是暴力算法,然后因公司上市,业务发展的复杂度也越来越高,传统行业对 IT 技术人才是不太尊重,感觉目前的代码不重构,会因为复杂度,迟早黑盒子一样的存在.....

    28 条回复    2020-12-18 20:59:11 +08:00
    Easzz
        1
    Easzz  
       2020-12-17 16:51:26 +08:00
    可以每天 /月生成历史数据,出报表直接统计天 /月的数据即可,不用查询明细。
    linksNoFound
        2
    linksNoFound  
       2020-12-17 16:57:38 +08:00
    你看看 io 和 cpu 哪个跑满了,都还空着就试试多进程吧,比改代码判断逻辑简单
    MakeItGreat
        3
    MakeItGreat  
       2020-12-17 17:00:20 +08:00 via Android
    我好像觉得 Excel 更简单
    xzour
        4
    xzour  
    OP
       2020-12-17 17:13:11 +08:00
    @MakeItGreat 就是因为财务随着业务复杂度越来越高,汇总数据太麻烦,开始考虑上系统了呀。
    whosesmile
        5
    whosesmile  
       2020-12-17 17:39:00 +08:00
    我做前端的,看了第一个题目,瞎说下:
    遍历每个客户感觉没必要,因为每个你无非是要找当前客户的的收款明细,倒不如直接一次性检索出所有的收款明细按用户 ID 分组,同样检索所有发票也按用户 ID 分组,然后这两个组都按用户 ID 排序,然后剩下的是算法层次的事情,找两个集合的对对碰。

    前提是你的数据量不大,你的内存足够装下这么多数据,这里的优化是不需要每个 for 迭代都去查询 DB,全部换成内存操作了。
    Bazingal
        6
    Bazingal  
       2020-12-17 17:39:59 +08:00 via Android
    数组转字典,另外是不是可以遍历发票找对应的收款和客户
    whileFalse
        7
    whileFalse  
       2020-12-17 17:40:01 +08:00
    我估计你套了双层 /三层循环。请善用字典。
    whosesmile
        8
    whosesmile  
       2020-12-17 17:41:15 +08:00
    因为我理解你是每个月对账,所以这个用户的数据不是全库的,而是按时间维度收窄的,你的内存应该装得下的。
    Bazingal
        9
    Bazingal  
       2020-12-17 17:43:55 +08:00 via Android
    计算部分根据实际情况使用异步或者并行
    whileFalse
        10
    whileFalse  
       2020-12-17 17:44:38 +08:00
    另外,对“技术深度不行表现在没有 MVC”不能苟同。
    xzour
        11
    xzour  
    OP
       2020-12-17 18:22:34 +08:00
    @whileFalse 一个微型的 ERP 没有 MVC 分层思想不严重吗?
    xzour
        12
    xzour  
    OP
       2020-12-17 18:24:44 +08:00
    @whileFalse 具体表现为控制器方法都是为 3~1000 行的代码,db,业务逻辑耦合一起。
    xzour
        13
    xzour  
    OP
       2020-12-17 18:31:11 +08:00
    @whileFalse 大佬举个数组转字典的优化例子,不太懂怎么用。
    xzour
        14
    xzour  
    OP
       2020-12-17 18:31:28 +08:00
    @Bazingal 大佬举个数组转字典的优化例子,不太懂怎么用。
    xzour
        15
    xzour  
    OP
       2020-12-17 18:33:26 +08:00
    @whosesmile 目前是一次取 DB,客户,收款,发票,然后是内存检索客户对应的收款发票对冲的。 你的思路是个好思路客户 ID 是可以排序的。谢谢
    laminux29
        16
    laminux29  
       2020-12-18 01:16:48 +08:00
    1.C/Cpp 以及传统主流数据库,对于 1000 * ( 1000 + 1000 )规模的两层 for 循环或游标遍历,甚至 1000 * 1000 * 1000 规模的 3 层循环或游标遍历,根本毫无压力。

    如果有瓶颈,需要分析瓶颈在哪。

    比如常见的错误实践,在编程语言中这样写:

    for
    ----for
    ----.----for
    ----.----.----string SQL = "SELECT *....";
    ----.----.----db.search(SQL);

    那么主要的瓶颈就在 [db.search(SQL);] 这条语句这里,具体一点就是编程语言的数据库客户端驱动,与数据库的接口调用这里。这种问题需要把程序逻辑全部移动到数据库的存储过程里,然后编程语言这边对数据库进行一次调用,瓶颈就解决了。


    2.就算不改这些东西,总体时间也只是 30 分钟的话,可以考虑增加 N 台服务器,把数据分为 N 组,每台跑一组,这样总体时间可以缩减到 30 分钟 ÷ N + 汇总时间。
    xzour
        17
    xzour  
    OP
       2020-12-18 09:09:23 +08:00
    @laminux29 为什么要用存储过程这种方式?而不是在遍历之前把所有数据先查询好?是考虑内存限制的问题吗?在多数据源的情况下是不是不好处理了
    l00t
        18
    l00t  
       2020-12-18 09:22:11 +08:00
    @xzour #17 他是怀疑你每次都只读一条,这样大量时间耗费在数据库交互上。如果你一次全部读出来,那自然是无所谓,这个位置也不会是 db.search
    no1xsyzy
        19
    no1xsyzy  
       2020-12-18 09:52:39 +08:00
    @laminux29 6000*6000*6000 按 NOIP 估法,遍历总量除以 1e8 为秒数,大约 36 分钟
    O(n^3) 增长挺快的。
    no1xsyzy
        20
    no1xsyzy  
       2020-12-18 10:02:04 +08:00
    第二个问题可以用局部表达式结果缓存来优化
    动态修改的体感上的话,也可以通过每个被计算的对象依次计算,有值就推给前端来实现至少前几个能够立即响应(简单的 UX 设计)
    甚至前端做动态滚动显示,滚动到的范围才会被求值(需要前端能力)
    laminux29
        21
    laminux29  
       2020-12-18 10:12:08 +08:00
    @xzour 我的意思是,只要不在程序里 for 循环然后一条一条去查就行了。存储过程是我建议的方式,你用其他方式也行。总之瓶颈不要被限制在数据库客户端驱动的 rpc call 就行。
    jj783850915
        22
    jj783850915  
       2020-12-18 12:26:42 +08:00 via Android
    善用表驱动 避免非必要的查找
    tlday
        23
    tlday  
       2020-12-18 12:54:51 +08:00
    第一个问题感觉本质上是数据库结构设计有问题,依我对财务极其粗浅的理解,收款和发票理想不该是一对一,最次不该是多对多的关系么。

    第二个问题我建议你还是从根本上解决,从后端实现,不知道为什么你觉得改前端代码就不算改代码了...而且前端传表达式这种东西,漏洞,bug 会非常多。我不是很懂计算提成这种东西敢放在前端计算是什么操作。即便只是 bool 条件,也是很容易存在很大漏洞的。更别提如果后面上了权限管理,你可能需要 hack 表达式解析引擎了。我查了一下你说的这个 Flee,298 个 star,上一次提交是二月份合并了两个 pr,再上一次提交已经是去年三月份了,这种不仅用的人少而且 maintainer 不活跃的库真的不建议用在生产环境。
    xzour
        24
    xzour  
    OP
       2020-12-18 13:51:33 +08:00
    @tlday 第一个问题:公司的业务收款发票不是一对一,但是给业务员的提成是实时回款结算,回多少结算多少,并且根据产品性质,营销活动,所在区域,下单日期等等落实到具体单据综合结算的。比如一个客户分别在一个月内买了 N 个商品(不同属性),前期预付款 30%,2 个月后付款 70%,这还算比较简单的。你也可以理解为 2W 多条数据,目前有 70 种提成条件这样子。
    所以收款明细与发票,最多能保持总额账的话是相等的。但不代表每期是相等的。收款不是一笔收完的。可以理解为多对多关系。
    第二条,是因为 C#的解决方案好像是没有 JAVA 解决方案多还是,个人能力有限,不能很好的找到替代方案,之前的公司方案是用 sql 来计算 BOOL 和数值的。公式解放给财务配置,我想这是未来财务自动化一个很大的方向。我是想参考 OA 自动工作流中如何自定义审批流来实现的。(云之家,泛微、企业微信的一些审批第三方件都支持),即拖拉思维导图,写上触发条件,即走哪一条线,(当然这次一条数据),比我 2W 条数据会少很多。体感也不错。
    至于 eval 的问题,当然弊端多多,但是既然商业上有这种产品,说明他们是有克服前端传过去执行所产生的的问题。就比如这个 Flee,它分三步,会事先限定每个变量的类型,再渲染公式计算树,最后才计算 bool 或者值,也可以传入自定义的静态方法,反射调用?安全性比我用 sql,用 eval(javascript )安全多了,当然,表达式引擎 JAVA 好像比较多的解决方案,如果有人有接触过这种动态表达式的开发,不胜感激。
    tlday
        25
    tlday  
       2020-12-18 20:13:49 +08:00
    @xzour 如果我是你,就实时核销,他们要反清帐,对应核销记录就置 revoked 。怎么实时核销,想写了就每次创建收款记录 /发票记录时自己算,不想写了让财务人员自己选,自由还给他们,做错了是他们的问题,你的责任是保证系统不出错,逻辑没问题,尽量提示他们的低级疏忽,尽量简化他们的操作,但没有义务承担他们所有操作的责任。

    如果要看客户期末余额,就收款 /发票两个表 group by user_id,sum 一下相减。
    如果要看发票剩余金额,就发票 /核销两个表 filter by revoked, group by fapiao_id,sum 一下 filter 出值不为 0 的。



    如果不想大改系统,就用 whosesmile 的办法。你要做的实际上是创建这样一个结构:
    [
    { 用户 id: xxx, 收款记录: [],发票记录: [] },
    { 用户 id: xxx, 收款记录: [],发票记录: [] }....
    ]
    然后遍历整个数组,每个项里面的两个数组采用类似“合并两个有序数组”的方法对对碰。
    xzour
        26
    xzour  
    OP
       2020-12-18 20:34:00 +08:00
    @tlday 大概理解了 3N 遍历与 N^3 遍历的代码区别了,十分感谢!我应该是为了省事,没有重新组装数据结构而导致的,手动核销他们是不愿意的,因为核销逻辑大体思路上就是先进先销,都是重复工作,需要解放劳动力的,未来财务更多是检查,现在这种核销,这只是一种规则,未来有可能只是一种配置,据我了解,这也是一种财务自动化的一个具体实施点,类似 SAP 的自动核销,只是 SAP 目前的核销没有细节到发票构成,不满足我司的使用习惯。


    楼上一些回复转字典,HashTable 的我倒现在还不太懂。很抱歉会产生 X-Y 的问题。
    tlday
        27
    tlday  
       2020-12-18 20:48:05 +08:00
    @xzour 他们的意思是类似这种结构
    收款记录:HashMap {
    xxx: [],
    yyy: []...
    }
    发票记录:HashMap {
    xxx: [].
    yyy: []...
    }
    然后遍历 HashMap 两边同 key (用户 id )的数组进行对对碰。


    也可以
    HashMap{
    xxx: Map{ 收款记录: [], 发票记录: [] ... }
    yyy: Map{ 收款记录: [], 发票记录: [] ... } ...
    }



    上面三种结构效果几乎都是等同的,时间复杂度的数量级上不会有什么差异.
    xzour
        28
    xzour  
    OP
       2020-12-18 20:59:11 +08:00
    @tlday 这个我就理解清晰多了,我以为是什么 HashMap<key,value> 能把 value 的这个<T>实体的某些字段做成类似索引的效果,达成 O(1),举的例子又是 int,这就超出我对它的认知了。原来是唯一 key 做文章。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5688 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 06:36 · PVG 14:36 · LAX 22:36 · JFK 01:36
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.