2023年4月

RocketMQ 是大家耳熟能详的消息队列,开源项目 rocketmq-spring 可以帮助开发者在 Spring Boot 项目中快速整合 RocketMQ。

这篇文章会介绍
Spring Boot 项目使用 rocketmq-spring SDK 实现消息收发的操作流程
,同时笔者会
从开发者的角度解读 SDK 的设计逻辑

1 SDK 简介


项目地址:

https://github.com/apache/rocketmq-spring

rocketmq-spring 的本质是一个 Spring Boot starter

Spring Boot 基于“
约定大于配置
”(Convention over configuration)这一理念来快速地开发、测试、运行和部署 Spring 应用,并能通过简单地与各种启动器(如 spring-boot-web-starter)结合,让应用直接以命令行的方式运行,不需再部署到独立容器中。

Spring Boot starter 构造的启动器使用起来非常方便,开发者只需要在 pom.xml
引入 starter 的依赖
定义,在配置文件中
编写约定的配置
即可。

下面我们看下 rocketmq-spring-boot-starter 的配置:

1、引入依赖

<dependency>
  <groupId>org.apache.rocketmq</groupId>
  <artifactId>rocketmq-spring-boot-starter</artifactId>
  <version>2.2.3</version>
</dependency>

2、约定配置

接下来,我们分别按照生产者和消费者的顺序,详细的讲解消息收发的操作过程。

2 生产者

首先我们添加依赖后,进行如下三个步骤:

1、配置文件中配置如下

rocketmq:
  name-server: 127.0.0.1:9876
  producer:
      group: platform-sms-server-group
    # access-key: myaccesskey
    # secret-key: mysecretkey
  topic: sms-common-topic

生产者配置非常简单,主要配置
名字服务地址

生产者组

2、
需要发送消息的类中注入 RcoketMQTemplate

@Autowired
private RocketMQTemplate rocketMQTemplate;

@Value("${rocketmq.topic}")
private String smsTopic;

3、
发送消息,消息体可以是自定义对象,也可以是 Message 对象

rocketMQTemplate 类包含多钟发送消息的方法:

  1. 同步发送 syncSend
  2. 异步发送 asyncSend
  3. 顺序发送 syncSendOrderly
  4. oneway发送 sendOneWay

下面的代码展示如何同步发送消息。

String destination = StringUtils.isBlank(tags) ? topic : topic + ":" + tags;
SendResult sendResult =
         rocketMQTemplate.syncSend(
            destination, 
            MessageBuilder.withPayload(messageContent).
            setHeader(MessageConst.PROPERTY_KEYS, uniqueId).
            build()
          );
if (sendResult != null) {
    if (sendResult.getSendStatus() == SendStatus.SEND_OK) {
       // send message success ,do something 
    }
}

syncSend 方法的第一个参数是发送的目标,格式是:
topic + ":" + tags

第二个参数是:
spring-message 规范
的 message 对象 ,而 MessageBuilder 是一个工具类,
方法链式调用
创建消息对象。

3 消费者

1、配置文件中配置如下

rocketmq:
  name-server: 127.0.0.1:9876
  consumer1:
    group: platform-sms-worker-common-group
    topic: sms-common-topic

2、实现消息监听器

@Component
@RocketMQMessageListener(
    consumerGroup = "${rocketmq.consumer1.group}",  //消费组
    topic = "${rocketmq.consumer1.topic}"  					//主题
)
public class SmsMessageCommonConsumer implements RocketMQListener<String> {
    public void onMessage(String message) {
       System.out.println("普通短信:" + message);
    }
}

消费者实现类也可以实现 RocketMQListener<MessageExt>, 在 onMessage 方法里
通过 RocketMQ 原生消息对象 MessageExt 获取更详细的消息数据

public void onMessage(MessageExt message) {
    try {
        String body = new String(message.getBody(), "UTF-8");
        logger.info("普通短信:" + message);
    } catch (Exception e) {
        logger.error("common onMessage error:", e);
    }
}

4 源码概览

最新源码中,我们可以看到源码中包含四个模块:

1、rocketmq-spring-boot-parent

该模块是父模块,定义项目所有依赖的 jar 包。

2、rocketmq-spring-boot

核心模块,实现了 starter 的核心逻辑。

3、rocketmq-spring-boot-starter

SDK 模块,简单封装,外部项目引用。

4、rocketmq-spring-boot-samples

示例代码模块。这个模块非常重要,当用户使用 SDK 时,可以参考示例快速开发。

5 starter 实现

我们重点分析下 rocketmq-spring-boot 模块的核心源码:


spring-boot-starter 实现需要包含如下三个部分:

1、定义 Spring 自身的依赖包和 RocketMQ 的依赖包
;

2、定义spring.factories 文件

在 resources 包下创建 META-INF 目录后,新建 spring.factories 文件,并在文件中
定义自动加载类
,文件内容是:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration

spring boot 会根据文件中配置的自动化配置类来自动初始化相关的 Bean、Component 或 Service。

3、实现自动加载类

在 RocketMQAutoConfiguration 类的具体实现中,我们重点分析下生产者和消费者是如何分别启动的。

▍生产者发送模板类:RocketMQTemplate

RocketMQAutoConfiguration 类定义了两个默认的 Bean :

首先SpringBoot项目中配置文件中的配置值会根据属性条件绑定到 RocketMQProperties 对象 中,然后使用 RocketMQ 的原生 API 分别创建生产者 Bean 和拉取消费者 Bean , 分别将两个 bean 设置到 RocketMQTemplate 对象中。

两个重点需要强调:

  • 发送消息时,将 spring-message 规范下的消息对象封装成 RocketMQ 消息对象

  • 默认拉取消费者 litePullConsumer 。拉取消费者一般用于大数据批量处理场景 。

    原生使用方式

​ RocketMQTemplate 类封装了拉取消费者的receive方法,以方便开发者使用。

▍自定义消费者类

下图是并发消费者的例子:

消费者示例代码

那么 rocketmq-spring 是如何自动启动消费者呢 ?

spring 容器首先注册了消息监听器
后置处理器
,然后调用 ListenerContainerConfiguration 类的 registerContainer 方法 。

对比并发消费者的例子,我们可以看到: DefaultRocketMQListenerContainer 是对 DefaultMQPushConsumer 消费逻辑的封装。

封装
消费消息的逻辑
,同时
满足 RocketMQListener 泛化接口支持不同参数
,比如 String 、MessageExt 、自定义对象 。

首先DefaultRocketMQListenerContainer初始化之后, 获取 onMessage 方法的参数类型 。

然后消费者调用 consumeMessage 处理消息时,封装了一个 handleMessage 方法 ,将原生 RocketMQ 消息对象 MessageExt 转换成 onMessage 方法定义的参数对象,然后调用 rocketMQListener 的 onMessage 方法。

mnjh9

上图右侧标红的代码也就是该方法的精髓:

rocketMQListener.onMessage(doConvertMessage(messageExt));

6 写到最后

开源项目 rocketmq-spring 有很多值得学习的地方 ,我们可以从如下四个层面逐层进阶:

1、
学会如何使用
:参考 rocketmq-spring-boot-samples 模块的示例代码,学会如何发送和接收消息,快速编码;

2、
模块设计
:学习项目的模块分层 (父模块、SDK 模块、核心实现模块、示例代码模块);

3、
starter 设计思路
:定义自动配置文件 spring.factories 、设计配置属性类 、在 RocketMQ client 的基础上实现优雅的封装、深入理解 RocketMQ 源码等;

4、
举一反三
:当我们理解了 rocketmq-spring 的源码,我们可以尝试模仿该项目写一个简单的 spring boot starter。


如果我的文章对你有所帮助,还请帮忙
点赞、在看、转发
一下,你的支持会激励我输出更高质量的文章,非常感谢!

还是先举个例子,你侄女对天文知识感兴趣,然后你就用程序写了太阳系九大星系(水星、金星、地球、火星、木星、土星、天王星、海王星、冥王星)的运行轨迹图,然后拿给侄女看。然后她说错了错了,你的知识太旧了,多了一颗星。根据2006年8月24日国际天文联合大会召开,在会议上经过投票表决,冥王星被降级为矮行星,太阳系目前只剩下八颗行星。所以你需要删除一颗星。这个时候你打开电脑开始删除冥王星。

你从下面List中删除一颗星。

List<String> tempList = Arrays.asList("水星","金星","地球","火星","木星","土星","天王星","海王星","冥王星","冥王星");

怎么删除List中的冥王星呢?[PS为了演示某些删除方法不可靠,重复写了冥王星] 。

先写一段阿里规约:

【强制】不要再foreach循环里进行元素的remove/add操作,remove元素请使用Iterator方式,如果并发的操作,需要对Iterator对象加锁。

好了,那就让我们来写所有可能删除元素的方法

1:普通的for循环的删除(不可靠)。

List<String> list = newArrayList(tempList);for (int i = 0; i < list.size(); i++) {
String str
=list.get(i);if ("冥王星".equals(str)) {
list.remove(i);
}
}
System.out.println(list);

运行结果如下:

[水星, 金星, 地球, 火星, 木星, 土星, 天王星,    海王星, 冥王星]

奇了怪了,没删除干净?

问题出在 list.size(),因为 list.size() 和 i 都是动态变化的,i 的值一直在累加,list.size() 一直在减少,所以 list 就会早早结束了循环。所以这种方式虽然不会报错,但存在隐患,并且不容易被察觉,不建议使用。

2:普通的for循环提取变量进行删除(这个更不可靠,会报错)。

List<String> list = newArrayList(tempList);int size =list.size();for (int i = 0; i < size; i++) {
String result
=list.get(i);if ("冥王星".equals(result)) {
list.remove(i);
}
}
System.out.println(list);

结果如下:

这更不对了,一下子搞出个下标越界。

因为 size 变量是固定的,但 list 的实际大小是不断减小的,而 i 的大小是不断累加的,一旦 i >= list 的实际大小肯定就异常了。

3:普通的for循环倒叙删除(
这个用法可以,但也不推荐,倒叙看着很别扭,个人意见
)。

for (int i = list.size() - 1; i > 0; i--) {
String result
=list.get(i);if ("冥王星".equals(result)) {
list.remove(i);
}
}
System.out.println(list);

运行结果如下:

[水星, 金星, 地球, 火星, 木星, 土星, 天王星, 海王星]

4:使用增强的for循环删除(
会抛出异常,不推荐,
注意我这次为了演示效果,把行星的顺序换一下
),不少开发者喜欢用这种方式。

List<String> tempList = Arrays.asList("水星","金星","地球","火星","冥王星","土星","天王星","海王星","冥王星","木星");
List
<String> list = newArrayList(tempList);for(String item : list) {if ("冥王星".equals(item)) {
list.remove(item);
}
}
System.out.println(list);

结果如下:

奇了怪了,又抛异常了。不过这次的异常和上面的下标异常不太一样,这次是:

java.util.ConcurrentModificationException

这个是集合操作中很常见的异常之一,即并发修改异常!

增强的 for循环,其内部是调用的 Iterator 的方法,取下个元素的时候都会去判断要修改的数量(modCount)和期待修改的数量(expectedModCount)是否一致,不一致则会报错,而 ArrayList 中的 remove 方法并没有同步期待修改的数量(expectedModCount)值,所以会抛异常了。

5、迭代器循环迭代器删除(
可靠,也是十分推荐的用法
)。

Iterator<String> iterator =list.iterator();while(iterator.hasNext()){
String item
=iterator.next() ;if ("冥王星".equals(item)){
iterator.remove();
}
}
System.out.println(list);

结果如下,十分完美和正确:

[水星, 金星, 地球, 火星, 土星, 天王星, 海王星, 木星]

这是因为迭代器中的 remove 方法将期待修改的数量(expectedModCount)值进行了同步。

6:迭代器循环集合删除(这个可能很多开发者也会这样写,也可能会抛出异常的)。

Iterator<String> iterator =list.iterator();while(iterator.hasNext()){
String item
=iterator.next() ;if ("冥王星".equals(item)){
list.remove(item);
}
}
System.out.println(list);

结果如下:

7:Stream filter 过滤(
十分推荐,当然使用这个删除需要JDK的环境在8及其8以上的版本
)。

list = list.stream().filter(item -> !"冥王星".equals(item)).
collect(Collectors.toList());
System.out.println(list);

结果如下,十分完美和正确:

这个方法利用了 Stream 的筛选功能,快速过滤所需要的元素,虽然不是进行集合删除,但达到了同样的目的,这种方法要更简洁

看了上面的几个例子,相信你熟悉了List删除元素的用法了,希望你看了上面的例子,开发的时候不会再犯错了。

概述

背景

函数式编程的理论基础是阿隆佐·丘奇(Alonzo Church)于 1930 年代提出的 λ 演算(Lambda Calculus)。λ 演算是一种形式系统,用于研究函数定义、函数应用和递归。它为计算理论和计算机科学的发展奠定了基础。随着 Haskell(1990年)和 Erlang(1986年)等新一代函数式编程语言的诞生,函数式编程开始在实际应用中发挥作用。

函数式的价值

随着硬件越来越便宜,程序的规模和复杂性都在呈线性的增长。这一切都让编程工作变得困难重重。我们想方设法使代码更加一致和易懂。我们急需一种
语法优雅,简洁健壮,高并发,易于测试和调试
的编程方式,这一切恰恰就是
函数式编程(FP)
的意义所在。

函数式语言已经产生了优雅的语法,这些语法对于非函数式语言也适用。 例如:如今 Python,Java 8 都在吸收 FP 的思想,并且将其融入其中,你也可以这样想:

OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数 式编程)是抽象行为。

新旧对比

用传统形式和 Java 8 的方法引用、Lambda 表达式分别演示。代码示例:

interface Strategy {
    String approach(String msg);
}

class Soft implements Strategy {
    public String approach(String msg) {
        return msg.toLowerCase() + "?";
    }
}

class Unrelated {
    static String twice(String msg) {
        return msg + " " + msg;
    }
}

public class Strategize {

    Strategy strategy;
    String msg;
    Strategize(String msg) {
        strategy = new Soft(); // [1] 构建默认的 Soft
        this.msg = msg;
    }

    void communicate() {
        System.out.println(strategy.approach(msg));
    }

    void changeStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    public static void main(String[] args) {
        Strategy[] strategies = {
                new Strategy() { // [2] Java 8 以前的匿名内部类
                    public String approach(String msg) {
                        return msg.toUpperCase() + "!";
                    }
                },
                msg -> msg.substring(0, 5), // [3] 基于 Ldmbda 表达式,实例化 interface
                Unrelated::twice // [4] 基于 方法引用,实例化 interface
        };
        Strategize s = new Strategize("Hello there");
        s.communicate();
        for(Strategy newStrategy : strategies) {
            s.changeStrategy(newStrategy); // [5] 使用默认的 Soft 策略
            s.communicate(); // [6] 每次调用 communicate() 都会产生不同的行为
        }
    }
}

输出结果:

hello there?
HELLO THERE!
Hello
Hello there Hello there

Lambda 表达式

Lambda 表达式是使用
最小可能
语法编写的函数定义:(原则)

  1. Lambda 表达式产生函数,而不是类
  2. Lambda 语法尽可能少,这正是为了使 Lambda 易于编写和使用

Lambda 用法:

interface Description {
    String brief();
}

interface Body {
    String detailed(String head);
}

interface Multi {
    String twoArg(String head, Double d);
}

public class LambdaExpressions {

    static Body bod = h -> h + " No Parens!"; // [1] 一个参数时,可以不需要扩展 (), 但这是一个特例
    static Body bod2 = (h) -> h + " More details"; // [2] 正常情况下的使用方式
    static Description desc = () -> "Short info"; // [3] 没有参数的情况下的使用方式
    static Multi mult = (h, n) -> h + n; // [4] 多参数情况下的使用方式

    static Description moreLines = () -> { 
        // [5] 多行代码情况下使用 `{}` + `return` 关键字
        // (在单行的 Lambda 表达式中 `return` 是非法的)
        System.out.println("moreLines()");
        return "from moreLines()";
    };

    public static void main(String[] args) {
        System.out.println(bod.detailed("Oh!"));
        System.out.println(bod2.detailed("Hi!"));
        System.out.println(desc.brief());
        System.out.println(mult.twoArg("Pi! ", 3.14159));
        System.out.println(moreLines.brief());
    }
}

输出结果:

Oh! No Parens!
Hi! More details
Short info
Pi! 3.14159
moreLines()
from moreLines()

总结:Lambda 表达式通常比匿名内部类产生更易读的代码,因此我们将尽可能使用它们。

方法引用

方法引用由类名或者对象名,后面跟着
::
然后跟方法名称,

使用示例:

interface Callable { // [1] 单一方法的接口(重要)
    void call(String s);
}

class Describe {
    void show(String msg) { // [2] 符合 Callable 接口的 call() 方法实现
        System.out.println(msg);
    }
}

public class MethodReferences {
    static void hello(String name) { // [3] 也符合 call() 方法实现
        System.out.println("Hello, " + name);
    }

    static class Description {
        String about;

        Description(String desc) {
            about = desc;
        }

        void help(String msg) { // [4] 静态类的非静态方法
            System.out.println(about + " " + msg);
        }
    }

    static class Helper {
        static void assist(String msg) { // [5] 静态类的静态方法,符合 call() 方法
            System.out.println(msg);
        }
    }

    public static void main(String[] args) {
        Describe d = new Describe();
        Callable c = d::show; // [6] 通过方法引用创建 Callable 的接口实现
        c.call("call()"); // [7] 通过该实例 call() 方法调用 show() 方法

        c = MethodReferences::hello; // [8] 静态方法的方法引用
        c.call("Bob");

        c = new Description("valuable")::help; // [9] 实例化对象的方法引用
        c.call("information");

        c = Helper::assist; // [10] 静态方法的方法引用
        c.call("Help!");
    }
}

输出结果:

call()
Hello, Bob
valuable information
Help!

Runnable 接口

使用 Lambda 和方法引用改变 Runnable 接口的写法:

// 方法引用与 Runnable 接口的结合使用

class Go {
    static void go() {
        System.out.println("Go::go()");
    }
}

public class RunnableMethodReference {

    public static void main(String[] args) {

        new Thread(new Runnable() {
            public void run() {
                System.out.println("Anonymous");
            }
        }).start();

        new Thread(
                () -> System.out.println("lambda")
        ).start();

        new Thread(Go::go).start();		// 通过 方法引用创建 Runnable 实现的引用
    }
}

输出结果:

Anonymous
lambda
Go::go()

未绑定的方法引用

使用未绑定的引用时,需要先提供对象:

// 未绑定的方法引用是指没有关联对象的普通方法
class X {
    String f() {
        return "X::f()";
    }
}

interface MakeString {
    String make();
}

interface TransformX {
    String transform(X x);
}

public class UnboundMethodReference {

    public static void main(String[] args) {
        // MakeString sp = X::f;       // [1] 你不能在没有 X 对象参数的前提下调用 f(),因为它是 X 的方法
        TransformX sp = X::f;       // [2] 你可以首个参数是 X 对象参数的前提下调用 f(),使用未绑定的引用,函数式的方法不再与方法引用的签名完全相同
        X x = new X();
        System.out.println(sp.transform(x));      // [3] 传入 x 对象,调用 x.f() 方法
        System.out.println(x.f());      // 同等效果
    }
}

输出结果:

X::f()
X::f()

我们通过更多示例来证明,通过未绑的方法引用和 interface 之间建立关联:

package com.github.xiao2shiqi.lambda;

// 未绑定的方法与多参数的结合运用
class This {
    void two(int i, double d) {}
    void three(int i, double d, String s) {}
    void four(int i, double d, String s, char c) {}
}
interface TwoArgs {
    void call2(This athis, int i, double d);
}
interface ThreeArgs {
    void call3(This athis, int i, double d, String s);
}
interface FourArgs {
    void call4(
            This athis, int i, double d, String s, char c);
}

public class MultiUnbound {

    public static void main(String[] args) {
        TwoArgs twoargs = This::two;
        ThreeArgs threeargs = This::three;
        FourArgs fourargs = This::four;
        This athis = new This();
        twoargs.call2(athis, 11, 3.14);
        threeargs.call3(athis, 11, 3.14, "Three");
        fourargs.call4(athis, 11, 3.14, "Four", 'Z');
    }
}

构造函数引用

可以捕获构造函数的引用,然后通过引用构建对象

class Dog {
    String name;
    int age = -1; // For "unknown"
    Dog() { name = "stray"; }
    Dog(String nm) { name = nm; }
    Dog(String nm, int yrs) {
        name = nm;
        age = yrs;
    }
}

interface MakeNoArgs {
    Dog make();
}

interface Make1Arg {
    Dog make(String nm);
}

interface Make2Args {
    Dog make(String nm, int age);
}

public class CtorReference {
    public static void main(String[] args) {
        // 通过 ::new 关键字赋值给不同的接口,然后通过 make() 构建不同的实例
        MakeNoArgs mna = Dog::new; // [1] 将构造函数的引用交给 MakeNoArgs 接口
        Make1Arg m1a = Dog::new; // [2] …………
        Make2Args m2a = Dog::new; // [3] …………
        Dog dn = mna.make();
        Dog d1 = m1a.make("Comet");
        Dog d2 = m2a.make("Ralph", 4);
    }
}

总结

  • 方法引用在很大程度上可以理解为创建一个函数式接口的实例
  • 方法引用实际上是一种简化 Lambda 表达式的语法糖,它提供了一种更简洁的方式来创建一个函数式接口的实现
  • 在代码中使用方法引用时,实际上是在创建一个匿名实现类,引用方法实现并且覆盖了接口的抽象方法
  • 方法引用大多用于创建函数式接口的实现

函数式接口

  • Lambda 包含类型推导
  • Java 8 引入
    java.util.function
    包,解决类型推导的问题

通过函数表达式创建 Interface:

// 使用 @FunctionalInterface 注解强制执行此 “函数式方法” 模式
@FunctionalInterface
interface Functional {
    String goodbye(String arg);
}

interface FunctionalNoAnn {
    String goodbye(String arg);
}

public class FunctionalAnnotation {
    // goodbye
    public String goodbye(String arg) {
        return "Goodbye, " + arg + "!";
    }

    public static void main(String[] args) {
        FunctionalAnnotation fa = new FunctionalAnnotation();

        // FunctionalAnnotation 没有实现 Functional 接口,所以不能直接赋值
//        Functional fac = fa;      // Incompatible ?

        // 但可以通过 Lambda 将函数赋值给接口 (类型需要匹配)
        Functional f = fa::goodbye;
        FunctionalNoAnn fna = fa::goodbye;
        Functional fl = a -> "Goodbye, " + a;
        FunctionalNoAnn fnal = a -> "Goodbye, " + a;
    }
}

以上是自己创建 函数式接口的示例。

但在
java.util.function
包旨在创建一组完整的预定义接口,使得我们一般情况下不需再定义自己的接口。


java.util.function
的函数式接口的基本使用基本准测,如下

  1. 只处理对象而非基本类型,名称则为 Function,Consumer,Predicate 等,参数通过泛型添加
  2. 如果接收的参数是基本类型,则由名称的第一部分表示,如 LongConsumer, DoubleFunction,IntPredicate 等
  3. 如果返回值为基本类型,则用 To 表示,如 ToLongFunction 和 IntToLongFunction
  4. 如果返回值类型与参数类型一致,则是一个运算符
  5. 如果接收两个参数且返回值为布尔值,则是一个谓词(Predicate)
  6. 如果接收的两个参数类型不同,则名称中有一个 Bi

基本类型

下面枚举了基于 Lambda 表达式的所有不同 Function 变体的示例:

class Foo {}

class Bar {
    Foo f;
    Bar(Foo f) { this.f = f; }
}

class IBaz {
    int i;
    IBaz(int i) { this.i = i; }
}

class LBaz {
    long l;
    LBaz(long l) { this.l = l; }
}

class DBaz {
    double d;
    DBaz(double d) { this.d = d; }
}

public class FunctionVariants {
    // 根据不同参数获得对象的函数表达式
    static Function<Foo, Bar> f1 = f -> new Bar(f);
    static IntFunction<IBaz> f2 = i -> new IBaz(i);
    static LongFunction<LBaz> f3 = l -> new LBaz(l);
    static DoubleFunction<DBaz> f4 = d -> new DBaz(d);
    // 根据对象类型参数,获得基本数据类型返回值的函数表达式
    static ToIntFunction<IBaz> f5 = ib -> ib.i;
    static ToLongFunction<LBaz> f6 = lb -> lb.l;
    static ToDoubleFunction<DBaz> f7 = db -> db.d;
    static IntToLongFunction f8 = i -> i;
    static IntToDoubleFunction f9 = i -> i;
    static LongToIntFunction f10 = l -> (int)l;
    static LongToDoubleFunction f11 = l -> l;
    static DoubleToIntFunction f12 = d -> (int)d;
    static DoubleToLongFunction f13 = d -> (long)d;

    public static void main(String[] args) {
        // apply usage examples
        Bar b = f1.apply(new Foo());
        IBaz ib = f2.apply(11);
        LBaz lb = f3.apply(11);
        DBaz db = f4.apply(11);

        // applyAs* usage examples
        int i = f5.applyAsInt(ib);
        long l = f6.applyAsLong(lb);
        double d = f7.applyAsDouble(db);

        // 基本类型的相互转换
        long applyAsLong = f8.applyAsLong(12);
        double applyAsDouble = f9.applyAsDouble(12);
        int applyAsInt = f10.applyAsInt(12);
        double applyAsDouble1 = f11.applyAsDouble(12);
        int applyAsInt1 = f12.applyAsInt(13.0);
        long applyAsLong1 = f13.applyAsLong(13.0);
    }
}

以下是用表格整理基本类型相关的函数式接口:

函数式接口 特征 用途 方法名
Function<T, R> 接受一个参数,返回一个结果 将输入参数转换成输出结果,如数据转换或映射操作 R apply(T t)
IntFunction
接受一个 int 参数,返回一个结果 将 int 值转换成输出结果 R apply(int value)
LongFunction
接受一个 long 参数,返回一个结果 将 long 值转换成输出结果 R apply(long value)
DoubleFunction
接受一个 double 参数,返回一个结果 将 double 值转换成输出结果 R apply(double value)
ToIntFunction
接受一个参数,返回一个 int 结果 将输入参数转换成 int 输出结果 int applyAsInt(T value)
ToLongFunction
接受一个参数,返回一个 long 结果 将输入参数转换成 long 输出结果 long applyAsLong(T value)
ToDoubleFunction
接受一个参数,返回一个 double 结果 将输入参数转换成 double 输出结果 double applyAsDouble(T value)
IntToLongFunction 接受一个 int 参数,返回一个 long 结果 将 int 值转换成 long 输出结果 long applyAsLong(int value)
IntToDoubleFunction 接受一个 int 参数,返回一个 double 结果 将 int 值转换成 double 输出结果 double applyAsDouble(int value)
LongToIntFunction 接受一个 long 参数,返回一个 int 结果 将 long 值转换成 int 输出结果 int applyAsInt(long value)
LongToDoubleFunction 接受一个 long 参数,返回一个 double 结果 将 long 值转换成 double 输出结果 double applyAsDouble(long value)
DoubleToIntFunction 接受一个 double 参数,返回一个 int 结果 将 double 值转换成 int 输出结果 int applyAsInt(double value)
DoubleToLongFunction 接受一个 double 参数,返回一个 long 结果 将 double 值转换成 long 输出结果 long applyAsLong(double value)

非基本类型

在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。Java 会将你的方法映射到接口方法。示例:

import java.util.function.BiConsumer;

class In1 {}
class In2 {}

public class MethodConversion {

    static void accept(In1 in1, In2 in2) {
        System.out.println("accept()");
    }

    static void someOtherName(In1 in1, In2 in2) {
        System.out.println("someOtherName()");
    }

    public static void main(String[] args) {
        BiConsumer<In1, In2> bic;

        bic = MethodConversion::accept;
        bic.accept(new In1(), new In2());

        // 在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。Java 会将你的方法映射到接口方法。
        bic = MethodConversion::someOtherName;
        bic.accept(new In1(), new In2());
    }
}

输出结果:

accept()
someOtherName()

将方法引用应用于基于类的函数式接口(即那些不包含基本类型的函数式接口)

import java.util.Comparator;
import java.util.function.*;

class AA {}
class BB {}
class CC {}

public class ClassFunctionals {

    static AA f1() { return new AA(); }
    static int f2(AA aa1, AA aa2) { return 1; }
    static void f3 (AA aa) {}
    static void f4 (AA aa, BB bb) {}
    static CC f5 (AA aa) { return new CC(); }
    static CC f6 (AA aa, BB bb) { return new CC(); }
    static boolean f7 (AA aa) { return true; }
    static boolean f8 (AA aa, BB bb) { return true; }
    static AA f9 (AA aa) { return new AA(); }
    static AA f10 (AA aa, AA bb) { return new AA(); }

    public static void main(String[] args) {
        // 无参数,返回一个结果
        Supplier<AA> s = ClassFunctionals::f1;
        s.get();
        // 比较两个对象,用于排序和比较操作
        Comparator<AA> c = ClassFunctionals::f2;
        c.compare(new AA(), new AA());
        // 执行操作,通常是副作用操作,不需要返回结果
        Consumer<AA> cons = ClassFunctionals::f3;
        cons.accept(new AA());
        // 执行操作,通常是副作用操作,不需要返回结果,接受两个参数
        BiConsumer<AA, BB> bicons = ClassFunctionals::f4;
        bicons.accept(new AA(), new BB());
        // 将输入参数转换成输出结果,如数据转换或映射操作
        Function<AA, CC> f = ClassFunctionals::f5;
        CC cc = f.apply(new AA());
        // 将两个输入参数转换成输出结果,如数据转换或映射操作
        BiFunction<AA, BB, CC> bif = ClassFunctionals::f6;
        cc = bif.apply(new AA(), new BB());
        // 接受一个参数,返回 boolean 值: 测试参数是否满足特定条件
        Predicate<AA> p = ClassFunctionals::f7;
        boolean result = p.test(new AA());
        // 接受两个参数,返回 boolean 值,测试两个参数是否满足特定条件
        BiPredicate<AA, BB> bip = ClassFunctionals::f8;
        result = bip.test(new AA(), new BB());
        // 接受一个参数,返回一个相同类型的结果,对输入执行单一操作并返回相同类型的结果,是 Function 的特殊情况
        UnaryOperator<AA> uo = ClassFunctionals::f9;
        AA aa = uo.apply(new AA());
        // 接受两个相同类型的参数,返回一个相同类型的结果,将两个相同类型的值组合成一个新值,是 BiFunction 的特殊情况
        BinaryOperator<AA> bo = ClassFunctionals::f10;
        aa = bo.apply(new AA(), new AA());
    }
}

以下是用表格整理的非基本类型的函数式接口:

函数式接口 特征 用途 方法名
Supplier
无参数,返回一个结果 获取值或实例,工厂模式,延迟计算 T get()
Comparator
接受两个参数,返回 int 值 比较两个对象,用于排序和比较操作 int compare(T o1, T o2)
Consumer
接受一个参数,无返回值 执行操作,通常是副作用操作,不需要返回结果 void accept(T t)
BiConsumer<T, U> 接受两个参数,无返回值 执行操作,通常是副作用操作,不需要返回结果,接受两个参数 void accept(T t, U u)
Function<T, R> 接受一个参数,返回一个结果 将输入参数转换成输出结果,如数据转换或映射操作 R apply(T t)
BiFunction<T, U, R> 接受两个参数,返回一个结果 将两个输入参数转换成输出结果,如数据转换或映射操作 R apply(T t, U u)
Predicate
接受一个参数,返回 boolean 值 测试参数是否满足特定条件 boolean test(T t)
BiPredicate<T, U> 接受两个参数,返回 boolean 值 测试两个参数是否满足特定条件 boolean test(T t, U u)
UnaryOperator
接受一个参数,返回一个相同类型的结果 对输入执行单一操作并返回相同类型的结果,是 Function 的特殊情况 T apply(T t)
BinaryOperator
接受两个相同类型的参数,返回一个相同类型的结果 将两个相同类型的值组合成一个新值,是 BiFunction 的特殊情况 T apply(T t1, T t2)

多参数函数式接口

java.util.functional 中的接口是有限的,如果需要 3 个参数函数的接口怎么办?自己创建就可以了,如下:

// 创建处理 3 个参数的函数式接口
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
    
    R apply(T t, U u, V v);
}

验证如下:

public class TriFunctionTest {
    static int f(int i, long l, double d) { return 99; }

    public static void main(String[] args) {
        // 方法引用
        TriFunction<Integer, Long, Double, Integer> tf1 = TriFunctionTest::f;
        // Lamdba 表达式
        TriFunction<Integer, Long, Double, Integer> tf2 = (i, l, d) -> 12;
    }
}

高阶函数

高阶函数(Higher-order Function)其实很好理解,并且在函数式编程中非常常见,它有以下特点:

  1. 接收一个或多个函数作为参数
  2. 返回一个函数作为结果

先来看看一个函数如何返回一个函数:

import java.util.function.Function;

interface FuncSS extends Function<String, String> {}        // [1] 使用继承,轻松创建属于自己的函数式接口

public class ProduceFunction {
    // produce() 是一个高阶函数:既函数的消费者,产生函数的函数
    static FuncSS produce() {
        return s -> s.toLowerCase();    // [2] 使用 Lambda 表达式,可以轻松地在方法中创建和返回一个函数
    }

    public static void main(String[] args) {
        FuncSS funcSS = produce();
        System.out.println(funcSS.apply("YELLING"));
    }
}

然后再看看,如何接收一个函数作为函数的参数:

class One {}
class Two {}

public class ConsumeFunction {
    static Two consume(Function<One, Two> onetwo) {
        return onetwo.apply(new One());
    }

    public static void main(String[] args) {
        Two two = consume(one -> new Two());
    }
}

总之,高阶函数使代码更加简洁、灵活和可重用,常见于
Stream
流式编程中

闭包

在 Java 中,闭包通常与 lambda 表达式和匿名内部类相关。简单来说,闭包允许在一个函数内部访问和操作其外部作用域中的变量。在 Java 中的闭包实际上是一个特殊的对象,它封装了一个函数及其相关的环境。这意味着闭包不仅仅是一个函数,它还携带了一个执行上下文,其中包括外部作用域中的变量。这使得闭包在访问这些变量时可以在不同的执行上下文中保持它们的值。

让我们通过一个例子来理解 Java 中的闭包:

public class ClosureExample {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;

        // 这是一个闭包,因为它捕获了外部作用域中的变量 a 和 b
        IntBinaryOperator closure = (x, y) -> x * a + y * b;

        int result = closure.applyAsInt(3, 4);
        System.out.println("Result: " + result); // 输出 "Result: 110"
    }
}

需要注意的是,在 Java 中,闭包捕获的外部变量必须是
final
或者是有效的
final
(即在实际使用过程中保持不变)。这是为了防止在多线程环境中引起不可预测的行为和数据不一致。

函数组合

函数组合(Function Composition)意为 “多个函数组合成新函数”。它通常是函数式 编程的基本组成部分。

先看 Function 函数组合示例代码:

import java.util.function.Function;

public class FunctionComposition {
    static Function<String, String> f1 = s -> {
        System.out.println(s);
        return s.replace('A', '_');
    },
    f2 = s -> s.substring(3),
    f3 = s -> s.toLowerCase(),
    // 重点:使用函数组合将多个函数组合在一起
    // compose 是先执行参数中的函数,再执行调用者
    // andThen 是先执行调用者,再执行参数中的函数
    f4 = f1.compose(f2).andThen(f3);        

    public static void main(String[] args) {
        String s = f4.apply("GO AFTER ALL AMBULANCES");
        System.out.println(s);
    }
}

代码示例使用了 Function 里的 compose() 和 andThen(),它们的区别如下:

  • compose 是先执行参数中的函数,再执行调用者
  • andThen 是先执行调用者,再执行参数中的函数

输出结果:

AFTER ALL AMBULANCES
_fter _ll _mbul_nces

然后,再看一段 Predicate 的逻辑运算演示代码:

public class PredicateComposition {
    static Predicate<String>
            p1 = s -> s.contains("bar"),
            p2 = s -> s.length() < 5,
            p3 = s -> s.contains("foo"),
            p4 = p1.negate().and(p2).or(p3);    // 使用谓词组合将多个谓词组合在一起,negate 是取反,and 是与,or 是或

    public static void main(String[] args) {
        Stream.of("bar", "foobar", "foobaz", "fongopuckey")
                .filter(p4)
                .forEach(System.out::println);
    }
}

p4 通过函数组合生成一个复杂的谓词,最后应用在 filter() 中:

  • negate():取反值,内容不包含 bar
  • and(p2):长度小于 5
  • or(p3):或者包含 f3

输出结果:

foobar
foobaz


java.util.function
中常用的支持函数组合的方法,大致如下:

函数式接口 方法名 描述
Function<T, R> andThen 用于从左到右组合两个函数,即:
h(x) = g(f(x))
Function<T, R> compose 用于从右到左组合两个函数,即:
h(x) = f(g(x))
Consumer
andThen 用于从左到右组合两个消费者,按顺序执行两个消费者操作
Predicate
and 用于组合两个谓词函数,返回一个新的谓词函数,满足两个谓词函数的条件
Predicate
or 用于组合两个谓词函数,返回一个新的谓词函数,满足其中一个谓词函数的条件
Predicate
negate 用于对谓词函数取反,返回一个新的谓词函数,满足相反的条件
UnaryOperator andThen 用于从左到右组合两个一元操作符,即:
h(x) = g(f(x))
UnaryOperator compose 用于从右到左组合两个一元操作符,即:
h(x) = f(g(x))
BinaryOperator andThen 用于从左到右组合两个二元操作符,即:
h(x, y) = g(f(x, y))

柯里化

柯里化(Currying)是函数式编程中的一种技术,它将一个接受多个参数的函数转换为一系列单参数函数。

让我们通过一个简单的 Java 示例来理解柯里化:

public class CurryingAndPartials {
    static String uncurried(String a, String b) {
        return a + b;
    }

    public static void main(String[] args) {
        // 柯里化的函数,它是一个接受多参数的函数
        Function<String, Function<String, String>> sum = a -> b -> a + b;
        System.out.println(uncurried("Hi ", "Ho"));

        // 通过链式调用逐个传递参数
        Function<String, String> hi = sum.apply("Hi ");
        System.out.println(hi.apply("Ho"));

        Function<String, String> sumHi = sum.apply("Hup ");
        System.out.println(sumHi.apply("Ho"));
        System.out.println(sumHi.apply("Hey"));
    }
}

输出结果:

Hi Ho
Hi Ho
Hup Ho
Hup Hey

接下来我们添加层级来柯里化一个三参数函数:

import java.util.function.Function;

public class Curry3Args {
    public static void main(String[] args) {
        // 柯里化函数
        Function<String,
                Function<String,
                        Function<String, String>>> sum = a -> b -> c -> a + b + c;

        // 逐个传递参数
        Function<String, Function<String, String>> hi = sum.apply("Hi ");
        Function<String, String> ho = hi.apply("Ho ");
        System.out.println(ho.apply("Hup"));
    }
}

输出结果:

Hi Ho Hup

在处理基本类型的时候,注意选择合适的函数式接口:

import java.util.function.IntFunction;
import java.util.function.IntUnaryOperator;

public class CurriedIntAdd {
    public static void main(String[] args) {
        IntFunction<IntUnaryOperator> curriedIntAdd = a -> b -> a + b;
        IntUnaryOperator add4 = curriedIntAdd.apply(4);
        System.out.println(add4.applyAsInt(5));
    }
}

输出结果:

9

总结

Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持(Java 的历史包袱太重了),这些特性满足了很大一部分的、羡慕 Clojure 和 Scala 这类更函数化语言的 Java 程序员。阻止了他们投奔向那些语言(或者至少让他们在投奔之前做好准备)。总之,Lambdas 和方法引用是 Java 8 中的巨大改进

微软 3月22日 一篇文章“Semantic-kernel
嵌入和记忆:使用聊天UI探索GitHub Repos
”[1] ,文章中进行了展示了嵌入,该文章解释了他们如何帮助开发人员提出有关GitHub存储库的问题或使用自然语言查询探索GitHub存储库。与嵌入一起,这是在SK
存储器
[2](嵌入集合)的帮助下完成的,这有助于为提示(或SK世界中的ASK)提供更广泛的上下文。

浏览 GitHub 中的示例:
https://aka.ms/sk/repo/samples/github-repo-qa-bot

阅读有关示例的文档:
https://aka.ms/sk/github-bot

首先把
https://github.com/microsoft/semantic-kernel
克隆到本地。

第一步是运行 本地 API 服务,是基于 Azure function v4 用 C# 编写的,并公开了一些Semantic-kernel API。 运行这个服务,需要安装 Azure Functions Core Tools:
https://learn.microsoft.com/zh-cn/azure/azure-functions/functions-run-local?tabs=v4%2Cwindows%2Ccsharp%2Cportal%2Cbash
,下载工具本地安装,Azure function是用.NET 构建的:

image

安装好后,切换到目录samples\dotnet\KernelHttpServer 目录下运行 func start --csharp:

image

这个表示成功运行了Api服务,接着我们去把 React的 前端应用运行起来,切换到目录 samples\apps\github-qna-webapp-react,运行之前需要安装好工具yarn。

运行 yarn install  和 yarn start,看到类似输出,同时打开了浏览器
http://localhost:3000
:

image


这个示例的聊天模型选择 text-davinci-003:

image

Embedding 模型选择text-embedding-ada-002:

image

这个示例向量化的文档是Markdown的文件,我这里使用金融大数据量化分析:
https://github.com/plouto-quants/FBDQA-2019A
来做体验一下

image

把github 仓库中的markdown 文件下载下来后,我们就可以开始体验问答了:

image

image

从上面这两个截图你可以看到这个机器人还是很聪明的了,我们问他这个仓库里的文档里的相关问题,都能够回答得很好,这个示例没有记忆功能,也没有持久化,每次运行要重新配置。 实际得产品环境下可以继续基于这样的示例进行加强,把记忆 和 知识库向量持久化做好,那么就可以很轻松的应对以下场景:

  • 程序文档
  • 学生教育材料
  • 公司合同
  • 产品文档


概括

Onehouse 客户现在可以将他们的 Hudi 表查询为 Apache Iceberg 和/或 Delta Lake 表,享受从云上查询引擎到顶级开源项目的原生性能优化。

在数据平台需求层次结构的基础上,存在摄取、存储、管理和转换数据的基本需求。 Onehouse 提供这种基础数据基础架构作为服务,以在客户数据湖中摄取和管理数据。 随着数据湖在组织内的规模和种类不断增长,将基础数据基础架构与处理数据的计算引擎分离变得势在必行。 不同的团队可以利用专门的计算框架,例如 Apache Flink(流处理)、Ray(机器学习)或 Dask(Python 数据处理),以解决对其组织重要的问题。 解耦允许开发人员在以开放格式存储的数据的单个实例上使用这些框架中的一个或多个,而无需将其复制到和存储紧密耦合的另一服务中。 Apache Hudi、Apache Iceberg 和 Delta Lake 已成为领先的开源项目,为这个解耦存储层提供一组强大的原语,这些原语在云存储中围绕打开的文件提供事务和元数据(通常称为表格式)层,像 Apache Parquet 这样的格式。

背景

AWS 和 Databricks 在 2019 年为此类技术创造了最初的商业势头,分别支持 Apache Hudi 和 Delta Lake。 如今大多数云数据供应商都支持其中一种或多种格式。 然而他们继续构建一个垂直优化的平台以推动对他们自己的查询引擎的粘性,其中数据优化被锁定到某些存储格式, 例如要解锁 Databricks 的 Photon 引擎的强大功能需要使用 Delta Lake。 多年来 AWS 已在其所有分析服务中
预安装 Apache Hudi
,并继续近乎实时地支持更高级的工作负载。 Snowflake 宣布与 Iceberg 更强大的外表集成,甚至能够将 Delta 表作为外表进行查询。 BigQuery 宣布与所有三种格式集成,
首先是 Iceberg

所有这些不同的选项都提供了混合支持,我们甚至还没有开始列出各种开源查询引擎、数据目录或数据质量产品的支持。 这种越来越大的兼容性矩阵会让组织担心他们会被锁定在一组特定的供应商或可用工具的子集中,从而在进入数据湖之旅时产生不确定性和焦虑。

为什么要建立 Onetable?

在过去的一年里,我们发布了开源项目之间的
全面比较
,展示了 Hudi 如何具有显着的技术差异化优势,尤其是在为 Hudi 和 Onehouse 的增量数据服务提供支持的更新繁重工作负载方面。 此外Hudi 用于管理和优化表格的自动化表格服务为数据基础架构奠定了全面的基础,同时完全开源。 在选择表格格式时,工程师目前面临着一个艰难的选择,即哪些好处对他们来说最重要。 例如选择 Hudi 的
表服务
或像
Databricks Photon
这样快速的Spark引擎。 在 Onehouse我们会问真的有必要进行选择吗? 我们希望客户在处理他们的数据时获得尽可能好的体验,这意味着支持 Hudi 以外的格式以利用数据生态系统中不断增长的工具和查询引擎集。 作为一家倡导跨查询引擎互操作性的公司,如果我们不对元数据格式应用相同的标准以帮助避免将数据分解成孤岛,那我们的表现就很虚伪。 今天我们通过
Onetable
朝着这个方向迈出了一大步。

什么是 Onetable?

Onehouse 致力于开放,并希望通过我们的云产品 Onetable 上的一项新功能,帮助组织享受 Hudi 解锁的成本效率和高级功能,而不受当前市场产品的限制。 当数据静止在湖中时三种格式并没有太大区别。 它们都提供了对一组文件的表抽象,以及模式、提交历史、分区和列统计信息。 Onetable 采用源元数据格式并将表元数据提取为通用格式,然后可以将其同步为一种或多种目标格式。 这使我们能够将通过 Hudi 摄取的表公开为 Iceberg 和/或 Delta Lake 表,而无需用户复制或移动用于该表的底层数据文件,同时维护类似的提交历史记录以启用适当的时间点查询。

这种方法类似于 Snowflake 为 Iceberg 表保留其内部元数据,同时为外部互操作性创建 Iceberg 元数据的方式。 Hudi 还已经支持与 BigQuery 的集成,大型开源用户和 Onehouse 用户正在使用它。

我们为什么兴奋?

Onehouse 客户可以选择启用 Onetable 作为目录来自动将他们的数据公开为 Hudi 表以及 Iceberg 和/或 Delta Lake 表。 以这些不同的元数据格式公开表使客户能够轻松地加入 Onehouse 并享受
托管Lakehouse
的好处,同时使用他们喜欢的工具和查询引擎维护他们现有的工作流程。

例如Databricks 是运行 Apache Spark 工作负载的一个非常受欢迎的选择,其专有的
Photon
引擎可在使用 Delta Lake 表格式时提供性能加速。 为了确保使用 Onehouse 和 Databricks 的客户获得良好的体验而没有任何性能缺陷,我们使用 1TB TPC-DS 数据集来对查询性能进行基准测试。 我们比较了 Apache Hudi 和 Delta Lake 表,有/没有 Onetable 和 Databricks 的平台加速,如
磁盘缓存
和 Photon。 下图显示了 Onetable 如何通过基于 Delta Lake 协议转换元数据来解锁 Onehouse/Hudi 表上 Databricks 内部的更高性能。

此外我们将 Snowflake 中的同一张表公开为外部表,该表通常用于数仓。 我们执行了类似的 1TB TPC-DS 基准测试,比较了 Snowflake 的原生/专有表、外部Parquet表和使用 Onetable 的 Hudi 表。 下图显示 Onetable 如何向 Snowflake 查询公开 Hudi 表的一致快照,同时提供与 Snowflake 的 parquet 表类似的性能。

虽然上述外表的性能不如本地 Snowflake 表快,Onetable 提供了公开 Snowflake 内部数据湖的最新视图的功能,以帮助支持下游 ETL/转换或在组织过渡到构建Lakehouse以补充其 Snowflake 数据仓库时保持查询运行。 这种方法避免了将全套数据复制到仓库或使存储成本加倍,同时仍允许工程师和分析师派生出更有意义的聚合原生表,以快速提供报告和仪表板,充分利用 Snowflake 的强大功能。

最重要的是我们很高兴看到这如何帮助用户使用灵活的分层数据架构取得成功,这种架构已经在许多
大型数据组织
中流行。 Apache Hudi 为数据湖上的增量摄取/etl 提供
行业领先
的速度和成本效益,这是 Onehouse 的基础。 用户利用 Hudi 将这种高效、成本优化的数据摄取到
原始
/
铜银
表中。 Onehouse 的表管理服务可以直接在湖级别优化此数据的布局,以获得更好的查询性能。 然后用户可以使用 BigQuery、Redshift、Snowflake 等仓库引擎或 Databricks、AWS EMR、Presto 和 Trino 等湖引擎转换这些优化表。 然后将派生数据提供给最终用户,以构建个性化、近实时仪表板等数据应用程序。 Onetable 为用户提供了非常需要的可移植性,让他们可以根据自己的需求和成本/性能权衡来选择他们喜欢的查询引擎。 同时用户可以通过 Hudi 经验证的具有挑战性的变更数据捕获场景的效率以及 Onehouse 的表优化/管理服务来降低计算成本。

未来工作

数据空间中查询引擎、开源工具和新产品的格局在不断发展。 每年涌现的这些现有服务和新服务中的每一项都对这些表格格式提供了不同程度的支持。 Onetable 允许我们的客户使用任何与三种格式中的至少一种集成的服务,从而为他们提供尽可能多的选择。

Onehouse 致力于开源,其中包括 Onetable。 最初这将是为 Onehouse 客户保留的内部功能,因为我们会迭代设计和实施。 我们正在寻找来自其他项目和社区的合作伙伴来迭代这个共享的表标准表示,并最终为整个生态系统开源该项目。 例如当底层 Hudi 表发生变化时,Hudi 的目录同步表服务会增量维护此类目录元数据。 与 Onetable 的类似实现,将通过单个集成使不同引擎之间的元数据保持同步,从而为数据湖用户创造巨大的价值。