首页 > 数据库 >MongoDB高阶特性:事务、索引

MongoDB高阶特性:事务、索引

时间:2023-10-05 17:22:29浏览次数:45  
标签:transaction MongoDB db update 索引 result 高阶 id

一、事务

一)MongoDB的事务

首先我们需要知道MongoDB是有多种存储引擎的,不同的存储引擎在实现ACID的时候,使用不同的机制。而Mongodb从3.0开始默认使用的是WiredTiger引擎,本文后续所有文字均是针对WiredTiger引擎。
WiredTiger引擎可以针对单个文档来保证ACID特性,但是当需要操作多个文档的时候无法保证ACID,也即无法提供事务支持。但是,我们是否就无法实现事务呢?实际上,MongoDB本身虽然不支持跨文档的事务,但是我们依然可以可以在应用层来获取类似事务的支持。这其中有很多方式,MongoDB公司的Antoine Girbal曾经撰写过文章详细阐释了五种方式来支持事务,可以参考Reference中的链接。不过在此之前,让我们先了解下MongoDB在单文档上是如何实现ACID特性的。

二)单文档的ACID是如何实现的?

MongoDB在更新单个文档时,会对该文档加锁,而要理解MongoDB的锁机制,需要先了解以下几个概念:

  • Intent Lock(我把它翻译为意图锁): 意图锁表明读写方(reader-writer)意图针对更细粒度的资源进行读取或写入操作。比如:如果当某个Collection被加了intent lock,那么说明读写方意图针对该Collection中的某个文档进行读或写的操作。如下图所示:
    图片描述
    上图展示了当reader or writer需要操作文档时,相对更高的层级都需要加intent lock.
  • Multiple granularity locking (我把它翻译为多粒度锁机制): MongoDB采用的是所谓的MGL多粒度锁机制,具体可以参考文末的wiki链接。简单来说就是结合了多种不同粒度的锁,包括S锁(Shared lock),X锁(Exclusive lock), IS锁(Intent Share lock), IX(Intent Exclusive lock),这几种锁的互斥关系如下表所示:
    图片描述

下面,我用一个例子来简单说明下。假设我要更改name为Jim的document

db.user_collection.update({'name': 'Jim'}, {$set: {'age': 26, 'score': 50}})

此时,如图1所示,MongoDB会为name为Jim的document加上X锁,同时为包含该document的Collection,Database和instance都加上IX锁,这时,针对该文档的操作就保证了原子性。
需要注意的是:

  • 如果当age修改成功,而score没有修改成功时,MongoDB会自动回滚,因此我们可以说针对单个文档,MongoDB是支持事务,保证ACID的(严格来说,要想保证Durability,需要在写操作时使用特殊的write concern,这个后边再谈)
  • 所有的锁都是平等的,它们是排在一个队列里,符合FIFO原则。但是,MongoDB做了优化,即当一个锁被采用时,所有与它兼容的锁(即上表为yes的锁)都会被采纳,从而可以并发操作。举个例子,当你针对Collection A中的Document a使用S锁时,其它reader可以同时使用S锁来读取该Document a,也可以同时读取同一个Collection的Document b.因为所有的S锁都是兼容的。那么,如果此时针对Collection A中的Document c进行写操作是否可以呢?显然需要为Document c赋予x锁,此时Collection A就需要IX锁,而由于IX和IS是兼容的,所以没有问题。简单来说,只要不是同一个Document,读写操作是可以并发的;如果是同一个Document,读可以并发,但写不可以。
  • WiredTiger针对global, db, collection level只能使用intent lock。另外,针对冲突的情况,WiredTiger会自动重试。

三)跨文档的事务支持

前面已经说过,针对多文档,MongoDB是不支持事务的,但是我们的应用却可以自己去实现类事务的功能,这里只针对其中最常用的两步提交方式来做详细阐释。
假设我们有两个账户A和B,现在我们要让账户A转账100元给账户B,我们需要将整个过程放在一个事务当中,来保证数据的一致性。在这个应用模拟的事务当中,需要涉及两个Collection,一个是accounts collection,另一个是transaction collection(用于存储交易的信息和状态)。
先来看下transaction最终成功的大体流程:如图2所示
图片描述

伪代码如下:

initial accounts

bulk_result = db.accounts.insert(
   [
     { _id: "A", balance: 1000, pendingTransactions: [] },
     { _id: "B", balance: 1000, pendingTransactions: [] }
   ]
)
if bulk_result.nInserted != 2:
   print "insert account failed."
  return False

add a transaction

write_result = db.transactions.insert(
    { _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() }
)
if write_result.nInserted != 1:
  print "transaction failed"
  return False

update transaction to pending

t = db.transactions.findOne( { state: "initial" } )
result = db.transactions.update(
    { _id: t._id, state: "initial" },
    {
      $set: { state: "pending" },
      $currentDate: { lastModified: true }
    }
)
if result.nModified != 1:
  print "transaction failed"
  return False

update accounts & push transaction id

result_source = db.accounts.update(
   { _id: t.source, pendingTransactions: { $ne: t._id } },
   { $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } }
)
result_destination = db.accounts.update(
   { _id: t.destination, pendingTransactions: { $ne: t._id } },
   { $inc: { balance: t.value }, $push: { pendingTransactions: t._id } }
)
if result_source.nModified != 1 or result_destination.nModified != 1:
   # 进入回滚的流程
  ...
   return False

update transaction to applied

result = db.transactions.update(
   { _id: t._id, state: "pending" },
   {
     $set: { state: "applied" },
     $currentDate: { lastModified: true }
   }
)
if result.nModified != 1:
  # 重新update accounts & push transaction id
  # 注意:如果上一步是成功的,pendingTransactions列表中会有相应的Transaction,那么就不会重复更新账户
  ...

pull transaction id

result_source = db.accounts.update(
   { _id: t.source, pendingTransactions: t._id },
   { $pull: { pendingTransactions: t._id } }
)
result_destination = db.accounts.update(
   { _id: t.destination, pendingTransactions: t._id },
   { $pull: { pendingTransactions: t._id } }
)
if result_source.nModified != 1 or result_destination.nModified != 1:
  # 重新执行pull transaction id
  ...

update transaction to done

result = db.transactions.update(
   { _id: t._id, state: "applied" },
   {
     $set: { state: "done" },
     $currentDate: { lastModified: true }
   }
)
if result.nModified != 1:
  # 重新从pull transaction id执行
  ...

包含回滚和失败的整体流程如图3:
图片描述
从上图可以看出,任何一步失败都有相应的应对措施来保证事务或者执行完毕或者回滚。当然所有的实现都需要应用程序自己实现,更何况如果涉及多个应用并发的情况时,会更加复杂,如何保证多个事务不互相影响,又会进一步增加复杂度,这也就是为什么如果需要此类跨文档事务支持的时候推荐使用关系数据库。

四)MongoDB的一致性

1、外部一致性

当我们说外部一致性时,是针对分布式系统所说的CAP理论中的一致性,简单来说就是如何使得多台机器的副本保持一致,实际上Mongodb只能做到最终一致性,总会有“不一致时间窗口”,这是由于Mongodb在更新操作的时候,需要将同样的更新复制到副本节点当中,而这段时间无法保证reader读到的一定是最新数据,即使ReadConcern设置为majority,也只能保证返回目前大多数节点的所持有的数据,而不一定是最新的数据(比如,只有primary节点更新完成,其它所有secondary节点都还没有更新完成)。

db.collection.find(
   {
      // 查询条件
   },
   {
      readConcern: { level: "majority" }
   }
)

2、内部一致性

当我们说内部一致性时,是针对ACID中的一致性,可以通过设置Read Concern和Write Concer来实现

这里主要针对如何避免脏读,当Mongodb无法在大多数节点成功的更新操作时,会导致回滚操作,这时如果Reader已经读取了更改后的数据,就会产生脏读现象。而避免脏读,当我们设置Read Concern为majority时,可以保证返回的数据是大多数节点所持有的数据,这种情况是不会发生回滚的,也就避免了脏读。

还有一种情况可能出现脏读,就是当writer写数据时,虽然已经写入到了内存当中,但是并没有写入到磁盘中,这时reader读取到了更新后的数据,但当Mongodb将内存中的数据写入磁盘时可能会产生错误,从而导致磁盘写入失败,这时就可能导致该数据丢失,这种情况下也会产生脏读,而为了避免这种情况,我们需要在Write Concern设置的时候使用j:1,这样实际是在写入journal之后才返回写入成功,保证不会出现上述的脏读现象。当然这种情况下,性能势必会受到影响。所以还是要根据业务情况来决定,非关键业务不需要很强的一致性的情况下,也不需要此种设置。

3、ReadConcern(读取关注级别)

用于控制读取操作的一致性要求。它可以确保读取操作返回的数据在不同条件下的一致性。

  • local:默认级别,提供较低的一致性保证。
  • available:确保数据来自可用的节点。
  • majority:确保读取的数据已经被多数节点接受,提供较高级别的一致性。
    通过指定 readConcern 选项,可以在查询中或在读取操作中明确设置所需的一致性级别。
db.collection.find(
   {
      // 查询条件
   },
   {
      readConcern: { level: "majority" }
   }
)

4、WriteConcern(写入关注级别):

WriteConcern 用于控制写入操作的数据持久性。它确定了写入操作何时被视为成功,并返回响应。

  • acknowledged:默认级别,确保写入操作被MongoDB接收,但不一定已持久化。
  • w:1:确保写入被主节点接受。
  • w:"majority":确保写入被多数节点接受,提供较高的持久性。
    通过在写入操作中设置 writeConcern 选项,可以明确指定所需的写入关注级别。
db.collection.insertOne(
   {
      // 插入的文档数据
   },
   {
      writeConcern: { w: "majority" }
   }
)

索引

标签:transaction,MongoDB,db,update,索引,result,高阶,id
From: https://www.cnblogs.com/yifanSJ/p/17743649.html

相关文章

  • 聚簇索引与非聚簇索引区别
    聚簇索引和非聚簇索引是关系型数据库中常用的两种索引类型,它们的主要区别如下:存储方式不同:聚簇索引是将数据按照索引顺序存储在磁盘上,因此聚簇索引的数据存储和索引存储是混合在一起的;而非聚簇索引则是将索引和数据分开存储的。唯一性不同:聚簇索引必须是唯一的,因为它们是按照......
  • MongoDBHelper + Expression+ JsonResult
    usingMongoDB.Driver;usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Linq.Expressions;namespaceMongodbTest.Common{///<summary>///MongoDb帮助类///</summary>publicclassMongoDbHelper......
  • es倒序索引原理
    3.倒排索引原理:(1).两个document:ThequickbrownfoxjumpedoverthelazydogQuickbrownfoxesleapoverlazydogsinsummer(2).提取文档分词,建立倒排索引(区分了大小写):TermDoc_1Doc_2-------------------------Quick||XThe|X|br......
  • MongoDB高阶特性:副本集、分片、事务、索引
    一、副本集(主从复制)1、docker-compose.ymlversion:'3'services:mongo1:image:mongocontainer_name:mongo1command:mongod--replSetrs0--port27017volumes:-./mongodb-cluster/mongod1:/data/dbports:-"27017:2......
  • MySQL学习(3)B+树索引是如何快速查询的
    前言我们已经知道在磁盘中,有很多索引页,这些页并非在物理结构上相连接,而是通过双向链表关联。如果要查找一条数据,需要通过页目录中的槽,通过二分法定位到分组再进行遍历查找。比如下面这样:SELECT[查询列表]FROM表名WHERE条件; 假设表中只有一个页,在查找记录时,可以根据搜......
  • MongoDB 和 Redis 的区别
    在现代的应用程序开发中,数据库是不可或缺的组成部分。MongoDB和Redis是两种流行的数据库,它们在一些方面有相似之处,但在其他方面则有着显著的差异。下面,我们将探讨MongoDB和Redis的区别。数据模型MongoDB是一个面向文档的数据库,它存储的数据是以BSON(BinaryJSON)格式存......
  • ElasticSearch系列-索引原理与数据读写流程
    索引原理倒排索引倒排索引(InvertedIndex)也叫反向索引,有反向索引必有正向索引。通俗地来讲,正向索引是通过key找value,反向索引则是通过value找key。ES底层在检索时底层使用的就是倒排索引。索引模型现有索引和映射如下:{"products":{"mappings":{"proper......
  • MongoDB使用教程
    一、基本概念一)数据库(database)数据库是一个仓库,在仓库中可以存放集合二)集合(collection)集合类似于数组,在集合中可以存放文档三)文档(document)文档是数据库中的最小单位,存储和操作的内容都是文档在MongoDB中,数据库和集合都不需要手动创建,当我们创建文档时,如果文档所在的集合或......
  • MongoDB搭建
    一、Docker-composeversion:'3'services:mongodb:image:mongocontainer_name:mongodbrestart:alwaysports:-27017:27017volumes:-/var/docker/server/mongodb/database:/data/dbenvironment:-MONGO_......
  • driverindexer 是一个用于管理Windows系统驱动程序的工具。它被用于创建、修改和维护W
    driverindexer是一个用于管理Windows系统驱动程序的工具。它被用于创建、修改和维护Windows操作系统的驱动程序索引。驱动程序索引是一个记录系统中所有已安装驱动程序信息的数据库。它包含了驱动程序的名称、文件路径、版本号等详细信息。Windows系统通过查询驱动程序索引来查......