Future的执行顺序 - 在db非阻塞

时间:2018-01-24 12:07:55

标签: scala cassandra functional-programming apache-kafka akka-stream

这里有一个简单的场景。我使用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的任务可以提供帮助吗?

3 个答案:

答案 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