注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

snoopyxdy的博客

https://github.com/DoubleSpout

 
 
 

日志

 
 

mongodb多字段唯一索引表设计  

2017-02-18 12:18:44|  分类: golang |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
原始表设计是这样的:

_id, keyA, keyB, keyC, ...其他字段忽略

其中  keyA, keyB, keyC 是联合唯一索引
keyA 有20万左右
keyB 对于每个keyA有800个左右
keyC 和keyB是一对多的关系,对于每个keyB有800个左右,
就是一个keyB绝大部分只会对应1个keyC,
偶尔会对应2-3个(这里忽略不计)

如果全部打平存储,我们的数据量是:(忽略keyB对应多个keyC的场景)
200000*800 = 1.6亿条

crud的操作也很简单,对各个主要字段进行更新就可以了。
因为业务场景,所以每次CRUD操作都会带上keyA,
所以我们可以用keyA作为片键
对于1.6亿条数据,我们就需要分4片集群,
每片2台物理机,加两台路由机器,加3台config(路由机跑掉),
共需要10台物理机


这样操作的优点和缺点:
优点:
1、数据结构简单清晰,程序员编码简单
2、CRUD操作的语句编写简单

缺点:
1、数据量大
2、机器消耗大10台物理机的投入
3、每次update操作都会造成索引的重排,性能可能不理想
4、当查询数据流量大,查询12个KeyA,就相当于查询上万多个keyB时
5、对于更新和读取,每片都需要在4千万的数据量上进行检索,
读取性能可能存在问题


下面我们对这个业务需求进行一下结构优化

_id,
keyA, //唯一索引
keyBs:{
keyB1:{
keyC1:1,
},
keyB2:{
keyC2-1:1,
keyC2-2:1,
},...
},
... 其他字段

这样改有几个优点和缺点:
优点:
1、数据的文档条数,从1.6亿减少为20万,所以在20万条数据量检索,
性能会比4千万好不少
2、索引只有keyA一个唯一索引,索引相对简单,对于update操作性能好
3、对于20万条记录,只需要一个3台机器的副本集就可以了,
相对机器消耗较少
4、当查询12个keyA时,流量消耗也相对较少


缺点:
1、数据结构较复杂,程序员编写程序难度大
2、相比原始方案,对于只查询一条或几条记录的情况,
新方案每次都会把整个内嵌文档都响应出去,性能差
3、由于Mongodb3的存储引擎是文档锁,在非常频繁的更新文档的场景下,
新方案对于同一个keyA瞬间并发很大的update,性能不佳
4、对于keyB和keyC,只能使用string类型,
如果是其他类型需要在程序获取到数据后手动转类型
5、无法单独对keyC进行操作,比如根据keyA更新掉某些keyC的内容
6、无法对KeyB进行单独更新操作,比如根据keyA更新掉某些keyB的内容,
而不传入keyC的内容
对于5,6这样的需求只能两段式提交了(不安全,非原子性操作)


那么新方案我们究竟改如何进行crud呢?
我们的数据库内容如下:

{
    "_id" : ObjectId("58a7aa5092bc315538faa2de"),
    "a" : "1",
    "bs" : {
        "b1" : {
            "c1" : 1
        },
        "b2" : {
            "c2" : 1
        },
        "b3" : {
            "c3-1" : 1,
            "c3-2" : 1
        }
    }
}


1、查询一个带 keyA="1",keyB="b1",keyC="c1" 的场景:

db.getCollection('s2').find({"a":"1", "bs.b1.c1":{$exists:1}})


2、查询一个带 keyA="1",keyB="b1 and b2 and b3" 的场景:

db.getCollection('s2').find({"a":"1",
"bs.b1":{$exists:1},
"bs.b2":{$exists:1},
"bs.b3":{$exists:1} })



2、查询一个带 keyA="1",keyB="b1 or b4 or b5" 的场景:
db.getCollection('s2').find({"a":"1",
$or:[
{"bs.b1":{$exists:1}},
{"bs.b4":{$exists:1}},
{"bs.b5":{$exists:1}}
]
})

3、插入一个keyB,条件 keyA="1", keyB="b4"

db.getCollection('s2').update( // query { "a" : "1" }, // update { "$set":{ "bs.b4":{} } }, // options { "multi" : false, // update only one document "upsert" : false // insert a new document, if no existing document match the query } );


4、插入一个keyC,条件 keyA="1", keyB="b4", keyC = "c4"

db.getCollection('s2').update( // query { "a" : "1" }, // update { "$set":{ "bs.b4.c4":1
} }, // options { "multi" : false, // update only one document "upsert" : false // insert a new document, if no existing document match the query } );


5、插入一个新的 keyB 和 keyC,条件 keyA="1", keyB="b5", keyC = "c5"
db.getCollection('s2').update( // query { "a" : "1" }, // update { "$set":{ "bs.b5.c5":1
} }, // options { "multi" : false, // update only one document "upsert" : false // insert a new document, if no existing document match the query } );

6、删除一个keyC,条件 keyA="1", keyB="b5", keyC = "c5"

db.getCollection('s2').update( // query { "a" : "1" }, // update { "$unset":{ "bs.b5.c5":1} }, // options { "multi" : false, // update only one document "upsert" : false // insert a new document, if no existing document match the query } );


7、删除一个keyB,条件 keyA="1", keyB="b4"

      db.getCollection('s2').update( // query 
        {
            "a" : "1"
        },
        
        // update 
        {
             "$unset":{ "bs.b4":1}
        },
        
        // options 
        {
            "multi" : false,  // update only one document 
            "upsert" : false  // insert a new document, if no existing document match the query 
        }
    );



8、将一个keyB下面的keyC更新,
条件 keyA="1", keyB="b3" , keyC="c3-1" 更新为 keyC="c3-3"
这个比较麻烦,其实就是删除一个,再插入一个

db.getCollection('s2').update( // query { "a" : "1" }, // update { "$unset":{ "bs.b3.c3-1":1}, "$set":{ "bs.b3.c3-3":1}, }, // options { "multi" : false, // update only one document "upsert" : false // insert a new document, if no existing document match the query } );



最后是性能对比,我在自己的pc机上对这些数据量进行一个简单的测试,
只做更新和查询的
考虑到插入数据时间太长,
我把方案1缩小40倍,数据量100万,方案2同样缩小40倍,5000条
1、插入100万python代码

# -*- coding: utf-8 -*-
#coding=utf-8
import datetime
from pymongo import MongoClient
from pymongo.write_concern import WriteConcern

client = MongoClient('mongodb://127.0.0.1:27017/test')
db = client.test
col = db.s1.with_options(write_concern=WriteConcern(w=1))

j = 0
for i in range(0, 1000000):
    data = {
        "a": "a1",
        "b": "b1",
        "c": "c1",
        "t1":"text1",
        "t2":"text2",
        "t3":"text3",
        "t4":"text4",
        "t5":"text5",
        "wt":datetime.datetime.now(),
    }
#本来每片5万个keyA,缩小40倍就是1250个,所以没800次循环增加一个keyA
    if i%800== 0:
        print("insrt {0}".format(i))
        j += 1
    data["a"] = "a{0}".format(j)
    data["b"] = "b{0}".format(i)
    data["c"] = "c{0}".format(i)
    result = col.insert_one(data)


2、下面同样缩小40倍,准备5000条方案2的数据,我们存入表s2

# -*- coding: utf-8 -*- #coding=utf-8 import datetime from pymongo import MongoClient from pymongo.write_concern import WriteConcern client = MongoClient('mongodb://127.0.0.1:27017/test') db = client.test col = db.s22.with_options(write_concern=WriteConcern(w=1)) for i in range(0, 5000): data = { "a": "", "b": {}, "t1":"text1", "t2":"text2", "t3":"text3", "t4":"text4", "t5":"text5", "wt":datetime.datetime.now(), } data["a"] = "a{0}".format(i) for k in range(0,100): data["b"]["b{0}".format(k)] = {} data["b"]["b{0}".format(k)]["c{0}".format(k)] = 1 result = col.insert_one(data)


1、测试1,更新操作,对keyA,keyB,keyC进行全条件更新,操作5万次,看运行时间
对于完全打平的情况

# -*- coding: utf-8 -*-
#coding=utf-8
import datetime
import time
import math
from pymongo import MongoClient
from pymongo.write_concern import WriteConcern

client = MongoClient('mongodb://127.0.0.1:27017/test')
db = client.test
col = db.s1.with_options(write_concern=WriteConcern(w=1))


s = time.mktime(datetime.datetime.now().timetuple())
for i in range(100000, 150000):
   if i %10000== 0:
       print(i)
   xi = int(math.floor(i/800)) + 1
   cond = {
        "a":"a{0}".format(xi),
        "b":"b{0}".format(i),
        "c":"c{0}".format(i),
   }
   up = {
        "$set":{
            "c":"c{0}x".format(i)
        }
   }
   result = col.update_one(cond, up)
   if result.matched_count == 0:
        print(cond, "error")
        break


e = time.mktime(datetime.datetime.now().timetuple())
print("time {0}s".format(e-s))

运行结果,更新1万次

time 57.0s


对于新的方案

# -*- coding: utf-8 -*-
#coding=utf-8
import datetime
import time
import math
from pymongo import MongoClient
from pymongo.write_concern import WriteConcern

client = MongoClient('mongodb://127.0.0.1:27017/test')
db = client.test
col = db.s22.with_options(write_concern=WriteConcern(w=1))


s = time.mktime(datetime.datetime.now().timetuple())
for i in range(5, 15):
  for j in range(0,5000):
    if j %10000 == 0:
       print(j)
    key = "b.b{0}.c{0}".format(i,i)
    key2 = "b.b{0}.c{0}x".format(i,i)
    cond = {
        "a":"a{0}".format(j),
    }
    cond[key] = {"$exists":1}
    up = {
       "$unset":{},
       "$set":{},
    }
    up["$unset"][key] = 1
    up["$set"][key2] = 1

    result = col.update_one(cond, up)
    if result.matched_count == 0:
        print(cond, "error")
        break


e = time.mktime(datetime.datetime.now().timetuple())
print("time {0}s".format(e-s))

更新5万个数据,耗时

time 54.0s


2、测试查询,查询混合条件1万次
打平查询:

# -*- coding: utf-8 -*- #coding=utf-8 import datetime import time import math from pymongo import MongoClient from pymongo.write_concern import WriteConcern client = MongoClient('mongodb://127.0.0.1:27017/test') db = client.test col = db.s1.with_options(write_concern=WriteConcern(w=1)) s = time.mktime(datetime.datetime.now().timetuple()) for i in range(10000, 20000): if i %1000 == 0: print(i) xi = int(math.floor(i/800)) + 1 cond1 = { "a":"a{0}".format(xi), } cond2 = { "a":"a{0}".format(xi), "b":"b{0}".format(i), } result = col.find(cond1) if not result[0]: print(cond, "error") break result = col.find(cond2) if not result[0]: print(cond2, "error") break e = time.mktime(datetime.datetime.now().timetuple()) print("time {0}s".format(e-s))

运行时间:

time 27.0s


内嵌文档方案:

# -*- coding: utf-8 -*- #coding=utf-8 import datetime import time import math from pymongo import MongoClient from pymongo.write_concern import WriteConcern client = MongoClient('mongodb://127.0.0.1:27017/test') db = client.test col = db.s22.with_options(write_concern=WriteConcern(w=1)) s = time.mktime(datetime.datetime.now().timetuple()) for i in range(0, 2): for j in range(0,5000): if j %1000 == 0: print(j) cond1 = { "a":"a{0}".format(j), } cond2 = { "a":"a{0}".format(j), } cond2["b.b{0}".format(i)] = {"$exists":1} result = col.find(cond1) if not result[0]: print(cond, "error") break result = col.find(cond2) if not result[0]: print(cond2, "error") break e = time.mktime(datetime.datetime.now().timetuple()) print("time {0}s".format(e-s))

运行时间:

time 23.0s


总结一下:
在经过测试和优化之后,数据量从1.6亿减少到20万,服务器从10台减少到3台,但是性能还有些许提高
所以实践证明,我们的改造是可行的



  评论这张
 
阅读(167)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017