知道了這些 MongoDB設計技巧,提升效率50%

think123發表於2019-10-22

正規化化設計還是反正規化

考慮下這樣的場景,我們的訂單資料是這樣的

商品:
{
  "_id": productId,
  "name": name,
  "price": price,
}

訂單:
{
  "_id": orderId,
  "user": userId,
  "items": [
    productId1,
    productId2,
    productId3
  ]
}
複製程式碼

當我們查詢訂單內容的時候,先通過orderId查詢訂單,然後在通過訂單資訊中的productId查詢到對應的商品資訊。這種設計下一次查詢無法獲取完整的訂單。

正規化化結果就是讀取速度比較忙,當所有訂單的一致性會有保證。

在來看看反正規化化設計

訂單:
{
  "_id": orderId,
  "user": userId,
  "items": [
   {
    "_id": productId1,
    "name": name,
    "price": price,
   },
   {
    "_id": productId2,
    "name": name,
    "price": price,
   },
  ]
}

複製程式碼

這裡將商品資訊作為內嵌文件存在訂單資料中,這樣當顯示的時候就只需要一次查詢就可以了。

反正規化讀取速度快,一致性稍弱,商品資訊的變更不能原子性地更新到多個文件。

那麼我們一般使用哪一個呢?我們在設計的時候要考慮以下問題

  1. 讀寫比是怎樣的?

可能讀取了商品資訊一萬次才修改一次它的詳細資訊,為了那一次寫入快一點或者保證一致性,搭上一萬次的讀取消耗值得嗎?還有你認為引用的資料多久會更新一次?更新越少,越適合反正規化化。有些極少變化的資料幾乎根本不值得引用。比如名字,性別,地址等。

  1. 一致性重要嗎?

如果是肯定的,則應該正規化化。

  1. 要不要快速的讀取? 如果想要讀取儘可能快,則要反正規化化。在這個引用中就無所謂了,所以不能算考量因素,實時的應用要儘可能地反正規化化。

訂單文件非常適合反正規化化,因為其中的商品資訊不經常變化。就算變了也不必更新到所有訂單。正規化化再次就沒有什麼優勢可言了。

所以本例中就是將訂單反正規化化。

嵌入時間點資料

當一個商品打折或者換了圖片,並不需要更改原來的訂單中的資訊。類似這種特定於某一時刻的時間點資料,都應該做嵌入處理。

在我們上面提到的訂單文件中有一處也是這樣,地址就屬於時間點資料。若某人更新了個人資訊,那麼並不需要改變其以往的訂單內容。

千萬不要嵌入不斷增加的資料

MongoDB儲存資料的機制決定了對陣列不斷追加資料是很低效的。在正常使用中陣列和物件大小應該相對固定。

嵌入20,100,或者100000個子文件都不是問題,關鍵是提前這麼做,之後基本保持不變。否則放任文件增長會使得系統慢的你受不了。

對於那些不斷增加的內容,必須評論這個時候應該將其作為單獨的文件處理比較合適。

儘可能預先分配空間

只要知道文件開始比較小,後來會變為確定的大小就可以使用這種優化方法,一開始插入文件的時候,就用和最終資料大小一樣的垃圾資料填充,比如新增一個garbage欄位(其中包含一個字串,串大小與文件最終大小相同),然後馬上重置欄位

db.collection.insert({"_id" : 1,/* other fields */, "garbase": longString});
db.collection.update({"_id" : 1, });

複製程式碼

這樣,MongDB就會為文件今後的增長分配足夠的空間

mongodb中儲存文件是預留了空間的,允許文件擴容,但是當文件增大到一定地步的時候,就會超過原本分配的空間,此時文件就會進行移動

用陣列存放要匿名訪問的內嵌資料

一個常見的問題就是內嵌的資訊到底是用陣列還是用子文件存。如果確切知道要查詢的內容,就要用子文件。如果有時候不太清楚查詢的具體內容,就要用陣列。當知道一些條目的查詢條件時,通常該使用陣列。

假設我想記錄下遊戲中某些物品的屬性。我們可以這樣建模

{
  "_id": 1,
  "items" : {

    "slingshot": {
      "type" : "weapon",
      "damage" : 30,
      "ranged" : true
    },

    "jar" : {
      "type": "container",
      "contains": "fairy"
    }

  }
}

複製程式碼

假設要找出所有damage大於20的武器,子文件不支援這種查詢方式,你只能知曉具體某種物品的資訊才能查詢,比如{"items.jar.damage": {"$gt":20}}. 如果無需識別符號,就要用陣列

{
  "_id": 1,
  "items" : [

    {
      "id" : "slingshot"
      "type" : "weapon",
      "damage" : 30,
      "ranged" : true
    },

    {
      "id" : "jar",
      "type": "container",
      "contains": "fairy"
    }

  ]
}

複製程式碼

比如{"items.damage":{"$gt":20}}就行了。如果還需要多條件查詢,可以使用$elemMatch.

如何使用自增id代替ObjectId

有時候在使用過程中受限於業務或者其他情況,並不想使用ObjectId,而是想要使用自動Id來代替。但是MongoDB本身並沒有提供這個功能,那麼如何實現呢?

可以新建一個collection來儲存自增id

{
    "_id" : ObjectId("59ed8d3df772d09a67eb25f6"),
    "fieldName" : "user",
    "seq" : NumberLong(100064)
}

複製程式碼

fieldName表示哪個集合,那麼下次要使用的時候只用取出這個值加1就可以了。程式碼如下

 public Long getNextSequence(String fieldName, long gap) {
    try {
        Query query = new Query();
        query.addCriteria(Criteria.where("fieldName").is(fieldName));

        Update update = new Update();
        update.inc("seq", gap);

        FindAndModifyOptions options = FindAndModifyOptions.options();
        options.upsert(true);
        options.returnNew(true);

        Counter counter = mongoTemplate.findAndModify(query, update, options, Counter.class);

        if (counter != null) {
            return counter.getSeq();
        }
    } catch (Throwable t) {
        log.error("Exception when getNextSequence from mongodb", t);
    }
    return gap;
}

複製程式碼

不要到處使用索引

索引是很強大,但是要提醒你的是,不是所有查詢都可以用索引的。比如你要返回集合中90%的文件而非獲取一些記錄,就不應該使用索引。

如果對這種查詢用了索引,結果就是幾乎遍歷整個索引樹,把其中一部分,比方說40GB的索引都載入到記憶體。然後按照索引中的指標載入集合中200GB的文件資料,最終將載入 200GB + 40GB = 240GB的資料,比不用索引還多。

所以索引一般用在返回結果只是總體資料的小部分的時候。根據經驗,一旦要大約返回集合一般的資料就不要使用索引了。

若是已經對某個欄位建立了索引,又想在大規模查詢時不使用它(因為使用索引可能會較低效),可以使用自然排序來強制MongoDB禁用索引。自然排序就是“按照磁碟上的儲存順序返回資料",這樣MongDB就不會使用索引了。

db.students.find().sort({"$natural" : 1});
複製程式碼

如果某個查詢不用索引,MongoDB就會做全表掃描。

索引覆蓋查詢

如果你想要返回某些欄位且這些欄位都可以放到索引中,那麼MongoDB可以做索引覆蓋查詢,這種查詢不會訪問指標指向的文件,而是直接用索引的資料返回結果,比如有如下索引

db.students.ensureIndex(x : 1, y : 1, z : 1);
複製程式碼

現在查詢被索引的欄位,並要求只返回這些欄位,MongoDB就沒有必要載入整個文件

db.students.find({"x" : "xxx", "y" : "xxx"},{x : 1, y : 1, z : 1, "_id" : 0});
複製程式碼

注意由於_id是預設返回的,而它又不是索引的一部分,所以MongoDB就需要到文件中獲取_id,去掉它,就可以僅根據索引返回結果了。

若是查詢值返回幾個欄位,則考慮將其放到索引中,即使不對他們執行查詢,也能做索引覆蓋查詢。比如上面的欄位z。

AND查詢要點

假設要查詢滿足條件A、B、C的文件。滿足A的文件有40000,滿足B的有9000,滿足C的有200,要是讓MongoDB按照這個順序查詢,效率可不高。

如果把C放到最前,然後是B,然後是A,則針對B,C只需要查詢最多200個文件。

這樣工作量顯著減少了。要是已知某個查詢條件更加苛刻,則要將其放置到最前面。

OR型查詢要點

OR與AND查詢相反,匹配最多的查詢語句應該放到最前面,因為MongDB每次都要匹配不在結果集中的文件。

單表查詢儘量使用Respostory

開發中,對於簡單的查詢我一般使用MongoRepository來實現功能,如果有複雜的結合MongoTemplate,注意這兩者是可以混合使用的。

converter的建議

開發中我們要寫對於一個collection,其中有些特殊的型別(比如列舉)需要我們寫converter,大多時候是雙向的,比如db-->collection和collection-->db 如果只有一個型別需要轉換,我們可以針對這一個屬性進行轉換,比如下面的例子


@WritingConverter
@Component
public class UserStatusToIntConverter implements Converter<UserStatus, Integer> {

    @Override
    public Integer convert(UserStatus userStatus) {
        return userStatus.getStatus();
    }
}


@ReadingConverter
@Component
public class UserStatusFromIntConverter implements Converter<Integer, UserStatus> {

    @Override
    public UserStatus convert(Integer source) {
        return UserStatus.findStatus(source);
    }
}

複製程式碼

一個欄位還好,如果一個類中有很多個欄位都需要做轉換的話,就會產生很多個converter,這個時候我們可以寫一個類級別的轉換器

@ReadingConverter
@Component
public class OperateLogFromDbConverter extends AbstractReadingConverter<Document, OperateLog> {
  @Override
  public OperateLog convert(Document source) {

      OperateLog opLog = convertBasicField(source);

      if (source.containsKey("_id")) {
          opLog.setId(source.getLong("_id"));
      }

      if (source.containsKey("module")) {

          opLog.setModule(ModuleEnum.findModule(source.getInteger("module")));
      }

      if (source.containsKey("opType")) {
          opLog.setOpType(OpTypeEnum.findOpType(source.getInteger("opType")));
      }

      if (source.containsKey("level")) {
          opLog.setLevel(OpLevelEnum.findOpLevel(source.getInteger("level")));
      }

      return opLog;
  }

  private OperateLog convertBasicField(Document source) {
      Gson gson = new Gson();
      return gson.fromJson(source.toJson(), OperateLog.class);
  }
}

複製程式碼

上面程式碼我用了GSON做common field的轉換,如果你不這樣寫,就需要判斷每個欄位,然後進行填充。

想關注文章動態的可以關注公眾號喲:

知道了這些 MongoDB設計技巧,提升效率50%

相關文章