这里有一个简单的场景。我使用akka流来读取kafka并写入外部源代码,在我的情况下:cassandra。
Akka溪流(reactive-kafka)图书馆为我提供了背压和其他漂亮的东西,使这成为可能。
kafka是一个Source而Cassandra是一个Sink,当我收到大量事件时,例如通过Kafka这里的cassandra查询应该按顺序执行(例如:它可以是INSERT,UPDATE和DELETE并且必须是顺序的。
我不能使用mayAsync
并执行这两个语句,Future非常渴望并且有可能在INSERT之前首先执行DELETE或UPDATE。
我被迫使用Cassandra的execute
而不是executeAsync
,这是非阻塞的。
没有办法为这个问题制作一个完整的异步解决方案,但是有一个更优雅的方法来做到这一点?
对于ex:使Future变得懒惰和顺序,并将其卸载到不同的执行上下文中。
mapAsync
也提供并行选项。
Monix Task可以在这里提供帮助吗?
这是一个一般性的设计问题,可以采取哪些方法。
更新:
Flow[In].mapAsync(3)(input => {
input match {
case INSERT => //do insert - returns future
case UPDATE => //do update - returns future
case DELETE => //delete - returns future
}
情景稍微复杂一些。可能有成千上万的插入,更新和删除,以便特定的密钥(在卡夫卡) 理想情况下,我希望按顺序执行单个键的3个期货。我相信Monix的任务可以提供帮助吗?
答案 0 :(得分:1)
如果处理并行度为1的事物,它们将按严格顺序执行,这将解决您的问题。
但那并不有趣。如果需要,您可以并行运行不同 键 的操作 - 如果不同键的处理是独立的,我可以根据您的描述进行处理。为此,您必须缓冲传入的值,然后重新组合它。我们来看一些代码:
import monix.reactive.Observable
import scala.concurrent.duration._
import monix.eval.Task
// Your domain logic - I'll use these stubs
trait Event
trait Acknowledgement // whatever your DB functions return, if you need it
def toKey(e: Event): String = ???
def processOne(event: Event): Task[Acknowledgement] = Task.deferFuture {
event match {
case _ => ??? // insert/update/delete
}
}
// Monix Task.traverse is strictly sequential, which is what you need
def processMany(evs: Seq[Event]): Task[Seq[Acknowledgement]] =
Task.traverse(evs)(processOne)
def processEventStreamInParallel(source: Observable[Event]): Observable[Acknowledgement] =
source
// Process a bunch of events, but don't wait too long for whole 100. Fine-tune for your data source
.bufferTimedAndCounted(2.seconds, 100)
.concatMap { batch =>
Observable
.fromIterable(batch.groupBy(toKey).values) // Standard collection methods FTW
.mapAsync(3)(processMany) // processing up to 3 different keys in parallel - tho 3 is not necessary, probably depends on your DB throughput
.flatMap(Observable.fromIterable) // flattening it back
}
此处concatMap
运算符将确保您的块也按顺序处理。因此,即使一个缓冲区具有key1 -> insert, key1 -> update
而另一个缓冲区具有key1 -> delete
,也不会产生任何问题。在Monix中,这与flatMap
相同,但在其他Rx库中flatMap
可能是mergeMap
的别名,它没有排序保证。
这也可以用Future
来完成,因为没有标准的“顺序遍历”,所以你必须自己动手,比如:
def processMany(evs: Seq[Event]): Future[Seq[Acknowledgement]] =
evs.foldLeft(Future.successful(Vector.empty[Acknowledgement])){ (acksF, ev) =>
for {
acks <- acksF
next <- processOne(ev)
} yield acks :+ next
}
答案 1 :(得分:1)
您可以使用akka-streams子流,按键分组,然后合并子流,如果您想对从数据库操作中获得的内容做一些事情:
def databaseOp(input: In): Future[Out] = input match {
case INSERT => ...
case UPDATE => ...
case DELETE => ...
}
val databaseFlow: Flow[In, Out, NotUsed] =
Flow[In].groupBy(Int.maxValues, _.key).mapAsync(1)(databaseOp).mergeSubstreams
请注意,输入源的顺序不会像mapAsync
中那样保留在输出中,但同一个键上的所有操作仍然是有序的。
答案 2 :(得分:0)
您正在寻找Future.flatMap
:
def doSomething: Future[Unit]
def doSomethingElse: Future[Unit]
val result = doSomething.flatMap { _ => doSomethingElse }
执行第一个函数,然后,当满足Future
时,启动第二个函数。 result
是一个新的Future
,它在第二次执行的结果满足时完成。
第一个未来的结果将传递给您给.flatMap
的函数,因此第二个函数可以取决于第一个函数的结果。例如:
def getUserID: Future[Int]
def getUser(id: Int): Future[User]
val userName: Future[String] = getUserID.flatMap(getUser).map(_.name)
您也可以将其写为for-comprehension
:
for {
id <- getUserID
user <- getUser(id)
} yield user.name