返回介绍

NoSQL 数据库 MongoDB

发布于 2025-04-20 18:52:15 字数 16480 浏览 0 评论 0 收藏

为什么使用 NoSQL

NoSQL 最常见的解释是 Not Only SQL,泛指非关系型的数据库。之前介绍的 Redis 就是一种 NoSQL 数据库。NoSQL 不使用 SQL 作为查询语言,其数据存储不需要预先定义模式,只要保证程序的兼容即可:这在构建 Web 应用程序中尤为有用。它的出现还解决了关系型数据库本身无法克服的一些缺陷:

  • 对数据库可以进行高并发读写的需求。
  • 对海量数据的高效率存储和访问的需求。
  • 高可扩展性和高可用性。

而 NoSQL 相对于传统的关系型数据库的一些不足显得不再那么重要,或者可以通过其他方式来保证:

  • 数据库事务一致性需求。
  • 数据库的实时读写需求。
  • 对复杂的 SQL 查询,特别是多表关联查询的需求。这里需要强调一点,大数据量的 Web 应用都应该减少甚至忌讳使用多个大表的关联查询。

NoSQL 是不能完全替代关系型数据库的,它们没有绝对的孰优孰劣,我们需要根据存储的数据以及操作数据的方式来选用不同的方案。

MongoDB

MongoDB 是一个为当代 Web 应用而生的 NoSQL 数据库,它有如下优点:

1.文档型存储。可以把关系型数据库的表理解为一个电子表格,列表示字段,每行的记录其实是按照列的字段顺序排列的值的元组。而存储在 MongoDB 中的文档被存储为键-值对的形式,值却可以是任意类型且可以嵌套。之前在用关系数据库的时候,我们需要把产品信息打散到不同的表中,要通过关系表或者使用 join 拼接成复杂的 SQL 语句的方式才能获得需要的数据。现在我们可以更多地把产品信息放在一起,也不需要提前预定产品信息的模式。

2.使用高效的二进制 BSON 作为数据存储。BSON 是一个类 JSON 的格式,选择 BSON 可以提供更快的遍历速度,提供比 JSON 更多的内置数据类型。

3.自带高可用及分区的解决方案,分别为副本集(Replica Set)和分片(sharding)。

4.基于文档的富查询语言。MongoDB 支持动态查询,支持非常多的查询方式,并且可以对文档中的属性建立索引。

5.内置聚合工具。可以通过 MapReduce 等方式进行复杂的统计和并行计算。

6.MongoDB 在 3.0 之后增加了高性能、可伸缩、支持压缩和文档级锁的数据存储引擎 WiredTiger。官方的性能测试(http://bit.ly/28SnMaa )显示,使用新的存储引擎后带来 4~7 倍的性能提升。WiredTiger 从 MongoDB 3.2 开始作为默认的存储引擎,另外它和原来的基于内存映射技术的存储引擎(MMAP)兼容。如果用户将当前自己的应用升级到基于 WiredTiger 的应用,无须更改现有的任何部署,也无须停机进行即可完成。

安装 MongoDB

默认源中没有最新的 MongoDB,需要使用如下方法添加源并安装:

> sudo apt-key adv--keyserver hkp://keyserver.ubuntu.com:80--recv EA312927
> echo"deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.2 multiverse"|
    sudo tee/etc/apt/sources.list.d/mongodb-org-3.2.list
> sudo apt-get update
> sudo apt-get install-y mongodb-org

现在需要手动下载管理脚本:

> wget https://github.com/mongodb/mongo/raw/master/debian/init.d
> sudo mv init.d/etc/init.d/mongodb
> sudo chmod+x/etc/init.d/mongodb

还需要修改两个内核参数:

> sudo sh-c 'echo never>/sys/kernel/mm/transparent_hugepage/enabled'
> sudo sh-c 'echo never>/sys/kernel/mm/transparent_hugepage/defrag'

然后启动 MongoDB:

> sudo/etc/init.d/mongodb start

Vagrant 环境下还可以使用 systemd 管理:

> wget https://github.com/mongodb/mongo/raw/master/debian/mongod.service
> sudo mv mongod.service/etc/systemd/system
> sudo systemctl start mongodb

安装 MongoDB 的 Python 驱动:

> pip install pymongo

项目的名字是 mongo-python-driver(http://bit.ly/28WeOf0 ),pymongo 只是包的名字。

使用 pymongo 的例子

我们先看一下基本的 CRUD 的例子(example_pymongo.py):

import random

import pymongo

client=pymongo.MongoClient('mongodb://localhost:27017/')
client.drop_database('test') #保证之前没有数据,删除名为 test 的数据库
db=client.test #使用 test 这个数据库
coll=db.coll #使用 coll 这个集合

#插入单条记录
rs=coll.insert_one({'a':1, 'b':2})
object_id=rs.inserted_id
print rs.inserted_id #打印插入的对象 id

#插入多条记录
rs=coll.insert_many([{'a':random.randint(1, 10), 'b':10}
                    for_in range(10)])
print rs.inserted_ids #打印插入的对象 id 列表

#查询单条(符合的第一条)记录
print coll.find_one({'a':1, 'b':2})

#集合当前全部文档数
print coll.count()
cursor=coll.find({'a':{'$lte':1}}) #查询结果是一个游标
print cursor.count() #符合查询的文档数

for r in cursor:
    print r, r['b'] #打印符合查询的文档内容,以及其中 b 键的值

#注意,这个循环只能进行一次。如果想再获得查询结果,需要重新 find 或者使用 list(
   cursor) 把结果存起来

#对查询结果排序
print list(coll.find({'a':{'$lte':1}}).sort([('b',-1)]))
#-1 也可以表示为 pymongo.DESCENDING

#对查询结果可以限制返回文档数,控制跳过的结果数
print coll.find({'b':{'$gt':1}}).limit(1).skip(1).next() # next 相当于 find_one

#找到后更新,下面的例子第一个参数是过滤条件,第二个参数是要更新的操作(设置 b 为
   3,a 自增长 1)
# upsert 为 True 表示找不到会创建一条记录,可以理解为 get_or_create
rs=coll.find_one_and_update({'a':1, 'b':2},
                            {'$set':{'b':3}, '$inc':{'a':1}},
                            upsert=False)
print rs #返回更新前的文档
#同样的还有 find_one_and_replace 和 find_one_and_delete
print list(coll.find({'a':2, 'b':3})) #上述文档已经被更新为这个文档
coll.find_one_and_update({'a':1, 'b':2},
                         {'$set':{'b':3}, '$inc':{'a':1}},
                         upsert=True) #虽然没有符合{'a':1, 'b':2}的记录,但是会新建一个
print coll.find({'a':2, 'b':3}).count() #发现现在有两条文档记录了


#删除单个文档
coll.delete_one({'a':2, 'b':3})

#一次性删除多个文档

rs=coll.delete_many({'a':2, 'b':3})
#如果没有符合的条目也不会提示,但是可以通过 rs.deleted_count 获得删除的数量
print rs.deleted_count

MongoDB 的一大核心是聚合功能,主要用于返回计算好的结果。

常用的执行聚合的方式有如下三种。

1.使用管道。管道在 UNIX 和 Linux 中一般用于将当前命令的输出结果作为下一个命令的参数:

In : cursor=coll.aggregate([
   {'$match':{'b':{'$gt':1, '$lt':11}}},
   {'$group':{'_id':None, 'count':{'$sum':1}, 'averageA':{'$avg':'$a'
       }}},
   {'$project':{'_id':0, 'count':1, 'averageA':1}}])
In : cursor.next()
Out: {u'averageA':5.0, u'count':10}

先通过“$match”获得符合条件的文档,把它传给第二个“$group”来分组,分组会计算总数(count)和 a 的平均值(averageA),然后传给第三个“$project”过滤掉字段_id。这里需要注意使用管道时的一些利弊,可以参考 Aggregation Pipeline Optimization(http://bit.ly/28WbHGk )。

2.MapReduce。Map-Reduce 是一种计算模型。它将大批量的工作(数据)分解执行,然后再将结果合并成最终结果(Reduce)。这种模式下使用原生的 JavaScript 函数:

In : from bson.code import Code
In : mapper=Code("""function (){
...: var key;
...: if (this.a<3){
...: key='lt 3';//根据 a 的值设置不同的键
...: }else{
...: key='gte 3';
...: }
...: emit(key, 1);}""")
In : reducer=Code("""function (key, values){
...: return Array.sum(values)}""")
In : rs=coll.map_reduce(mapper, reducer, 'c', query={'b':{'$gt':1, '$lt':
     11}}) # c 是存放计算结果的集合的名字。query 用来过滤需要计算的文档,符合条件的才会做 MapReduce 计算
In : for i in rs.find():
...: print i
...:
{u'_id':u'gte 3', u'value':8.0}#_id 就是上面的键,符合大于或等于 3 的文档有 8 个
{u'_id':u'lt 3', u'value':2.0}#符合小于 3 的文档有 2 个

如果这样的 JavaScript 函数或者变量非常常用,可以存放在服务器上以备重复使用:

In : db.system.js.insert_one({'_id':'mapper', 'value':mapper})
In : rs=coll.map_reduce(db.eval('mapper'), reducer, 'c'})

3.单一目的的聚合操作方法。MongoDB 目前已经提供了 count、distinct 和 group 等聚合方法,可以直接调用:

In : coll.distinct('a') #找出 a 键的所有不同的值
Out: [8, 5, 7, 3, 6, 2, 1]

这就是聚合的魅力。聚合运算符的种类非常多,可以在 Aggregation Pipeline Operators (http://bit.ly/294MTYm )找到全部的运算符。

使用 Mongoengine 的例子

在前面我们曾经使用 MySQL 的 ORM 框架 SQLAlchemy,同样的 MongoDB 也有自己的 ODM (对象文档映射)框架,最知名的是 Mongoengine。如果了解甚至使用过 SQLAlchemy 的话会发现它会非常容易理解和上手。

本节把文件托管服务的模型改成使用 Mongoengine。并不是说我们使用 Flask 和 Mongo-engine,就要安装 Flask-Mongoengine,虽然 Flask-Mongoengine 集成了 WTForms,也提供了一些其他功能,但是对我们这个应用没有太大意义。我们将直接使用 Mongoengine 来实现,这样也可以了解扩展隐藏起来的一些细节。

先安装它:

> pip install mongoengine

现在把第 3 章 3.6 节的文件托管服务改成使用 Mongoengine。改动分为如下三部分:

  • 替换 PasteFile 模型。
  • 修改模型的方法。
  • 修改视图中保存 PasteFile 模型实例的方法。

首先替换定义的模型:

from mongoengine import (
    Document as BaseDocument, connect, ValidationError, DoesNotExist,
    QuerySet, MultipleObjectsReturned, IntField, DateTimeField, StringField,
    SequenceField)
#连接本机 27017 端口的 MongoDB 服务器的数据库 r
connect('r', host='localhost', port=27017)


class BaseQuerySet(QuerySet):
    #当捕捉到多个值/不存在或者验证失败这三种异常时,直接返回 404,这个方法很常用
    def get_or_404(self,*args,**kwargs):
        try:
           return self.get(*args,**kwargs)
           except (MultipleObjectsReturned, DoesNotExist, ValidationError):
               abort(404)
 
 
class Document(BaseDocument):
    meta={'abstract':True,
          'queryset_class':BaseQuerySet}#使用自定义的 BaseQuerySet
 
class PasteFile(Document):
    id=SequenceField(primary_key=True)
    filename=StringField(max_length=5000, null=False)
    filehash=StringField(max_length=128, null=False, unique=True)
    filemd5=StringField(max_length=128, null=False, unique=True)
    uploadtime=DateTimeField(null=False)
    mimetype=StringField(max_length=128, null=False)
    size=IntField(null=False)
    meta={'collection':'paste_file'}#自定义集合的名字

需要说明的是,MongoDB 默认使用是_id 这个字段作为主键,但是它是 bson.ObjectId 的实例,不方便 short_url 模块获得短链接,而使用 SequenceField 替换它,就可以达到 id 自增长的目的了。

PasteFile 的__init__方法中需要初始化父类的__init__方法:

def__init__(self, filename='', mimetype='application/octet-stream',
             size=0, filehash=None, filemd5=None,*args,**kwargs):
   super(PasteFile, self).__init__(filename=filename, mimetype=mimetype,
                                   size=size, filehash=filehash,
                                   filemd5=filemd5,*args,**kwargs)
   self.uploadtime=datetime.now()
   ...

PasteFile 中的三个 get_by*方法中获得模型实例的方式也要修改一下:

@classmethod
def get_by_symlink(cls, symlink, code=404):
    id=short_url.decode_url(symlink)
    return cls.objects.get_or_404(id=id)

@classmethod
def get_by_filehash(cls, filehash, code=404):
    return cls.objects.get_or_404(filehash=filehash)

@classmethod
def get_by_md5(cls, filemd5):
    rs=cls.objects(filemd5=filemd5)
    return rs[0] if rs else None

最后修改视图中的保存方法,原来使用:

db.session.add(paste_file)
db.session.commit()

现在只需要 save 就可以了:

paste_file.save()

这样就替换完成了。

MongoDB 实践经验

善用组合式的大文档

可以尽量把数据组合起来节省空间,提高读的性能。MongoDB 的一个应用场景是实时分析。假设我们每分钟都要记一次日志,内容格式如下:

{metric:"review_count", client_type:2, value:23, date:ISODate("2016-03-22 10:10")
   }
{metric:"review_count", client_type:2, value:9, date:ISODate("2016-03-22 10:11")}

采用组合的方式,例如把一个小时的日志放到一个文档中:

{metric:"review_count", client_type:2, date:"2016-03-22", hour:10, 10:23, 11:9)
    }

可以通过如下的测试对比一下(use_big_documents.py):

import random
from datetime import datetime, timedelta

import pymongo

RANGE=1440
l=[random.randint(1, 100) for i in range(RANGE)] #模拟一天的数据
client=pymongo.MongoClient("localhost", 27017)
client.drop_database('test') #保证之前没有数据
db=client.test

#传统方案
for i in range(RANGE):
    s={'metric':'review_count', 'client_type':2, 'value':l[i],
       'date':datetime(2016, 03, 22, 0, 0)+timedelta(minutes=i)}
    db.a.insert_one(s)


#单个大文档(每小时一个文档)的方案
for i in range(24):
    s={'metric':'review_count', 'client_type':2, 'date':'2016-03-22', 'hour':i}
    s.update({str(k):v for k, v in enumerate(l[60*i:60*(i+1)])})
    db.b.insert_one(s)


def get_hour_data1(hour):
    return [i['value'] for i in db.a.find({
        'date':{'$gte':datetime(2016, 3, 22, hour, 0),
                '$lt':datetime(2016, 3, 22, hour+1, 0)}})]


def get_hour_data2(hour):
    c=db.b.find_one({'hour':hour})
    return [c[str(i)] for i in range(60)]

对比一下性能:

In:db.eval('db.a.stats()["size"]')
Out:128160.0

In:db.eval('db.b.stats()["size"]')
Out:13560.0

In:%timeit-n 10000 get_hour_data1(1)
10000 loops, best of 3:1.05 ms per loop

In:%timeit-n 10000 get_hour_data2(1)
10000 loops, best of 3:311□s per loop

按月查询所占用空间只有原来的 8%左右,花费的时间不到原来的 1/3。

正确使用索引

添加索引是为了提高查询速度。但是无论代码评审做得多好,缺少索引、索引错误、索引太多这三类问题总还是会有漏网之鱼,笔者的经验是经常关注 MongDB 的日志,另外每周一都会收到上周慢查询 TOP100 的邮件(MySQL 的慢查询也会有类似的邮件),从其中发现问题并寻找优化的可能,及时修复。举个实际的例子(预先去掉一个正确的索引):

In:import pymongo
In:client=pymongo.MongoClient()
In:db=client.products
In:c=db.promo_status.find({'symbol':1}).sort('create_date',-1).limit(1)
In:c.next()

这个时候能感觉执行 next 的时候很慢。MongDB 默认会记录用时超过 100 ms 的慢查询(可以通过 slowms 参数修改这个值),这时候看/var/log/mongodb/mongod.log 新增了这样一条记录:

2016-05-31T00:08:17.267+0800 I COMMAND [conn10] command products.promo_status command:
      find{find:"promo_status", filter:{symbol:1}, sort:{create_date:-1},
      limit:1}planSummary:COLLSCAN keysExamined:0 docsExamined:6318177 hasSortStage:1
      cursorExhausted:1 keyUpdates:0 writeConflicts:0 numYields:73611 nreturned:1 reslen
      :296 locks:{Global:{acquireCount:{r:73612}}, MMAPV1Journal:{acquireCount:
      {r:73612}}, Database:{acquireCount:{r:73612}}, Collection:{
      acquireCount:{R:73612}}}protocol:op_query 34162ms

通过日志确实是可以发现问题的,但是需要处理日志,操作起来不太方便。可以打开分析器,把慢查询记录到 system.profile 集合中,方便最后对结果进行处理(可以通过 profile 参数指定)。需要注意 system.profile 是一个 1 MB 的固定集合,当集合达到这个固定值之后新文档就开始覆盖最早的文档。根据需要,至少要保存一周的慢查询,所以最好设置成 10 MB 集合。如果你的项目问题非常多,那么需要再加大这个值:

In : db.set_profiling_level(0)
In : db.system.profile.drop()
In : db.create_collection('system.profile', capped=True, size=10000000)
In : db.set_profiling_level(1)
In : db.profiling_level()
Out: 1

现在就可以获得我们想要的分析结果了:

#获得上一周花费时间 TOP100 的慢查询日志
In:db.system.profile.find({'ts':{'$gte':datetime(2016, 3, 14), '$lt':datetime
   (2016, 3, 21)}}).sort('millis',-1).limit(100)
#获得数据库 products 的 promo_status 集合,花费时间超过 300 ms 的慢查询日志
In:db.system.profile.find({'ns':'products.promo_status', 'millis':{'$gt':300}})

还可以聚合:

In:from bson.code import Code
In:reducer=Code("""
....:function(obj, prev){
....:prev.count++;
....:}""")
In : db.system.profile.group(key={'ns':True}, condition={}, initial={'count':0},
   reduce=reducer) #聚合不同的集合的慢查询数量
In : db.system.profile.group(key={'ns':True}, condition={},
initial={'millis':0}, reduce=Code('function(obj, prev){prev.millis+=obj.millis;}'))
       #聚合不同集合的慢查询花费的总时间

现在分析下之前提到的那个慢查询:

In : rs=c.explain()
In : rs['queryPlanner']['winningPlan']['inputStage']['inputStage']['stage']
Out: u'COLLSCAN' #这是一个全集合扫描
In :  rs['executionStats']['totalDocsExamined']
Out: 6318177 #扫描的文档数量,也就是全表的文档数量
In :  rs['executionStats']['nReturned']
Out: 1 #但是只返回了一个结果

很显然,这个查询的结果数和需要扫描的文档数的比例完全不合理,需要创建索引。创建索引有以下几条规则:

1.通常创建的联合索引是按照查询条件的顺序来的。

2.排序字段通常放在联合索引的最后。但是涉及范围查询时可能要把排序字段提前,需要根据业务实际情况作对比。

3.单个字段建索引时,不需要考虑索引升序还是降序。但是当数据量很大,建联合索引时,需要注意索引升序和降序的查询效率。

4.把能过滤数据量多的字段放在前面。

创建一个标准索引:

In:db.promo_status.create_index([('symbol', 1), ('create_date',-1)])
Out:u'symbol_1_create_date_-1'

如果生产环境的数据量很大,可以在后台创建索引,减少对用户的影响:

db.promo_status.create_index([('symbol', 1), ('create_date',-1)], background=True)

现在再分析之前的查询:

In : rs=c.explain()
In : rs['queryPlanner']['winningPlan']['inputStage']['inputStage']['stage']
Out: u'IXSCAN' #使用索引的扫描
In : rs['executionStats']['totalKeysExamined']
Out: 1 #只扫描了一个索引条目就找到了
In : rs['executionStats']['totalDocsExamined']
Out: 1 #只扫描了一个文档就找到了
In :  rs['executionStats']['executionTimeMillis']
Out: 0

执行时间因为太短,显示为 0 s,从 34,162 ms 锐减到 0 ms,这都是索引的功劳。

除此之外,我们还要关注索引字段的顺序。MongoDB 和传统数据库一样,是采用 B 树作为索引的数据结构。对于树形的索引来说,保存热数据使用到的索引在存储上越集中,索引浪费的内存也越小。我们对比如下两种创建索引的方法:

db.promo_status.create_index([('symbol', 1), ('create_date',-1)])

db.promo_status.create_index([('create_date',-1), ('symbol', 1)])

使用后者可以保证插入速度的稳定,因为新数据更新索引时只是在索引的尾巴处进行修改,那些插入时间过早的索引在后续的插入操作中几乎不需要进行修改,但是对于我们的查询需求来看,它要扫描的索引条目更多:

In:rs=db.promo_status.find({'symbol':1}).sort('create_date',-1).limit(1).hint([('
   create_date',-1), ('symbol', 1)]).explain() # hint 可以强迫使用特定索引来查询
In:rs['executionStats']['totalKeysExamined']
Out:1211 #需要扫描 1211 个条目
In:rs['executionStats']['executionTimeMillis']
Out:1 #花费了 1 ms,比之前的慢

1 ms 还是可以接受的,这个时候需要根据业务做出取舍。

MongoDB 的索引也遵循“最左前缀”原则,上面创建了“[('symbol', 1), ('create_date',-1)]”这个复合索引,就不需要再创建“[('symbol', 1)]”这个索引了(如果存在的话,可以删掉)。

高可用方案

复制(repliaction)是大多数数据库系统的标配,即在多台服务器上保存数据的副本。因为故障是无法避免的,复制可以保证生产环境的数据在发生故障之后仍然保持可用状态。故障常见于如下场景:

  • 服务器硬件故障。
  • 计划的停机,比如服务器硬件维护、升级系统、切换新服务器等。有时候停机重启时可能会出现某些问题,比如新安装的软件或者硬件和系统不兼容。
  • 异常断电。断电非常有可能损坏数据,而且需要时间来恢复。

在生产环境中使用复制是非常明智的。MongoDB 提供两种复制,主从复制和副本集。两者的复制机制相同,但是后者提供自动的故障转移:主服务器因为某些原因下线后,如果可能的话,可以把一个从节点提升为主节点。副本集是推荐的复制策略,不要使用主从复制。我们先来看副本集节点的常见角色类型。

  • Primary:主节点,处理客户端的请求,一般是读写请求。
  • Secondary:从节点,一般有多个。它们各自保存主服务器的数据副本,主服务器出问题时其中的一个从节点可提升为新主节点,可以处理客户端的读请求。
  • Arbiter:仲裁节点,不保存数据,只参与选举,不复制数据。如果副本集有偶数个节点,可以增加一个投票节点以保证其成员个数为奇数。这样才能可以正常地选举出主节点。仲裁节点所需资源很少,可以用做其他用途。
  • Hidden:隐藏节点,只用于备份节点,不处理客户端的读请求。
  • Secondary-Only:只能作为从节点,主要防止这种性能不高的节点成为主节点。

常见的副本集架构如下:

  • 一个主节点用于读写,两个从节点(或者一个从节点,一个仲裁节点),从节点可读。这是最小的配置,适用于规模比较小的应用。
  • 一个主节点用于写,多个从节点用于读,一个从节点用于备份。从节点的数量和业务相关,从节点的数量越多,可发生的故障转移的次数就越多。目前副本集最多能有 50 个节点,当业务需要超过 50 个节点时,才要用到主从复制的架构。在异地分布式架构中,用于备份的从节点一般在另一个数据中心。读写分离是为了让主数据库处理增、改、删这样的写操作,而从数据库处理查询这样的读操作。

需要明确的是,复制不是备份。复制并不能避免人为操作的错误。例如管理员突然删除了产品数据,或者部署了错误版本的应用程序代码以致搞乱了部分或者全部数据。所以,必须要有一个能够让我们从这种场景中恢复数据的备份。

分片方案

随着数据量的增加,应用对读写吞吐量的要求越来越高,单台服务器总会遇到内存、CPU、磁盘空间等的瓶颈。这时候就可以将数据库用某种规则分开,存储在多台服务器上,这就是分片。可以预想,分片能让集合扩充到无限大。分片没必要考虑得太早,但是当需求超过单台服务器能提供的 70%(经验值)的时候,就应该开始考虑分片,并准备实施了。

MongoDB 支持自动分片。它的分片是把一个集合根据片键分成多个块(chunk,一个 chunk 包括这个片键区间的数据),然后将各个块分别分发到各个分片副本集上。所以,片键的选择非常重要。

1.不要单独使用_id 或者时间戳这种有递增特性的属性作为片键,它们只会一直往最后一个副本集中添加数据。

2.使用随机性高且基数比较大的片键,这样避免一个分片承受绝大多数负载,并保证了分片间数据的均衡。

3.使用多个属性作为片键。如果没有随机性高且基数比较大的片键,可能会造成巨大块,这时候可以和_id 一起作为片键。

4.使用数据文档_id 的哈希值,可通过“db.collection.create_index([('_id', 'hashed')])”。如果业务上实在找不到符合的片键,这是一个不错的方案。但是它有一个缺点,对多个文档的查询必将命中所有的分片。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。