事务功能使用及原理介绍-阿里云开发者社区

开发者社区> 阿里云数据库> 正文
登录阅读全文

事务功能使用及原理介绍

简介: 作者 | 沧河

一、事务的概念

(一)什么是事务?


事务四大特性:A(原子性)、C(一致性)、I(隔离性)、D(持久性)


原子性:

原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失

败。


一致性:

事务必须使数据库从一个一致性状态变换到另外一个一致性状态。


隔离性:

事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被 其他事务的操作数据所干扰,多个并发事务之间要相互隔离。


持久性:

一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生 故障也不应该对其有任何影响。


(二)为什么 MongoDB 需要事务


MongoDB 推荐文档模型,使用嵌入文档将不同数据放在一个文档中,以下图为例:

(二)为什么 MongoDB 需要事务.png

这个文档是某位职场人信息记录,其中包含名字、职位、保险身份证号以及地址。地址 信息就是一个嵌入文档,包括城市街道和邮政编码。


文档模型将多种相关数据放到一个文档当中,使得业务在使用的时候,修改文档只需要 支持单行事务,就可以对一个文档中的不同字段同时进行修改。

但是在某些场景下,单行事务无法满足业务需求,需要跨行事务。


例如:


1)大量多对多的关系

大量多对多的关系.png

股票价格和交易详情是大量多对多关系的常见场景。股票的每个交易详情都会影响到股 票的价格变动,因此需要新写一条交易记录,然后同时并发更改股票价格。这两个操作需要 在一个事务当中原子地进行。但由于这些交易详情的数据过多,一个文档限制为 16MB 无 法放置,所以这种场景需要进行跨表操作。


2)事件处理

2)事件处理.png

在创建用户的时候,需要在用户表与事件表里分别写一条记录,这样应用中其他系统就 可以去处理新建用户的事件。


3)业务操作记录

3)业务操作记录.png

比如某业务需要进行一个数据操作,同时要在一个日志表里记录数据操作,业务进行操 作和日志表记录需要在一个事务当中,操作记录需在另一个独立的表中实施,此时两个操作 需要是跨表进行,并且是原子操作。


以上三种场景单行事务无法满足,但通过 MongoDB 跨行事务的功能可以得到有效解

决。


(三)MongoDB 对事务的支持

MongoDB 对事务的支持.png


  • 单行事务:原子更新一个文档中的多个字段;
  • 副本集的跨行事务(v4.0):在副本集上,跨多个文档、多个表、多个 DB 的多个操作保 持原子性;
  • 集群的跨行事务(v4.2):在分片集群上,跨多个文档、多个表、多个 DB 的多个操作保 持原子性;
  • 事务包含部分 DDL(v4.4):包括创建表和索引。


二、事务的使用

(一)MongoDB 单行事务

)MongoDB 单行事务.png

单行操作为什么需要事务?单行事务并不只是对一个数据文档进行更新,也需要对多个 文档进行更新。如上图所示,将 MongoDB 写入一个文档需要五个步骤:


第一,单行事务对 People 表写一条记录,People 表里面有两个索引,一个是非唯一 索引的名字,另外一个是唯一索引,它是一个身份证号,Key 为 1 表明第一行记录, Value 为记录详情;


第二,MongoDB 会添加一个_ID 字段,会将索引项写到_ID 索引中,因此第二行 Index_ID 索引表里也写条记录,它的 Key 就是一个_ID,Value 对应的就是数据表中行的 记录;


第三,将索引项写到用户创建的索引当中。由于用户创了两个索引,因此需要先写一个 唯一索引 Index_Ssn。接着需要写一个非唯一索引,因为非唯一索引可能出现多个,因此 除了要把非唯一索引数据放进来,还需要将行号也放进来;


第四,唯一索引检查 Key 是否重复,检查 Ssn 索引表里这个记录是否已经存在,如果 存在的话则会报错;


第五,写入操作日志到复制表,将写操作同步到 Secondary 节点上。

单行操作存储引擎支持事务的,这样才能将多个操作放在一个事务中进行。


(二)MongoDB 副本集的跨行事务


当单行事务无法满足业务需求时,则需要跨行事务(4.0 版本之后)。

MongoDB 副本集的跨行事务.png

上面为一个副本集架构图,应用读写 Primary,写的记录会复制到 Secondary 存库中, 事务的操作均为读写主库。


事务参数:


ReadConcern: snapshot

WriteConcern: majority

ReadPreference: primary


跨行事务举例:

跨行事务举例:.png

上图场景为某职场人离开一家公司加入一家新公司。


首先先开启一个 Session,在 Session 上获取员工 People 表与公司 Company 表。 随后 Session 开启一个事务 ReadConcern: Snapshot,WriteConcern: Majority。


接着在员工表中确认员工身份证 ID 与信息是否存在,如果存在则将相关信息在员工表 与公司表中进行替换,将这两个操作放到一个事务中,对事务进行提交。


以上就是一个较为常见的跨表事务操作场景,该操作也存在一定的限制:

限制一:事务最长生命周期:TransactionLifetimeLimitSeconds(60s); 限制二:(v4.0)所有写操作的数据大小不超过 16MB(4.2 之后不再限制)。


生产环境的事务代码,考虑写冲突、网络报错:

网络报错.png

以上图为例,代码两个函数调用组成。


先看下面函数的调用,它通过循环运行事务,然后用 Try Except 捕捉操作来报错, 报错的识别错误中有个标识一类错误码 Error Label。


当这一类错误码是 TransientTransactionError 时,意味着遇到一个写冲突或者是请 求过程时发现网络报错,在这种报错场景下,用户往往可以通过重试解决。


上面的函数是一个事务的操作,通过 Session 开启事务,然后往两个表里分别写一条 记录,在 Commit_transaction 时做 while 循环,目的是解决网络请求报错,这个请求报 错是由于 MongoDB Commit 结束以后回包时网络的报错,导致这个包没有被客户端接 收,因此客户端无法得知事务是否提交成功,所以客户端会返回一个报错"UnknownTran sactionCommitResult ",除了这种类型的报错需要循环以外,其他报错无需再循环重试。


在业务环境当中,可以根据实际情况加上一个重试次数或重复时间的约束限制,避免做 无限重试。


(三)MongoDB 集群的跨行事务(分布式事务)


从 4.2 版本开始,MongoDB 实现了集群的跨行事务,也就是分布式事务。


集群样例图如下:

集群样例图如下.png

事务参数:


ReadConcern: snapshot

WriteConcern: majority

ReadPreference: primary


分片表:

People { ssn: "hashed" }


跨行事务样例:

跨行事务样例.png

这个样例上面提到的样例类似,不同的地方在于它的 People 表是在分片集群里的哈希 分片表。


它的使用方式与副本集事务完全一样,这样的好处在于当数据库从副本集迁到集群时, 业务代码无需更改,对于开发者非常友好。


(四)MongoDB 事务包含部分 DDL


MongoDB 从 4.4 版本开始事务包含部分 DDL,事务操作包含创建表和索引,这里有 两种场景需要在事务中使用 DDL,第一种场景举例如下:

MongoDB 事务包含部分 DDL.png

如上图所示,用户业务一开始使用北京的 Region,并在其中部署了一个 MongoDB 库,当在杭州扩展业务时,杭州 Region 中也需要部署一个杭州的 MongoDB 库。


此时需要做一些库初始化操作,例如创造一些表或者索引这些信息,需要同步移植到杭 州库中。因为需要保证业务库的初始化操作符合原子性,因此需要使用事务的 DDL。


第二种场景是事务中一些 Insert 的操作,比如我们在开发测试环境当中,我们的数据 对性能要求不高,可以直接将业务代码写入一个初始的空库中,此时 Insert 操作会包含自 动创表等操作,能够也可以保证我们业务代码能够不报错,但是它可能没有索引相关信息。


创表案例如下:

创表案例如.png

三、事务的原理

(一)MongoDB 事务的特征

(一)MongoDB 事务的特征.png

All or Nothing


具有原子性。


Snapshot 隔离


事务开始时会产生 Snapshot,后续的读写操作不会影响 Snapshot。


Read Your Own Write


事务在新写数据时,第一条操作为写操作,第二个操作为读操作,可以直接读取事务内 尚未提交的写操作数据,事务以外的写操作需要等提交后才可读取。


(二)MongoDB 事务冲突

MongoDB 事务冲突.png

写操作冲突内部原理图


如上图所示,第一个 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 存储引擎的事务能力.png

上图为一个 MongoDB,由 Server 层与 WiredTiger 层组成。当进行读操作的时候, 会产生一个 Snapshot,例如:

Snapshot.png

如上图所示,读操作只能读到之前提交的写操作,比如 t1 与 t2,之后新写的 t3 操作无 法读取。


这个动作的实现主要是因为使用 WiredTiger 一个多版本并发控制 MVCC 的技术,支 持事务的冲突检测功能。


数据和索引在 WiredTiger 的 B 树中存储。


下图为 WiredTiger 存储事务的实现图:

WiredTiger.png

WiredTiger 对事务的支持

  • Update List:多版本数据,实现读写互斥
  • Update Check
    • 遍历 Update List,判断是否存在冲突
    • 冲突:Prepare 的修改或并发的修改
    • Modify:原子操作插入到 Update List 最前面

  • Read Check
    • 遍历 Update List,找到最新可见的版本
    • 冲突:Prepare 的修改
    • 对 Prepare 修改造成的冲突会自动重试

(四)MongoDB 副本集的跨行事务

MongoDB 副本集的跨行事务.png


  • 事务参数 WriteConcern: Majority 保证写的数据不会回滚;
  • 事务参数 ReadConcer: Snapshot 隐含 Majority,保证读到数据不会回滚;
  • MongoDB 通过这种方式,将传统事务隔离性从单机扩展到分布式场景。


非事务的读请求


实现原理如下:

实现原理如下.png

非事务读请求存在以下特点:


  • 非事务读请求没有 Snapshot 隔离;
  • MongoDB 在一个读请求期间会多次 Yield,释放 WiredTiger Snapshot;
  • MongoDB 在多个读请求同样会使用多个 WiredTiger Snapshot。

非事务读请求存在以下特.png

上图为一个非事务读请求,在 t2 与 t3 直接触发一次 Find,可以读到 t1、t2。之后触 发了一次 GetMore,同样的遍历读到了 t3。


读事务的 Snapshot 隔离

读事务的 Snapshot 隔离.png


  • 读事务在一个读请求只会使用一个 WiredTiger Snapshot;
  • 读事务在多个读请求利用 Logical Session 保留 Context,同样只会使用一个 Wired Tiger Snapshot。


读事务对存储引擎 Cache 压力

读事务对存储引擎 Cache 压力.png


  • 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,混合逻辑时钟可以解决分布式场景下,物理时钟不一致无法定序的问题。

分布式 snapshot 隔离级别.png


如上图所示,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 集群的跨行事务通过两阶段提交实现,原理图如下:

原理图如下.png


  • 两阶段提交:
    • 参与者 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

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
阿里云数据库
使用钉钉扫一扫加入圈子
+ 订阅

帮用户承担一切数据库风险,给您何止是安心!

官方博客
链接