将典型的三层架构转移给参与者

发布于 2024-10-12 02:32:00 字数 2179 浏览 1 评论 0原文

这个问题困扰了我一段时间(我希望我不是唯一的一个)。我想采用一个典型的 3 层 Java EE 应用程序,看看它如何看起来像是用 actor 实现的。我想知道进行这种转变是否真的有意义,以及如果确实有意义的话我如何从中受益(也许是性能、更好的架构、可扩展性、可维护性等......)。

以下是典型的Controller(表示)、Service(业务逻辑)、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 = { ... }
}

在很多Spring应用程序中你可以找到类似的东西。我们可以采用没有任何共享状态的简单实现,这是因为没有同步块......所以所有状态都在数据库中,并且应用程序依赖于事务。 Service、controller和dao只有一个实例。因此,对于每个请求,应用程序服务器将使用单独的线程,但线程不会相互阻塞(但会被 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")

控制器和服务会做出同样的事情。在这种情况下,整个工作流程将是同步的。更糟糕的是 - 我一次只能处理一个请求(同时所有其他请求都会到达控制器的邮箱中)。所以我需要将其全部异步化。

在此设置中是否有任何优雅的方法来异步执行每个处理步骤?

据我了解,每个层都应该以某种方式保存它接收到的消息的上下文,然后将消息发送到下面的层。当下面的层回复一些结果消息时,我应该能够恢复初始上下文并将此结果回复给原始发件人。这是正确的吗?

此外,目前我每一层只有一个演员实例。即使它们异步工作,我仍然只能并行处理一个控制器、服务和 dao 消息。这意味着我需要更多同类型的演员。这引导我了解每一层的 LoadBalancer。这也意味着,如果我有 UserService 和 ItemService,我应该分别对它们进行 LoadBalace。

我有一种感觉,我理解有问题。所有所需的配置似乎都过于复杂。您对此有何看法?

(PS:了解数据库事务如何适应这张图也很有趣,但我认为这对于这个线程来说有点过分了)

This question bothers me for some time now (I hope I'm not the only one). I want to take a typical 3-tier Java EE app and see how it possibly can look like implemented with actors. I would like to find out whether it actually makes any sense to make such transition and how I can profit from it if it does makes sense (maybe performance, better architecture, extensibility, maintainability, etc...).

Here are typical Controller (presentation), Service (business logic), DAO (data):

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 = { ... }
}

You can find something like this in many spring applications. We can take simple implementation that does not have any shared state and that's because does not have synchronized blocks... so all state is in the database and application relies on transactions. Service, controller and dao have only one instance. So for each request application server will use separate thread, but threads will not block each other (but will be blocked by DB IO).

Suppose we are trying to implement similar functionality with actors. It can look like this:

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) => ...
}

I think it's not very important here how Get() and Post() extractors are implemented. Suppose I write a framework to implement this. I can send message to controller like this:

controller !! Get("/users")

The same thing would be made by controller and service. In this case the whole workflow would be synchronous. Even worse - I can process only one request at time (in meantime all other requests would land in controller's mailbox). So I need to make it all asynchronous.

Is there any elegant way to perform each processing step asynchronously in this setup?

As far as I understand each tier should somehow save the context of the message it receives and then send message to the tier beneath. When tier beneath replies with some result message I should be able to restore initial context and reply with this result to the original sender. Is this correct?

Moreover, at the moment I have only one instance of actor for each tier. Even if they will work asynchronously, I still can process in parallel only one controller, service and dao message. This means that I need more actors of the same type. Which leads me to LoadBalancer for each tier. This also means, that if I have UserService and ItemService I should LoadBalace both of them separately.

I have feeling, that I understand something wrong. All needed configuration seems to be overcomplicated. What do you think about this?

(PS: It would be also very interesting to know how DB transactions fit into this picture, but I think it's overkill for this thread)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

段念尘 2024-10-19 02:32:00

避免异步处理,除非您有明确的理由这样做。参与者是可爱的抽象,但即使它们也不能消除异步处理固有的复杂性。

我通过艰难的方式发现了这个真理。我希望将我的大部分应用程序与潜在不稳定的一个真正点隔离:数据库。演员来救援!特别是 Akka 演员。太棒了。

我手里拿着锤子,开始敲击视野中的每一个钉子。用户会话?是的,他们也可以成为演员。嗯...访问控制怎么样?当然可以,为什么不呢!随着越来越多的不安感,我把迄今为止简单的架构变成了一个怪物:多层参与者、异步消息传递、处理错误条件的复杂机制以及严重的丑陋情况。

我基本上退出了。

我保留了为我提供所需内容的参与者(持久性代码的容错能力),并将所有其他参与者转变为普通类。

我可以建议您仔细阅读Akka 的良好用例问题/答案吗?这可能会让你更好地理解演员何时以及如何变得有价值。如果您决定使用 Akka,您可能想查看我对之前有关 编写负载平衡参与者

Avoid asynchronous processing unless and until you have a clear reason for doing it. Actors are lovely abstractions, but even they don't eliminate the inherent complexity of asynchronous processing.

I discovered that truth the hard way. I wanted to insulate the bulk of my application from the one real point of potential instability: the database. Actors to the rescue! Akka actors in particular. And it was awesome.

Hammer in hand, I then set about bashing every nail in view. User sessions? Yes, they could be actors too. Um... how about that access control? Sure, why not! With a growing sense of un-ease, I turned my hitherto simple architecture into a monster: multiple layers of actors, asynchronous message passing, elaborate mechanisms to deal with error conditions, and a serious case of the uglies.

I backed out, mostly.

I retained the actors that were giving me what I needed - fault-tolerance for my persistence code - and turned all of the others into ordinary classes.

May I suggest that you carefully read the Good use case for Akka question/answers? That may give you a better understanding of when and how actors will be worthwhile. Should you decide to use Akka, you might like to view my answer to an earlier question about writing load-balanced actors.

薄荷→糖丶微凉 2024-10-19 02:32:00

只是重复,但是......

我认为如果你想使用演员,你应该扔掉所有以前的模式并想象一些新的东西,然后也许根据需要重新合并旧的模式(控制器,dao 等)来填充差距。

例如,如果每个用户都是 JVM 中的单独参与者,或者通过远程参与者,位于许多其他 JVM 中,该怎么办?每个用户负责接收更新消息,发布有关其自身的数据,并将其自身保存到磁盘(或 DB 或 Mongo 等)。

我想我的意思是,所有有状态对象都可以是等待消息更新自身的参与者。

(对于 HTTP(如果您想自己实现),每个请求都会生成一个 Actor,该 Actor 会阻塞,直到收到回复(使用 !? 或 future),然后将其格式化为响应。您可以生成很多 Actor 时

当请求更改用户“[email] 的密码 protected]”,您将消息发送到 '[email protected]'!更改密码(“新密码”)。

或者您有一个目录进程来跟踪所有用户参与者的位置。 UserDirectory actor 本身可以是一个 actor(每个 JVM 一个),它接收有关当前正在运行的 User actor 及其名称的消息,然后将消息从 Request actor 转发给它们,委托给其他联合 Directory actor。您可以询问 UserDirectory 用户在哪里,然后直接发送该消息。 UserDirectory actor 负责启动 User actor(如果尚未运行)。用户参与者恢复其状态,然后更新除外。

等等等等。

想想就很有趣。例如,每个 User actor 都可以将自身保存到磁盘,在一定时间后超时,甚至可以向 Aggregation Actor 发送消息。例如,User actor 可能会向 LastAccess actor 发送一条消息。或者,PasswordTimeoutActor 可能会向所有用户参与者发送消息,告诉他们如果密码早于特定日期,则需要更改密码。用户参与者甚至可以将自己克隆到其他服务器上,或者将自己保存到多个数据库中。

乐趣!

Just riffing, but...

I think if you want to use actors, you should throw away all previous patterns and dream up something new, then maybe re-incorporate the old patterns (controller, dao, etc) as necessary to fill in the gaps.

For instance, what if each User is an individual actor sitting in the JVM, or via remote actors, in many other JVMs. Each User is responsible for receiving update messages, publishing data about itself, and saving itself to disk (or a DB or Mongo or something).

I guess what I'm getting at is that all your stateful objects can be actors just waiting for messages to update themselves.

(For HTTP (if you wanted to implement that yourself), each request spawns an actor that blocks until it gets a reply (using !? or a future), which is then formatted into a response. You can spawn a LOT of actors that way, I think.)

When a request comes in to change the password for user "[email protected]", you send a message to '[email protected]' ! ChangePassword("new-secret").

Or you have a directory process which keeps track of the locations of all User actors. The UserDirectory actor can be an actor itself (one per JVM) which receives messages about which User actors are currently running and what their names are, then relays messages to them from the Request actors, delegates to other federated Directory actors. You'd ask the UserDirectory where a User is, and then send that message directly. The UserDirectory actor is responsible for starting a User actor if one isn't already running. The User actor recovers its state, then excepts updates.

Etc, and so on.

It's fun to think about. Each User actor, for instance, can persist itself to disk, time out after a certain time, and even send messages to Aggregation actors. For instance, a User actor might send a message to a LastAccess actor. Or a PasswordTimeoutActor might send messages to all User actors, telling them to require a password change if their password is older than a certain date. User actors can even clone themselves onto other servers, or save themselves into multiple databases.

Fun!

瞄了个咪的 2024-10-19 02:32:00

大型计算密集型原子事务很难实现,这也是数据库如此受欢迎的原因之一。因此,如果您询问是否可以透明且轻松地使用 Actor 来替换数据库的所有事务性和高度可扩展的功能(您在 Java EE 模型中非常依赖数据库的功能),答案是否定的。

但你可以玩一些技巧。例如,如果一个参与者似乎造成了瓶颈,但您不想花费精力创建调度员/工人农场结构,那么您也许可以将密集型工作转移到未来:

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

这样,真正的昂贵的任务在新线程中产生(假设您不需要担心原子性和死锁等问题,您可能会担心 - 但同样,解决这些问题通常并不容易)并且消息会发送到任何地方不管怎样,他们都应该去。

Large compute-intensive atomic transactions are tricky to pull off, which is one reason why databases are so popular. So if you are asking whether you can transparently and easily use actors to replace all the transactional and highly-scalable features of a database (whose power you are very heavily leaning on in the Java EE model), the answer is no.

But there are some tricks you can play. For example, if one actor seems to be causing a bottleneck, but you don't want to go to the effort of creating a dispatcher/worker farm structure, you may be able to move the intensive work into futures:

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

This way, the really expensive tasks get spawned off in new threads (assuming that you don't need to worry about atomicity and deadlocks and so on, which you may--but again, solving these problems is not easy in general) and messages get sent along to wherever they should be going regardless.

流年里的时光 2024-10-19 02:32:00

对于与 actor 的事务,您应该看看 Akka 的“Transcators”,它将 actor 与 STM(软件事务内存)结合起来: http://doc.akka.io/transactors-scala

这是非常棒的东西。

For transactions with actors, you should take a look at Akka's "Transcators", which combine actors with STM (software transactional memory): http://doc.akka.io/transactors-scala

It's pretty great stuff.

养猫人 2024-10-19 02:32:00

正如你所说,!! = 阻塞 = 不利于可扩展性和性能,请参阅以下内容:
性能! !!

当您持久化状态而不是事件时,通常会出现对事务的需求。
请查看 CQRS 和 DDDD(分布式域驱动设计)和 事件溯源,因为正如您所说,我们仍然没有分布式 STM。

As you said, !! = blocking = bad for scalability and performance, see this:
Performance between ! and !!

The need for transactions usually occur when you are persisting state instead of events.
Please have a look at CQRS and DDDD (Distributed Domain Driven Design) and Event Sourcing, because, as you say, we still haven't got a distributed STM.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文