Mongodb 成就系统的schema最佳存储

Mongodb schema best storage of Achievement system

我打算在 Mongodb 中创建一个成就系统。但我不确定我将如何 format/store 它在数据库中。

至于用户应该有一个进步(在每项成就上他们都会有一些 progress value 存储),我真的很困惑什么是执行此操作的最佳方法,并且没有性能问题。

我该怎么办?因为我不知道,我的想法可能是这样的:

我是否应该将每个成就存储在成就集合中的唯一行中,并在该行中存储一个用户数组,包含具有用户 ID 和成就进度的对象?

当它的 1000+ 成就时,我会遇到性能问题吗,那是经常被检查的仙女?

或者我应该做点别的吗?

上述选项的示例模式:

  {  
   name:{  
      type:String,
      default:'Achievement name'
   },
   users:[  
      {  
         userid:{  
            type:String,
            default:' users id here'
         },
         progress:{  
            type:Number,
            default:0
         }
      }
   ]
}

您可能正在寻找徽章样式的实现。就像 Stack Overflow 奖励它的用户取得特定成就的徽章一样。

方法 1:您可以在用户个人资料中为每个徽章设置标志。由于您是在 NoSQL 数据库中执行此操作,因此您只需为每个徽章设置一个标志。

const badgeSchema = new mongoose.Schema({
  badgeName: {
    type: String,
    required: true,
  },
  badgeDescription: {
    type: String,
    required: true,
  }
});
const userSchema = new mongoose.Schema({
  userName: {
    type: String,
    required: true,
  },
  badges: {
    type: [Object],
    required: true,
  }
});

如果您的应用程序架构是基于事件的,您可以触发向用户授予徽章。该操作只是在 User badges 数组中插入 Badge 对象并取得进展。

{ 
    badgeId: ObjectId("602797c8242d59d42715ba2c"),
    progress: 10
}

更新操作将查找并更新带有进度百分比数字的徽章数组

在用户界面上显示用户成就时,您可以循环遍历 badges 数组来显示该用户获得的徽章及其进度。

方法 2: 有一个单独的 mongo 徽章和用户映射集合。每当用户获得徽章时,您就会在该集合中插入一条记录。它将是用户 _id 和徽章 _id 和进度值的一对一映射。但是随着 table 变得越来越大,您将需要进行索引以有效地查询用户和徽章映射。

您必须根据您的具体用例对最佳方法进行分析。

MongoDB 足够灵活,可以让团队快速开发应用程序,并在应用程序需要时让他们的模型包含垃圾摩擦。如果您从第一天起就需要一个稳健的模型,他们的方法是一种灵活的方法,可以指导您完成数据建模过程。

methodology组成:

  1. 工作负载:此阶段是关于收集尽可能多的信息以了解您的数据。这将允许您制定假设,您的数据大小将针对它的性能(读取和写入),量化操作和限定操作。

您可以通过以下方式获得:

  • 场景
  • 原型
  • 生产日志和统计信息(如果您正在迁移)。
  1. 关系:确定数据中不同实体之间的关系,量化这些关系并应用嵌入或链接。一般来说,默认情况下您应该更喜欢嵌入,但请记住,数组不应无限制地增长 (6 Rules of Thumb for MongoDB Schema Design: Part 3)。

  2. 模式:应用模式设计模式。看看 Building with Patterns: A Summary,它提供了一个矩阵,突出显示了可能对给定用例有用的模式。

最后,此方法的目标是帮助您创建一个模型,该模型可以在压力下扩展并表现良好。

如果你这样设计成就模式:

{
  name: {
    type: String,
    default: "Achievement name",
  },
  userid: {
    type: String,
    default: " users id here",
  },
  progress: {
    type: Number,
    default: 0,
  },
}
}

获得成就后,您只需添加另一个条目

获得成就 Map-Reduce 是数据库上 运行ning map reduce 的一个很好的候选者。您可以 运行 不定期地使用它们来离线计算您想要的数据。

基于documentation你可以像下面的照片那样做

即使问题是专门针对数据库设计的,我也会针对 tracking/awarding 逻辑给出一个解决方案,以便为数据库设计建立更准确的上下文。

我会将成就进度与已授予的成就分开存储,以便更清晰地跟踪和发现。

整个逻辑都是基于事件的,有多层事件处理。这为您提供了跟踪数据的灵活性,并为您提供了一个很好的跟踪历史的机制。基本上,您可以将其视为一种日志记录形式。

当然,您的系统设计和合同在很大程度上取决于您要跟踪的信息及其复杂性。一个简单的 progress 字段可能不足以满足每种情况(您可能想要跟踪更复杂的东西,而不是 X 和 Y 之间的简单数字)。还有更新非常频繁的跟踪数据的情况(例如,在游戏中行进的距离)。您没有提供有关成就系统主题的任何背景信息,因此我们将坚持使用通用解决方案。这只是您应该注意的几件事,因为它会影响设计。

好吧,那么,让我们从头开始,针对一个被追踪的数据,追踪整个流程,最终实现进度。假设我们正在跟踪 连续 天的用户登录,当他达到 [10] 时,我们将奖励他一项成就。

请注意,下面的所有内容都只是一个伪代码

那么,假设今天是 [2017 年 7 月 8 日]。现在,我们的 User 实体看起来像这样:

User: {
   id: 7;
   trackingData: {
      lastLogin: 7 of July, 2017 (should be full DateTime object, but using this for brevity),
      consecutiveDays: 9
   },
   achievementProgress: [
      {
         achievementID: 10,
         progress: 9
      }
   ],
   achievements: []
}

我们的成就集合包含以下实体:

Achievement: {
   id: 10,
   name: '10 Consecutive Days',
   rewardValue: 10
}

用户尝试登录(或访问网站)。应用程序处理程序注意到这一点,并在处理登录逻辑后触发类型为 ACTION:

的事件
ACTION_EVENT = {
   type: ACTION,
   name: USER_LOGIN,
   payload: {
      userID: 7,
      date: 8 of July, 2017 (should be full DateTime object, but using this for brevity)
   }
}

我们有一个 ActionHandler 监听类型为 ACTION:

的事件
ActionHandler.handleEvent(actionEvent) {
   subscribersMap = Map<eventName, handlers>;

   subscribersMap[actionEvent.name].forEach(subscriber => subscriber.execute(actionEvent.payload));
}

subscribersMap 为我们提供了一组处理程序,这些处理程序应该响应每个特定的操作(这应该为我们解析为 USER_LOGIN)。在我们的例子中,我们可以有 1 个或 2 个关注更新 user 实体中 lastLoginconsecutiveDays 跟踪属性的用户跟踪信息。我们案例中的处理程序将更新跟踪信息并进一步触发新事件。 再一次,为简洁起见,我们将两者合而为一:

updateLoginHandler: function(payload) {
   user = db.getUser(payload.userID);
   
   let eventType;
   let eventValue;

   if (date - user.trackingData.lastLogin > 1 day) {
      user.trackingData = 1;

      eventType = 'PROGRESS_RESET';
      eventValue = 1;
   }
   else {
      const newValue = user.trackingData.consecutiveDays + 1;

      user.trackingData.consecutiveDays = newValue;

      eventType = 'PROGRESS_INCREASE';
      eventValue = newValue;
   }

   user.trackingData.lastLogin = payload.date;

   /* DISPATCH NEW EVENT OF TYPE ACHIEVEMENT_PROGRESS */
   AchievementProgressHandler.dispatch({
      type: ACHIEVEMENT_PROGRESS
      name: eventType,
      payload: {
         userID: payload.userID,
         achievmentID: 10,
         value: eventValue
      }
   });
}

在这里,PROGRESS_RESETPROGRESS_INCREASE 具有相同的合同,但具有不同的语义含义,出于 history/tracking 的目的,我将它们分开。如果您愿意,可以将它们组合成一个 PROGRESS_UPDATE 事件。 基本上,我们更新依赖于 lastLogin 日期的跟踪字段并触发一个新的 ACHIEVEMENT_PROGRESS 事件,该事件应由具有相同模式(AchievementProgressHandler)的单独处理程序处理。在我们的例子中:

ACHIEVEMENT_PROGRESS_EVENT = {
   type: ACHIEVEMENT_PROGRESS,
   name: PROGRESS_INCREASE
   payload: {
      userID: 7,
      achievementID: 10,
      value: 10
   }
}

然后,在AchievementProgressHandler中我们遵循相同的模式:

AchievementProgressHandler: function(event) {
   achievementCheckers = Map<achievementID, achievementChecker>;

   /* update user.achievementProgress code */

   switch(event.name): {
      case 'PROGRESS_INCREASE':
         achievementCheckers[event.payload.achievementID].execute(event.payload);
         break;
      case 'PROGRESS_RESET':
         ...
   }
}

achievementCheckers 包含每个特定成就的检查器功能,用于决定成就是否已达到其预期值(进度为 100%)并应予以奖励。这使我们能够处理各种复杂的案件。如果您只跟踪单个 X out Y 场景,则可以在所有成就之间共享该功能。

处理程序基本上是这样做的:

achievementChecker: function(payload) {
   achievementAwardHandler;

   achievement = db.getAchievement(payload.achievementID);

   if (payload.value >= achievement.rewardValue) {
      achievementAwardHandler.dispatch({
         type: ACHIEVEMENT_AWARD,
         name: ACHIEVEMENT_AWARD,
         payload: {
            userID: payload.userID,
            achievementID: achievementID,
            awardedAt: [current date]
         }
      });

      /* Here you can clear the entry from user.achievementProgress as you no longer need it. You can also move this inside the achievementAwardHandler. */
   }   
}

我们再次派发事件并使用事件处理程序 - achievementAwardHandler。您可以跳过事件创建步骤并直接将成就奖励给用户,但我们会使其与整个历史记录流程保持一致。 这里的一个额外好处是,您可以使用处理程序将成就奖励推迟到特定的稍后时间,从而有效地为多个用户批处理奖励,这有几个目的,包括性能增强。

基本上,这个伪代码处理从[用户操作]到[成就奖励] 包括所有中间步骤。它不是一成不变的,您可以随意修改它,但总而言之,它为您提供了一个清晰的关注点分离,更清晰的实体,它的性能,让您添加复杂的检查和处理程序,这些在相同的情况下很容易推理时间提供了用户整体进度的重要历史日志。

关于数据库模式实体,我建议如下:

User: {
   id: any;
   trackingData: {},
   achievementProgress: {} || [],
   achievements: []
}

其中:

  • trackingData 是一个包含你想要的一切的对象 跟踪用户。美妙之处在于这里的属性是 独立于成就数据。您可以跟踪任何内容并最终将其用于成就目的。
  • achievementProgress<key: achievementID, value: data>的地图或 包含每个成就的当前进度的数组。
  • achievements: 一系列奖励成就。

Achievement:

Achievement: {
   id: any,
   name: any,
   rewardValue: any (or any other field/fields. You have complete freedom to introduce any kind of tracking with the approach above),
   users?: [
      {
         userID: any,
         awardedAt: date
      }
   ]
}

users 是已获得给定成就奖励的用户的集合。这是可选的,仅当您使用它并经常查询此数据时才会出现。