将典型的3层架构转化为Actor模式

21

这个问题困扰我有一段时间了(希望我不是唯一一个)。我想找一个典型的3层Java EE应用程序,并看看它如何可能使用actors实现。我想知道是否真的有意义进行这样的转换,如果有意义,我如何从中受益(例如性能,更好的架构,可扩展性,可维护性等)。

以下是典型的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阻塞)。
假设我们正在尝试使用Actor实现类似的功能。它可能是这样的:
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")
控制器和服务将执行相同的事情。在这种情况下,整个工作流程将是同步的。更糟糕的是 - 我一次只能处理一个请求(同时所有其他请求都会落入控制器的邮箱中)。因此我需要将它全部变成异步。
在这种设置中,有没有优雅的方法可以异步执行每个处理步骤?
据我所了解,每个层应该以某种方式保存其接收到的消息的上下文,然后向下一层发送消息。当下一层回复某个结果消息时,我应该能够恢复初始上下文并将此结果回复给原始发送方。这正确吗?
此外,目前对于每个层,我只有一个Actor实例。即使它们异步工作,我仍然只能并行处理一个控制器、服务和dao消息。这意味着我需要更多相同类型的Actor。这导致每个层都需要LoadBalancer。这也意味着,如果我有UserService和ItemService,我应该分别负载均衡它们。
我有一种感觉,我可能理解错了什么。所有需要的配置似乎过于复杂。你对此有什么看法?
(PS:了解DB事务如何适合这种情况也很有趣,但我认为这对于这个主题来说太过复杂了。)

+1 - 你很有雄心壮志,Easy Angel。 - duffymo
5个回答

11
除非你有明确的理由,否则避免异步处理。演员是可爱的抽象,但它们甚至不能消除异步处理的固有复杂性。
我以艰难的方式发现了这个真相。我想将我的应用程序大部分与潜在不稳定点:数据库隔离开来。救命的演员!特别是Akka演员。而且真是太棒了。
手里拿着锤子,我开始敲打视野中的每一个钉子。用户会话?是的,他们也可以成为演员。嗯...访问控制怎么样?当然可以!我越来越感到不安,将我的迄今为止简单的架构变成了一个怪物:多层演员,异步消息传递,处理错误条件的复杂机制,以及严重的丑陋症。
我退出了,大部分时间都是这样。
我保留了给我所需的内容 - 对我的持久性代码容错 - 并将所有其他内容转换为普通类。
我建议你仔细阅读好的Akka使用案例问题/答案?这可能会让你更好地了解何时以及如何使用演员。如果你决定使用Akka,你可能会喜欢查看我对早期关于编写负载平衡演员的问题的回答。

谢谢分享您的经验!我之前确实读过您关于负载均衡的答案,并且很喜欢它 - 简单实用(这次我能够投票了 :) - tenshi

5

只是随意想象,但...

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

例如,如果每个用户都是一个独立的演员坐在JVM中,或通过远程演员在许多其他JVM中。每个用户负责接收更新消息、发布有关自身的数据,并将自身保存到磁盘(或DB或Mongo等)。

我想我的意思是,所有具有状态的对象都可以成为演员,只等待消息来更新自己。

(对于HTTP(如果您想自己实现),每个请求都会生成一个阻塞直到得到回复的演员(使用!?或future),然后将其格式化为响应。您可以以这种方式生成大量的演员。)

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

或者您有一个目录进程,它跟踪所有用户演员的位置。UserDirectory演员本身可以是一个演员(每个JVM一个),它接收有关当前运行的所有用户演员及其名称的消息,然后从请求演员中转发消息,委派给其他联合目录演员。您可以询问UserDirectory用户在哪里,然后直接发送该消息。UserDirectory演员负责启动一个用户演员(如果尚未运行)。用户演员恢复其状态,然后接受更新。

等等,等等。

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

好玩!


1
构思新事物绝对是个好主意,但你的细节很危险。阻塞的演员会阻塞线程,而你的虚拟机只能处理有限数量的这些演员。也就是说,将所有东西都实现为演员可能根本无法扩展。 - Raphael
+1 - 这绝对很有趣。我同意 - 我应该走出这个框框,尝试去想想它之外的东西。我认为作为第一步,我可以集中精力于实际目标 - 我真正想要实现什么?这个新架构应该具备哪些特点?分析典型的架构并尝试识别我喜欢的和想要改进的东西也会很有帮助。我不相信只靠演员模型就能实现我的目标……我将尝试总结所有这些事情。 - tenshi

4
大型计算密集型原子事务很难实现,这也是数据库如此受欢迎的原因之一。因此,如果您想知道是否可以透明轻松地使用Actor替换Java EE模型中非常依赖于数据库强大功能的所有事务性和高可扩展性特性,则答案是否定的。
但是有一些技巧可以使用。例如,如果一个Actor似乎导致瓶颈,但您不想费力创建分发器/工作程序结构,则可以将密集的工作移入Futures中:
val service = actor {
  ...
  case m: MakeSomethingWithUsers() =>
    Futures.future { sender ! myExpensiveOperation(m) }
}

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


除非您在单个服务器上开始生成许多这样的线程,否则您的解决方案将无法良好扩展。 - wheaties
1
@wheaties:确实。在这台机器上,你的数据库性能也会非常糟糕。 - Rex Kerr

3

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

这是非常棒的东西。


我同意你的观点 - STM是处理事务的好方案,除非我有多个JVM在运行。如果我错了,请纠正我,但我认为在Akka中,事务无法跨越多个JVM分布(但据我所知,他们正在研究分布式STM)。如果我要扩展我的应用程序,我要么设置几个相同的JVM并平衡负载,要么将我的actors分散到几个JVM中。在任何情况下,我都不能在所有JVM上拥有相同的事务。但是使用数据库事务,我可以实现这一点。 - tenshi

3

正如您所说,!! = 阻塞 = 不利于可扩展性和性能,可以参考以下链接: ! 和 !! 的性能对比

在持久化状态而不是事件时,通常需要事务处理。 请查看 CQRS 和 DDDD(分布式领域驱动设计)以及 事件溯源,因为正如您所说,我们仍然没有分布式STM。


谢谢提供这些参考资料!看起来非常有趣,我一定会深入研究。 - tenshi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接