去重方法-精确去重(Kylin的去重)

去重方法

在 OLAP 数据分析领域,去重计数(count distinct)是非常常见的需求(这可以作为一个度量),根据去重结果的要求分为近似去重精确去重(在Kylin中,可以自行选择):

  • Kylin的近似去重计数是基于HLL(HyperLogLog)来实现。

    简单来说,每个需要被计数的值都会经过特定Hash函数的计算,将得到的哈希值放入到byte数组中,最后根据特定算法对byte数据的内容进行统计,就可以得到近似的去重结果。

    这种方式的好处是,不论数据有多少,byte数组的大小是有理论上限的,通常不会超过128KB,存储压力非常小。也就是说能够满足超大数据集和快速响应的要求。

    但最大的问题就是结果是非精确的,这是因为保存的都是经过hash计算的值,而一旦使用hash就一定会有冲突,这就是导致结果不精确的直接原因。此外在实践中发现,hash函数的计算是计算密集型的任务,需要大量的CPU资源。

  • 对于精确去重,最常用的处理方法是 bit map 方法。对于整型数据,我们可以将这些整数直接保存到 bit map 中。但除了整型之外,还有其他类型,如 String,为了实现精确的重复数据删除,我们首先需要对这些数据建立一个字典进行统一映射,然后使用 bit map 方法进行统计。

    目前业界有很多成熟的bitmap库可用,比如RoaringBitmap,ConciseSet等,其中RoaringBitmap的读写速度最快,存储效率也很高,目前已经在Spark,Drill等开源项目中使用,因此Kylin也选择了RoaringBitmap。经过试验,证明了想法是可行的,通过实现一种新的基于bitmap的去重计算指标,确实能够实现对Int型数据的精确去重计数。在百万量级的数据量下,存储的结果可以控制在几兆,同时查询能够在秒级内完成,且可以支持任意粒度的上卷聚合计算。

精确去重的原理

精确去重指标实现的两个核心难点:

  1. 支持任意粒度的上卷聚合
  2. 支持String等非Int类型数据

难点的解决方案:

  1. 前面提到,为了支持任意粒度的上卷聚合,我们需要保留明细数据,而计算机存储的最小单位是bit,所以采用了Bitmap来存储Kylin的精确去重指标。Kylin在实际工程中采用的是业界中广泛使用, 性难最优的RoaringBitmap库。
  2. RoaringBitmap仅支持Int类型数据,为了支持String等非Int类型数据,我们需要一个String到Int的映射,而且要求所有Segment中同一个String始终映射到同一个Int。 为什么呢? 假如UUID列的String “A” 在Segment1中映射到Int 1,但是String “A” 却在Segment2中映射到INT 2,那么当UUID列需要跨Segment1和Segment2去重时,显然就会出错。 所以我们引入了全局字典,来保证String等非Int类型数据始终映射到同一个Int值。

无需上卷的精确去重查询优化

前面提到,为了支持任意粒度的上卷聚合,我们使用Bitmap存储精确去重指标。所以在查询时,我们需要先从HBase端将整个Bitmap传输给Kylin的QueryServer,Kylin的QueryServr再根据Bitmap计算出 去重值。但是在实际的使用场景中,用户的一些甚至多数精确去重查询是不需要上卷聚合的, 比如用户的Cube按照dt列分区,且已经预计算好(A,dt)的Cuboid,那么下面的SQL查询时在HBase端和Kylin的QueryServer端都是无需聚合的:

RoaringBitmap

RoaringBitmap是高效压缩位图,简称RBM。

RBM的历史并不长,它于2016年由S. Chambi、D. Lemire、O. Kaser等人在论文《Better bitmap performance with Roaring bitmaps》与《Consistently faster and smaller compressed bitmaps with Roaring》中提出.

实现思路
  • 将 32bit int(无符号的)类型数据 划分为 2^16 个桶,即最多可能有216=65536个桶,论文内称为container。用container来存放一个数值的低16位;
  • 在存储和查询数值时,将数值 k 划分为高 16 位和低 16 位,取高 16 位值找到对应的桶,然后在将低 16 位值存放在相应的 Container 中(存储时如果找不到就会新建一个)

比如要将31这个数放进roarigbitmap中,它的16进制为:0000001F,前16位为0000,后16为001F。

所以先需要根据前16位的值:0,找到它对应的通的编号为0,然后根据后16位的值:31,确定这个值应该放到桶中的哪一个位置,如下图所示。

在这里插入图片描述

小桶的类型

在roaringbitmap中共有3种小桶:

  • arraycontainer(数组容器)
  • bitmapcontainer(位图容器)
  • runcontainer(行程步长容器)

(1)arraycontainer

当ArrayContainer(其中每一个元素的类型为 short int 占两个字节,且里面的元素都是按从大到小的顺序排列的)的容量超过4096(即8k)后,会自动转成BitmapContainer(这个所占空间始终都是8k)存储。

4096这个阈值很聪明,低于它时ArrayContainer比较省空间,高于它时BitmapContainer比较省空间。也就是说ArrayContainer存储稀疏数据,BitmapContainer存储稠密数据,可以最大限度地避免内存浪费。

在这里插入图片描述

(2)bitmapcontainer

这个容器就是位图,只不过这里位图的位数为216(65536)个,也就是2^16个bit, 所占内存就是8kb。然后每一位用0,1表示这个数不存在或者存在,如下图:

在这里插入图片描述

(3)runcontainer

RunContainer中的Run指的是行程长度压缩算法(Run Length Encoding),对连续数据有比较好的压缩效果。

它的原理是,对于连续出现的数字,只记录初始数字和后续数量。即:

  • 对于数列11,它会压缩为11,0;
  • 对于数列11,12,13,14,15,它会压缩为11,4;
  • 对于数列11,12,13,14,15,21,22,它会压缩为11,4,21,1;

不过这种容器不常用,所以在使用的时候需要自行调用相关的转换函数来判断是不是需要将arraycontiner,或bitmapcontainer转换为runcontainer。

这种压缩算法的性能和数据的连续性(紧凑性)关系极为密切,对于连续的100个short,它能从200字节压缩为4字节,但对于完全不连续的100个short,编码完之后反而会从200字节变为400字节。

如果要分析RunContainer的容量,我们可以做下面两种极端的假设:

  • 最好情况,即只存在一个数据或只存在一串连续数字,那么只会存储2个short,占用4字节
  • 最坏情况,0~65535的范围内填充所有的奇数位(或所有偶数位),需要存储65536个short,128kb

(4)读取性能

增删改查的时间复杂度方面,BitmapContainer只涉及到位运算且可以根据下标直接寻址,显然为O(1)。而ArrayContainer和RunContainer都需要用二分查找在有序数组中定位元素,故为O(logN)。

  • ArrayContainer一直线性增长,在达到4096后就完全比不上BitmapContainer了
  • BitmapContainer是一条横线,始终占用8kb
  • RunContainer比较奇葩,因为和数据的连续性关系太大,因此只能画出一个上下限范围。不管数据量多少,下限始终是4字节;上限在最极端的情况下可以达到128kb。

空间占用(即序列化时写出的字节流长度)方面,BitmapContainer是恒定为8KB的。ArrayContainer的空间占用与基数(c)有关,为(2 + 2c)B;RunContainer的则与它存储的连续序列数(r)有关,为(2 + 4r)B。

与bitmap的性能对比

roaringbitmap除了比bitmap占用内存少之外,其并集和交集操作的速度也要比bitmap的快,原因如下:

  • 计算上的优化

    • 对于roaringbitmap本质上是将大块的bitmap分成各个小块,其中每个小块在需要存储数据的时候才会存在。所以当进行交集或并集运算的时候,roaringbitmap只需要去计算存在的一些块而不需要像bitmap那样对整个大的块进行计算。如果块内非常稀疏,那么只需要对这些小整数列表进行集合的 AND、OR 运算,这样的话计算量还能继续减轻。这里既不是用空间换时间,也没有用时间换空间,而是用逻辑的复杂度同时换取了空间和时间。

    • 同时在roaringbitmap中32位长的数据,被分割成高 16 位和低 16 位,高 16 位表示块偏移,低16位表示块内位置,单个块可以表达 64k 的位长,也就是 8K 字节。这样可以保证单个块都可以全部放入 L1 Cache,可以显著提升性能。

  • 程序逻辑上的优化:

    • roaringbitmap维护了排好序的一级索引以及有序的arraycontainer,当进行交集操作的时候,只需要根据一级索引中对应的值来获取需要合并的容器,而不需要合并的容器则不需要对其进行操作直接过滤掉。
    • 当进行合并的arraycontainer中数据个数相差过大的时候采用基于二分查找的方法对arraycontainer求交集,避免不必要的线性合并花费的时间开销。
    • roaingbitmap在做并集的时候同样根据一级索引只对相同的索引的容器进行合并操作,而索引不同的直接添加到新的roaringbitmap上即可,不需要遍历容器。
    • roaringbitmap在合并容器的时候会先预测结果,生成对应的容器,避免不必要的容器转换操作。

全局字典介绍

在Apache Kylin的现有实现中,Cube的每个Segment都会创建独立的字典,这种方式会导致相同数据在不同Segment字典中被映射成不同的值,这会导致最终的去重结果出错。

为此,在 Cube 的增量构建过程中,为了避免由于对不同时间段分别构造字典而导致最终去重结果出现错误,一个 Cube 中的所有 segments 将使用同一个字典,即全局字典。全局字典的意义是保证所有Value映射到全局唯一且连续的Int ID

具体来说,根据字典的资源路径(元数据名+库名+表名+列名)可以从元数据中获取同一个字典实例,后续的数据追加也是基于这个唯一的字典实例创建的builder进行的。

全局字典最重要的意义是支持精确去重指标跨Segment上卷,但在某些应用场景下,用户的确不需要Segment上卷。 比如用户只需要按天进行去重,或者Cube本身就是不分区的(每次全量构建)。 针对这一点,新增了一种SegmentAppendTrieDictBuilder(前面提到的Segment Dictionary ),底层的数据结构依然还是AppendTrieDictionary,只是每次构建时Working目录不是Copy最新的Version目录,而是从空Working目录开始构建,同时字典的元数据也需要重新初始化。由于SegmentAppendTrieDictBuilder是segment粒度的,也不需要分布式锁,所以可以并发构建。使用SegmentAppendTrieDictBuilder后构建和加载时的内存问题也基本不会再有。

原理:

  • 每个构建任务都将生成一个新的全局字典;
  • 每个新的构建任务的字典会根据版本号保存,旧的全局字典会逐渐删除;
  • 一个全局字典包含一个元数据文件和多个字典文件,每个字典文件称为一个 bucket (bucket);
  • 每个 bucket 被划分为两个映射(Map<Object, Long>),并将这两个映射组合成一个完整的映射关系。

在这里插入图片描述

Kylin引入了桶这一概念,可以理解为在处理数据的时候,将数据分到若干个桶(即多个分区)中进行并行处理。 第一次构建字典的时候会对每个桶内的值从1开始编码,在所有桶的编码完成之后再根据每个桶的offset值进行整体字典值的分配。在代码中两次编码是通过两个HashMap进行存储的,其中一个存储桶内相对的字典值,另一个存储所有桶之间绝对的字典值

下图所示的是编号为1的桶多次构建任务中,桶内字典的传递,每一次构建都会为桶创建一个新的版本(即v1, v2, v3等),加入版本控制的原因后面会有解释。Curr(current)和Prev(Previous)是一个桶内的两个HashMap,分别存储着当前桶内字典的相对(Relative)编码值和之前已经构建的所有字典值的绝对(Absolute)编码值。

在这里插入图片描述

构建步骤:

  • 通过 Spark 创建平表并获取需精确去重列的 distinct 值
  • 根据确定去重后的字面值数量来确认分片数, 并且根据需求判断是否需要扩容
  • 将数据分配(repartition)到多个分片(Partition)中,分别进行编码, 存储到各自的字典文件中
  • 为当前构建任务分配版本号
  • 保存字典文件和 metadata数据(桶数量和桶的 offset 值)
  • 根据条件判断需要删除旧版本

初次构建:

  • 计算桶的大小:取需要构建字典的数量处理 单个桶阈值 和 桶数量默认值 的最大值。
  • 创建桶并分配数据进行编码
  • 生成meta文件记录桶的offsets

以下是相关配置项及其默认值:

kylin.dictionary.globalV2-min-hash-partitions=10
kylin.dictionary.globalV2-threshold-bucket-size=500000

在这里插入图片描述

非初次构建:

  • 根据字典数量确定桶是否需要扩容
  • 已编码的字典值对扩容后的桶进行重新分配
  • 读取之前最新版本的字典数据,并分配到各个桶中
  • 将新的值分配到桶中
  • 前一次构建的字典值不会改变

在这里插入图片描述

Trie树与AppendTrie树

在全局字典中,映射关系有很多种实现方式,比如基于格式的编码,hash编码等,其中空间和性能效率都比较高,且通用性更强的是基于Trie树的字典编码方式。目前Apache Kylin之前已经实现了TrieDictionary,用于维度值的编码。

Trie树又名前缀树,是是一种有序树,一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,根节点对应空字符串,而每个字符串对应的编码值由对应节点在树中的位置来决定。图1是一棵典型的Trie树示意图,注意并不是每个节点都有对应的字符串值。

在构造Trie树时,将每个值依次加入到树中,可以分为三种情况,如图所示:

上述的是Trie树在构建过程中的内存模型,当所有数据加入之后,需要将整棵Trie树的数据序列化并持久化。具体的格式如图所示:

从上图中可以按照,整棵树按照广度优先的顺序依次序列化,其中childOffsetnValuesBeneath的长度是可变的,这是为了根据整棵树的大小尽可能使用更短的数据,降低存储量。此外需要注意到,childOffset的最高两位是标志位,分别标识当前节点是否是最后一个child,以及当前节点是否对应了原始数据的一个值。

当需要从字典中检索某个值的映射id时,直接从序列化后的数据读取即可,不需要反序列化整棵Trie树,这样能在保证检索速度的同时保持较低的内存用量。整个过程如图所示。

通过上述对Trie树的分析可以看到,Trie树的效率很高,但有一个问题,Trie树的内容是不可变的。也就是说,当一颗Trie树构建完成后,不能再追加新的数据进去,也不能删除现有数据,这是因为每个原始数据对应的映射id是由对应节点在树中的位置决定的,一旦树的结构发生变化,那么会导致部分原始数据的映射id发生变化,从而导致错误的结果。为此,我们需要对Trie树进行改造,使之能够持续追加数据,也就是AppendTrie树。

下图是AppendTrie的序列化格式,可以看到和传统Trie树相比,主要区别在于不保留每个节点的子value数,转而将节点对应的映射id保存到序列化数据中,这样虽然增大了存储空间,但使得整棵树是可以追加的,而且同时也导致了AppendTrieDictionary只能根据Value查找Id,不能根据Id查找Value。

经过改造,AppendTrie树已经满足了我们的需求,能够支持我们实现对所有数据的统一编码,进而支持精确去重计数。但从实际情况来看,当统计的数据量超过千万时,整颗树的内存占用和序列化数据都会变得很大,因此需要考虑分片,将整颗树拆成多颗子树,通过控制每颗子树的大小,来实现整棵树的容量扩展。为了实现这个目标,当一棵树的大小超过设定阈值后,需要通过分裂算法将一棵树分裂成两棵子树,下图展示了整个过程。

在这里插入图片描述

在一棵AppendTrie树是由多棵子树构成的基础上,我们很容易想到通过LRU之类的算法来控制所有子树的加载和淘汰行为。为此我们基于guava的LoadingCache实现了特定的数据结构CachedTreeMap。这种map继承于TreeMap,同时value通过LoadingCache管理,可以根据策略加载或换出,从而在保证功能的前提下降低了整体的内存占用。

此外,为了支持字典数据的读写并发,数据持久化采用mvcc的理念,每次构建持久化的结果作为一个版本,版本一旦生成就不可再更改,后续更新必须复制版本数据后进行,并持久化为更新的版本。每次读取时则选择当前最新的版本读取,这样就避免了读写冲突。同时设置了基于版本个数和存活时间的版本淘汰机制,保证不会占用过多存储。

经过上述的改进和优化后,AppendTrie树完全达到了我们的要求,可以对所有类型的数据统一做映射编码,支持的数据量可以到几十亿甚至更多,且保持内存占用量的可控,这就是我们的可追加通用字典AppendTrieDictionary。

分布式环境下数据一致性的保证:

  • MVCC: 全局字典最终是持久化在HDFS目录上,为了避免读写冲突,我们采用了MVCC,当读AppendTrieDictionary时,永远只读取最新的Version目录;当写AppendTrieDictionary时,会将最新的Verion目录copy到working目录,修改完成且通过正确性校验后,会将working目录rename为新的Version目录。
  • 分布式锁:通过分布式锁,我们保证了在多JobServer的多Segment并发构建下,1个Kylin集群在同一时刻只会有1个线程可以修改AppendTrieDictionary。

参考:

RFID冗余数据近似消重 1.简介: 随着信息技术的发展,各种数据(如XML、RDF和RFID数据生成。RFID不需要接触即可检测射频识别标签的特性,因此被用于很多领域,如商业、军事和医学,导致了大量的RFID数据生成,沃尔玛采用RFID技术是一个典型的RFID在商业领域应用的例子。 然而,RFID技术也带来一系列的问题,由于RFID是非接触式探测,只要标签在阅读器的探测范围内,所有的标签信息都会被读到,因此,RFID标签在探测区移动缓慢或者停留都会产生冗余数据。另外,标签在探测区移动速度过快或者有多个标签同时移动,会造成阅读器漏读。为了避免漏读现象的产生,就会在一个区域放置多个阅读器,当同一个标签被几个阅读器读到时,冗余数据也就产生了。 一个智能的RFID阅读器能够去除自身产生的冗余数据,但是多个阅读器产生的冗余数据紧靠本身自包含的处理能力去除是不可能的,因此我们提出了一种基于中间件的处理冗余数据的技术。 上面这张图表示:有两个阅读器Reader1和Reader2,两个标签在探测区内,Reader1探测到标签用ID1,Reader2探测到标签用ID2表示,每个阅读器生成的信息表示形式为 : <tag ID, location of the reader, time>. Reader1探测到标签ID1产生RFID数据 : <ID1, Loc1, 1>, <ID1, Loc1, 2>, <ID1, Loc1,4>,然而, RFID 数据是重复的除了<ID1, Loc1, 1>. .Reader1 也探测到了ID2的RFID 数据 <ID2, Loc1, 3>, <ID2, Loc1, 5> , <ID2, Loc1, 5> 是重复数据. 同样的, Reader2 探测到的数据和产生的一些冗余数据在上图中都表示出来了。由于两个阅读器可能会探测到同一个RFID标签于是更多就会造成中间件中有更多的冗余数据。比如:Reader1<ID1, Loc1, 1>, Reader2<ID1, Loc2, 1> 就是冗余数据。中间件要进行去冗余处理,去冗余后结果为<ID1, Loc1, 1>, <ID2, Loc2, 2>.对于小数据的处理似乎是很简单的。 然而,RFID数据量是很庞大的,如果每个商品上都有一个标签,那么一个大型的零售商每天产生的数据将会超过1TB,并且,RFID数据是以流的形式产生的,也就意味着冗余数据在一个有限的内存中要立即进行去重处理,我们很难设计出一个精确的去冗余办法,因而,提出了一个近似去冗余方案去替代它。 有一种应用背景是这样的。我们要对大型百货商场中客户的移动进行实时分析,每个客户都有一个唯一的RFID标签,经理希望得到对客户的一些实时分析,比如:每个商店的顾客数量,哪个商店的顾客数量最多,百货商店的中间件就应该对这些数据进行去冗余处理。在这样的环境中,大量的重复数据同时进入中间件,特别是一个顾客长时间呆在同一个地方就会产生大量的重复数据,为了准确的去除冗余数据,我们需要在内存中长时间保存包括重复数据在内的所有RFID数据。当相对于RFID数据来说存储器的数量较少时,RFID数据实时去冗余也就很困难。在这个应用中,管理者是允许统计数据有误差,因而我们提出了在有限存储空间中的近似RFID去重技术。 Bloom filter是一种能够容忍误差的情况下被广泛应用的数据结构。要用少量的内存管理RFID数据流,我们设计了基于bloom filter的方法。然而,由于布鲁姆过滤器是针对静态数据,我们应该让bloom filter适应RFID数据流的环境。因此,我们提出了time bloom filter,由于要用少量的内存管理RFID数据流,我们设计了基于bloom filter的方法。然而,由于布鲁姆过滤器是针对静态数据,我们应该让bloom filter适应RFID数据流的环境。因此,我们提出了time bloom filter,由于time bloom filter是基于bloom filter的,它们不会产生假阴性错误,但是,它们可能产生假阳性错误。我们也计算出了time bloom filter的假阳性错误率,为了降低错误率,我们又提出了time interval bloom filter。 2.相关工作: 3.初期(基本bloom filter介绍): 为了表达S={x1, x2,…,xn}这样一个n个元素的集合,Bloom Filter使用k个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。对任意一个元素x,第i个哈希函数映射的位置hi(x)就会被置为1(1≤i≤k)。注意,如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。在下图中,k=3,且有两个哈希函数选中同一个位置(从左边数第五位)。 在判断a是否属于这个集合时,我们对a应用k次哈希函数,如果所有hi(a)的位置都是1(1≤i≤k),那么我们就认为a是集合中的元素,否则就认为a不是集合中的元素。A如果不是集合中的元素但却被误认为是集合中的元素。这就是一个false positive。 错误率: 当k= 时错误率最低。 4.问题说明: 系统结构如图 2所示,RFID数据流产生并进入中间件。中间件中有过滤模块和应用程序在。在原始RFID数据流进入应用程序之前,它通过过滤模块去除重复RFID数据,之后应用程序接收到非重复的RFID数据。即,在过滤模块中,重复的RFID数据被去除。 RFID数据是在许多RFID阅读器中连续地产生的。 RFID阅读器发送RFID数据到中间件。因此,中间件接收到一系列的RFID数据流。 其中RFID数据包括标签识别符(即,EPC),RFID阅读器的位置,检测标签的时间。 RFID数据流定义如下: Definition1:数据流S(s1,s2,s3,……sn),si=(TagID,Loc,Time), 在RFID数据流,,如果在RFID数据流中存在γ(≠x),使得x.TagID= y.TagID 和 x.Time-y.Time≤τ,RFID数据x被认为是一个重复的,其中τ是应用程序专用的正值[2,21,22]。从冗余的定义中,我们可以通过去除重复数据找到非重复的RFID数据流。然而,有这样一种情况,即在时间间隔小于或等于τ时相同标签被探测到的RFID数据产生,找到一个非重复的RFID数据流就是混乱的。例如,一个RFID数据流S= {S1,S2,S3},其中S1 =(tag1,loc1,5),S2 =(tag 1,loc 1,10)和s3=(tag 1,loc1,15)(τ= 8)。根据s1我们知道S2是重复的,根据s2我们知道S3是重复的。然而,S连续的到达中间件的。如果S2到达中间件,我们可以先从{S1,S2}中除去s2。在这种情况下,如果S3到达中间件,因为不存在s2所以S3就判断为不是一个重复数据。(相同时间去定义重复) 根据不同的应用程序,非重复的数据流S的定义是不同的。 Definition 2:(不同时间定义重复数据)标签相同,出现时间间隔小于等于t即为重复。 Definition 3:集合S'(⊂S)为S的无重复数据集合,如果不存在x∈S',则称x在S中属于重复数据。 Definition 4:集合S'(⊂S)为S中的最大无重复集合, x∈S-S’,则称x在S中属于重复数据。 Min=|Sˆ−S| / |S’| , Max=s’。 5.time bloom filter 为了去除冗余数据,我们提出了一个基于bloom filter 的简单的time bloom filter,从definition2 中可以知道,标签相同的RFID数据不一定就是冗余数据,因此,可以用时间信息来检测冗余。Bloom filter的位数组被设置成0或者1,TBF的数组设置成RFID标签的检测时间。也就是说,TBF用整数数组而不是一个bit数组。 TBF如图三所示。它使用k个相互独立的哈希函数(h1,h2,…..hk),如BF一样映射到(0,….,m-1).TBF第i个单元的值用M[i]表示。为了存储RFID数据x,找到h1(x.TagID),⋯,hk(x.TagID)对应的k个单元,TBF将K个单元的值设置为x的检测时间x.time。如果这个单元已经被设置成之前RFID数据的检测时间,TBF用当前RFID数据的检测时间。 为了检测RFID数据x是否是冗余数据,检测h1(x.TagID),….,hk(x.TagID)对应的k个单元,如果至少存在一个单元满足x.Time−M[hi(x.TagID)]>τ,那么x不是冗余数据(因为冗余数据产生在t时间内)。在TBF中,可以将x.Time−M[hi(x.TagID)]≤t看作BF中的1,x.Time−M[hi(x.TagID)]>τ看作BF中的0。 图四是数据流基于TBF的去冗余算法,这个算法能够在有限的存储空间中找到非重复的数据,TBF所有的单元初始值为0,当RFID数据到达中间件,通过TBF 的数据将被过滤,换句话说,数据x要么被过滤掉要么被发送给应用程序。首先,当数据x通过TBF的时候,检测x.Time-M[hi(x.TagID)]≤τ for all i∈{1, 2,⋯,k}是否全部满足来判断数据x是否是重复数据。因为所有单元的初始值都是0,如果当至少有一个单元的值为0时,数据x就不是重复数据。如果 x.Time−M[hi(x.TagID)]≤τ满足对于所有的 i∈{1, 2,⋯,k}没有满足 M[hi(x.TagID)]为0的,数据x可能就是重复数据,然后将x过滤掉,将非重复数据发送给应用程序,最后 x是否是重复数据都将单元的值更新为x.time。如果我们仅当数据不是重复数据的时候更新,那么当有相同标签的数据在小于t的时间间隔内连续出现时就不能进行被过滤掉。 例子1:如图五所示,图五表示数据流S={s1, s2, s3}在通过TBF过滤之后的TBF 的状态。s1=(ID1, Loc1, 10), s2=(ID2, Loc2, 120), and s3=(ID1, Loc1, 130).TBF的数组长度为8,哈希函数个数k为3,时间间隔t为100,为了很好的解释TBF,我们假定h2(ID1)=h1(ID2)。 考虑TBF如何处理数据流S={s1, s2, s3},当s1到达TBF的时候,它将会检测s1是否是冗余数据,因为M[0],M[5],and M[2]的初始值都是0,s1不是重复数据,s1被发送给应用程序,然后TBF设置M[0],M[5],and M[2]的值为10。当s2通过TBF的时候因为没有重复数据,它被发送给应用程序,TBF设置M[5],M[7],and M[3]的值为120,对于M[5]单元,虽然之前的值10,但也被重新设置为120。s3通过TBF时,由于 130−M[0]>τ and 130−M[2]>τ,不是重复数据,s3发送到应用程序。TBF设置M[0],M[5],and M[2]的值为130.在这个例子中,因为没有数据是重复的,所以应用程序可以接受到所有的RFID数据。 TBF假阳性率(错误率): 公式: ,其中k为哈希函数个数,n’ 为在t时间内非重复的元素个数,m为单元个数。 证明:为了得到错误率,我们假定到达TBF的数据流都是非重复的,然而,在真正的应用中,RFID数据流中有很多重复数据,这些数据流的标签相同,到达时间集中因而在TBF被哈希到相同的单元,单元设置的时间值相似。因而,重复数据到达TBF后被过滤和非重复数据到达TBF导致的TBF状态是一样的。因而,假定到达TBF的数据都是非重复数据是合理的。 思考,之前有RFID数据到达TBF,现在对于任意的元素x我们想计算其假阳性率。重复数据的定义是在时间t内到达的数据,也就是说在时间t内到达TBF的数据是无效的,因此,我们只考虑相对于x.time来说t时间内的数据。 P1为x.Time−M[hi(x.TagID)]≤τ for 1≤i≤k的概率,p2为x可能为重复数据的概率。 X的假阳性概率为:x=p1-p2; 为了得到p1,我们先得到对于u的x.Time−M[u]≤τ的 概率,我们假定哈希函数有统一的分布,对于在时间t内的RFID数据y来说概率为:hi(y.TagID)≠u for some i is (1−1/m),对于所有的i∈{1, 2,…,k},hi(y.TagID)≠u的概率为 。 假定n’为在t时间内到达TBF的RFID数据的数量, 。 . 因为我们假定到达TBF的数据是非重复数据,因而,p2=0. 所有y的假阳性概率为: 。 6.time interval bloom filter . 在前面的部分,我们介绍了TBF,TBF只是BF的一个简单扩展,我们提出了TIBF去优化TBF。 如图3即为TIBF的结构,M[i].StartTime和M[i].EndTime分别表示第i个单元的开始和结束时间,开始和结束时间的初始值分别为0和-1.考虑TIBF如何保持每个单元的开始和结束时间,一个简单的解释是,假定哈希函数的个数为1,TIBF的每一个单元对应一个标签,当RFID数据x(TagID=1)首先到达时,设置TagID=1对应单元的开始和结束时间为x.time,也就是初始时间间隔是一个时间点,当另一个数据y(TagID=1)到达时,仅仅改变TagID=1对应单元的结束时间为y.time。因此,TIBF保持了TagID=1对应的时间间隔,对RFID数据x,如果有x.Time-EndTime>t, x.Time-x.StartTime>t,时间间隔没有用,在这种情况下,初始化时间间隔,设置单元的开始和结束时间为x.Time. 因为一个单元可能被不同TagID的RFID数据设置,对于每个TagID的时间间隔可能被混合,但是,单元的间隔时间[StartTime,EndTime]涉及所有跟单元有关的RFID数据的检测时间。 为了确定RFID数据x是否是冗余数据,检查h1(x),h2(x),…hk(x)对应的所有时间间隔的交集是否为空。如果接收到的RFID数据TagID=x.TagID在时间t内到达TIBF,x.TagID对应的所有时间间隔应该包括了该数据的检测时间,并且其交集应不为空。因此,当交集为空,亏确定任何RFID数据TagID=x.TagID在时间t内没有到达TIBF.. 图6说明了如何确认RFID数据x是否为冗余数据,图6中TIBF右侧坐标表示数据x对应的时间间隔,在图6(a)中,四个时间间隔的交集是[10,15]。因为交集非空,x是冗余数据。在图6(b)中,四个时间间隔的交集为空,x非冗余数据。 图7 为TIBF 去除冗余数据的算法,RFID数据x到达TIBF,首先检测x是否为冗余数据,为了检测x是否为冗余数据,首先要检查时间间隔对应的h1(x),h2(x),….,hk(x)的交集是不是空集,如果交集在x.time-t之前(即x.time-intersection.EndTime>t),我们认为他是空集,因为重复数据定义在t之间之内的,当交集不是空集并且x. Time−intersectInterval.EndTime≤τ, 则认为数据x是重复数据并且将被过滤掉,否则将会被发送给应用程序。 例子2:考虑数据流{(ID1, Loc1, 10), (ID1, Loc1, 11), (ID2, Loc2, 14), (ID3, Loc3, 15), (ID2, Loc4, 17), (ID2,Loc2, 18)}.在解释TIBF的操作之前,我们先解释数据流通过TBF后的TBF的状态,哈希函数如图8,9所示,单元数组的长度为8,k为3,t为100,为了解释TIBF,假定h1(ID1)=h2(ID4), h1(ID2)=h1(ID4), and h3(ID3)=h3(ID4).如果(ID4, Loc4,20)通过TBF,因为20−M[h1(ID4)]≤τ, 20−M[h2(ID4)]≤τ and 20−M[h3(ID4)]≤τ,TIBF将会检测到ID4为重复数据,实际上ID4不是重复数据,也就是说这里出现了假阳性错误。 同时,TIBF记录了开始时间和结束时间,如图9为上述数据流通过TIBF后TIBF的状态,如图9,考虑数据(ID4,Loc4,20)通过TIBF,ID4的三个时间间隔的集合(即[10,11], [14,18], [15])为空,因此,时间间隔过滤器将会检测到ID4为非重复数据。 对于TIBF,必须根据时间间隔的交集来计算错误率,所以很难计算,但是考虑TBF和TIBF具有相同个数单元和相同哈希函数设置,那么,对于任何RFID数据流,TIBF的错误率小于等于TBF。 理论2:考虑TBF和TIBF具有相同个数单元和相同哈希函数设置,那么,对于任何RFID数据流,TIBF的错误率小于等于TBF。 证明:假定相同的数据流通过TBF和TIBF ,当数据被检测出来是重复时就会出现假阳性错误,证明,对于任何元素x,当被TIBF检测出为重复时,被TBF检测出也为重复。 假定TIBF检测x为重复数据。 Then intersectInterval≠ϕ and x.Time−intersectInterval.EndTime ≤τ ei ≥intersectInterval.EndTime for 1≤ i≤k (also, si ≤intersectInterval:StartTime) mi = ei ≥intersectInterval.EndTime [mi is M[hi(x.TagID)] −mi≤−intersectInterval:EndTime By adding x.Time in both sides, x.Time−mi ≤ x:Time−intersectInterval:EndTime Since x.Time−intersectInterval.EndTime≤τ by Eq. (1), x.Time−mi ≤τ 所以对于任何元素x,当被TIBF检测出为重复时,被TBF检测出也为重复。 6.1空间优化: 如果TBF和TIBF的元素个数相同,那么TIBF需要的空间是TBF的两倍。但是,因为只有t以内的时间间隔有用,可以对TIBF的空间进行优化,如图10所示,时间间隔只需保存StartTime 和 EndTime−StartTime(DiffTime)代替了StartTime 和 EndTime,因为DiffTime所需空间小于EndTime。但是用如图7所示的算法运行的时候DiffTime的值可能会大于t,为了使DiffTime的值小于等于t,我们修改了算法如图11所示。StartTime和 EndTime需要保存数月,日,时,分,秒,但是t只是一个很小的值,因此保存DiffTime会大大减少了空间需求。 如图11所示,考虑空间优化后的TIBF算法,StartTime 和 DiffTime的初始值为0和-1,类似TIBF()算法,TIBF_OPT()首先检查数据x是否为重复数据,EndTime可以通过DiffTime+StartTime计算出来。 为了保证t的值小于等于t,TIBF_OPT()更新单元值的方法与TIBF()不同,当StartTime=0 或者x.Time−(StartTime+DiffTime)>τ时,设置StartTime = x.Time , DiffTime = 0,当x.Time−StartTime>τ时StartTime = x.Time−τ ,DiffTime =τ,当x.Time−StartTime≤τ时,只设置DiffTime =x.Time−StartTime。 7.参数设置。 对于TIBF,因为必须根据时间间隔的交集计算错误率,所以很难计算。但是考虑TBF和TIBF具有相同个数单元和想要哈希函数设置。对于BF,k设置为(ln2)(m/n)(n为数据个数)时,BF的错误率最低,那么,对于任何RFID数据流,TIBF的错误率小于等于TBF(理论2,证明2)。同样,k设置为(ln2)(m/n’)时,可以保证TIBF的错误率小于等于TBF。因此,对于TIBF和TBF,k值均为(ln2)(m/n’)(n’为非重复数据的个数)。 8.实验 为了验证算法的有效性,我们设计了实验进行验证。比较了BF,TBF和TIBF处理数据额能力。实验环境:3GHz PC with 1 GB 主存。 8.1数据集 由于没有一个已知的数据集,我们生成了类似的合成数据,用检测模型去模拟RFID标签检测。 如果标签和阅读器的距离过远,标签将不能被检测到,随着标签靠近阅读器,标签被检测到的概率和距离成正比,这个区域被称作次检测区域,当标签和阅读器的距离较近时,标签被检测到的概率是恒定的,这个区域被称为主检测区域。 由于我们的目的是检测去重效率,所以假定标签是直线移动的,如图13所示。标签只在出发的时候产生,然后沿着直线移动,移动速度是标签生成时随机分配的,同一时间产生的标签分配的速度相同因为在现实的应用中,标签是一起移动的,标签到达目的地后就会消失,另外,为了提高检出率在检测区内存在多个标签和多个阅读器。我们分别在有一个阅读器和三个阅读器的检测区生成了107, 2×107, 3×107, 4×107, 5×107,和6×107(10的7次方)个元组。之前的数据称为SData 之后的数据称为MData。 8.2 实验结果 8.2.1显示的是根据元数据多少的实验结果,8.2.2显示的是根据空间大小的实验结果。 如图14.15所示为根据元数组大小的实验结果,对于数据SData和MData,BF的processing rate随着元数组的增加而增大,而TBF和TIBF的processing rate恒定。但是SData的processing rate小于MData,因为MData的k值较大(k值较小,在BF中访问的单元就比较小,因而,processing rate较大)。 为了验证算法的有效性,根据元数据量对TIBF,TBF和BF的错误率进行了测评,对于SData数据,如图17所示,BF的错误率更高,对于MData数据,如图17所示,TIBF的错误率都低于0.007%,随着元数组数量的增加,错误率都随之增大。 8.2.2根据空间大小的实验结果。 给定的元组数量为4*107(10的7次方),测定根据空间大小的processing rate 和error rate。如图18,,19所示随着空间的增大,BF,BTF,BITF的processing rate减少。 对于数据SDate,BF的processing rate比BTF和BITF高很多,对于数据MDate,BF的processing rate 比TBF和TBIF也高出一些,这是因为BF中MDate的k值比BTF和BITF中的K值增加的更多。 如图20和21为根据空间大小的错误率结果。随着空间增大BF的错误率保持恒定,而TBF和TIBF的错误率降低。而TIBF的错误率比起其他算法来是最低的,因为BF不能动态改变单元的值,并且元数组的数量越少错误率越高。 8.2.3优化k值 和BF类似,k值也影响TBF和TIBF的错误率,为了测试在第7部分提出来的K值是否有效,从1到10改变k的值去测试错误率,如图22和23所示,给定的空间大小为3*107(10的7次方),元数组数量为3*107(10的7次方),单个圆圈表示用第7部分公式测试的k值,双圆圈表示用BF的公式测试的k值。 实验结果表明虽然第7部分给定的K值不是最优解,但是计算出来的错误率最低。 9.结论 通常,RFID数据流中有大量的重复数据,由于数据是以流的形式产生的,所以要在很小的存储空间上去除重复数据是很困难的,因此,我们提出了近似去重方法,基于BF的TBF和TIBF。根据RFID数据中重复的定义是和检测时间有关的,我们用时间信息的整数代替了BF中的bit,为了降低错误率,TIBF用了时间间隔。最后,实验结果表明,新提出的算法去重的错误率更低。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值