前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >tRPC-Go 链路透传消息的源码级解读

tRPC-Go 链路透传消息的源码级解读

原创
作者头像
Martin Hong
发布于 2024-05-14 13:13:31
发布于 2024-05-14 13:13:31
1920
举报

概述

在分布式链路追踪等场景下,会使用到微服务调用链路上的透传能力,tRPC-Go 基于 tRPC 协议的头部设计实现了对链路透传的支持,这篇文章从源码角度分析链路透传的设计实现,文章中会涉及 tRPC-go 里不同场景中如何正确使用链路透传功能。

说明

  1. 本文基于以下源码以及版本 trpc-go: v0.9.4 (截止本文编写时的最新发布版本)
  2. 所有 tRPC-Go 源码均在文章中提供了链接,可以点击链接直达工蜂仓库。

源码走读

先从应用层调用讲起

一般而言,一个典型的 tRPC-go 的 RPC 调用起点类似如下代码:

代码语言:go
AI代码解释
复制
proxy := pb.NewOrderClientProxy()
rsp, err := proxy.PlaceOrder(ctx, req)

这里的工厂函数和方法都是我们用 trpc create 命令生成的桩代码里提供的实现,下一小节我们会看到桩代码里的实现。

另一方面,按照官方文档的说法,从主调向被调传递透传信息的话,需要使用 client.WithMetaData 传递选项,假如我们需要向 PlaceOrder 传递一个 requestID,值为 123456

代码语言:go
AI代码解释
复制
options := []client.Option{
    client.WithMetaData("requestID", []byte("123456")),
}
rsp, err := proxy.PlaceOrder(ctx, req, options...)

而假设被调的 PlaceOrder 方法会反过来透传一个流控信息,ExceedRateLimit"true",则主调还需要显式设定一个协议头对象指针:

代码语言:go
AI代码解释
复制
head := &trpc.ResponseProtocol{}
options := []client.Option{
    client.WithMetaData("requestID", []byte("123456")),
	client.WithRspHead(head),
}
rsp, err := proxy.PlaceOrder(ctx, req, options...)
if err == nil {
    fmt.Println(head.TransInfo) // 一个包含 key 为 ExceedRateLimit 的字典,类型为 map[string][]byte
}

记住这段代码,这是我们后面揭秘的基础,我们会研究这一段代码背后工作的原理。

从 tRPC-go 桩代码着手

我们重点看看方法桩里边的代码:

代码语言:go
AI代码解释
复制
func (c *OrderClientProxyImpl) PlaceOrder(ctx context.Context, req *EmptyMessage, opts ...client.Option) (rsp *EmptyMessage, err error) {
   // 这里通过 ctx 派生出新的 ctx 和消息,后面会有源码解读
   ctx, msg := codec.WithCloneMessage(ctx)
   // 这里会对派生出的消息进行被调信息设置,上面的 `WithCloneMessage` 会负责设置消息里的主调信息
   msg.WithClientRPCName("/trpc.example.orders_svr.Order/PlaceOrder")
   msg.WithCalleeServiceName(OrderServer_ServiceDesc.ServiceName)
   msg.WithCalleeApp("example")
   msg.WithCalleeServer("orders_svr")
   msg.WithCalleeService("Order")
   msg.WithCalleeMethod("PlaceOrder")
   msg.WithSerializationType(codec.SerializationTypePB)

   rsp = &EmptyMessage{}

   err = c.client.Invoke(ctx, req, rsp, callopts...)
   // 一些收尾工作,不相关,跳过
}

可以看到,每个 RPC 方法桩的实现里,其实都是标准的过程:

  1. 通过 codec.WithCloneMessage(ctx) 派生一组新的 context 和 message 实例;
  2. 设置 message 的被调信息,被调信息用于指明服务发现的目标以及 RPC 被调的服务名和方法名;
  3. 合并 opts 选项信息;
  4. 通过 c.client.Invoke 方法发起实际的远程调用。

我们只想关注链路透传过程,所以我们后面重点关注 codec.WithCloneMessage 函数和 c.clientInvoke 方法的逻辑。

codec.WithCloneMessage 的作用

我们进一步检查 codec.WithCloneMessage 的源码:

代码语言:go
AI代码解释
复制
func WithCloneMessage(ctx context.Context) (context.Context, Msg) {
    newMsg := msgPool.Get().(*msg)    // 从 sync.Pool 实例 msgPool 获取一个池化指针,这种写法是为了减少对象创建和销毁的开销
    val := ctx.Value(ContextKeyMessage)  // 从 ctx 对象中获取其绑定的 *codec.Message 指针,一般是当前服务收到的一个请求的消息的指针
    m, ok := val.(*msg)
    if !ok { // 下面 3 行,是在传入的 ctx 不是一个合法的 tRPC context 的情况下,直接使用全新的 *codec.Message
        ctx = context.WithValue(ctx, ContextKeyMessage, newMsg)
        newMsg.context = ctx
        return ctx, newMsg
    }
	// 以下两行,将新的 ctx 和新的消息指针 newMsg 相互绑定,后续业务逻辑里,使用 ctx 便可以找到绑定的消息指针
    ctx = context.WithValue(ctx, ContextKeyMessage, newMsg)
    newMsg.context = ctx
	// 以下两行,从原有的消息指针指向的对象上拷贝部分值到新的消息指针指向的内存上
    copyCommonMessage(m, newMsg)
    copyServerToClientMessage(m, newMsg)
    return ctx, newMsg
}

如代码注释说明,WithCloneMessage 实现一组新的 ctxmsg 的派生,消息派生过程中,具体拷贝了哪些信息呢?我们接着看 copyCommonMessagecopyServerToClientMessage

先看 copyCommonMessage

代码语言:go
AI代码解释
复制
func copyCommonMessage(m *msg, newMsg *msg) {
    newMsg.frameHead = m.frameHead
    newMsg.requestTimeout = m.requestTimeout
    newMsg.serializationType = m.serializationType
    newMsg.serverRPCName = m.serverRPCName
    newMsg.clientRPCName = m.clientRPCName
    newMsg.serverReqHead = m.serverReqHead
    newMsg.serverRspHead = m.serverRspHead
    newMsg.dyeing = m.dyeing
    newMsg.dyeingKey = m.dyeingKey
    newMsg.serverMetaData = m.serverMetaData
    newMsg.logger = m.logger
    newMsg.namespace = m.namespace
    newMsg.envName = m.envName
    newMsg.setName = m.setName
    newMsg.envTransfer = m.envTransfer
    newMsg.commonMeta = m.commonMeta.Clone()
}

可以看到,copyCommonMessage 主要是拷贝了这些信息:

  • 请求超时时间
  • RPC 请求和响应各自的名称和头部
  • serverMetaData:这个是当前服务收到的请求中由主调方传递到被调服务方的链路透传信息
  • 其他一些框架定义的上下文信息,比如 namespaceenvName 等。

接着再看 copyServerToClientMessage

代码语言:go
AI代码解释
复制
func copyServerToClientMessage(m *msg, newMsg *msg) {
	// 将原消息的服务端收到的链路透传消息复制给新消息的客户端链路透传
	// 也就是说,将当前服务收到的链路透传进一步传递给下一个服务
    newMsg.clientMetaData = m.serverMetaData.Clone()

	// 一般需要拷贝消息的场景都是因为当前服务也需要 RPC 调用,所以从当前服务收到的消息派生
	// RPC 调用消息的话,需要更正主调信息为自己的服务信息
    newMsg.callerServiceName = m.calleeServiceName
    newMsg.callerApp = m.calleeApp
    newMsg.callerServer = m.calleeServer
    newMsg.callerService = m.calleeService
    newMsg.callerMethod = m.calleeMethod
}

如上,到这里,一个派生的消息就从一个现有消息中拷贝了框架定义的一些上下文信息了,并且也设置了主调信息为自身服务,但是,被调信息呢?你回到开头看 tRPC 的桩代码就会知道了,桩代码里会负责进一步重写被调信息。

抬头看看 c.client.Invoke 方法

了解完消息派生,再来看看 RPC 调用请求的核心过程,c.client 是一个 client.Client 接口类型的对象,考虑到默认的 tRPC 协议请求的话,使用的是框架默认的 client 实现,我们看看它的 Invoke 方法:

代码语言:go
AI代码解释
复制
func (c *client) Invoke(ctx context.Context, reqbody interface{}, rspbody interface{}, opt ...Option) error {
    // 此处跳过一些不相关的逻辑

    // start filter chain processing
    return c.getFilters(opts).Filter(contextWithOptions(ctx, opts), reqbody, rspbody, callFunc)
}

Invoke 方法主要初始化请求的 opt 合并以及超时控制,核心是通过过滤器链的 Filter 方法开始逐层处理逻辑,而真正的请求处理逻辑则定义在 callFunc 中:

代码语言:go
AI代码解释
复制
func callFunc(ctx context.Context, reqbody interface{}, rspbody interface{}) error {
    msg := codec.Message(ctx)
    // 这里省略一些无关逻辑

    // 这里开始消息编码
    reqbuf, err := prepareRequestBuf(msg, reqbody, opts)
    if err != nil {
        return err
    }

	// 这里开始底层的连接和数据传输
    rspbuf, err := opts.Transport.RoundTrip(ctx, reqbuf, opts.CallOptions...)
    
    var rspbodybuf []byte
    if opts.EnableMultiplexed {
        rspbodybuf = rspbuf
    } else {
		// 消息解码,将收到的二进制解码到标准消息结构
        rspbodybuf, err = opts.Codec.Decode(msg, rspbuf)
        if err != nil {
            return errs.NewFrameError(errs.RetClientDecodeFail, "client codec Decode: "+err.Error())
        }
    }
	// 进一步做反序列化等操作
    return processResponseBuf(msg, rspbody, rspbodybuf, opts)
}

这里涉及几个关键操作,我们下来逐个展开。

prepareRequestBuf 的逻辑

以下是 prepareRequestBuf 的源码:

代码语言:go
AI代码解释
复制
func prepareRequestBuf(msg codec.Msg, reqbody interface{}, opts *Options) ([]byte, error) {
	// 序列化和压缩
    reqbodybuf, err := serializeAndCompress(msg, reqbody, opts)
    if err != nil {
        return nil, err
    }
    // 将序列化和压缩后的数据进一步按照传输协议编码
    reqbuf, err := opts.Codec.Encode(msg, reqbodybuf)
    if err != nil {
        return nil, errs.NewFrameError(errs.RetClientEncodeFail, "client codec Encode: "+err.Error())
    }
    return reqbuf, nil
}

序列化和压缩的逻辑在此不展开,因为不涉及我们要讨论的链路透传中的头部的处理,我们看 opts.Codec.Encode 的逻辑,它实现了对消息的编码。一般情况下,我们使用标准的 tRPC 编解码协议的话,使用的是框架默认的编解码器,我们看它的 Encode 方法:

代码语言:go
AI代码解释
复制
func (c *ClientCodec) Encode(msg codec.Msg, reqbody []byte) (reqbuf []byte, err error) {
    frameHead := getFrameHead(msg)  // 从消息元数据中获取帧信息,这个取决于具体协议,这里因为是默认实现,所以这里的帧头信息是 tRPC 的默认实现
    if frameHead.FrameType != uint8(TrpcDataFrameType_TRPC_UNARY_FRAME) {
        return c.streamCodec.Encode(msg, reqbody)
    }

	// 检查客户端应用层调用 RPC 时是否显式设置了请求头信息,有则使用其指定的,没有则使用默认头:
	// 	 Version:  uint32(TrpcProtoVersion_TRPC_PROTO_V1)
	//   CallType: uint32(TrpcCallType_TRPC_UNARY_CALL),
    req, err := getRequestHead(msg)
    if err != nil {
        return nil, err
    }
	// 从 msg 中提取请求所需的主被调信息、链路透传信息等
    c.updateReqHead(req, msg)

    // 协议升级到具体通信协议
    upgradeProtocol(frameHead, msg, req.RequestId)

    // 使用 protobuf 对请求头进行编码
    reqhead, err := proto.Marshal(req)
    if err != nil {
        return nil, err
    }
	// 将请求协议头与正文进行组装,形成完整的一次请求的编码
    return frameHead.construct(reqhead, reqbody)
}

这里的编码过程也比较清晰了,我们重点看看 c.updateReqHead(req, msg) 就好:

代码语言:go
AI代码解释
复制
func (c *ClientCodec) updateReqHead(req *RequestProtocol, msg codec.Msg) {
    // 此处省略一堆与本文无关的源码
    req.TransInfo = setClientTransInfo(msg, req.TransInfo)
}

这里就是请求的链路透传数据了,setClientTransInfo 负责将 msg 的链路透传信息复制到 req.TransInfo 中:

代码语言:go
AI代码解释
复制
func setClientTransInfo(msg codec.Msg, trans map[string][]byte) map[string][]byte {
    // set MetaData
    if len(msg.ClientMetaData()) > 0 {
        if trans == nil {
            trans = make(map[string][]byte)
        }
        for k, v := range msg.ClientMetaData() {
            trans[k] = v
        }
    }
    // 此处省略后续的一些额外的 transInfo 操作
    return trans
}

那么 msgClientMetaData 是怎么来的?还记得开头示例代码里的 client.WithMetaData("requestID", []byte("123456")) 吗?知道了吧,这个问题留给你自己探索一下。

opts.Codec.Decode 的逻辑

opts.Codec.Decode 方法调用发生在 RoundTrip 之后,也就是网络往返完成之后,此时客户端已经收到服务器端返回的完整响应了,我们看看它需要做什么事情:

代码语言:go
AI代码解释
复制
func (c *ClientCodec) Decode(msg codec.Msg, rspbuf []byte) (rspbody []byte, err error) {
    // 此处省略一些无关代码

    // 获取响应头信息
    rsp, err := c.getResponseHead(msg)
    if err != nil {
        return nil, err
    }

    // 此处省略一些代码:主要完成对头部信息二进制片段的截取
	
	// 将头部信息进行反序列化
    if err := proto.Unmarshal(rspbuf[begin:end], rsp); err != nil {
        return nil, err
    }

	// 使用响应头信息更
    if err := updateMsg(msg, frameHead, rsp); err != nil {
        return nil, err
    }

    // body decoded
    return rspbuf[end:], nil
}

先看 c.getResponseHead 的代码,它会在应用层有显式设定的情况下返回应用层设定的头部信息的指针:

代码语言:go
AI代码解释
复制
func (c *ClientCodec) getResponseHead(msg codec.Msg) (*ResponseProtocol, error) {
    if msg.ClientRspHead() != nil {
        // 这里不为空的话,意味着应用层执行了 client.WithRspHead(head) 设置了响应头,这种情况下就用
		// 应用层指定的指针反序列化头部,这样应用层就可以获取到响应头部了,应用层可以进一步使用 head.TransInfo 获取链路透传信息
        rsp, ok := msg.ClientRspHead().(*ResponseProtocol)
        if !ok {
            return nil, errors.New("client decode rsp head type invalid, must be trpc response protocol head")
        }
        return rsp, nil
    }
    
	// 没有指定的情况下,用默认的新的头部信息指针
    rsp := &ResponseProtocol{}
    msg.WithClientRspHead(rsp)
    return rsp, nil
}

所以,我们在文章开头示例代码中通过 client.WithRspHead(head) 设定的 head 会在此时被 getResponseHead 返回

接下来看 updateMsg(msg, frameHead, rsp)

代码语言:go
AI代码解释
复制
func updateMsg(msg codec.Msg, frameHead *FrameHead, rsp *ResponseProtocol) error {
    // 此处省略少量无关代码

    // 这里将被调服务端透传回来的信息合并到 ClientMetaData 中
    if len(rsp.TransInfo) > 0 {
        md := msg.ClientMetaData()
        if len(md) == 0 {
            md = codec.MetaData{}
        }
        for k, v := range rsp.TransInfo {
            md[k] = v
        }
        msg.WithClientMetaData(md)
    }

    // 此处省略一些无关代码
}

可以看到,这里其中的一个有意思的事情是会将服务端链路透传信息合并到请求时的消息的 ClientMetaData 中,如果你能访问到这个消息的指针,也就能够有第二种方式获取到服务端返回的链路透传信息了。但是显然你在应用层是不能的,为什么呢?因为请求使用的 msg 是在桩代码中派生的,不是你应用层提供的。所以,应用层想要获取链路回传信息,只有官方文档上的那一条路。但是为什么我要提有第二种可能性呢?那是因为你可以在 filter 里访问到这个 message,怎么访问?想想 trpc.Message(ctx)

总结

到这里,涉及链路往返透传的相关源码就剖析完整了,用一个流程图结束本文:

链路透传整体过程
链路透传整体过程

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
漫谈数据仓库之维度建模
数据仓库包含的内容很多,它可以包括架构、建模和方法论。对应到具体工作中的话,它可以包含下面的这些内容:
架构师修炼
2021/01/05
7740
漫谈数据仓库之维度建模
维度模型数据仓库(二) —— 维度模型基础
        既然维度模型是数据仓库建设中的一种数据建模方法,那不妨先看一下几种主流的数据仓库架构。
用户1148526
2022/12/02
1K0
维度模型数据仓库(二) —— 维度模型基础
数据仓库常见建模方法与建模实例演示[通俗易懂]
为什么要进行数据仓库建模?大数据的数仓建模是通过建模的方法更好的组织、存储数据,以便在 性能、成本、效率和数据质量之间找到最佳平衡点。一般主要从下面四点考虑
全栈程序员站长
2022/11/09
3.5K0
数据仓库常见建模方法与建模实例演示[通俗易懂]
深入讲解四种数仓建模理论方法
数据仓库的建设的最重要的核心核心之一就是数仓模型的设计和构建,这个决定了数仓的复用和性能,本文将介绍四种建模的理论:维度建模、关系建模、Data Vault建模、Anchor模型建模,文后也介绍几种常见的数仓建模工具。
Spark学习技巧
2024/01/26
2.9K0
深入讲解四种数仓建模理论方法
数据仓库常见建模方法与大数据领域建模实例综述
随着从IT时代到DT时代的跨越,数据开始出现爆发式的增长,这当中产生的价值也是不言而喻。如何将这些数据进行有序、有结构地分类组织存储,是我们所有数据从业者都要面临的一个挑战。
全栈程序员站长
2022/08/22
1.9K0
数据仓库常见建模方法与大数据领域建模实例综述
数仓建模——维度表详细讲解
来源:菜鸟数据之旅 本文约2100字,建议阅读5分钟 维度表是一种数据建模技术,用于存储与数据中心的各个业务领域相关的维度信息。 一、 维度表是什么 维度表是一种数据建模技术,用于存储与数据中心的各个业务领域相关的维度信息。它通常用于构建数据仓库、数据集市等决策支持系统,以便进行多维数据分析和报告。 在数据仓库中,维度表是与事实表相对应的表。维度表是维度建模的基础和灵魂。事实表紧紧围绕业务过程进行设计,事实表存储度量数据,如销售额、数量、收入等,而维度表则围绕业务过程所处的环境进行设计,维度表存储描述度
数据派THU
2023/05/11
1.3K0
数仓建模——维度表详细讲解
通俗易懂数仓建模—Inmon范式建模与Kimball维度建模
本文开始先简单理解两种建模的核心思想,然后根据一个具体的例子,分别使用这两种建模方式进行建模,大家便会一目了然!
五分钟学大数据
2021/04/15
2.1K0
通俗易懂数仓建模—Inmon范式建模与Kimball维度建模
基于Hadoop生态圈的数据仓库实践 —— 概述(一)
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wzy0623/article/details/51757011
用户1148526
2019/05/25
7580
数据仓库建模方法详解视频_三维建模流程步骤
范式建模法其实是我们在构建数据模型常用的一个方法,该方法的主要由Inmon所提倡,主要解决关系型数据库得数据存储,利用的一种技术层面上的方法,主要用于业务系统,所以范式建模主要是利用关系型数据库进行数仓建设
全栈程序员站长
2022/11/09
7940
数据仓库建模方法详解视频_三维建模流程步骤
数仓建模 - 维度 vs 关系
数据管理一直在演进,从早期的电子表格、蛛网系统到架构式数据仓库。发展至今以维度建模和关系建模为主,而随着互联网的发展,数据从GB到PB的裱花,企业业务迭代更新亦是瞬息万变,对维度模型的偏爱渐渐有统一互联网数仓建模标准的趋势。
大数据老哥
2022/02/17
8870
数仓建模 - 维度 vs 关系
数据仓库系列之维度建模
上一篇文章我已经简单介绍了数据分析中为啥要建立数据仓库,从本周开始我们开始一起学习数据仓库。学习数据仓库,你一定会了解到两个人:数据仓库之父比尔·恩门(Bill Inmon)和数据仓库权威专家Ralph Kimball。Inmon和Kimball两种DW架构支撑了数据仓库以及商业智能近二十年的发展,其中Inmon主张自上而下的架构,不同的OLTP数据集中到面向主题、集成的、不易失的和时间变化的结构中,用于以后的分析;且数据可以通过下钻到最细层,或者上卷到汇总层;数据集市应该是数据仓库的子集;每个数据集市是针对独立部门特殊设计的。而Kimball正好与Inmon相反,Kimball架构是一种自下而上的架构,它认为数据仓库是一系列数据集市的集合。企业可以通过一系列维数相同的数据集市递增地构建数据仓库,通过使用一致的维度,能够共同看到不同数据集市中的信息,这表示它们拥有公共定义的元素。
黄昏前黎明后
2019/08/26
1.4K0
Greenplum 实时数据仓库实践(2)——数据仓库设计基础
本篇首先介绍关系数据模型、多维数据模型和Data Vault模型这三种常见的数据仓库模型和与之相关的设计方法,然后讨论数据集市的设计问题,最后说明一个数据仓库项目的实施步骤。规划实施过程是整个数据仓库设计的重要组成部分。
用户1148526
2021/12/07
1.9K0
Greenplum 实时数据仓库实践(2)——数据仓库设计基础
浅谈数仓建模及其方法论
1.简单报表阶段:这个阶段,系统的主要目标是解决一些日常的工作中业务人员需要的报表,以及生成一些简单的能够帮助领导进行决策所需要的汇总数据。这个阶段的大部分表现形式为数据库和前端报表工具。
大数据真好玩
2021/03/15
1.9K0
数据仓库(03)数仓建模之星型模型与维度建模
维度建模是一种将数据结构化的逻辑设计方法,也是一种广泛应用的数仓建模方式,它将客观世界划分为度量和上下文。度量是常常是以数值形式出现,事实周围有上下文包围着,这种上下文被直观地分成独立的逻辑块,称之为维度。它与实体-关系建模有很大的区别,实体-关系建模是面向应用,遵循第三范式,以消除数据冗余为目标的设计技术。维度建模是面向分析,为了提高查询性能可以增加数据冗余,反规范化的设计技术。
张飞的猪
2022/09/03
8120
系列 | 漫谈数仓第二篇NO.2 数据模型(维度建模)
model对于数仓是最核心的东西,数据模型是数据组织和存储方法,模型的好坏,决定了数仓能支撑企业业务多久。
Spark学习技巧
2019/09/26
2.9K0
系列 | 漫谈数仓第二篇NO.2 数据模型(维度建模)
8000字,详解数据建模的方法、模型、规范和工具!
由于在变化快速的商业世界里,业务形态多种多样,为了能够更有针对性的进行数据建模,经过长时间的摸索,业界逐步形成了数据建模的四部曲:业务建模->领域建模->逻辑建模->物理建模。
肉眼品世界
2022/01/20
4.6K0
8000字,详解数据建模的方法、模型、规范和工具!
万字漫游数据仓库模型从入门到放弃
数据模型就是数据组织和存储方法,它强调从业务、数据存取和使用角度合理存储数据。只有将数据有序的组织和存储起来之后,数据才能得到高性能、低成本、高效率、高质量的使用。
Spark学习技巧
2023/09/07
6480
万字漫游数据仓库模型从入门到放弃
数据仓库建模
如果把数据看作图书馆里的书,我们希望看到它们在书架上分门别类地放置;如果把数据看作城市的建筑,我们希望城市规划布局合理;如果把数据看作电脑文件和文件夹,我们希望按照自己的习惯有很好的文件夹组织方式,而不是糟糕混乱的桌面,经常为找一个文件而不知所措。
用户1217611
2020/06/19
1.4K0
数仓建模与分析建模_数据仓库建模与数据挖掘建模
数据仓库: 数据仓库是一个面向主题的、集成的、非易失的、随时间变化的数据集合。重要用于组织积累的历史数据,并且使用分析方法(OLAP、数据分析)进行分析整理,进而辅助决策,为管理者、企业系统提供数据支持,构建商业智能。
全栈程序员站长
2022/11/09
1.4K0
数仓建模与分析建模_数据仓库建模与数据挖掘建模
【读书笔记】《 Hadoop构建数据仓库实践》第2章
一个列或者列集,唯一标识表中的一条记录。超键可能包含用于唯一标识记录所不必要的额外的列,我们通常只对仅包含能够唯一标识记录的最小数量的列感兴趣。
辉哥
2022/05/13
1K0
【读书笔记】《 Hadoop构建数据仓库实践》第2章
推荐阅读
相关推荐
漫谈数据仓库之维度建模
更多 >
LV.5
这个人很懒,什么都没有留下~
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档