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. 记一次 .NET 某物流API系统 CPU爆高分析

    一:背景 1. 讲故事 前段时间有位朋友找到我,说他程序CPU直接被打满了,让我帮忙看下怎么回事,截图如下: 看了下是两个相同的程序,既然被打满了那就抓一个 dump 看看到底咋回事. 二:为什么会打 ...

  2. [PWN之路]堆攻击那些事儿

    原文:https://www.freebuf.com/articles/endpoint/371095.html 0x00 前言 根据某大佬所说,pwn之路分为栈,堆,和内核.当前,如果你看到这个文章 ...

  3. MAUI+Blazor混合应用开发示例

    前言 笔者之前在公司搭建过一套生产管理系统,该系统要求能和硬件进行串口通信,同时又要提供后台信息查询.笔者给出的解决方案就是:MAUI + Blazor,这样只需要提供一套UI,就能满足桌面端.移动端 ...

  4. MAUI+Masa Blazor APP 各大商店新手发布指南(三)vivo篇

    目录 前言 准备材料 审核流程 测试报告 隐私测试报告 隐私行为数据 其他问题 总结 前言 上架vivo商店,使用厂家的离线推送当然是一个重要原因,与小米不同,vivo的推送服务可以在应用未上架的情况 ...

  5. C# object类型与dynamic类型的使用

    获取动态变化的类型属性 例: var类型的参数a中包含属性Name或Age 获取这个不固定的数据 首先验证a中存在的是什么属性 /// <summary> /// 验证object类型是否 ...

  6. 对比 MyBatis 和 MyBatis-Plus 批量插入、批量更新的性能和区别

    1 环境准备 demo 地址:learn-mybatis · Sean/spring-cloud-alibaba - 码云(gitee.com) 1.1 搭建 MyBatis-Plus 环境 创建 m ...

  7. 解决SVN死锁问题

    svn执行clean up后出现提示:svn cleanup failed–previous operation has not finished; run cleanup if it was int ...

  8. LeetCode买卖股票之一:基本套路(122)

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 关于<LeetCode买卖股票>系列 在L ...

  9. 「atcoder - ABC215G」Colorful Candies 2

    link. 称题目中的 \(c_i\) 为 \(a_i\),令 \(c_i\) 为第 \(i\) 种颜色的出现次数,令 \(C\) 为颜色总数.固定 \(k\),令 \(t_i=1\),如果颜色 \( ...

  10. ConcurrentHashMap底层源码分析

    ConcurrentHashMap源码底层分析 1.ConcurrentHashMap初始化 jdk8之后,ConcurrentHashMap采用了HashMap的底层结构(数据,链表,红黑树),在此 ...