拖了好久終於開始作實驗4了。lab4有三個大任務1. Lock Manager、2. DEADLOCK DETECTION 、3. CONCURRENT QUERY EXECUTION。這裏20年的lab好像和以前的不太同樣記得以前有日誌和錯誤恢復lab的。不過就作這個最新的了。html
這個任務只須要修改兩個文件concurrency/lock_manager.cpp
和concurrency/lock_manager.h
。這裏cmu已經給咱們提供了和事物相關的一些函數。在include/concurrency/transaction.h
。這裏的==鎖管理==(下用LM表示)針對於tuple級別。咱們須要對lock/unlock
請求做出正確的行爲。若是出現錯誤應該拋出異常。node
1. 請仔細閱讀位於lock_manager.h內的LockRequestQueue類
這會幫助你肯定哪些事物在等待一個鎖🔒c++
2. 建議使用std::condition_variable
來通知那些等待鎖的事物git
3. 使用shared_lock_set_
和exclusive_lock_set_
來區別共享鎖和排他鎖。這樣當TransactionManager
想要提交和abort事物的時候。LM就能夠合理的釋放鎖github
4. 你應該通讀TransactionManager::Abort
來了解對於Abort
狀態的事物。LM是如何釋放鎖的web
==一些參考閱讀資料==算法
一、兩個經常使用的互斥對象:std::mutex(互斥對象),std::shared_mutex(讀寫互斥對象) 二、三個用於代替互斥對象的成員函數,管理互斥對象的鎖(都是構造加鎖,析構解鎖):std::lock_guard用於管理std::mutex,std::unique_lock與std::shared_lock管理std::shared_mutex。
X鎖(排他鎖,Exclusive Locks)
當加X鎖的時候,表示咱們要寫這個tuple。當一個事物擁有排他鎖時,其餘任何事務必須等到X鎖被釋放才能對該頁進行訪問;X鎖一直到事務結束才能被釋放。下面來看一個例子
T1: update table set column1='hello' where id<1000
T2: update table set column1='world' where id>1000
對於這個sql語句而言。加入事物T1先到達。這個過程T1會對id < 1000的記錄施加排他鎖,可是因爲T2的更新和T1並沒有關係,因此它不會阻塞T2的更新
一樣看下面的例子。
T1: update table set column1='hello' where id<1000
T2: update table set column1='world' where id>900
如同上例,若是T1先達,T2馬上也到,T1加的排他鎖會阻塞T2的update.
對於本實驗的實現。咱們須要記住。
若是有事物對當前rid加了共享鎖。則必須等共享鎖釋放以後才能再加X鎖 若是有事物對當前rid加了X鎖以後,則在該X鎖釋放以前,不會有任何的鎖被施加
S鎖(共享鎖,Shared Locks)
多個事務可封鎖一個共享頁;任何事務都不能修改該頁; 一般是該頁被讀取完畢,S鎖當即被釋放。
本實驗對於S鎖的實現要和U鎖結合起來。所以會在下面說明。
U鎖(更新鎖,Updated Locks)
爲了解決死鎖。引入了更新鎖,看下面的例子
----------------------------------------
T1:
begin tran
select * from table(updlock) (加更新鎖)
update table set column1='hello'
T2:
begin tran
select * from table(updlock)
update table set column1='world'
----------------------------------------
事物T1加更新鎖的意思就是。我如今雖然只想讀。可是我要預留一個寫的名額,所以當有事物施加U鎖以後,其餘事物便不能加U鎖。好比本例,T1執行select,加更新鎖。T2運行,準備加更新鎖,但發現已經有一個更新鎖在那兒了,只好等。
除此以外,更新鎖能夠和讀操做共存這也是咱們這個實驗實現時須要重點考慮的
----------------------------------------
T1: select * from table(updlock) (加更新鎖)
T2: select * from table(updlock) (等待,直到T1釋放更新鎖,由於同一時間不能在同一資源上有兩個更新鎖)
T3: select * from table (加共享鎖,但不用等updlock釋放,就能夠讀)
----------------------------------------
所以在咱們這個實驗實現的時候咱們須要注意
若是有事物對當前rid加了更新鎖。則不容許加X和S鎖 當被讀取的頁將要被更新時,則升級爲X鎖;U鎖要一直到事務結束時才能被釋放。
注:不會附上不少代碼。(聽Andy教授的話)
1. 對於S鎖
只須要考慮下面這些狀況
Lock_queue
中有X鎖則須要wait
簡單附上一些代碼
if (mode == LockMode::SHARED) {
auto shared_wait_for = [&]() { return !lock_queue.upgrading_ && !lock_queue.hasExclusiveLock(txn); };
while (!shared_wait_for()) {
lock_queue.cv_.wait(_lock);
}
txn->GetSharedLockSet()->emplace(rid);
}
lock_queue.hasExclusiveLock(txn)
函數
就是用來判斷是否有排他鎖
inline bool hasExclusiveLock(Transaction *txn) {
std::list<LockRequest>::iterator curr = request_queue_.begin();
for (; curr != request_queue_.end(); curr++) {
if (curr->lock_mode_ == LockMode::EXCLUSIVE) {
return true;
}
}
return false;
}
2. 對於X鎖
一樣附上一些簡單的代碼
if (mode == LockMode::EXCLUSIVE) {
auto exclusive_wait_for = [&]() { return !lock_queue.upgrading_ && lock_queue.request_queue_.size() == 1; };
while (!exclusive_wait_for()) {
lock_queue.cv_.wait(_lock);
}
txn->GetExclusiveLockSet()->emplace(rid);
}
和共享鎖的實現基本相似
附上帶註釋的核心邏輯代碼。基本能夠說明白這個地方
// 標記位更新。代表如今正處於等待update鎖階段
queue.upgrading_ = true;
// 假如說當前的request_queue中只有當前update lock這一個請求。則能夠加U鎖,不然應該wait
while (lock_table_[rid].request_queue_.size() != 1) {
queue.cv_.wait(unique_lock);
}
// 加X鎖。並把標記位重製
queue.request_queue_.back() = LockRequest(txn->GetTransactionId(), LockMode::EXCLUSIVE);
queue.upgrading_ = false;
這個任務要求你的LM可以進行死鎖檢測。死鎖檢測算法就是最多見的資源分配圖算法
下面用cmu課上ppt的例子來看一下這個算法。
核心就是若是事物Ti在等待事物Tj釋放鎖。則畫一條從i-->j的邊。若是檢測完全部的衝突事務。若是出現環則表示出現了死鎖。若是沒有環則表示沒有死鎖。
這樣造成了一個環就發生了死鎖,這個時候就須要abort
就直接附上代碼不說廢話
void LockManager::AddEdge(txn_id_t t1, txn_id_t t2) {
for (const auto &txn_id : waits_for_[t1]) {
if (txn_id == t2) {
return;
}
}
waits_for_[t1].push_back(t2);
}
void LockManager::RemoveEdge(txn_id_t t1, txn_id_t t2) {
LOG_DEBUG("we can remove edge");
auto &vec = waits_for_[t1];
for (auto iter = vec.begin(); iter != vec.end(); ++iter) {
if (*iter == t2) {
vec.erase(iter);
return;
}
}
}
這個函數的實現。就是對於咱們上面算法的實現。因爲依賴圖是一個有向圖。所以咱們須要知道如何判斷一個有向圖是否有環。
用簡單的dfs就能夠實現這一功能。固然除了一個visited
數組來判斷這個元素是否被遍歷過以外。咱們還須要另一個數組recStack
用來 keep track of vertices in the recursion stack.
具體的過程就和下圖同樣
下面附上上面那個算法的代碼實習。可是關於本任務須要的代碼沒有給出
// This function is a variation of DFSUtil() in https://www.geeksforgeeks.org/archives/18212
bool Graph::isCyclicUtil(int v, bool visited[], bool *recStack)
{
if (visited[v] == false)
{
// Mark the current node as visited and part of recursion stack
visited[v] = true;
recStack[v] = true;
// Recur for all the vertices adjacent to this vertex
list<int>::iterator i;
for(i = adj[v].begin(); i != adj[v].end(); ++i)
{
if( !visited[*i] && isCyclicUtil(*i, visited, recStack) )
return true;
else if (recStack[*i])
return true;
}
}
recStack[v] = false; // remove the vertex from recursion stack
return false;
}
由於構建圖以前咱們要獲取全部已經加鎖和等待鎖的事物id。
這裏用兩個函數來實現這兩個步驟
這裏附上找到全部等待鎖事物的函數。另外一個再也不給出。
std::unordered_set<txn_id_t> getWaitingSet() {
std::list<LockRequest>::iterator wait_start;
std::unordered_set<txn_id_t> blocking;
// 遍歷找到wait_start的位置
std::list<LockRequest>::iterator curr = request_queue_.begin();
for (; curr != request_queue_.end() && (curr->lock_mode_ == LockMode::SHARED || curr ->lock_mode_ == LockMode::EXCLUSIVE); curr++) {
}
wait_start = curr;
for (; wait_start != request_queue_.end(); wait_start++) {
if (GetTransaction(wait_start->txn_id_)->GetState() != TransactionState::ABORTED) {
blocking.insert(wait_start->txn_id_);
}
}
return blocking;
}
};
Read Uncommitted(讀取未提交內容)
在該隔離級別,全部事務均可以看到其餘未提交事務的執行結果。本隔離級別不多用於實際應用,由於它的性能也不比其餘級別好多少。讀取未提交的數據,也被稱之爲髒讀(Dirty Read)。
就比如還沒肯定的消息,你卻先知道了發佈出去,最後又變動了,這樣就發生了錯誤
Read Committed(讀取提交內容)
這是大多數數據庫系統的默認隔離級別(但不是MySQL默認的)。它知足了隔離的簡單定義:一個事務只能看見已經提交事務所作的改變。這種隔離級別 也支持所謂的不可重複讀(Nonrepeatable Read
),由於同一事務的其餘實例在該實例處理其間可能會有新的commit,因此同一select可能返回不一樣結果。
Repeatable Read(可重讀)
這是MySQL的默認事務隔離級別,它確保同一事務的多個實例在併發讀取數據時,會看到一樣的數據行。不過理論上,這會致使另外一個棘手的問題:幻讀 (Phantom Read)。簡單的說,幻讀指當用戶讀取某一範圍的數據行時,另外一個事務又在該範圍內插入了新行,當用戶再讀取該範圍的數據行時,會發現有新的「幻影」 行。
因爲對於整個表的遍歷。也就是讀操做,在併發的狀況下可能發生錯誤。因此必須加以控制
這裏附上一些加鎖的代碼進行解釋
首先是對於隔離級別的區分
READ_UNCOMMITTED
則不須要加鎖
READ_COMMITTED
或者
REPEATABLE_READ
則須要判斷。若是當前rid沒有被加鎖。則加上共享鎖
Transaction *txn = exec_ctx_->GetTransaction();
LockManager *lock_mgr = exec_ctx_->GetLockManager();
if (lock_mgr != nullptr) {
switch (txn->GetIsolationLevel()) {
case IsolationLevel::READ_UNCOMMITTED:
break;
case IsolationLevel::READ_COMMITTED:
case IsolationLevel::REPEATABLE_READ:
RID r = iter->GetRid();
if (txn->GetSharedLockSet()->empty() && txn->GetExclusiveLockSet()->empty()) {
lock_mgr->LockShared(txn, r);
txn->GetSharedLockSet()->insert(r);
}
break;
}
}
這個更新比較簡單。
主要有下面兩個原則
update
的時候須要把它更新成update Lock
if (lock_mgr != nullptr) {
if (txn->IsSharedLocked(*rid)) {
lock_mgr->LockUpgrade(txn, *rid);
txn->GetSharedLockSet()->erase(*rid);
txn->GetExclusiveLockSet()->insert(*rid);
} else if (txn->GetExclusiveLockSet()->empty()) {
lock_mgr->LockExclusive(txn, *rid);
}
}
對於delete的併發控制和update的徹底同樣。只須要加入上面的原則便可
總算磕磕絆絆把四個lab都寫完了。感謝在知乎和github
以及qq羣裏面獲得的各類幫助。後面準備配合這門課的要求把對應章節的書的內容讀一下。同時把以前沒有寫完的上課筆記寫完。順帶有時間把前面lab的博客改一下。由於實現方面有了變化。後面就準備開搞下一個lab了。不知道你們是推薦824分佈式仍是推薦os那。