在MongoDB中使用自增来存储唯一用户ID序列

53

我正在制作一个分析系统,API调用将提供唯一用户ID,但它不是连续的且太稀疏。

我需要给每个唯一的用户ID分配自动增量ID,以在位数组/位集中标记分析数据点。因此,第一个遇到的用户对应于位数组中的第一个位,第二个用户对应于位数组中的第二个位,依此类推。

那么,在MongoDB中生成递增的唯一用户ID是否有可靠且快速的方法?


我遇到了和你一样的问题,就是如何生成ID来设置bitset的位置。你解决了这个问题吗? - brucenan
希望这篇文章能对你有所帮助:https://medium.com/@yesdeepakverma/implementing-sequence-types-in-mongodb-2de035582c23 - Deepak Verma
也许这可以帮助你:https://www.mongodb.com/blog/post/generating-globally-unique-identifiers-for-use-with-mongodb - Guihgo
9个回答

35

如选定答案所述,您可以使用findAndModify生成序列号。

但我强烈反对认为您不应该这样做的观点。这完全取决于您的业务需求。拥有12字节的ID可能会消耗大量资源,并在未来引起重大的可扩展性问题。

我在这里详细回答了这个问题


1
如果你愿意的话,可以这样做。但是我不同意,因为这是一个内置的Mongo特性,用于 .createIndex ( { "number" : 1 }, { unique : true } ),其中数字1代表递增,-1代表递减。 - Tino Costa 'El Nino'
3
@TinoCosta'ElNino',你所说的并没有创建一个增量字段,它只是在number字段上创建了一个索引,并且该索引是增量的并强制唯一性,它并不会自动增加字段,甚至不需要默认值。 - Amr Saber
1
实际上,就答案本身而言,我不认为每个文档12字节会对数据库/集合造成严重的扩展问题。将 _id 从12字节更改为4字节(BJSON限制)对于那些从12字节开始出现扩展问题的集合来说,可能会在一段时间后溢出。此外,您节省的这些字节相当于用户输入的8个字符(如果集合包含用户输入,这几乎总是情况),根本不值得付出努力和失去所有好处。 - Amr Saber

33

3
问题在于并发性。迭代会发出重复的增量ID。 - est
每次迭代都通过增量ID获取数据点是非常不明智的,特别是当你处理每个数据点的数百万个用户时。进行MAU需要大约30倍的迭代。 - est
1
当你一开始就有数百万用户时,使用增量序列是不明智的。然而,数百万用户也不适合使用位数组,对吧?我很难确定你到底想要实现什么。使用findAndModify并发将不会成为问题。另请参阅http://www.mongodb.org/display/DOCS/Object+IDs和HiLo算法:https://dev59.com/ZXVC5IYBdhLWcg3weBA- - mnemosyn
我只想将一些 Redis 位图数据存储在 Mongo 中,以备后续查询。参考链接:http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps/。 - est
看起来你需要在 Redis 的位图中将一些 UID 映射到位。如果是这样,你也可以在 Redis 中相当有效地存储这个映射关系。而且它会很好地扩展,因为映射是写入一次,只读取。 - Konstantin Pribluda
显示剩余7条评论

20

我知道这是一个老问题,但我将发布我的答案供后人参考...

这取决于您正在构建的系统和特定的业务规则。

我正在使用MongoDb、C# (后端 API)和Angular (前端 Web 应用程序)构建中到大型的 CRM,发现 ObjectId 在 Angular 路由中选择特定实体方面非常糟糕。同样适用于API控制器路由。

上面的建议对我的项目非常有效。

db.contacts.insert({
 "id":db.contacts.find().Count()+1,
 "name":"John Doe",
 "emails":[
    "john@doe.com",
    "john.doe@business.com"
 ],
 "phone":"555111322",
 "status":"Active"
});

对于我的情况,它是完美的解决方案,但并非适用于所有情况。正如上面的评论所述,如果你从集合中删除了3条记录,则会出现碰撞。

我的业务规则规定,由于我们内部SLA(服务级别协议)的原因,不允许在我正在编写的应用程序潜在生命周期之外删除通信数据或客户记录,因此,我只需用一个枚举“状态”标记记录,其值为“Active”或“Deleted”。你可以从UI中删除某些内容,它会显示“联系人已被删除”,但应用程序实际上只是更改了该联系人的状态为“Deleted”,当应用程序调用存储库以获取联系人列表时,我会在将数据推送到客户端应用程序之前过滤掉已删除的记录。

因此,db.collection.find().count() + 1 对于我来说是完美的解决方案......

它并不适用于所有人,但如果您不会删除数据,那么它就可以正常工作。

编辑

pymongo的最新版本:

db.contacts.count() + 1

使用函数和计数器序列而不是您提出的解决方案 db.xxx.find.count+1,MongoDB 是否有特殊原因? 也许事务处理会搞乱事情吗? 您的解决方案在 Web 服务器环境的CRUD操作中工作良好吗? 谢谢您的回答。 - ckinfos
4
在并发设置中这样做不好。如果同时进行计数,很容易获得具有相同 _id 的文档。 - zephos2014
2
当然可以!在我的情况下,我不需要处理并发或分片,所以使用find().Count()+1没有任何问题。就像我原来的答案一样,这种方法并不适用于所有人和所有情况,但在我的特定场景中,它绝对有效。该应用程序已经在生产中运行了近12个月,关于我的增加ID的问题,没有出现任何问题。 - Alex Nicholas
1
最好获取最大的ID,而不是计数。 - Amin Shojaei
1
作为软件设计师,你不能简单地说“我不需要处理并发”。你的中等到大型CRM将会失败,这只是时间问题。正如上面的人们正确指出的那样,如果两个用户同时尝试插入一个文档,你可能会得到重复的ID。 - Phil
显示剩余7条评论

1
我在MySQL中使用类似嵌套查询的方法来模拟自增,这对我很有效。要获取最新的id并将其加1,可以使用以下语句:
lastContact = db.contacts.find().sort({$natural:-1}).limit(1)[0];
db.contacts.insert({
    "id":lastContact ?lastContact ["id"] + 1 : 1, 
    "name":"John Doe",
    "emails": ["john@doe.com", "john.doe@business.com"], 
    "phone":"555111322",
    "status":"Active"
})

它解决了 Alex's answer 中提到的删除问题。因此,如果删除任何记录,则不会出现重复的 id更多解释:我只是获取最新插入文档的 id,将其加一,然后将其设置为新记录的 id。三元运算符用于当我们还没有任何记录或所有记录都被删除的情况。

1
// await collection.insertOne({ autoIncrementId: 1 });
const { value: { autoIncrementId } } = await collection.findOneAndUpdate(
  { autoIncrementId: { $exists: true } },
  {
    $inc: { autoIncrementId: 1 },
  },
);
return collection.insertOne({ id: autoIncrementId, ...data });

1
你的回答目前写得不够清晰,请进行[编辑]以添加更多细节,以帮助其他人理解它如何回答所提出的问题。你可以在帮助中心找到有关编写良好答案的更多信息。 - Community

1

第一条记录应该被添加

"_id" = 1    in your db

$database = "demo";
$collections ="democollaction";
echo getnextid($database,$collections);

function getnextid($database,$collections){

     $m = new MongoClient();
    $db = $m->selectDB($database);
    $cursor = $collection->find()->sort(array("_id" => -1))->limit(1);
    $array = iterator_to_array($cursor);

    foreach($array as $value){



        return $value["_id"] + 1;

    }
 }

对于空集合,这将失败。此外,对于大型集合来说,由于需要获取所有集合并对其进行排序,因此这将占用太多内存。它不会占用太多处理器,因为 _id 已经被索引了,但是仍然会占用很多内存。 - Amr Saber

1

我遇到了类似的问题,即我想生成唯一的数字作为标识符,但不一定必须如此。我想出了以下解决方案。首先初始化集合:

fun create(mongo: MongoTemplate) {
        mongo.db.getCollection("sequence")
                .insertOne(Document(mapOf("_id" to "globalCounter", "sequenceValue" to 0L)))
    }

然后提供一个返回唯一(且递增)数字的服务:

@Service
class IdCounter(val mongoTemplate: MongoTemplate) {

    companion object {
        const val collection = "sequence"
    }

    private val idField = "_id"
    private val idValue = "globalCounter"
    private val sequence = "sequenceValue"

    fun nextValue(): Long {
        val filter = Document(mapOf(idField to idValue))
        val update = Document("\$inc", Document(mapOf(sequence to 1)))
        val updated: Document = mongoTemplate.db.getCollection(collection).findOneAndUpdate(filter, update)!!
        return updated[sequence] as Long
    }
}

我相信id不会像其他解决方案一样在并发环境中出现弱点。


1
在获取最后一个ID和创建新文档之间会有一段时间,这两个操作不是原子操作。在并发操作中,您不能保证非原子操作将在其他线程执行其他操作之前执行。因此,以下情况可能发生在2个线程A和B中:A获取ID-> B获取ID-> B创建文档-> A创建文档。这将导致数据库键不一致。 - Amr Saber
解决方案是使用findOneAndUpdate在DB序列上同步,这是原子性的。因此,如果在线程切换之后获取ID,则会得到以下结果:1)获取文档A的ID,idA = 1; 2)获取文档B的ID,idB = 2; 3)保存B {id:2};4)保存A {id:1}。不可能引入不一致性。 - Marian
您将拥有比先前创建的文档ID更低的后期创建的文档。当然,这不是重复错误,但如果您依赖于ID的顺序(这就是大多数人使用增量ID的原因),它可能会引入问题。除此之外,我认为这是最好的解决方案之一,只是该问题没有本地支持,因此没有干净完全可行的解决方案。 - Amr Saber
完全同意。我只是没有把它视为不一致性。 - Marian

0
// First check the table length

const data = await table.find()
if(data.length === 0){
   const id = 1
   // then post your query along with your id  
}
else{
   // find last item and then its id
   const length = data.length
   const lastItem = data[length-1]
   const lastItemId = lastItem.id // or { id } = lastItem
   const id = lastItemId + 1
   // now apply new id to your new item
   // even if you delete any item from middle also this work
}

0

这可能是另一种方法

const mongoose = require("mongoose");

const contractSchema = mongoose.Schema(
  {
    account: {
      type: mongoose.Schema.Types.ObjectId,
      required: true,
    },
    idContract: {
      type: Number,
      default: 0,
    },
  },
  { timestamps: true }
);

contractSchema.pre("save", function (next) {
  var docs = this;
  mongoose
    .model("contract", contractSchema)
    .countDocuments({ account: docs.account }, function (error, counter) {
      if (error) return next(error);
      docs.idContract = counter + 1;
      next();
    });
});

module.exports = mongoose.model("contract", contractSchema);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接