0

技术干货| MongoDB 事务原理

 1 year ago
source link: https://mongoing.com/archives/82187
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

技术干货| MongoDB 事务原理

MongoDB 作为领先的 NoSQL ,为了支撑更多的需求场景,也在不断完善其功能。从早期支持大吞吐量读/写操作的 MMAPv1 存储引擎,到引入支持高并发操作的 WiredTiger 存储引擎,以及对事务功能的持续演进,MongoDB 不仅保留了最初的架构优势,同时又汲取了其他数据库的优点。

MongoDB 从 3.0版本引入 WiredTiger 存储引擎之后开始支持事务,MongoDB 3.6之前的版本只能支持单文档的事务,从 MongoDB 4.0版本开始支持复制集部署模式下的事务,从 MongoDB 4.2版本开始支持分片集群中的事务。

本文就主要对 MongoDB 事务的基本原理、事务的 snapshot 隔离、实现事务间并发操作的 MVCC 并发控制机制,以及事务日志做一些介绍!

事务的基本原理

与关系型数据库一样,MongoDB 事务同样具有 ACID 特性,说明如下:

  • 原子性( Automicity ):一个事务要么完全执行成功,要么不做任何改变。
  • 一致性( Consistency ):当多个事务并行执行时,元素的属性在每个事务中保持一致。
  • 隔离性( Isolation ):当多个事务同时执行时,互不影响。WiredTiger 本身支持多种不同类型的隔离级别,如读-未提交( read-uncommitted )、读-已提交( read-committed )和快照( snapshot )隔离。MongoDB 默认选择的是快照隔离。
  • 持久性( Durability ):一旦提交事务,数据的更改就不会丢失。

在不同隔离级别下,一个事务的生命周期内,可能出现脏读、不可重复读、幻读等现象。

下面介绍这3种现象出现的场景与含义。

1. 脏读现象

例如,某款手机在数据库中的库存还有1部,客户 A 发起一个查询手机库存的事务,同时,客户 B 发起了一个购买手机的事务(但未提交事务),此时客户 A 读到手机库存为0部,认为售完了。但客户 B 突然不想购买这款手机了,于是回滚了此事务,手机库存又变为1部,客户 A 读到的手机库存为0部就是一个脏读数据,如下图所示。

c4ca4238a0b9238-1.png

2. 不可重复读现象

例如,某款手机在数据库中的库存还有1部,客户 A 发起一个查询手机库存的事务(事务还未完成),读到其值为1。同时,客户 B 发起了一个购买手机的事务(提交了事务),此时客户 A 再次查询手机库存,读到其值为0。客户 A 在同一个事务中读到的同一条记录的取值不一样,这种现象就是不可重复读,如下图所示。

c81e728d9d4c2f6-1.png

3. 幻读现象

例如,某款手机在数据库中的库存还有1部,客户 A 发起一个购买手机的事务(事务还未完成),读到其值为1。同时,管理员 B 发起了一个增加1部手机的事务(提交了事务),此时客户 A 再次查询手机库存,读到其值为1(有新增数据)。客户 A 在同一个事务中本来应该读到的库存值为0,认为手机已经售完,但发现库存中还有1部手机,客户 A 两次读到的数据集不一样,这种现象就是幻读,如下图所示。

eccbc87e4b5ce2f-1.png

下面介绍与事务相关的数据结构,如下图所示。

a87ff679a2f3e71-1.png

(1)id 字段:这是事务的全局唯一标识,通过分析它与具体的操作关联,就能够知道一个事务包含哪些操作。

(2)snapshot_data 字段:MongoDB 使用的是快照隔离级别的事务,这个字段用于保存事务的快照信息,具体来说它会有 snap_min和snap_max 两个属性,通过这两个属性能够计算一个事务开始时的数据范围,每个事务开始时都会构造一个这样的快照。

(3)commit_timestamp 字段:表示事务提交的时间。

(4)durable_timestamp 字段:表示事务修改的数据已持久化的时间,与具体操作中的 durable_ts 字段关联。

(5)prepare_timestamp 字段:表示事务开始准备的时间。

(6)WT_TXN_OP 字段:包含事务的修改操作,用于事务回滚和生成事务日志( Journal )。

(7)logrec 字段:表示事务日志的缓存,用于在内存中保存事务日志(对于 MongoDB 来说 Journal 日志就是事务日志)。

事务的 snapshot 隔离

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

事务开始时,系统会创建一个快照,从已提交的事务中获取行版本数据,如果行版本数据标识的事务尚未提交,则从更早的事务中获取已提交的行版本数据作为其事务开始时的值。

通过事务可以看到其他还未提交的事务修改的行版本数据,但不会看到事务 id 大于 snap_max 的事务修改的数据。

快照数据的获取流程如下图所示。

假设图中的5个事务对同一条记录进行操作,E 事务开始时,生成的快照数据包含 B、D 两个未完成的事务,同时获取离它最近且完成了的 C 事务修改后的值作为事务开始时的取值,即2。

如果 E 事务为写事务,对库存值进行修改,则会进行冲突检测,以防止对过期数据的修改,保证数据的一致性(如 D 事务在 E 事务提交之前完成,行版本已发生变化,若 E 事务还要进行修改,则提交时会产生冲突)。

通过一段代码加深对快照隔离级别事务的认识:

session1 = client.start_session()  //开启一个session
session1.start_transaction()   //在session内部,开启一个事务
inventory.insert_one({'_id': 4, 'model':'switch', 'count': 200}, session= session)
doc1 = inventory.find_one({'_id': 4}, session=session1)
pprint.pprint(doc1)
doc2 = inventory.find_one({'_id': 4})
pprint.pprint(doc2)
session1.commit_transaction()  //提交事务
doc3 = inventory.find_one({'_id': 4})
pprint.pprint(doc3)
session1.end_session()    //结束session

任何事务都是封装在一个 session 中进行的。

MVCC 并发控制机制

要实现事务之间的并发操作,可以使用锁机制或 MVCC 控制等。对于 WiredTiger 来说,使用 MVCC 控制来实现并发操作,相较于其他锁机制的并发,MVCC 实现的是一种乐观并发机制。

MVCC 并发控制机制如下图所示:

e4da3b7fbbce234-1.png

(1)A 事务首先从表中读取要修改的行数据,读取的库存值为100,行记录的版本号为1。

(2)B 事务也从中读取要修改的相同行数据,读取的库存值为100,行记录的版本号为1。

(3)A 事务修改库存值后提交,同时行记录版本号加1,变为2,大于 A 事物一开始读取行记录版本号1,A 事务可以提交。

(4)但 B 事务提交时发现此时行记录版本号已经变为2,产生冲突,B 事务提交失败。

(5)B 事务尝试重新提交,此时再次读取的版本号为2,加1后版本号变为3,不会产生冲突,正常提交 B 事务。

通过代码分析事务的并发与冲突。

session1 = client.start_session() //开启一个session1
session1.start_transaction()  //在session1中开启一个事务1
inventory.delete_one({'_id':4}, session=session1)
doc1 = inventory.find_one({'_id': 4},session=session1)
pprint.pprint(doc1)     //输出none,说明在事务中已经删除session2 = client.start_session() //开启一个session2
session2.start_transaction()  //在session2中开启一个事务2
inventory.delete_one({'_id':4}, session=session2) //执行产生事务冲突session1.abort_transaction()      //终止事务1
session1.end_session()        //结束session1
session2.abort_transaction()      //终止事务2
session2.end_session()        //结束session2doc2 = inventory.find_one({'_id': 4}) //隐式开启第3个session和事务
pprint.pprint(doc2)    //在事务外可以找到,说明事务1被终止后回滚了

事务日志( Journal )

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

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

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

(1)按时间周期落盘。

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

(2)提交写操作时强制同步落盘。

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

(3)事务日志文件的大小达到100MB。

关于作者:郭远威

MongoDB 中文社区长沙分会主席;资深大数据架构师,著有《大数据存储 MongoDB 实战指南》《 MongoDB 核心原理与实践》;通信行业业务架构与数据迁移专家,先后在华为,中兴工作十余年;曾负责实施了海外多个运营商的大数据迁移及 BI 等大数据系统的设计开发。

以上内容节选自《 MongoDB 核心原理与实践》一书,详情可点击链接查看:

https://mp.weixin.qq.com/s/lWFvBkZ74smSjR7k7IN7wg

社区招募为了让社区组委会成员和志愿者朋友们灵活参与,同时我们为想要深度参与社区建设的伙伴们开设了“招募通道”,如果您想要在社区里面结交志同道合的技术伙伴,想要通过在社区沉淀有价值的干货内容,想要一个展示自己的舞台,提升自身的技术影响力,即刻加入社区贡献队伍~ 点击链接提交申请:http://mongoingmongoing.mikecrm.com/CPDCj1B


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK