开发者学堂课程【MongoDB 快速入门:事务功能使用及原理介绍】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/49/detail/1005
事务功能使用及原理介绍(二)
三、事务的原理
1.MongoDB事务的特征
一是具有原子性的 All or Nothing Execution;二是它的“读”,会产生Snapshot隔离,在上面写是不会影响后面数据;三是Read Your Own Write,是指事务在写数据时,第一条操作是写操作,后面的读操作是可以直接读到自己写的数据,这个事务的写操作还没有提交也是可读的,但这个事务之外的事务是无法读取到这个数据,只能等到事务提交后才可以读取。
2.MongoDB事务冲突
使用过程中难免遇到写冲突的事务场景。之前讲过写冲突是一个transaction error,只要不断重试就是可以成功的。
写冲突的内部原理(下图左侧):首先是黄色框里对事务进行一个写操作,会占用一个文件的锁;红色框是同时进行一个写操作对文档进行一个修改,并且不能获得文档1的锁,所以就会把事务abort进行回滚。若此时有个非事务性的写操作,同时对文档1 进行写操作,一样无法解锁,这个事务不会直接回滚,但操作会阻塞到到TXN事务提交,除非阻塞时间超过MaxTimeMs,这时就还会报错。
TXN Write 无法获取write lock,事务abort全部回滚
Operation Non-Transaction Write 无法获取write lock ,操作会阻塞到TXN事务提交,除非阻塞时间超过MaxTimeMs
3.MongoDB存储引擎的事务能力
MongoDB能支持事务主要是因为它使用了一个支持单机事务的存储引擎WiredTiger,它在4.0以后决定使用Timetamp来决定事务的顺序性,使得事务的副本集操作成为可能。下图中左侧内容表示它主要分为Server层和WiredTiger层,在读的时候会产生一个snapshot。图中左上角可以看到,之前提交的1,2写操作和新写的3操作,它是读不到3操作的,这个实现是依赖于WiredTiger多版本并发控制MVCC技术,同时也提供了事务的冲突检测功能。数据和索引在wiredTiger里面的B数中存储的。
WiredTiger对数据存储实现的一个叙写,它是由中间页和两个子页组成,中间页是表达从小到大的一个列表,每个keys下面都对应的是一个子页,每个下面都是包含kv的一个匹配对和列表,每一次会对一个kv进行更新,它的value就会在下方产生一个updatelicheck结构,这个结构里面会记录它的一个信息以及提交时间和一些数据,当有多个操作对同一个value进行更新的时候就会组成一个update list。在里面实现多版本数据,通过updatelist实现互斥,比如在更新的时候会让遍历updatelist来测验是否有冲突。冲突的规则就是判断前一个操作是prepare状态还是并发修改的状态。前面的更新还没有提交的话,它当前的更新就触发了一个写冲突,当前的操作就要进行回滚,prepare状态是在分布式事务中的应用场景,分布式状态中它会先把一个写操作以prepare的形式提交,就会产生prepare状态等同于prepare状态的操作是没有提交的,就会触发写冲突。当没有出发写冲突就吹进行一个css操作,把新更新的数据组合成update check结构,原子操作插入到update list最前面。所以update就是从new list to old list的顺序。读操作也是根据updatelist的操作遍历时间点,当读的过程中发现了prepare的修改也会产生冲突,此时mongodb对Prapare修改造成的读冲突会自动重试。
这就是WiredTiger对事务的一个支持,通过对读检测和写检测保证是符合snapshot隔离的
4.MongoDB副本集的跨行事务
非事务的读请求,就是读操作在getfind和getmind操作的实现原理就是下图左侧的伪代码形式。
首先是判断读操作返回的back数据是否满,未满就会按照过滤条件一行一行取出数据并放到batch中,但是每放入一个就会有一个tempyield中断,就会先检查中断里面 有没有一个care操作,有的直接退出结束遍历,或是保留一个concern状态并释放锁和当前的snapshot,同时会获取一个新的snapshot,之后回到concern状态。这个yield就像等于释放了一个事务开始时的snapshot产生了一个新的snapshot,就会导致非事务的读请求没有snapshot的隔离,在一次读请求中间出现多次yield,释放snapshot并读取另一个snapshot所以一次对请求中可能会出现多次读取数据的情况,也会在多个读请求中出现这个问题(参考下图的右上角)非事务读请求会多触发一次find,它是时刻间的一个操作是可以读到12之间的,之后在触发GetMore操作,同样的遍历就会读到事务3的操作。
非事务读请求没有snapshot隔离
MongoDB在一个读请求期间会多次yield,释放wired Tiger snapshot
MongoDB在多个读请求同样会使用多个wired Tiger snapshot
读事务是可以保证事务隔离的,一个find和一个getMore是不会触发到12的事务场景,只会使用一个snapshot,不会触发yeild场景,不会释放一个snapshot并创建一个新的snapshot
读事务在一个读请求只会使用一个wiredTiger snapshot
读事务在多个读请求利用logicalsession保留context,同时只会使用一个wired Tiger snapshot,也就是下图,即使是在一个事务里面,它的读操作、find还有GetMore访问的都是t2和t3之间的时间点数据,所以就只能读到1、2的数据,不会出现前面读到3而没有3 的数据的情况。
该事务对存储引擎Cache压力,造成原因是在一个事务开始读之后会产生一个snapshot,这个时间点后触发了更新。比如事件从time=0开始进行,time=0之后又进行了写操作,time=1触发了写操作,然后做一个修改;time=2做一个触发进行另外的修改,就会产生一个很长的update list 的链表,这个数据都是放在内存里的,当更新量越来越大的时候,链表所占内存就会越来越多,此时就会对Cache造成很大压力,所以链表就只能到这个事务的开始之前链表的结束之后(比如被commit报回以后)这个链表才会被自动回收。
Cache压力来自于事务snapshot之后的写请求
事务的整个生命周期会使使用新的snapshot
Update Structure在snapshot被evict后才能清理
5.如何避免存储引擎Cache压力?
TransactionLifetimeLimitSeconds设为默认60s,不要轻易修改,使用默认60s就是说事务没有结束后MongoDB 会自动的close掉事务,避免事务的写操作数据量过多。
提交Read-Only事务,不能因为事务没有写操作就不提交,即使事务会被系统在MongoDB之后的60秒后进行释放,但这个60s时间过长,还是会对内存造成过大的压力。
中止不需要的事务,避免open一个事务的时间过长
事务的修改的文档<1000Documents且<16MBoplog,在4.0版本时还会有16.0MB的限制
6.MongoDB集群的跨行事务
集群的事务参数和副本集的事务参数是一致的,事务参数:readConcern:snapshot,writeConcern:mjority,readPreference:primary
为了使多个shard保持数据的一致性,需要产生一个分布式的snapshot,相当于在多个shard之间产生一个数据一致的snapshot分布式snapshot隔离级别:可重复读取多个shard数据一致的副本集snapshot
多个shard数据一直通过缓和逻辑时钟来实现(如下图左侧),pt有物理时钟,c是逻辑时钟,中间的是逻辑的一个操作数。当两个shard有联系时就会产生一个因果关系,先看10s时shard0,shard1同步了一条信息,shard0的物理时钟是10 ,但shard1、2、3是0,当shard0往shard1同步一条数据之后,shard1的物理时钟变为1,但是它的收到逻辑时钟收到消息的时钟就是1的最大值,而content会进行加1就变成11;当消息进行shard之间多次传递后到所有shard会产生一个一致的逻辑时钟;当使用逻辑时钟0,1等于0,c=10时间点做一个snapshot,就会发现数据就是一致的,就是黑线上的时间点,它的时间点可以保证分布式时间点数据的一致。所有shard节点触发一个snapshot,混合逻辑时钟可以解决分布式场景下,物理时钟不一致无法定序的问题。
采用两阶段提交
参与者Prepare:生成PrepareTs
参与者Commit:使用协调者收集的 max(PrepareTS)作为CommitTS
协调者:收集决策、记录Commit Log
参与者:执行事务、记录Prepare Log
故障回复:config节点保存分布式事务的状态
协调者状态记录于config.transaction_coordinators
参与者状态记录与config.transaction表
采用两阶段的方式实现,比如用户访问Mongos,访问的第一个shard作为一个协调者,这个协调者会与其他的shard进行一个通信,其他shard作为一个参与者,一开始参与者通信就是作为prepare提交,生成PrepareTs,协调者收到preparets的返回就会收集起来选择一个最大值作为CommitTS,再把CommitTs作为提交时间传给其他参与者进行二次提交,这两次提交就保证了分布式事务的实现。协调者主要就是发送提交已经手机决策的操作,会记录提交的一个日志,参与者主要是记录一个Prepare的日志。两个节点之间可能会有故障有节点崩溃的场景,在cf场景会保存分布式事务的状态,协调者状态记录于config.transaction_coordinators,参与者状态记录与config.transaction表,这个好处在于当协调者参与者发生崩溃时,会有备库被选为主库时,会重新选择一个来获取事务的状态,来继续集群事务两阶段的提交过程,这就是整个集群事务的跨行实现。
7.MongDB事务使用的注意事项
由于支持了跨行输入,各种数据模型都能适用,比如传统的关系模型是可以适用的。
事务不应该是最常用的操作,它的性能是受影响的,应该更多的使用文档模型或是单行输入做操作,少量用跨行输入,这才是MongoDB比较推荐的使用规则。
事务的操作中,都应该要包含sesion,在操作的过程中指定事务的操作,如果不指定操作就会被认为是一个外部的非事务操作
事务会报错,需要增加重试逻辑。无论是因为写冲突还是网络问题,增加重试逻辑来保证应用不会受到影响。
不必要的事务snapshot要尽快关闭,避免造成对cache的压力
如果像产生写冲突,确保事务做了写操作。测试过程中想产生写冲突,一定要确保事务进行了真正的修改,是两个事务都进行修改,而不是把a事务改为a事务,这个并不是真正的写操作,在MongoDB里,会是一个not的操作,另外的事务是可以进行正常的更新的,就不会产生写冲突,所以要确保有真正的写操作。
注意ddl操作,已有的事务操作会阻塞ddl,ddl会阻塞之后的事务。已有事务会加上一个意向锁阻止ddl操作,ddl操作一旦下发就回去阻止之后的操作就会对应用产生写影响。
四、总结
1.首先介绍了MongoDB为什么需要事务。在大量的多对多关系或是一个事件驱动或是一个操作日志中,这种场景往往需要一个跨表的操作,需要操作来保持原子性,这就要用MongoDB跨行事务
2.MongoDB在副本集和集群上跨行事务的使用方法,这里面要注意增加的有重试操作,可以设置一个重试的次数限制和时间限制
3.MongoDB存储引擎的事务能力
主要讲述了update list的读和写以及更新的时候要进行一个检查来查验冲突是否存在
4.副本集的跨行事务的原理,以及对cache造成的压力和避免方法,需要尽可能快的把事务进行提交或终止
5.集群的跨行事务的原理,提交是进行一致性的提交,每一个commitTS一定是每一个shard prepareTS的最大值
6.事务使用的注意事项,按照注意事项避免过度使用,过度使用会对数据库造成较大的性能压力。