在前面我們介紹了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