【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 既能适应 低延迟、高吞吐 的场景,又能满足 历史数据回放 的需求。
相关推荐
胖虎17 小时前
Android 布局系列(五):GridLayout 网格布局的使用
android·网格布局·安卓布局·gridlayout
云泽野7 小时前
Pytest之parametrize参数化
android·python·pytest
野有蔓草W8 小时前
Android实现漂亮的波纹动画
android·java
万户猴9 小时前
【Flow进阶篇一】SharedFlow 入门:冷流 vs. 热流的区别与基础用法
android·kotlin
李大圣的博客10 小时前
使用binlog2sql来恢复mysql误删除数据
android
奥顺11 小时前
PHP函数与类:面向对象编程实践指南
android·开发语言·mysql·开源·php
tangweiguo0305198711 小时前
Kotlin 5种单例模式
javascript·单例模式·kotlin
向上的车轮11 小时前
40岁开始学Java:Java中单例模式(Singleton Pattern),适用场景有哪些?
android·java·单例模式
人民的石头13 小时前
Android 系统 AMS(ActivityManagerService)
android