Project #4 - Concurrency Control (Leaderboard Task)
Predicate pushdown to SeqScan
将 SeqScan 算子上层的 Filter 算子结合进 SeqScan 里,这样仅需锁住符合 Predicate 的行。
更改 SeqScanExecutor 的 Next()
函数:
auto SeqScanExecutor::Next(Tuple *tuple, RID *rid) -> bool {
do {
if (table_iterator_ == table_info_->table_->End()) {
// SeqScan 结束,若是读提交,要提前释放所有 S 行锁和 IS 表锁
if (exec_ctx_->GetTransaction()->GetIsolationLevel() == IsolationLevel::READ_COMMITTED) {
try {
if (!exec_ctx_->GetTransaction()->GetSharedRowLockSet()->empty()) {
const auto locked_row_set = exec_ctx_->GetTransaction()->GetSharedRowLockSet()->at(table_info_->oid_);
for (auto lock_rid : locked_row_set) {
exec_ctx_->GetLockManager()->UnlockRow(exec_ctx_->GetTransaction(), table_info_->oid_, lock_rid);
}
}
exec_ctx_->GetLockManager()->UnlockTable(exec_ctx_->GetTransaction(), table_info_->oid_);
} catch (TransactionAbortException e) {
throw ExecutionException("SeqScan Executor Unlock Row/Table Lock Failed" + e.GetInfo());
}
}
return false;
}
*tuple = *table_iterator_++; // 先返回 iter 对应的值,再++
*rid = tuple->GetRid();
} while (plan_->filter_predicate_ != nullptr && !plan_->filter_predicate_->Evaluate(tuple, table_info_->schema_).GetAs<bool>());
// 不是读未提交,就要获取行的 S 锁
if (exec_ctx_->GetTransaction()->GetIsolationLevel() != IsolationLevel::READ_UNCOMMITTED) {
try {
if (!exec_ctx_->GetLockManager()->LockRow(exec_ctx_->GetTransaction(), LockManager::LockMode::SHARED, table_info_->oid_, *rid)) {
throw ExecutionException("SeqScan Executor Get S Row Lock Failed");
}
} catch (TransactionAbortException e) {
throw ExecutionException("SeqScan Executor Get S Row Lock Failed" + e.GetInfo());
}
}
return true;
}
在 Optimizer 中启用 OptimizeMergeFilterScan
规则:
auto Optimizer::OptimizeCustom(const AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef {
auto p = plan;
p = OptimizeMergeProjection(p); // 合并投影
p = OptimizeMergeFilterScan(p); // 谓词下推到 SeqScan !!
p = OptimizeMergeFilterNLJ(p); // 谓词下推到 nested loop join 中
p = OptimizeNLJAsIndexJoin(p); // 将 nested loop join 优化为 nested index join
p = OptimizeNLJAsHashJoin(p); // 将 nested index join 优化为 hash join
p = OptimizeOrderByAsIndexScan(p); // 优化 order by 如果有一个索引
p = OptimizeSortLimitAsTopN(p); // 将 sort + limit 优化为 topN
return p;
}
Implement UpdateExecutor
实现 Update 算子,这样可以在原地直接修改 tuple,不需要先 Delete 再 Insert。也比较简单,注意表加 IX 锁,行加 X 锁。返回更新的行的数量。锁在 Commit/Abort 时统一释放,无需手动释放。
src/include/execution/executors/update_executor.h 成员变量:
/** The update plan node to be executed */
const UpdatePlanNode *plan_;
/** Metadata identifying the table that should be updated */
const TableInfo *table_info_;
/** The child executor to obtain value from */
std::unique_ptr<AbstractExecutor> child_executor_;
bool end_ = false;
src/execution/update_executor.cpp
UpdateExecutor::UpdateExecutor(ExecutorContext *exec_ctx, const UpdatePlanNode *plan, std::unique_ptr<AbstractExecutor> &&child_executor)
: AbstractExecutor(exec_ctx), plan_(plan), child_executor_(std::move(child_executor)) {
// As of Fall 2022, you DON'T need to implement update executor to have perfect score in project 3 / project 4.
}
void UpdateExecutor::Init() {
child_executor_->Init();
table_info_ = exec_ctx_->GetCatalog()->GetTable(plan_->TableOid());
// 为表加 IX 锁(应该是升级)
try {
if (!exec_ctx_->GetLockManager()->LockTable(exec_ctx_->GetTransaction(), LockManager::LockMode::INTENTION_EXCLUSIVE, table_info_->oid_)) {
throw ExecutionException("Update Executor Get IX Table Lock Failed");
}
} catch (TransactionAbortException e) {
throw ExecutionException("Update Executor Get IX Table Lock Failed");
}
}
auto UpdateExecutor::Next(Tuple *tuple, RID *rid) -> bool {
if (end_) {
return false;
}
Tuple old_tuple;
Transaction *txn = exec_ctx_->GetTransaction();
int32_t update_count = 0;
while (child_executor_->Next(&old_tuple, rid)) {
// 为行加 X 锁(应该是升级)
try {
if (!exec_ctx_->GetLockManager()->LockRow(txn, LockManager::LockMode::EXCLUSIVE, table_info_->oid_, *rid)) {
throw ExecutionException("Update Executor Get X Row Lock Failed");
}
} catch (TransactionAbortException e) {
throw ExecutionException("Update Executor Get X Row Lock Failed" + e.GetInfo());
}
std::vector<Value> update_values{};
update_values.reserve(child_executor_->GetOutputSchema().GetColumnCount());
for (const auto &expr : plan_->target_expressions_) {
update_values.emplace_back(expr->Evaluate(&old_tuple, child_executor_->GetOutputSchema()));
}
Tuple update_tuple = Tuple{update_values, &child_executor_->GetOutputSchema()};
if (!table_info_->table_->UpdateTuple(update_tuple, *rid, txn)) {
throw std::logic_error("The tuple could not be found.");
}
update_count++;
}
*tuple = Tuple{std::vector<Value>{Value(TypeId::INTEGER, update_count)}, &GetOutputSchema()};
end_ = true;
return true;
}
最后,别忘了在 tools/terrier_bench/terrier_bench_config.h 中启用对应配置,要指示 Terriers 使用 UPDATE
来交换 NFTs。
#define TERRIER_BENCH_ENABLE_UPDATE
// #define TERRIER_BENCH_ENABLE_INDEX
Use Index
在执行类似 👇 这种 sql,并且在 id 上有索引时,可以利用索引来查询,避免遍历整张表。这是三项优化中效果最明显的一项,可以把 QPS 从几十提升到几万。
SELECT ... WHERE id = 1;
在实现这个优化时,不需要考虑太多的情况,只用按测试中查询的格式来匹配。测试中主要需要优化的是这条 sql:
SELECT * FROM nft WHERE id = <nft_id>
实际这条 sql 是在 id 已建立索引的基础上,将底层的 SeqScanExecutor 通过优化规则优化为 IndexScanExecutor,并且在 IndexScanExecutor 中添加单点查询逻辑。其中优化规则中要匹配形如 👇 的情况。
Filter
|
SeqScan
Filter 的 Predicate 形如 x=a
,其中 x 是 ColumnValue
,x 上有索引,a
是 ConstantValue
。注意这条优化规则要在 MergeFilterScan
之前执行,否则 Filter 会直接被 Merge 到 SeqScan 里。或者直接优化已经完成 Merge 的 SeqScanExecutor。(我这里选择把优化规则直接写在 MergeFilterScan
之前)
成功匹配后,IndexScanExecutor 中将提取出 a,使用 ScanKey(tuple{a})
查询 (这需要构造一下包含 a 的 tuple)。同样,单点查询时 IndexScan
算子中要注意非 READ_UNCOMMITTED
时要加锁,表加 IS 行加 S,若是 READ_COMMITTED
要在最后没有数据时提前释放之前持有的锁。
以上是大致实现思路,下面看下实现逻辑:
我们先添加上述优化规则。
auto Optimizer::OptimizeMergeFilterIndexScan(const AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef {
std::vector<AbstractPlanNodeRef> children;
for (const auto &child : plan->GetChildren()) {
children.emplace_back(OptimizeMergeFilterIndexScan(child));
}
auto optimized_plan = plan->CloneWithChildren(std::move(children));
if (optimized_plan->GetType() == PlanType::Filter) {
const auto &filter_plan = dynamic_cast<const FilterPlanNode &>(*optimized_plan);
BUSTUB_ASSERT(optimized_plan->children_.size() == 1, "must have exactly one children");
const auto &child_plan = *optimized_plan->children_[0];
if (child_plan.GetType() == PlanType::SeqScan) {
const auto &seq_scan_plan = dynamic_cast<const SeqScanPlanNode &>(child_plan);
const auto *table_info = catalog_.GetTable(seq_scan_plan.GetTableOid());
const auto indexes = catalog_.GetTableIndexes(table_info->name_);
// filter 是一个比较运算符
if (const auto *expr = dynamic_cast<const ComparisonExpression *>(filter_plan.GetPredicate().get());
expr != nullptr) {
// 比较运算符形式是 left_expr = 常数
if (expr->comp_type_ == ComparisonType::Equal) {
if (const auto *left_expr = dynamic_cast<const ColumnValueExpression *>(expr->children_[0].get());
left_expr != nullptr) {
if (const auto *right_expr = dynamic_cast<const ConstantValueExpression *>(expr->children_[1].get());
right_expr != nullptr) {
// 能找到一个在 left_expr 上的索引
for (const auto index : indexes) {
const auto &columns = index->key_schema_.GetColumns();
if (columns.size() == 1 &&
columns[0].GetName() == table_info->schema_.GetColumn(left_expr->GetColIdx()).GetName()) {
return std::make_shared<IndexScanPlanNode>(optimized_plan->output_schema_, index->index_oid_,
filter_plan.GetPredicate());
}
}
}
}
}
}
}
}
return optimized_plan;
}
然后在 OptimizeCustom()
中应用该规则:
auto Optimizer::OptimizeCustom(const AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef {
auto p = plan;
p = OptimizeMergeProjection(p); // 合并投影
p = OptimizeMergeFilterIndexScan(p); // 若存在索引,则将谓词下推到索引 (p4 Leaderboard Task)
p = OptimizeMergeFilterScan(p); // 谓词下推到 SeqScan
p = OptimizeMergeFilterNLJ(p); // 谓词下推到 nested loop join 中
p = OptimizeNLJAsIndexJoin(p); // 将 nested loop join 优化为 nested index join
p = OptimizeNLJAsHashJoin(p); // 将 nested index join 优化为 hash join
p = OptimizeOrderByAsIndexScan(p); // 优化 order by 如果有一个索引
p = OptimizeSortLimitAsTopN(p); // 将 sort + limit 优化为 topN
return p;
}
我们应用了该规则后,下一步就是更改 IndexScanExecutor 来执行。
首先要更改 IndexScanPlanNode。此处模仿 SeqScanPlanNode,添加成员变量 filter_predicate_
用于存储 IndexScanExecutor 要进行单点查询的谓词。
AbstractExpressionRef filter_predicate_;
再在 IndexScanExecutor.h 中添加成员变量,用于存储单点查询得到的所有符合的 tuples 的 rids。
std::vector<RID> rids_;
std::vector<RID>::iterator rid_iter_;
我们将在 IndexScanExecutor 中的 Init()
函数中判断若含有谓词,则使用索引的 ScanKey
直接在索引上查找符合条件的 key,将查到的所有 rids 存储在成员变量 rids_
中。后续在 Next()
函数中,直接通过 rids_
的迭代器 rid_iter_
遍历该 rids_
一个个输出。
// p4 Leaderboard Task: 要实现谓词下推到 IndexScanExecutor,若存在谓词则不需要 index_iterator 了
IndexScanExecutor::IndexScanExecutor(ExecutorContext *exec_ctx, const IndexScanPlanNode *plan)
: AbstractExecutor(exec_ctx),
plan_(plan),
index_info_(exec_ctx_->GetCatalog()->GetIndex(plan_->GetIndexOid())),
table_info_(exec_ctx_->GetCatalog()->GetTable(index_info_->table_name_)),
index_(dynamic_cast<BPlusTreeIndexForOneIntegerColumn *>(index_info_->index_.get())),
index_iterator_(plan_->filter_predicate_ != nullptr ? BPlusTreeIndexIteratorForOneIntegerColumn()
: index_->GetBeginIterator()) {}
void IndexScanExecutor::Init() {
// 不是读未提交,就需要获取表的 IS 锁
if (exec_ctx_->GetTransaction()->GetIsolationLevel() != IsolationLevel::READ_UNCOMMITTED) {
try {
if (!exec_ctx_->GetLockManager()->LockTable(exec_ctx_->GetTransaction(), LockManager::LockMode::INTENTION_SHARED, table_info_->oid_)) {
throw ExecutionException("IndexScan Executor Get IS Table Lock Failed");
}
} catch (TransactionAbortException e) {
throw ExecutionException("IndexScan Executor Get IS Table Lock Failed" + e.GetInfo());
}
}
if (plan_->filter_predicate_ != nullptr) {
// 直接获取谓词,然后在索引上查找相关 key
const auto *right_expr = dynamic_cast<const ConstantValueExpression *>(plan_->filter_predicate_->children_[1].get());
Value v = right_expr->val_;
index_->ScanKey(Tuple{{v}, index_info_->index_->GetKeySchema()}, &rids_, exec_ctx_->GetTransaction());
rid_iter_ = rids_.begin();
}
}
auto IndexScanExecutor::Next(Tuple *tuple, RID *rid) -> bool {
// 谓词下推
if (plan_->filter_predicate_ != nullptr) {
if (rid_iter_ == rids_.end()) {
// IndexScan 结束,若是读提交,要提前释放所有 S 行锁和 IS 表锁
if (exec_ctx_->GetTransaction()->GetIsolationLevel() == IsolationLevel::READ_COMMITTED) {
try {
if (!exec_ctx_->GetTransaction()->GetSharedRowLockSet()->empty()) {
const auto locked_row_set = exec_ctx_->GetTransaction()->GetSharedRowLockSet()->at(table_info_->oid_);
for (auto lock_rid : locked_row_set) {
exec_ctx_->GetLockManager()->UnlockRow(exec_ctx_->GetTransaction(), table_info_->oid_, lock_rid);
}
}
exec_ctx_->GetLockManager()->UnlockTable(exec_ctx_->GetTransaction(), table_info_->oid_);
} catch (TransactionAbortException e) {
throw ExecutionException("IndexScan Executor Unlock Row/Table Lock Failed" + e.GetInfo());
}
}
return false;
}
*rid = *rid_iter_;
rid_iter_++;
// 不是读未提交,就要获取行的 S 锁
if (exec_ctx_->GetTransaction()->GetIsolationLevel() != IsolationLevel::READ_UNCOMMITTED) {
try {
if (!exec_ctx_->GetLockManager()->LockRow(exec_ctx_->GetTransaction(), LockManager::LockMode::SHARED, table_info_->oid_, *rid)) {
throw ExecutionException("IndexScan Executor Get S Row Lock Failed");
}
} catch (TransactionAbortException e) {
throw ExecutionException("IndexScan Executor Get S Row Lock Failed" + e.GetInfo());
}
}
return table_info_->table_->GetTuple(*rid, tuple, exec_ctx_->GetTransaction());
}
// 不存在谓词,则是一个完成的索引扫描和表扫描
if (index_iterator_ == index_->GetEndIterator()) {
// IndexScan 结束,若是读提交,要提前释放所有 S 行锁和 IS 表锁
if (exec_ctx_->GetTransaction()->GetIsolationLevel() == IsolationLevel::READ_COMMITTED) {
try {
if (!exec_ctx_->GetTransaction()->GetSharedRowLockSet()->empty()) {
const auto locked_row_set = exec_ctx_->GetTransaction()->GetSharedRowLockSet()->at(table_info_->oid_);
for (auto lock_rid : locked_row_set) {
exec_ctx_->GetLockManager()->UnlockRow(exec_ctx_->GetTransaction(), table_info_->oid_, lock_rid);
}
}
exec_ctx_->GetLockManager()->UnlockTable(exec_ctx_->GetTransaction(), table_info_->oid_);
} catch (TransactionAbortException e) {
throw ExecutionException("IndexScan Executor Unlock Row/Table Lock Failed" + e.GetInfo());
}
}
return false;
}
*rid = (*index_iterator_).second;
index_iterator_.operator++();
// 不是读未提交,就要获取行的 S 锁
if (exec_ctx_->GetTransaction()->GetIsolationLevel() != IsolationLevel::READ_UNCOMMITTED) {
try {
if (!exec_ctx_->GetLockManager()->LockRow(exec_ctx_->GetTransaction(), LockManager::LockMode::SHARED, table_info_->oid_, *rid)) {
throw ExecutionException("IndexScan Executor Get S Row Lock Failed");
}
} catch (TransactionAbortException e) {
throw ExecutionException("IndexScan Executor Get S Row Lock Failed" + e.GetInfo());
}
}
return table_info_->table_->GetTuple(*rid, tuple, exec_ctx_->GetTransaction());
}
最后的最后,别忘了在 tools/terrier_bench/terrier_bench_config.h 中启用对应配置指示 Terriers 在交换 NFTs 之前创建索引。
#define TERRIER_BENCH_ENABLE_UPDATE
#define TERRIER_BENCH_ENABLE_INDEX
测试

优化 GrantLock()
感觉还是有点慢,尝试再优化一下 GrantLock()
:
原本的逻辑是 LockTable()
,LockRow()
中无论当前锁请求是不是锁升级,都将新的请求加入到请求队列末尾 (二者区别在于锁升级要删除请求队列中的旧请求)。然后在 GrantLock()
中先遍历请求队列,获取所有已授予锁的请求 granted_lock_set
和在当前请求前等待被授予锁的请求 wait_lock_set
。按 👇 逻辑判断。
之所以要先遍历一遍请求队列获取两个 set 是因为,锁升级的请求也是加在请求队列末尾,但是它后续的加锁优先级是最高的。这会导致 grant 的锁请求不一定都在队列首部。
if (!granted_lock_set.empty()) {
if (当前 lock_request 与 granted_lock_set 都兼容) {
// 若当前有锁升级请求,若锁升级请求正为当前请求,则优先级最高;否则其他事务的优先级高于当前请求
if (lock_request_queue->upgrading_ != INVALID_TXN_ID) {
return lock_request_queue->upgrading_ == lock_request->txn_id_;
}
// 当前没有锁升级请求,若前面没有等待授予的锁,则当前请求的优先级最高;否则前面等待授予的锁优先级高
return wait_lock_set.empty();
}
return false;
}
return 当前 lock_request 是否与 wait_lock_set 都兼容
更新的逻辑:
LockTable()
,LockRow()
中,将锁升级的请求放在请求队列中第一个等待的位置。普通锁请求放在请求队列末尾。
auto lock_request = std::make_shared<LockRequest>(txn->GetTransactionId(), lock_mode, oid, rid);
if (lock_request_queue->upgrading_ == txn->GetTransactionId()) {
std::list<std::shared_ptr<LockRequest>>::iterator lr_iter;
for (lr_iter = lock_request_queue->request_queue_.begin(); lr_iter != lock_request_queue->request_queue_.end(); lr_iter++) {
if (!(*lr_iter)->granted_) {
break;
}
}
lock_request_queue->request_queue_.insert(lr_iter, lock_request);
} else {
lock_request_queue->request_queue_.push_back(lock_request);
}
这样 GrantLock()
中就不用先遍历一遍请求队列获取 granted_lock_set
和 wait_lock_set
了。因为 grant 的锁请求肯定都分布在请求队列首部。我们直接遍历请求队列,若是 grant 的锁,判断是否兼容;若不是 grant 的锁,证明 grant 的锁都已经比对完了 or 请求队列中本身就没有 grant 的锁,这时,若当前请求是第一个 wait 的请求,则授予锁;若不是第一个 wait 的请求,则不授予锁。
若当前请求是锁升级请求,则肯定是第一个 wait 的请求。
“不是第一个 wait 的请求,则不授予锁” 这并不会影响若它与第一个 wait 锁请求兼容时获取锁。以第二个 wait 锁请求为例,因为在 LockTable()
, LockRow()
逻辑中,第一个 wait 请求获取锁后,若它不是 X 锁,会 notify_all
,此时它后面 wait 的锁会与 grant 锁比较,兼容则能获取锁。
auto LockManager::GrantLock(const std::shared_ptr<LockRequest> &lock_request, const std::shared_ptr<LockRequestQueue> &lock_request_queue) -> bool {
for (auto &lr : lock_request_queue->request_queue_) {
if (lr->granted_) {
switch (lock_request->lock_mode_) {
case LockMode::SHARED:
if (lr->lock_mode_ == LockMode::INTENTION_EXCLUSIVE ||
lr->lock_mode_ == LockMode::SHARED_INTENTION_EXCLUSIVE || lr->lock_mode_ == LockMode::EXCLUSIVE) {
return false;
}
break;
case LockMode::EXCLUSIVE:
return false;
break;
case LockMode::INTENTION_SHARED:
if (lr->lock_mode_ == LockMode::EXCLUSIVE) {
return false;
}
break;
case LockMode::INTENTION_EXCLUSIVE:
if (lr->lock_mode_ == LockMode::SHARED || lr->lock_mode_ == LockMode::SHARED_INTENTION_EXCLUSIVE ||
lr->lock_mode_ == LockMode::EXCLUSIVE) {
return false;
}
break;
case LockMode::SHARED_INTENTION_EXCLUSIVE:
if (lr->lock_mode_ != LockMode::INTENTION_SHARED) {
return false;
}
break;
}
} else if (lock_request.get() != lr.get()) {
return false;
} else {
return true;
}
}
return false;
}
最终测试
我们再提交下 GradeScope,从第 7 升到了第 5。

本文作者:Joey-Wang
本文链接:https://www.cnblogs.com/joey-wang/p/17350136.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步