在Spring Web中更新/通知其他用户

时间:2018-10-27 09:22:57

标签: java spring hibernate spring-web

我遇到一些设计/实现问题,只是无法解决。我目前正在与多个玩家一起开发基于文本的游戏。我有点理解它如何用于Player-to-Server,这意味着Server会将每个Player视为相同。

我正在使用spring-boot 2,spring-web,thymeleaf,hibernate。

我实现了自定义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会反映当前用户的说话(播放器1或2或3,依此类推)。对于大多数自己发生的事情,例如购买一些东西,更新配置文件等,它都可以正常工作。 但是我无法或不知道如何实现两个玩家互动时

例如,玩家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应将更新的统计信息从db拉到内存。

  2. 如何在此处解决可能的并发问题?例如,播放器2的生命值在数据库中为50。玩家2然后执行一些动作(例如购买健康药水+ 30),然后更新数据库(生命值至80)。但是,就在DB更新之前,Player 1已经发起了攻击,并从DB夺回了Player 2的状态,由于DB尚未更新,Player 2将返回50。因此,现在,getUserByPlayername()中所做的任何更改以及对DB的更新都是错误的,并且Player的整个状态将为“不同步”。我希望我在这里有意义。

我知道休眠中有@Version用于乐观锁定,但是我不确定在这种情况下是否适用。在这种情况下,春季会议会有用吗?

用户登录时,我是否应该将任何数据都不存储在内存中?当执行某些操作时,是否应该总是仅从数据库 检索数据?就像viewProfile一样,然后我从accountRepository中提取。或当viewStats时,我从statsRepository等处提取。

指向正确的方向。对于任何具体示例或某种视频/文章,将不胜感激。如果需要任何其他信息,请告诉我,我将尽力解释我的情况。

谢谢。

2 个答案:

答案 0 :(得分:2)

我认为您不应该在Controller方法中更新currentUser,也不应依赖该对象中的数据来表示玩家的当前状态。可能有多种方法可以使它起作用,但是您需要弄乱更新安全性上下文。

我还建议您通过id而不是userName查找“用户”,因此将使用该方法编写此答案的其余部分。如果您坚持要通过userName查找Users,请在必要时进行调整。

因此,为了简单起见,我将在Controller中引用accountRepository,然后,每当需要获取或更新玩家状态时,请使用

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

是的,@Version和乐观锁定将有助于解决您所关注的并发问题。您可以从数据库中重新加载实体,如果发现@OptimisticLockException,请重试该操作。或者,您可能想用诸如“玩家2刚购买了治愈药水,现在已经80希瑟,你还想进攻吗?”之类的东西来回应玩家1。

答案 1 :(得分:1)

我不是Spring用户,但我认为问题比技术上更具概念性。
我将尝试提供一种使用通用方法的答案,同时以JavaEE风格编写示例,以使它们应易于理解,并希望可以移植到春天。

首先:每个单独的DETACHED实体都是陈旧的数据。而且陈旧的数据也不是“可信的”。

所以:

  1. 每个修改对象状态的方法应从事务内部的DB重新获取该对象:

    updatePlayerState()应该是事务边界方法(或在tx内部调用),getUserByPlayername(player)应该从数据库中获取目标对象。

    JPA发言:em.merge()被禁止(没有适当的锁定,即@Version)。

    如果您(或春季)已经在进行此操作,则没有什么可补充的。
    WRT您在您的2.中提到的“丢失更新问题”。请注意,这涵盖了应用程序服务器端(JPA / Hibernate),但在DB端可能也存在同样的问题,至少应正确配置此问题, 可重复读取隔离。如果正在使用MySQL does not conform to Repeatable Read really,请看一下。



  1. 您必须处理引用过时的Players / Users / Objects的控制器字段。您至少有两个选择。

    1. 针对每个请求重新获取:假设Player1攻击了Player2,并将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应该是服务器管理的对象。