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

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

  •  
  •   coderstory ·
    coderstory · 64 天前 · 4274 次点击
    这是一个创建于 64 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    
    [email protected]:/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 条附言  ·  64 天前
    业务需求时,给定一张表的名字,查询这张表的外键表信息,如果这张外键表还存在外键,则继续查询外键表。。。
    把这个关联到的所有的表查询出来。


    目前找到了 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  
       64 天前
    for 加 stream 看着有点吓人
    DonaldY
        2
    DonaldY  
       64 天前
    估计是 OOM ,跟 Java8 stream 没啥关系。

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

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

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