流处理之流式join模式

458 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情

本系列主要是《数据密集型应用系统设计》阅读笔记,本文记录流处理主题的笔记心得。

流式join

通过主键来join数据,这种join可以构成数据管道。流处理将数据管道推广为对无界数据集的增量处理,因此对流进行join的需求也适用。

流和流join

假设某网站支持搜索,并且想要检测网站搜索的最新趋势。

每次有人输入搜索查询,都会记录包含查询和返回结果的事件。每当有人单击其中一个搜索结果时,就会记录另一个单击的事件。为了记录搜索结果中每个网址的单记率,需要将搜索操作和单击操作的事件组合在一起,这些事件通过具有相同的会话ID进行join。广告系统也需要类似的分析。

如果用户并不相信搜索结果,可能永远不会发生单击事件,即使发生了这样的情况,搜索和单击之间的时间也可能存在很大间隔:在很多情况下可能是几秒钟。由于网络延迟也是变化的,单击事件甚至可能在搜索事件之前到达。这时可以定义合适的join窗口,例如,如果搜索和单击的间隔不超过一小时,则可以join它们。 请注意:在单击事件中嵌入搜索的细节并不等同于join事件:这样做只会告诉你用户单击了什么搜索结果,而无法告诉你用户没单击时的情况。

为了实现这种类型的join,流处理操作需要维护状态。例如:在最后一小时发生的所有事件(基于会话ID索引)。无论什么时候发生搜索事件或单击事件,都会将其添加到适当的索引,而且流处理还检查另一个索引,以查看同一个会话ID的另一个事件是否已到达。如果有匹配的事件,则发出一个派生事件来表明哪个搜索结果发生了单击。如果搜索事件过期而没有匹配到单击事件,则发出一个派生事件表明哪些搜索结果未被单击。

流和表join

假设有用户活动事件流,另外还有用户资料表。 将用户活动事件视为流,输出是在用户ID上追加了用户资料信息的活动事件流。这个过程有时被称为使用数据库的信息丰富活动事件

表和表join(物化视图维护)

Twitter时间线的例子

当用户想要查看其主页时间线时,循环遍历用户关注的所有人,查找它们最近的推文并将其合并,这个操作代价很高。 相反,我们需要一个时间线缓存:这是一种基于每个用户的“收件箱”,在发送推文时将其写入其中,因此读取时间线需要一次查询。 实现和维护这个缓存需要以下事件处理:

  • 当用户u发送新的推文时,它被添加到每个关注你的用户的时间线上
  • 用户u删除推文时,会从所有用户的时间线中删除
  • 用户u1开始关注u2,u2最近的推文被添加到u1的时间线上
  • ... 可以这么理解这个流处理过程:
select follows.follow_id as timeline_id,
       array_agg(tweets.* order by tweets.timestamp desc)
from tweets
join follows on follows.follow_id=tweets.sender_id
group by follows.follow_id

流的join直接对应于该查询中的表的join,时间线实际上是这个查询结果的缓存。

join的时间依赖性

这三种join有很多相似之处,都需要流处理系统根据一个join输入来维护某些状态,在另一个join输入的消息上查询该状态。

维持这个状态的事件的顺序非常重要。在分区日志中,单个分区内的事件排序被保留,但通常在不同的流或分区之间没有排序的保证。

这就产生了一个问题:如果不同流中的事件发生在相似的时间里,它们按照何种顺序进行处理?在流和表join的例子里面,如果用户更新了他们的资料,哪些活动事件会和旧资料join,哪些又与新资料join,如果状态随时间改变,而join操作需要输入状态信息,那么应该使用什么时间点的状态来join呢?

这种时间依赖性可能会在很多地方发生,比如,销售某些商品,需要对发票计算适当的税率,而税率取决于不同的国家或州、产品类型和销售日期。而销售情况与税率表join时,就希望join销售时所对应的税率,而如果正在重新处理历史数据,这可能就与当时的税率不同。 那么跨流的事件排序是不确定的,那么join也变成非确定性的。 在数据仓库中,这个问题被称为缓慢变化的维度,通常通过对特定版本的join记录赋予唯一的标识符来解决。例如,每当税率改变时,就赋予一个新的标志符,而发票也包括销售时的税率标志符。这种方式使join操作具有确定性,但是由于表中不同版本的记录都需要保留,最后日志压缩几乎无法有效工作。