業(yè)務(wù)流程是服務(wù)的主要用例之一,你是否嘗試以盡可能多的并發(fā)方式編排多個(gè)服務(wù)調(diào)用,因此可以獲得盡可能好的響應(yīng)時(shí)間,或者你嘗試進(jìn)行多個(gè)業(yè)務(wù)操作,數(shù)據(jù)寫入,數(shù)據(jù)讀取, 服務(wù)調(diào)用等等,依賴于彼此等等。簡潔地描述您的業(yè)務(wù)邏輯的能力對(duì)于使服務(wù)易于理解和維護(hù)至關(guān)重要。業(yè)務(wù)流程DSL——squbs-pattern的一部分——將使異步代碼易于讀寫和推理。
依賴
業(yè)務(wù)流DSL是squbs-pattern的一部分。請(qǐng)?zhí)砑尤缦乱蕾嚕?/p>
"org.squbs" %% "squbs-pattern" % squbsVersion,
"com.typesafe.akka" %% "akka-contrib" % akkaVersion
開始
讓我們開始一個(gè)簡單但完整的業(yè)務(wù)流程示例。這個(gè)業(yè)務(wù)流程由3個(gè)相互關(guān)聯(lián)的異步任務(wù)構(gòu)成:
- 加載請(qǐng)求此業(yè)務(wù)流程的用戶。
- 加載項(xiàng)目。詳細(xì)信息依賴于查看的用戶。
- 生成項(xiàng)目視圖,基于用戶和項(xiàng)目數(shù)據(jù)。
讓我們深入到流程和細(xì)節(jié):
// 1. Define the orchestrator actor.
class MyOrchestrator extends Actor with Orchestrator {
// 2. Provide the initial expectOnce block that will receive the request message.
expectOnce {
case r: MyOrchestrationRequest => orchestrate(sender(), r)
}
// 3. Define orchestrate - the orchestration function.
def orchestrate(requester: ActorRef, request: MyOrchestrationRequest) {
// 4. Compose the orchestration flow using pipes (>>) as needed by the business logic.
val userF = loadViewingUser
val itemF = userF >> loadItem(request.itemId)
val itemViewF = (userF, itemF) >> buildItemView
// 5. Conclude and send back the result of the orchestration.
for {
user <- userF
item <- itemF
itemView <- itemViewF
} {
requester ! MyOrchestrationResult(user, item, itemView)
context.stop(self)
}
// 6. Make sure to stop the orchestrator actor by calling
// context.stop(self).
}
// 7. Implement the orchestration functions as in the following patterns.
def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
val itemPromise = OPromise[Option[Item]]
context.actorOf(Props[ItemActor]) ! ItemRequest(itemId, seller.id)
expectOnce {
case item: Item => itemPromise success Some(item)
case e: NoSuchItem => itemPromise success None
}
itemPromise.future
}
def loadViewingUser: OFuture[Option[User]] = {
val userPromise = OPromise[Option[User]]
...
userPromise.future
}
def buildItemView(user: Option[User], item: Option[Item]): OFuture[Option[ItemView]] = {
...
}
}
依賴
添加如下依賴:
"org.squbs" %% "squbs-pattern" % squbsVersion
核心概念
業(yè)務(wù)流程
Orchestrator是由actor繼承用于支持業(yè)務(wù)流程功能的特質(zhì)。從技術(shù)上講, 它是聚合器的子特性, 并提供其所有功能。從技術(shù)上講, 它是Aggregator的子特質(zhì), 并提供其所有功能。此外, 它還提供了功能和語法, 允許有效的業(yè)務(wù)流程組合, 以及在下面詳細(xì)討論的創(chuàng)建業(yè)務(wù)流程功能時(shí)經(jīng)常需要的實(shí)用工具。要使用業(yè)務(wù)流程, actor可以簡單地繼承Orchestrator特質(zhì)。
import org.squbs.pattern.orchestration.Orchestrator
class MyOrchestrator extends Actor with Orchestrator {
...
}
與Aggregator類似, 業(yè)務(wù)流程通常不聲明Akka actor接收塊, 但允許expect/expectOnce/unexpect 塊來定義在任何點(diǎn)上預(yù)期的響應(yīng)。這些預(yù)期代碼塊通常從內(nèi)部業(yè)務(wù)流程函數(shù)中使用。
業(yè)務(wù)流程的Future和Promise
業(yè)務(wù)流程的promise和future與scala.concurrent.Future 和 scala.concurrent.Promise 非常相似,這里描述的指示一個(gè)名字的變化到OFuture和OPromise,
標(biāo)志著它們應(yīng)當(dāng)在actor里用于業(yè)務(wù)流程。業(yè)務(wù)流程版本通過它們與沒有并發(fā)行為的工件版本區(qū)分開來。它們?cè)诤灻胁皇褂?(隱式) ExecutionContext, 也不用于執(zhí)行。它們還缺少一些顯式異步執(zhí)行閉包的函數(shù)。在演員內(nèi)部使用, 他們的回調(diào)絕不會(huì)超出Actor的范圍之外。這將消除由于回調(diào)而從不同線程同時(shí)修改Actor狀態(tài)的危險(xiǎn)。此外, 它們還包括性能優(yōu)化, 假設(shè)它們總是在Actor內(nèi)部使用。
注意:不要傳遞一個(gè)OFuture 到actor外面。提供了隱式轉(zhuǎn)換以在 scala.concurrent.Future 和 OFuture 之間進(jìn)行轉(zhuǎn)換。
import org.squbs.pattern.orchestration.{OFuture, OPromise}
異步的業(yè)務(wù)流程函數(shù)
業(yè)務(wù)流程函數(shù)是一個(gè)被稱為orchestration 流的函數(shù),用來執(zhí)行單一業(yè)務(wù)流程任務(wù),例如調(diào)用一個(gè)服務(wù)或者函數(shù)。業(yè)務(wù)流程功能必須符合以下準(zhǔn)則:
1,它必須以非Future參數(shù)作為輸入。根據(jù) Scala 的當(dāng)前限制, 參數(shù)的數(shù)目最多可達(dá)22。在所有情況下, 這些函數(shù)不應(yīng)該有那么多參數(shù)。
2,Scala函數(shù)可以柯里化,從 piped (future)輸入中直接分離輸入。管道輸入必須是在柯里化函數(shù)中最后一組參數(shù)。
3,它必須導(dǎo)致異步執(zhí)行。異步執(zhí)行通常通過發(fā)送消息給不同的actor來實(shí)現(xiàn)。
4,Scala實(shí)現(xiàn)必須返回一個(gè)OFuture (orchestration future)。
以下是業(yè)務(wù)流程函數(shù)的例子:
def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
val itemPromise = OPromise[Option[Item]]
context.actorOf(Props[ItemActor]) ! ItemRequest(itemId, seller.id)
expectOnce {
case item: Item => itemPromise success Some(item)
case e: NoSuchItem => itemPromise success None
}
itemPromise.future
}
這個(gè)例子,函數(shù)是柯里化的。itemId 參數(shù)是同步傳遞的, 而發(fā)送方是異步傳遞的。
下面的示例在邏輯上與第一個(gè)示例相同, 只是使用 ask 而不是tell:
private def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
import context.dispatcher
implicit val timeout = Timeout(3 seconds)
(context.actorOf[ItemActor] ? ItemRequest(itemId, seller.id)).mapTo[Option[Item]]
}
此例中,ask ? 操作返回了一個(gè)scala.concurrent.Future。Orchestrator 特質(zhì)提供了scala.concurrent.Future 和OFuture的隱式轉(zhuǎn)換,所以ask ?的結(jié)果轉(zhuǎn)換為這個(gè)函數(shù)申明的返回類型 OFuture 且無需顯示調(diào)用轉(zhuǎn)換。
ask 或者?似乎需要編寫更少的代碼, 但它既不性能, 也不像expect/expectOnce那樣靈活 。預(yù)期塊中的邏輯也可用于結(jié)果的進(jìn)一步轉(zhuǎn)換。同樣可以通過ask返回的future使用回調(diào)來實(shí)現(xiàn)。但是, 由于以下原因, 無法輕松地補(bǔ)償性能:
1,Ask將創(chuàng)建一個(gè)新的actor作為響應(yīng)接收者。
2,從 scala.concurrent.Future 到 OFuture 的轉(zhuǎn)換以及 Java API 的填充操作需要將消息發(fā)送回orchestrator, 從而添加一個(gè)消息跳轉(zhuǎn), 同時(shí)增加了延遲和 CPU。
測(cè)試顯示,當(dāng)使用ask,與expect/expectOnce相對(duì)時(shí),有更高的延遲和CPU利用。
組合
管道,或者>>符號(hào)使用一個(gè)或多個(gè)業(yè)務(wù)流程future OFuture ,并使其結(jié)果作為業(yè)務(wù)流程功能的輸入。當(dāng)所有表示對(duì)函數(shù)輸入的 OFutures 都被解析時(shí), 實(shí)際的業(yè)務(wù)流程函數(shù)調(diào)用將異步發(fā)生。
管道是業(yè)務(wù)流程 DSL 的主要組件, 它允許根據(jù)其輸入和輸出組成業(yè)務(wù)流程功能。業(yè)務(wù)流程流是由業(yè)務(wù)流程聲明隱式地確定的,或者通過管道來聲明業(yè)務(wù)流程流。
當(dāng)多個(gè) OFutures 被輸送到一個(gè)業(yè)務(wù)流程函數(shù)時(shí), OFutures 需要以逗號(hào)分隔并括在圓括號(hào)中, 構(gòu)造一個(gè) OFutures 的元組作為輸入。元組中的元素,它們的 OFuture 類型必須與函數(shù)參數(shù)和類型匹配, 或者是在柯里化的情況下的最后一組參數(shù), 否則編譯將失敗。此類錯(cuò)誤通常也由 IDE 捕獲。
下面的例子展示了一個(gè)簡單的業(yè)務(wù)流程聲明以及使用前面章節(jié)申明的loadItem 業(yè)務(wù)流程函數(shù):
val userF = loadViewingUser
val itemF = userF >> loadItem(request.itemId)
val itemViewF = (userF, itemF) >> buildItemView
上面的代碼可以如下描述:
- 首先調(diào)用loadViewingUser (不帶輸入?yún)?shù))
- 當(dāng)查看用戶可用的時(shí)候,使用查看用戶作為調(diào)用loadItem 的輸入(在這種情況下前面有個(gè)itemId可用)。本例中,loadItem遵循上面業(yè)務(wù)流程函數(shù)聲明中確切的簽名。
- 當(dāng)用戶和項(xiàng)目可用時(shí),調(diào)用buildItemView。
業(yè)務(wù)流程實(shí)例生命周期
業(yè)務(wù)流程通常是一次性actor。它們接收初始請(qǐng)求, 然后根據(jù)調(diào)用的業(yè)務(wù)流程函數(shù)發(fā)出的請(qǐng)求進(jìn)行多個(gè)響應(yīng)。
為了允許一個(gè)業(yè)務(wù)流程服務(wù)多個(gè)業(yè)務(wù)流程請(qǐng)求,業(yè)務(wù)流程必須結(jié)合每個(gè)請(qǐng)求的輸入和響應(yīng), 并將它們與不同的請(qǐng)求隔離。這將大大使其發(fā)展復(fù)雜化,并且可能不會(huì)按照這些示例中看到的清晰的業(yè)務(wù)流程表示中得到最終結(jié)果。創(chuàng)建一個(gè)新的actor是足夠廉價(jià)的,我們能夠容易地為每個(gè)業(yè)務(wù)流程請(qǐng)求創(chuàng)建一個(gè)新的業(yè)務(wù)流程actor。
業(yè)務(wù)流程回調(diào)的最后部分是停止actor。在Scala中,通過調(diào)用context.stop(self)或者context stop self (如果中綴表示法是首選)。
完成業(yè)務(wù)流程流
在這里, 我們把上述所有的概念放在一起。用更完整的解釋來重復(fù)上面的例子:
// 1. Define the orchestrator actor.
class MyOrchestrator extends Actor with Orchestrator {
// 2. Provide the initial expectOnce block that will receive the request message.
// After this request message is received, the same request will not be
// expected again for the same actor.
// The expectOnce likely has one case match which is the initial request and
// uses the request arguments or members, and the sender() to call the high
// level orchestration function. This function is usually named orchestrate.
expectOnce {
case r: MyOrchestrationRequest => orchestrate(sender(), r)
}
// 3. Define orchestrate. Its arguments become immutable by default
// allowing developers to rely on the fact these will never change.
def orchestrate(requester: ActorRef, request: MyOrchestrationRequest) {
// 4. If there is anything we need to do synchronously to setup for
// the orchestration, do this in the first part of orchestrate.
// 5. Compose the orchestration flow using pipes as needed by the business logic.
val userF = loadViewingUser
val itemF = userF >> loadItem(request.itemId)
val itemViewF = (userF, itemF) >> buildItemView
// 6. End the flow by calling functions composing the response(s) back to the
// requester. If the composition is very large, it may be more readable to
// use for-comprehensions rather than a composition function with very large
// number of arguments. There may be multiple such compositions in case partial
// responses are desired. This example shows the use of a for-comprehension
// just for reference. You can also use an orchestration function with
// 3 arguments plus the requester in such small cases.
for {
user <- userF
item <- itemF
itemView <- itemViewF
} {
requester ! MyOrchestrationResult(user, item, itemView)
context.stop(self)
}
// 7. Make sure the last response stops the orchestrator actor by calling
// context.stop(self).
}
// 8. Implement the asynchronous orchestration functions inside the
// orchestrator actor, but outside the orchestrate function.
def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
val itemPromise = OPromise[Option[Item]]
context.actorOf[ItemActor] ! ItemRequest(itemId, seller.id)
expectOnce {
case item: Item => itemPromise success Some(item)
case e: NoSuchItem => itemPromise success None
}
itemPromise.future
}
def loadViewingUser: OFuture[Option[User]] = {
val userPromise = OPromise[Option[User]]
...
userPromise.future
}
def buildItemView(user: Option[User], item: Option[Item]): OFuture[Option[ItemView]] = {
...
}
}
業(yè)務(wù)流程函數(shù)重用
業(yè)務(wù)流程功能通常依賴于Orchestrator特質(zhì)提供的工具, 無法獨(dú)立運(yùn)行。但是, 在許多情況下, 需要在多個(gè)協(xié)調(diào)中重用業(yè)務(wù)流程功能, 以不同的方式進(jìn)行編排。在這種情況下, 重要的是將業(yè)務(wù)流程功能分成不同的特質(zhì), 這些特質(zhì)將被混合到每個(gè)業(yè)務(wù)流程中。該特性必須具有對(duì)業(yè)務(wù)流程功能的訪問權(quán)限, 并且需要對(duì)Orchestrator的自我引用。下面顯示了這樣一個(gè)特質(zhì)的示例:
trait OrchestrationFunctions { this: Actor with Orchestrator =>
def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
...
}
}
上面例子Actor with Orchestrator 是一個(gè)類型自應(yīng)用。它告訴Scala編譯器,這個(gè)特質(zhì)只能夠混入到一個(gè)同樣也是Orchestrator 的Actor,因此可以訪問Actor和Orchestrator提供的工具。
要使用業(yè)務(wù)流程中的 OrchestrationFunctions特質(zhì), 你只需將這個(gè)特質(zhì)混合到一個(gè)業(yè)務(wù)流程中, 如下所示:
class MyOrchestrator extends Actor with Orchestrator with OrchestrationFunctions {
...
}
確保響應(yīng)的唯一性
當(dāng)使用expect 或 expectOnce時(shí),我們受到單個(gè)期望塊的模式匹配功能的限制, 它的范圍有限, 無法區(qū)分多個(gè)業(yè)務(wù)流程函數(shù)中的多個(gè)預(yù)期塊之間的匹配。在同一業(yè)務(wù)流程函數(shù)中聲明預(yù)期之前, 收到的請(qǐng)求消息中沒有邏輯鏈接的。對(duì)于復(fù)雜的業(yè)務(wù)流程, 我們可能會(huì)遇到消息混淆的問題。響應(yīng)沒有與正確的請(qǐng)求關(guān)聯(lián), 也沒有正確處理。解決這個(gè)問題有幾個(gè)策略:
如果初始消息的接收者,因此響應(yīng)消息的發(fā)送者是唯一的,那么匹配可能包括對(duì)消息的發(fā)送方的引用,就像在下面的示例模式匹配中一樣。
def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
val itemPromise = OPromise[Option[Item]]
val itemActor = context.actorOf(Props[ItemActor])
itemActor ! ItemRequest(itemId, seller.id)
expectOnce {
case item: Item if sender() == itemActor => itemPromise success Some(item)
case e: NoSuchItem if sender() == itemActor => itemPromise success None
}
itemPromise.future
}
或者, Orchestrator 特質(zhì)提供了一個(gè)消息 id 生成器, 它在與參與者實(shí)例結(jié)合時(shí)是唯一的。我們可以使用這個(gè) id 發(fā)生器生成一個(gè)唯一的消息 id。接受此類消息的Actor只需要將此消息 id 作為響應(yīng)消息的一部分返回。下面的示例顯示了使用消息 id 生成器的業(yè)務(wù)流程函數(shù)。
def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
val itemPromise = OPromise[Option[Item]]
// Generate the message id.
val msgId = nextMessageId
context.actorOf(Props[ItemActor]) ! ItemRequest(msgId, itemId, seller.id)
// Use the message id as part of the response pattern match. It needs to
// be back-quoted as to not be interpreted as variable extractions, where
// a new variable is created by extraction from the matched object.
expectOnce {
case item @ Item(`msgId`, _, _) => itemPromise success Some(item)
case NoSuchItem(`msgId`, _) => itemPromise success None
}
itemPromise.future
}