transaction的支持在 MongoDB 里面是逐渐进行的, 从3.2版本开始支持单文档的原子操作, 这是后续的多文档原子操作, 以及4.0 的副本集的事务, 以及后续的计划推出的4.2的分片的事务操作的基础。本文着重描述一下单文档的原子操作。
原子性: 就是一个操作要么成功, 要么失败。具体来说, 就是一个插入、更新或者删除操作, 应该只有commit或者rollback两种结果中的一种。对于每一个操作, 都有一个上下文, 来记录相关的信息,OperationContext以及子类OperationContextImpl就是用来记录能够进行的操作, 以及相关的状态信息。
class OperationContext {
protected:
RecoveryUnitState _ruState = kNotInUnitOfWork;
private:
friend class WriteUnitOfWork;
Client* const _client;
const unsigned int _opId;
// Not owned.
Locker* const _locker;
// Follows the values of ErrorCodes::Error. The default value is 0 (OK), which means the
// operation is not killed. If killed, it will contain a specific code. This value changes only
// once from OK to some kill code.
AtomicWord<ErrorCodes::Error> _killCode{ErrorCodes::OK};
WriteConcernOptions _writeConcern;
};
class OperationContextImpl : public OperationContext {
private:
std::unique_ptr<RecoveryUnit> _recovery;
bool _writesAreReplicated;
};
从OperationContext看出, 它的主要实现类是RecoveryUnit, 该类是在storage下面定义的, 进行不同存储引擎下的change操作的commit与rollbackup行为。要保持某个操作的原子性, 最只要的实现就在RecoveryUnit的函数。对于每一个change操作, 要从RecoveryUnit:Chnage来继承和定义其commit和rollback行为。定义好一个Change类之后, 可以通过registerChange函数, 将该Change添加到这个RecoveryUnit里面的一个保存此次操作的所有的change的集合内。比如, 在WiredTiger下面, 我们定义了class WiredTigerRecoveryUnit。
class RecoveryUnit {
class Change {
public:
virtual ~Change() {}
virtual void rollback() = 0;
virtual void commit() = 0;
};
virtual void registerChange(Change* change) = 0;
virtual void beginUnitOfWork(OperationContext* opCtx) = 0;
virtual void commitUnitOfWork() = 0;
virtual void abortUnitOfWork() = 0;
}
class WiredTigerRecoveryUnit final : public RecoveryUnit {
private:
WiredTigerSessionCache* _sessionCache; // not owned
WiredTigerSession* _session; // owned, but from pool
bool _areWriteUnitOfWorksBanned = false;
bool _inUnitOfWork;
bool _active;
// 记录RecoveryUnit对应的snapshotId
uint64_t _mySnapshotId;
bool _everStartedWrite;
Timer _timer;
RecordId _oplogReadTill;
bool _readFromMajorityCommittedSnapshot = false;
SnapshotName _majorityCommittedSnapshot = SnapshotName::min();
// 包含operationContext里面的所有的Change
typedef OwnedPointerVector<Change> Changes;
Changes _changes;
};
如上所示, RecoveryUnit主要进行beginUnitOfWork, commitUnitOfWork以及abortUnitOfWork。其用法为:
beginUnitOfWork
//insert or update
if normal {
commitUnitOfWork
}else {
abortUnitOfWork
}
是不是很类似常见的关系型数据库的事务操作?
原子性读操作
在分布式下同下, 并发的读写是很常见的情形, MongoDB是如何避免脏读的?
在MongoDB里面, 通过snapshot, 来记录下一个个的数据库瞬间的状态。通过SnapshotThread类, 周期性的创建一系列的snapshot, 这些snapshot, 通过WiredTigerSnapshotManager来管理。WiredTigerSnapshotManager::_committedSnapshot来记录已经提交的snapshot 的ID, 在这个ID之前的snapshot是可读的, 并且是可以被清理的。