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

Posted

技术标签:

【中文标题】将典型的 3 层架构转移给参与者【英文标题】:Transferring typical 3-tier architecture to actors 【发布时间】:2011-06-09 20:26:12 【问题描述】:

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

【问题讨论】:

+1 - 来自你的雄心勃勃的东西,Easy Angel。 【参考方案1】:

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

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

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

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

【讨论】:

当然,除非您开始在单个服务器上生成此类线程。那么您的解决方案将无法扩展。 @wheaties:确实。您的数据库性能在所述机器上也不会给人留下深刻印象。【参考方案2】:

只是重复,但是...

我认为如果你想使用演员,你应该扔掉所有以前的模式并构思新的东西,然后可能会根据需要重新合并旧模式(控制器、dao 等)以填补空白。

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

我想我的意思是,你所有的有状态对象都可以是角色,只是等待消息更新自己。

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

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

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

等等。

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

好玩!

【讨论】:

设计新东西绝对是个好主意,但你的细节很危险。被阻塞的actor阻塞了一个线程,而你的虚拟机只能处理这么多的线程。也就是说,将所有内容都实现为 actor 可能不会有丝毫的扩展性。 +1 - 这绝对很有趣。我同意——我应该逃离这个盒子并尝试在它之外思考。我认为作为第一步,我可以专注于实际目标——我真正想要实现的目标是什么?这个新架构应该有什么特点?分析典型架构并尝试找出我喜欢它的地方以及我想要改进的地方也会很有帮助。我不相信,我可以单独使用actor模型来实现我的目标......我会尝试总结所有这些事情。【参考方案3】:

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

这是非常棒的东西。

【讨论】:

我同意你的看法——除非我有多个 JVM 正在运行,否则 STM 将是事务处理的不错解决方案。如果我错了,请纠正我,但我认为在 Akka 的事务中不能分布在多个 JVM 上(但据我所知,他们正在使用分布式 STM)。如果我要扩展我的应用程序,我将设置几个相同的 JVM 并对它们进行负载平衡,或者只是将我的参与者分散到多个 JVM 上。无论哪种情况,我的所有 JVM 都不能有相同的事务。但是通过数据库事务我可以做到这一点。【参考方案4】:

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

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

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

我退出了,主要是。

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

我可以建议您仔细阅读Good use case for Akka 问题/答案吗?这可能会让您更好地了解演员何时以及如何有价值。如果您决定使用 Akka,您可能想查看我对之前关于 writing load-balanced actors 的问题的回答。

【讨论】:

感谢分享您的经验!实际上我之前读过你关于负载平衡的回答,我喜欢它 - 简单实用(这次我能够投票:)【参考方案5】:

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

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

【讨论】:

感谢您的参考!看起来很有趣,我一定会深入研究这些。

以上是关于将典型的 3 层架构转移给参与者的主要内容,如果未能解决你的问题,请参考以下文章

架构基础3-网站的高可用

本周个人总结

怎么确保站点的可用性

一个监控系统的典型架构是什么样的

RESTful 架构 (表现层状态转移)

转帖:3层架构?