【java】使用表达式处理数据 - Aviator

背景

工作中有涉及在内存中进行数据筛选计算,进行一番搜索后,我选择使用 AviatorScript 来实现,这里探索一下这个方案,并测试一下它的性能。

实现

准备数据

写个函数用于创建数据:

java 复制代码
private static List<Map<String, Object>> buildData(int cnt) {
    List<Map<String, Object>> base = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        Map<String, Object> item = new HashMap<String, Object>();
        for (int j = 0; j < 26; j++) {
            item.put((char) ('a' + j) + "", Math.random() * 100);
        }
        base.add(item);
    }
    if (cnt <= 100) {
        return base;
    }
    List<Map<String, Object>> res = new ArrayList<>(cnt);
    for (int i = 0; i < cnt / 100; i++) {
        res.addAll(base);
    }
    return res;
}

方式1:预编译后逐条执行

因为我们要处理的是大量的数据,简单的直接执行表达式自然是不可取的,我们直接略过,翻看文档时,我发现Aviator提供了预先编译表达式的方式(即:先编译表达式, 返回一个编译的结果, 然后传入不同的env来复用编译结果),如此一来能省去很多重复工作。

我们测试100w条数据,每个数据26个字段,表达式中用到了8个字段:

java 复制代码
public static void main(String[] args) {
    List<Map<String, Object>> list = buildData(100*10000);
    System.out.println(list.size());
    String expression = "a>10 && b < 80 && a - b > 10 || e>10 && f < 80 && g - h > 10";
    // 预编译方式
    Expression compiledExp = AviatorEvaluator.compile(expression);
    DurationUtil.duration("预编译方式", () -> {
        List<Map<String, Object>> res = list.stream()
                .filter(map -> compiledExp.execute(map).equals(true))
                .toList();
        System.out.println("筛选后大小:"+res.size());
    });

}

运行结果如下:

228ms,在这个数据量下,速度还算能够接受。

方式2:使用 seq 库一次计算

Aviator 还提供了类似java的Stream的能力,描述如下:

刚刚我们是在 Stream 的filter中调用 Aviator 的,但seq库提供的能力可以让我们一次性把整个list整个塞进去,将全部数据操作都给 Aviator 进行,这样少了外部到Aviator的交互,会不会更快呢?

我们添加代码:

java 复制代码
Map<String, Object> env = new HashMap<>();
env.put("list",list);
String seqExp = "filter(list, lambda(m) ->m.a > 10 && m.b < 80 && m.a - m.b > 10 || m.e>10 && m.f < 80 && m.g - m.h > 10 end)";
// seq 方式
Expression seqCompiledExp = AviatorEvaluator.compile(seqExp);
DurationUtil.duration("seq 方式", () -> {
    List<Map<String, Object>> res =(List<Map<String, Object>>) seqCompiledExp.execute(env);
    System.out.println("筛选后大小:"+res.size());
});

这里注意表达式要做一些修改:

python 复制代码
filter(list, lambda(m) ->m.a > 10 && m.b < 80 && m.a - m.b > 10 || m.e>10 && m.f < 80 && m.g - m.h > 10 end)

运行结果:

要同样数据集测试两个方式才有对比性,所以我将它们放在了一起,再跑一遍,从输入日志可以看出,这种方式耗时要多541ms,

内存消耗比较

我们使用 IDEA 的 Profiler 测试一下:

方式1使用了大概 53 MB,是原数据的2倍,还行。

而方式2,好家伙, 473MB! 真吓到我了!

后面我仔细一想,可能是因为原数据只有100条,后续数据都是引用,才造成这么大的区别,所以我将数据换成真正100w条实体对象,再次测试:

区别并不大,前者逐条处理,使用的内存自然要小很多,这也在情理之中。

其中有意思的是,这时测试的耗时要多了不少(跟Profiler无关,单独运行),这个我只能盲猜它内部有一些缓存机制,有大拿知道的话希望能解答一下:

完整代码

java 复制代码
public class AviatorTest {
    public static void main(String[] args) {
        List<Map<String, Object>> list = buildData(100*10000);
        System.out.println("数据大小:"+list.size());
        String expression = "a>10 && b < 80 && a - b > 10 || e>10 && f < 80 && g - h > 10";
        // 预编译方式
        Expression compiledExp = AviatorEvaluator.compile(expression);
        DurationUtil.duration("预编译方式", () -> {
            List<Map<String, Object>> res = list.stream()
                    .filter(map -> compiledExp.execute(map).equals(true))
                    .toList();
            System.out.println("筛选后大小:"+res.size());
        });
        System.out.println("==================================");
        Map<String, Object> env = new HashMap<>();
        env.put("list",list);
        String seqExp = "filter(list, lambda(m) ->m.a > 10 && m.b < 80 && m.a - m.b > 10 || m.e>10 && m.f < 80 && m.g - m.h > 10 end)";
        // seq 方式
        Expression seqCompiledExp = AviatorEvaluator.compile(seqExp);
        DurationUtil.duration("seq 方式", () -> {
            List<Map<String, Object>> res =(List<Map<String, Object>>) seqCompiledExp.execute(env);
            System.out.println("筛选后大小:"+res.size());
        });

    }

    private static List<Map<String, Object>> buildData(int cnt) {
        List<Map<String, Object>> base = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            Map<String, Object> item = new HashMap<String, Object>();
//            item.put("seq", i);
            for (int j = 0; j < 26; j++) {
                item.put((char) ('a' + j) + "", Math.random() * 100);
            }
            base.add(item);
        }
        if (cnt <= 10000) {
            return base;
        }
//        base.addAll(buildData(cnt-10000));
//        return base;
        List<Map<String, Object>> res = new ArrayList<>(cnt);
        for (int i = 0; i < cnt / 10000; i++) {
            res.addAll(base);
        }
        return res;
    }
}

结论

经过上面的验证,逐条去过滤数据似乎要比一次性注入所有数据快的多,个人猜测可能是大量数据传输和解析造成的。

因此,在100w行以内的数据,使用 Aviator 处理,再开线程做一些优化,其性能还是可以接受的,但还是有OOM风险,最好严格控制数据量;至于超过了100w,相信也不会有多少人选择在内存中做计算了吧?

参考

AviatorScript 文档 (yuque.com)

相关推荐
一勺菠萝丶9 分钟前
PDF24 转图片出现“中间横线”的根本原因与终极解决方案(DPI 原理详解)
java
姓蔡小朋友13 分钟前
Unsafe类
java
一只专注api接口开发的技术猿27 分钟前
如何处理淘宝 API 的请求限流与数据缓存策略
java·大数据·开发语言·数据库·spring
荒诞硬汉28 分钟前
对象数组.
java·数据结构
期待のcode29 分钟前
Java虚拟机的非堆内存
java·开发语言·jvm
黎雁·泠崖30 分钟前
Java入门篇之吃透基础语法(二):变量全解析(进制+数据类型+键盘录入)
java·开发语言·intellij-idea·intellij idea
仙俊红33 分钟前
LeetCode484周赛T4
java
计算机毕设指导61 小时前
基于微信小程序的丽江市旅游分享系统【源码文末联系】
java·spring boot·微信小程序·小程序·tomcat·maven·旅游
Mr -老鬼1 小时前
Rust 的优雅和其他语言的不同之处
java·开发语言·rust