将典型的3层架构转移给演员

ten*_*shi 21 spring scala 3-tier java-ee actor

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

这是典型的Controller(演示),服务(业务逻辑),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 = { ... }
}
Run Code Online (Sandbox Code Playgroud)

你可以在许多弹簧应用中找到类似的东西.我们可以采用没有任何共享状态的简单实现,因为没有同步块...所以所有状态都在数据库中,应用程序依赖于事务.服务,控制器和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) => ...
}
Run Code Online (Sandbox Code Playgroud)

我认为在这里如何实现Get()和Post()提取器并不是很重要.假设我编写了一个框架来实现它.我可以像这样向控制器发送消息:

controller !! Get("/users")
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

我有种感觉,我理解错了.所有需要的配置似乎都过于复杂.你怎么看待这件事?

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

Dav*_*vid 10

避免异步处理,除非并且直到您有明确的理由这样做.Actor是可爱的抽象,但即使它们也没有消除异步处理的固有复杂性.

我用艰难的方式发现了真相.我想将我的大部分应用程序与潜在不稳定性的一个真实点隔离开来:数据库.演员救援!尤其是Akka演员.它太棒了.

手里拿着锤子,然后开始抨击每一根钉子.用户会话?是的,他们也可能是演员.嗯......访问控制怎么样?当然,为什么不呢!随着越来越不轻松的感觉,我把我迄今为止简单的架构变成了一个怪物:多层演员,异步消息传递,处理错误条件的精细机制,以及一个严重的丑陋案例.

我退出了,主要是.

我保留了给我所需要的演员 - 我的持久性代码的容错性 - 并将所有其他人变成了普通的课程.

我建议您仔细阅读Akka问题/答案的好用例吗?这可能会让您更好地了解演员何时以及如何值得.如果您决定使用Akka,您可能希望查看我之前关于编写负载均衡actor的问题的答案.


小智 5

只是匆匆而过,但......

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

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

我想我得到的是,所有有状态的对象都可以是等待消息自我更新的actor.

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

当请求更改用户"foo@example.com"的密码时,您会向'Foo@Example.Com'发送消息!ChangePassword( "新密").

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

等等,等等.

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

有趣!


Rex*_*err 4

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

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

val service = actor {
  ...
  case m: MakeSomethingWithUsers() =>
    Futures.future { sender ! myExpensiveOperation(m) }
}
Run Code Online (Sandbox Code Playgroud)

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