本文基于Mongodb3.6,对于Mongodb上层事务中会让人困惑的几点进行源码层面的分析
mongodb 的写操作(insert/update/delete)提供的“单行一致性”的具体含义,如何做到的?
为何db.coll.count()在宕机崩溃后经常就不准了。
mongodb 查询操作的事务隔离级别。
1.写操作的事务性
Mongodb的数据组织
在了解写操作的事务性之前,需要先了解mongo层的每一个table,是如何与wiredtiger层的table(btree)对应的。mongo层一个最简单的table包含一个 ObjectId(_id) 索引。_id类似于Mysql中主键的概念。
rs1:PRIMARY> db.abc.getIndexes()
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "test.abc"
}
]
但是mongo中并不会将_id索引与行内容存放在一起(即没有聚簇索引的概念)。取而代之的,mongodb将索引与数据分开存放,通过RecordId进行间接引用。 举例一张包含两个索引(_id 和 name)的表,在wt层将有三张表与其对应。通过name索引找到行记录的过程为:先通过name->Record的索引找到RecordId,再通过RecordId->RowData的索引找到记录内容。
此外,一个Mongodb实例还包含一张记录对每一行的写操作的表local.oplog.rs, 该表主要用于复制(primary-secondary replication)。每一次(对实例中任何一张表的任何一行的)更新操作,都会产生唯一的一条oplog,记录在local.oplog.rs表里。
2.理解单行事务
mongodb对某一行的写操作,会产生三个动作:
对wt层的数据段btree(上图中的Data Ident)执行写操作
对wt层索引段的每个索引btree执行写操作
对oplog表执行写操作
mongodb的单行事务,说的是:对数据,索引,oplog这三者的更新是原子的。不存在索引段中的某个RecordId,在数据段中找不到,也不存在一条记录的更改被应用,但是没有记录到oplog中, 反之亦然。
从下面的代码可以看到,一个插入操作,更新数据,索引,以及Oplog的过程。
collection_impl.cpp
Status CollectionImpl::insertDocuments(OperationContext* opCtx)
Status status = _insertDocuments(opCtx, begin, end, enforceQuota, opDebug); // 更新数据和索引
getGlobalServiceContext()->getOpObserver()->onInserts(opCtx, ns(), uuid(), begin, end, fromMigrate); // 更新Oplog
return Status::OK();
}
Status CollectionImpl::_insertDocuments(OperationContext* opCtx)
_recordStore->insertRecords(opCtx, &records, ×tamps, _enforceQuota(enforceQuota)); // 更新数据
std::vector bsonRecords;
int recordIndex = 0;
for (auto it = begin; it != end; it++) {
RecordId loc = records[recordIndex++].id;
BsonRecord bsonRecord = {loc, &(it->doc)};
bsonRecords.push_back(bsonRecord);
}
int64_t keysInserted;
status = _indexCatalog.indexRecords(opCtx, bsonRecords, &keysInserted); // 更新所有索引
return status;
}
3.单行事务的实现
OperationContext与RecoveryUnit
客户端的每个请求(insert/update/delete/find/getmore),会生成一个唯一的OperationContext记录执行的上下文,OperationContext从请求解析时创建,到请求执行完成时释放。一般情况下,其生命周期等同于一个操作执行的生命周期。OperationContext创建时,会初始化RecoveryUnit。
service_context_d.cpp:288
std::unique_ptr ServiceContextMongoD::_newOpCtx(Client* client, unsigned opId) {
auto opCtx = stdx::make_unique(client, opId);
opCtx->setRecoveryUnit(getGlobalStorageEngine()->newRecoveryUnit(),
OperationContext::kNotInUnitOfWork);
return opCtx;
}
RecoveryUnit封装了wiredTiger层的事务。RecoveryUnit::_txnOpen 对应于WT层的beginTransaction。 RecoveryUnit::_txnClose封装了WT层的commit_transaction和rollback_transaction。
beginTransaction
wiredtiger_recovery_unit.cpp
void WiredTigerRecoveryUnit::_txnOpen() {
invariantWTOK(session->begin_transaction(session, NULL));
_active = true;
}
commit/rollback
wiredtiger_recovery_unit.cpp
void WiredTigerRecoveryUnit::_txnClose(bool commit) {
if (commit) {
wtRet = s->commit_transaction(s, NULL);
} else {
wtRet = s->rollback_transaction(s, NULL);
invariant(!wtRet);
}
_active = false;
}
WriteUnitOfWork
WriteUnitOfWork 是事务框架提供给server层,方便执行事务的API。它是对OperationContext和RecoveryUnit的封装。
class WriteUnitOfWork {
WriteUnitOfWork(OperationContext* opCtx) {
_opCtx->recoveryUnit()->beginUnitOfWork(_opCtx);
}
~WriteUnitOfWork() {
_opCtx->recoveryUnit()->abortUnitOfWork();
}
}
server层执行一个写操作的事务:
mongo/db/exec/update.cpp
WriteUnitOfWork wunit(getOpCtx());
uassertStatusOK(_collection->insertDocument(getOpCtx(),
InsertStatement(request->getStmtId(), newObj),
_params.opDebug,
enforceQuota,
request->isFromMigration()));
wunit.commit();
4.总结
简而言之,对一行记录的更改,涉及到数据,索引,和Oplog三者,在wiredTiger层,这样的更改对应于对多张表的更改。Mongodb通过实现事务框架(RecoveryUnit,OperationContext, WriteUnitOfWork)将细节封装。但归根结底非常简单,依然是教科书般的:
begin_transaction
do writes
end_transaction(commit/rollback)
下图是对上面的代码分析整理的调用层次关系。