假设我有不同水果的来源,我想将他们的计数插入数据库。
我可以这样做:
Flow[Fruits]
.map { item =>
insertItemToDatabase(item)
}
但这显然很慢 - 为什么在我可以将它们分组时插入每个项目的数据库?所以我想出了一个更好的解决方案:
Flow[Fruits]
.grouped(10000)
.map { items =>
insertItemsToDatabase(items)
}
但这意味着我必须在内存中保存10 000个元素[banana, orange, orange, orange, banana, ...]
,直到它们被刷新到数据库。这不是效率低下的吗?也许我可以这样做:
Flow[Fruits]
.grouped(100)
.map { items =>
consolidate(items) // this will return Map[String, Int]
}
.grouped(100)
// here I have Seq[Map[String, Int]]
.map { mapOfItems=>
insertMapToDatabase(mapOfItems)
}
根据我的理解,这也应该同时处理10 000个元素,但不应占用尽可能多的内存(假设元素经常重复)。但是每个键在内存中仍然重复100次。当然,我可以做.grouped(10).map().grouped(10).map().grouped(10).map().grouped(10).map()
......但是,有没有更好的方法?也许是这样的:
Flow[Fruits]
.map { item =>
addToMap(item)
if(myMap.length == 10000) {
insertToDatabase(myMap)
clearMyMap()
}
}
但它是否打破了Akka流的概念,即处理阶段的独立性(以及因此并发性)?
答案 0 :(得分:2)
如果Fruit
集的基数较低,那么您可以保留一个包含所有计数的单一映射,然后在流式传输所有Fruit值后将其刷新到数据库。
首先,构建一个保持运行计数的Flow:
type Count = Int
type FruitCount = Map[Fruit, Count]
val zeroCount : FruitCount =
Map.empty[Fruit, Count] withDefaultValue 0
val appendFruitToCount : (FruitCount, Fruit) => FruitCount =
(fruitCount, fruit) => fruitCount + (fruit -> fruitCount(fruit) + 1)
val fruitCountFlow : Flow[Fruit, FruitCount, NotUsed] =
Flow[Fruit].scan(zeroCount)(appendFruitToCount)
现在创建一个接收最后FruitCount
的接收器并实现流:
val lastFruitCountSink : Sink[FruitCount, _] = Sink.lastOption[FruitCount]
val fruitSource : Source[Fruit, NotUsed] = ???
val lastFruitCountFut : Future[Option[FruitCount]] =
fruitSource
.via(fruitCountFlow)
.to(lastFruitCountSink)
.run()
然后可以使用lastFruitCountFut
将值发送到数据库:
lastFruitCountFut foreach (_ foreach (_ foreach { (fruit, count) =>
insertItemsToDatabase( Iterator.fill(count)(fruit) )
}))
使用Iterator
,因为它是用于构建TraversableOnce
水果项目的内存效率最高的集合。
此解决方案仅在内存中保留1 Map
,每个不同的Fruit类型& 1每个键的整数。