【Flow进阶篇二】SharedFlow 缓存机制深度解析

1. 缓存机制的目的

SharedFlow 作为热流(hot flow),在多消费者环境下运行时,需要一个高效的缓存机制来协调数据的存储与分发。其主要目标包括:

  • 数据重播(Replay) :允许新加入的消费者接收之前已经发送的事件,避免数据丢失。
  • 数据缓冲(Buffering) :在消费者消费速率不同的情况下,缓存区确保数据不会丢失,保证慢消费者能够获取数据。
  • 溢出控制(Overflow Handling) :在缓存区满时,SharedFlow 通过不同策略(丢弃、挂起)来保证系统的稳定性。

2. SharedFlow 的缓存结构

从源码看出,SharedFlow 的缓存数据结构如下:

sql 复制代码
                  buffered values
             /-----------------------\
                          replayCache      queued emitters
                          /----------/----------------------\
         +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
         |   | 1 | 2 | 3 | 4 | 5 | 6 | E | E | E | E | E | E |   |   |   |
         +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
               ^           ^           ^                      ^
               |           |           |                      |
              head         |      head + bufferSize     head + totalSize
               |           |           |
     index of the slowest  |    index of the fastest
      possible collector   |     possible collector
               |           |
               |     replayIndex == new collector's index
               ---------------------- /
          range of possible minCollectorIndex

          head == minOf(minCollectorIndex, replayIndex) // by definition
          totalSize == bufferSize + queueSize // by definition

关键概念解析

  1. head:缓存区的起始位置,指向当前最慢消费者或新加入消费者的数据起点。
  2. bufferSize:缓存的最大容量,决定了 SharedFlow 可以存储的数据量。
  3. replayIndex:用于新加入的消费者,决定它们从哪里开始接收数据。
  4. minCollectorIndex:最慢消费者的位置,它决定了缓存数据的最小索引。
  5. totalSize:整个缓存区的大小,由 bufferSize 和 queueSize 之和决定。
  6. queued emitters:所有等待消费数据的消费者队列。

3. 缓存机制运作解析

3.1 数据存储

SharedFlow 采用 环形缓冲区(Circular Buffer) 来存储数据,其主要存储区域包括 replayCachequeued emitters 两部分:

3.1.1 replayCache(历史数据缓存)

  • replayCache 是 SharedFlow 用于 存储历史数据 的部分,它的大小由 replay 参数决定。
  • 当新消费者加入时,会从 replayCache 读取历史数据,以确保它们不会错过重要事件。
  • replayIndex 决定了新加入的消费者应该从哪里开始接收数据。

3.1.2. queued emitters(等待消费的队列)

  • queued emitters 存储 尚未被消费者消费的数据,可以看作是一个队列结构。
  • 它的大小由 extraBufferCapacity 决定,影响 当所有消费者处理速度慢于生产速度时,最多可以缓存多少数据
  • 这个部分的数据会被当前的活跃消费者 按顺序消费,确保数据不会丢失。
  • bufferSize 决定了 replayCachequeued emitters 的总大小,即 totalSize = bufferSize + queueSize

3.1.3. 存储过程

  1. 当新的数据进入 SharedFlow:
    • 如果 replayCache 未满 ,则数据会先存入 replayCache

    • 如果 replayCache 已满 ,那么数据会进入 queued emitters(如果 extraBufferCapacity 允许)。

    • 如果 queued emitters 也满了,那么 SharedFlow 会根据 onBufferOverflow 策略决定如何处理:

      • SUSPEND:挂起新的 emit 操作,直到有空间释放。
      • DROP_OLDEST:丢弃最旧的数据,腾出空间存放新数据。
      • DROP_LATEST:丢弃新数据,维持原有缓存内容不变。

3.2 消费者的消费过程

  • 慢消费者 :其索引 minCollectorIndex 低于最新数据索引,意味着它仍在消费较早的数据。
  • 快消费者:可以快速处理数据,处于最新数据的索引位置。

3.3 数据重播

  • 当新消费者订阅时,从 replayCache 获取历史数据,其起始位置由 replayIndex 确定。

3.4 缓存溢出

  • 挂起生产者(SUSPEND)(默认策略):等待消费者处理完数据后再继续生产。
  • 丢弃旧数据(DROP_OLDEST):如果缓存已满,最旧的数据会被移除。
  • 丢弃最新数据(DROP_LATEST):如果缓存已满,最新的数据会被丢弃。

4. 代码示例

4.1缓存场景1

kotlin 复制代码
fun main() = runBlocking {
    val sharedFlow = MutableSharedFlow<Int>(
        replay = 3, //缓存最近3个数据,供新加入的消费者重播
        extraBufferCapacity = 0, //额外缓存2个数据,用于缓解生产者与消费者速率不一致的情况
        onBufferOverflow = BufferOverflow.DROP_OLDEST //DROP_OLDEST:当缓存满后,丢弃最旧的数据,为新数据腾出空间
    )

    // 生产者协程:每隔100ms发送一个数据
    launch {
        for (i in 1..10) {
            println("${System.currentTimeMillis()} Emit: $i")
            sharedFlow.emit(i)
            delay(100L)
        }
    }

    // 第一个消费者:较快消费数据
    launch {
        // 延迟启动,以便部分数据已在 replayCache 中
        delay(350L)
        sharedFlow.collect { value ->
            println("${System.currentTimeMillis()} 快速消费者接收到: $value")
            delay(150L) // 模拟较快的消费速度
        }
    }

    // 第二个消费者:较慢消费数据
    launch {
        // 延迟启动,让其错过一部分最新数据,但会通过 replayCache 补上历史数据
        delay(500L)
        sharedFlow.collect { value ->
            println("${System.currentTimeMillis()} 慢速消费者接收到: $value")
            delay(300L) // 模拟较慢的消费速度
        }
    }
    delay(10000)  // 让所有协程有足够时间运行
}

运行结果示例:

yaml 复制代码
1740973705932 Emit: 1
1740973706053 Emit: 2
1740973706160 Emit: 3
1740973706271 Emit: 4   -> replayCache = [2, 3, 4]
1740973706316 快速消费者接收到: 2  ->收到replayCache的第一个数据2
1740973706382 Emit: 5    ->-   由于 `extraBufferCapacity = 0`,当 **新的数据 5 进入时,旧数据 2 被丢弃(DROP_OLDEST机制)。此时 replayCache =[3, 4, 5] 。
1740973706447 慢速消费者接收到: 3   ->收到replayCache =[3, 4, 5]的第一条
1740973706477 快速消费者接收到: 3
1740973706493 Emit: 6
1740973706605 Emit: 7
1740973706637 快速消费者接收到: 5  ->同理DROP_OLDEST机制,replayCache = [5, 6, 7],所以收到的是5而不是4
1740973706715 Emit: 8
1740973706763 慢速消费者接收到: 6
1740973706795 快速消费者接收到: 6
1740973706827 Emit: 9
1740973706938 Emit: 10
1740973706954 快速消费者接收到: 8
1740973707066 慢速消费者接收到: 8
1740973707113 快速消费者接收到: 9
1740973707273 快速消费者接收到: 10
1740973707368 慢速消费者接收到: 9
1740973707671 慢速消费者接收到: 10

结论

  • ReplayCache 只存 replay 个数据,与有多少个订阅者无关,它的作用仅限于:

    • 新订阅者 能够获取历史数据。
    • 旧数据满了会被丢弃,但不会因为有多个订阅者而存更多数据。
  • 不同订阅者的消费速率互不影响,慢速消费者可以补上 ReplayCache 中的历史数据,而快速消费者仍然可以继续接收新数据。

这证明 ReplayCache 只是一个固定大小的缓存,而不是为每个订阅者单独存储数据。

  • onBufferOverflow(DROP_OLDEST) 生效

    • 由于 extraBufferCapacity = 0,SharedFlow 只能缓存 replay = 3 个数据,导致 旧数据不断被丢弃

4.2缓存场景2

现在优化下上场景1,生产者能够继续 emit 而不会因为缓冲区满而丢弃数据:

ini 复制代码
val sharedFlow = MutableSharedFlow<Int>(
    replay = 3,
    extraBufferCapacity = 2,  // 额外缓存2个数据,用于缓解生产者与消费者速率不一致的情况
    onBufferOverflow = BufferOverflow.SUSPEND  // 让 emit 挂起,而不是丢弃数据
)

运行结果示例:

yaml 复制代码
1740982283097 Emit: 1
1740982283209 Emit: 2
1740982283318 Emit: 3
1740982283430 Emit: 4  -> replayCache = [2, 3, 4]
1740982283476 快速消费者接收到: 2   ->快消费者收到replayCache第一条数据
1740982283541 Emit: 5  -> replayCache = [3, 4,5]
1740982283606 慢速消费者接收到: 3   ->慢消费者收到replayCache第一条数据
1740982283636 快速消费者接收到: 3
1740982283652 Emit: 6
1740982283763 Emit: 7
1740982283795 快速消费者接收到: 4
1740982283874 Emit: 8
1740982283920 慢速消费者接收到: 4
1740982283952 快速消费者接收到: 5
1740982283984 Emit: 9
1740982284094 Emit: 10
1740982284109 快速消费者接收到: 6
1740982284221 慢速消费者接收到: 5
1740982284269 快速消费者接收到: 7
1740982284426 快速消费者接收到: 8
1740982284522 慢速消费者接收到: 6
1740982284586 快速消费者接收到: 9
1740982284743 快速消费者接收到: 10
1740982284836 慢速消费者接收到: 7
1740982285138 慢速消费者接收到: 8
1740982285440 慢速消费者接收到: 9
1740982285742 慢速消费者接收到: 10
  • extraBufferCapacity=2 可以replayCache满了之后可以缓存 2 个额外的数据,使得即使消费者较慢,生产者也能继续发送数据,而不会丢弃。注意:生效前提是这个订阅者处于活跃的状态下
  • SUSPENDemit 挂起,确保所有数据被处理.ReplayCache + extraBufferCapacity 满时,生产者 emit 会被挂起,等待消费者消费数据后再继续。这里就是等待慢消费者处理完了才会继续emit
  • 日志显示,数据都成功被消费,没有丢失!

5.SharedFlow 缓存机制总结

SharedFlow 作为一种热流(hot flow),在多消费者环境下运行时,其缓存机制旨在提供 数据重播(Replay)、数据缓冲(Buffering) 以及 溢出控制(Overflow Handling) ,以确保数据的可靠性和系统的稳定性。

1. Replay 机制

  • replayCache 负责存储最近 replay 个数据,仅用于新加入的消费者,以保证它们能够获取历史数据。
  • replayCachereplay 参数控制,与订阅者数量无关,不会因订阅者增加而存储额外数据
  • 旧数据会被丢弃(FIFO 机制),但不会影响已接收数据的消费者。

2. Extra Buffer 机制

  • extraBufferCapacity 允许 SharedFlow 额外缓存数据,以缓解生产者与消费者速率不匹配的情况。
  • replayCache 已满,新的数据会进入 extraBuffer ,但只有 活跃消费者 存在时才会生效。
  • 生产者速度 > 消费者速度 时,extraBuffer 能够暂存部分数据,避免数据丢失。

3. 消费者速度对缓存的影响

  • 慢速消费者 可能会落后,但仍然能够通过 replayCache 补上历史数据。
  • 快速消费者 始终获取最新数据,不受慢速消费者的影响。
  • 多消费者情况下,SharedFlow 仅保留最慢消费者仍需的数据,最早消费完的消费者不会影响数据缓存策略。

4. 缓存溢出控制

当缓存区满时,SharedFlow 提供三种策略:

  1. SUSPEND (默认):挂起 emit 操作,直到有空余空间(适用于保证数据完整性的场景)。
  2. DROP_OLDEST:丢弃最旧的数据,腾出空间存放新数据(适用于只关注最新数据的场景)。
  3. DROP_LATEST:丢弃新数据,不影响已有数据(适用于优先保证已有数据不被覆盖的场景)。

5. 关键结论

  • Replay 机制不会为每个订阅者单独存储数据,它只决定新订阅者可以看到多少历史数据。
  • 额外缓存(extraBufferCapacity)只在有活跃订阅者时生效,否则数据仍可能丢失。
  • 不同订阅者的消费速率互不影响,但整体缓存区受限于最慢消费者的进度。
  • 合理搭配 replayextraBufferCapacityonBufferOverflow,可以优化 SharedFlow 在不同应用场景下的表现
  • 这套缓存机制使得 SharedFlow 既能适应 低延迟、高吞吐 的场景,又能满足 历史数据回放 的需求。
相关推荐
移动开发者1号15 分钟前
Kotlin协程与响应式编程深度对比
android·kotlin
tq10862 小时前
使用协程简化异步资源获取操作
kotlin·结构化并发
花花鱼10 小时前
android studio 设置让开发更加的方便,比如可以查看变量的类型,参数的名称等等
android·ide·android studio
alexhilton11 小时前
为什么你的App总是忘记所有事情
android·kotlin·android jetpack
刘龙超13 小时前
如何应对 Android 面试官 -> 玩转 Jetpack DataBinding
android jetpack
AirDroid_cn14 小时前
OPPO手机怎样被其他手机远程控制?两台OPPO手机如何相互远程控制?
android·windows·ios·智能手机·iphone·远程工作·远程控制
尊治14 小时前
手机电工仿真软件更新了
android
xiangzhihong817 小时前
使用Universal Links与Android App Links实现网页无缝跳转至应用
android·ios
车载应用猿18 小时前
基于Android14的CarService 启动流程分析
android
没有了遇见18 小时前
Android 渐变色实现总结
android