2

看到最后的结果


我想使用文档数据库(出于各种原因)——可能是 CouchDB 或 MongoDB。但是,我的多文档交易也需要 ACID。

但是,我确实计划使用“仅添加”模型 - 更改作为新文档添加(添加是添加,更新是添加副本+转换数据,删除是添加具有相同 ID + 删除标志的空文档)。我会定期对数据库运行压缩以删除非当前文档。

考虑到这一点,以下想法是否存在漏洞:

维护正在进行的当前事务的集合。此集合将保存带有正在进行的事务的事务 ID(GUID + 时间戳)的文档。

Atomicity:
    On a transaction:
        Add a document to the transactions in progress collection.
        Add the new documents (add is add, update is copy+add, delete is add with ID and “deleted” flag).
            Each added document will have the following management fields:
            Transaction ID.
            Previous document ID (linked list).
        Remove the document added to the transactions in progress collection.
    On transaction fail:
        Remove all added documents
        Remove the document from the transactions in progress collection.
    Periodically:
        Go over all transaction in progress, get ones that have been abandoned (>10 minutes?), remove the associated documents in the DB (index on transaction ID) and then remove the transaction in progress.
Read transaction consistency (read only committed transactions):
    On data retrieval:
        Load transactions in progress set.
        Load needed documents.
        For all documents, if the document transaction ID is in “transactions in progress” or later (using timestamp), load the previous document in the linked list (recursive).

它有点像 MVCC,有点像 Git。我通过我知道在我开始之前设法完成的事务来设置检索上下文。我通过保留“正在进行的交易”而不是“交易修订”的列表来避免单一序列(因此单一执行)。而且,当然,我避免阅读未提交的事务并提供冲突回滚。

那么 - 这有什么漏洞吗?我的表现会受到严重影响吗?

编辑1:拜托拜托-不要敲打“如果您需要多文档交易,请不要使用文档数据库”。我知道,出于其他原因,我还是需要一个文档数据库。

Edit2:添加时间戳以避免来自检索事务开始后开始的事务的数据。可能会将时间戳更改为序列 ID。

Edit3:这是我想到的另一种算法 - 它可能比上面的更好:

新算法 - 更容易理解(这次可能更正:))

Support structures:
transaction_support_tempalte {
    _created-by-transaction: <txid>
    _made-obsolete-by-transaction: <txid>
}

transaction_record { //
    transaction_id: <txid>
    timestamp: <tx timestamp>
    updated_documents: {
        [doc1_id, doc2_id...]
    }   
}

transaction_numer { //atomic counter - used for ordering transactions.
    _id: "transaction_number"
    next_transaction_id: 0 //initial.
}

Note: all IDs are model object IDs, not DB ids (don't confuse with logical IDs which are different).
DB ID - different for each document - but multiple DB documents are revisions of one model object.
Model object ID - same for all revisions of the model object.
Logical ID - client-facing ID.


First time setup:
1. Create the transaction_number document:

Commit process:
1. Get new transaction ID by atomic increment on the transaction number counter.
2. Insert a new transaction record with the transaction id, the timestamp and the updated documents.
3. Create the new version for each document. Make sure the _created-by-transaction is set.
4. Update the old version of each updated or deleted document as 
   "_made-obsolete-by-transaction" with the transaction id.
   This is the time to detect conflicts! if seen a conflict, rollback.
   Note - this can be done as find-and-modify rather then by serializing the entire document again.
5. Remove the transaction record.

Cleanup process:
1. Go over transaction record, sorted by id, ascending (oldest transaction first).
2. For each transaction, if it expired (by timestamp), do rollback(txid).

Rollback(txid) process:
1. Get the transaction record for the given transaction id.
2. For each document id in the "updated documents":
    2.1 If the document exists and has "_made-obsolete-by-transaction" with 
        the correct transaction id, remove the _made-obsolete-by-transaction data.
3. For each document with the _created-by-transaction-id:
    3.1 remove the document.
4. Remove the transaction record document.

Retrieval process:
1. Top-transaction-id = transaction ID counter.
2. Read all transactions from the transactions collection. 
   Current-transaction-ids[] = Get all transaction IDs.
3. Retrieve documents as needed. Always use "sort by transaction_id, desc" as last sort clause.
    3.1 If a document "_created-by-transaction-id" is in the Current-transaction-ids[] 
        or is >= Top-transaction-id - ignore it (not yet committed).
    3.2 If a document "_made-obsolete-by-transaction" is not in the Current-transaction-ids[] 
        and is < Top-transaction-id - ignore it (a newer version was committed).
4. We may have to retrieve more chunks to satisfy original requests if documents were ignored.

我们开始时文件是否已提交?
如果我们在当前执行的事务中看到具有事务 ID 的文档——它是在我们开始检索之前开始但当时尚未提交的事务——所以我们不想要它。如果我们看到一个事务 ID >= top transaction ID 的文档——它是在我们开始检索之后开始的事务——所以我们不想要它。

文档是最新的(最新版本)吗?
如果我们看到一个不在当前事务 ID 中的文档(事务在我们开始之前开始)并且是 < 顶级事务 ID(在我们开始之后开始的事务) - 那么有一个事务在我们过去完成提交使这个文件过时了——所以我们不想要它。

为什么排序不会受到伤害?
因为我们将排序添加为最后一个子句,所以我们总是会首先看到真正的排序工作。对于每个真正的排序“桶”,我们可能会得到多个文档,这些文档代表不同版本的模型对象。但是,模型对象之间的排序顺序仍然存在。

为什么计数器不让事务串行执行(一次一次)?
因为这不是 RDBMS——我们并没有真正的事务,所以我们不会像“选择更新”那样等待事务提交。另一个事务可以在我们完成后立即进行原子更改。

压缩:
有时必须进行一次压缩——获取所有真正旧的文档并将它们删除到另一个数据存储中。这不应该影响任何正在运行的检索或事务。

优化:

  1. 将条件放入查询本身。
  2. 将事务 ID 添加到所有索引。
  3. 确保具有相同模型对象 ID 的文档不会被分片到不同的节点。

费用是多少?
假设我们想要多个文档版本用于历史记录和审计,额外的成本是自动更新计数器、创建交易记录、“密封”每个模型对象的先前版本(标记为过时)并删除交易文档。这不应该太大。请注意,如果上述假设不成立,则额外成本相当高,尤其是对于检索而言。


结果:

我已经实现了上述算法(修改后的算法略有改动)。从功能上讲,它正在工作。但是,性能(至少在主从复制拓扑中具有 3 个节点的 MongoDB 上,没有 fsync 但在“提交”结束之前需要复制)是非常糟糕的。我一直在阅读我刚刚从不同线程中写入的内容。我在事务集合上获得了持续的集合锁,而我的索引跟不上持续的翻转。对于具有 10 个馈线线程的微小事务,性能上限为 20 TPS。

简而言之 - 不是一个好的通用解决方案。

4

1 回答 1

2

在没有详细说明您的计划的情况下,我认为首先了解 mongoDB 对 ACID 要求的支持可能很有用。

原子性:Mongo 支持对单个文档进行原子更改。通常,最重要的原子操作是 "$set" 和 findAndModify 一些关于这些操作和 mongoDB 中原子性的文档:

http://www.mongodb.org/display/DOCS/Atomic+Operations
[http://www.mongodb.org/display/DOCS/Updating#Updating-%24set][1]
http://www.mongodb.org/display/DOCS/findAndModify+Command

一致性:难以实现且相当复杂。我不会尝试在这篇文章中进行总结,但是关于这个主题有很多文章:

http://blog.mongodb.org/post/475279604/on-distributed-consistency-part-1
[http://blog.mongodb.org/post/498145601/on-distributed-consistency-part-2-some-eventual][2]

隔离:mongoDB 中的隔离确实存在于文档中,但不存在于任何更高级别。同样,这是一个复杂的主题。除了上面的原子操作链接,我发现的最好的资源是以下堆栈溢出线程:

为什么 MongoDB 不使用 fsync()?(一般来说,这个主题的最佳答案有点像金矿,尽管一些关于耐久性的信息已经过时了)

持久性:用户确保数据持久性的主要方法是使用 getLastError 命令(有关更多信息,请参阅下面的链接)来确认副本集中的大多数节点在调用返回之前已写入数据。

http://www.mongodb.org/display/DOCS/getLastError+Command#getLastErrorCommand-majority 
http://docs.mongodb.org/manual/core/replication-internals/ (linked to in the above document)

了解了 mongo 中有关 ACID 的所有这些信息,查看一些已经在 mongo 中解决的类似问题的示例将非常有用。我希望以下两个链接对您非常有用,因为它们非常完整且主题正确。

Two-Phase Commits: http://cookbook.mongodb.org/patterns/perform-two-phase-commits/

Transactions for e-commerce work: http://www.slideshare.net/spf13/mongodb-ecommerce-and-transactions-10524960

最后,我要问:为什么要进行交易?很少有 mongoDB 的用户发现他们真正需要 ACID 来实现他们的目标。在您继续在 mongo 之上实现整个层以获取事务之前,可能值得退后一步并尝试从另一个角度解决问题。

于 2012-09-13T20:49:51.197 回答