一、事务的概念
(一)什么是事务?
事务四大特性:A(原子性)、C(一致性)、I(隔离性)、D(持久性)
原子性:
原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失
败。
一致性:
事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
隔离性:
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被 其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性:
一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生 故障也不应该对其有任何影响。
(二)为什么 MongoDB 需要事务
MongoDB 推荐文档模型,使用嵌入文档将不同数据放在一个文档中,以下图为例:
这个文档是某位职场人信息记录,其中包含名字、职位、保险身份证号以及地址。地址 信息就是一个嵌入文档,包括城市街道和邮政编码。
文档模型将多种相关数据放到一个文档当中,使得业务在使用的时候,修改文档只需要 支持单行事务,就可以对一个文档中的不同字段同时进行修改。
但是在某些场景下,单行事务无法满足业务需求,需要跨行事务。
例如:
1)大量多对多的关系
股票价格和交易详情是大量多对多关系的常见场景。股票的每个交易详情都会影响到股 票的价格变动,因此需要新写一条交易记录,然后同时并发更改股票价格。这两个操作需要 在一个事务当中原子地进行。但由于这些交易详情的数据过多,一个文档限制为 16MB 无 法放置,所以这种场景需要进行跨表操作。
2)事件处理
在创建用户的时候,需要在用户表与事件表里分别写一条记录,这样应用中其他系统就 可以去处理新建用户的事件。
3)业务操作记录
比如某业务需要进行一个数据操作,同时要在一个日志表里记录数据操作,业务进行操 作和日志表记录需要在一个事务当中,操作记录需在另一个独立的表中实施,此时两个操作 需要是跨表进行,并且是原子操作。
以上三种场景单行事务无法满足,但通过 MongoDB 跨行事务的功能可以得到有效解
决。
(三)MongoDB 对事务的支持
- 单行事务:原子更新一个文档中的多个字段;
- 副本集的跨行事务(v4.0):在副本集上,跨多个文档、多个表、多个 DB 的多个操作保 持原子性;
- 集群的跨行事务(v4.2):在分片集群上,跨多个文档、多个表、多个 DB 的多个操作保 持原子性;
- 事务包含部分 DDL(v4.4):包括创建表和索引。
二、事务的使用
(一)MongoDB 单行事务
单行操作为什么需要事务?单行事务并不只是对一个数据文档进行更新,也需要对多个 文档进行更新。如上图所示,将 MongoDB 写入一个文档需要五个步骤:
第一,单行事务对 People 表写一条记录,People 表里面有两个索引,一个是非唯一 索引的名字,另外一个是唯一索引,它是一个身份证号,Key 为 1 表明第一行记录, Value 为记录详情;
第二,MongoDB 会添加一个_ID 字段,会将索引项写到_ID 索引中,因此第二行 Index_ID 索引表里也写条记录,它的 Key 就是一个_ID,Value 对应的就是数据表中行的 记录;
第三,将索引项写到用户创建的索引当中。由于用户创了两个索引,因此需要先写一个 唯一索引 Index_Ssn。接着需要写一个非唯一索引,因为非唯一索引可能出现多个,因此 除了要把非唯一索引数据放进来,还需要将行号也放进来;
第四,唯一索引检查 Key 是否重复,检查 Ssn 索引表里这个记录是否已经存在,如果 存在的话则会报错;
第五,写入操作日志到复制表,将写操作同步到 Secondary 节点上。
单行操作存储引擎支持事务的,这样才能将多个操作放在一个事务中进行。
(二)MongoDB 副本集的跨行事务
当单行事务无法满足业务需求时,则需要跨行事务(4.0 版本之后)。
上面为一个副本集架构图,应用读写 Primary,写的记录会复制到 Secondary 存库中, 事务的操作均为读写主库。
事务参数:
ReadConcern: snapshot
WriteConcern: majority
ReadPreference: primary
跨行事务举例:
上图场景为某职场人离开一家公司加入一家新公司。
首先先开启一个 Session,在 Session 上获取员工 People 表与公司 Company 表。 随后 Session 开启一个事务 ReadConcern: Snapshot,WriteConcern: Majority。
接着在员工表中确认员工身份证 ID 与信息是否存在,如果存在则将相关信息在员工表 与公司表中进行替换,将这两个操作放到一个事务中,对事务进行提交。
以上就是一个较为常见的跨表事务操作场景,该操作也存在一定的限制:
限制一:事务最长生命周期:TransactionLifetimeLimitSeconds(60s); 限制二:(v4.0)所有写操作的数据大小不超过 16MB(4.2 之后不再限制)。
生产环境的事务代码,考虑写冲突、网络报错:
以上图为例,代码两个函数调用组成。
先看下面函数的调用,它通过循环运行事务,然后用 Try Except 捕捉操作来报错, 报错的识别错误中有个标识一类错误码 Error Label。
当这一类错误码是 TransientTransactionError 时,意味着遇到一个写冲突或者是请 求过程时发现网络报错,在这种报错场景下,用户往往可以通过重试解决。
上面的函数是一个事务的操作,通过 Session 开启事务,然后往两个表里分别写一条 记录,在 Commit_transaction 时做 while 循环,目的是解决网络请求报错,这个请求报 错是由于 MongoDB Commit 结束以后回包时网络的报错,导致这个包没有被客户端接 收,因此客户端无法得知事务是否提交成功,所以客户端会返回一个报错"UnknownTran sactionCommitResult ",除了这种类型的报错需要循环以外,其他报错无需再循环重试。
在业务环境当中,可以根据实际情况加上一个重试次数或重复时间的约束限制,避免做 无限重试。
(三)MongoDB 集群的跨行事务(分布式事务)
从 4.2 版本开始,MongoDB 实现了集群的跨行事务,也就是分布式事务。
集群样例图如下:
事务参数:
ReadConcern: snapshot
WriteConcern: majority
ReadPreference: primary
分片表:
People { ssn: "hashed" }
跨行事务样例:
这个样例上面提到的样例类似,不同的地方在于它的 People 表是在分片集群里的哈希 分片表。
它的使用方式与副本集事务完全一样,这样的好处在于当数据库从副本集迁到集群时, 业务代码无需更改,对于开发者非常友好。
(四)MongoDB 事务包含部分 DDL
MongoDB 从 4.4 版本开始事务包含部分 DDL,事务操作包含创建表和索引,这里有 两种场景需要在事务中使用 DDL,第一种场景举例如下:
如上图所示,用户业务一开始使用北京的 Region,并在其中部署了一个 MongoDB 库,当在杭州扩展业务时,杭州 Region 中也需要部署一个杭州的 MongoDB 库。
此时需要做一些库初始化操作,例如创造一些表或者索引这些信息,需要同步移植到杭 州库中。因为需要保证业务库的初始化操作符合原子性,因此需要使用事务的 DDL。
第二种场景是事务中一些 Insert 的操作,比如我们在开发测试环境当中,我们的数据 对性能要求不高,可以直接将业务代码写入一个初始的空库中,此时 Insert 操作会包含自 动创表等操作,能够也可以保证我们业务代码能够不报错,但是它可能没有索引相关信息。
创表案例如下:
三、事务的原理
(一)MongoDB 事务的特征
All or Nothing
具有原子性。
Snapshot 隔离
事务开始时会产生 Snapshot,后续的读写操作不会影响 Snapshot。
Read Your Own Write
事务在新写数据时,第一条操作为写操作,第二个操作为读操作,可以直接读取事务内 尚未提交的写操作数据,事务以外的写操作需要等提交后才可读取。
(二)MongoDB 事务冲突
写操作冲突内部原理图
如上图所示,第一个 TXN 对文档 1 触发一个写操作,它会占用文档 1 的 Write Lock, 如果此时第二个 TXN 也进行了一个写操作对文档 1 做修改,那么第二个 TNX 将无法获得 文档 1 的 Write Lock,事务会 Abort 全部回滚。
如果此时有另外一个非事务也对文档 1 进行写操作,那么它更新时也无法获得 Write Lock,而且因为它是非事务操作,所以无法直接回滚,造成阻塞。直到第一个 TXN 事务 提交,或者阻塞时间超过 MaxTimeMs,则会发生报错。
(三)MongoDB 存储引擎的事务能力
MongoDB 之所以能支持事务得益于存储引擎 WiredTiger, WiredTiger 在 4.0 版 本之后使用 Timestamp 决定事务的顺序性,实现了副本集的事务操作。
上图为一个 MongoDB,由 Server 层与 WiredTiger 层组成。当进行读操作的时候, 会产生一个 Snapshot,例如:
如上图所示,读操作只能读到之前提交的写操作,比如 t1 与 t2,之后新写的 t3 操作无 法读取。
这个动作的实现主要是因为使用 WiredTiger 一个多版本并发控制 MVCC 的技术,支 持事务的冲突检测功能。
数据和索引在 WiredTiger 的 B 树中存储。
下图为 WiredTiger 存储事务的实现图:
WiredTiger 对事务的支持
- Update List:多版本数据,实现读写互斥
- Update Check
- 遍历 Update List,判断是否存在冲突
- 冲突:Prepare 的修改或并发的修改
- Modify:原子操作插入到 Update List 最前面
- Read Check
- 遍历 Update List,找到最新可见的版本
- 冲突:Prepare 的修改
- 对 Prepare 修改造成的冲突会自动重试
(四)MongoDB 副本集的跨行事务
- 事务参数 WriteConcern: Majority 保证写的数据不会回滚;
- 事务参数 ReadConcer: Snapshot 隐含 Majority,保证读到数据不会回滚;
- MongoDB 通过这种方式,将传统事务隔离性从单机扩展到分布式场景。
非事务的读请求
实现原理如下:
非事务读请求存在以下特点:
- 非事务读请求没有 Snapshot 隔离;
- MongoDB 在一个读请求期间会多次 Yield,释放 WiredTiger Snapshot;
- MongoDB 在多个读请求同样会使用多个 WiredTiger Snapshot。
上图为一个非事务读请求,在 t2 与 t3 直接触发一次 Find,可以读到 t1、t2。之后触 发了一次 GetMore,同样的遍历读到了 t3。
读事务的 Snapshot 隔离
- 读事务在一个读请求只会使用一个 WiredTiger Snapshot;
- 读事务在多个读请求利用 Logical Session 保留 Context,同样只会使用一个 Wired Tiger Snapshot。
读事务对存储引擎 Cache 压力
- Cache 压力来自于事务 Snapshot 之后的写请求量;
- 事务的整个生命周期会使用相同的 Snapshot;
- Update Structure 在 Snapshot 被 Evict 后才能清理。
如何避免存储引擎 Cache 压力
- TransactionLifetimeLimitSeconds 设为默认 60s;
- 提交 Read-Only 事务; 中止不需要的事务;
- 事务的修改的文档大于 1000 Documents 且小于 16MB Oplog。
(五)MongoDB 集群的跨行事务
事务参数:
ReadConcern: snapshot
WriteConcern: majority
ReadPreference: primary
分布式 snapshot 隔离级别:
可重复读取多个 Shard 数据一致的副 本集 Snapshot。
多个 Shard 数据一致通过混合逻辑时钟来实现,所有 Shard 节点触发一个 Snapshot,混合逻辑时钟可以解决分布式场景下,物理时钟不一致无法定序的问题。
如上图所示,PT 是物理时钟,然后 I 是逻辑时钟,C 是这个逻辑时钟之间发生的操作
数。
当两个 Shard 之间有通信时,则产生前后的因果关系。比如看 10 秒,Shard 0 往 Shard 1 同步了一条信息, Shard 0 的物理时钟是 10,但 Shard 1、Shard 2、Shard 3 的物理时钟是 0,当 Shard 0 往 Shard 1 同步一条数据后,假如 Shard 1 的物理时钟 变成了 1,但它的逻辑时钟是收到了时钟 10 跟 1 的最大值,它会进行加 1,那就变成了 1, 11。
当消息在 Shard 之间直接进行多次传递后,所有 Shard 都会产生数据时间一致的逻辑 时钟。当使用逻辑时钟,比如 I=10,c=0 时做一个 Snapshot,会发现图中黑线的时间点, 可以保证在分布式场景下数据是一致的。
MongoDB 集群的跨行事务通过两阶段提交实现,原理图如下:
- 两阶段提交:
- 参与者 Prepare:生成 PrepareTs
- 参与者 Commit:使用协调者收集的 max{PrepareTS} 作为 CommitTS
- 协调者:收集决策、记录 Commit Log
- 参与者:执行事务、记录 Prepare Log
- 故障恢复:Config 节点保存分布式事务的状态
- 协调者状态记录于 Config.Transaction_Coordinators
- 参与者状态记录于 Config.Transaction 表
(六)MongoDB 事务使用的注意事项
- 各种数据模型都能适用;
- 事务不应该是最常用的操作;
- 事务的操作中,都应该要包含 Session;
- 事务会报错,需要增加重试逻辑;
- 不必要的事务 Snapshot 要尽快关闭;
- 如果想产生写冲突,确保事务做了写操作;
- 注意 DDL 操作,已有的事务操作会阻塞 DDL,DDL 会阻塞之后的事务。
四、总结
- 为什么 MongoDB 需要事务?
- 在多对多的关系、某个事件驱动或记操作日志时的场景下,需要进行跨表操作,同时需 要操作保持原子性,因此 MongoDB 需要事务。
- MongoDB 在副本集和集群上跨行事务的使用方法
- 特别注意,在实际业务中根据需求,设置重试时间或重试次数。
- MongoDB 存储引擎的事务能力。
- 副本集的跨行事务的原理,以及对 Cache 造成的压力和避免方法。
- 集群的跨行事务的原理。
- 事务使用的注意事项。
快速掌握MongoDB核心技术干货目录
电子书下载:《玩转MongoDB从入门到实战》 | https://developer.aliyun.com/article/780915 |
走进 MongoDB | https://developer.aliyun.com/article/781079 |
MongoDB聚合框架 | https://developer.aliyun.com/article/781095 |
复制集使用及原理介绍 | https://developer.aliyun.com/article/781137 |
分片集群使用及原理介绍 | https://developer.aliyun.com/article/781104 |
ChangeStreams 使用及原理 | https://developer.aliyun.com/article/781107 |
事务功能使用及原理介绍 | https://developer.aliyun.com/article/781111 |
MongoDB最佳实践一 | https://developer.aliyun.com/article/781139 |
MongoDB最佳实践二 | https://developer.aliyun.com/article/781141 |