21

この質問は、しばらくの間私を悩ませています (私だけではないといいのですが)。典型的な 3 層の Java EE アプリを取り上げて、それがアクターでどのように実装されるかを見てみたいと思います。そのような移行を行うことが実際に意味があるかどうか、また、それが意味をなす場合にどのように利益を得ることができるかを調べたいと思います (おそらく、パフォーマンス、より良いアーキテクチャ、拡張性、保守性など...)。

典型的なコントローラー (プレゼンテーション)、サービス (ビジネス ロジック)、DAO (データ) は次のとおりです。

trait UserDao {
  def getUsers(): List[User]
  def getUser(id: Int): User
  def addUser(user: User)
}

trait UserService {
  def getUsers(): List[User]
  def getUser(id: Int): User
  def addUser(user: User): Unit

  @Transactional
  def makeSomethingWithUsers(): Unit
}


@Controller
class UserController {
  @Get
  def getUsers(): NodeSeq = ...

  @Get
  def getUser(id: Int): NodeSeq = ...

  @Post
  def addUser(user: User): Unit = { ... }
}

このようなものは、多くの春のアプリケーションで見つけることができます。同期されたブロックがないため、共有状態を持たない単純な実装を使用できます...したがって、すべての状態はデータベースにあり、アプリケーションはトランザクションに依存します。サービス、コントローラー、および dao にはインスタンスが 1 つしかありません。したがって、リクエストごとにアプリケーションサーバーは個別のスレッドを使用しますが、スレッドは互いにブロックしません (ただし、DB IO によってブロックされます)。

アクターで同様の機能を実装しようとしているとします。次のようになります。

sealed trait UserActions
case class GetUsers extends UserActions
case class GetUser(id: Int) extends UserActions
case class AddUser(user: User) extends UserActions
case class MakeSomethingWithUsers extends UserActions

val dao = actor {
  case GetUsers() => ...
  case GetUser(userId) => ...
  case AddUser(user) => ...
}

val service = actor {
  case GetUsers() => ...
  case GetUser(userId) => ...
  case AddUser(user) => ...
  case MakeSomethingWithUsers() => ...
}

val controller = actor {
  case Get("/users") => ...
  case Get("/user", userId) => ...
  case Post("/add-user", user) => ...
}

ここでは、Get() および Post() エクストラクタがどのように実装されているかはあまり重要ではないと思います。これを実装するためのフレームワークを作成するとします。次のようにコントローラーにメッセージを送信できます。

controller !! Get("/users")

同じことがコントローラーとサービスによって行われます。この場合、ワークフロー全体が同期します。さらに悪いことに、一度に 1 つの要求しか処理できません (その間、他のすべての要求はコントローラーのメールボックスに届きます)。だから私はそれをすべて非同期にする必要があります。

このセットアップで各処理ステップを非同期に実行するエレガントな方法はありますか?

私が理解している限り、各層は受信したメッセージのコンテキストを何らかの形で保存してから、下の層にメッセージを送信する必要があります。下の層が何らかの結果メッセージで返信する場合、最初のコンテキストを復元し、この結果を元の送信者に返信できるはずです。これは正しいです?

さらに、現時点では、各階層にアクターのインスタンスが 1 つしかありません。それらが非同期で動作する場合でも、1 つのコントローラー、サービス、および dao メッセージのみを並行して処理できます。これは、同じタイプのアクターがもっと必要であることを意味します。これにより、各層の LoadBalancer が表示されます。これは、UserService と ItemService がある場合、両方を別々に LoadBalac する必要があることも意味します。

何か間違ったことを理解しているような気がします。必要な設定はすべて複雑すぎるようです。これについてあなたはどう思いますか?

(PS: DB トランザクションがこの図にどのように適合するかを知ることも非常に興味深いですが、このスレッドではやり過ぎだと思います)

4

5 に答える 5

11

明確な理由がない限り、非同期処理は避けてください。アクターは素晴らしい抽象化ですが、それでも非同期処理固有の複雑さを解消するものではありません。

私はその真実を難しい方法で発見しました。アプリケーションの大部分を、不安定になる可能性のある 1 つの実際のポイントであるデータベースから隔離したかったのです。救助に俳優!特にアッカ俳優。そして、それは素晴らしかったです。

手にハンマーを持って、視界にあるすべての釘を叩き始めました。ユーザーセッション?はい、彼らも俳優になることができます。ええと...そのアクセス制御はどうですか?もちろん!不安感が募り、これまでの単純なアーキテクチャを怪物に変えてしまいました。複数のアクター層、非同期メッセージ パッシング、エラー状態を処理するための精巧なメカニズム、そして厄介な問題の深刻なケースです。

ほとんどの場合、私は撤退しました。

私が必要としていたもの (持続性コードのフォールト トレランス) を提供してくれたアクターを保持し、他のすべてを通常のクラスに変更しました。

Akka の質問/回答の適切な使用例を注意深く読むことをお勧めしますか? これにより、アクターがいつ、どのように役立つかをよりよく理解できるようになります。Akka を使用することに決めた場合は、負荷分散されたアクターの記述に関する以前の質問に対する私の回答を参照してください。

于 2011-01-16T12:45:17.220 に答える
5

リフばかりですが…

アクターを使用する場合は、以前のパターンをすべて捨てて新しいものを考え、必要に応じて古いパターン (コントローラー、dao など) を再組み込みしてギャップを埋める必要があると思います。

たとえば、各ユーザーが JVM に座っている個々のアクターである場合、またはリモート アクターを介して他の多くの JVM にある場合はどうなるでしょうか。各ユーザーは、更新メッセージを受信し、自分自身に関するデータを公開し、自分自身をディスク (または DB や Mongo など) に保存する責任があります。

私が理解しているのは、すべてのステートフル オブジェクトが、メッセージが自分自身を更新するのを待っているアクターになる可能性があるということです。

(HTTP の場合 (自分で実装したい場合)、各リクエストは、(!? または future を使用して) 応答を受け取るまでブロックするアクターを生成し、応答にフォーマットされます。多くのアクターを生成できます。方法、私は思う。)

ユーザー "foo@example.com" のパスワードを変更する要求が来たら、メッセージを 'Foo@Example.Com' に送信します。ChangePassword(「新しい秘密」)。

または、すべてのユーザー アクターの場所を追跡するディレクトリ プロセスがあります。UserDirectory アクターは、アクター自体 (JVM ごとに 1 つ) であり、現在実行中のユーザー アクターとその名前に関するメッセージを受信し、リクエスト アクターからメッセージを中継し、他のフェデレーション ディレクトリ アクターに委任します。UserDirectory に User の場所を尋ねてから、そのメッセージを直接送信します。UserDirectory アクターは、ユーザー アクターがまだ実行されていない場合、それを開始する責任があります。ユーザー アクターはその状態を回復し、更新を除外します。

などなど。

考えるのは楽しいです。たとえば、各ユーザー アクターは、ディスクに永続化し、一定時間後にタイムアウトし、集約アクターにメッセージを送信することさえできます。たとえば、User アクターは LastAccess アクターにメッセージを送信する場合があります。または、PasswordTimeoutActor はすべての User アクターにメッセージを送信し、パスワードが特定の日付よりも古い場合はパスワードの変更を要求するよう伝えます。ユーザー アクターは、自分自身を他のサーバーにクローンしたり、複数のデータベースに保存したりすることもできます。

楽しい!

于 2011-01-16T04:34:08.387 に答える
4

大規模なコンピューティング集約型のアトミック トランザクションは実行するのが難しいため、データベースが非常に人気がある理由の 1 つです。したがって、アクターを透過的かつ簡単に使用して、データベースのすべてのトランザクション機能と高度にスケーラブルな機能を置き換えることができるかどうかを尋ねる場合 (Java EE モデルでその力に大きく依存している)、答えはノーです。

しかし、あなたがプレイできるいくつかのトリックがあります。たとえば、1 つのアクターがボトルネックを引き起こしているように見えても、ディスパッチャ/ワーカー ファーム構造を作成する努力をしたくない場合は、集中的な作業を先物に移すことができる場合があります。

val service = actor {
  ...
  case m: MakeSomethingWithUsers() =>
    Futures.future { sender ! myExpensiveOperation(m) }
}

このようにして、非常に高価なタスクが新しいスレッドで生成され (アトミック性やデッドロックなどを心配する必要がないと仮定すると、そうなるかもしれませんが、これらの問題を解決することは一般的に簡単ではありません)。関係なく、行くべき場所に送られます。

于 2011-01-16T03:52:27.257 に答える
3

アクターとのトランザクションについては、アクターを STM (ソフトウェア トランザクション メモリ) と組み合わせた Akka の「Transcators」を参照してください: http://doc.akka.io/transactors-scala

それはかなり素晴らしいものです。

于 2011-01-16T05:26:13.260 に答える
3

おっしゃるとおりです!!= ブロッキング = スケーラビリティとパフォーマンスに悪い、これを参照してください: Performance between ! と !!

通常、イベントではなく状態を永続化する場合、トランザクションが必要になります。CQRSと DDDD (Distributed Domain Driven Design) とEvent Sourcingを見てください。なぜなら、あなたが言うように、まだ分散 STM を持っていないからです。

于 2011-01-17T11:28:55.070 に答える