Подтвердить что ты не робот

Передача типичной трехуровневой архитектуры актерам

Этот вопрос беспокоит меня в течение некоторого времени (надеюсь, я не единственный). Я хочу взять типичное трехуровневое приложение 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 = { ... }
}

Во многих приложениях spring вы можете найти что-то подобное. Мы можем выполнить простую реализацию, которая не имеет никакого общего состояния, и потому, что у нее нет синхронизированных блоков... поэтому все состояние находится в базе данных, а приложение использует транзакции. Сервис, контроллер и дао имеют только один экземпляр. Поэтому для каждого запроса сервер приложений будет использовать отдельный поток, но потоки не будут блокировать друг друга (но будут заблокированы 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: Было бы также очень интересно знать, как транзакции БД вписываются в это изображение, но я думаю, что это излишне для этого потока)

4b9b3361

Ответ 1

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

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

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

Я, в основном, отказался.

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

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

Ответ 2

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

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

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

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

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

Когда приходит запрос на изменение пароля для пользователя "[email protected]", вы отправляете сообщение "[email protected]"! ChangePassword ( "новый секретный" ).

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

Etc и т.д.

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

Fun!

Ответ 3

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

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

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

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

Ответ 4

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

Это довольно здорово.

Ответ 5

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

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