MongoDB的事务实现

9 篇文章 3 订阅
3 篇文章 0 订阅

在前面我们介绍了MongoDB单文档原子操作, 这是单文档下的事务, 它能够保证对于单文档的操作的原子性。 在MongoDB3.6里面, 支持了session, 这与之前的OperationContext很类似, 所不同的实, session可以多个CRUD操作使用共同的session。在MongoDB4.0里面, 终于有了针对副本集的多文档事务实现, 使得我们可以向使用其他的数据库事务一样进行事务操作。
WiredTiger对事务的支持, 是MongoDB支持事务的基础, 这里重点介绍一下WT下事务的实现过程。

transaction 相关的数据结构

__wt_txn_global

在WT_CONNECTION中, 有一个全局的事务相关的上下文结构体__wt_txn_global, 用来记录事务实现的全局信息。 在open connection的时候调用__wt_txn_global_init来构建和初始化, 在关闭connection的时候调用__wt_txn_global_destroy,来清理资源。 主要包括:

  • 全局唯一的自增事务ID;
  • 最旧的未提交事务ID等;
  • checkpoint 相关信息;
  • named snapshot队列信息;
  • 各个session对应state的数组;

为了支持事务, 在__wt_txn_global里面增加了如下的field:

  • commit_timestamp
  • last_ckpt_timestamp
  • meta_ckpt_timestamp
  • oldest_timestamp
    oldest_timestamp记录了一个时间点, 在这个时间点之前的版本不需要保存, 由于每个读操作都与一个snapshot关联, 只要有读操作与之相应, 该版本就需要保留, 为了不保留太多的版本, 给WT的内存造成压力, oldest_timestamp就是告诉引擎层, 该时间点之前的版本不需要保留。
  • pinned_timestamp
  • recovery_timestamp
  • stable_timestamp
    stable_timestamp记录了当前已经同步到大多数节点的操作的时间点,随着新的操作的进行, 该时间点逐渐变大, 当我们进行回滚操作, 不再像之前那样, 对要回顾的操作进行相反的逆操作, 而是直接回到stable_timestamp。
  • TAILQ_HEAD(__wt_txn_cts_qh, __wt_txn) commit_timestamph;
  • TAILQ_HEAD(__wt_txn_rts_qh, __wt_txn) read_timestamph;
类型名称描述
uint64_tcurrent全局唯一的递增transactionID
uint64_tlast_running最旧的运行中的transaction id
uint64_toldest_id系统中最小的transaction id
wt_timestamp_tcommit_timestamp记录事务可以读取的时间点, 再次之前是读不到的
wt_timestamp_tlast_ckpt_timestamp上一次checkpoint 的时间
wt_timestamp_tmeta_ckpt_timestampmetadata里面记录的checkpoint time
wt_timestamp_toldest_timestamp记录最旧的事务时间点
wt_timestamp_tpinned_timestamp当前最小的需要固定在内存的transactionID, 就是有read绑定到它上面
wt_timestamp_trecovery_timestamp记录recovery的时候, 恢复到的checkpoint的timestamp
wt_timestamp_tstable_timestamp一个固定时间点, 由客户端设定, 主要用于rollback_to_stable
boolhas_commit_timestamp是否有commit_timestamp
boolhas_oldest_timestamp是否有oldest_timestamp
boolhas_pinned_timestamp是否有pinned_timestamp设定
boolhas_stable_timestamp是否有set_timestamp设定stable
boololdest_is_pinnedpinned_timestamp == oldest_timestamp?
boolstable_is_pinnedpinned_timestamp == stable_timestamp?
WT_SPINLOCKid_lock用于更新current, 获取一个transactionID的时候用
WT_RWLOCKvisibility_rwlock可见性保护锁, 防止日志, checkpoint,事务完成之前可见
WT_RWLOCKcommit_timestamp_rwlockcommit_timestamp读写锁, 修改commit timestamp队列的时候用
TAILQ_HEAD(__wt_txn_cts_qh, __wt_txn)commit_timestamphcommit_timestamp队列
uint32_tcommit_timestampq_lencommit_timestamp的长度
WT_RWLOCKread_timestamp_rwlockread_timestamp读写锁
TAILQ_HEAD(__wt_txn_rts_qh, __wt_txn)read_timestamphread_timestamp队列
uint32_tread_timestampq_lenread_timestamp队列的长度
boolcheckpoint_runningcheckpoint是否正在运行中
uint32_tcheckpoint_idcheckpoint id
WT_TXN_STATEcheckpoint_statecheckpoint的状态
uint64_tmetadata_pinned绑定的metadata中最小的transactionID
WT_RWLOCKnsnap_rwlock命名snapshot的读写锁
uint64_tnsnap_oldest_id最旧的命名snapshot ID
TAILQ_HEAD(__wt_nsnap_qh, __wt_named_snapshot)nsnaph命名snapshot队列
WT_TXN_STATE*states事务列表的状态
__wt_txn

每一个session, 都有一个相关的上下文状态的结构__wt_txn, 它用来产生事务结构体, 并且记录了session的当前正在执行的transaction的相关信息。主要包括:

  • 事务的ID;
  • snapshot 数组;
  • WT_TXN_OP 修改数组;
  • log record日志记录;
  • checkpoint的状态信息;
  • transaction的类型状态信息;
  • commit_timestamp
  • durable_timestamp
  • first_commit_timestamp
  • prepare_timestamp
  • read_timestamp
  • TAILQ_HEAD(__wt_txn_cts_qh, __wt_txn) commit_timestamph;
  • TAILQ_HEAD(__wt_txn_rts_qh, __wt_txn) read_timestamph;
类型名称描述
uint64_tidtransaction ID
WT_TXN_ISOLATIONisolation事务的隔离级别: read uncommitted, read committed, snapshot
uint32_tforced_iso强制使用的隔离级别
uint64_tsnap_min, snap_max该事务对应的snapshot的最大最小id
uint64_t *snapshot该事务对应的snapshot数组
uint32_tsnapshot_count该事务的对应的snapshot数组的大小
uint32_ttxn_logsync事务日志的sync设定
wt_timestamp_tcommit_timestamp该transaction的commit timestamp
wt_timestamp_tfirst_commit_timestampcommit_timstamp在一个transaction可以被设定多次, 这里记录第一个
wt_timestamp_tprepare_timestamp该事务的prepare timestamp
TAILQ_ENTRY(__wt_txn)commit_timestampqcommit timestamp队列
TAILQ_ENTRY(__wt_txn)read_timestampqreadtimestamp队列
boolclear_commit_qcommit timestamp队列是否需要被清理
boolclear_read_qread timestamp队列是否需要被清理
WT_TXN_OP*modtransaction里面的修改操作数组
size_tmod_alloctransaction里面的修改操作数组大小
u_intmod_counttransaction里面的修改操作数组已分配的个数
WT_ITEM*logrectransaction对应的log内存buffer, 记录了transaction的操作
WT_TXN_NOTIFY *notifytransaction的notify函数
WT_LSNckpt_lsncheckpoint的logical sequence number
uint32_tckpt_nsnapshotcheckpoint对应的snapshot count
WT_ITEM*ckpt_snapshot记录checkpoint的snapshot数组的ID
boolfull_ckpt是不是fullcheckpoint
const char *rollback_reasontransaction rollback的原因
uint32_tflagstransaction的设定
__wt_txn_state

Per-session的transaction状态信息。

事务的ACID

  • 原子性(Atomicity):事务内的所有操作要么全部完成,要么全部回滚;
  • 一致性(Consistency):事务执行前后的状态都是合法的数据状态,不会违反任何的数据完整性;
  • 隔离性(Isolation):主要是事务之间的相互的影响,根据隔离有不同的影响效果。
  • 持久性(Durability):事务一旦提交,就会体现在数据库上,不能回滚

这些基本的特性是如何保证的哪?对比mysql, 参考下面的表格:

事务属性MySQLMongoDB
Atomicityundo log机制来实现journal 机制来实现
Consistencyredo log机制来实现journal机制来实现
Isolation隔离级别+ 读写锁 + MVCC 来实现隔离级别 + 读写锁 + snapshot+ MVCC来实现
Durability依赖WAL 刷盘频率的设定依赖WAL 刷盘频率的设定
事务的隔离级别
log刷盘设

4.0事务的限制

  • 事务执行时间的限制
    默认情况下, 一个事务从开始到事务提交的时间间隔要小于60秒, 如果大于60秒, 该事务就认为失败, 进行回滚。这个时间限制可以通过transactionLifetimeLimitSeconds来进行修改。

    db.adminCommand( { setParameter: 1, transactionLifetimeLimitSeconds: 40 } )
    
  • oplog size的限制
    在MongoDB下面, 默认一个JSON文件的最大为16M, oplog本身也是一个JSON,也遵守这个规则。一个事务, 包括它里面所有的写操作都包括在一个oplog里面。

  • 事务获取锁的等待时间限制
    默认情况下, 使用事务需要获取锁的等待时间为5毫秒, 超过这个时间事务就失败。我们可以通过修改参数maxTransactionLockRequestTimeoutMillis开改变这个设定。

    db.adminCommand( { setParameter: 1, maxTransactionLockRequestTimeoutMillis: 20 } )
    
  • 事务写冲突的限制

时间点事务1事务2
t1x=1
t2begin_transaction
t3x=2
t4y=1
t5y=2
t6commit

如上图所示, 有事务1和事务2, you个时间t1 ~ t5.在时间点t1, 事务2将X设定为1, 在t3, 另外一个事务将其该为2, 由于事务2没有在一个局部的事务里面, 它的操作可以认为在事务之外, 这种情况下, 事务1会失败;
另外一种情况, 在事务1内部时间点t4,y的值被改为1, 在事务外部t5, y的值被改为2, 这种情况下, y=2的设定要等待事务1提交之后, 才会被处理;

  • 事务读取旧的数据
时间点事务1事务2事务3
t1x=1y=1
t2begin_transaction
t3begin_transactiony=2
t4x=2y=3
t5commitx=3
t6commit

如上图所示, 在t1是x=1,y=1,事务1 在时间点t4将x变成2, 事务2在时间点t3将y值设定为2,在外部的在t4将y设定为3, 但是在整个的事务2(t2 ~ t5), 我们读取的事务2的y的值始终是2.

事务的执行过程

整体上来看, MongoDB的事务的使用和其他的数据库非常类似, 如下是从官网复制的一个transaction API的例子, 我们顺着程序的执行顺序来分析下事务的实现。

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

employeesCollection = session.getDatabase("hr").employees;
eventsCollection = session.getDatabase("reporting").events;

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

// Operations inside the transaction
try {
   employeesCollection.updateOne( { employee: 3 }, { $set: { status: "Inactive" } } );
   eventsCollection.insertOne( { employee: 3, status: { new: "Inactive", old: "Active" } } );
} catch (error) {
   // Abort transaction on error
   session.abortTransaction();
   throw error;
}

// Commit the transaction using write concern set at transaction start
session.commitTransaction();
session.endSession();

startSession

startSession 是3.6版本新开放的API,它可以产生一个内部的session, 它联合client的write concern 和read concern, 可以实现因果一致性

startTransaction

对于某个session, 在任一时刻只能有一个运行的transaction。
通过__session_begin_transaction --> __wt_txn_begin调用__wt_txn_get_snapshot, 来产生一个新的事务。其实, 产生一个snapshot的过程很简单, 就是在__wt_txn_global->stats, 将某个session->id指定为当前的__wt_txn_state, 更新里面的字段, 当我们需要某个transaction的时候, 通过TXN的ID可以找到相关的snapshot的ID。

transaction中间的CRUD

在startTransaction之后, commitTranscation/abortTransaction之前的操作, 都是在同一个事务里面, 遵守事务的ACID特性。
相应的操作除了直接应用到内存的数据和索引内存页之外, 还记录到journal对应的txn->mod里面, 这样, 如果事务失败, 通过__wt_txn_unmodify, 回滚log里面记录的事务内的操作, 可以实现事务的回滚。
在内存里面, 内存页里面有page对应的insert_array和update_array;在一个事务里面, 每一个更新操作, 都有一个update操作对应__wt_txn_modify, 将该操作与实务id相关联。

commitTransaction

__wt_txn_commit
当事务需要提交的时候, __wt_txn_commit需要根据配置设定事务的日志提交方式, 然后通过__wt_txn_log_commit写入日志。 如果写入成功, 就释放掉该事务对应的修改操作txn->mod, 并且将事务清除; 如果失败, 就调用__wt_txn_rollback rollback处理。

abortTransaction

一旦需要rollback, __wt_txn_rollback会将txn->mod的所有需改, 状态变成WT_TXN_ABORTED, 然后清理事务对应的资源。

参考文档

https://source.wiredtiger.com/3.0.0/transactions.html
https://www.jianshu.com/p/9b83ea78b380
https://www.cnblogs.com/cjsblog/p/8365921.html
https://baijiahao.baidu.com/s?id=1625607423998953705&wfr=spider&for=pc

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MongoDB 从版本 4.0 开始支持多文档事务。在 MongoDB 中,事务可以跨越多个集合和数据库进行操作。以下是 MongoDB 实现事务的方式: 1. 开启事务:使用`startSession()`方法开启一个会话,然后调用`startTransaction()`方法来开启事务。 2. 执行事务:在事务中执行操作时,可以使用`session`参数指定会话。所有操作都必须在同一个会话中执行,以便在事务之外进行回滚。 3. 回滚事务:在任何时候,如果您需要回滚事务,则可以在会话中调用`abortTransaction()`方法。 4. 提交事务:在执行完所有操作后,您可以使用`commitTransaction()`方法来提交事务。 5. 事务嵌套:在 MongoDB 中,事务可以嵌套,这意味着您可以在一个事务中执行另一个事务。 需要注意的是,MongoDB事务只能用于支持多文档事务的副本集和分片集群。在单节点环境下,事务是不支持的。 以下是一个简单的 MongoDB 事务示例: ``` session = client.start_session() with session.start_transaction(): collection1.insert_one({'name': 'Alice'}) collection2.insert_one({'name': 'Bob'}) session.commit_transaction() ``` 在上面的示例中,我们使用`start_session()`方法创建一个会话,并使用`start_transaction()`方法开启一个事务。然后,我们在事务中向两个不同的集合插入数据,并在`commit_transaction()`方法中提交事务。如果发生任何错误,我们可以在会话中使用`abort_transaction()`方法回滚事务

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值