Transferring typical 3-tier architecture to actors
这个问题困扰了我一段时间(我希望我不是唯一一个)。我想使用一个典型的三层JavaEE应用程序,看看它可能看起来像是用演员实现的。我想知道进行这种转换是否有意义,以及如果它有意义,我如何从中获益(可能是性能、更好的体系结构、可扩展性、可维护性等等)。
以下是典型的控制器(表示)、服务(业务逻辑)、DAO(数据):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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应用程序中找到类似的内容。我们可以采用没有任何共享状态的简单实现,这是因为没有同步块…所以所有状态都在数据库中,应用程序依赖于事务。服务、控制器和DAO只有一个实例。因此,对于每个请求,应用服务器将使用单独的线程,但线程不会相互阻塞(但会被DBIO阻塞)。
假设我们正在尝试与参与者实现类似的功能。它可以是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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()提取器并不重要。假设我编写了一个框架来实现这一点。我可以这样向控制器发送消息:
1 | controller !! Get("/users") |
控制器和服务也会做同样的事情。在这种情况下,整个工作流将是同步的。更糟糕的是,我一次只能处理一个请求(同时,所有其他请求都将放在控制器的邮箱中)。所以我需要让它都是异步的。
在这个设置中,有没有优雅的异步执行每个处理步骤的方法?
据我所知,每一层都应该以某种方式保存它接收到的消息的上下文,然后将消息发送到下面的层。当下面的层用一些结果消息答复时,我应该能够恢复初始上下文,并用这个结果答复原始发件人。这是正确的吗?
此外,目前我对每一层只有一个演员实例。即使它们异步工作,我仍然只能并行处理一个控制器、服务和DAO消息。这意味着我需要更多同类型的演员。这会导致我找到每一层的负载均衡器。这也意味着,如果我有userservice和itemservice,我应该分别对它们进行负载平衡。
我有种感觉,我理解错误。所有需要的配置似乎都过于复杂。你觉得这个怎么样?
(附言:知道DB事务如何适合这张图片也很有趣,但我认为这对这个线程来说太过分了)
避免异步处理,除非您有明确的理由这样做。参与者是可爱的抽象,但即使他们也不能消除异步处理固有的复杂性。
我很难发现真相。我想将我的大部分应用程序与潜在的不稳定点隔离开来:数据库。演员们来营救!尤其是阿卡演员。太棒了。
我手里拿着锤子,然后开始敲打眼前的每颗钉子。用户会话?是的,他们也可以当演员。嗯。。。访问控制怎么样?当然,为什么不呢?随着一种越来越不易理解的感觉,我把我迄今为止简单的体系结构变成了一个怪物:多层角色、异步消息传递、处理错误条件的复杂机制,以及一个严重的uglies案例。
大部分时候我都退缩了。
我保留了那些给我所需要的东西的演员——对我的持久性代码的容错能力——并且把所有其他的都变成了普通的类。
我可以建议您仔细阅读AKKA问题/答案的良好用例吗?这可能会让你更好地了解演员何时以及如何才能有价值。如果您决定使用AKKA,您可能会看到我对先前关于编写负载平衡参与者的问题的回答。
只是即兴演奏,但是…
我认为,如果你想使用演员,你应该放弃所有以前的模式,梦想一些新的东西,然后根据需要重新合并旧的模式(控制器、DAO等)来填补空白。
例如,如果每个用户都是一个单独的参与者,坐在JVM中,或者通过远程参与者,坐在许多其他的JVM中,该怎么办?每个用户负责接收更新消息、发布关于自身的数据,并将自己保存到磁盘(或者数据库、Mongo或其他东西)。
我想我得到的是,所有有状态的对象都可以是等待消息自我更新的参与者。
(对于HTTP(如果您想自己实现它),每个请求都会产生一个参与者,该参与者会一直阻塞,直到得到一个回复(使用!)?或未来),然后将其格式化为响应。我想,你可以用这种方式培养很多演员。)
当请求进入更改用户"[email protected]"的密码时,您将向"[email protected]"发送一条消息!更改密码("新密码")。
或者,您有一个目录进程来跟踪所有用户参与者的位置。user directory actor可以是actor本身(每个jvm一个),它接收有关当前运行的用户actor及其名称的消息,然后将来自请求actor的消息转发给它们,并委托给其他联合目录actor。您可以询问用户所在的用户目录,然后直接发送该消息。如果某个用户参与者尚未运行,则该用户目录参与者负责启动该用户参与者。用户参与者恢复其状态,然后排除更新。
等等,等等。
想起来很有趣。例如,每个用户参与者都可以将自己持久化到磁盘上,在一段时间后超时,甚至可以向聚合参与者发送消息。例如,用户参与者可能会向lastaccess参与者发送消息。或者,PasswordTimeoutActor可能会向所有用户参与者发送消息,告诉他们如果密码早于某个日期,则需要更改密码。用户参与者甚至可以将自己克隆到其他服务器上,或者将自己保存到多个数据库中。
好玩!
大型计算密集型的原子事务很难完成,这也是数据库如此流行的原因之一。因此,如果你问你是否可以透明地和容易地使用角色替换数据库的所有事务性和高度可伸缩的特性(其在Java EE模型中非常倚重的能力),答案是否定的。
但是你可以玩一些把戏。例如,如果一个参与者似乎造成了瓶颈,但您不想努力创建调度员/工人农场结构,则可以将密集的工作转移到未来:
1 2 3 4 5 |
这样,真正昂贵的任务就会在新的线程中产生(假设您不需要担心原子性和死锁等问题,您可以这样做——但同样地,解决这些问题通常并不容易),消息会被发送到它们应该去的任何地方,而不管它们去哪里。
正如你所说的!!=blocking=不利于扩展性和性能,请参见:性能介于!而且!!
对事务的需求通常发生在持久化状态而不是事件的时候。请看一下CQR和DDDD(分布式域驱动设计)以及事件源,因为正如您所说,我们还没有分布式STM。
对于与actors的事务,您应该看看akka的"transcators",它将actors与stm(软件事务内存)结合在一起:http://doc.akka.io/transactors-scala
这是非常棒的东西。