在 MongoDB 中,操作一个文档的行为是原子性的。这意味着你可以在单个文档内使用嵌套文档和数组来表达数据之间的关系,避免了将数据规范化到多个文档和集合中。这样的单文档原子性减少了许多实际场景中对于分布式事务的依赖和需求。
对于那些需要确保跨多个文档(无论它们位于单个集合还是多个集合中)的读写操作具有原子性的场景,MongoDB 提供了分布式事务的支持。分布式事务允许跨越多个操作、集合、数据库、文档甚至是不同的分片来执行事务,确保了广泛的操作能够统一完成或回滚。
什么是事务
事务是指一组操作,这些操作要么全部成功执行,要么全部不执行。它是数据库管理系统中的一个重要概念,用于确保数据的完整性和一致性。事务通常需要满足以下四个属性,通常被称为 ACID 属性:
-
原子性(Atomicity): 保证事务中的所有操作要么全部完成,要么全部不做,不会结束在中间某个环节。如果事务中的任何操作失败,整个事务都会回滚到事务开始前的状态。
-
一致性(Consistency): 确保事务的执行结果是数据库从一个一致性状态转换到另一个一致性状态,事务完成后,所有的数据规则都应用成功,保持数据的正确性和逻辑一致性。
-
隔离性(Isolation): 保证每个事务都是独立的,即事务的执行不会被其他事务干扰。多个并发执行的事务之间不会互相影响。
-
持久性(Durability): 一旦事务被提交,它对数据库的修改应该是永久性的,即使系统发生故障也不会丢失。
什么是原子性
原子性是指在数据库和计算领域中,一个操作系列作为一个不可分割的整体执行的特性。这意味着事务中的所有操作要么完全成功,要么完全失败,不会出现中间状态。如果事务中的某个操作失败,整个事务会回滚到开始前的状态,就好像这个事务从未发生过一样。原子性是确保数据一致性和系统稳定性的关键特性,特别是在处理复杂的、涉及多步操作的事务时。它是 ACID(原子性、一致性、隔离性、持久性)数据库事务特性中的第一个字母。
例如有这样的一个场景,我们有三个互相关联的表:
- 用户聊天对象表: 这个表记录了用户和他们的聊天对象。每个聊天对象都有一个唯一的标识符,我们称之为聊天对象 ID。
- 会话表(Conversation Table): 该表记录了每次聊天的详细信息,包括开始时间、结束时间等。这个表依赖于用户聊天对象表,因为每个会话都是围绕特定的聊天对象进行的。会话表中的每条记录都有一个自己的唯一标识符,即会话 ID,它与特定的聊天对象 ID 相关联。
- 聊天记录表: 这个表存储了所有的聊天消息,包括发送者、接收者、消息内容和时间戳等。聊天记录表依赖于会话表,因为每条聊天记录都属于一个特定的会话。聊天记录通过会话 ID 与特定的会话相关联。
在这样的设计中,用户聊天对象表、会话表和聊天记录表通过聊天对象 ID 和会话 ID 形成了依赖链。任何对这些表的操作都需要保持原子性,以确保数据的一致性和完整性。例如,如果你要为一个新的聊天对象创建一个会话和相关的聊天记录,你需要确保这三个表的更新要么全部成功,要么全部失败,这样就不会有悬挂的会话或无法追溯的聊天记录。
什么是隔离性
隔离性是数据库事务四大特性(ACID)之一,它指的是在并发环境中,能够隔离或分离多个事务,使它们不会相互干扰,确保每个事务都是独立执行的。隔离性的主要目的是控制事务对共享数据的并发访问,防止因多个事务同时操作同一数据而导致的数据不一致问题。
实现隔离性主要是通过锁定资源和版本控制等机制。数据库系统通常提供不同级别的隔离,每个级别都能在性能和一致性之间提供不同的平衡。以下是常见的隔离级别,从最低到最高排列:
-
读未提交(Read Uncommitted): 最低级别的隔离,事务可以读取未被其他事务提交的数据。这可能导致
脏读
,即读取到其他事务未提交的更改。 -
读提交(Read Committed): 确保一个事务不会读取到其他未提交事务的数据,避免了脏读,但仍然可能遇到
不可重复读
和幻读
问题。 -
可重复读(Repeatable Read): 确保在一个事务内多次读取同一数据的结果是一致的,避免了不可重复读,但在某些系统中可能仍然面临幻读问题。
-
串行化(Serializable): 最高级别的隔离,通过强制事务串行执行,避免了脏读、不可重复读和幻读,但相应地降低了并发性能。
隔离性的实现和级别选择会直接影响到数据库的并发处理能力和系统整体性能。在设计和选择数据库事务的隔离级别时,通常需要在数据一致性和系统性能之间做出权衡。
事务和原子性
对于那些需要确保跨多个文档(这些文档可能位于同一个或不同的集合中)进行读取和写入操作的原子性,MongoDB 提供了分布式事务的支持。这种支持不仅适用于副本集,也适用于分片集群上的事务。
分布式事务被称为原子的是因为它们保证了即使在分布式系统的多个节点上执行多个操作,这些操作要么全部成功,要么全部失败。在分布式系统中,数据可能被分散在不同的服务器或位置上,而分布式事务需要跨越这些分散的数据点来执行。
以下是为什么分布式事务被视为原子的几个关键点:
-
全局一致性: 分布式事务确保所有参与节点,不管它们物理上位于何处,最终都将达到一个全局一致的状态。如果事务在任何节点上失败,所有其他节点上的变更都会被回滚,保持全局的数据一致性。
-
协调和一致性协议: 分布式事务通常依赖于复杂的协调和一致性协议,如两阶段提交(2PC)或三阶段提交等,这些协议确保了事务在所有相关节点上要么全部提交,要么全部回滚
-
故障恢复和持久性: 在分布式事务中,如果部分节点由于网络分区或其他原因失败,事务管理系统会确保这些事务在节点恢复后能够继续完成或回滚,从而保持操作的原子性。
-
隔离性保证: 即使在并发环境中,分布式事务也提供了隔离级别的保证,确保并发事务不会互相干扰,每个事务都像是在一个隔离的环境中执行一样。
因此,通过这些机制和属性,分布式事务提供了一种方式,能够在分布式环境中安全地执行跨多个节点的操作,同时确保所有操作要么全部成功完成,要么在错误发生时完全不发生,这就是它们被称为原子的原因。
Read Concern/Write Concern/Read Preference
在执行事务操作时,MongoDB 提供了几个选项:readConcern、writeConcern 和 readPreference,用来调整和控制事务会话(Session)的具体行为。接下来,我们将分别详细介绍这些选项。
事务和 Write Concern
Write Concern(写关注等级)是 MongoDB 中的一个设置,用于控制数据写入操作的确认级别。它定义了在认为一个写操作成功之前需要满足的条件,从而允许开发者根据对数据一致性和可靠性的需求,与性能之间做出权衡。
事务使用事务级写入关注点来提交写入操作。事务内部的写入操作必须在没有显式写入关注规范的情况下发出,并使用默认的写入关注。在提交时,然后使用事务级写入关注点提交写入。
这句话好像挺难理解,接下来我们再来详细拆分一下这句话:
-
事务级写入关注点: 在 MongoDB 中,你可以为整个事务指定一个写入关注点。这个事务级的写入关注点定义了事务提交时必须满足的条件。它决定了在认为整个事务成功之前,对数据库所做的更改需要被多少个副本确认。
-
事务内部的写入操作: 当你在事务中执行多个写入操作时(比如插入、更新或删除数据),这些操作本身通常不会指定写入关注点。它们使用数据库的默认写入关注设置来执行。
-
没有显式写入关注规范: 这意味着在事务执行过程中,单个写入操作不会带有特定的写入关注点。它们依赖于数据库的默认设置或者事务级的设置。
-
提交事务: 当你准备提交事务时,即你完成了所有的写入操作,并想要将这些更改永久地应用到数据库时,此时事务级的写入关注点就起作用了。事务会根据你为整个事务设置的写入关注点来提交。如果没有满足这个级别的条件,事务将不会成功提交。
简而言之,这段话说明在 MongoDB 中,单个写入操作在事务中默认使用数据库的写入关注设置执行,而整个事务的成功提交则需要满足事务级别设定的写入关注点。这种机制确保了事务的原子性和数据的一致性,同时提供了灵活性,允许不同事务根据需求设置不同的确认级别。
写入关注包括以下字段:
js
{ w: <value>, j: <boolean>, wtimeout: <number> }
- w 选项:用于请求确认写入操作已传播到指定数量的实例或具有指定标签的 mongod mongod 实例。
- J 选项:请求确认写入操作已写入磁盘日志的选项。
该 w 选项请求确认写入操作已传播到指定数量的实例或具有指定标签的 mongod mongod 实例。如果写入关注点缺少该字段,MongoDB 会将该 w 选项设置为默认写入关注点。
使用该 w 选项,可以使用以下 w: <value>
写入问题:
-
majority:这确保了写操作被数据库集群中的大多数成员确认。这意味着数据至少已经复制到了足够多的副本上,即使发生了一些故障,数据也不太可能丢失。这一般用于那些对数据可靠性和一致性要求较高的场景。但是会有更高的延迟,因为需要等待更多的确认。
-
0:意味着写操作不需要被确认。操作会被发送到服务器,但客户端不会等待任何确认,也不会知道操作是否成功。这通常用于那些不太重要的数据,或者当性能比数据的完整性更重要时,因为如果发生错误或者函数丢失,你可能无法得知。
-
1:这是默认设置,表示只要主节点确认了写操作,它就被认为是成功的。提供了一个平衡点,确保了数据至少被存储在主节点,同时保持了良好的性能。如果主节点发生故障,最近的写入可能会丢失。
-
<number>
:可以指定一个特定的数字,表示操作需要被集群中特定数量的成员确认,这允许更细粒度的控制,适用于需要特定可靠性级别的场景。这就需要了解你的数据库集群的具体结构和状态,以选择合适的数字。 -
<tag>
:在复制集的配置中,你可以使用标签来指定哪些成员必须确认写操作,这允许更复杂的部署策略,例如,确保特定的地理区域或数据中心已经接收到数据。这就需要仔细配置和管理标签,以确保它们反映了你的需求。
选择合适的 w 值取决于你的具体需求。如果你需要高性能和低延迟,你可能会选择一个较低的 w 值。但如果你的数据非常重要,你可能会选择 majority 或更高的设置以确保数据的安全和一致性。在任何情况下,了解你的数据和应用程序的需求,并根据这些需求来平衡性能和可靠性是非常重要的。
设置示例:
js
writeConcern: {
w:"majority" // 大多数原则
j:true,
wtimeout: 5000,
}
在 nestjs 中使用示例:
ts
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { CatDocument, CatSchema } from "./schemas/cat.schema";
@Injectable()
export class CatService {
constructor(
@InjectModel(CatSchema.name) private catModel: Model<CatDocument>
) {}
async createWithWriteConcern(data: any): Promise<CatDocument> {
const createdData = new this.catModel(data);
return await createdData.save({ w: "majority" }); // 设置写关注点为 majority
}
}
对于重要数据可以应用 w:"majority" 设置,普通数据 w:1 设置则可以保证性能最佳,w 设置的节点数越多,等待的延迟也就越大,如果 w 等于总节点数,一旦其中某个节点出现故障就会导致整个写入失败,也是有风险的。
-
对于需要高度一致性和耐用性的重要数据,建议使用 w:"majority"设置。这意味着一个写操作只有在大多数副本集成员确认后才被视为成功。这样做可以确保即使在发生某些节点故障的情况下,数据也不会丢失,因为它已经被数据库集群中的大多数节点确认。
-
w 设置的节点数越多,等待的延迟也就越大: 当你增加确认写操作所需的节点数量时(例如,设置 w 为一个具体的数字),MongoDB 需要等待更多的节点来确认每个写操作。虽然这提高了数据的一致性和耐用性,但也意味着每个写操作的完成时间会更长,因此延迟会增加。
-
如果 w 等于总节点数,一旦其中某个节点出现故障就会导致整个写入失败,也是有风险的: 如果你将 w 设置为等于集群中的总节点数,这意味着每个写操作需要所有节点的确认才能成功。这种配置虽然最大化了数据的一致性,但也带来了风险:如果任何节点因为网络问题、硬件故障或其他原因变得无法访问,所有的写操作都会失败。这种情况下,系统的可用性会受到严重影响。
事务和 Read Preference
在一个事务操作中使用事务级别的 readPreference 来决定读取时从哪个节点读取。可方便的实现读写分离、就近读取策略。
-
读写分离:使用 readPreference 可以实现读写分离策略。例如,你可以配置所有的写操作都发送到主节点,而读操作则可以分散到其他副本节点。这样可以减轻主节点的负载,优化整体性能和响应时间。
-
就近读取策略: 如果你的 MongoDB 集群跨越多个地理位置,使用 readPreference 可以配置读操作优先从地理位置上接近客户端的节点读取数据。这样可以减少延迟,提高读操作的速度。
-
通常,readPreference 可以在每个读操作或连接级别进行设置。但在事务操作中指定 readPreference 意味着在该事务的上下文中,所有的读操作都将遵循这一策略,无论是从主节点还是从特定的副本节点读取。这提供了一种在事务层面上统一控制读取行为的方式。
通过在事务中使用 readPreference,可以根据应用需求和部署架构灵活地控制数据的读取方式,优化性能和资源利用,同时确保数据的一致性和可靠性。
MongoDB 的读取首选项(readPreference)允许你选择从哪个副本集成员或分片中读取数据。这可以帮助你平衡查询性能、数据一致性和系统可用性。以下是 MongoDB 中几种常见的读取首选项模式:
-
Primary (主节点): 这是默认的读取模式,所有的读操作都会被直接发送到主节点。这样做可以确保读取到的数据是最新且一致的,非常适合对数据一致性要求极高的应用场景。
-
PrimaryPreferred (主节点优先): 在这种模式下,系统会首选主节点进行读取。但如果主节点不可用,比如因为网络分区或故障,读操作会自动退回到可用的副本节点。这种模式提供了高一致性的同时增加了系统的容错能力,适用于既想确保一致性又不希望在主节点故障时服务完全中断的场景。
-
Secondary (副本节点): 此模式下所有的读操作都会被发送到副本节点。这有助于减轻主节点的负载并利用副本集群的读取能力,适合于读操作远多于写操作,且可以容忍数据延迟的场景。
-
SecondaryPreferred (副本节点优先): 默认情况下从副本节点读取数据,但如果没有可用的副本节点,读操作会自动退回到主节点。这种模式优先考虑性能和分摊负载,在副本不可用时仍能保证服务的可用性,适用于优先考虑性能和分摊负载,但在副本不可用时仍需保证服务的场景。
-
Nearest (最近节点): 读操作会被发送到网络延迟最低的节点,不论是主节点还是副本节点。这有助于最小化读操作的延迟,适合对读操作响应时间非常敏感的场景。
选择合适的 MongoDB 读取首选项取决于我们的应用特定需求,包括对数据一致性、可用性、读取性能和网络延迟的考虑。下面是对每种读取首选项适用场景的指导:
-
Primary (主节点):需要保证读取到的数据是最新和最一致的情况,例如金融交易系统,其中读取的数据必须是准确无误的最新数据。
-
PrimaryPreferred (主节点优先): 希望在大多数情况下保证数据的一致性,但当主节点不可用时仍希望服务可用。适用于对一致性要求高,但又需要一定容错能力的应用,例如在线商务平台,主节点故障时可以临时从副本读取数据,保持服务不中断。
-
Secondary (副本节点):适合读取操作远多于写入操作,且可以容忍数据延迟的场景。例如,分析和报告系统,这些系统经常读取大量数据进行处理,而这些数据不需要是最新的。
-
SecondaryPreferred (副本节点优先):适合当主节点负载较高或响应时间较慢时,希望通过读取副本节点来提高性能和响应速度的应用。同时,在副本节点不可用时,可以回退到主节点,如一些内容分发网络(CDN)或缓存层。
-
Nearest (最近节点): 对于分布式部署跨多个地理位置的 MongoDB 集群,当应用对读操作的响应时间极其敏感时,最近节点模式可以确保读取操作尽可能快地完成。适用于地理分布广泛的用户基础,例如全球性的在线服务,可以从用户最近的数据中心读取数据。
在选择读取首选项时,重要的是要了解这些首选项对性能和一致性的影响,并考虑应用的具体需求。有时候,可能需要在数据的最新性和读取性能之间做权衡。在实际应用中,最好根据应用的实际运行情况和用户需求进行适当的调整和优化。
事务和 Read Concern
在 MongoDB 事务中,所有读操作都只遵循事务本身指定的读取关注点设置,而忽略在集合或数据库级别设定的任何读取关注点。这确保了事务内部的读操作具有统一的一致性标准。
与 readPreference 不同,readPreference 决定从哪个节点读取,readConcern 决定该节点的哪些数据是可读的。主要保证事务中的隔离性,避免脏读。
Read Concern 选项允许你指定一个读操作应该返回的数据快照的稳定性和一致性级别。以下是几种常见的 Read Concern 级别:
-
local:默认级别。返回操作节点上最近可用的数据。这可能包括尚未被副本集中大多数成员确认的最新写入。它提供最快的读取响应。适用于对读取速度要求高,但可以容忍读取到尚未完全一致的数据的场景。
-
available: 类似于 local,但在分片集群(sharded clusters)中,它只保证返回可用的数据,并不保证返回最新的写入。主要用于分片集群,确保读操作返回数据,但不一定是最新的。适用于可容忍某程度延迟的查询。
-
majority:只返回已经被副本集中大多数成员确认的数据。这确保了读取的数据是稳定的,并且不会因后续的回滚而变更。适用于需要高数据一致性的场景。它提供了更强的数据保证,适用于如金融服务等对数据准确性要求极高的应用。
-
linearizable:提供最高级别的数据一致性。确保读取操作返回的是集群中所有节点中最新确认的数据。它适用于对数据实时性和一致性要求非常高的场景。这种级别可以确保读取操作反映了所有最近的写入操作,但可能会有更高的延迟和降低的吞吐量。
-
snapshot:在多文档事务中使用,确保事务内的所有读操作都返回在事务开始时的数据快照。它适用于需要在事务中保持数据视图一致性的场景,如处理复杂的业务逻辑或批量更新。
MongoDB 的 readConcern 默认情况下是脏读,例如,用户在主节点读取一条数据之后,该节点未将数据同步至其它从节点,就因为异常挂掉了,待主节点恢复之后,将未同步至其它节点的数据进行回滚,就出现了脏读。
readConcern 级别的 majority 可以保证读到的数据已经落入到大多数节点。所以说保证了事务的隔离性,所谓隔离性也是指事务内的操作,事务外是看不见的。
参考文献
总结
MongoDB 的事务提供了 ACID 兼容的操作,允许跨多个文档进行原子性更改。它们支持分布式事务,适用于副本集和分片集群,但可能会影响性能。MongoDB 事务通过 readConcern 和 writeConcern 设置控制隔离级别,尽管强大,但在使用时需考虑操作限制和版本要求。