Обновить / уведомить другого пользователя в Spring Web

У меня есть проблема с дизайном / реализацией, которую я просто не могу понять. В настоящее время я работаю над текстовой игрой с несколькими игроками. Я как бы понимаю, как это работает для Player-to-Server, я имел в виду, что сервер видит каждого отдельного игрока как одного и того же.

Я использую spring-boot 2, spring-web, тимелеаф, спящий режим.

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

@Entity
@Table(name = "USER")
public class User implements Serializable {

    @Id
    private long userId;

    @Column(unique = true, nullable = false)
    private String userName;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "playerStatsId")
    private PlayerStats stats;
}

public class CurrentUserDetailsService implements UserDetailsService {

    @Override
    public CurrentUser loadUserByUsername(String userName) {

        User user = this.accountRepository.findByUserName(userName)
                .orElseThrow(() -> 
                    new UsernameNotFoundException("User details not found with the provided username: " + userName));

        return new CurrentUser(user);
    }
}

public class CurrentUser implements UserDetails {

    private static final long serialVersionUID = 1L;
    private User user = new User();

    public CurrentUser(User user) {
        this.user = user;
    }

    public PlayerStats getPlayerStats() {
        return this.user.getStats();
    }

    // removed the rest for brevity 
}

Следовательно, в моем контроллере я могу сделать это, чтобы получить CurrentUser. * Обратите внимание, что каждый Пользователь также является игроком.

@GetMapping("/attackpage")
public String viewAttackPage(@AuthenticationPrincipal CurrentUser currentUser) {

    // return the page view for list of attacks

    return "someview";
}

currentUser здесь будет отражать текущего пользователя, скажем, (Player 1 или 2 или 3 и так далее). Что отлично работает для большинства вещей, которые происходят сами с собой, таких как покупка некоторых вещей, обновление профиля и т. д. Но чего я не могу понять или не знаю, как достичь, так это когда 2 игрока взаимодействуют.

Например, Игрок 1 атакует Игрока 2. Если я Игрок 1, то что я сделаю, это щелкну «Атака» в обзоре, выберу Игрока 2 и отправлю команду. Значит, в контроллере это будет примерно так.

@GetMapping("/attack")
public String launchAttack(@AuthenticationPrincipal CurrentUser currentUser, @RequestParam("playername") String player2) {

    updatePlayerState(player2);

    return "someview";
}

public void updatePlayerState(String player) {

    User user = getUserByPlayername(player);
    // perform some update to player state (say health, etc)
    // update back to db?
}

Вот что меня действительно смутило. Как было замечено ранее, когда каждый пользователь / игрок входит в систему, набор текущего состояния пользователя (игрока) будет извлечен из базы данных и сохранен «в памяти». Следовательно, когда Игрок 1 атакует Игрока 2,

  1. Как мне «уведомить» или обновить Игрока 2, что статистика изменилась, и, таким образом, Игрок 2 должен извлечь обновленную статистику из базы данных в память.

  2. Как здесь решить возможную проблему параллелизма? Например, здоровье игрока 2 составляет 50 в DB. Затем игрок 2 выполняет какое-то действие (скажем, покупает зелье здоровья + 30), которое затем обновляет БД (здоровье до 80). Однако непосредственно перед обновлением БД Игрок 1 уже запустил атаку и захватил из БД состояние Игрока 2, где он вернет 50, поскольку БД еще не обновлена. Итак, теперь любые изменения, внесенные в getUserByPlayername() и обновление базы данных, будут неправильными, и все состояние проигрывателя будет «рассинхронизировано». Надеюсь, я здесь понимаю.

Я понимаю, что @Version находится в спящем режиме для оптимистической блокировки, но я не уверен, применимо ли это в данном случае. И будет ли в таком случае полезна весенняя сессия?

Должен ли я не сохранять какие-либо данные в памяти при входе пользователя в систему? Должен ли я всегда получать данные из БД Только при выполнении какого-либо действия? Например, когда viewProfile, я извлекаю из accountRepository. или при viewStats я вытаскиваю из statsRepository и так далее.

Укажи мне правильное направление. Был бы признателен за любой конкретный пример или какое-то видео / статьи. Если потребуется дополнительная информация, дайте мне знать, и я постараюсь лучше объяснить свой случай.

Спасибо.

Можно было начать с здесь

Branislav Lazic 27.10.2018 11:42

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

user1778855 27.10.2018 17:44

вам определенно нужно хранить данные в памяти, но не в сеансе, вам нужен механизм кэширования, который вы можете контролировать состояние. сделать кеш недействительным> получить из БД> кешировать agian. Я предлагаю infinispan / hazelcast или что-то попроще, например ehcache / jcache

Omid P 04.11.2018 11:37

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

user1778855 04.11.2018 16:32
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
4
598
2

Ответы 2

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

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

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

User user = accountRepository.findById(currentUser.getId())

Да, @Version и оптимистическая блокировка помогут с проблемами параллелизма, которые вас беспокоят. Вы можете перезагрузить Entity из базы данных и повторить операцию, если вы поймаете @OptimisticLockException. Или вы можете ответить игроку 1 чем-то вроде: «Игрок 2 только что купил зелье исцеления, и сейчас ему 80 здоровья, вы все еще хотите атаковать?»

Спасибо @Bernie. Если вы имели в виду, что getUserByPlayername(player) будет использовать id вместо username, то вы правы. Я просто делал пример использования имени пользователя. Спасибо за идею, приму к сведению для @retry. Итак, всякий раз, когда пользователь совершает какое-либо действие, я всегда должен делать вызов БД для получения последней информации вместо сохранения «в памяти»? Это подчеркнет часть ввода-вывода?

user1778855 01.11.2018 09:06

Да, я рекомендую загружать из репозитория всякий раз, когда вам нужна «актуальная» информация или вы хотите ее изменить. Существует потенциальная проблема с производительностью из-за ввода-вывода, но ее можно в определенной степени смягчить с помощью кэширования. Если у вас больше, чем сервер приложений, вам нужно быть осторожным с кэшированием на сервере приложений, так как он может рассинхронизироваться с другими в кластере. Если DB / IO действительно становится проблемой, вам может потребоваться изучить продукт / библиотеку, такую ​​как Hazelcast, чтобы синхронизировать общий кеш.

Bernie 02.11.2018 00:29

Хорошо, спасибо за советы. Я также хотел бы проверить, можно ли указать параметр 2 как таковой в launchAttack, верно? Таким образом, у меня был бы игрок, который инициирует атаку, получая currentUser, и игрок, который атакует, получая параметр playername. Мне было интересно, есть ли лучшая альтернатива этому подходу.

user1778855 02.11.2018 18:57

Здесь есть потенциальные проблемы с безопасностью, поскольку вы берете идентификатор второго игрока из запроса, не проверяя, не был ли он «взломан». Игроку будет довольно просто подделать запрос и отправить имя пользователя / идентификатор любого игрока и потенциально атаковать игрока, который даже не находится поблизости. Тем не менее, как предотвратить такое поведение, стоит задать новый вопрос.

Bernie 04.11.2018 22:03

Да, я согласен, это был бы отдельный вопрос. Я учту это при разработке приложения. Большое спасибо.

user1778855 07.11.2018 15:24

Я не пользуюсь Spring, но считаю, что проблема скорее концептуальная, чем техническая. Я постараюсь дать ответ, который использует общий подход, при написании примеров в стиле JavaEE, чтобы они были понятными и, надеюсь, переносимыми в Spring.

Прежде всего: каждая ОТДЕЛЕННАЯ сущность - это устаревшие данные. А устаревшие данные не заслуживают доверия.

Так:

  1. каждый метод, изменяющий состояние объекта, должен повторно извлекать объект из БД внутри транзакции:

    updatePlayerState() должен быть метод границы транзакции (или вызываться внутри tx), а getUserByPlayername(player) должен извлекать целевой объект из БД.

    Говоря JPA: em.merge() запрещен (без надлежащей блокировки, т.е. @Version).

    если вы (или весна) уже этим занимаетесь, добавить особо нечего. WRT «проблема с утерянным обновлением», которую вы упомянули в своем 2. Имейте в виду, что это касается стороны сервера приложений (JPA / Hibernate), но та же самая проблема может присутствовать на стороне БД, которая должна быть правильно настроена, по крайней мере, для Изоляция повторяемое чтение. Взгляните на MySQL действительно не соответствует Repeatable Read, если вы его используете.



  1. вам нужно обрабатывать поля контроллера, которые относятся к устаревшим игрокам / пользователям / объектам. У вас есть как минимум два варианта.

    1. повторная выборка для каждого запроса: предположим, что Player1 атаковал Player2 и уменьшил HP Player2 на 30. Когда Player2 переходит в представление, которое показывает его HP, контроллер, стоящий за этим представлением, должен повторно получить объект Player2 / User2 перед рендерингом представления.

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

      то есть вы можете использовать @WebListener для перезагрузки вашего плеера / пользователя:

      @WebListener
      public class CurrentUserListener implements ServletRequestListener {
      
          @Override
          public void requestInitialized(ServletRequestEvent sre) {
              CurrentUser currentUser = getCurrentUser();
              currentUser.reload();
          }
      
          @Override
          public void requestDestroyed(ServletRequestEvent sre) {
              // nothing to do
          }
      
          public CurrentUser getCurrentUser() {
              // return the CurrentUser
          }
      }
      

      или bean-компонент с ограничением по запросу (или что-то вроде Spring-аналога):

      @RequestScoped
      public class RefresherBean {
          @Inject
          private CurrentUser currentUser;
      
          @PostConstruct
          public void init()
          {
              currentUser.reload();
          }
      }
      
    2. уведомить другие экземпляры контроллера: если обновление прошло успешно, уведомление должно быть отправлено другим контроллерам.

      т.е. используя CDI @Observe (если у вас есть CDI):

      public class CurrentUser implements UserDetails {
      
          private static final long serialVersionUID = 1L;
          private User user = new User();
      
          public CurrentUser(User user) {
              this.user = user;
          }
      
          public PlayerStats getPlayerStats() {
              return this.user.getStats();
          }
      
          public void onUpdate(@Observes(during = TransactionPhase.AFTER_SUCCESS) User user) {
              if (this.user.getId() == user.getId()) {
                  this.user = user;
              }
          }
      
          // removed the rest for brevity 
      }
      

      Обратите внимание, что CurrentUser должен быть объектом управляемый сервером.

Для RefresherBean object означает ли это, что для каждого HTTP-запроса объекты будут обновляться с помощью .reload(), который вызывает DB для заполнения последних данных? HTTP-запрос относится к индивидуальному запросу игрока, верно? Это означает, что игрок A щелкает ссылку, которая запускает HTTP-запрос, поэтому она перезагружается только для игрока A. Данные игрока B будут перезагружены только тогда и только тогда, когда игрок B также щелкнул ссылку, запускающую HTTP-запрос. Правильно ли я так говорю? CurrentUser относится к AuthenticationPrincipal.

user1778855 07.11.2018 15:44

Точно. Каждый запрос перезагружает только (единственный) объект User относительно «текущего» пользователя. Вместо этого ссылки на других игроков / пользователей на других контроллерах следует загружать по запросу.

Michele Mariotti 07.11.2018 15:56

Будет ли разница, если HTTP-запрос является запросом Ajax? Есть ли что-то, что нужно обработать, или это не имеет значения, поскольку ajax также является формой HTTP-запроса?

user1778855 07.11.2018 16:00

Запросы Ajax также являются HTTP-запросами. Неважно. В худшем случае, если вы повторно визуализируете только фрагмент представления, другие части представления могут содержать устаревшую информацию.

Michele Mariotti 07.11.2018 19:21

Другие вопросы по теме