一 场景介绍

        在维度模型中,数据通常被划分为维度和事实两大阵营,而维度通常是渐变(Kimball维度模型领域通常称呼这种维度为缓慢变化维度或者又被称为渐变维度)的,这种场景下,要求我们在维表建模过程中,要更多的考虑维度版本的变化,保存维度变化的维表模型可以方便在ETL和应用过程中可以让事实数据匹配自己对应的维度信息,这种场景在实时数仓构建过程中是比较常见的。

通常在维度建模过程中,针对渐变维表建模的方式主要有以下几种方案:

  • Kimball渐变维表类型2(可以认为是拉链维表方案)技术方案
  • 快照维表(每个键每天固定一行数据,可以认为是极限的拉链维度,即每天一个版本)方案

        那么在实时数仓构建过程中,针对标准维表或者ETL维表而言,我们应该怎么处理这种渐变维度场景呢?本文主要介绍的就是这一场景下针对Redis作为缓存维表的解决方案。

传统离线数仓中的缓慢变化维度和实时数仓中的缓慢变化维度在理论定义上含义是一致的,但在实现上还是存在区别的,这要考虑到具体的ETL场景因素影响。传统离线数仓解决缓慢变化为通常有类型0类型1类型2以及多个组合场景,甚至在大数据领域常用的快照维表方案。本文描述的是实时数仓领域缓慢变化维度的标准解决方案,这也来源于作者的Kimball理论理解以及实战经验总结。

Kimball维度模型理论至今已经发展30余年,从传统离线数仓到目前的实时数仓建模都能看到维度模型的影响,Kimball不愧是业界的大神。作者认为一个合格的数仓工程师,应该聚焦于准确的理解Kimball理论,然后着手于各种场景下的标准实现。

  • 概念解释
  • 标准维表:指的是那些数仓通用的维表,通常在事实表或应用底表做关联的维表,特点之一是对外部可见;
  • ETL维表:指那些用于数仓模型ETL装载过程中的一类维表,通常只针对ETL内任务可见,特点之一是对外不可见;

二 实时数仓维表技术方案回顾

        本节内容笔者在《实时数仓架构》系列章节中已经有所介绍,但在那里着重介绍的是实时数仓的架构方案,而不是具体到场景的技术实现。在真正介绍场景的技术实现之前,笔者在这里还是要简单回顾一下实时数仓建模下的维表技术方案。

        在笔者的工作中,通常来说对于实时数仓构建,维表会选择Hbase和Redis作为存储介质,这里要先介绍一下背景,针对Flink实时开发而言,为什么我们要将维表存储到外部存储(Hbase或者Redis)呢?我们想象一个场景,比如针对电商的交易业务来说,比如淘宝交易场景,这里有几个角色要定义清楚,首先,淘宝作为平台方,商家入驻淘宝平台,上线产品在线售卖,买家基于平台购买商家商品服务。在这个场景下,买家购买的商品很可能是商家在去年或者更久之前上线的产品,这也就意味着,在当前时间的交易下,购买的商家产品是更早之前上线售卖的。从这里我们可以很容易的得到一个结论,如果维度信息作为流参与计算,那么在流上我们要保留一年甚至更久的状态数据才能支撑交易场景下的实时计算,这种时间周期下的状态显然是不合理的。也正是因为这个原因,在Flink引擎设计过程中,针对维表Join才会衍生出Lookup Join技术方案。为了解决这种场景下的技术问题,我们需要将全量的维表数据存储到外部系统中,然后针对事实数据而言,在事实数据发生的一瞬间通过关联外部存储来为事实数据补全维度等ETL操作。

        在上面段落中,我们已经论述过了维表数据在外部存储缓存全量数据的必要性,同时针对维表缓存而言,一般会采用KV存储引擎(工作中常用的技术方案是Hbase和Redis)来实现维表缓存。针对于待缓存的维度数据,要么缓存全量当前最新的维度数据集合;要么缓存带版本信息的维度数据集合;带版本维表缓存结束也适用于当前最新版本数据。本文也是针对这两种场景的技术实现进行介绍。

三 基于Redis实现版本维表缓存

        上文已经详细介绍过了背景,这里就不废话。这里笔者通过两个部分来介绍生产上如何实现Redis维表的全量缓存实现,分别为选择数据结构执行SOP

1 选择数据结构

        由于使用Redis保存全量维度版本数据集合,那么Redis如何保存全量版本维度集合呢?这里有两个核心问题,第一个是Redis哪种数据结构适合保存版本数据?第二个问题是在分布式处理架构下,如何保证性能?这里衍生一下,是否需要分布式锁?如果不需要如何设计?

        为了解决上述两个问题,这里选择Redis的Sorted Set和Hash结合方式来缓存版本维表数据。Sorted Set用来存储当前自然键对应的各个版本信息;而Hash用来存储每个版本的具体数据。通过Redis的两层数据结构来实现渐变维表的全局版本数据缓存。一种常见的设计如下:

  • Sorted set设计:有序集合键设计成dim_data:<table_name>:<natural_key>格式,值设计成<natural_key>:<timestamp>格式,分数由时间戳值指定。
  • Hase设计:键变量设计成同Sorted set值相同格式,即<natural_key>:<timestamp>,加上固定区分的表的前缀后可能的一种设计如versions:<table_name>:<natural_key>:<timestamp>,值设计成当前版本的具体行数据格式。

        由于维表数据采用了Redis的两层数据结构方案缓存,我们一起分析下针对实际场景下的操作情况,这里分为维度更新和维度获取两种场景分别介绍。

        对于维度更新而言,首先针对hash结构存储维度版本行数据来说,通常同一个版本下的维度数据都是一致的(这在实际场景下是说的通的,而且我们的ETL逻辑也应该保证这一点的实现)。那么如果上游维度数据按照keyby分流可以保证每一个版本都有自己对应的数据存在,这种场景下的缓存数据更新由一个线程完成,对于使用方来说透明。如果上游数据没有keyby分流操作,那么可能同一个版本数据从不同的线程(甚至进程)处理,那么由于同一个业务自然键对应的同一个版本数据是一致的,即时存在重复更新也不会影响最终数据,那么对于使用方来说也是透明的。即对于hash数据的更新而言,是不需要加分布式锁进行同步操作的,这样对于性能影响最小。对于Sorted set数据结构来说,由于同一个自然键对应的版本数据存储到同一个set集合中,且按照版本时间戳进行排序,而且值是固定的格式(hash表的主键),那么对于数据是否keyby都是没有影响的,因为最后结构都是排序的,同样情况也不需要加分布式锁。对于同一自然键,对于多进程的更新,最终数据结果不变,对性能影响也是最小的。综上所述,基于Redis的两层数据结构方案的维表数据缓存是可行的。

        对于数据获取而言,每当一条事实数据到来,程序都会到Redis根据事实对应的维度自然键获取Sorted set集合对应的自然键的集合,然后根据事实对应的时间戳选取匹配的维度版本记录键,然后根据选择的维度版本记录键(<natural_key>:<timestamp>)去hash表获取对应的维度版本数据。

2 执行SOP

要使用Redis实现版本维表的缓存操作,如下:

  • Redis全量版本维表缓存操作SOP
  1. 初始化历史数据
  • 由于维度数据时间周期都很长,要想让事实务必关联到自己对应的维度数据,通常第一步要通过离线数据初始化Redis缓存
  1. 实时处理增量数据
  • 历史数据导入Redis,并不代表后面维度数据就不会变化,我们依然后监控维表数据源,通过实时处理技术将增量维度数据及时同步到Redis中。
  •         当上述两个步骤都设计好之后才能保证Redis中的维度数据是完整的,是可以用来支持事实数据计算的。