21

这个问题困扰了我一段时间(我希望我不是唯一一个)。我想以一个典型的 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:知道数据库事务如何适应这张图片也很有趣,但我认为这对这个线程来说有点矫枉过正)

4

5 回答 5

11

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

我以艰难的方式发现了这个真相。我想将我的大部分应用程序与一个真正的潜在不稳定性点隔离开来:数据库。演员来救场!特别是 Akka 演员。这太棒了。

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

我退出了,主要是。

我保留了那些给我我需要的东西的演员——我的持久性代码的容错能力——并将所有其他演员都变成了普通的类。

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

于 2011-01-16T12:45:17.220 回答
5

只是翻唱,但是...

我认为如果你想使用actors,你应该抛弃所有以前的模式并创造一些新的东西,然后可能会根据需要重新合并旧模式(控制器、dao 等)以填补空白。

例如,如果每个用户都是 JVM 中的单个参与者,或者通过远程参与者,在许多其他 JVM 中会怎样。每个用户负责接收更新消息,发布关于自己的数据,并将自己保存到磁盘(或 DB 或 Mongo 或其他东西)。

我想我要说的是,您所有的有状态对象都可以是只是等待消息更新自己的参与者。

(对于 HTTP(如果你想自己实现),每个请求都会产生一个actor,它会阻塞直到它得到回复(使用 !? 或未来),然后将其格式化为响应。你可以产生很多actor方式,我想。)

当请求更改用户“foo@example.com”的密码时,您会向“Foo@Example.Com”发送一条消息!更改密码(“新秘密”)。

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

等等等等。

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

乐趣!

于 2011-01-16T04:34:08.387 回答
4

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

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

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

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

于 2011-01-16T03:52:27.257 回答
3

如你所说, !!= 阻塞 = 对可扩展性和性能不利,请参阅: Performance between ! 和 !!

当您持久化状态而不是事件时,通常会需要事务。请看一下CQRS和 DDDD(分布式域驱动设计)和事件溯源,因为正如你所说,我们还没有分布式 STM。

于 2011-01-17T11:28:55.070 回答
3

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

这是非常棒的东西。

于 2011-01-16T05:26:13.260 回答