在前面我们介绍了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_t | current | 全局唯一的递增transactionID |
uint64_t | last_running | 最旧的运行中的transaction id |
uint64_t | oldest_id | 系统中最小的transaction id |
wt_timestamp_t | commit_timestamp | 记录事务可以读取的时间点, 再次之前是读不到的 |
wt_timestamp_t | last_ckpt_timestamp | 上一次checkpoint 的时间 |
wt_timestamp_t | meta_ckpt_timestamp | metadata里面记录的checkpoint time |
wt_timestamp_t | oldest_timestamp | 记录最旧的事务时间点 |
wt_timestamp_t | pinned_timestamp | 当前最小的需要固定在内存的transactionID, 就是有read绑定到它上面 |
wt_timestamp_t | recovery_timestamp | 记录recovery的时候, 恢复到的checkpoint的timestamp |
wt_timestamp_t | stable_timestamp | 一个固定时间点, 由客户端设定, 主要用于rollback_to_stable |
bool | has_commit_timestamp | 是否有commit_timestamp |
bool | has_oldest_timestamp | 是否有oldest_timestamp |
bool | has_pinned_timestamp | 是否有pinned_timestamp设定 |
bool | has_stable_timestamp | 是否有set_timestamp设定stable |
bool | oldest_is_pinned | pinned_timestamp == oldest_timestamp? |
bool | stable_is_pinned | pinned_timestamp == stable_timestamp? |
WT_SPINLOCK | id_lock | 用于更新current, 获取一个transactionID的时候用 |
WT_RWLOCK | visibility_rwlock | 可见性保护锁, 防止日志, checkpoint,事务完成之前可见 |
WT_RWLOCK | commit_timestamp_rwlock | commit_timestamp读写锁, 修改commit timestamp队列的时候用 |
TAILQ_HEAD(__wt_txn_cts_qh, __wt_txn) | commit_timestamph | commit_timestamp队列 |
uint32_t | commit_timestampq_len | commit_timestamp的长度 |
WT_RWLOCK | read_timestamp_rwlock | read_timestamp读写锁 |
TAILQ_HEAD(__wt_txn_rts_qh, __wt_txn) | read_timestamph | read_timestamp队列 |
uint32_t | read_timestampq_len | read_timestamp队列的长度 |
bool | checkpoint_running | checkpoint是否正在运行中 |
uint32_t | checkpoint_id | checkpoint id |
WT_TXN_STATE | checkpoint_state | checkpoint的状态 |
uint64_t | metadata_pinned | 绑定的metadata中最小的transactionID |
WT_RWLOCK | nsnap_rwlock | 命名snapshot的读写锁 |
uint64_t | nsnap_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_t | id | transaction ID |
WT_TXN_ISOLATION | isolation | 事务的隔离级别: read uncommitted, read committed, snapshot |
uint32_t | forced_iso | 强制使用的隔离级别 |
uint64_t | snap_min, snap_max | 该事务对应的snapshot的最大最小id |
uint64_t * | snapshot | 该事务对应的snapshot数组 |
uint32_t | snapshot_count | 该事务的对应的snapshot数组的大小 |
uint32_t | txn_logsync | 事务日志的sync设定 |
wt_timestamp_t | commit_timestamp | 该transaction的commit timestamp |
wt_timestamp_t | first_commit_timestamp | commit_timstamp在一个transaction可以被设定多次, 这里记录第一个 |
wt_timestamp_t | prepare_timestamp | 该事务的prepare timestamp |
TAILQ_ENTRY(__wt_txn) | commit_timestampq | commit timestamp队列 |
TAILQ_ENTRY(__wt_txn) | read_timestampq | readtimestamp队列 |
bool | clear_commit_q | commit timestamp队列是否需要被清理 |
bool | clear_read_q | read timestamp队列是否需要被清理 |
WT_TXN_OP | *mod | transaction里面的修改操作数组 |
size_t | mod_alloc | transaction里面的修改操作数组大小 |
u_int | mod_count | transaction里面的修改操作数组已分配的个数 |
WT_ITEM* | logrec | transaction对应的log内存buffer, 记录了transaction的操作 |
WT_TXN_NOTIFY * | notify | transaction的notify函数 |
WT_LSN | ckpt_lsn | checkpoint的logical sequence number |
uint32_t | ckpt_nsnapshot | checkpoint对应的snapshot count |
WT_ITEM* | ckpt_snapshot | 记录checkpoint的snapshot数组的ID |
bool | full_ckpt | 是不是fullcheckpoint |
const char * | rollback_reason | transaction rollback的原因 |
uint32_t | flags | transaction的设定 |
__wt_txn_state
Per-session的transaction状态信息。
事务的ACID
- 原子性(Atomicity):事务内的所有操作要么全部完成,要么全部回滚;
- 一致性(Consistency):事务执行前后的状态都是合法的数据状态,不会违反任何的数据完整性;
- 隔离性(Isolation):主要是事务之间的相互的影响,根据隔离有不同的影响效果。
- 持久性(Durability):事务一旦提交,就会体现在数据库上,不能回滚
这些基本的特性是如何保证的哪?对比mysql, 参考下面的表格:
事务属性 | MySQL | MongoDB |
---|---|---|
Atomicity | undo log机制来实现 | journal 机制来实现 |
Consistency | redo 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 |
---|---|---|
t1 | x=1 | |
t2 | begin_transaction | |
t3 | x=2 | |
t4 | y=1 | |
t5 | y=2 | |
t6 | commit |
如上图所示, 有事务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 |
---|---|---|---|
t1 | x=1 | y=1 | |
t2 | begin_transaction | ||
t3 | begin_transaction | y=2 | |
t4 | x=2 | y=3 | |
t5 | commit | x=3 | |
t6 | commit |
如上图所示, 在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