近期在使用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重复,则需要在写入前先执行查询。