- 【Flow进阶篇一】SharedFlow 入门:冷流 vs. 热流的区别与基础用法 -【Flow进阶篇二】SharedFlow 缓存机制深度解析
- 【Flow进阶篇三】SharedFlow 与 StateFlow 的详细对比及适用场景
- SharedFlow 的订阅模式:单播 vs. 多播 vs. 重播(Replay)
- 详细分析 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
关键概念解析
- head:缓存区的起始位置,指向当前最慢消费者或新加入消费者的数据起点。
- bufferSize:缓存的最大容量,决定了 SharedFlow 可以存储的数据量。
- replayIndex:用于新加入的消费者,决定它们从哪里开始接收数据。
- minCollectorIndex:最慢消费者的位置,它决定了缓存数据的最小索引。
- totalSize:整个缓存区的大小,由 bufferSize 和 queueSize 之和决定。
- queued emitters:所有等待消费数据的消费者队列。
3. 缓存机制运作解析
3.1 数据存储
SharedFlow 采用 环形缓冲区(Circular Buffer) 来存储数据,其主要存储区域包括 replayCache 和 queued emitters 两部分:
3.1.1 replayCache(历史数据缓存)
replayCache
是 SharedFlow 用于 存储历史数据 的部分,它的大小由replay
参数决定。- 当新消费者加入时,会从
replayCache
读取历史数据,以确保它们不会错过重要事件。 replayIndex
决定了新加入的消费者应该从哪里开始接收数据。
3.1.2. queued emitters(等待消费的队列)
queued emitters
存储 尚未被消费者消费的数据,可以看作是一个队列结构。- 它的大小由
extraBufferCapacity
决定,影响 当所有消费者处理速度慢于生产速度时,最多可以缓存多少数据。 - 这个部分的数据会被当前的活跃消费者 按顺序消费,确保数据不会丢失。
bufferSize
决定了replayCache
和queued emitters
的总大小,即totalSize = bufferSize + queueSize
。
3.1.3. 存储过程
- 当新的数据进入 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 个额外的数据,使得即使消费者较慢,生产者也能继续发送数据,而不会丢弃。注意:生效前提是这个订阅者处于活跃的状态下
SUSPEND
让emit
挂起,确保所有数据被处理.当ReplayCache + extraBufferCapacity
满时,生产者emit
会被挂起,等待消费者消费数据后再继续。这里就是等待慢消费者处理完了才会继续emit- 日志显示,数据都成功被消费,没有丢失!
5.SharedFlow 缓存机制总结
SharedFlow 作为一种热流(hot flow),在多消费者环境下运行时,其缓存机制旨在提供 数据重播(Replay)、数据缓冲(Buffering) 以及 溢出控制(Overflow Handling) ,以确保数据的可靠性和系统的稳定性。
1. Replay 机制
replayCache
负责存储最近replay
个数据,仅用于新加入的消费者,以保证它们能够获取历史数据。replayCache
受replay
参数控制,与订阅者数量无关,不会因订阅者增加而存储额外数据。- 旧数据会被丢弃(FIFO 机制),但不会影响已接收数据的消费者。
2. Extra Buffer 机制
extraBufferCapacity
允许 SharedFlow 额外缓存数据,以缓解生产者与消费者速率不匹配的情况。- 当
replayCache
已满,新的数据会进入 extraBuffer ,但只有 活跃消费者 存在时才会生效。 - 生产者速度 > 消费者速度 时,
extraBuffer
能够暂存部分数据,避免数据丢失。
3. 消费者速度对缓存的影响
- 慢速消费者 可能会落后,但仍然能够通过
replayCache
补上历史数据。 - 快速消费者 始终获取最新数据,不受慢速消费者的影响。
- 多消费者情况下,SharedFlow 仅保留最慢消费者仍需的数据,最早消费完的消费者不会影响数据缓存策略。
4. 缓存溢出控制
当缓存区满时,SharedFlow 提供三种策略:
- SUSPEND (默认):挂起
emit
操作,直到有空余空间(适用于保证数据完整性的场景)。 - DROP_OLDEST:丢弃最旧的数据,腾出空间存放新数据(适用于只关注最新数据的场景)。
- DROP_LATEST:丢弃新数据,不影响已有数据(适用于优先保证已有数据不被覆盖的场景)。
5. 关键结论
- Replay 机制不会为每个订阅者单独存储数据,它只决定新订阅者可以看到多少历史数据。
- 额外缓存(extraBufferCapacity)只在有活跃订阅者时生效,否则数据仍可能丢失。
- 不同订阅者的消费速率互不影响,但整体缓存区受限于最慢消费者的进度。
- 合理搭配
replay
、extraBufferCapacity
、onBufferOverflow
,可以优化 SharedFlow 在不同应用场景下的表现。 - 这套缓存机制使得 SharedFlow 既能适应 低延迟、高吞吐 的场景,又能满足 历史数据回放 的需求。