13. 状态的底层存储——CopyOnWriteStateMap

一、状态的底层存储——CopyOnWriteStateMap概述

不管是ValueState、ListState、MapState,它们的底层存储都是用的StateTable<K, N, S>,而StateTable的底层存储又是StateMap<K, N, S>的数组,每个StateMap代表一个keyGroup。在获取元素时,先根据key计算出来它所属的keyGroup,拿到对应的StateMap,再通过map.get(key)的方式获取到对应的元素。

在这里插入图片描述

K:keyBy时用的key

N:namespace, 用于区分窗口。

假设需要统计 app1 和 app2 每个小时的 pv 指标,则需要使用小时级别的窗口。状态引擎为了区分 app1 在 7 点和 8 点的 pv 值,就必须新增一个维度用来标识窗口。

Flink 用 Namespace 来标识窗口,这样就可以在状态引擎中区分出 app1 在 7 点和 8 点的状态信息。

S:存储的值,对于ValueState来说,里面就是一个值;对于ListState来说,里面是一个ArrayList;对于MapState来说,里面是一个HashMap。

所以,Flink的状态的真实数据其实是存储在StateMap中的,我们这里重点研究StateMap。

StateMap是一个抽象类,它有两个实现类:CopyOnWriteStateMap和NestedStateMap(目前还没看到NestedStateMap的用处,先重点介绍CopyOnWriteStateMap)

CopyOnWriteStateMap的实现类似于HashMap,数组+链表,每个元素是StateMapEntry。CopyOnWriteStateMap用的是数组+链表,而不像HashMap一样采用数组+链表+红黑树的结构,是因为CopyOnWriteStateMap在插入元素时只有头插法,无法构造树形结构。

在做checkpoint的时候把数据复制到新数组中,而在put数据的时候,可能会导致新数组中的数据被重写,所以不能直接修改,而要复制出来。

CopyOnWriteMap相比于普通的HashMap,它实现了两个功能:

  1. 渐进式rehash,即不是一下把数据全部迁移到新的hash表,而是慢慢去迁移数据到新的hash表中

    渐进式rehash允许哈希表在进行扩展或收缩时,保持对现有数据的可用性。这意味着在进行rehash的过程中,哈希表可以同时处理新旧键值对的插入、查找和删除操作,而不会中断服务。这对于需要保持高可用性和持续操作的实时应用程序特别有用。

    对于普通hash表的一次性rehash,它在rehash的时候,会把线程占住一直等它做完,如果hash表中的数据很大,这个过程会很长

  2. Checkpoint 时 CopyOnWriteStateMap 支持异步快照,即Checkpoint在做快照的同时,用户仍然对CopyOnWriteStateMap中数据进行修改,那么需要解决的问题是:数据修改了,怎么保证快照数据的准确性呢。

    这里同时涉及到checkpoint时的snapshot复制机制

二、渐进式rehash策略

2.1 简述

CopyOnWriteMap中有2个数组,一个是primaryTable,作为主数组;另一个是incrementalRehashTable(简称rehashTable),当primaryTable的元素大于theshhold时,就创建一个2*capacity的rehashTable,然后在每次增、删、改、查的时候都将primaryTable中的一部分元素rehash到rehashTable。

rehash时有一个规律,假设primaryTable的长度是4,rehashTable的长度为8,那么 primaryTable 中 0 位置上的元素会迁移到 rehashTable 的 0 和 4 位置上,同理 primaryTable 中 1 位置上的元素会迁移到 rehashTable 的 1 和 5 位置上,如下图所示。

这是因为当数组长度为2的倍数时,pos = hashcode & (length - 1),t = length - 1是一个二级制都为1的数,数组长度扩大2倍,相当于在t的二进制前面加了一个1,所以如果hashcode的二进制第3位如果是0,那么pos在rehash之后不变,如果是1,那么在rehash之后加4。

在这里插入图片描述

上面说到了“一部分”,那这个一部分到底是多少呢?

看源码可以看到,当数组中元素充足时,那就迁移MIN_TRANSFERRED_PER_INCREMENTAL_REHASH(默认是4)个桶中的元素;如果已经到头了,就退出。注意:这里说的是桶,而不是个,所以迁移过程的时间长短,也要取决于桶中的元素个数。

在这里插入图片描述

2.2 当同时存在2个桶时,在增删改查时要用哪个

在这里插入图片描述

依据于计算出来的位置pos与rehashIndex的大小进行比较,rehashIndex是记录的是当前在primarytTable中rehash到的位置,如果pos >= rehashIndex,说明该元素还未迁移;如果pos < rehashIndex,说明已迁移,该去rehashTable中去找。

三、CopyOnWrite机制

3.1 StateMap的Snapshot策略

  1. 如果Snapshot时不在扩容

    如果在做snapshot时,map不在扩容阶段,那么就把primaryTable中的引用直接复制一份到snapshotData中,注意:这里复制的只是primaryTable中的引用,而不是实际的数据,且都是头节点的引用,因为用头节点就可以拿到后面的数据了

在这里插入图片描述

  1. 如果Snapshot时在扩容

    是从三部分复制到snapshotData的

    i. primaryTable的[rehashIndex, length),即没有被rehash的数

    ii.rehashTable的 [0,rehashIndex)

    iii.rehashTable的 [length >>> 1, length >>> 1 + rehashIndex)

    ii 和 iii 加起来为全部已经被rehash的数,如上面的rehash策略所说,pos=0的数据被rehash到0和4上,pos=1的数据被rehash到1和5上

在这里插入图片描述

3.2 增删改查流程

3.2.1 修改元素

如果是头节点,就直接修改头节点的引用;如果是中间节点,就复制目标节点及目标节点以前的所有元素

解释:

修改头节点

在这里插入图片描述

修改中间节点

一个错误的思想:

在这里插入图片描述

如上图,如果我们想修改节点b,那么只把b复制出来一份,然后让a指向b copy,这样成立吗?显然是不成立的,a的指向只能有一个,要么是b,要么是b copy

正确的做法应该是把b及b以前的元素全部复制,如下图

在这里插入图片描述

3.2.2 查找元素

为了防止在查找以后把Value的引用修改,所以查找也要做深拷贝,所以查找和修改的处理方法是一样的,把目标节点以前的所有元素都复制出来

在这里插入图片描述

3.2.3 增加元素

​ 看了上面的修改和查找流程,我们应该对CopyOnWrite有了理解,CopyOnWrite就是在只要Map中的元素有可能被修改,那么它就要进行复制,所以增加元素为了避免复制的开销,它只会在头节点添加元素。我理解这同时也是CopyOnWriteStateMap为什么只是数组+链表的结构,而不像HashMap一样还有数组+红黑树的优化。

在这里插入图片描述

3.2.4 删除元素

删除流程和修改流程其实一样,如果是头节点,就直接修改引用,如果是中间节点,复制该节点以前的所有元素

删除头节点,直接修改头节点所在桶的引用

在这里插入图片描述

删除中间节点,复制该节点以前的所有元素
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值