Lời nói đầu
Một hệ thống xử lý giao dịch đủ tiêu chuẩn phải có bốn đặc tính: tính nguyên tử, tính nhất quán, tính cô lập và tính bền vững. Tính năng cô lập đảm bảo rằng các hoạt động được thực hiện bởi một giao dịch đang hoạt động (chưa được xác nhận hoặc khôi phục) trên cơ sở dữ liệu sẽ không hiển thị với các giao dịch đang hoạt động khác và trông giống như chỉ có một giao dịch đang vận hành cơ sở dữ liệu tại một thời điểm. Tuy nhiên, sự cô lập hoàn hảo sẽ dẫn đến giảm hiệu suất đồng thời của hệ thống cơ sở dữ liệu. Đôi khi chúng ta có thể chấp nhận sự không nhất quán của dữ liệu, vì vậy chúng ta có thể hy sinh một số sự cô lập để đổi lấy sự đồng thời tốt hơn. Blog này sẽ giới thiệu quy trình triển khai thí nghiệm thứ tư của CMU15-445 Mùa thu năm 2020, Kiểm soát đồng thời, sử dụng cơ chế khóa hai pha để triển khai ba mức cô lập là đọc chưa cam kết, đọc đã cam kết và đọc lặp lại, đồng thời sử dụng biểu đồ chờ để phát hiện tình trạng bế tắc thường xuyên.
Khóa hai pha
Khả năng tuần tự hóa là mức độ cô lập cao nhất. Nếu lịch trình giao dịch có thể tuần tự hóa, hiệu ứng cuối cùng tương đương với việc thực hiện nhiều giao dịch tuần tự. Nếu cả hai giao dịch chỉ thực hiện các hoạt động đọc trên cơ sở dữ liệu, sẽ không có xung đột và không có vấn đề về tính nhất quán bất kể chúng được lên lịch như thế nào. Nhưng khi giao dịch có hoạt động ghi, xung đột sẽ xảy ra.
Đối với lịch trình được hiển thị trong hình bên dưới, chúng ta có thể thấy có xung đột và có các hoạt động đọc và ghi trong cả T1 và T2. Vì R(B) và W(A), W(B) và R(A) không ảnh hưởng lẫn nhau, chúng ta có thể đạt được hiệu ứng tương tự như tuần tự hóa bằng cách điều chỉnh thứ tự của chúng, được gọi là tuần tự hóa xung đột.
Lịch trình điều chỉnh được hiển thị trong hình bên dưới. Kết quả cuối cùng là T2 ghi các bản ghi A và B.
Nhưng một số lịch trình không thể được sắp xếp theo thứ tự bất kể bạn hoán đổi thứ tự như thế nào.
Nếu chúng ta không thể tuần tự hóa các xung đột thông qua việc lập lịch, chúng ta chỉ có thể hoàn tác giao dịch, điều này thật lãng phí công sức. Một cách để xác định xem xung đột có thể tuần tự hóa được hay không là xây dựng biểu đồ thứ tự ưu tiên dựa trên lịch trình, trong đó mỗi giao dịch là một đỉnh trong biểu đồ và các cạnh trong biểu đồ biểu diễn một hoạt động xung đột. Hình sau có xung đột giữa WR và RW. Đồ thị ưu tiên được xây dựng có chu kỳ, do đó không thể tuần tự hóa.
Mặc dù phương pháp xây dựng biểu đồ ưu tiên hoạt động hiệu quả nhưng nó đòi hỏi chúng ta phải biết trước toàn bộ lịch trình trông như thế nào. Để giải quyết vấn đề này, giao thức khóa hai pha đã ra đời. Giao thức yêu cầu mỗi giao dịch phải thực hiện yêu cầu khóa theo hai giai đoạn:
- Giai đoạn tăng trưởng: giao dịch có thể có được khóa nhưng không thể giải phóng chúng
- Giảm giai đoạn: Giao dịch có thể giải phóng khóa nhưng không thể có được khóa mới
Ban đầu, giao dịch đang trong giai đoạn phát triển, thu thập khóa khi cần và chuyển sang giai đoạn thu hẹp khi khóa được giải phóng. Giả sử trong hình bên dưới, T1 thêm khóa đọc trước R(A). Sau đó, sau R(A), nó không thể lấy lại khóa ghi bằng cách giải phóng khóa đọc. Nó chỉ có thể chuyển đổi khóa đọc thành khóa ghi bằng cách nâng cấp khóa.
Mặc dù khóa hai pha có thể đạt được tuần tự hóa xung đột, nhưng không thể tránh được các lần đọc bẩn và khôi phục tuần tự. Trong trường hợp này, cần có một giao thức khóa hai pha mạnh để giải phóng tất cả các khóa do giao dịch nắm giữ cùng một lúc chỉ sau khi Cam kết.
Quản lý khóa
Để đảm bảo các hoạt động giao dịch được xen kẽ hợp lý, hệ thống quản lý cơ sở dữ liệu sử dụng trình quản lý khóa để kiểm soát thời điểm các giao dịch được phép truy cập vào các mục dữ liệu. Ý tưởng cơ bản của trình quản lý khóa là duy trì cấu trúc dữ liệu nội bộ về các khóa hiện đang được các giao dịch đang hoạt động nắm giữ. Các giao dịch phát hành yêu cầu khóa cho trình quản lý khóa trước khi được phép truy cập vào các mục dữ liệu. Trình quản lý khóa sẽ cấp khóa cho giao dịch đang gọi, chặn giao dịch hoặc hủy giao dịch.
Cấu trúc dữ liệu được sử dụng trong Bustub là bảng khóa unordered_map lock_table_, như thể hiện trong hình sau. Bảng khóa thực chất là một bảng băm, trong đó khóa là ID hàng của bộ và giá trị là danh sách được liên kết ghi lại tất cả các giao dịch đã phát hành yêu cầu khóa cho bộ này. Các giao dịch màu xanh đậm giữ khóa, còn các giao dịch màu xanh nhạt bị chặn.
Cơ chế mà trình quản lý khóa xử lý các yêu cầu khóa là:
- Khi có tin nhắn yêu cầu khóa đến, nếu danh sách liên kết của mục dữ liệu tương ứng tồn tại, một bản ghi sẽ được thêm vào cuối danh sách liên kết; nếu không, một danh sách liên kết mới chỉ chứa bản ghi yêu cầu sẽ được tạo. Yêu cầu khóa đầu tiên luôn được cấp cho một mục dữ liệu hiện không bị khóa. Tuy nhiên, khi một giao dịch áp dụng cho một khóa trên một mục dữ liệu đã bị khóa, trình quản lý khóa sẽ chỉ cấp khóa cho yêu cầu nếu yêu cầu tương thích với khóa hiện đang được giữ và tất cả các yêu cầu trước đó đã được cấp khóa. Nếu không, yêu cầu phải đợi.
- Khi trình quản lý khóa nhận được thông báo mở khóa cho một giao dịch, nó sẽ xóa bản ghi trong bảng vòng cổ dữ liệu tương ứng với giao dịch, sau đó kiểm tra các bản ghi tiếp theo. Nếu có bất kỳ bản ghi nào, như mô tả ở trên, nó sẽ kiểm tra xem yêu cầu có thể được ủy quyền hay không. Nếu có, trình quản lý khóa sẽ ủy quyền cho yêu cầu và xử lý các bản ghi tiếp theo. Nếu có nhiều bản ghi hơn, chúng sẽ được xử lý từng bản ghi một theo cách tương tự.
- Nếu giao dịch bị hủy, trình quản lý khóa sẽ xóa tất cả các yêu cầu được thực hiện bởi giao dịch đang chờ khóa. Khi hệ thống cơ sở dữ liệu thực hiện hành động thích hợp để hoàn tác giao dịch, tất cả các khóa do giao dịch bị hủy giữ sẽ được giải phóng.
Như bạn có thể thấy, cơ chế này giống như khóa đọc-ghi, đảm bảo rằng các yêu cầu khóa sẽ không bị thiếu hụt.
Định nghĩa của LockRequestQueue như sau. Chúng ta thêm các thành viên reader_count_ và writer_enter_ vào LockRequestQueue. Cùng với biến điều kiện cv_ và std::mutex latch_ trong trình quản lý khóa, chúng ta có thể triển khai khóa đọc-ghi:
enum class LockMode { CHIA SẺ, ĐỘC QUYỀN }; class LockRequest { public: LockRequest(txn_id_t txn_id, LockMode lock_mode) : txn_id_(txn_id), lock_mode_(lock_mode), granted_(false) {} txn_id_t txn_id_; LockMode lock_mode_; bool granted_; }; class LockRequestQueue { public: std::list request_queue_; std::condition_variable cv_; // để thông báo các giao dịch bị chặn trên rid này bool lifting_ = false; uint32_t reader_count_ = 0; bool writer_enter_ = false; };
Thêm khóa chia sẻ
Thí nghiệm này yêu cầu chúng ta phải thực hiện ba cấp độ cô lập:
- Đọc chưa cam kết: Giao dịch không cần phải giữ khóa và có thể truy cập trực tiếp vào bộ, vì vậy hãy gọi
KhóaChia sẻ()
Khi bạn cần ném một ngoại lệ
- Đã đọc cam kết: Giao dịch cần thêm khóa chia sẻ khi đọc tuple và mở khóa sau khi đọc. Theo cách này, trước khi giữ khóa, chỉ dữ liệu được cam kết bởi những người khác mới có thể được đọc (vì khóa ghi được giữ bởi các giao dịch khác được giải phóng đồng đều sau khi Cam kết) và không ai sẽ cập nhật dữ liệu tuple sau khi giữ khóa
- Đọc lặp lại: Giao dịch cần thêm khóa chia sẻ khi đọc bộ dữ liệu và mở khóa khi cam kết (giao thức khóa hai giai đoạn mạnh), để dữ liệu được đọc trước khi cam kết sẽ không bị các giao dịch khác sửa đổi
Đối với yêu cầu thêm khóa chia sẻ, nếu có yêu cầu khóa độc quyền trước đó, giao dịch hiện tại cần phải bị chặn cho đến khi giao dịch giữ khóa độc quyền giải phóng khóa.
bool LockManager::LockShared(Transaction *txn, const RID &rid) { std::unique_lock lock(latch_); // Không được phép khóa trong giai đoạn thu hẹp CheckShrinking(txn); // Không cần khóa nhiều lần if (txn->IsSharedLocked(rid)) { return true; } auto txn_id = txn->GetTransactionId(); // Đọc chưa cam kết, không yêu cầu khóa đọc if (txn->GetIsolationLevel() == IsolationLevel::READ_UNCOMMITTED) { txn->SetState(TransactionState::ABORTED); throw TransactionAbortException(txn_id, AbortReason::LOCKSHARED_ON_READ_UNCOMMITTED); } // Tạo yêu cầu khóa auto &queue = lock_table_[rid]; auto &request = queue.request_queue_.emplace_back(txn_id, LockMode::SHARED); // Bị chặn nếu không lấy được khóa queue.cv_.wait(lock, [&] { return !queue.writer_enter_ || txn->IsAborted(); }); // Bế tắc sẽ khiến giao dịch bị hủy CheckAborted(txn); // Cập nhật trạng thái của yêu cầu khóa queue.reader_count_++; request.granted_ = true; txn->GetSharedLockSet()->emplace(rid); return true; } void LockManager::CheckShrinking(Transaction *txn) { if (txn->GetState() == TransactionState::SHRINKING) { txn->SetState(TransactionState::ABORTED); throw TransactionAbortException(txn->GetTransactionId(), AbortReason::LOCK_ON_SHRINKING); } } void LockManager::CheckAborted(Transaction *txn) { if (txn->IsAborted()) { ném TransactionAbortException(txn->GetTransactionId(), AbortReason::DEADLOCK); } }
Thêm khóa độc quyền
Đối với yêu cầu thêm khóa độc quyền, bạn cần đợi yêu cầu ghi và yêu cầu đọc trước đó hoàn tất trước khi nhận được khóa:
bool LockManager::LockExclusive(Transaction *txn, const RID &rid) { std::unique_lock lock(latch_); CheckShrinking(txn); if (txn->IsExclusiveLocked(rid)) { return true; } // Tạo yêu cầu khóa auto &queue = lock_table_[rid]; auto &request = queue.request_queue_.emplace_back(txn->GetTransactionId(), LockMode::EXCLUSIVE); // Vào trạng thái bị chặn mà không nhận được khóa ghi queue.cv_.wait(lock, [&] { return (!queue.writer_enter_ && queue.reader_count_ == 0) || txn->IsAborted(); }); // Bế tắc sẽ khiến giao dịch bị hủy bỏ CheckAborted(txn); queue.writer_enter_ = true; request.granted_ = true; txn->GetExclusiveLockSet()->emplace(rid); trả về giá trị đúng; }
Khóa leo thang
Nếu có một giao dịch phía trước yêu cầu nâng cấp khóa, một ngoại lệ cần được đưa ra và giao dịch hiện tại bị hủy bỏ. Nếu không, hoạt động nâng cấp chỉ có thể hoàn tất khi yêu cầu hiện tại trở thành yêu cầu đầu tiên trong hàng đợi yêu cầu:
bool LockManager::LockUpgrade(Transaction *txn, const RID &rid) { std::unique_lock lock(latch_); txn->GetSharedLockSet()->erase(rid); auto &queue = lock_table_[rid]; queue.reader_count_--; auto request_it = GetRequest(txn->GetTransactionId(), rid); request_it->lock_mode_ = LockMode::EXCLUSIVE; request_it->granted_ = false; // Nếu có một giao dịch trong hàng đợi để nâng cấp khóa, hãy trả về trực tiếp if (queue.upgrading_) { txn->SetState(TransactionState::ABORTED); throw TransactionAbortException(txn->GetTransactionId(), AbortReason::UPGRADE_CONFLICT); } queue.upgrading_ = true; queue.cv_.wait(lock, [&] { return (!queue.writer_enter_ && queue.reader_count_ == 0) || txn->IsAborted(); }); // Bế tắc sẽ khiến giao dịch bị hủy CheckAborted(txn); queue.upgrading_ = false; queue.writer_enter_ = true; request_it->granted_ = true; txn->GetExclusiveLockSet()->emplace(rid); return true; }
Khóa nhả
Sau khi mở khóa, các giao dịch bị chặn khác cần được đánh thức kịp thời:
bool LockManager::Unlock(Transaction *txn, const RID &rid) { std::unique_lock lock(latch_); txn->GetSharedLockSet()->erase(rid); txn->GetExclusiveLockSet()->erase(rid); auto request_it = GetRequest(txn->GetTransactionId(), rid); auto lock_mode = request_it->lock_mode_; // Cập nhật trạng thái giao dịch, đọc đã cam kết không yêu cầu cơ chế khóa hai giai đoạn if (txn->GetState() == TransactionState::GROWING && !(lock_mode == LockMode::SHARED && txn->GetIsolationLevel() == IsolationLevel::READ_COMMITTED)) { txn->SetState(TransactionState::SHRINKING); } // Xóa yêu cầu giao dịch khỏi hàng đợi yêu cầu khóaauto &queue = lock_table_[rid]; queue.request_queue_.erase(request_it); if (lock_mode == LockMode::SHARED) { // Đánh thức luồng đang chờ khóa đọc if (--queue.reader_count_ == 0) { queue.cv_.notify_all(); } } else { // Đánh thức luồng đang chờ khóa ghi lockqueue.writer_enter_ = false; queue.cv_.notify_all(); } return true; }
Phát hiện bế tắc
Khóa hai pha không thể tránh được tình trạng bế tắc, do đó chúng ta cần một luồng nền để thực hiện phát hiện bế tắc thường xuyên. Phương pháp phát hiện bế tắc là xây dựng một đồ thị chờ có hướng dựa trên tình trạng chờ của các giao dịch. Nếu có một chu kỳ trong đồ thị chờ, giao dịch trẻ nhất trong chu kỳ sẽ bị chấm dứt cho đến khi không còn chu kỳ nào xuất hiện.
unordered_map> waits_for_ trong LockManager biểu diễn đồ thị chờ, trong đó khóa là giao dịch T (đỉnh) và giá trị là tập hợp các giao dịch khác mà giao dịch T đang chờ để được mở khóa. Hai thành phần này tạo thành các cạnh trong đồ thị chờ.
void LockManager::AddEdge(txn_id_t t1, txn_id_t t2) { txns_.insert(t1); txns_.insert(t2); tự động &neighbors = waits_for_[t1]; tự động it = std::find(neighbors.begin(), neighbors.end(), t2); nếu (nó == neighbors.end()) { neighbors.push_back(t2); } } void LockManager::RemoveEdge(txn_id_t t1, txn_id_t t2) { tự động &neighbors = waits_for_[t1]; tự động it = std::find(neighbors.begin(), neighbors.end(), t2); nếu (nó != neighbors.end()) { neighbors.erase(nó); } } std::vector<>> LockManager::GetEdgeList() { std::vector<>> cạnh; cho (tự động &[t1, hàng xóm] : chờ_) { cho (tự động t2 : hàng xóm) { cạnh.emplace_back(t1, t2); } } trả về các cạnh; }
Để phát hiện vòng lặp, bạn cần sử dụng thuật toán theo chiều sâu: bắt đầu từ đỉnh \(v_s\), duyệt qua các đỉnh liền kề của đỉnh đó. Nếu cuối cùng bạn có thể quay lại \(v_s\), điều đó có nghĩa là có một vòng lặp. Việc triển khai cụ thể là duy trì đỉnh đang được truy cập trong tập on_stack_txns_. Nếu \(v_s\) có thể được tìm thấy trong tập khi duyệt qua các đỉnh liền kề, điều đó có nghĩa là có một chu trình.
bool LockManager::HasCycle(txn_id_t *txn_id) { cho (tự động &t1 : txns_) { DFS(t1); nếu (có_chu_trình_) { *txn_id = *on_stack_txns_.rbegin(); on_stack_txns_.clear(); has_cycle_ = false; trả về true; } } on_stack_txns_.clear(); trả về false; } void LockManager::DFS(txn_id_t txn_id) { nếu (có_chu_trình_) { trả về; } on_stack_txns_.insert(txn_id); tự động &neighbors = waits_for_[txn_id]; std::sort(neighbors.begin(), neighbors.end()); cho (tự động t2 : neighbors) { nếu (!on_stack_txns_.count(t2)) { DFS(t2); } else { has_cycle_ = true; trả về; } } on_stack_txns_.erase(txn_id); }
Khi phát hiện vòng lặp định kỳ, trước tiên bạn cần xây dựng biểu đồ chờ dựa trên bảng khóa, sau đó xóa tất cả các vòng lặp và hủy các giao dịch liên quan, và cuối cùng xóa biểu đồ chờ:
void LockManager::RunCycleDetection() { while (enable_cycle_detection_) { std::this_thread::sleep_for(cycle_detection_interval); { std::unique_lock l(latch_); // Xây dựng biểu đồ chờ cho (auto &[rid, queue] : lock_table_) { std::vector grant; auto it = queue.request_queue_.begin(); while (it != queue.request_queue_.end() && it->granted_) { grant.push_back(it->txn_id_); it++; } while (it != queue.request_queue_.end()) { for (auto &t2 : grant) { AddEdge(it->txn_id_, t2); } wait_rids_[it->txn_id_] = rid; it++; } } // Xóa giao dịch có id nhỏ nhất trong vòng txn_id_t txn_id; while (HasCycle(&txn_id)) { AbortTransaction(txn_id); } // Xóa biểu đồ waits_for_.clear(); wait_rids_.clear(); txns_.clear(); } } } void LockManager::AbortTransaction(txn_id_t txn_id) { auto txn = TransactionManager::GetTransaction(txn_id); txn->SetState(TransactionState::ABORTED); waits_for_.erase(txn_id); // Giải phóng tất cả các khóa ghi được giữ bởi txn for (auto &rid : *txn->GetExclusiveLockSet()) { for (auto &req : lock_table_[rid].request_queue_) { if (!req.granted_) { RemoveEdge(req.txn_id_, txn_id); } } } // Giải phóng tất cả các khóa đọc được giữ bởi txnfor (auto &rid : *txn->GetSharedLockSet()) { for (auto &req : lock_table_[rid].request_queue_) { if (!req.granted_) { RemoveEdge(req.txn_id_, txn_id); } } } // Thông báo cho luồng nơi txn được đặt rằng giao dịch đã bị chấm dứtlock_table_[wait_rids_[txn_id]].cv_.notify_all(); }
Thực hiện đồng thời
Trong phần trước, chúng ta đã triển khai một trình thực thi luồng đơn. Bây giờ chúng ta cần sửa đổi nó để hỗ trợ các giao dịch đồng thời ở ba cấp độ cô lập.
Quét toàn bộ bảng
Nếu mức độ cô lập cao hơn Đọc chưa cam kết, khóa chia sẻ sẽ được thêm vào giao dịch và giao dịch Đọc đã cam kết cần phải giải phóng khóa trước khi trả về bộ dữ liệu.
void SeqScanExecutor::Unlock(Transaction *txn, const RID &rid) { if (txn->GetIsolationLevel() == IsolationLevel::READ_COMMITTED) { exec_ctx_->GetLockManager()->Unlock(txn, rid); } } bool SeqScanExecutor::Next(Tuple *tuple, RID *rid) { auto predicate = plan_->GetPredicate(); auto txn = exec_ctx_->GetTransaction(); while (it_ != table_metadata_->table_->End()) { *rid = it_->GetRid(); // Lockif (txn->GetIsolationLevel() != IsolationLevel::READ_UNCOMMITTED && !txn->IsExclusiveLocked(*rid)) { exec_ctx_->GetLockManager()->LockShared(txn, *rid); } *tuple = *it_++; if (!predicate || predicate->Evaluate(tuple, &table_metadata_->schema_).GetAs()) { // Chỉ giữ lại các giá trị của cột đầu ra std::vector; for (auto &col : GetOutputSchema()->GetColumns()) { values.push_back(col.GetExpr()->Evaluate(tuple, &table_metadata_->schema_)); } *tuple = {values, GetOutputSchema()}; // UnlockUnlock(txn, *rid); return true; } Unlock(txn, *rid); } return false; }
chèn
Khi chèn, bạn cần thêm khóa ghi vào tuple, nhưng bạn không nên thêm khóa này sau TableHeap::InsertTuple() mà nên thêm bên trong hàm để không có giao dịch nào khác có thể sửa đổi tuple sau khi chèn. Để thực hiện thao tác khôi phục, bạn cũng cần thêm IndexWriteRecord. Vì TableWriteRecord đã được thêm vào bên trong TableHeap::InsertTuple() nên chúng ta không cần phải thêm nó lần nữa.
void InsertExecutor::InsertTuple(Tuple *tuple, RID *rid) { // Cập nhật bảng dữ liệu, cần khóa trong TablePage::InsertTuple table_metadata_->table_->InsertTuple(*tuple, rid, exec_ctx_->GetTransaction()); // Cập nhật chỉ mục cho (auto &index_info : index_infos_) { index_info->index_->InsertEntry( tuple->KeyFromTuple(table_metadata_->schema_, index_info->key_schema_, index_info->index_->GetKeyAttrs()), *rid, exec_ctx_->GetTransaction()); IndexWriteRecord record(*rid, table_metadata_->oid_, WType::INSERT, *tuple, index_info->index_oid_, exec_ctx_->GetCatalog()); exec_ctx_->GetTransaction()->AppendTableWriteRecord(bản ghi); } }
làm mới
Trước khi cập nhật, bạn cần nâng cấp khóa đọc thành khóa ghi. Nếu trước đó bạn chưa nhận được khóa, bạn cần trực tiếp yêu cầu khóa ghi.
bool UpdateExecutor::Next([[maybe_unused]] Tuple *tuple, RID *rid) { if (!child_executor_->Next(tuple, rid)) { return false; } // Cập nhật bảng dữ liệu auto new_tuple = GenerateUpdatedTuple(*tuple); // Khóa auto txn = exec_ctx_->GetTransaction(); if (txn->IsSharedLocked(*rid)) { exec_ctx_->GetLockManager()->LockUpgrade(txn, *rid); } else { exec_ctx_->GetLockManager()->LockExclusive(txn, *rid); } table_info_->table_->UpdateTuple(new_tuple, *rid, exec_ctx_->GetTransaction()); // Cập nhật chỉ mục cho (auto &index_info : index_infos_) { // Xóa tuple cũ index_info->index_->DeleteEntry( tuple->KeyFromTuple(table_info_->schema_, index_info->key_schema_, index_info->index_->GetKeyAttrs()), *rid, exec_ctx_->GetTransaction()); // Chèn tuple mới index_info->index_->InsertEntry( new_tuple.KeyFromTuple(table_info_->schema_, index_info->key_schema_, index_info->index_->GetKeyAttrs()), *rid, exec_ctx_->GetTransaction()); IndexWriteRecord record(*rid, table_info_->oid_, WType::UPDATE, *tuple, index_info->index_oid_, exec_ctx_->GetCatalog()); exec_ctx_->GetTransaction()->AppendTableWriteRecord(record); } return true; }
phần phụ chú
Thông qua thử nghiệm này, chúng ta có thể hiểu sâu hơn về các mức cô lập và giao thức khóa hai pha. Mã có nhiều yêu cầu hơn đối với công nghệ đồng bộ hóa đa luồng, nhưng nếu bạn đã thực hiện thử nghiệm của năm ngoái, thì đó không phải là vấn đề lớn. Vậy thôi~.
Cuối cùng, bài viết này về Dự án Hệ thống cơ sở dữ liệu CMU15445 (Mùa thu 2020) #4-Giải thích chi tiết về Kiểm soát đồng thời có tại đây. Nếu bạn muốn biết thêm về Dự án Hệ thống cơ sở dữ liệu CMU15445 (Mùa thu 2020) #4-Giải thích chi tiết về Kiểm soát đồng thời, vui lòng tìm kiếm các bài viết của CFSDN hoặc tiếp tục duyệt các bài viết liên quan. Tôi hy vọng bạn sẽ ủng hộ blog của tôi trong tương lai! .
Tôi là một lập trình viên xuất sắc, rất giỏi!