如何解决使用可扩展 Node.js/Express/MongoDB 的方法为 1v1 游戏匹配用户
最近我一直在开发一款游戏,如果他们都试图加入具有相同参数的游戏,该游戏会自动将两个用户匹配到一起进行对战。这种匹配不是并发的,这意味着两个用户不必同时在线来匹配它们。我正在使用 MongoDB 来存储游戏,其架构如下所示。
{
competitorID1: {type: String,unique: false,required: true},competitorID2: {type: String,required: false,default: null},needsFill: {type: Boolean,required: true,default: true},parameter: {type: Number,required: true}
}
目前,这就是我创建/加入游戏的控制器功能的样子。
exports.join = (req,res) => {
Game.findOneAndUpdate({
needsFill: true,parameter: req.body.parameter,competitorID1: {$ne: req.user._id}
},{
// Add the second competitor
needsFill: false,competitorID2: req.user._id,}).then(contest => {
if(contest) {
// Game was found and updated to include two competitors
}
else {
// Game with parameter was not found. Create a new game with needsFill set to true and only the competitorID1 populated
}
});
}
我的问题是:这是否会扩展到同时使用系统并尝试加入游戏的大量用户?我的理解是
findOneAndUpdate
mongoose 提供的函数是原子的,因此可以防止多个请求并发修改。如果我的假设不好,请原谅我,我仍在尝试使用 MongoDB 找到自己的立足点。即使这确实工作正常,请告诉我是否有更好的方法来实现我的目标。非常感谢您提供任何帮助!
解决方法
我的理解是 findOneAndUpdate
函数由
猫鼬是原子的,因此防止并发修改
多个请求。
您的理解是正确的,findOneAndUpdate
是单文档操作,这意味着更改是原子应用的,客户端永远不会看到部分更新的文档。
如果您已将应用程序部署在独立的 mongod
服务器上,那么您的安全率为 99.99%。另一方面,如果您设置了副本集,您的客户可能会体验到一件有趣的事情。假设您在 5 个成员的副本集上(1 个主副本和 4 个辅助副本)。在某些时候有一个网络分区
在很短的时间内,有 2 名成员暂时认为是初选,在本例中为 Pold
和 Pnew
。对于像 w: 1
这样的弱写入问题,旧的 Primary (Pold
) 可能是唯一具有更新文档的主节点。如果在那个短时间范围内,同一文档的新 findOneAndUpdate
到达 Pnew
,则 2 个节点对同一场比赛的看法不一致。
尽管如此,在恢复分区之间的连接后,Pold
更改会回滚,数据库返回到以 Pnew
作为新主数据库的一致状态。为了缓解这种罕见的情况,您可以将更新与具有写关注 ClientSession
的 w: majority
相关联,而不是默认的 (1
)。
您没有提供有关您如何设计项目架构的任何信息,但我想说您实际上并不需要这种额外的复杂性。
即使这可以正常工作,请告诉我是否有 实现目标的更好方法
同样,我不知道 needsFill
是否还有其他含义,但如果它只是让您决定是否填充 competitorID2
,为什么不删除它而是检查 competitorID2
{1}} 是 null
?
简短的回复将是:
是的,它会扩展,因为正如您所说,您正在使用原子操作。
但是,如果您有 200 个用户或 2 亿用户,如何确保性能保持不变。 让我们来看看您的应用的不同方面:
- 逻辑流程
- 架构
- 安全
- 请求
- 索引
- 可扩展性
1.逻辑流程
- 我是一名用户,我想与对手开始一场比赛。
- 服务器将尝试查找我未玩的游戏,并且:
- 创建游戏并等待对手
- 在现有游戏中添加我作为对手
- 然后你有自己的逻辑来做某事。
2.架构
我相信简单是关键,所以如果你在玩 1v1 游戏 >
{
status: {type: String,unique: false,required: true},playerId: {type: String,opponentId: {type: String,required: false,default: null},gameType: {type: Number,}
- 我用 status 字段替换了 NeedFill ,因为它会给你带来弹性,详情见下面的
4.requests
。 - 我也修改了命名。我相信,如果你想用你的逻辑让游戏有更多的用户,你会使用
players
的数组而不是competitorID1
、competitorID2
、...
- 我将参数重命名为 gameType,因为
parameter
对于名称来说似乎太大了,而且我们并不真正理解它的含义,我假设您指的是游戏类型。
此外,您可以在 mongo 中实现模式验证以保持数据一致性 https://docs.mongodb.com/manual/core/schema-validation/
db.runCommand({
collMod: "game",validator: {
$jsonSchema: {
bsonType: "object",required: ["status","playerId","gameType"],properties: {
status: {
bsonType: "string",description: "must be a string and is required",},playerId: {
bsonType: "string",opponentId: {
bsonType: "string",description: "must be a string",gameType: {
bsonType: "int",minimum: 1,maximum: 3,description: "must be an integer in [ 1,3 ] and is required",});
3.安全
从您的应用中获取用于在数据库中查询/插入数据的数据是危险的。您不能信任请求对象。如果我发送一个数字或对象怎么办?它可能会破坏您的应用。
因此请始终验证您的数据。
(req,res) => {
// You should have a validator type on top of your controller
// Something that will act like this
if (!req.body?.user?._id || typeof req.body.user._id !== "string") {
throw new Error("user._id must be defined and a type string");
}
if (!req.body?.gameType || typeof req.body.gameType !== "number") {
throw new Error("gameType must be defined and a type number");
}
};
4.请求
我更喜欢使用 async await 因为我发现它在代码中更清晰,而不是有很多逻辑子功能。
async (req,res) => {
let game = await Game.findOneAndUpdate(
{
status: "awaiting",gameType: req.body.gameType,playerId: { $ne: req.body.user._id },{
$set: {
status: "ready",opponentId: req.user._id,{
upsert: true,}
);
// Create the game if it doesn't exist
if (!game) {
game = await Game.insertOne({
status: "awaiting",playerId: req.body.user._id,});
}
// Next logic...
};
状态将帮助您涵盖游戏的更多方面:等待、播放、完成……您只需要一个索引即可快速获取这些统计信息。
我建议稍后通过添加以下属性来改进您的架构:startDate、finishDate、...
5.索引
为确保您的查询不会消耗集群的所有资源,请根据您执行的查询创建索引。 在上面的例子中:
db.game.createIndex({status:1,gameType:1,playerId:1},{background:true})
6.可扩展性
既然您的应用可以完成大部分工作,那么您如何确保可以正确扩展?
您需要确保您的应用可扩展以处理您的请求。这取决于您的基础设施资源和类型(无服务器、您自己的实例,...)。使用不同的网络指标来调整您需要的 ram 和 cpu。
首先您还需要确保您的 Mongo 服务器可以通过使用集群进行扩展,根据您的资源使用情况自动扩展您的集群。
您可以水平扩展(添加更多机器)或垂直扩展(添加更多资源)
可以涵盖更多点以提高速度和可扩展性(缓存、分片等),但我认为以上内容足以让您开始。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。