MongoDB 事务

前言

在 MongoDB 中,对单个文档的操作都是原子的。因为可以在单个文档结构中使用内嵌文档和数据获得数据之间的关系,所以不必跨多个文档和集合进行范式化,这种

结构特性,避免了很多场景中的对多文档事务的需求。

对于需要多个文档进行原子读写的场景,MongoDB 中引入了多文档事务和分布式事务。

  • 在4.0版本中,MongoDB支持副本集上的多文档事务;

  • 在4.2版本中,MongoDB 引入了分布式事务,增加了对分片集群上多文档事务的支持,并合并了对副本集上多文档事务的现有支持,事务可以跨多个操作、集合、数据库、文档和分片使用,这种方式事务的实现主要是借助于两阶段提交协议(2PC)实现的。

如何使用

MongoDB 中从 4.0 开始支持了事务,这里来看看 MongoDB 中的事务是如何使用的呢?

首先登陆 MongoDB 命令行

mongo -u <name> --port 27017 --host 127.0.0.1 admin  -p <pass>

1、打开 session

session = db.getMongo().startSession( { readPreference: { mode: "primary" } } );

2、将需要操作的 collection 进行变量绑定

testCollection = session.getDatabase("gleeman").test_explain;
test1Collection = session.getDatabase("gleeman").test_explain_1;

3、开始事务标注,指定MVCC的模式,写模式

session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } );

4、拼接执行语句,将需要执行的语句进行事务封装

try (ClientSession clientSession = client.startSession()) {
clientSession.startTransaction();
collection.insertOne(clientSession, docOne);
collection.insertOne(clientSession, docTwo);
clientSession.commitTransaction();
}

5、提交事务

session.commitTransaction();

6、关闭session

session.endSession();

事务的原理

MongoDB 中的 WiredTiger 存储引擎是目前使用最广泛的,这里主要介绍下 WiredTiger 中事务的实现原理。

WiredTiger 存储引擎支持 read-uncommitted 、read-committedsnapshot 3 种事务隔离级别,MongoDB 启动时默认选择 snapshot 隔离。

事务和复复制集以及存储引擎之间的关系

1、事务和复制集

复制集配置下,MongoDB 整个事务在提交时,会记录一条 oplog,包含了事务所有的操作,备节点拉取 oplog,并在本地重放事务操作。事务 oplog 包含了事务操作的 lsid,txnNumber,以及事务内所有的操作日志( applyOps 字段)。

WiredTiger 是如何实现事务和 ACID 呢。WiredTiger 事务主要使用了三个技术 snapshot(事务快照)、MVCC (多版本并发控制)和 redo log(重做日志)。同时为了实现这三个技术,还定义了一个基于这三个技术的事务对象和全局事务管理器。

wt_transaction{
transaction_id: // 本次事务的全局唯一的ID,用于标示事务修改数据的版本号
snapshot_object: // 当前事务开始或者操作时刻其他正在执行且并未提交的事务集合,用于事务隔离
operation_array: // 本次事务中已执行的操作列表,用于事务回滚。
redo_log_buf: // 操作日志缓冲区。用于事务提交后的持久化
State: // 事务当前状态
}

WiredTiger 中的 MVCC 是基于 key/value 中 value 值的链表,每个链表单元中存储有当先版本操作的事务 ID 和操作修改后的值。

wt_mvcc{
transaction_id: // 本次修改事务的ID
value: // 本次修改后的值
}

WiredTiger 中数据修改都是在这个链表中进行 append 操作,每次对值的修改都是 append 到链表头,每次读取值的时候读是从链表头根据对应的修改事务 transaction_id 和本次事务的 snapshot 来判断是否可读,如果不可读,向链表尾方向移动,直到找到都事务可以读到的数据版本。

什么是 snapshot 呢?

事务开始或者结束操作之前都会对整个 WiredTiger 引擎内部正在执行的或者将要执行的事务进行一次快照,保存当时整个引擎的事务状态,确定那些事务是对自己可见的,哪些事务是自己不可见的。

WiredTiger 中的事务隔离级别

传统的事务级别都分成下面四种:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。

  • 读未提交:一个事务还没提交时,它的变更就能被别的事务看到,读取未提交的数据也叫做脏读;

  • 读提交:一个事务提交之后,它的变更才能被其他的事务看到;

  • 可重复读:一个事务执行的过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,在此隔离级别下,未提交的变更对其它事务也是不可见的,此隔离级别基本上避免了幻读;

  • 串行化:这是事务的最高级别,顾名思义就是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

我们熟知的 MySQL 对于事务隔离级别的实现,可重复读读提交 主要是通过 MVCC 来实现,MVCC 的实现主要用到了 undo log 日志版本链和 Read View。串行化 和 读未提交,主要实现方式是通过加锁来实现的。

其中 Read View 可以在理解为一个数据的快照,可重复读隔离级别会在每次启动的事务的时候生成一个 Read View 记录下当前事务启动瞬间,当前所有活跃的事务 ID。具体细节可参见 MySQL中的事务的隔离级别

WiredTiger 存储引擎支持 read-uncommitted、read-committedsnapshot 3种事务隔离级别,MongoDB 启动时默认选择 snapshot 隔离。

  • Read-Uncommited:读未提交,一个事务还没提交时,它的变更就能被别的事务看到,读取未提交的数据也叫做脏读,WiredTiger 引擎在实现这个隔方式时,就是将事务对象中的 snap_object.snap_array 置为空即可,那么在读取 MVCC list 中的版本值时,总是读取到 MVCC list 链表头上的第一个版本数据,这样就总是能读取到最新的数据了;

  • read-committed:一个事务提交之后,它的变更才能被其他的事务看到;这种对于一个长事务可能存在多次读取,读取到的值不一样,因为每次读取都是读取的最新提交的数据,WiredTiger 引擎在实现该事务隔离级别,就是在事务在每次执行之前,都对系统机型一次快照,然后在这个事务快照中读取最新提交的数据;

  • snapshot:快照隔离方式,一个事务开始时,就进行一次快照,并且只会进行一次快照,这样事务看到的值提交版本,这个值在整个事务过程中看到的都是一样;

WiredTiger 中对于事务的实现也是基于 MVCC 实现的,MVCC 可以提供基于某个时间点的快照,有了这个快照,就能确定当前事务能看到的数据了,通过这个来实现对应的事务隔离级别,这点也个人感觉和 mysql 中的 Read View 类似,不展开分析了。

WiredTiger 没有使用传统的事务独占锁和共享访问锁来保证事务隔离,而是通过对系统中写事务的 snapshot 截屏来实现。这样做的目的是在保证事务隔离的情况下又能提高系统事务并发的能力。

WiredTiger 事务过程

一般事务有三个阶段:开启事务,执行事务,提交事务。如果事务执行失败,会进行事务的回滚操作,事务正常执行,最近进行事务的提交 (commit) 即可。

事务开启

事务开启的过程中,首先会为事务创建一个事务对象并把这个对象加入到全局的事务管理器当中,然后根据配置确定事务的隔离级别和 redo_log 的刷盘方式,并将事务状态设置成执行状态,最后判断事务的隔离级别,如果是 snapshot 级的事务隔离,在本次事务执行之前会创建一个系统并发事务的 snapshot 截屏,,保存当时整个引擎的事务状态,确定那些事务是对自己可见的,哪些事务是自己不可见的。

事务执行

事务在执行阶段,如果是读操作,不做任何处理,因为读操作不需要回滚和提交。如果是写操作,WiredTiger 会对每个操作做详细的记录。

这里就会用使用到上面介绍的事务对象(wt_transaction)中的 operation_array 和 redo_log_buf。

operation_array:主要记录本次事务中已经提交的操作列表,数组单元中,会包含一个指向 MVCC list 对应修改版本值的指针,用于事务的回滚。

redo_log_buf: 操作日志缓冲区。用于事务提交后的持久化。

来描述下具体的更新操作过程:

1、创建一个 mvcclist 的值对象 update;

2、根据事务对象的 transaction_id 和事务状态判断是为本次事务创建写的事务id,如果没有,为本次事务分配一个事务id,并将事务的状态设置成 HAS_TXN_ID 状态;

3、将本次事务的 ID 设置到 update 单元中作为 mvcc 版本号;

4、同时会创建一个 operation 对象,这个对象的指针会指向 update,这个对象会加入到 operation_array 中,用来进行操作事务的回滚;

5、update 会被加入到 mvcclist 的头部;

6、最后会写一条 redo_log 到本次事务的 redo_log_buffer 当中。

事务提交

事务提交

提交事务对象中的 redo_log_buf 中的数据到 redo_log_file(重做日志中),并将 redo_log_file 持久化到磁盘上,清除提交事务对象的 snapshot,再将事务对象的transaction_id 设置成 WT_TNX_NODE,保证其他事务在创建 snapshot 时本次事务的状态是已提交的状态。

事务回滚

WiredTiger 引擎对事务的回滚过程比较简单,首先遍历 operation_array ,对每个数组单元对应的 update 事务 id 设置一个 WT_TXN_ABORTED ,标识 mvcc 对应的事务单元被回滚,在其它事务进行 mvcc 读操作的时候,跳过这个放弃的值即可,整个过程是一个无锁的操作,高效,简洁。

事务日志(journal)

Journal 是一种 WAL(Write Ahead Log)事务日志,目的是实现事务提交层面的数据持久化。

Journal 是 MongoDB 存储引擎层面的概念,MongoDB 主要支持的 mmapv1、wiredtiger、mongorocks 等存储引擎,都⽀持配置 JournalMongoDB 可以基于 Journal 来恢复因为崩溃未及时写到磁盘的信息。

Journal 持久化的对象不是修改的数据,而是修改的动作,以日志形式先保存到事务日志缓存中,再根据相应的配置按一定的周期,将缓存中的日志数据写入日志文件中。

事务日志落盘的规则如下。

  • 1、按时间周期落盘。

在默认情况下,以50毫秒为周期,将内存中的事务日志同步到磁盘中的日志文件。

  • 2、提交写操作时强制同步落盘。

当设置写操作的写关注为j:true时,强制将此写操作的事务日志同步到磁盘中的日志文件。

  • 3、事务日志文件的大小达到100MB。

总结

1、在4.0版本中,MongoDB支持副本集上的多文档事务;

2、在4.2版本中,MongoDB 引入了分布式事务,增加了对分片集群上多文档事务的支持,并合并了对副本集上多文档事务的现有支持,事务可以跨多个操作、集合、数据库、文档和分片使用;

3、MongoDB 中的 WiredTiger 存储引擎是目前使用最广泛的,WiredTiger 存储引擎支持 read-uncommitted 、read-committed 和 snapshot 3 种事务隔离级别,MongoDB 启动时默认选择 snapshot 隔离;

  • Read-Uncommited:读未提交,一个事务还没提交时,它的变更就能被别的事务看到,读取未提交的数据也叫做脏读,WiredTiger 引擎在实现这个隔方式时,就是将事务对象中的 snap_object.snap_array 置为空即可,那么在读取 MVCC list 中的版本值时,总是读取到 MVCC list 链表头上的第一个版本数据,这样就总是能读取到最新的数据了;

  • read-committed:一个事务提交之后,它的变更才能被其他的事务看到;这种对于一个长事务可能存在多次读取,读取到的值不一样,因为每次读取都是读取的最新提交的数据,WiredTiger 引擎在实现该事务隔离级别,就是在事务在每次执行之前,都对系统机型一次快照,然后在这个事务快照中读取最新提交的数据;

  • snapshot:快照隔离方式,一个事务开始时,就进行一次快照,并且只会进行一次快照,这样事务看到的值提交版本,这个值在整个事务过程中看到的都是一样;

4、WiredTiger 中对于事务的实现也是基于 MVCC 实现的,MVCC 可以提供基于某个时间点的快照,有了这个快照,就能确定当前事务能看到的数据了,通过这个来实现对应的事务隔离级别,这点也个人感觉和 mysql 中的 Read View 类似,不展开分析了;

5、WiredTiger 没有使用传统的事务独占锁和共享访问锁来保证事务隔离,而是通过对系统中写事务的 snapshot 截屏来实现。这样做的目的是在保证事务隔离的情况下又能提高系统事务并发的能力。

参考

【MongoDB事务】https://docs.mongoing.com/transactions

【WiredTiger的事务实现详解 】https://blog.csdn.net/daaikuaichuan/article/details/97893552

【MongoDB中并发控制】https://blog.csdn.net/baijiwei/article/details/89436861

MongoDB 中的事务的更多相关文章

  1. 聊聊MongoDB中连接池、索引、事务

    大家好,我是哪吒. 三分钟你将学会: MongoDB连接池的使用方式与常用参数 查询五步走,能活九十九? MongoDB索引与MySQL索引有何异同? MongoDB事务与ACID 什么是聚合框架? ...

  2. MongoDB中的读写锁

    1. MongoDB 使用的锁 MongoDB 使用的是“readers-writer”锁, 可以支持并发但有很大的局限性当一个读锁存在,许多读操作可以使用这把锁,然而, 当一个写锁的存在,一个单一的 ...

  3. MongoDB中的一些坑( 2.4.10 版本)

    http://www.jb51.net/article/62654.htm 1.MongoDB 数据库级锁 MongoDB的锁机制和一般关系数据库如 MySQL(InnoDB), Oracle 有很大 ...

  4. MongoDB: 原子性和事务

    在MongoDB中, 文档级别的的写操作是原子性的, 甚至是在对某个文档的操作中修改其多个内嵌的子文档, 也是原子性的. 在一个写操作同时修改多个文档的情况, 对其中单独的某个文档而言是原子的, 但是 ...

  5. 在 MongoDB 上模拟事务操作来实现支付

    我们的产品叫「学海密探」,属于在线教育行业,产品需要有支付功能,然而支付最蛋疼是什么?有人会说是支付宝和微信等支付接口的接入开发!没错,但支付接口的开发算是比较简单的了,我觉得凡是跟钱有关系的操作最重 ...

  6. MongoDB中的一些坑(最好不要用)

    MongoDB 是目前炙手可热的 NoSQL 文档型数据库,它提供的一些特性很棒:如自动 failover 机制,自动 sharding,无模式 schemaless,大部分情况下性能也很棒.但是薄荷 ...

  7. MongoDB中WiredTiger的数据可用性设置

    此文已由作者温正湖授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. MongoDB中WiredTiger的参数配置主要通过 wiredtiger_open (http://so ...

  8. Java生鲜电商平台-SpringCloud微服务架构中分布式事务解决方案

    Java生鲜电商平台-SpringCloud微服务架构中分布式事务解决方案 说明:Java生鲜电商平台中由于采用了微服务架构进行业务的处理,买家,卖家,配送,销售,供应商等进行服务化,但是不可避免存在 ...

  9. MongoDB 4.0 事务实现解析

    MongoDB 4.0 引入的事务功能,支持多文档ACID特性,例如使用 mongo shell 进行事务操作 > s = db.getMongo().startSession() sessio ...

  10. MongoDB中如何优雅地删除大量数据

    删除大量数据,无论是在哪种数据库中,都是一个普遍性的需求.除了正常的业务需求,我们需要通过这种方式来为数据库"瘦身". 为什么要"瘦身"呢? 表的数据量到达一定 ...

随机推荐

  1. 日历插件zaneDate 不依赖任何第三方插件 简单高效

    先来找图看看时间选择器的效果:             没错就是这个吊样,如果你不需要这个色调,你可以fork我的github项目任意修改美美的色调. 当然也欢迎你给我提很多很多的bug让我改不停 . ...

  2. C# CEFSharp WCF开发桌面程序实现“同一网站多开”

    前言 孔乙己显出极高兴的样子,将两个指头的长指甲敲着柜台,点头说:"对呀,对呀!CEFSharp,你用过么?访问同一网址实现多开怎么实现?比如我有3个淘宝店,我想同时登录维护,就像传说中的指 ...

  3. openlayers学习笔记

    https://www.cnblogs.com/suRimn/p/10649816.html

  4. 通过Proxy和Reflect实现vue的响应式原理

    vue3通过Proxy+Reflect实现响应式,vue2通过defineProperty来实现 Proxy Proxy是什么 Proxy是ES6中增加的类,表示代理. 如果我们想要监听对象的操作过程 ...

  5. 用户空间协议栈设计和netmap综合指南

    本文分享自华为云社区<用户空间协议栈设计和netmap综合指南,将网络效率提升到新高度>,作者:Lion Long . 协议概念 1.1.七层网络模型和五层网络模型 应用层: 最接近用户的 ...

  6. IE浏览器不支持TextDecoder()的问题

    IE浏览器不支持TextDecoder()方法,因此在进行Arrbuffer转string或中文时,出现未定义的错误.通过网上查找方法,可以通过引用第三方库进行解决. github地址:https:/ ...

  7. 如何将项目打包上传到NuGet服务器?

    作者:西瓜程序猿 主页传送门:https://www.cnblogs.com/kimiliucn 前言 在我写[在.NET Framework中使用RocketMQ(阿里云版)]这篇博客的时候,因为封 ...

  8. 云服务器中Linux如何安装宝塔面板?

    作者:西瓜程序猿 主页传送门:https://www.cnblogs.com/kimiliucn 官方使用手册:https://www.kancloud.cn/chudong/bt2017/42420 ...

  9. LeetCode--1039

    Smiling & Weeping ----我总是躲在梦与季节的身处, 听花与黑夜唱尽梦魇, 唱尽繁华,唱断所有记忆的来路. 题目链接:1039. 多边形三角剖分的最低得分 - 力扣(Leet ...

  10. 【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅

    前言 缘由 博友的需求就是我最大的动力 博友一说话,本狗笑哈哈.博友要我写啥,我就写啥. 特来一篇关于SpringBoot接口返回结果及异常统一处理,虽说封不封装都能用,但咱后端也得给前端小姐姐留个好 ...