我遇到一些设计/实现问题,只是无法解决。我目前正在与多个玩家一起开发基于文本的游戏。我有点理解它如何用于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时,
我如何“通知”或更新播放器2统计信息已更改,因此,播放器2应将更新的统计信息从db拉到内存。
如何在此处解决可能的并发问题?例如,播放器2的生命值在数据库中为50。玩家2然后执行一些动作(例如购买健康药水+ 30),然后更新数据库(生命值至80)。但是,就在DB更新之前,Player 1已经发起了攻击,并从DB夺回了Player 2的状态,由于DB尚未更新,Player 2将返回50。因此,现在,getUserByPlayername()
中所做的任何更改以及对DB的更新都是错误的,并且Player的整个状态将为“不同步”。我希望我在这里有意义。
我知道休眠中有@Version
用于乐观锁定,但是我不确定在这种情况下是否适用。在这种情况下,春季会议会有用吗?
用户登录时,我是否应该将任何数据都不存储在内存中?当执行某些操作时,是否应该总是仅从数据库 检索数据?就像viewProfile一样,然后我从accountRepository中提取。或当viewStats时,我从statsRepository等处提取。
指向正确的方向。对于任何具体示例或某种视频/文章,将不胜感激。如果需要任何其他信息,请告诉我,我将尽力解释我的情况。
谢谢。
答案 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实体都是陈旧的数据。而且陈旧的数据也不是“可信的”。
所以:
每个修改对象状态的方法应从事务内部的DB重新获取该对象:
updatePlayerState()
应该是事务边界方法(或在tx内部调用),getUserByPlayername(player)
应该从数据库中获取目标对象。
JPA发言:em.merge()
被禁止(没有适当的锁定,即@Version)。
如果您(或春季)已经在进行此操作,则没有什么可补充的。
WRT您在您的2.中提到的“丢失更新问题”。请注意,这涵盖了应用程序服务器端(JPA / Hibernate),但在DB端可能也存在同样的问题,至少应正确配置此问题, 可重复读取隔离。如果正在使用MySQL does not conform to Repeatable Read really,请看一下。
您必须处理引用过时的Players / Users / Objects的控制器字段。您至少有两个选择。
针对每个请求重新获取:假设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();
}
}
通知其他控制器实例:如果更新成功,则应将通知发送给其他控制器。
即使用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
应该是服务器管理的对象。