【发布时间】:2012-09-04 17:06:22
【问题描述】:
查看最后的结果
我想使用文档数据库(出于各种原因)——可能是 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。我通过我知道在我开始之前设法完成的事务来设置检索上下文。我通过保留“正在进行的交易”而不是“交易修订”的列表来避免单一序列(因此单一执行)。当然,我避免读取未提交的事务并提供冲突回滚。
那么 - 这有什么漏洞吗?我的表现会受到严重影响吗?
Edit1:拜托拜托-不要敲打“如果您需要多文档交易,请不要使用文档数据库”。我知道,出于其他原因,我还是需要一个文档数据库。
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 >= 顶级事务 ID 的文档 - 这是一个在之后开始的事务
我们开始检索 - 所以我们不想要它。
文档是最新的(最新版本)吗?
如果我们看到一个不在当前事务 ID 中的已过时文档(事务已启动
在我们开始之前)并且是
为什么排序不会受到伤害?
因为我们将排序添加为最后一个子句,所以我们总是会首先看到真正的排序工作。对于每一个真实的
对“桶”进行排序,我们可能会得到多个文档,这些文档代表不同版本的模型对象。
但是,模型对象之间的排序顺序仍然存在。
为什么计数器不让事务串行执行(一次一个)?
因为这不是 RDBMS - 我们没有真正的事务所以我们不等待事务
像我们对“选择更新”所做的那样提交。
另一个事务可以在我们完成后立即进行原子更改。
压实:
有时必须进行一次压缩——获取所有非常旧的文档并将它们删除到另一个数据存储中。
这不应影响任何正在运行的检索或事务。
优化:
- 将条件放入查询本身。
- 将事务 ID 添加到所有索引。
- 确保具有相同模型对象 ID 的文档不会被分片到不同的节点。
费用是多少?
假设我们想要多个文档版本用于历史和审计,额外的成本是
自动更新计数器,创建交易记录,“密封”每个模型对象的先前版本
(标记作废)并删除交易文件。这不应该太大。
请注意,如果上述假设不成立,则额外成本相当高,尤其是对于检索而言。
结果:
我已经实现了上述算法(修改后的算法稍作改动)。从功能上讲,它正在工作。但是,性能(至少在主从复制拓扑中具有 3 个节点的 MongoDB 上,没有 fsync 但在“提交”结束之前需要复制)是非常糟糕的。我一直在阅读我刚刚从不同线程中写入的内容。我在事务集合上获得了持续的集合锁,而我的索引跟不上持续的翻转。对于具有 10 个馈线线程的微小事务,性能上限为 20 TPS。
简而言之 - 不是一个好的通用解决方案。
【问题讨论】:
-
顺便说一句,将每个事务记录标记为“已完成”而不是删除它会创建一个事务日志。
-
你一直在做这个吗?我会对你可能有的任何后续行动感兴趣。
-
@Jean-PhilippePellet - 将在接下来的几周内开始处理这个问题(希望如此)。我会尽量记住更新结果。
-
@Jean-PhilippePellet 添加了结果(坏的)。我已经放弃了这种方法。
-
@RanBiron 你检查过这个算法吗:github.com/rystsov/mongodb-transaction-example
标签: mongodb transactions couchdb acid document-database