曾经沧海难为水,除却巫山不是云。|

Joey-Wang

园龄:4年3个月粉丝:17关注:0

2023-04-24 17:00阅读: 95评论: 0推荐: 0

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 上有索引,aConstantValue。注意这条优化规则要在 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

测试

image-20230424163412372

优化 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_setwait_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。

image-20230424165807075

本文作者:Joey-Wang

本文链接:https://www.cnblogs.com/joey-wang/p/17350136.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Joey-Wang  阅读(95)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开