后台作业 #

在后台将复杂或冗长的任务作为计划作业运行是一种常见的工程做法。这通常在游戏服务器环境中用于执行数据维护或运行任务,例如在特定时间向整个玩家群分发玩家每日奖励。在玩家群体很小时,这些任务通常很快,没有问题;然而,随着玩家群体开始增长,执行这些任务所需的时间也将延长。这可能会在一天中的特定时间在服务器上产生高CPU和数据库负载峰值,导致这些时段内的性能和玩家体验下降。

使用实时(JIT)方法可以解决这一问题。请看以下示例,在该示例中,无论玩家当天是否登录游戏都会获得100枚金币。

后台任务方法 #

使用后台作业的方法,可以在对玩家群影响最小的时间安排一个任务,比如午夜,然后在所有玩家中循环运行该任务,向其发放100枚金币。

这里可以设置一些巧妙的过滤,即,只在某段时间内活跃的玩家中循环,但在这种情况下,我们希望向每个玩家发放金币,无论他们最近是否登录游戏。

在最佳情况下,该方法的时间复杂度为O(n)量级;因此,随着玩家群体的扩大,执行此任务所需的时间也会随之延长。

可想而知,此任务将给服务器的CPU和数据库不断地带来越来越大的负担,并最终在这段时间内对您的玩家造成网络性能影响。

实时方法 #

与每天午夜进行批量金币分发不同,实时方法将在玩家下次登录游戏时向他们分发适量的金币。

要实现这一点,您需要在玩家登录时采取以下步骤:

  • 通过用户元数据上的属性或存储引擎中的值查看玩家上次登录的时间
  • 如果没有上一次登录,向玩家发放100枚金币
  • 如果有上一次登录,请根据玩家上次登录后的时间长短,向其支付所欠的金币金额
  • 将上次登录时间戳存储为当前时间

无论哪个用户在一天中的什么时间登录,该任务的时间复杂度始终为O(1)量级。

此方法有三个好处:

  1. 这项任务对CPU/数据库的需求分成小块分布在一天中的有需要时
  2. 玩家不会像在大型玩家群中执行任务时那样,体验到网络性能下降的峰值
  3. 从不重新登录游戏的玩家不会花费任何CPU/数据库处理时间

如何实现JIT方法 #

使用同样的例子,在午夜每天向每个玩家分发100枚金牌,让我们使用上述JIT方法来实现它。

由于我们希望在用户登录游戏时触发此操作,因此我们将为用户使用设备ID进行身份验证设置After挂钩。我们将在InitModule函数内注册它。

1
2
3
let InitModule: nkruntime.InitModule = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  initializer.registerAfterAuthenticateDevice(afterAuthenticateDevice)
}

然后定义要运行的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
let afterAuthenticateDevice : AfterHookFunction<Session, AuthenticateDeviceRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, session: nkruntime.Session, request: nkruntime.AuthenticateDeviceRequest) {
  // Define a constant for how many milliseconds are in a day
  const msInADay = 1000 * 60 * 60 * 24;

  // Get previous midnight
  const previousMidnight = new Date().setHours(0, 0, 0, 0);

  // Get the user's account so that we can check their metadata for a lastLoginTimestamp
  const account = nk.accountGetId(ctx.userId);
  const lastLoginTimestamp : number = account.user.metadata.lastLoginTimestamp;

  let rewardCoins = 0;

  if (!lastLoginTimestamp) {
    // If the user has never logged in, give them 100 coins
    rewardCoins = 100;
    logger.debug("Player has never logged in before, giving them 100 coins.");
  } else if(lastLoginTimestamp < previousMidnight) {
    // If the user's last login was before midnight, we're going to give them at least 100 coins
    rewardCoins = 100;

    // Calculate how many full days have passed since midnight and the time they last logged in
    const timeBetweenMidnightAndLastLogin = previousMidnight - account.user.metadata.lastLoginTimestamp;
    const totalFullDaysSinceLastLogin = Math.floor(timeBetweenMidnightAndLastLogin / msInADay);

    // Increase the reward by 100 for each full day since last login
    rewardCoins += totalFullDaysSinceLastLogin * 100;

    logger.debug(`Player has logged in for the first time today and it has been a further ${totalFullDaysSinceLastLogin} days; giving them ${rewardCoins} coins.`);
  } else {
    logger.debug("Player has already logged in today, no coins are being rewarded.");
  }

  // Update the user's lastLoginTimestamp
  const metadata = account.user.metadata;
  metadata.lastLoginTimestamp = Date.now();
  nk.accountUpdateId(ctx.userId, null, null, null, null, null, null, metadata);

  // Give the player their coins
  nk.walletUpdate(ctx.userId, { coins: rewardCoins });
};

从上例可见,当用户登录时,afterAuthenticateDevice函数作为挂钩运行。该函数检查自用户上次登录(如果有)后有多长时间,并为当天首次登录奖励100枚硬币,对自上次登录以来每多出一个全天再奖励100枚硬币。

本示例使用了一个在用户进行身份验证后运行的挂钩,但是您不必受限于此。您可以在用户会话的不同点执行所需的逻辑。例如,在用户链接另一种身份验证方法之前,或者在其添加好友之后执行逻辑。也可以让用户调用自定义RPC 函数。关于挂钩的完整列表,请参阅文档的消息名称部分。