V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
coderstory
V2EX  ›  Java

Java 8 的 stream 常规操作导致线程卡死

  •  
  •   coderstory ·
    coderstory · 2022-08-01 17:06:01 +08:00 · 5421 次点击
    这是一个创建于 581 天前的主题,其中的信息可能已经有所发展或是发生改变。

    java 8 的 stream 操作导致 线程卡死

    先贴一段堆栈打印 0x46 这个线程一直无法完成任务

    
    root@data-f7b697db9-hq9lf:/app# jstack 8 | grep -A20 0x46
    "http-nio-8200-exec-1" #60 daemon prio=5 os_prio=0 cpu=573228.20ms elapsed=783.76s tid=0x00007f8751e8d800 nid=0x46 runnable  [0x00007f871eaf1000]
       java.lang.Thread.State: RUNNABLE
            at java.util.stream.ReferencePipeline$2$1.accept([email protected]/ReferencePipeline.java:176)
            at java.util.ArrayList$ArrayListSpliterator.forEachRemaining([email protected]/ArrayList.java:1655)
            at java.util.stream.AbstractPipeline.copyInto([email protected]/AbstractPipeline.java:484)
            at java.util.stream.AbstractPipeline.wrapAndCopyInto([email protected]/AbstractPipeline.java:474)
            at java.util.stream.ReduceOps$ReduceOp.evaluateSequential([email protected]/ReduceOps.java:913)
            at java.util.stream.AbstractPipeline.evaluate([email protected]/AbstractPipeline.java:234)
            at java.util.stream.ReferencePipeline.collect([email protected]/ReferencePipeline.java:578)
            at cn.bobmao.pro.data.repository.externalDataSourceHelper.DataBaseRepositoryImplCommon.getForeignKeyTable(DataBaseRepositoryImplCommon.java:32)
            at cn.bobmao.pro.data.repository.externalDataSourceHelper.DataBaseRepositoryImplCommon.getForeignKeyTable(DataBaseRepositoryImplCommon.java:40)
            at cn.bobmao.pro.data.repository.externalDataSourceHelper.DataBaseRepositoryImplCommon.getForeignKeyTable(DataBaseRepositoryImplCommon.java:40)
            at cn.bobmao.pro.data.repository.externalDataSourceHelper.DataBaseRepositoryImpl.getTableNames(DataBaseRepositoryImpl.java:66)
            at cn.bobmao.pro.data.repository.externalDataSourceHelper.ExternalDataSourceExecutor.getTableNames(ExternalDataSourceExecutor.java:57)
            at cn.bobmao.pro.data.service.ExternalDataSourceService.updateTable(ExternalDataSourceService.java:215)
            at cn.bobmao.pro.data.controller.ExternalDataSourceController.getTableInfo(ExternalDataSourceController.java:50)
            at cn.bobmao.pro.data.controller.ExternalDataSourceController$$FastClassBySpringCGLIB$$f577fbc0.invoke(<generated>)
            at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
            at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
            at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
            at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    

    pid 为 70 的线程( 16 进制就是 0x46 )为异常线程

        PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                                                                                                              
         12 root      20   0 7793308   1.9g  24564 S  61.3   6.0   2:03.58 G1 Conc#0                                                                                                                                                            
         70 root      20   0 7793308   1.9g  24564 S  16.3   6.0  10:17.51 http-nio-8200-e                                                                                                                                                      
         29 root      20   0 7793308   1.9g  24564 S   5.0   6.0   0:06.73 GC Thread#1                                                                                                                                                          
         10 root      20   0 7793308   1.9g  24564 S   4.7   6.0   0:06.69 GC Thread#0                                                                                                                                                          
         15 root      20   0 7793308   1.9g  24564 S   0.7   6.0   0:01.93 VM Thread  
    

    对应 java 代码

        @Override
       public List<String> getForeignKeyTable(List<String> tableNames, DataSourceEntity info, List<ForeignKeyInfo> foreignKeyInfos) {
           List<ColumnInfo> result = new ArrayList<>();
           List<String> allTableNames = new ArrayList<>(tableNames);
           for (String tableName : tableNames) {
               result.addAll(findAllTable(info.getDataBaseName(), tableName));
           }
           result = result.stream().filter(column -> Arrays.stream(DB_KEYWORD).noneMatch(it -> column.getColumnName().equalsIgnoreCase(it))).collect(Collectors.toList());
           Map<String, List<ColumnInfo>> tables = result.stream().collect(Collectors.groupingBy(ColumnInfo::getTableName));
           List<String> childTables = new ArrayList<>();
           for (String name : tables.keySet()) {
               List<ColumnInfo> columnInfos = tables.get(name);
               for (ColumnInfo columnInfo : columnInfos) {
                   List<ForeignKeyInfo> collect = foreignKeyInfos.stream().filter(it -> it.getSourceTableName().equals(name) && it.getSourceColumnName().equals(columnInfo.getColumnName())).collect(Collectors.toList()); // 32 行
                   if (!CollectionUtils.isEmpty(collect)) {
                       //获取子表的表名
                       childTables.addAll(collect.stream().map(ForeignKeyInfo::getTargetTableName).collect(Collectors.toList()));
                   }
               }
           }
           if (!CollectionUtils.isEmpty(childTables)) {
               allTableNames.addAll(getForeignKeyTable(childTables, info, foreignKeyInfos)); // 40 行
           }
           return allTableNames;
       }
    
    

    一脸疑问,不清楚怎么排查。。。等一会儿容器就被 k8s 杀死重启了。

    第 1 条附言  ·  2022-08-01 18:48:04 +08:00
    业务需求时,给定一张表的名字,查询这张表的外键表信息,如果这张外键表还存在外键,则继续查询外键表。。。
    把这个关联到的所有的表查询出来。


    目前找到了 2 个文件
    1.入参 foreignKeyInfos 居然有 2W 个对象 原因是 mysql 查询外键的 sql 有问题 导致大量重复数据 他把整个库的外键全查询出来了

    2.部分表 存在多个指向相同表的外键 比如 A 表有 2 个字段外键指向了 B 表
    导致 allTableNames 变量内塞了 N 个 B 表的名字
    已经处理过外键关系的表不需要再次处理。

    PS:不是我写的代码 自测从普通的程序员升值到组长后,我就不知道如何评价别人的代码了。。我把代码修完,给他看代码查询。他还是没看懂怎么回事。
    41 条回复    2022-08-15 15:05:31 +08:00
    zhangleshiye
        1
    zhangleshiye  
       2022-08-01 17:15:32 +08:00
    for 加 stream 看着有点吓人
    DonaldY
        2
    DonaldY  
       2022-08-01 17:25:12 +08:00
    估计是 OOM ,跟 Java8 stream 没啥关系。

    这个在递归调诶。
    allTableNames.addAll(getForeignKeyTable(childTables, info, foreignKeyInfos));

    可以去看下 gc.log 或者 日志中是否有 OutOfMemoryError
    Bootis
        3
    Bootis  
       2022-08-01 17:34:01 +08:00
    childTables 逻辑有问题,你可以加一个日志打印,应该第二次以后的每次递归调用的入参 tableNames 都是一样的
    MarkP
        4
    MarkP  
       2022-08-01 17:35:11 +08:00
    你这个递归都没出口。。。
    MarkP
        5
    MarkP  
       2022-08-01 17:36:34 +08:00
    看错了,有出口,但我怀疑就是这个递归的问题
    hhjswf
        6
    hhjswf  
       2022-08-01 18:16:16 +08:00
    恐怖。。这么多遍历,肉眼看上去起码有三层,再递归一下,这算法复杂度得是什么规模啊
    coderstory
        7
    coderstory  
    OP
       2022-08-01 18:38:36 +08:00
    @MarkP childTables 是空的 就返回了
    coderstory
        8
    coderstory  
    OP
       2022-08-01 18:40:46 +08:00
    面向业务编程的结果 按代码一行行看很容易 就是查询一张表的外键以及外键表的外键表。。。整个外键引用链表全查出来 先循环表 在循环列 然后 表的列查询是否有外键
    guxingke
        9
    guxingke  
       2022-08-01 18:52:10 +08:00
    递归了就有问题吧

    a -> b -> c -> ... -> b -> ...
    DT37
        10
    DT37  
       2022-08-01 21:14:48 +08:00
    我看 Stream 就头疼,用的太多就很懵逼
    Hug125
        11
    Hug125  
       2022-08-01 21:22:09 +08:00 via iPhone   ❤️ 1
    stream 用不明白的话建议先把 stream 换成 for debug 明白了再换回 stream 回归测试。
    stream 和 for 混着来建议统一换成 stream
    流在处理大批量的数据还是有性能优势的
    Leviathann
        12
    Leviathann  
       2022-08-01 21:46:48 +08:00   ❤️ 3
    评价一下:
    为什么 columnInfo 的 list 要叫 result ?
    为什么 result 要用 new list + foreach addAll 的方法初始化,然后又用 stream 过滤?
    为什么过滤以后的 result 又直接赋值给 result ?
    为什么复杂的 filter 不抽成函数?
    为什么不用 map.entrySet().stream 遍历而是写得这么麻烦?
    为什么要 foreignKeyInfos 过滤以后要 collect 再判空再 add 到 childTables 而不是直接 forEach 里 add?
    为什么 foreignKeyInfos 过滤后的名字叫 collect ?
    为什么不是遍历 foreignkeyinfos 而是遍历用来过滤的中间变量 tables ?

    说实话代码这样我一般都懒得看具体逻辑
    iosyyy
        13
    iosyyy  
       2022-08-01 21:57:28 +08:00
    这个应该是拷贝 list 的时候太大导致卡住了 要不用个 map? 这过滤写的..不忍直视
    oneisall8955
        14
    oneisall8955  
       2022-08-01 22:15:19 +08:00 via Android
    这和 stream 没关系,改成 for 递归也一样
    oneisall8955
        15
    oneisall8955  
       2022-08-01 22:16:24 +08:00 via Android
    @oneisall8955 口误,for 迭代变量
    oneisall8955
        16
    oneisall8955  
       2022-08-01 22:16:34 +08:00 via Android
    @oneisall8955 遍历。。。
    ChicC
        17
    ChicC  
       2022-08-01 23:23:48 +08:00
    没注释,已经理不清了
    dqzcwxb
        18
    dqzcwxb  
       2022-08-02 00:57:46 +08:00
    换成 for 一样卡死,跟 stream 没有关系
    Vegetable
        19
    Vegetable  
       2022-08-02 01:14:39 +08:00
    这写法麻了,这种复杂度还敢用 stream ?真的绝了
    TWorldIsNButThis
        20
    TWorldIsNButThis  
       2022-08-02 02:28:41 +08:00 via iPhone
    @Vegetable 他想干的事情根本不复杂,是瞎 jb 写的代码导致看起来复杂
    TWorldIsNButThis
        21
    TWorldIsNButThis  
       2022-08-02 02:32:43 +08:00 via iPhone
    @Vegetable 而且这里的所谓 stream 全 tm 是单步操作然后就 collect ,看得出来这人根本就不怎么会,完全是把 stream 当成 collectionutil.filter 在用
    Aloento
        22
    Aloento  
       2022-08-02 02:33:59 +08:00
    好恐怖呀哈哈哈
    chengchen
        23
    chengchen  
       2022-08-02 03:38:51 +08:00 via iPhone
    这不就是二叉树层序遍历的变形题吗,leetcode 的 easy 难度
    MoYi123
        24
    MoYi123  
       2022-08-02 10:13:56 +08:00
    看起来像是数据里有环.
    Belmode
        25
    Belmode  
       2022-08-02 10:54:52 +08:00
    数据库里存在表外键循环依赖了,导致内存居高不下,一直 GC
    dorr
        26
    dorr  
       2022-08-02 11:01:35 +08:00
    @chengchen 这个是图的遍历吧,一个表有多个字段外键指向另一个表,这个路径可以看做同一条
    zmal
        27
    zmal  
       2022-08-02 11:21:33 +08:00
    线程卡死本身和 stream 没啥关系。
    但这个代码写的实在是太辣了。stream 不是让这么用的。
    lmshl
        28
    lmshl  
       2022-08-02 11:32:05 +08:00   ❤️ 4
    先帮你等价替换一版,Stream API 其实写起来很漂亮的,只要改换一下思路就好了。
    lmshl
        29
    lmshl  
       2022-08-02 11:36:18 +08:00
    pocketz
        30
    pocketz  
       2022-08-02 14:41:36 +08:00
    @lmshl 虽然但是,即使不看代码,这个配色也挺好看,能分享一下吗
    lmshl
        31
    lmshl  
       2022-08-02 17:53:10 +08:00   ❤️ 1
    @pocketz Idea New UI ,配色是 New Dark ,字体是 Fire Code
    bigfei
        32
    bigfei  
       2022-08-02 18:15:08 +08:00
    MYSQL 有元数据表的呀。。直接用 CTP 查询元数据表就可以了
    bigfei
        33
    bigfei  
       2022-08-02 18:17:08 +08:00
    bigfei
        34
    bigfei  
       2022-08-02 18:21:27 +08:00
    lmshl
        35
    lmshl  
       2022-08-02 18:49:20 +08:00   ❤️ 2
    梳理了一遍依赖以后发现中间没必要 groupingBy ,代码可以再缩减到这程度。如果想再精简的话就得结合业务功能分析了,我估计结合业务还能砍掉 3-5 行,如果换成 Scala 大概 5-10 行就写完了。
    lmshl
        36
    lmshl  
       2022-08-02 18:58:22 +08:00   ❤️ 2
    还能接着缩,逻辑依然等价
    nbndco
        37
    nbndco  
       2022-08-02 19:00:08 +08:00 via iPad
    每当这个时候我就特别能理解为什么说千万不要用新特性,没事不要修改不要更新不要升级了。这水平要是写 for 可能这代码还跑的快一点,至少不用 collect 这么多次。可读性本来就没有,所以也无所谓了。
    chengchen
        38
    chengchen  
       2022-08-02 19:54:58 +08:00 via iPhone
    @dorr 层序遍历不就用到了图的广度优先搜索么
    chrisia
        39
    chrisia  
       2022-08-03 12:34:31 +08:00
    @lmshl 优雅 😀
    golangLover
        40
    golangLover  
       2022-08-13 01:02:48 +08:00
    @lmshl new dark? 找不到啊
    ozipin
        41
    ozipin  
       2022-08-15 15:05:31 +08:00
    是不是多表之间的外键形成了环状结构然后有没有加以检测
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1258 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 17:44 · PVG 01:44 · LAX 09:44 · JFK 12:44
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.