近期在使用mongodb的過程中遇到一次表中有幾百條_id字段重復的記錄(相同_id的有兩條),着實嚇了一大跳,因為_id字段在mongodb里面已經默認創建了唯一索引,理論上是不可能有重復記錄的,因此特把排查過程記錄下來。
1. 問題定位
發現這個現象,是在定位一個問題的時候,發現了這批重復臟數據,bug出現的步驟:把一條記錄中的某個字段修改后,再執行save方法,由於修改的字段是shard key,且保存的時候路由到另外一組shard(和原記錄的shard不同),導致了重復_id的出現。
2. 問題復現
首先,准備測試元數據,插入腳本如下:
db.auth("test","test"); var total = 500; var page = 1000; for(i=1; i<=total; i++){ for(var j 0= 1; j <= page; j++){ db.users.save({'_id': 'user'+i+"-"+j,'age':j,"content":"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901223456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567823456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"}); } }
其中content字段的內容很長,1000多個字符,這樣50w條的數據量是滿足分片后的數據遷移條件的(數據量太小,mongodb是不會遷移的)。
把該文件保存在mongo可執行程序的目錄,再執行數據插入:
/mongo 127.0.0.1:30000/test saveUser.js
隨后對test集合創建索引,並進行分片:
db.users.createIndex({"age":1})
sh.enableSharding("test") sh.shardCollection("test.user", { age: 1 } )
等待分片數據遷移結束后,查看分片狀態:
sh.status()
user表的分片數據如下:
{ "_id" : "test", "primary" : "rep1", "partitioned" : true } test.user shard key: { "age" : 1 } unique: false balancing: true chunks: rep1 14 rep2 11 too many chunks to print, use verbose if you want to force print
基礎數據已經准備完畢了,下面開始造數據,首先查詢到第一條記錄內容如下:
{ "_id" : "user1-1", "age" : 1.0, "content" : "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901223456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567823456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
然后把該記錄的內容拷貝一份,並把age修改為1000,然后再保存到users集合中:
MongoDB Enterprise mongos> db.users.save({ "_id" : "user1-1", "age" : 1000.0, "content" : "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901223456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567823456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" })
此時_id為“user1-1”的記錄已經有兩條了:
MongoDB Enterprise mongos> db.users.find({"_id":"user1-1"}) { "_id" : "user1-1", "age" : 1000, "content" : "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901223456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567823456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" } { "_id" : "user1-1", "age" : 1, "content" : "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901223456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567823456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
3. 避免措施
從以上分析可知,在對分片集合進行修改操作或者新寫入操作時,要特別注意,由於shard key的路由問題,可能會導致_id字段或者其他唯一字段重復記錄(保存在不同的shard中),為了避免重復記錄,選擇shard key時,可以把唯一字段也加入到shard key中,以本次測試為例,shard key可以設置為{"age":1, "_id":1},如果不想把_id加入到shard key中,且業務上面不允許_id重復,則需要在寫入前先執行查詢。