Передача типичной 3-уровневой архитектуры актерам - PullRequest
21 голосов
/ 16 января 2011

Этот вопрос беспокоит меня уже некоторое время (надеюсь, я не единственный).Я хочу взять типичное 3-уровневое приложение Java EE и посмотреть, как оно может выглядеть реализованным с актерами.Я хотел бы выяснить, действительно ли имеет смысл делать такой переход и как я могу извлечь из этого выгоду, если это имеет смысл (может быть, производительность, лучшая архитектура, расширяемость, ремонтопригодность и т. Д.).

Вот типичные контроллеры (презентация), сервис (бизнес-логика), 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 = { ... }
}

Вы можете найти что-то подобное во многих весенних приложениях.Мы можем взять простую реализацию, которая не имеет общего состояния и потому что не имеет синхронизированных блоков ... поэтому все состояние находится в базе данных, и приложение зависит от транзакций.Сервис, контроллер и дао имеют только один экземпляр.Таким образом, для каждого запроса сервер приложений будет использовать отдельный поток, но потоки не будут блокировать друг друга (но будут блокироваться вводом-выводом БД).

Предположим, мы пытаемся реализовать аналогичную функциональность с актерами.Это может выглядеть так:

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")

То же самое будет сделано контроллером и службой.В этом случае весь рабочий процесс будет синхронным.Еще хуже - я могу обрабатывать только один запрос за раз (в то же время все остальные запросы будут попадать в почтовый ящик контроллера).Поэтому мне нужно сделать все это асинхронным.

Есть ли какой-нибудь элегантный способ асинхронного выполнения каждого шага обработки в этой настройке?

Насколько я понимаю, каждый уровень долженкаким-то образом сохранить контекст полученного сообщения, а затем отправить сообщение на уровень ниже.Когда уровень ниже отвечает с некоторым сообщением результата, я должен быть в состоянии восстановить начальный контекст и ответить с этим результатом исходному отправителю.Это правильно?

Более того, на данный момент у меня есть только один экземпляр актера для каждого уровня.Даже если они будут работать асинхронно, я все равно смогу параллельно обрабатывать только один контроллер, сервис и дао-сообщение.Это означает, что мне нужно больше актеров того же типа.Что приводит меня к LoadBalancer для каждого уровня.Это также означает, что если у меня есть UserService и ItemService, я должен загрузить их оба по отдельности.

У меня есть ощущение, что я что-то не так понимаю.Все необходимые настройки кажутся слишком сложными.Что вы думаете об этом?

(PS: Было бы также очень интересно узнать, как транзакции БД вписываются в эту картину, но я думаю, что это излишне для этой темы)

Ответы [ 5 ]

10 голосов
/ 16 января 2011

Избегайте асинхронной обработки, если только у вас нет явной причины для ее выполнения. Актеры - прекрасные абстракции, но даже они не устраняют сложность асинхронной обработки.

Я открыл эту истину трудным путем. Я хотел изолировать основную часть моего приложения от одной реальной точки потенциальной нестабильности: базы данных. Актеры на помощь! Актеры Akka в частности. И это было потрясающе.

С молотком в руке, я принялся бить каждый гвоздь в поле зрения. Пользовательские сессии? Да, они тоже могут быть актерами. Хм ... как насчет контроля доступа? Конечно, почему бы и нет! С растущим чувством неловкости я превратил свою доселе простую архитектуру в монстра: многоуровневые акторы, асинхронная передача сообщений, сложные механизмы, позволяющие справляться с ошибочными условиями, и серьезный случай уродов.

Я отступил, в основном.

Я сохранил актеров, которые давали мне то, что мне было нужно - отказоустойчивость для моего кода персистентности - и превратил всех остальных в обычные классы.

Могу ли я предложить вам внимательно прочитать Хороший вариант использования для Akka вопрос / ответы? Это может дать вам лучшее понимание того, когда и как будут полезны актеры. Если вы решите использовать Akka, вы можете посмотреть мой ответ на предыдущий вопрос о написании актеров с балансировкой нагрузки .

5 голосов
/ 16 января 2011

Просто рифф, но ...

Я думаю, что если вы хотите использовать актеров, вам следует выбросить все предыдущие шаблоны и придумать что-то новое, а затем, возможно, заново включить старые шаблоны (контроллер, дао и т. Д.), Чтобы заполнить пробелы.

Например, что если каждый пользователь является отдельным действующим лицом, сидящим в JVM или через удаленных участников во многих других JVM. Каждый Пользователь несет ответственность за получение сообщений об обновлениях, публикацию данных о себе и сохранение себя на диск (или в БД, или в Mongo, или в чем-либо другом).

Я предполагаю, что все ваши объекты с состоянием могут быть актерами, просто ожидающими обновления сообщений.

(Для HTTP (если вы хотите реализовать это самостоятельно) каждый запрос порождает актера, который блокируется до получения ответа (используя!? Или будущее), который затем форматируется в ответ. Вы можете порождать LOT я думаю об актерах.)

Когда приходит запрос на изменение пароля для пользователя "foo@example.com", вы отправляете сообщение на Foo@Example.Com! ChangePassword ( "новый секретный").

Или у вас есть процесс каталогов, который отслеживает местоположение всех действующих лиц. Актором UserDirectory может быть сам актер (по одному на JVM), который получает сообщения о том, какие действующие субъекты пользователя в настоящее время работают и каковы их имена, а затем передает им сообщения от участников запроса, делегатов другим субъектам федеративного каталога. Вы спросите UserDirectory, где находится пользователь, а затем отправите это сообщение напрямую. Актер UserDirectory отвечает за запуск актера User, если он еще не запущен. Актер User восстанавливает свое состояние, затем исключает обновления.

и т. Д. И т. Д.

Это интересно думать. Например, каждый пользователь-пользователь может сохранять себя на диске, время ожидания истекает через некоторое время и даже отправлять сообщения субъектам агрегации. Например, субъект User может отправлять сообщение субъекту LastAccess. Либо PasswordTimeoutActor может отправлять сообщения всем действующим лицам, сообщая им о необходимости изменения пароля, если их пароль старше определенной даты. Актеры-пользователи могут даже клонировать себя на других серверах или сохранять в нескольких базах данных.

Fun!

4 голосов
/ 16 января 2011

Сложные транзакции с большими вычислительными затратами сложно осуществить, что является одной из причин популярности баз данных. Поэтому, если вы спрашиваете, можете ли вы прозрачно и легко использовать акторы для замены всех транзакционных и хорошо масштабируемых функций базы данных (на мощь которых вы очень сильно опираетесь в модели Java EE), ответ - нет.

Но есть несколько трюков, которые вы можете сыграть. Например, если один из участников создает узкое место, но вы не хотите прилагать усилия для создания структуры фермы диспетчера / рабочего, вы можете перевести интенсивную работу в будущее:

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

Таким образом, действительно дорогие задачи порождаются в новых потоках (при условии, что вам не нужно беспокоиться об атомарности, взаимных блокировках и т. Д., Что вы можете сделать - но, опять же, решение этих проблем в общем-то непростое ) и сообщения отправляются туда, куда они должны идти независимо.

3 голосов
/ 17 января 2011

Как вы сказали, !!= блокирование = плохо для масштабируемости и производительности, смотрите это: Производительность между!и !!

Необходимость транзакций обычно возникает, когда вы сохраняете состояние вместо событий.Пожалуйста, взгляните на CQRS и DDDD (управляемый распределенным доменом дизайн) и Event Sourcing , потому что, как вы говорите, у нас еще нет распределенного STM.

3 голосов
/ 16 января 2011

Для транзакций с актерами, вы должны взглянуть на "Transcators" Akka, которые объединяют акторов с STM (программная транзакционная память): http://doc.akka.io/transactors-scala

Это очень классные вещи.

...